Initial sync up with master after Prettier
This commit is contained in:
@@ -3,33 +3,22 @@
|
|||||||
"es6": true,
|
"es6": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": ["eslint:recommended", "prettier"],
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"indent": [
|
||||||
"error",
|
"error",
|
||||||
4,
|
4,
|
||||||
{
|
{
|
||||||
"SwitchCase" : 1
|
"SwitchCase": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linebreak-style": [
|
"linebreak-style": ["error", "unix"],
|
||||||
"error",
|
"quotes": ["error", "single"],
|
||||||
"unix"
|
"semi": ["error", "always"],
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"comma-dangle": 0,
|
"comma-dangle": 0,
|
||||||
"no-trailing-spaces" :"warn"
|
"no-trailing-spaces": "error"
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2020
|
"ecmaVersion": 2020
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -22,7 +22,7 @@ A clear and concise description of what you expected to happen.
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Environment**
|
**Environment**
|
||||||
* [ ] I am using Node.js v12.x LTS or higher
|
* [ ] I am using Node.js v14.x LTS or higher
|
||||||
* [ ] `npm install` or `yarn` reports success
|
* [ ] `npm install` or `yarn` reports success
|
||||||
* Actual Node.js version (`node --version`):
|
* Actual Node.js version (`node --version`):
|
||||||
* Operating system (`uname -a` on *nix systems):
|
* Operating system (`uname -a` on *nix systems):
|
||||||
|
|||||||
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
art
|
||||||
|
config
|
||||||
|
db
|
||||||
|
docs
|
||||||
|
drop
|
||||||
|
gopher
|
||||||
|
logs
|
||||||
|
misc
|
||||||
|
www
|
||||||
|
mkdocs.yml
|
||||||
|
*.md
|
||||||
|
.github
|
||||||
19
.prettierrc.json
Normal file
19
.prettierrc.json
Normal file
@@ -0,0 +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
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
## Style
|
## Style & Formatting
|
||||||
Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins.
|
* In general, [Prettier](https://prettier.io) is used. See the [Prettier installation and basic instructions](https://prettier.io/docs/en/install.html) for more information.
|
||||||
There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise.
|
* Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins.
|
||||||
Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces.
|
* There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise.
|
||||||
|
* Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces.
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
|
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
|
||||||
|
|
||||||
## 0.0.13-beta
|
## 0.0.13-beta
|
||||||
* Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with ENiGMA½. For more information, see [Issue #222](https://github.com/NuSkooler/enigma-bbs/issues/222). This may also resolve other issues, such as [Issue #365](https://github.com/NuSkooler/enigma-bbs/issues/365), and [Issue #320](https://github.com/NuSkooler/enigma-bbs/issues/320). Anyone that previously had terminal incompatibilities please re-check and let us know!
|
* **Note for contributors**: ENiGMA has switched to [Prettier](https://prettier.io) for formatting/style. Please see [CONTRIBUTING](CONTRIBUTING.md) and the Prettier website for more information.
|
||||||
* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to V14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience.
|
* Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with Enigma 1/2. For more information, see [Issue #222](https://github.com/NuSkooler/enigma-bbs/issues/222). This may also resolve other issues, such as [Issue #365](https://github.com/NuSkooler/enigma-bbs/issues/365), and [Issue #320](https://github.com/NuSkooler/enigma-bbs/issues/320). Anyone that previously had terminal incompatibilities please re-check and let us know!
|
||||||
* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in `UPGRADE.md`
|
* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to v14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience.
|
||||||
|
* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in [UPGRADE](UPGRADE.md).
|
||||||
* New Waiting For Caller (WFC) support via the `wfc.js` module.
|
* New Waiting For Caller (WFC) support via the `wfc.js` module.
|
||||||
* Many new system statistics available via the StatLog such as current and average load, memory, etc.
|
* Many new system statistics available via the StatLog such as current and average load, memory, etc.
|
||||||
* Many new MCI codes: `MB`, `MF`, `LA`, `CL`, `UU`, `FT`, `DD`, `FB`, `DB`, `LC`, `LT`, `LD`, and more. See [MCI](./docs/art/mci.md).
|
* Many new MCI codes: `MB`, `MF`, `LA`, `CL`, `UU`, `FT`, `DD`, `FB`, `DB`, `LC`, `LT`, `LD`, and more. See [MCI](./docs/art/mci.md).
|
||||||
* SyncTERM style font support detection.
|
* SyncTERM style font support detection.
|
||||||
* Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the [project repository](https://github.com/NuSkooler/enigma-bbs) for more information.
|
* Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the [project repository](https://github.com/NuSkooler/enigma-bbs) for more information.
|
||||||
|
|
||||||
|
|
||||||
## 0.0.12-beta
|
## 0.0.12-beta
|
||||||
* The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](./docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276).
|
* The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](./docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276).
|
||||||
* Development now occurs against [Node.js 14 LTS](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md).
|
* Development now occurs against [Node.js 14 LTS](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md).
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const DropFile = require('./dropfile.js');
|
const DropFile = require('./dropfile.js');
|
||||||
const Door = require('./door.js');
|
const Door = require('./door.js');
|
||||||
const theme = require('./theme.js');
|
const theme = require('./theme.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const {
|
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
||||||
trackDoorRunBegin,
|
const Log = require('./logger').log;
|
||||||
trackDoorRunEnd
|
|
||||||
} = require('./door_util.js');
|
|
||||||
const Log = require('./logger').log;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
|
|
||||||
const activeDoorNodeInstances = {};
|
const activeDoorNodeInstances = {};
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Abracadabra',
|
name: 'Abracadabra',
|
||||||
desc : 'External BBS Door Module',
|
desc: 'External BBS Door Module',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -71,15 +68,15 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
this.config = options.menuConfig.config;
|
this.config = options.menuConfig.config;
|
||||||
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
||||||
// .. and/or EnigAssert
|
// .. and/or EnigAssert
|
||||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
assert(_.isString(this.config.name, "Config 'name' is required"));
|
||||||
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
assert(_.isString(this.config.cmd, "Config 'cmd' is required"));
|
||||||
|
|
||||||
this.config.nodeMax = this.config.nodeMax || 0;
|
this.config.nodeMax = this.config.nodeMax || 0;
|
||||||
this.config.args = this.config.args || [];
|
this.config.args = this.config.args || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementActiveDoorNodeInstances() {
|
incrementActiveDoorNodeInstances() {
|
||||||
if(activeDoorNodeInstances[this.config.name]) {
|
if (activeDoorNodeInstances[this.config.name]) {
|
||||||
activeDoorNodeInstances[this.config.name] += 1;
|
activeDoorNodeInstances[this.config.name] += 1;
|
||||||
} else {
|
} else {
|
||||||
activeDoorNodeInstances[this.config.name] = 1;
|
activeDoorNodeInstances[this.config.name] = 1;
|
||||||
@@ -88,7 +85,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
decrementActiveDoorNodeInstances() {
|
decrementActiveDoorNodeInstances() {
|
||||||
if(true === this.activeDoorInstancesIncremented) {
|
if (true === this.activeDoorInstancesIncremented) {
|
||||||
activeDoorNodeInstances[this.config.name] -= 1;
|
activeDoorNodeInstances[this.config.name] -= 1;
|
||||||
this.activeDoorInstancesIncremented = false;
|
this.activeDoorInstancesIncremented = false;
|
||||||
}
|
}
|
||||||
@@ -100,14 +97,16 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function validateNodeCount(callback) {
|
function validateNodeCount(callback) {
|
||||||
if(self.config.nodeMax > 0 &&
|
if (
|
||||||
|
self.config.nodeMax > 0 &&
|
||||||
_.isNumber(activeDoorNodeInstances[self.config.name]) &&
|
_.isNumber(activeDoorNodeInstances[self.config.name]) &&
|
||||||
activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax)
|
activeDoorNodeInstances[self.config.name] + 1 >
|
||||||
{
|
self.config.nodeMax
|
||||||
|
) {
|
||||||
self.client.log.info(
|
self.client.log.info(
|
||||||
{
|
{
|
||||||
name : self.config.name,
|
name: self.config.name,
|
||||||
activeCount : activeDoorNodeInstances[self.config.name]
|
activeCount: activeDoorNodeInstances[self.config.name],
|
||||||
},
|
},
|
||||||
`Too many active instances of door "${self.config.name}"`);
|
`Too many active instances of door "${self.config.name}"`);
|
||||||
|
|
||||||
@@ -118,11 +117,15 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.client.term.write('\nToo many active instances. Try again later.\n');
|
self.client.term.write(
|
||||||
|
'\nToo many active instances. Try again later.\n'
|
||||||
|
);
|
||||||
|
|
||||||
// :TODO: Use MenuModule.pausePrompt()
|
// :TODO: Use MenuModule.pausePrompt()
|
||||||
self.pausePrompt( () => {
|
self.pausePrompt(() => {
|
||||||
return callback(Errors.AccessDenied('Too many active instances'));
|
return callback(
|
||||||
|
Errors.AccessDenied('Too many active instances')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -135,21 +138,26 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
|
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
|
||||||
},
|
},
|
||||||
function generateDropfile(callback) {
|
function generateDropfile(callback) {
|
||||||
if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') {
|
if (
|
||||||
|
!self.config.dropFileType ||
|
||||||
|
self.config.dropFileType.toLowerCase() === 'none'
|
||||||
|
) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dropFile = new DropFile(
|
self.dropFile = new DropFile(self.client, {
|
||||||
self.client,
|
fileType: self.config.dropFileType,
|
||||||
{ fileType : self.config.dropFileType }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return self.dropFile.createFile(callback);
|
return self.dropFile.createFile(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.toString() }, 'Could not start door');
|
self.client.log.warn(
|
||||||
|
{ error: err.toString() },
|
||||||
|
'Could not start door'
|
||||||
|
);
|
||||||
self.lastError = err;
|
self.lastError = err;
|
||||||
self.prevMenu();
|
self.prevMenu();
|
||||||
} else {
|
} else {
|
||||||
@@ -174,8 +182,8 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.dropFile) {
|
if (this.dropFile) {
|
||||||
exeInfo.dropFile = this.dropFile.fileName;
|
exeInfo.dropFile = this.dropFile.fileName;
|
||||||
exeInfo.dropFilePath = this.dropFile.fullPath;
|
exeInfo.dropFilePath = this.dropFile.fullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
||||||
@@ -188,14 +196,17 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
if (exeInfo.dropFilePath) {
|
if (exeInfo.dropFilePath) {
|
||||||
fs.unlink(exeInfo.dropFilePath, err => {
|
fs.unlink(exeInfo.dropFilePath, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.');
|
Log.warn(
|
||||||
|
{ error: err, path: exeInfo.dropFilePath },
|
||||||
|
'Failed to remove drop file.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// client may have disconnected while process was active -
|
// client may have disconnected while process was active -
|
||||||
// we're done here if so.
|
// we're done here if so.
|
||||||
if(!this.client.term.output) {
|
if (!this.client.term.output) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +216,10 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||||||
//
|
//
|
||||||
this.client.term.rawWrite(
|
this.client.term.rawWrite(
|
||||||
ansi.normal() +
|
ansi.normal() +
|
||||||
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
|
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
|
||||||
ansi.setScrollRegion() +
|
ansi.setScrollRegion() +
|
||||||
ansi.goto(this.client.term.termHeight, 0) +
|
ansi.goto(this.client.term.termHeight, 0) +
|
||||||
'\r\n\r\n'
|
'\r\n\r\n'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.autoNextMenu();
|
this.autoNextMenu();
|
||||||
|
|||||||
@@ -2,38 +2,28 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const ConfigLoader = require('./config_loader');
|
const ConfigLoader = require('./config_loader');
|
||||||
const { getConfigPath } = require('./config_util');
|
const { getConfigPath } = require('./config_util');
|
||||||
const UserDb = require('./database.js').dbs.user;
|
const UserDb = require('./database.js').dbs.user;
|
||||||
const {
|
const { getISOTimestampString } = require('./database.js');
|
||||||
getISOTimestampString
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
} = require('./database.js');
|
const { getConnectionByUserId } = require('./client_connections.js');
|
||||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
const UserProps = require('./user_property.js');
|
||||||
const {
|
const { Errors, ErrorReasons } = require('./enig_error.js');
|
||||||
getConnectionByUserId
|
const { getThemeArt } = require('./theme.js');
|
||||||
} = require('./client_connections.js');
|
const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js');
|
||||||
const UserProps = require('./user_property.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const {
|
const StatLog = require('./stat_log.js');
|
||||||
Errors,
|
const Log = require('./logger.js').log;
|
||||||
ErrorReasons
|
|
||||||
} = require('./enig_error.js');
|
|
||||||
const { getThemeArt } = require('./theme.js');
|
|
||||||
const {
|
|
||||||
pipeToAnsi,
|
|
||||||
stripMciColorCodes
|
|
||||||
} = require('./color_codes.js');
|
|
||||||
const stringFormat = require('./string_format.js');
|
|
||||||
const StatLog = require('./stat_log.js');
|
|
||||||
const Log = require('./logger.js').log;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser;
|
exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser;
|
||||||
|
|
||||||
class Achievement {
|
class Achievement {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
@@ -44,59 +34,65 @@ class Achievement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static factory(data) {
|
static factory(data) {
|
||||||
if(!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let achievement;
|
let achievement;
|
||||||
switch(data.type) {
|
switch (data.type) {
|
||||||
case Achievement.Types.UserStatSet :
|
case Achievement.Types.UserStatSet:
|
||||||
case Achievement.Types.UserStatInc :
|
case Achievement.Types.UserStatInc:
|
||||||
case Achievement.Types.UserStatIncNewVal :
|
case Achievement.Types.UserStatIncNewVal:
|
||||||
achievement = new UserStatAchievement(data);
|
achievement = new UserStatAchievement(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default : return;
|
default:
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(achievement.isValid()) {
|
if (achievement.isValid()) {
|
||||||
return achievement;
|
return achievement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get Types() {
|
static get Types() {
|
||||||
return {
|
return {
|
||||||
UserStatSet : 'userStatSet',
|
UserStatSet: 'userStatSet',
|
||||||
UserStatInc : 'userStatInc',
|
UserStatInc: 'userStatInc',
|
||||||
UserStatIncNewVal : 'userStatIncNewVal',
|
UserStatIncNewVal: 'userStatIncNewVal',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid() {
|
isValid() {
|
||||||
switch(this.data.type) {
|
switch (this.data.type) {
|
||||||
case Achievement.Types.UserStatSet :
|
case Achievement.Types.UserStatSet:
|
||||||
case Achievement.Types.UserStatInc :
|
case Achievement.Types.UserStatInc:
|
||||||
case Achievement.Types.UserStatIncNewVal :
|
case Achievement.Types.UserStatIncNewVal:
|
||||||
if(!_.isString(this.data.statName)) {
|
if (!_.isString(this.data.statName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(!_.isObject(this.data.match)) {
|
if (!_.isObject(this.data.match)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default : return false;
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchDetails(/*matchAgainst*/) {
|
getMatchDetails(/*matchAgainst*/) {}
|
||||||
}
|
|
||||||
|
|
||||||
isValidMatchDetails(details) {
|
isValidMatchDetails(details) {
|
||||||
if(!details || !_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) {
|
if (
|
||||||
|
!details ||
|
||||||
|
!_.isString(details.title) ||
|
||||||
|
!_.isString(details.text) ||
|
||||||
|
!_.isNumber(details.points)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (_.isString(details.globalText) || !details.globalText);
|
return _.isString(details.globalText) || !details.globalText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +101,13 @@ class UserStatAchievement extends Achievement {
|
|||||||
super(data);
|
super(data);
|
||||||
|
|
||||||
// sort match keys for quick match lookup
|
// sort match keys for quick match lookup
|
||||||
this.matchKeys = Object.keys(this.data.match || {}).map(k => parseInt(k)).sort( (a, b) => b - a);
|
this.matchKeys = Object.keys(this.data.match || {})
|
||||||
|
.map(k => parseInt(k))
|
||||||
|
.sort((a, b) => b - a);
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid() {
|
isValid() {
|
||||||
if(!super.isValid()) {
|
if (!super.isValid()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !Object.keys(this.data.match).some(k => !parseInt(k));
|
return !Object.keys(this.data.match).some(k => !parseInt(k));
|
||||||
@@ -118,11 +116,11 @@ class UserStatAchievement extends Achievement {
|
|||||||
getMatchDetails(matchValue) {
|
getMatchDetails(matchValue) {
|
||||||
let ret = [];
|
let ret = [];
|
||||||
let matchField = this.matchKeys.find(v => matchValue >= v);
|
let matchField = this.matchKeys.find(v => matchValue >= v);
|
||||||
if(matchField) {
|
if (matchField) {
|
||||||
const match = this.data.match[matchField];
|
const match = this.data.match[matchField];
|
||||||
matchField = parseInt(matchField);
|
matchField = parseInt(matchField);
|
||||||
if(this.isValidMatchDetails(match) && !isNaN(matchField)) {
|
if (this.isValidMatchDetails(match) && !isNaN(matchField)) {
|
||||||
ret = [ match, matchField, matchValue ];
|
ret = [match, matchField, matchValue];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
@@ -151,7 +149,7 @@ class Achievements {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configLoaded = () => {
|
const configLoaded = () => {
|
||||||
if(true !== this.config.get().enabled) {
|
if (true !== this.config.get().enabled) {
|
||||||
Log.info('Achievements are not enabled');
|
Log.info('Achievements are not enabled');
|
||||||
this.enabled = false;
|
this.enabled = false;
|
||||||
this.stopMonitoringUserStatEvents();
|
this.stopMonitoringUserStatEvents();
|
||||||
@@ -163,11 +161,11 @@ class Achievements {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.config = new ConfigLoader({
|
this.config = new ConfigLoader({
|
||||||
onReload : err => {
|
onReload: err => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
configLoaded();
|
configLoaded();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.config.init(configPath, err => {
|
this.config.init(configPath, err => {
|
||||||
@@ -182,10 +180,10 @@ class Achievements {
|
|||||||
|
|
||||||
_getConfigPath() {
|
_getConfigPath() {
|
||||||
const path = _.get(Config(), 'general.achievementFile');
|
const path = _.get(Config(), 'general.achievementFile');
|
||||||
if(!path) {
|
if (!path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return getConfigPath(path); // qualify
|
return getConfigPath(path); // qualify
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAchievementHitCount(user, achievementTag, field, cb) {
|
loadAchievementHitCount(user, achievementTag, field, cb) {
|
||||||
@@ -193,7 +191,7 @@ class Achievements {
|
|||||||
`SELECT COUNT() AS count
|
`SELECT COUNT() AS count
|
||||||
FROM user_achievement
|
FROM user_achievement
|
||||||
WHERE user_id = ? AND achievement_tag = ? AND match = ?;`,
|
WHERE user_id = ? AND achievement_tag = ? AND match = ?;`,
|
||||||
[ user.userId, achievementTag, field],
|
[user.userId, achievementTag, field],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
return cb(err, row ? row.count : 0);
|
return cb(err, row ? row.count : 0);
|
||||||
}
|
}
|
||||||
@@ -202,14 +200,23 @@ class Achievements {
|
|||||||
|
|
||||||
record(info, localInterruptItem, cb) {
|
record(info, localInterruptItem, cb) {
|
||||||
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1);
|
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1);
|
||||||
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points);
|
StatLog.incrementUserStat(
|
||||||
|
info.client.user,
|
||||||
|
UserProps.AchievementTotalPoints,
|
||||||
|
info.details.points
|
||||||
|
);
|
||||||
|
|
||||||
const cleanTitle = stripMciColorCodes(localInterruptItem.title);
|
const cleanTitle = stripMciColorCodes(localInterruptItem.title);
|
||||||
const cleanText = stripMciColorCodes(localInterruptItem.achievText);
|
const cleanText = stripMciColorCodes(localInterruptItem.achievText);
|
||||||
|
|
||||||
const recordData = [
|
const recordData = [
|
||||||
info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField,
|
info.client.user.userId,
|
||||||
cleanTitle, cleanText, info.details.points,
|
info.achievementTag,
|
||||||
|
getISOTimestampString(info.timestamp),
|
||||||
|
info.matchField,
|
||||||
|
cleanTitle,
|
||||||
|
cleanText,
|
||||||
|
info.details.points,
|
||||||
];
|
];
|
||||||
|
|
||||||
UserDb.run(
|
UserDb.run(
|
||||||
@@ -217,20 +224,17 @@ class Achievements {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
||||||
recordData,
|
recordData,
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.events.emit(
|
this.events.emit(Events.getSystemEvents().UserAchievementEarned, {
|
||||||
Events.getSystemEvents().UserAchievementEarned,
|
user: info.client.user,
|
||||||
{
|
achievementTag: info.achievementTag,
|
||||||
user : info.client.user,
|
points: info.details.points,
|
||||||
achievementTag : info.achievementTag,
|
title: cleanTitle,
|
||||||
points : info.details.points,
|
text: cleanText,
|
||||||
title : cleanTitle,
|
});
|
||||||
text : cleanText,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
@@ -238,12 +242,12 @@ class Achievements {
|
|||||||
}
|
}
|
||||||
|
|
||||||
display(info, interruptItems, cb) {
|
display(info, interruptItems, cb) {
|
||||||
if(interruptItems.local) {
|
if (interruptItems.local) {
|
||||||
UserInterruptQueue.queue(interruptItems.local, { clients : info.client } );
|
UserInterruptQueue.queue(interruptItems.local, { clients: info.client });
|
||||||
}
|
}
|
||||||
|
|
||||||
if(interruptItems.global) {
|
if (interruptItems.global) {
|
||||||
UserInterruptQueue.queue(interruptItems.global, { omit : info.client } );
|
UserInterruptQueue.queue(interruptItems.global, { omit: info.client });
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
@@ -252,7 +256,7 @@ class Achievements {
|
|||||||
recordAndDisplayAchievement(info, cb) {
|
recordAndDisplayAchievement(info, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
return this.createAchievementInterruptItems(info, callback);
|
return this.createAchievementInterruptItems(info, callback);
|
||||||
},
|
},
|
||||||
(interruptItems, callback) => {
|
(interruptItems, callback) => {
|
||||||
@@ -262,7 +266,7 @@ class Achievements {
|
|||||||
},
|
},
|
||||||
(interruptItems, callback) => {
|
(interruptItems, callback) => {
|
||||||
return this.display(info, interruptItems, callback);
|
return this.display(info, interruptItems, callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -271,164 +275,228 @@ class Achievements {
|
|||||||
}
|
}
|
||||||
|
|
||||||
monitorUserStatEvents() {
|
monitorUserStatEvents() {
|
||||||
if(this.userStatEventListeners) {
|
if (this.userStatEventListeners) {
|
||||||
return; // already listening
|
return; // already listening
|
||||||
}
|
}
|
||||||
|
|
||||||
const listenEvents = [
|
const listenEvents = [
|
||||||
Events.getSystemEvents().UserStatSet,
|
Events.getSystemEvents().UserStatSet,
|
||||||
Events.getSystemEvents().UserStatIncrement
|
Events.getSystemEvents().UserStatIncrement,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => {
|
this.userStatEventListeners = this.events.addMultipleEventListener(
|
||||||
if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) {
|
listenEvents,
|
||||||
return;
|
userStatEvent => {
|
||||||
}
|
if (
|
||||||
|
|
||||||
if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// :TODO: Make this code generic - find + return factory created object
|
|
||||||
const achievementTags = Object.keys(_.pickBy(
|
|
||||||
_.get(this.config.get(), 'achievements', {}),
|
|
||||||
achievement => {
|
|
||||||
if(false === achievement.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const acceptedTypes = [
|
|
||||||
Achievement.Types.UserStatSet,
|
|
||||||
Achievement.Types.UserStatInc,
|
|
||||||
Achievement.Types.UserStatIncNewVal,
|
|
||||||
];
|
|
||||||
return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName;
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
if(0 === achievementTags.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async.eachSeries(achievementTags, (achievementTag, nextAchievementTag) => {
|
|
||||||
const achievement = Achievement.factory(this.getAchievementByTag(achievementTag));
|
|
||||||
if(!achievement) {
|
|
||||||
return nextAchievementTag(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statValue = parseInt(
|
|
||||||
[ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ?
|
|
||||||
userStatEvent.statValue :
|
|
||||||
userStatEvent.statIncrementBy
|
|
||||||
);
|
|
||||||
if(isNaN(statValue)) {
|
|
||||||
return nextAchievementTag(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue);
|
|
||||||
if(!details) {
|
|
||||||
return nextAchievementTag(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async.waterfall(
|
|
||||||
[
|
[
|
||||||
(callback) => {
|
UserProps.AchievementTotalCount,
|
||||||
this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => {
|
UserProps.AchievementTotalPoints,
|
||||||
if(err) {
|
].includes(userStatEvent.statName)
|
||||||
return callback(err);
|
) {
|
||||||
}
|
return;
|
||||||
return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null);
|
}
|
||||||
});
|
|
||||||
},
|
if (
|
||||||
(callback) => {
|
!_.isNumber(userStatEvent.statValue) &&
|
||||||
const client = getConnectionByUserId(userStatEvent.user.userId);
|
!_.isNumber(userStatEvent.statIncrementBy)
|
||||||
if(!client) {
|
) {
|
||||||
return callback(Errors.UnexpectedState('Failed to get client for user ID'));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: Make this code generic - find + return factory created object
|
||||||
|
const achievementTags = Object.keys(
|
||||||
|
_.pickBy(
|
||||||
|
_.get(this.config.get(), 'achievements', {}),
|
||||||
|
achievement => {
|
||||||
|
if (false === achievement.enabled) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
const acceptedTypes = [
|
||||||
|
Achievement.Types.UserStatSet,
|
||||||
|
Achievement.Types.UserStatInc,
|
||||||
|
Achievement.Types.UserStatIncNewVal,
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
acceptedTypes.includes(achievement.type) &&
|
||||||
|
achievement.statName === userStatEvent.statName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const info = {
|
if (0 === achievementTags.length) {
|
||||||
achievementTag,
|
return;
|
||||||
achievement,
|
}
|
||||||
details,
|
|
||||||
client,
|
|
||||||
matchField, // match - may be in odd format
|
|
||||||
matchValue, // actual value
|
|
||||||
achievedValue : matchField, // achievement value met
|
|
||||||
user : userStatEvent.user,
|
|
||||||
timestamp : moment(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const achievementsInfo = [ info ];
|
async.eachSeries(
|
||||||
return callback(null, achievementsInfo, info);
|
achievementTags,
|
||||||
},
|
(achievementTag, nextAchievementTag) => {
|
||||||
(achievementsInfo, basicInfo, callback) => {
|
const achievement = Achievement.factory(
|
||||||
if(true !== achievement.data.retroactive) {
|
this.getAchievementByTag(achievementTag)
|
||||||
return callback(null, achievementsInfo);
|
);
|
||||||
}
|
if (!achievement) {
|
||||||
|
return nextAchievementTag(null);
|
||||||
|
}
|
||||||
|
|
||||||
const index = achievement.matchKeys.findIndex(v => v < matchField);
|
const statValue = parseInt(
|
||||||
if(-1 === index || !Array.isArray(achievement.matchKeys)) {
|
[
|
||||||
return callback(null, achievementsInfo);
|
Achievement.Types.UserStatSet,
|
||||||
}
|
Achievement.Types.UserStatIncNewVal,
|
||||||
|
].includes(achievement.data.type)
|
||||||
|
? userStatEvent.statValue
|
||||||
|
: userStatEvent.statIncrementBy
|
||||||
|
);
|
||||||
|
if (isNaN(statValue)) {
|
||||||
|
return nextAchievementTag(null);
|
||||||
|
}
|
||||||
|
|
||||||
// For userStat, any lesser match keys(values) are also met. Example:
|
const [details, matchField, matchValue] =
|
||||||
// matchKeys: [ 500, 200, 100, 20, 10, 2 ]
|
achievement.getMatchDetails(statValue);
|
||||||
// ^---- we met here
|
if (!details) {
|
||||||
// ^------------^ retroactive range
|
return nextAchievementTag(null);
|
||||||
//
|
}
|
||||||
async.eachSeries(achievement.matchKeys.slice(index), (k, nextKey) => {
|
|
||||||
const [ det, fld, val ] = achievement.getMatchDetails(k);
|
|
||||||
if(!det) {
|
|
||||||
return nextKey(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadAchievementHitCount(userStatEvent.user, achievementTag, fld, (err, count) => {
|
async.waterfall(
|
||||||
if(!err || count && 0 === count) {
|
[
|
||||||
achievementsInfo.push(Object.assign(
|
callback => {
|
||||||
{},
|
this.loadAchievementHitCount(
|
||||||
basicInfo,
|
userStatEvent.user,
|
||||||
{
|
achievementTag,
|
||||||
details : det,
|
matchField,
|
||||||
matchField : fld,
|
(err, count) => {
|
||||||
achievedValue : fld,
|
if (err) {
|
||||||
matchValue : val,
|
return callback(err);
|
||||||
}
|
}
|
||||||
));
|
return callback(
|
||||||
|
count > 0
|
||||||
|
? Errors.General(
|
||||||
|
'Achievement already acquired',
|
||||||
|
ErrorReasons.TooMany
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
callback => {
|
||||||
|
const client = getConnectionByUserId(
|
||||||
|
userStatEvent.user.userId
|
||||||
|
);
|
||||||
|
if (!client) {
|
||||||
|
return callback(
|
||||||
|
Errors.UnexpectedState(
|
||||||
|
'Failed to get client for user ID'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextKey(null);
|
const info = {
|
||||||
});
|
achievementTag,
|
||||||
},
|
achievement,
|
||||||
() => {
|
details,
|
||||||
return callback(null, achievementsInfo);
|
client,
|
||||||
});
|
matchField, // match - may be in odd format
|
||||||
},
|
matchValue, // actual value
|
||||||
(achievementsInfo, callback) => {
|
achievedValue: matchField, // achievement value met
|
||||||
// reverse achievementsInfo so we display smallest > largest
|
user: userStatEvent.user,
|
||||||
achievementsInfo.reverse();
|
timestamp: moment(),
|
||||||
|
};
|
||||||
|
|
||||||
async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => {
|
const achievementsInfo = [info];
|
||||||
return this.recordAndDisplayAchievement(achInfo, err => {
|
return callback(null, achievementsInfo, info);
|
||||||
return nextAchInfo(err);
|
},
|
||||||
});
|
(achievementsInfo, basicInfo, callback) => {
|
||||||
},
|
if (true !== achievement.data.retroactive) {
|
||||||
|
return callback(null, achievementsInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = achievement.matchKeys.findIndex(
|
||||||
|
v => v < matchField
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
-1 === index ||
|
||||||
|
!Array.isArray(achievement.matchKeys)
|
||||||
|
) {
|
||||||
|
return callback(null, achievementsInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For userStat, any lesser match keys(values) are also met. Example:
|
||||||
|
// matchKeys: [ 500, 200, 100, 20, 10, 2 ]
|
||||||
|
// ^---- we met here
|
||||||
|
// ^------------^ retroactive range
|
||||||
|
//
|
||||||
|
async.eachSeries(
|
||||||
|
achievement.matchKeys.slice(index),
|
||||||
|
(k, nextKey) => {
|
||||||
|
const [det, fld, val] =
|
||||||
|
achievement.getMatchDetails(k);
|
||||||
|
if (!det) {
|
||||||
|
return nextKey(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadAchievementHitCount(
|
||||||
|
userStatEvent.user,
|
||||||
|
achievementTag,
|
||||||
|
fld,
|
||||||
|
(err, count) => {
|
||||||
|
if (!err || (count && 0 === count)) {
|
||||||
|
achievementsInfo.push(
|
||||||
|
Object.assign({}, basicInfo, {
|
||||||
|
details: det,
|
||||||
|
matchField: fld,
|
||||||
|
achievedValue: fld,
|
||||||
|
matchValue: val,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextKey(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return callback(null, achievementsInfo);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(achievementsInfo, callback) => {
|
||||||
|
// reverse achievementsInfo so we display smallest > largest
|
||||||
|
achievementsInfo.reverse();
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
achievementsInfo,
|
||||||
|
(achInfo, nextAchInfo) => {
|
||||||
|
return this.recordAndDisplayAchievement(
|
||||||
|
achInfo,
|
||||||
|
err => {
|
||||||
|
return nextAchInfo(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
err => {
|
err => {
|
||||||
return callback(err);
|
if (err && ErrorReasons.TooMany !== err.reasonCode) {
|
||||||
});
|
Log.warn(
|
||||||
}
|
{ error: err.message, userStatEvent },
|
||||||
],
|
'Error handling achievement for user stat event'
|
||||||
err => {
|
);
|
||||||
if(err && ErrorReasons.TooMany !== err.reasonCode) {
|
}
|
||||||
Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event');
|
return nextAchievementTag(null); // always try the next, regardless
|
||||||
}
|
}
|
||||||
return nextAchievementTag(null); // always try the next, regardless
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopMonitoringUserStatEvents() {
|
stopMonitoringUserStatEvents() {
|
||||||
if(this.userStatEventListeners) {
|
if (this.userStatEventListeners) {
|
||||||
this.events.removeMultipleEventListener(this.userStatEventListeners);
|
this.events.removeMultipleEventListener(this.userStatEventListeners);
|
||||||
delete this.userStatEventListeners;
|
delete this.userStatEventListeners;
|
||||||
}
|
}
|
||||||
@@ -436,34 +504,38 @@ class Achievements {
|
|||||||
|
|
||||||
getFormatObject(info) {
|
getFormatObject(info) {
|
||||||
return {
|
return {
|
||||||
userName : info.user.username,
|
userName: info.user.username,
|
||||||
userRealName : info.user.properties[UserProps.RealName],
|
userRealName: info.user.properties[UserProps.RealName],
|
||||||
userLocation : info.user.properties[UserProps.Location],
|
userLocation: info.user.properties[UserProps.Location],
|
||||||
userAffils : info.user.properties[UserProps.Affiliations],
|
userAffils: info.user.properties[UserProps.Affiliations],
|
||||||
nodeId : info.client.node,
|
nodeId: info.client.node,
|
||||||
title : info.details.title,
|
title: info.details.title,
|
||||||
//text : info.global ? info.details.globalText : info.details.text,
|
//text : info.global ? info.details.globalText : info.details.text,
|
||||||
points : info.details.points,
|
points: info.details.points,
|
||||||
achievedValue : info.achievedValue,
|
achievedValue: info.achievedValue,
|
||||||
matchField : info.matchField,
|
matchField: info.matchField,
|
||||||
matchValue : info.matchValue,
|
matchValue: info.matchValue,
|
||||||
timestamp : moment(info.timestamp).format(info.dateTimeFormat),
|
timestamp: moment(info.timestamp).format(info.dateTimeFormat),
|
||||||
boardName : Config().general.boardName,
|
boardName: Config().general.boardName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormattedTextFor(info, textType, defaultSgr = '|07') {
|
getFormattedTextFor(info, textType, defaultSgr = '|07') {
|
||||||
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
|
const themeDefaults = _.get(
|
||||||
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
|
info.client.currentTheme,
|
||||||
|
'achievements.defaults',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
|
||||||
|
|
||||||
const formatObj = this.getFormatObject(info);
|
const formatObj = this.getFormatObject(info);
|
||||||
|
|
||||||
const wrap = (input) => {
|
const wrap = input => {
|
||||||
const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g');
|
const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g');
|
||||||
return input.replace(re, (m, formatVar, formatOpts) => {
|
return input.replace(re, (m, formatVar, formatOpts) => {
|
||||||
const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr;
|
const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr;
|
||||||
let r = `${varSgr}{${formatVar}`;
|
let r = `${varSgr}{${formatVar}`;
|
||||||
if(formatOpts) {
|
if (formatOpts) {
|
||||||
r += formatOpts;
|
r += formatOpts;
|
||||||
}
|
}
|
||||||
return `${r}}${textTypeSgr}`;
|
return `${r}}${textTypeSgr}`;
|
||||||
@@ -480,10 +552,10 @@ class Achievements {
|
|||||||
info.client.currentTheme.helpers.getDateTimeFormat();
|
info.client.currentTheme.helpers.getDateTimeFormat();
|
||||||
|
|
||||||
const title = this.getFormattedTextFor(info, 'title');
|
const title = this.getFormattedTextFor(info, 'title');
|
||||||
const text = this.getFormattedTextFor(info, 'text');
|
const text = this.getFormattedTextFor(info, 'text');
|
||||||
|
|
||||||
let globalText;
|
let globalText;
|
||||||
if(info.details.globalText) {
|
if (info.details.globalText) {
|
||||||
globalText = this.getFormattedTextFor(info, 'globalText');
|
globalText = this.getFormattedTextFor(info, 'globalText');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,13 +564,13 @@ class Achievements {
|
|||||||
_.get(info.details, `art.${name}`) ||
|
_.get(info.details, `art.${name}`) ||
|
||||||
_.get(info.achievement, `art.${name}`) ||
|
_.get(info.achievement, `art.${name}`) ||
|
||||||
_.get(this.config.get(), `art.${name}`);
|
_.get(this.config.get(), `art.${name}`);
|
||||||
if(!spec) {
|
if (!spec) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
const getArtOpts = {
|
const getArtOpts = {
|
||||||
name : spec,
|
name: spec,
|
||||||
client : this.client,
|
client: this.client,
|
||||||
random : false,
|
random: false,
|
||||||
};
|
};
|
||||||
getThemeArt(getArtOpts, (err, artInfo) => {
|
getThemeArt(getArtOpts, (err, artInfo) => {
|
||||||
// ignore errors
|
// ignore errors
|
||||||
@@ -507,67 +579,86 @@ class Achievements {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const interruptItems = {};
|
const interruptItems = {};
|
||||||
let itemTypes = [ 'local' ];
|
let itemTypes = ['local'];
|
||||||
if(globalText) {
|
if (globalText) {
|
||||||
itemTypes.push('global');
|
itemTypes.push('global');
|
||||||
}
|
}
|
||||||
|
|
||||||
async.each(itemTypes, (itemType, nextItemType) => {
|
async.each(
|
||||||
async.waterfall(
|
itemTypes,
|
||||||
[
|
(itemType, nextItemType) => {
|
||||||
(callback) => {
|
async.waterfall(
|
||||||
getArt(`${itemType}Header`, headerArt => {
|
[
|
||||||
return callback(null, headerArt);
|
callback => {
|
||||||
});
|
getArt(`${itemType}Header`, headerArt => {
|
||||||
},
|
return callback(null, headerArt);
|
||||||
(headerArt, callback) => {
|
|
||||||
getArt(`${itemType}Footer`, footerArt => {
|
|
||||||
return callback(null, headerArt, footerArt);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(headerArt, footerArt, callback) => {
|
|
||||||
const itemText = 'global' === itemType ? globalText : text;
|
|
||||||
interruptItems[itemType] = {
|
|
||||||
title,
|
|
||||||
achievText : itemText,
|
|
||||||
text : `${title}\r\n${itemText}`,
|
|
||||||
pause : true,
|
|
||||||
};
|
|
||||||
if(headerArt || footerArt) {
|
|
||||||
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
|
|
||||||
const defaultContentsFormat = '{title}\r\n{message}';
|
|
||||||
const contentsFormat = 'global' === itemType ?
|
|
||||||
themeDefaults.globalFormat || defaultContentsFormat :
|
|
||||||
themeDefaults.format || defaultContentsFormat;
|
|
||||||
|
|
||||||
const formatObj = Object.assign(this.getFormatObject(info), {
|
|
||||||
title : this.getFormattedTextFor(info, 'title', ''), // ''=defaultSgr
|
|
||||||
message : itemText,
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
(headerArt, callback) => {
|
||||||
|
getArt(`${itemType}Footer`, footerArt => {
|
||||||
|
return callback(null, headerArt, footerArt);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(headerArt, footerArt, callback) => {
|
||||||
|
const itemText = 'global' === itemType ? globalText : text;
|
||||||
|
interruptItems[itemType] = {
|
||||||
|
title,
|
||||||
|
achievText: itemText,
|
||||||
|
text: `${title}\r\n${itemText}`,
|
||||||
|
pause: true,
|
||||||
|
};
|
||||||
|
if (headerArt || footerArt) {
|
||||||
|
const themeDefaults = _.get(
|
||||||
|
info.client.currentTheme,
|
||||||
|
'achievements.defaults',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const defaultContentsFormat = '{title}\r\n{message}';
|
||||||
|
const contentsFormat =
|
||||||
|
'global' === itemType
|
||||||
|
? themeDefaults.globalFormat ||
|
||||||
|
defaultContentsFormat
|
||||||
|
: themeDefaults.format || defaultContentsFormat;
|
||||||
|
|
||||||
const contents = pipeToAnsi(stringFormat(contentsFormat, formatObj));
|
const formatObj = Object.assign(
|
||||||
|
this.getFormatObject(info),
|
||||||
|
{
|
||||||
|
title: this.getFormattedTextFor(
|
||||||
|
info,
|
||||||
|
'title',
|
||||||
|
''
|
||||||
|
), // ''=defaultSgr
|
||||||
|
message: itemText,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interruptItems[itemType].contents =
|
const contents = pipeToAnsi(
|
||||||
`${headerArt || ''}\r\n${contents}\r\n${footerArt || ''}`;
|
stringFormat(contentsFormat, formatObj)
|
||||||
}
|
);
|
||||||
return callback(null);
|
|
||||||
|
interruptItems[itemType].contents = `${
|
||||||
|
headerArt || ''
|
||||||
|
}\r\n${contents}\r\n${footerArt || ''}`;
|
||||||
|
}
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return nextItemType(err);
|
||||||
}
|
}
|
||||||
],
|
);
|
||||||
err => {
|
},
|
||||||
return nextItemType(err);
|
err => {
|
||||||
}
|
return cb(err, interruptItems);
|
||||||
);
|
}
|
||||||
},
|
);
|
||||||
err => {
|
|
||||||
return cb(err, interruptItems);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let achievementsInstance;
|
let achievementsInstance;
|
||||||
|
|
||||||
function getAchievementsEarnedByUser(userId, cb) {
|
function getAchievementsEarnedByUser(userId, cb) {
|
||||||
if(!achievementsInstance) {
|
if (!achievementsInstance) {
|
||||||
return cb(Errors.UnexpectedState('Achievements not initialized'));
|
return cb(Errors.UnexpectedState('Achievements not initialized'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,39 +667,42 @@ function getAchievementsEarnedByUser(userId, cb) {
|
|||||||
FROM user_achievement
|
FROM user_achievement
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
ORDER BY DATETIME(timestamp);`,
|
ORDER BY DATETIME(timestamp);`,
|
||||||
[ userId ],
|
[userId],
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const earned = rows.map(row => {
|
const earned = rows
|
||||||
|
.map(row => {
|
||||||
|
const achievement = Achievement.factory(
|
||||||
|
achievementsInstance.getAchievementByTag(row.achievement_tag)
|
||||||
|
);
|
||||||
|
if (!achievement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const achievement = Achievement.factory(achievementsInstance.getAchievementByTag(row.achievement_tag));
|
const earnedInfo = {
|
||||||
if(!achievement) {
|
achievementTag: row.achievement_tag,
|
||||||
return;
|
type: achievement.data.type,
|
||||||
}
|
retroactive: achievement.data.retroactive,
|
||||||
|
title: row.title,
|
||||||
|
text: row.text,
|
||||||
|
points: row.points,
|
||||||
|
timestamp: moment(row.timestamp),
|
||||||
|
};
|
||||||
|
|
||||||
const earnedInfo = {
|
switch (earnedInfo.type) {
|
||||||
achievementTag : row.achievement_tag,
|
case [Achievement.Types.UserStatSet]:
|
||||||
type : achievement.data.type,
|
case [Achievement.Types.UserStatInc]:
|
||||||
retroactive : achievement.data.retroactive,
|
case [Achievement.Types.UserStatIncNewVal]:
|
||||||
title : row.title,
|
earnedInfo.statName = achievement.data.statName;
|
||||||
text : row.text,
|
break;
|
||||||
points : row.points,
|
}
|
||||||
timestamp : moment(row.timestamp),
|
|
||||||
};
|
|
||||||
|
|
||||||
switch(earnedInfo.type) {
|
return earnedInfo;
|
||||||
case [ Achievement.Types.UserStatSet ] :
|
})
|
||||||
case [ Achievement.Types.UserStatInc ] :
|
.filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore).
|
||||||
case [ Achievement.Types.UserStatIncNewVal ] :
|
|
||||||
earnedInfo.statName = achievement.data.statName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return earnedInfo;
|
|
||||||
}).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore).
|
|
||||||
|
|
||||||
return cb(null, earned);
|
return cb(null, earned);
|
||||||
}
|
}
|
||||||
@@ -617,8 +711,8 @@ function getAchievementsEarnedByUser(userId, cb) {
|
|||||||
|
|
||||||
exports.moduleInitialize = (initInfo, cb) => {
|
exports.moduleInitialize = (initInfo, cb) => {
|
||||||
achievementsInstance = new Achievements(initInfo.events);
|
achievementsInstance = new Achievements(initInfo.events);
|
||||||
achievementsInstance.init( err => {
|
achievementsInstance.init(err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
core/acs.js
57
core/acs.js
@@ -2,12 +2,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const checkAcs = require('./acs_parser.js').parse;
|
const checkAcs = require('./acs_parser.js').parse;
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
class ACS {
|
class ACS {
|
||||||
constructor(subject) {
|
constructor(subject) {
|
||||||
@@ -16,15 +16,15 @@ class ACS {
|
|||||||
|
|
||||||
static get Defaults() {
|
static get Defaults() {
|
||||||
return {
|
return {
|
||||||
MessageConfRead : 'GM[users]', // list/read
|
MessageConfRead: 'GM[users]', // list/read
|
||||||
MessageConfWrite : 'GM[users]', // post/write
|
MessageConfWrite: 'GM[users]', // post/write
|
||||||
|
|
||||||
MessageAreaRead : 'GM[users]', // list/read; requires parent conf read
|
MessageAreaRead: 'GM[users]', // list/read; requires parent conf read
|
||||||
MessageAreaWrite : 'GM[users]', // post/write; requires parent conf write
|
MessageAreaWrite: 'GM[users]', // post/write; requires parent conf write
|
||||||
|
|
||||||
FileAreaRead : 'GM[users]', // list
|
FileAreaRead: 'GM[users]', // list
|
||||||
FileAreaWrite : 'GM[sysops]', // upload
|
FileAreaWrite: 'GM[sysops]', // upload
|
||||||
FileAreaDownload : 'GM[users]', // download
|
FileAreaDownload: 'GM[users]', // download
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ class ACS {
|
|||||||
acs = acs ? acs[scope] : defaultAcs;
|
acs = acs ? acs[scope] : defaultAcs;
|
||||||
acs = acs || defaultAcs;
|
acs = acs || defaultAcs;
|
||||||
try {
|
try {
|
||||||
return checkAcs(acs, { subject : this.subject } );
|
return checkAcs(acs, { subject: this.subject });
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS');
|
Log.warn({ exception: e, acs: acs }, 'Exception caught checking ACS');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,39 +76,42 @@ class ACS {
|
|||||||
|
|
||||||
hasMenuModuleAccess(modInst) {
|
hasMenuModuleAccess(modInst) {
|
||||||
const acs = _.get(modInst, 'menuConfig.config.acs');
|
const acs = _.get(modInst, 'menuConfig.config.acs');
|
||||||
if(!_.isString(acs)) {
|
if (!_.isString(acs)) {
|
||||||
return true; // no ACS check req.
|
return true; // no ACS check req.
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return checkAcs(acs, { subject : this.subject } );
|
return checkAcs(acs, { subject: this.subject });
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS');
|
Log.warn({ exception: e, acs: acs }, 'Exception caught checking ACS');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditionalValue(condArray, memberName) {
|
getConditionalValue(condArray, memberName) {
|
||||||
if(!Array.isArray(condArray)) {
|
if (!Array.isArray(condArray)) {
|
||||||
// no cond array, just use the value
|
// no cond array, just use the value
|
||||||
return condArray;
|
return condArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_.isString(memberName));
|
assert(_.isString(memberName));
|
||||||
|
|
||||||
const matchCond = condArray.find( cond => {
|
const matchCond = condArray.find(cond => {
|
||||||
if(_.has(cond, 'acs')) {
|
if (_.has(cond, 'acs')) {
|
||||||
try {
|
try {
|
||||||
return checkAcs(cond.acs, { subject : this.subject } );
|
return checkAcs(cond.acs, { subject: this.subject });
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS');
|
Log.warn(
|
||||||
|
{ exception: e, acs: cond },
|
||||||
|
'Exception caught checking ACS'
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return true; // no ACS check req.
|
return true; // no ACS check req.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(matchCond) {
|
if (matchCond) {
|
||||||
return matchCond[memberName];
|
return matchCond[memberName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const events = require('events');
|
const events = require('events');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.ANSIEscapeParser = ANSIEscapeParser;
|
exports.ANSIEscapeParser = ANSIEscapeParser;
|
||||||
|
|
||||||
const CR = 0x0d;
|
const CR = 0x0d;
|
||||||
const LF = 0x0a;
|
const LF = 0x0a;
|
||||||
@@ -20,49 +20,47 @@ function ANSIEscapeParser(options) {
|
|||||||
|
|
||||||
events.EventEmitter.call(this);
|
events.EventEmitter.call(this);
|
||||||
|
|
||||||
this.column = 1;
|
this.column = 1;
|
||||||
this.graphicRendition = {};
|
this.graphicRendition = {};
|
||||||
|
|
||||||
this.parseState = {
|
this.parseState = {
|
||||||
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||||
};
|
};
|
||||||
|
|
||||||
options = miscUtil.valueWithDefault(options, {
|
options = miscUtil.valueWithDefault(options, {
|
||||||
mciReplaceChar : '',
|
mciReplaceChar: '',
|
||||||
termHeight : 25,
|
termHeight: 25,
|
||||||
termWidth : 80,
|
termWidth: 80,
|
||||||
trailingLF : 'default', // default|omit|no|yes, ...
|
trailingLF: 'default', // default|omit|no|yes, ...
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||||
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
||||||
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
||||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
||||||
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
|
||||||
|
|
||||||
|
|
||||||
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
||||||
|
|
||||||
self.moveCursor = function(cols, rows) {
|
self.moveCursor = function (cols, rows) {
|
||||||
self.column += cols;
|
self.column += cols;
|
||||||
self.row += rows;
|
self.row += rows;
|
||||||
|
|
||||||
self.column = Math.max(self.column, 1);
|
self.column = Math.max(self.column, 1);
|
||||||
self.column = Math.min(self.column, self.termWidth); // can't move past term width
|
self.column = Math.min(self.column, self.termWidth); // can't move past term width
|
||||||
self.row = Math.max(self.row, 1);
|
self.row = Math.max(self.row, 1);
|
||||||
|
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
};
|
};
|
||||||
|
|
||||||
self.saveCursorPosition = function() {
|
self.saveCursorPosition = function () {
|
||||||
self.savedPosition = {
|
self.savedPosition = {
|
||||||
row : self.row,
|
row: self.row,
|
||||||
column : self.column
|
column: self.column,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
self.restoreCursorPosition = function() {
|
self.restoreCursorPosition = function () {
|
||||||
self.row = self.savedPosition.row;
|
self.row = self.savedPosition.row;
|
||||||
self.column = self.savedPosition.column;
|
self.column = self.savedPosition.column;
|
||||||
delete self.savedPosition;
|
delete self.savedPosition;
|
||||||
|
|
||||||
@@ -70,29 +68,28 @@ function ANSIEscapeParser(options) {
|
|||||||
// self.rowUpdated();
|
// self.rowUpdated();
|
||||||
};
|
};
|
||||||
|
|
||||||
self.clearScreen = function() {
|
self.clearScreen = function () {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row = 1;
|
self.row = 1;
|
||||||
self.emit('clear screen');
|
self.emit('clear screen');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.positionUpdated = function () {
|
||||||
self.positionUpdated = function() {
|
|
||||||
self.emit('position update', self.row, self.column);
|
self.emit('position update', self.row, self.column);
|
||||||
};
|
};
|
||||||
|
|
||||||
function literal(text) {
|
function literal(text) {
|
||||||
const len = text.length;
|
const len = text.length;
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let charCode;
|
let charCode;
|
||||||
let lastCharCode;
|
let lastCharCode;
|
||||||
|
|
||||||
while(pos < len) {
|
while (pos < len) {
|
||||||
charCode = text.charCodeAt(pos) & 0xff; // 8bit clean
|
charCode = text.charCodeAt(pos) & 0xff; // 8bit clean
|
||||||
|
|
||||||
switch(charCode) {
|
switch (charCode) {
|
||||||
case CR :
|
case CR:
|
||||||
self.emit('literal', text.slice(start, pos));
|
self.emit('literal', text.slice(start, pos));
|
||||||
start = pos;
|
start = pos;
|
||||||
|
|
||||||
@@ -101,7 +98,7 @@ function ANSIEscapeParser(options) {
|
|||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LF :
|
case LF:
|
||||||
// Handle ANSI saved with UNIX-style LF's only
|
// Handle ANSI saved with UNIX-style LF's only
|
||||||
// vs the CRLF pairs
|
// vs the CRLF pairs
|
||||||
if (lastCharCode !== CR) {
|
if (lastCharCode !== CR) {
|
||||||
@@ -116,13 +113,13 @@ function ANSIEscapeParser(options) {
|
|||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default :
|
default:
|
||||||
if(self.column === self.termWidth) {
|
if (self.column === self.termWidth) {
|
||||||
self.emit('literal', text.slice(start, pos + 1));
|
self.emit('literal', text.slice(start, pos + 1));
|
||||||
start = pos + 1;
|
start = pos + 1;
|
||||||
|
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
|
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
} else {
|
} else {
|
||||||
@@ -138,15 +135,15 @@ function ANSIEscapeParser(options) {
|
|||||||
//
|
//
|
||||||
// Finalize this chunk
|
// Finalize this chunk
|
||||||
//
|
//
|
||||||
if(self.column > self.termWidth) {
|
if (self.column > self.termWidth) {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
|
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
const rem = text.slice(start);
|
const rem = text.slice(start);
|
||||||
if(rem) {
|
if (rem) {
|
||||||
self.emit('literal', rem);
|
self.emit('literal', rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,18 +158,18 @@ function ANSIEscapeParser(options) {
|
|||||||
var id;
|
var id;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
pos = mciRe.lastIndex;
|
pos = mciRe.lastIndex;
|
||||||
match = mciRe.exec(buffer);
|
match = mciRe.exec(buffer);
|
||||||
|
|
||||||
if(null !== match) {
|
if (null !== match) {
|
||||||
if(match.index > pos) {
|
if (match.index > pos) {
|
||||||
literal(buffer.slice(pos, match.index));
|
literal(buffer.slice(pos, match.index));
|
||||||
}
|
}
|
||||||
|
|
||||||
mciCode = match[1];
|
mciCode = match[1];
|
||||||
id = match[2] || null;
|
id = match[2] || null;
|
||||||
|
|
||||||
if(match[3]) {
|
if (match[3]) {
|
||||||
args = match[3].split(',');
|
args = match[3].split(',');
|
||||||
} else {
|
} else {
|
||||||
args = [];
|
args = [];
|
||||||
@@ -180,58 +177,62 @@ function ANSIEscapeParser(options) {
|
|||||||
|
|
||||||
// if MCI codes are changing, save off the current color
|
// if MCI codes are changing, save off the current color
|
||||||
var fullMciCode = mciCode + (id || '');
|
var fullMciCode = mciCode + (id || '');
|
||||||
if(self.lastMciCode !== fullMciCode) {
|
if (self.lastMciCode !== fullMciCode) {
|
||||||
|
|
||||||
self.lastMciCode = fullMciCode;
|
self.lastMciCode = fullMciCode;
|
||||||
|
|
||||||
self.graphicRenditionForErase = _.clone(self.graphicRendition);
|
self.graphicRenditionForErase = _.clone(self.graphicRendition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self.emit('mci', {
|
self.emit('mci', {
|
||||||
position : [self.row, self.column],
|
position: [self.row, self.column],
|
||||||
mci : mciCode,
|
mci: mciCode,
|
||||||
id : id ? parseInt(id, 10) : null,
|
id: id ? parseInt(id, 10) : null,
|
||||||
args : args,
|
args: args,
|
||||||
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
|
SGR: ansi.getSGRFromGraphicRendition(self.graphicRendition, true),
|
||||||
});
|
});
|
||||||
|
|
||||||
if(self.mciReplaceChar.length > 0) {
|
if (self.mciReplaceChar.length > 0) {
|
||||||
const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase);
|
const sgrCtrl = ansi.getSGRFromGraphicRendition(
|
||||||
|
self.graphicRenditionForErase
|
||||||
|
);
|
||||||
|
|
||||||
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3));
|
self.emit(
|
||||||
|
'control',
|
||||||
|
sgrCtrl,
|
||||||
|
'm',
|
||||||
|
sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)
|
||||||
|
);
|
||||||
|
|
||||||
literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
|
literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
|
||||||
} else {
|
} else {
|
||||||
literal(match[0]);
|
literal(match[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} while (0 !== mciRe.lastIndex);
|
||||||
|
|
||||||
} while(0 !== mciRe.lastIndex);
|
if (pos < buffer.length) {
|
||||||
|
|
||||||
if(pos < buffer.length) {
|
|
||||||
literal(buffer.slice(pos));
|
literal(buffer.slice(pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.reset = function(input) {
|
self.reset = function (input) {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row = Math.min(options?.startRow ?? 1, self.termHeight);
|
self.row = Math.min(options?.startRow ?? 1, self.termHeight);
|
||||||
|
|
||||||
self.parseState = {
|
self.parseState = {
|
||||||
// ignore anything past EOF marker, if any
|
// ignore anything past EOF marker, if any
|
||||||
buffer : input.split(String.fromCharCode(0x1a), 1)[0],
|
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
|
||||||
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||||
stop : false,
|
stop: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
self.stop = function() {
|
self.stop = function () {
|
||||||
self.parseState.stop = true;
|
self.parseState.stop = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.parse = function(input) {
|
self.parse = function (input) {
|
||||||
if(input) {
|
if (input) {
|
||||||
self.reset(input);
|
self.reset(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,53 +241,53 @@ function ANSIEscapeParser(options) {
|
|||||||
var match;
|
var match;
|
||||||
var opCode;
|
var opCode;
|
||||||
var args;
|
var args;
|
||||||
var re = self.parseState.re;
|
var re = self.parseState.re;
|
||||||
var buffer = self.parseState.buffer;
|
var buffer = self.parseState.buffer;
|
||||||
|
|
||||||
self.parseState.stop = false;
|
self.parseState.stop = false;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if(self.parseState.stop) {
|
if (self.parseState.stop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos = re.lastIndex;
|
pos = re.lastIndex;
|
||||||
match = re.exec(buffer);
|
match = re.exec(buffer);
|
||||||
|
|
||||||
if(null !== match) {
|
if (null !== match) {
|
||||||
if(match.index > pos) {
|
if (match.index > pos) {
|
||||||
parseMCI(buffer.slice(pos, match.index));
|
parseMCI(buffer.slice(pos, match.index));
|
||||||
}
|
}
|
||||||
|
|
||||||
opCode = match[2];
|
opCode = match[2];
|
||||||
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
|
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
|
||||||
|
|
||||||
escape(opCode, args);
|
escape(opCode, args);
|
||||||
|
|
||||||
//self.emit('chunk', match[0]);
|
//self.emit('chunk', match[0]);
|
||||||
self.emit('control', match[0], opCode, args);
|
self.emit('control', match[0], opCode, args);
|
||||||
}
|
}
|
||||||
} while(0 !== re.lastIndex);
|
} while (0 !== re.lastIndex);
|
||||||
|
|
||||||
if(pos < buffer.length) {
|
if (pos < buffer.length) {
|
||||||
var lastBit = buffer.slice(pos);
|
var lastBit = buffer.slice(pos);
|
||||||
|
|
||||||
// :TODO: check for various ending LF's, not just DOS \r\n
|
// :TODO: check for various ending LF's, not just DOS \r\n
|
||||||
if('\r\n' === lastBit.slice(-2).toString()) {
|
if ('\r\n' === lastBit.slice(-2).toString()) {
|
||||||
switch(self.trailingLF) {
|
switch (self.trailingLF) {
|
||||||
case 'default' :
|
case 'default':
|
||||||
//
|
//
|
||||||
// Default is to *not* omit the trailing LF
|
// Default is to *not* omit the trailing LF
|
||||||
// if we're going to end on termHeight
|
// if we're going to end on termHeight
|
||||||
//
|
//
|
||||||
if(this.termHeight === self.row) {
|
if (this.termHeight === self.row) {
|
||||||
lastBit = lastBit.slice(0, -2);
|
lastBit = lastBit.slice(0, -2);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'omit' :
|
case 'omit':
|
||||||
case 'no' :
|
case 'no':
|
||||||
case false :
|
case false:
|
||||||
lastBit = lastBit.slice(0, -2);
|
lastBit = lastBit.slice(0, -2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -343,69 +344,69 @@ function ANSIEscapeParser(options) {
|
|||||||
function escape(opCode, args) {
|
function escape(opCode, args) {
|
||||||
let arg;
|
let arg;
|
||||||
|
|
||||||
switch(opCode) {
|
switch (opCode) {
|
||||||
// cursor up
|
// cursor up
|
||||||
case 'A' :
|
case 'A':
|
||||||
//arg = args[0] || 1;
|
//arg = args[0] || 1;
|
||||||
arg = isNaN(args[0]) ? 1 : args[0];
|
arg = isNaN(args[0]) ? 1 : args[0];
|
||||||
self.moveCursor(0, -arg);
|
self.moveCursor(0, -arg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// cursor down
|
// cursor down
|
||||||
case 'B' :
|
case 'B':
|
||||||
//arg = args[0] || 1;
|
//arg = args[0] || 1;
|
||||||
arg = isNaN(args[0]) ? 1 : args[0];
|
arg = isNaN(args[0]) ? 1 : args[0];
|
||||||
self.moveCursor(0, arg);
|
self.moveCursor(0, arg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// cursor forward/right
|
// cursor forward/right
|
||||||
case 'C' :
|
case 'C':
|
||||||
//arg = args[0] || 1;
|
//arg = args[0] || 1;
|
||||||
arg = isNaN(args[0]) ? 1 : args[0];
|
arg = isNaN(args[0]) ? 1 : args[0];
|
||||||
self.moveCursor(arg, 0);
|
self.moveCursor(arg, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// cursor back/left
|
// cursor back/left
|
||||||
case 'D' :
|
case 'D':
|
||||||
//arg = args[0] || 1;
|
//arg = args[0] || 1;
|
||||||
arg = isNaN(args[0]) ? 1 : args[0];
|
arg = isNaN(args[0]) ? 1 : args[0];
|
||||||
self.moveCursor(-arg, 0);
|
self.moveCursor(-arg, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'f' : // horiz & vertical
|
case 'f': // horiz & vertical
|
||||||
case 'H' : // cursor position
|
case 'H': // cursor position
|
||||||
//self.row = args[0] || 1;
|
//self.row = args[0] || 1;
|
||||||
//self.column = args[1] || 1;
|
//self.column = args[1] || 1;
|
||||||
self.row = isNaN(args[0]) ? 1 : args[0];
|
self.row = isNaN(args[0]) ? 1 : args[0];
|
||||||
self.column = isNaN(args[1]) ? 1 : args[1];
|
self.column = isNaN(args[1]) ? 1 : args[1];
|
||||||
//self.rowUpdated();
|
//self.rowUpdated();
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// save position
|
// save position
|
||||||
case 's' :
|
case 's':
|
||||||
self.saveCursorPosition();
|
self.saveCursorPosition();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// restore position
|
// restore position
|
||||||
case 'u' :
|
case 'u':
|
||||||
self.restoreCursorPosition();
|
self.restoreCursorPosition();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// set graphic rendition
|
// set graphic rendition
|
||||||
case 'm' :
|
case 'm':
|
||||||
self.graphicRendition.reset = false;
|
self.graphicRendition.reset = false;
|
||||||
|
|
||||||
for(let i = 0, len = args.length; i < len; ++i) {
|
for (let i = 0, len = args.length; i < len; ++i) {
|
||||||
arg = args[i];
|
arg = args[i];
|
||||||
|
|
||||||
if(ANSIEscapeParser.foregroundColors[arg]) {
|
if (ANSIEscapeParser.foregroundColors[arg]) {
|
||||||
self.graphicRendition.fg = arg;
|
self.graphicRendition.fg = arg;
|
||||||
} else if(ANSIEscapeParser.backgroundColors[arg]) {
|
} else if (ANSIEscapeParser.backgroundColors[arg]) {
|
||||||
self.graphicRendition.bg = arg;
|
self.graphicRendition.bg = arg;
|
||||||
} else if(ANSIEscapeParser.styles[arg]) {
|
} else if (ANSIEscapeParser.styles[arg]) {
|
||||||
switch(arg) {
|
switch (arg) {
|
||||||
case 0 :
|
case 0:
|
||||||
// clear out everything
|
// clear out everything
|
||||||
delete self.graphicRendition.intensity;
|
delete self.graphicRendition.intensity;
|
||||||
delete self.graphicRendition.underline;
|
delete self.graphicRendition.underline;
|
||||||
@@ -421,49 +422,52 @@ function ANSIEscapeParser(options) {
|
|||||||
//self.graphicRendition.bg = 49;
|
//self.graphicRendition.bg = 49;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1 :
|
case 1:
|
||||||
case 2 :
|
case 2:
|
||||||
case 22 :
|
case 22:
|
||||||
self.graphicRendition.intensity = arg;
|
self.graphicRendition.intensity = arg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4 :
|
case 4:
|
||||||
case 24 :
|
case 24:
|
||||||
self.graphicRendition.underline = arg;
|
self.graphicRendition.underline = arg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 5 :
|
case 5:
|
||||||
case 6 :
|
case 6:
|
||||||
case 25 :
|
case 25:
|
||||||
self.graphicRendition.blink = arg;
|
self.graphicRendition.blink = arg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 7 :
|
case 7:
|
||||||
case 27 :
|
case 27:
|
||||||
self.graphicRendition.negative = arg;
|
self.graphicRendition.negative = arg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 8 :
|
case 8:
|
||||||
case 28 :
|
case 28:
|
||||||
self.graphicRendition.invisible = arg;
|
self.graphicRendition.invisible = arg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default :
|
default:
|
||||||
Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI');
|
Log.trace(
|
||||||
|
{ attribute: arg },
|
||||||
|
'Unknown attribute while parsing ANSI'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit('sgr update', self.graphicRendition);
|
self.emit('sgr update', self.graphicRendition);
|
||||||
break; // m
|
break; // m
|
||||||
|
|
||||||
// :TODO: s, u, K
|
// :TODO: s, u, K
|
||||||
|
|
||||||
// erase display/screen
|
// erase display/screen
|
||||||
case 'J' :
|
case 'J':
|
||||||
// :TODO: Handle other 'J' types!
|
// :TODO: Handle other 'J' types!
|
||||||
if(2 === args[0]) {
|
if (2 === args[0]) {
|
||||||
self.clearScreen();
|
self.clearScreen();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -474,30 +478,30 @@ function ANSIEscapeParser(options) {
|
|||||||
util.inherits(ANSIEscapeParser, events.EventEmitter);
|
util.inherits(ANSIEscapeParser, events.EventEmitter);
|
||||||
|
|
||||||
ANSIEscapeParser.foregroundColors = {
|
ANSIEscapeParser.foregroundColors = {
|
||||||
30 : 'black',
|
30: 'black',
|
||||||
31 : 'red',
|
31: 'red',
|
||||||
32 : 'green',
|
32: 'green',
|
||||||
33 : 'yellow',
|
33: 'yellow',
|
||||||
34 : 'blue',
|
34: 'blue',
|
||||||
35 : 'magenta',
|
35: 'magenta',
|
||||||
36 : 'cyan',
|
36: 'cyan',
|
||||||
37 : 'white',
|
37: 'white',
|
||||||
39 : 'default', // same as white for most implementations
|
39: 'default', // same as white for most implementations
|
||||||
|
|
||||||
90 : 'grey'
|
90: 'grey',
|
||||||
};
|
};
|
||||||
Object.freeze(ANSIEscapeParser.foregroundColors);
|
Object.freeze(ANSIEscapeParser.foregroundColors);
|
||||||
|
|
||||||
ANSIEscapeParser.backgroundColors = {
|
ANSIEscapeParser.backgroundColors = {
|
||||||
40 : 'black',
|
40: 'black',
|
||||||
41 : 'red',
|
41: 'red',
|
||||||
42 : 'green',
|
42: 'green',
|
||||||
43 : 'yellow',
|
43: 'yellow',
|
||||||
44 : 'blue',
|
44: 'blue',
|
||||||
45 : 'magenta',
|
45: 'magenta',
|
||||||
46 : 'cyan',
|
46: 'cyan',
|
||||||
47 : 'white',
|
47: 'white',
|
||||||
49 : 'default', // same as black for most implementations
|
49: 'default', // same as black for most implementations
|
||||||
};
|
};
|
||||||
Object.freeze(ANSIEscapeParser.backgroundColors);
|
Object.freeze(ANSIEscapeParser.backgroundColors);
|
||||||
|
|
||||||
@@ -512,24 +516,23 @@ Object.freeze(ANSIEscapeParser.backgroundColors);
|
|||||||
// can be grouped by concept here in code.
|
// can be grouped by concept here in code.
|
||||||
//
|
//
|
||||||
ANSIEscapeParser.styles = {
|
ANSIEscapeParser.styles = {
|
||||||
0 : 'default', // Everything disabled
|
0: 'default', // Everything disabled
|
||||||
|
|
||||||
1 : 'intensityBright', // aka bold
|
1: 'intensityBright', // aka bold
|
||||||
2 : 'intensityDim',
|
2: 'intensityDim',
|
||||||
22 : 'intensityNormal',
|
22: 'intensityNormal',
|
||||||
|
|
||||||
4 : 'underlineOn', // Not supported by most BBS-like terminals
|
4: 'underlineOn', // Not supported by most BBS-like terminals
|
||||||
24 : 'underlineOff', // Not supported by most BBS-like terminals
|
24: 'underlineOff', // Not supported by most BBS-like terminals
|
||||||
|
|
||||||
5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same
|
5: 'blinkSlow', // blinkSlow & blinkFast are generally treated the same
|
||||||
6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same
|
6: 'blinkFast', // blinkSlow & blinkFast are generally treated the same
|
||||||
25 : 'blinkOff',
|
25: 'blinkOff',
|
||||||
|
|
||||||
7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG"
|
7: 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG"
|
||||||
27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG"
|
27: 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG"
|
||||||
|
|
||||||
8 : 'invisibleOn', // FG set to BG
|
8: 'invisibleOn', // FG set to BG
|
||||||
28 : 'invisibleOff', // Not supported by most BBS-like terminals
|
28: 'invisibleOff', // Not supported by most BBS-like terminals
|
||||||
};
|
};
|
||||||
Object.freeze(ANSIEscapeParser.styles);
|
Object.freeze(ANSIEscapeParser.styles);
|
||||||
|
|
||||||
|
|||||||
@@ -2,58 +2,61 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser;
|
const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser;
|
||||||
const ANSI = require('./ansi_term.js');
|
const ANSI = require('./ansi_term.js');
|
||||||
const {
|
const { splitTextAtTerms, renderStringLength } = require('./string_util.js');
|
||||||
splitTextAtTerms,
|
|
||||||
renderStringLength
|
|
||||||
} = require('./string_util.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = function ansiPrep(input, options, cb) {
|
module.exports = function ansiPrep(input, options, cb) {
|
||||||
if(!input) {
|
if (!input) {
|
||||||
return cb(null, '');
|
return cb(null, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
options.termWidth = options.termWidth || 80;
|
options.termWidth = options.termWidth || 80;
|
||||||
options.termHeight = options.termHeight || 25;
|
options.termHeight = options.termHeight || 25;
|
||||||
options.cols = options.cols || options.termWidth || 80;
|
options.cols = options.cols || options.termWidth || 80;
|
||||||
options.rows = options.rows || options.termHeight || 'auto';
|
options.rows = options.rows || options.termHeight || 'auto';
|
||||||
options.startCol = options.startCol || 1;
|
options.startCol = options.startCol || 1;
|
||||||
options.exportMode = options.exportMode || false;
|
options.exportMode = options.exportMode || false;
|
||||||
options.fillLines = _.get(options, 'fillLines', true);
|
options.fillLines = _.get(options, 'fillLines', true);
|
||||||
options.indent = options.indent || 0;
|
options.indent = options.indent || 0;
|
||||||
|
|
||||||
// in auto we start out at 25 rows, but can always expand for more
|
// in auto we start out at 25 rows, but can always expand for more
|
||||||
const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) );
|
const canvas = Array.from(
|
||||||
const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } );
|
{ length: 'auto' === options.rows ? 25 : options.rows },
|
||||||
|
() => Array.from({ length: options.cols }, () => new Object())
|
||||||
|
);
|
||||||
|
const parser = new ANSIEscapeParser({
|
||||||
|
termHeight: options.termHeight,
|
||||||
|
termWidth: options.termWidth,
|
||||||
|
});
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
row : 0,
|
row: 0,
|
||||||
col : 0,
|
col: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastRow = 0;
|
let lastRow = 0;
|
||||||
|
|
||||||
function ensureRow(row) {
|
function ensureRow(row) {
|
||||||
if(canvas[row]) {
|
if (canvas[row]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas[row] = Array.from( { length : options.cols}, () => new Object() );
|
canvas[row] = Array.from({ length: options.cols }, () => new Object());
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.on('position update', (row, col) => {
|
parser.on('position update', (row, col) => {
|
||||||
state.row = row - 1;
|
state.row = row - 1;
|
||||||
state.col = col - 1;
|
state.col = col - 1;
|
||||||
|
|
||||||
if(0 === state.col) {
|
if (0 === state.col) {
|
||||||
state.initialSgr = state.lastSgr;
|
state.initialSgr = state.lastSgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRow = Math.max(state.row, lastRow);
|
lastRow = Math.max(state.row, lastRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
parser.on('literal', literal => {
|
parser.on('literal', literal => {
|
||||||
@@ -62,20 +65,23 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
//
|
//
|
||||||
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
|
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
|
||||||
|
|
||||||
for(let c of literal) {
|
for (let c of literal) {
|
||||||
if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) {
|
if (
|
||||||
|
state.col < options.cols &&
|
||||||
|
('auto' === options.rows || state.row < options.rows)
|
||||||
|
) {
|
||||||
ensureRow(state.row);
|
ensureRow(state.row);
|
||||||
|
|
||||||
if(0 === state.col) {
|
if (0 === state.col) {
|
||||||
canvas[state.row][state.col].initialSgr = state.initialSgr;
|
canvas[state.row][state.col].initialSgr = state.initialSgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas[state.row][state.col].char = c;
|
canvas[state.row][state.col].char = c;
|
||||||
|
|
||||||
if(state.sgr) {
|
if (state.sgr) {
|
||||||
canvas[state.row][state.col].sgr = _.clone(state.sgr);
|
canvas[state.row][state.col].sgr = _.clone(state.sgr);
|
||||||
state.lastSgr = canvas[state.row][state.col].sgr;
|
state.lastSgr = canvas[state.row][state.col].sgr;
|
||||||
state.sgr = null;
|
state.sgr = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +92,9 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
parser.on('sgr update', sgr => {
|
parser.on('sgr update', sgr => {
|
||||||
ensureRow(state.row);
|
ensureRow(state.row);
|
||||||
|
|
||||||
if(state.col < options.cols) {
|
if (state.col < options.cols) {
|
||||||
canvas[state.row][state.col].sgr = _.clone(sgr);
|
canvas[state.row][state.col].sgr = _.clone(sgr);
|
||||||
state.lastSgr = canvas[state.row][state.col].sgr;
|
state.lastSgr = canvas[state.row][state.col].sgr;
|
||||||
} else {
|
} else {
|
||||||
state.sgr = sgr;
|
state.sgr = sgr;
|
||||||
}
|
}
|
||||||
@@ -96,8 +102,8 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
|
|
||||||
function getLastPopulatedColumn(row) {
|
function getLastPopulatedColumn(row) {
|
||||||
let col = row.length;
|
let col = row.length;
|
||||||
while(--col > 0) {
|
while (--col > 0) {
|
||||||
if(row[col].char || row[col].sgr) {
|
if (row[col].char || row[col].sgr) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,18 +119,23 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
const lastCol = getLastPopulatedColumn(row) + 1;
|
const lastCol = getLastPopulatedColumn(row) + 1;
|
||||||
|
|
||||||
let i;
|
let i;
|
||||||
line = options.indent ?
|
line = options.indent
|
||||||
output.length > 0 ? ' '.repeat(options.indent) : '' :
|
? output.length > 0
|
||||||
'';
|
? ' '.repeat(options.indent)
|
||||||
|
: ''
|
||||||
|
: '';
|
||||||
|
|
||||||
for(i = 0; i < lastCol; ++i) {
|
for (i = 0; i < lastCol; ++i) {
|
||||||
const col = row[i];
|
const col = row[i];
|
||||||
|
|
||||||
sgr = !options.asciiMode && 0 === i ?
|
sgr =
|
||||||
col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' :
|
!options.asciiMode && 0 === i
|
||||||
'';
|
? col.initialSgr
|
||||||
|
? ANSI.getSGRFromGraphicRendition(col.initialSgr)
|
||||||
|
: ''
|
||||||
|
: '';
|
||||||
|
|
||||||
if(!options.asciiMode && col.sgr) {
|
if (!options.asciiMode && col.sgr) {
|
||||||
sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
|
sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,19 +144,22 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
|
|
||||||
output += line;
|
output += line;
|
||||||
|
|
||||||
if(i < row.length) {
|
if (i < row.length) {
|
||||||
output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
|
output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
|
||||||
if(options.fillLines) {
|
if (options.fillLines) {
|
||||||
output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`;
|
output += `${row
|
||||||
|
.slice(i)
|
||||||
|
.map(() => ' ')
|
||||||
|
.join('')}`; //${lastSgr}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.startCol + i < options.termWidth || options.forceLineTerm) {
|
if (options.startCol + i < options.termWidth || options.forceLineTerm) {
|
||||||
output += '\r\n';
|
output += '\r\n';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(options.exportMode) {
|
if (options.exportMode) {
|
||||||
//
|
//
|
||||||
// If we're in export mode, we do some additional hackery:
|
// If we're in export mode, we do some additional hackery:
|
||||||
//
|
//
|
||||||
@@ -156,7 +170,7 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
// * Replace contig spaces with ESC[<N>C as well to save... space.
|
// * Replace contig spaces with ESC[<N>C as well to save... space.
|
||||||
//
|
//
|
||||||
// :TODO: this would be better to do as part of the processing above, but this will do for now
|
// :TODO: this would be better to do as part of the processing above, but this will do for now
|
||||||
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
|
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
|
||||||
let exportOutput = '';
|
let exportOutput = '';
|
||||||
|
|
||||||
let m;
|
let m;
|
||||||
@@ -167,30 +181,30 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
splitTextAtTerms(output).forEach(fullLine => {
|
splitTextAtTerms(output).forEach(fullLine => {
|
||||||
renderStart = 0;
|
renderStart = 0;
|
||||||
|
|
||||||
while(fullLine.length > 0) {
|
while (fullLine.length > 0) {
|
||||||
let splitAt;
|
let splitAt;
|
||||||
const ANSI_REGEXP = ANSI.getFullMatchRegExp();
|
const ANSI_REGEXP = ANSI.getFullMatchRegExp();
|
||||||
wantMore = true;
|
wantMore = true;
|
||||||
|
|
||||||
while((m = ANSI_REGEXP.exec(fullLine))) {
|
while ((m = ANSI_REGEXP.exec(fullLine))) {
|
||||||
afterSeq = m.index + m[0].length;
|
afterSeq = m.index + m[0].length;
|
||||||
|
|
||||||
if(afterSeq < MAX_CHARS) {
|
if (afterSeq < MAX_CHARS) {
|
||||||
// after current seq
|
// after current seq
|
||||||
splitAt = afterSeq;
|
splitAt = afterSeq;
|
||||||
} else {
|
} else {
|
||||||
if(m.index < MAX_CHARS) {
|
if (m.index < MAX_CHARS) {
|
||||||
// before last found seq
|
// before last found seq
|
||||||
splitAt = m.index;
|
splitAt = m.index;
|
||||||
wantMore = false; // can't eat up any more
|
wantMore = false; // can't eat up any more
|
||||||
}
|
}
|
||||||
|
|
||||||
break; // seq's beyond this point are >= MAX_CHARS
|
break; // seq's beyond this point are >= MAX_CHARS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(splitAt) {
|
if (splitAt) {
|
||||||
if(wantMore) {
|
if (wantMore) {
|
||||||
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
|
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -202,7 +216,8 @@ module.exports = function ansiPrep(input, options, cb) {
|
|||||||
renderStart += renderStringLength(part);
|
renderStart += renderStringLength(part);
|
||||||
exportOutput += `${part}\r\n`;
|
exportOutput += `${part}\r\n`;
|
||||||
|
|
||||||
if(fullLine.length > 0) { // more to go for this line?
|
if (fullLine.length > 0) {
|
||||||
|
// more to go for this line?
|
||||||
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
|
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
|
||||||
} else {
|
} else {
|
||||||
exportOutput += ANSI.up();
|
exportOutput += ANSI.up();
|
||||||
|
|||||||
@@ -43,48 +43,48 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.getFullMatchRegExp = getFullMatchRegExp;
|
exports.getFullMatchRegExp = getFullMatchRegExp;
|
||||||
exports.getFGColorValue = getFGColorValue;
|
exports.getFGColorValue = getFGColorValue;
|
||||||
exports.getBGColorValue = getBGColorValue;
|
exports.getBGColorValue = getBGColorValue;
|
||||||
exports.sgr = sgr;
|
exports.sgr = sgr;
|
||||||
exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition;
|
exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition;
|
||||||
exports.clearScreen = clearScreen;
|
exports.clearScreen = clearScreen;
|
||||||
exports.resetScreen = resetScreen;
|
exports.resetScreen = resetScreen;
|
||||||
exports.normal = normal;
|
exports.normal = normal;
|
||||||
exports.goHome = goHome;
|
exports.goHome = goHome;
|
||||||
exports.disableVT100LineWrapping = disableVT100LineWrapping;
|
exports.disableVT100LineWrapping = disableVT100LineWrapping;
|
||||||
exports.setSyncTermFont = setSyncTermFont;
|
exports.setSyncTermFont = setSyncTermFont;
|
||||||
exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias;
|
exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias;
|
||||||
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
||||||
exports.setCursorStyle = setCursorStyle;
|
exports.setCursorStyle = setCursorStyle;
|
||||||
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
||||||
exports.vtxHyperlink = vtxHyperlink;
|
exports.vtxHyperlink = vtxHyperlink;
|
||||||
|
|
||||||
//
|
//
|
||||||
// See also
|
// See also
|
||||||
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js
|
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js
|
||||||
|
|
||||||
const ESC_CSI = '\u001b[';
|
const ESC_CSI = '\u001b[';
|
||||||
|
|
||||||
const CONTROL = {
|
const CONTROL = {
|
||||||
up : 'A',
|
up: 'A',
|
||||||
down : 'B',
|
down: 'B',
|
||||||
|
|
||||||
forward : 'C',
|
forward: 'C',
|
||||||
right : 'C',
|
right: 'C',
|
||||||
|
|
||||||
back : 'D',
|
back: 'D',
|
||||||
left : 'D',
|
left: 'D',
|
||||||
|
|
||||||
nextLine : 'E',
|
nextLine: 'E',
|
||||||
prevLine : 'F',
|
prevLine: 'F',
|
||||||
horizAbsolute : 'G',
|
horizAbsolute: 'G',
|
||||||
|
|
||||||
//
|
//
|
||||||
// CSI [ p1 ] J
|
// CSI [ p1 ] J
|
||||||
@@ -103,10 +103,10 @@ const CONTROL = {
|
|||||||
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
|
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
|
||||||
// and screen remainder
|
// and screen remainder
|
||||||
//
|
//
|
||||||
eraseData : 'J',
|
eraseData: 'J',
|
||||||
|
|
||||||
eraseLine : 'K',
|
eraseLine: 'K',
|
||||||
insertLine : 'L',
|
insertLine: 'L',
|
||||||
|
|
||||||
//
|
//
|
||||||
// CSI [ p1 ] M
|
// CSI [ p1 ] M
|
||||||
@@ -128,28 +128,28 @@ const CONTROL = {
|
|||||||
// incompatibilities & oddities around this sequence. ANSI-BBS
|
// incompatibilities & oddities around this sequence. ANSI-BBS
|
||||||
// states that it *should* work with any value of p1.
|
// states that it *should* work with any value of p1.
|
||||||
//
|
//
|
||||||
deleteLine : 'M',
|
deleteLine: 'M',
|
||||||
ansiMusic : 'M',
|
ansiMusic: 'M',
|
||||||
|
|
||||||
scrollUp : 'S',
|
scrollUp: 'S',
|
||||||
scrollDown : 'T',
|
scrollDown: 'T',
|
||||||
setScrollRegion : 'r',
|
setScrollRegion: 'r',
|
||||||
savePos : 's',
|
savePos: 's',
|
||||||
restorePos : 'u',
|
restorePos: 'u',
|
||||||
queryPos : '6n',
|
queryPos: '6n',
|
||||||
queryScreenSize : '255n', // See bansi.txt
|
queryScreenSize: '255n', // See bansi.txt
|
||||||
goto : 'H', // row Pr, column Pc -- same as f
|
goto: 'H', // row Pr, column Pc -- same as f
|
||||||
gotoAlt : 'f', // same as H
|
gotoAlt: 'f', // same as H
|
||||||
|
|
||||||
blinkToBrightIntensity : '?33h',
|
blinkToBrightIntensity: '?33h',
|
||||||
blinkNormal : '?33l',
|
blinkNormal: '?33l',
|
||||||
|
|
||||||
emulationSpeed : '*r', // Set output emulation speed. See cterm.txt
|
emulationSpeed: '*r', // Set output emulation speed. See cterm.txt
|
||||||
|
|
||||||
hideCursor : '?25l', // Nonstandard - cterm.txt
|
hideCursor: '?25l', // Nonstandard - cterm.txt
|
||||||
showCursor : '?25h', // Nonstandard - cterm.txt
|
showCursor: '?25h', // Nonstandard - cterm.txt
|
||||||
|
|
||||||
queryDeviceAttributes : 'c', // Nonstandard - cterm.txt
|
queryDeviceAttributes: 'c', // Nonstandard - cterm.txt
|
||||||
|
|
||||||
// :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes
|
// :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes
|
||||||
// apparently some terms can report screen size and text area via 18t and 19t
|
// apparently some terms can report screen size and text area via 18t and 19t
|
||||||
@@ -160,41 +160,44 @@ const CONTROL = {
|
|||||||
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
||||||
//
|
//
|
||||||
const SGRValues = {
|
const SGRValues = {
|
||||||
reset : 0,
|
reset: 0,
|
||||||
bold : 1,
|
bold: 1,
|
||||||
dim : 2,
|
dim: 2,
|
||||||
blink : 5,
|
blink: 5,
|
||||||
fastBlink : 6,
|
fastBlink: 6,
|
||||||
negative : 7,
|
negative: 7,
|
||||||
hidden : 8,
|
hidden: 8,
|
||||||
|
|
||||||
normal : 22, //
|
normal: 22, //
|
||||||
steady : 25,
|
steady: 25,
|
||||||
positive : 27,
|
positive: 27,
|
||||||
|
|
||||||
black : 30,
|
black: 30,
|
||||||
red : 31,
|
red: 31,
|
||||||
green : 32,
|
green: 32,
|
||||||
yellow : 33,
|
yellow: 33,
|
||||||
blue : 34,
|
blue: 34,
|
||||||
magenta : 35,
|
magenta: 35,
|
||||||
cyan : 36,
|
cyan: 36,
|
||||||
white : 37,
|
white: 37,
|
||||||
|
|
||||||
blackBG : 40,
|
blackBG: 40,
|
||||||
redBG : 41,
|
redBG: 41,
|
||||||
greenBG : 42,
|
greenBG: 42,
|
||||||
yellowBG : 43,
|
yellowBG: 43,
|
||||||
blueBG : 44,
|
blueBG: 44,
|
||||||
magentaBG : 45,
|
magentaBG: 45,
|
||||||
cyanBG : 46,
|
cyanBG: 46,
|
||||||
whiteBG : 47,
|
whiteBG: 47,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFullMatchRegExp(flags = 'g') {
|
function getFullMatchRegExp(flags = 'g') {
|
||||||
// :TODO: expand this a bit - see strip-ansi/etc.
|
// :TODO: expand this a bit - see strip-ansi/etc.
|
||||||
// :TODO: \u009b ?
|
// :TODO: \u009b ?
|
||||||
return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex
|
return new RegExp(
|
||||||
|
/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/,
|
||||||
|
flags
|
||||||
|
); // eslint-disable-line no-control-regex
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFGColorValue(name) {
|
function getFGColorValue(name) {
|
||||||
@@ -205,7 +208,6 @@ function getBGColorValue(name) {
|
|||||||
return SGRValues[name + 'BG'];
|
return SGRValues[name + 'BG'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
||||||
// :TODO: document
|
// :TODO: document
|
||||||
// :TODO: Create mappings for aliases... maybe make this a map to values instead
|
// :TODO: Create mappings for aliases... maybe make this a map to values instead
|
||||||
@@ -275,49 +277,48 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
|
|||||||
// replaced with '_' for lookup purposes.
|
// replaced with '_' for lookup purposes.
|
||||||
//
|
//
|
||||||
const FONT_ALIAS_TO_SYNCTERM_MAP = {
|
const FONT_ALIAS_TO_SYNCTERM_MAP = {
|
||||||
'cp437' : 'cp437',
|
cp437: 'cp437',
|
||||||
'ibm_vga' : 'cp437',
|
ibm_vga: 'cp437',
|
||||||
'ibmpc' : 'cp437',
|
ibmpc: 'cp437',
|
||||||
'ibm_pc' : 'cp437',
|
ibm_pc: 'cp437',
|
||||||
'pc' : 'cp437',
|
pc: 'cp437',
|
||||||
'cp437_art' : 'cp437',
|
cp437_art: 'cp437',
|
||||||
'ibmpcart' : 'cp437',
|
ibmpcart: 'cp437',
|
||||||
'ibmpc_art' : 'cp437',
|
ibmpc_art: 'cp437',
|
||||||
'ibm_pc_art' : 'cp437',
|
ibm_pc_art: 'cp437',
|
||||||
'msdos_art' : 'cp437',
|
msdos_art: 'cp437',
|
||||||
'msdosart' : 'cp437',
|
msdosart: 'cp437',
|
||||||
'pc_art' : 'cp437',
|
pc_art: 'cp437',
|
||||||
'pcart' : 'cp437',
|
pcart: 'cp437',
|
||||||
|
|
||||||
'ibm_vga50' : 'cp437',
|
ibm_vga50: 'cp437',
|
||||||
'ibm_vga25g' : 'cp437',
|
ibm_vga25g: 'cp437',
|
||||||
'ibm_ega' : 'cp437',
|
ibm_ega: 'cp437',
|
||||||
'ibm_ega43' : 'cp437',
|
ibm_ega43: 'cp437',
|
||||||
|
|
||||||
'topaz' : 'topaz',
|
topaz: 'topaz',
|
||||||
'amiga_topaz_1' : 'topaz',
|
amiga_topaz_1: 'topaz',
|
||||||
'amiga_topaz_1+' : 'topaz_plus',
|
'amiga_topaz_1+': 'topaz_plus',
|
||||||
'topazplus' : 'topaz_plus',
|
topazplus: 'topaz_plus',
|
||||||
'topaz_plus' : 'topaz_plus',
|
topaz_plus: 'topaz_plus',
|
||||||
'amiga_topaz_2' : 'topaz',
|
amiga_topaz_2: 'topaz',
|
||||||
'amiga_topaz_2+' : 'topaz_plus',
|
'amiga_topaz_2+': 'topaz_plus',
|
||||||
'topaz2plus' : 'topaz_plus',
|
topaz2plus: 'topaz_plus',
|
||||||
|
|
||||||
'pot_noodle' : 'pot_noodle',
|
pot_noodle: 'pot_noodle',
|
||||||
'p0tnoodle' : 'pot_noodle',
|
p0tnoodle: 'pot_noodle',
|
||||||
'amiga_p0t-noodle' : 'pot_noodle',
|
'amiga_p0t-noodle': 'pot_noodle',
|
||||||
|
|
||||||
'mo_soul' : 'mo_soul',
|
mo_soul: 'mo_soul',
|
||||||
'mosoul' : 'mo_soul',
|
mosoul: 'mo_soul',
|
||||||
'mo\'soul' : 'mo_soul',
|
"mo'soul": 'mo_soul',
|
||||||
'amiga_mosoul' : 'mo_soul',
|
amiga_mosoul: 'mo_soul',
|
||||||
|
|
||||||
'amiga_microknight' : 'microknight',
|
amiga_microknight: 'microknight',
|
||||||
'amiga_microknight+' : 'microknight_plus',
|
'amiga_microknight+': 'microknight_plus',
|
||||||
|
|
||||||
'atari' : 'atari',
|
|
||||||
'atarist' : 'atari',
|
|
||||||
|
|
||||||
|
atari: 'atari',
|
||||||
|
atarist: 'atari',
|
||||||
};
|
};
|
||||||
|
|
||||||
function setSyncTermFont(name, fontPage) {
|
function setSyncTermFont(name, fontPage) {
|
||||||
@@ -326,7 +327,7 @@ function setSyncTermFont(name, fontPage) {
|
|||||||
assert(p1 >= 0 && p1 <= 3);
|
assert(p1 >= 0 && p1 <= 3);
|
||||||
|
|
||||||
const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name);
|
const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name);
|
||||||
if(p2 > -1) {
|
if (p2 > -1) {
|
||||||
return `${ESC_CSI}${p1};${p2} D`;
|
return `${ESC_CSI}${p1};${p2} D`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,31 +344,30 @@ function setSyncTermFontWithAlias(nameOrAlias) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEC_CURSOR_STYLE = {
|
const DEC_CURSOR_STYLE = {
|
||||||
'blinking block' : 0,
|
'blinking block': 0,
|
||||||
'default' : 1,
|
default: 1,
|
||||||
'steady block' : 2,
|
'steady block': 2,
|
||||||
'blinking underline' : 3,
|
'blinking underline': 3,
|
||||||
'steady underline' : 4,
|
'steady underline': 4,
|
||||||
'blinking bar' : 5,
|
'blinking bar': 5,
|
||||||
'steady bar' : 6,
|
'steady bar': 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
function setCursorStyle(cursorStyle) {
|
function setCursorStyle(cursorStyle) {
|
||||||
const ps = DEC_CURSOR_STYLE[cursorStyle];
|
const ps = DEC_CURSOR_STYLE[cursorStyle];
|
||||||
if(ps) {
|
if (ps) {
|
||||||
return `${ESC_CSI}${ps} q`;
|
return `${ESC_CSI}${ps} q`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create methods such as up(), nextLine(),...
|
// Create methods such as up(), nextLine(),...
|
||||||
Object.keys(CONTROL).forEach(function onControlName(name) {
|
Object.keys(CONTROL).forEach(function onControlName(name) {
|
||||||
const code = CONTROL[name];
|
const code = CONTROL[name];
|
||||||
|
|
||||||
exports[name] = function() {
|
exports[name] = function () {
|
||||||
let c = code;
|
let c = code;
|
||||||
if(arguments.length > 0) {
|
if (arguments.length > 0) {
|
||||||
// arguments are array like -- we want an array
|
// arguments are array like -- we want an array
|
||||||
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
|
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
|
||||||
}
|
}
|
||||||
@@ -376,10 +376,10 @@ Object.keys(CONTROL).forEach(function onControlName(name) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create various color methods such as white(), yellowBG(), reset(), ...
|
// Create various color methods such as white(), yellowBG(), reset(), ...
|
||||||
Object.keys(SGRValues).forEach( name => {
|
Object.keys(SGRValues).forEach(name => {
|
||||||
const code = SGRValues[name];
|
const code = SGRValues[name];
|
||||||
|
|
||||||
exports[name] = function() {
|
exports[name] = function () {
|
||||||
return `${ESC_CSI}${code}m`;
|
return `${ESC_CSI}${code}m`;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -390,18 +390,18 @@ function sgr() {
|
|||||||
// - Each element can be either a integer or string found in SGRValues
|
// - Each element can be either a integer or string found in SGRValues
|
||||||
// which in turn maps to a integer
|
// which in turn maps to a integer
|
||||||
//
|
//
|
||||||
if(arguments.length <= 0) {
|
if (arguments.length <= 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
|
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
|
||||||
|
|
||||||
for(let i = 0; i < args.length; ++i) {
|
for (let i = 0; i < args.length; ++i) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
if(_.isString(arg) && arg in SGRValues) {
|
if (_.isString(arg) && arg in SGRValues) {
|
||||||
result.push(SGRValues[arg]);
|
result.push(SGRValues[arg]);
|
||||||
} else if(_.isNumber(arg)) {
|
} else if (_.isNumber(arg)) {
|
||||||
result.push(arg);
|
result.push(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,25 +414,25 @@ function sgr() {
|
|||||||
// to a ANSI SGR sequence.
|
// to a ANSI SGR sequence.
|
||||||
//
|
//
|
||||||
function getSGRFromGraphicRendition(graphicRendition, initialReset) {
|
function getSGRFromGraphicRendition(graphicRendition, initialReset) {
|
||||||
let sgrSeq = [];
|
let sgrSeq = [];
|
||||||
let styleCount = 0;
|
let styleCount = 0;
|
||||||
|
|
||||||
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => {
|
['intensity', 'underline', 'blink', 'negative', 'invisible'].forEach(s => {
|
||||||
if(graphicRendition[s]) {
|
if (graphicRendition[s]) {
|
||||||
sgrSeq.push(graphicRendition[s]);
|
sgrSeq.push(graphicRendition[s]);
|
||||||
++styleCount;
|
++styleCount;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(graphicRendition.fg) {
|
if (graphicRendition.fg) {
|
||||||
sgrSeq.push(graphicRendition.fg);
|
sgrSeq.push(graphicRendition.fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(graphicRendition.bg) {
|
if (graphicRendition.bg) {
|
||||||
sgrSeq.push(graphicRendition.bg);
|
sgrSeq.push(graphicRendition.bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(0 === styleCount || initialReset) {
|
if (0 === styleCount || initialReset) {
|
||||||
sgrSeq.unshift(0);
|
sgrSeq.unshift(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,11 +452,11 @@ function resetScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normal() {
|
function normal() {
|
||||||
return sgr( [ 'normal', 'reset' ] );
|
return sgr(['normal', 'reset']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goHome() {
|
function goHome() {
|
||||||
return exports.goto(); // no params = home = 1,1
|
return exports.goto(); // no params = home = 1,1
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -476,32 +476,36 @@ function disableVT100LineWrapping() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setEmulatedBaudRate(rate) {
|
function setEmulatedBaudRate(rate) {
|
||||||
const speed = {
|
const speed =
|
||||||
unlimited : 0,
|
{
|
||||||
off : 0,
|
unlimited: 0,
|
||||||
0 : 0,
|
off: 0,
|
||||||
300 : 1,
|
0: 0,
|
||||||
600 : 2,
|
300: 1,
|
||||||
1200 : 3,
|
600: 2,
|
||||||
2400 : 4,
|
1200: 3,
|
||||||
4800 : 5,
|
2400: 4,
|
||||||
9600 : 6,
|
4800: 5,
|
||||||
19200 : 7,
|
9600: 6,
|
||||||
38400 : 8,
|
19200: 7,
|
||||||
57600 : 9,
|
38400: 8,
|
||||||
76800 : 10,
|
57600: 9,
|
||||||
115200 : 11,
|
76800: 10,
|
||||||
}[rate] || 0;
|
115200: 11,
|
||||||
|
}[rate] || 0;
|
||||||
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
|
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function vtxHyperlink(client, url, len) {
|
function vtxHyperlink(client, url, len) {
|
||||||
if(!client.terminalSupports('vtx_hyperlink')) {
|
if (!client.terminalSupports('vtx_hyperlink')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
len = len || url.length;
|
len = len || url.length;
|
||||||
|
|
||||||
url = url.split('').map(c => c.charCodeAt(0)).join(';');
|
url = url
|
||||||
|
.split('')
|
||||||
|
.map(c => c.charCodeAt(0))
|
||||||
|
.join(';');
|
||||||
return `${ESC_CSI}1;${len};1;1;${url}\\`;
|
return `${ESC_CSI}1;${len};1;1;${url}\\`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const { MenuModule } = require('../core/menu_module.js');
|
const { MenuModule } = require('../core/menu_module.js');
|
||||||
const { resetScreen } = require('../core/ansi_term.js');
|
const { resetScreen } = require('../core/ansi_term.js');
|
||||||
const { Errors } = require('../core/enig_error.js');
|
const { Errors } = require('../core/enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const SSHClient = require('ssh2').Client;
|
const SSHClient = require('ssh2').Client;
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'ArchaicNET',
|
name: 'ArchaicNET',
|
||||||
desc : 'ArchaicNET Access Module',
|
desc: 'ArchaicNET Access Module',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class ArchaicNETModule extends MenuModule {
|
exports.getModule = class ArchaicNETModule extends MenuModule {
|
||||||
@@ -22,10 +22,10 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
// establish defaults
|
// establish defaults
|
||||||
this.config = options.menuConfig.config;
|
this.config = options.menuConfig.config;
|
||||||
this.config.host = this.config.host || 'bbs.archaicbinary.net';
|
this.config.host = this.config.host || 'bbs.archaicbinary.net';
|
||||||
this.config.sshPort = this.config.sshPort || 2222;
|
this.config.sshPort = this.config.sshPort || 2222;
|
||||||
this.config.rloginPort = this.config.rloginPort || 8513;
|
this.config.rloginPort = this.config.rloginPort || 8513;
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
@@ -35,10 +35,12 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
|
|||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
const reqConfs = [ 'username', 'password', 'bbsTag' ];
|
const reqConfs = ['username', 'password', 'bbsTag'];
|
||||||
for(let req of reqConfs) {
|
for (let req of reqConfs) {
|
||||||
if(!_.isString(_.get(self, [ 'config', req ]))) {
|
if (!_.isString(_.get(self, ['config', req]))) {
|
||||||
return callback(Errors.MissingConfig(`Config requires "${req}"`));
|
return callback(
|
||||||
|
Errors.MissingConfig(`Config requires "${req}"`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return callback(null);
|
return callback(null);
|
||||||
@@ -51,8 +53,8 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
|
|||||||
|
|
||||||
let needRestore = false;
|
let needRestore = false;
|
||||||
//let pipedStream;
|
//let pipedStream;
|
||||||
const restorePipe = function() {
|
const restorePipe = function () {
|
||||||
if(needRestore && !clientTerminated) {
|
if (needRestore && !clientTerminated) {
|
||||||
self.client.restoreDataHandler();
|
self.client.restoreDataHandler();
|
||||||
needRestore = false;
|
needRestore = false;
|
||||||
}
|
}
|
||||||
@@ -61,75 +63,91 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
|
|||||||
sshClient.on('ready', () => {
|
sshClient.on('ready', () => {
|
||||||
// track client termination so we can clean up early
|
// track client termination so we can clean up early
|
||||||
self.client.once('end', () => {
|
self.client.once('end', () => {
|
||||||
self.client.log.info('Connection ended. Terminating ArchaicNET connection');
|
self.client.log.info(
|
||||||
|
'Connection ended. Terminating ArchaicNET connection'
|
||||||
|
);
|
||||||
clientTerminated = true;
|
clientTerminated = true;
|
||||||
return sshClient.end();
|
return sshClient.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
// establish tunnel for rlogin
|
// establish tunnel for rlogin
|
||||||
const fwdPort = self.config.rloginPort + self.client.node;
|
const fwdPort = self.config.rloginPort + self.client.node;
|
||||||
sshClient.forwardOut('127.0.0.1', fwdPort, self.config.host, self.config.rloginPort, (err, stream) => {
|
sshClient.forwardOut(
|
||||||
if(err) {
|
'127.0.0.1',
|
||||||
return sshClient.end();
|
fwdPort,
|
||||||
|
self.config.host,
|
||||||
|
self.config.rloginPort,
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
return sshClient.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Send rlogin - [<bbsTag>]<userName> e.g. [Xibalba]NuSkooler
|
||||||
|
//
|
||||||
|
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
|
||||||
|
stream.write(rlogin);
|
||||||
|
|
||||||
|
// we need to filter I/O for escape/de-escaping zmodem and the like
|
||||||
|
self.client.setTemporaryDirectDataHandler(data => {
|
||||||
|
const tmp = data
|
||||||
|
.toString('binary')
|
||||||
|
.replace(/\xff{2}/g, '\xff'); // de-escape
|
||||||
|
stream.write(Buffer.from(tmp, 'binary'));
|
||||||
|
});
|
||||||
|
needRestore = true;
|
||||||
|
|
||||||
|
stream.on('data', data => {
|
||||||
|
const tmp = data
|
||||||
|
.toString('binary')
|
||||||
|
.replace(/\xff/g, '\xff\xff'); // escape
|
||||||
|
self.client.term.rawWrite(Buffer.from(tmp, 'binary'));
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('close', () => {
|
||||||
|
restorePipe();
|
||||||
|
return sshClient.end();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
);
|
||||||
//
|
|
||||||
// Send rlogin - [<bbsTag>]<userName> e.g. [Xibalba]NuSkooler
|
|
||||||
//
|
|
||||||
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
|
|
||||||
stream.write(rlogin);
|
|
||||||
|
|
||||||
// we need to filter I/O for escape/de-escaping zmodem and the like
|
|
||||||
self.client.setTemporaryDirectDataHandler(data => {
|
|
||||||
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
|
|
||||||
stream.write(Buffer.from(tmp, 'binary'));
|
|
||||||
});
|
|
||||||
needRestore = true;
|
|
||||||
|
|
||||||
stream.on('data', data => {
|
|
||||||
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
|
|
||||||
self.client.term.rawWrite(Buffer.from(tmp, 'binary'));
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('close', () => {
|
|
||||||
restorePipe();
|
|
||||||
return sshClient.end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sshClient.on('error', err => {
|
sshClient.on('error', err => {
|
||||||
return self.client.log.info(`ArchaicNET SSH client error: ${err.message}`);
|
return self.client.log.info(
|
||||||
|
`ArchaicNET SSH client error: ${err.message}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
sshClient.on('close', hadError => {
|
sshClient.on('close', hadError => {
|
||||||
if(hadError) {
|
if (hadError) {
|
||||||
self.client.warn('Closing ArchaicNET SSH due to error');
|
self.client.warn('Closing ArchaicNET SSH due to error');
|
||||||
}
|
}
|
||||||
restorePipe();
|
restorePipe();
|
||||||
return callback(null);
|
return callback(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client.log.trace( { host : self.config.host, port : self.config.sshPort }, 'Connecting to ArchaicNET');
|
self.client.log.trace(
|
||||||
sshClient.connect( {
|
{ host: self.config.host, port: self.config.sshPort },
|
||||||
host : self.config.host,
|
'Connecting to ArchaicNET'
|
||||||
port : self.config.sshPort,
|
);
|
||||||
username : self.config.username,
|
sshClient.connect({
|
||||||
password : self.config.password,
|
host: self.config.host,
|
||||||
|
port: self.config.sshPort,
|
||||||
|
username: self.config.username,
|
||||||
|
password: self.config.password,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.message }, 'ArchaicNET error');
|
self.client.log.warn({ error: err.message }, 'ArchaicNET error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the client is stil here, go to previous
|
// if the client is stil here, go to previous
|
||||||
if(!clientTerminated) {
|
if (!clientTerminated) {
|
||||||
self.prevMenu();
|
self.prevMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,26 +2,26 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const resolveMimeType = require('./mime_util.js').resolveMimeType;
|
const resolveMimeType = require('./mime_util.js').resolveMimeType;
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
|
|
||||||
// base/modules
|
// base/modules
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const pty = require('node-pty');
|
const pty = require('node-pty');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
|
||||||
let archiveUtil;
|
let archiveUtil;
|
||||||
|
|
||||||
class Archiver {
|
class Archiver {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.compress = config.compress;
|
this.compress = config.compress;
|
||||||
this.decompress = config.decompress;
|
this.decompress = config.decompress;
|
||||||
this.list = config.list;
|
this.list = config.list;
|
||||||
this.extract = config.extract;
|
this.extract = config.extract;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok() {
|
ok() {
|
||||||
@@ -29,21 +29,32 @@ class Archiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
can(what) {
|
can(what) {
|
||||||
if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) {
|
if (!_.has(this, [what, 'cmd']) || !_.has(this, [what, 'args'])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0;
|
return (
|
||||||
|
_.isString(this[what].cmd) &&
|
||||||
|
Array.isArray(this[what].args) &&
|
||||||
|
this[what].args.length > 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
canCompress() { return this.can('compress'); }
|
canCompress() {
|
||||||
canDecompress() { return this.can('decompress'); }
|
return this.can('compress');
|
||||||
canList() { return this.can('list'); } // :TODO: validate entryMatch
|
}
|
||||||
canExtract() { return this.can('extract'); }
|
canDecompress() {
|
||||||
|
return this.can('decompress');
|
||||||
|
}
|
||||||
|
canList() {
|
||||||
|
return this.can('list');
|
||||||
|
} // :TODO: validate entryMatch
|
||||||
|
canExtract() {
|
||||||
|
return this.can('extract');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = class ArchiveUtil {
|
module.exports = class ArchiveUtil {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.archivers = {};
|
this.archivers = {};
|
||||||
this.longestSignature = 0;
|
this.longestSignature = 0;
|
||||||
@@ -51,7 +62,7 @@ module.exports = class ArchiveUtil {
|
|||||||
|
|
||||||
// singleton access
|
// singleton access
|
||||||
static getInstance(hotReload = true) {
|
static getInstance(hotReload = true) {
|
||||||
if(!archiveUtil) {
|
if (!archiveUtil) {
|
||||||
archiveUtil = new ArchiveUtil();
|
archiveUtil = new ArchiveUtil();
|
||||||
archiveUtil.init(hotReload);
|
archiveUtil.init(hotReload);
|
||||||
}
|
}
|
||||||
@@ -60,7 +71,7 @@ module.exports = class ArchiveUtil {
|
|||||||
|
|
||||||
init(hotReload = true) {
|
init(hotReload = true) {
|
||||||
this.reloadConfig();
|
this.reloadConfig();
|
||||||
if(hotReload) {
|
if (hotReload) {
|
||||||
Events.on(Events.getSystemEvents().ConfigChanged, () => {
|
Events.on(Events.getSystemEvents().ConfigChanged, () => {
|
||||||
this.reloadConfig();
|
this.reloadConfig();
|
||||||
});
|
});
|
||||||
@@ -69,13 +80,12 @@ module.exports = class ArchiveUtil {
|
|||||||
|
|
||||||
reloadConfig() {
|
reloadConfig() {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(_.has(config, 'archives.archivers')) {
|
if (_.has(config, 'archives.archivers')) {
|
||||||
Object.keys(config.archives.archivers).forEach(archKey => {
|
Object.keys(config.archives.archivers).forEach(archKey => {
|
||||||
|
const archConfig = config.archives.archivers[archKey];
|
||||||
|
const archiver = new Archiver(archConfig);
|
||||||
|
|
||||||
const archConfig = config.archives.archivers[archKey];
|
if (!archiver.ok()) {
|
||||||
const archiver = new Archiver(archConfig);
|
|
||||||
|
|
||||||
if(!archiver.ok()) {
|
|
||||||
// :TODO: Log warning - bad archiver/config
|
// :TODO: Log warning - bad archiver/config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,27 +93,27 @@ module.exports = class ArchiveUtil {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isObject(config.fileTypes)) {
|
if (_.isObject(config.fileTypes)) {
|
||||||
const updateSig = (ft) => {
|
const updateSig = ft => {
|
||||||
ft.sig = Buffer.from(ft.sig, 'hex');
|
ft.sig = Buffer.from(ft.sig, 'hex');
|
||||||
ft.offset = ft.offset || 0;
|
ft.offset = ft.offset || 0;
|
||||||
|
|
||||||
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
|
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
|
||||||
const sigLen = ft.offset + ft.sig.length;
|
const sigLen = ft.offset + ft.sig.length;
|
||||||
if(sigLen > this.longestSignature) {
|
if (sigLen > this.longestSignature) {
|
||||||
this.longestSignature = sigLen;
|
this.longestSignature = sigLen;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(config.fileTypes).forEach(mimeType => {
|
Object.keys(config.fileTypes).forEach(mimeType => {
|
||||||
const fileType = config.fileTypes[mimeType];
|
const fileType = config.fileTypes[mimeType];
|
||||||
if(Array.isArray(fileType)) {
|
if (Array.isArray(fileType)) {
|
||||||
fileType.forEach(ft => {
|
fileType.forEach(ft => {
|
||||||
if(ft.sig) {
|
if (ft.sig) {
|
||||||
updateSig(ft);
|
updateSig(ft);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if(fileType.sig) {
|
} else if (fileType.sig) {
|
||||||
updateSig(fileType);
|
updateSig(fileType);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -113,15 +123,16 @@ module.exports = class ArchiveUtil {
|
|||||||
getArchiver(mimeTypeOrExtension, justExtention) {
|
getArchiver(mimeTypeOrExtension, justExtention) {
|
||||||
const mimeType = resolveMimeType(mimeTypeOrExtension);
|
const mimeType = resolveMimeType(mimeTypeOrExtension);
|
||||||
|
|
||||||
if(!mimeType) { // lookup returns false on failure
|
if (!mimeType) {
|
||||||
|
// lookup returns false on failure
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
let fileType = _.get(config, [ 'fileTypes', mimeType ] );
|
let fileType = _.get(config, ['fileTypes', mimeType]);
|
||||||
|
|
||||||
if(Array.isArray(fileType)) {
|
if (Array.isArray(fileType)) {
|
||||||
if(!justExtention) {
|
if (!justExtention) {
|
||||||
// need extention for lookup; ambiguous as-is :(
|
// need extention for lookup; ambiguous as-is :(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -129,12 +140,12 @@ module.exports = class ArchiveUtil {
|
|||||||
fileType = fileType.find(ft => justExtention === ft.ext);
|
fileType = fileType.find(ft => justExtention === ft.ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_.isObject(fileType)) {
|
if (!_.isObject(fileType)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fileType.archiveHandler) {
|
if (fileType.archiveHandler) {
|
||||||
return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] );
|
return _.get(config, ['archives', 'archivers', fileType.archiveHandler]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,37 +160,41 @@ module.exports = class ArchiveUtil {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
detectType(path, cb) {
|
detectType(path, cb) {
|
||||||
const closeFile = (fd) => {
|
const closeFile = fd => {
|
||||||
fs.close(fd, () => { /* sadface */ });
|
fs.close(fd, () => {
|
||||||
|
/* sadface */
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.open(path, 'r', (err, fd) => {
|
fs.open(path, 'r', (err, fd) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buf = Buffer.alloc(this.longestSignature);
|
const buf = Buffer.alloc(this.longestSignature);
|
||||||
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
|
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
closeFile(fd);
|
closeFile(fd);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => {
|
const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => {
|
||||||
const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ];
|
const fileTypeInfos = Array.isArray(fileTypeInfo)
|
||||||
|
? fileTypeInfo
|
||||||
|
: [fileTypeInfo];
|
||||||
return fileTypeInfos.find(fti => {
|
return fileTypeInfos.find(fti => {
|
||||||
if(!fti.sig || !fti.archiveHandler) {
|
if (!fti.sig || !fti.archiveHandler) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lenNeeded = fti.offset + fti.sig.length;
|
const lenNeeded = fti.offset + fti.sig.length;
|
||||||
|
|
||||||
if(bytesRead < lenNeeded) {
|
if (bytesRead < lenNeeded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const comp = buf.slice(fti.offset, fti.offset + fti.sig.length);
|
const comp = buf.slice(fti.offset, fti.offset + fti.sig.length);
|
||||||
return (fti.sig.equals(comp));
|
return fti.sig.equals(comp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,20 +209,26 @@ module.exports = class ArchiveUtil {
|
|||||||
// so we have this horrible, horrible hack:
|
// so we have this horrible, horrible hack:
|
||||||
let err;
|
let err;
|
||||||
proc.once('data', d => {
|
proc.once('data', d => {
|
||||||
if(_.isString(d) && d.startsWith('execvp(3) failed.')) {
|
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
|
||||||
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
|
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.once('exit', exitCode => {
|
proc.once('exit', exitCode => {
|
||||||
return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err);
|
return cb(
|
||||||
|
exitCode
|
||||||
|
? Errors.ExternalProcess(
|
||||||
|
`${action} failed with exit code: ${exitCode}`
|
||||||
|
)
|
||||||
|
: err
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
compressTo(archType, archivePath, files, workDir, cb) {
|
compressTo(archType, archivePath, files, workDir, cb) {
|
||||||
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
||||||
|
|
||||||
if(!archiver) {
|
if (!archiver) {
|
||||||
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,17 +238,17 @@ module.exports = class ArchiveUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fmtObj = {
|
const fmtObj = {
|
||||||
archivePath : archivePath,
|
archivePath: archivePath,
|
||||||
fileList : files.join(' '), // :TODO: probably need same hack as extractTo here!
|
fileList: files.join(' '), // :TODO: probably need same hack as extractTo here!
|
||||||
};
|
};
|
||||||
|
|
||||||
// :TODO: DRY with extractTo()
|
// :TODO: DRY with extractTo()
|
||||||
const args = archiver.compress.args.map( arg => {
|
const args = archiver.compress.args.map(arg => {
|
||||||
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
|
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileListPos = args.indexOf('{fileList}');
|
const fileListPos = args.indexOf('{fileList}');
|
||||||
if(fileListPos > -1) {
|
if (fileListPos > -1) {
|
||||||
// replace {fileList} with 0:n sep file list arguments
|
// replace {fileList} with 0:n sep file list arguments
|
||||||
args.splice.apply(args, [fileListPos, 1].concat(files));
|
args.splice.apply(args, [fileListPos, 1].concat(files));
|
||||||
}
|
}
|
||||||
@@ -235,9 +256,13 @@ module.exports = class ArchiveUtil {
|
|||||||
let proc;
|
let proc;
|
||||||
try {
|
try {
|
||||||
proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir));
|
proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir));
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return cb(Errors.ExternalProcess(
|
return cb(
|
||||||
`Error spawning archiver process "${archiver.compress.cmd}" with args "${args.join(' ')}": ${e.message}`)
|
Errors.ExternalProcess(
|
||||||
|
`Error spawning archiver process "${
|
||||||
|
archiver.compress.cmd
|
||||||
|
}" with args "${args.join(' ')}": ${e.message}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +272,7 @@ module.exports = class ArchiveUtil {
|
|||||||
extractTo(archivePath, extractPath, archType, fileList, cb) {
|
extractTo(archivePath, extractPath, archType, fileList, cb) {
|
||||||
let haveFileList;
|
let haveFileList;
|
||||||
|
|
||||||
if(!cb && _.isFunction(fileList)) {
|
if (!cb && _.isFunction(fileList)) {
|
||||||
cb = fileList;
|
cb = fileList;
|
||||||
fileList = [];
|
fileList = [];
|
||||||
haveFileList = false;
|
haveFileList = false;
|
||||||
@@ -257,29 +282,29 @@ module.exports = class ArchiveUtil {
|
|||||||
|
|
||||||
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
||||||
|
|
||||||
if(!archiver) {
|
if (!archiver) {
|
||||||
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fmtObj = {
|
const fmtObj = {
|
||||||
archivePath : archivePath,
|
archivePath: archivePath,
|
||||||
extractPath : extractPath,
|
extractPath: extractPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
let action = haveFileList ? 'extract' : 'decompress';
|
let action = haveFileList ? 'extract' : 'decompress';
|
||||||
if('extract' === action && !_.isObject(archiver[action])) {
|
if ('extract' === action && !_.isObject(archiver[action])) {
|
||||||
// we're forced to do a full decompress
|
// we're forced to do a full decompress
|
||||||
action = 'decompress';
|
action = 'decompress';
|
||||||
haveFileList = false;
|
haveFileList = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to treat {fileList} special in that it should be broken up to 0:n args
|
// we need to treat {fileList} special in that it should be broken up to 0:n args
|
||||||
const args = archiver[action].args.map( arg => {
|
const args = archiver[action].args.map(arg => {
|
||||||
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
|
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileListPos = args.indexOf('{fileList}');
|
const fileListPos = args.indexOf('{fileList}');
|
||||||
if(fileListPos > -1) {
|
if (fileListPos > -1) {
|
||||||
// replace {fileList} with 0:n sep file list arguments
|
// replace {fileList} with 0:n sep file list arguments
|
||||||
args.splice.apply(args, [fileListPos, 1].concat(fileList));
|
args.splice.apply(args, [fileListPos, 1].concat(fileList));
|
||||||
}
|
}
|
||||||
@@ -287,34 +312,42 @@ module.exports = class ArchiveUtil {
|
|||||||
let proc;
|
let proc;
|
||||||
try {
|
try {
|
||||||
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath));
|
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath));
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return cb(Errors.ExternalProcess(
|
return cb(
|
||||||
`Error spawning archiver process "${archiver[action].cmd}" with args "${args.join(' ')}": ${e.message}`)
|
Errors.ExternalProcess(
|
||||||
|
`Error spawning archiver process "${
|
||||||
|
archiver[action].cmd
|
||||||
|
}" with args "${args.join(' ')}": ${e.message}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb);
|
return this.spawnHandler(proc, haveFileList ? 'Extraction' : 'Decompression', cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
listEntries(archivePath, archType, cb) {
|
listEntries(archivePath, archType, cb) {
|
||||||
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
||||||
|
|
||||||
if(!archiver) {
|
if (!archiver) {
|
||||||
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fmtObj = {
|
const fmtObj = {
|
||||||
archivePath : archivePath,
|
archivePath: archivePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) );
|
const args = archiver.list.args.map(arg => stringFormat(arg, fmtObj));
|
||||||
|
|
||||||
let proc;
|
let proc;
|
||||||
try {
|
try {
|
||||||
proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
|
proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return cb(Errors.ExternalProcess(
|
return cb(
|
||||||
`Error spawning archiver process "${archiver.list.cmd}" with args "${args.join(' ')}": ${e.message}`)
|
Errors.ExternalProcess(
|
||||||
|
`Error spawning archiver process "${
|
||||||
|
archiver.list.cmd
|
||||||
|
}" with args "${args.join(' ')}": ${e.message}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,19 +359,24 @@ module.exports = class ArchiveUtil {
|
|||||||
});
|
});
|
||||||
|
|
||||||
proc.once('exit', exitCode => {
|
proc.once('exit', exitCode => {
|
||||||
if(exitCode) {
|
if (exitCode) {
|
||||||
return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`));
|
return cb(
|
||||||
|
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 };
|
const entryGroupOrder = archiver.list.entryGroupOrder || {
|
||||||
|
byteSize: 1,
|
||||||
|
fileName: 2,
|
||||||
|
};
|
||||||
|
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
|
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
|
||||||
let m;
|
let m;
|
||||||
while((m = entryMatchRe.exec(output))) {
|
while ((m = entryMatchRe.exec(output))) {
|
||||||
entries.push({
|
entries.push({
|
||||||
byteSize : parseInt(m[entryGroupOrder.byteSize]),
|
byteSize: parseInt(m[entryGroupOrder.byteSize]),
|
||||||
fileName : m[entryGroupOrder.fileName].trim(),
|
fileName: m[entryGroupOrder.fileName].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,12 +386,12 @@ module.exports = class ArchiveUtil {
|
|||||||
|
|
||||||
getPtyOpts(cwd) {
|
getPtyOpts(cwd) {
|
||||||
const opts = {
|
const opts = {
|
||||||
name : 'enigma-archiver',
|
name: 'enigma-archiver',
|
||||||
cols : 80,
|
cols: 80,
|
||||||
rows : 24,
|
rows: 24,
|
||||||
env : process.env,
|
env: process.env,
|
||||||
};
|
};
|
||||||
if(cwd) {
|
if (cwd) {
|
||||||
opts.cwd = cwd;
|
opts.cwd = cwd;
|
||||||
}
|
}
|
||||||
// :TODO: set cwd to supplied temp path if not sepcific extract
|
// :TODO: set cwd to supplied temp path if not sepcific extract
|
||||||
|
|||||||
187
core/art.js
187
core/art.js
@@ -2,24 +2,24 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const aep = require('./ansi_escape_parser.js');
|
const aep = require('./ansi_escape_parser.js');
|
||||||
const sauce = require('./sauce.js');
|
const sauce = require('./sauce.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.getArt = getArt;
|
exports.getArt = getArt;
|
||||||
exports.getArtFromPath = getArtFromPath;
|
exports.getArtFromPath = getArtFromPath;
|
||||||
exports.display = display;
|
exports.display = display;
|
||||||
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
||||||
|
|
||||||
// :TODO: Return MCI code information
|
// :TODO: Return MCI code information
|
||||||
// :TODO: process SAUCE comments
|
// :TODO: process SAUCE comments
|
||||||
@@ -28,37 +28,37 @@ exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
|||||||
const SUPPORTED_ART_TYPES = {
|
const SUPPORTED_ART_TYPES = {
|
||||||
// :TODO: the defualt encoding are really useless if they are all the same ...
|
// :TODO: the defualt encoding are really useless if they are all the same ...
|
||||||
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
|
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
|
||||||
'.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a },
|
'.ans': { name: 'ANSI', defaultEncoding: 'cp437', eof: 0x1a },
|
||||||
'.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a },
|
'.asc': { name: 'ASCII', defaultEncoding: 'cp437', eof: 0x1a },
|
||||||
'.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a },
|
'.pcb': { name: 'PCBoard', defaultEncoding: 'cp437', eof: 0x1a },
|
||||||
'.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a },
|
'.bbs': { name: 'Wildcat', defaultEncoding: 'cp437', eof: 0x1a },
|
||||||
|
|
||||||
'.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a },
|
'.amiga': { name: 'Amiga', defaultEncoding: 'amiga', eof: 0x1a },
|
||||||
'.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a },
|
'.txt': { name: 'Amiga Text', defaultEncoding: 'cp437', eof: 0x1a },
|
||||||
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
|
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
|
||||||
// :TODO: extension for atari
|
// :TODO: extension for atari
|
||||||
// :TODO: extension for topaz ansi/ascii.
|
// :TODO: extension for topaz ansi/ascii.
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFontNameFromSAUCE(sauce) {
|
function getFontNameFromSAUCE(sauce) {
|
||||||
if(sauce.Character) {
|
if (sauce.Character) {
|
||||||
return sauce.Character.fontName;
|
return sauce.Character.fontName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceAtEOF(data, eofMarker) {
|
function sliceAtEOF(data, eofMarker) {
|
||||||
let eof = data.length;
|
let eof = data.length;
|
||||||
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
|
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
|
||||||
|
|
||||||
for(let i = eof - 1; i > stopPos; i--) {
|
for (let i = eof - 1; i > stopPos; i--) {
|
||||||
if(eofMarker === data[i]) {
|
if (eofMarker === data[i]) {
|
||||||
eof = i;
|
eof = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eof === data.length) {
|
if (eof === data.length) {
|
||||||
return data; // nothing to do
|
return data; // nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to prevent goofs
|
// try to prevent goofs
|
||||||
@@ -71,43 +71,46 @@ function sliceAtEOF(data, eofMarker) {
|
|||||||
|
|
||||||
function getArtFromPath(path, options, cb) {
|
function getArtFromPath(path, options, cb) {
|
||||||
fs.readFile(path, (err, data) => {
|
fs.readFile(path, (err, data) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Convert from encodedAs -> j
|
// Convert from encodedAs -> j
|
||||||
//
|
//
|
||||||
const ext = paths.extname(path).toLowerCase();
|
const ext = paths.extname(path).toLowerCase();
|
||||||
const encoding = options.encodedAs || defaultEncodingFromExtension(ext);
|
const encoding = options.encodedAs || defaultEncodingFromExtension(ext);
|
||||||
|
|
||||||
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
|
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
|
||||||
|
|
||||||
function sliceOfData() {
|
function sliceOfData() {
|
||||||
if(options.fullFile === true) {
|
if (options.fullFile === true) {
|
||||||
return iconv.decode(data, encoding);
|
return iconv.decode(data, encoding);
|
||||||
} else {
|
} else {
|
||||||
const eofMarker = defaultEofFromExtension(ext);
|
const eofMarker = defaultEofFromExtension(ext);
|
||||||
return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding);
|
return iconv.decode(
|
||||||
|
eofMarker ? sliceAtEOF(data, eofMarker) : data,
|
||||||
|
encoding
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResult(sauce) {
|
function getResult(sauce) {
|
||||||
const result = {
|
const result = {
|
||||||
data : sliceOfData(),
|
data: sliceOfData(),
|
||||||
fromPath : path,
|
fromPath: path,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(sauce) {
|
if (sauce) {
|
||||||
result.sauce = sauce;
|
result.sauce = sauce;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.readSauce === true) {
|
if (options.readSauce === true) {
|
||||||
sauce.readSAUCE(data, (err, sauce) => {
|
sauce.readSAUCE(data, (err, sauce) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(null, getResult());
|
return cb(null, getResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +118,7 @@ function getArtFromPath(path, options, cb) {
|
|||||||
// If a encoding was not provided & we have a mapping from
|
// If a encoding was not provided & we have a mapping from
|
||||||
// the information provided by SAUCE, use that.
|
// the information provided by SAUCE, use that.
|
||||||
//
|
//
|
||||||
if(!options.encodedAs) {
|
if (!options.encodedAs) {
|
||||||
/*
|
/*
|
||||||
if(sauce.Character && sauce.Character.fontName) {
|
if(sauce.Character && sauce.Character.fontName) {
|
||||||
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
|
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
|
||||||
@@ -136,56 +139,58 @@ function getArtFromPath(path, options, cb) {
|
|||||||
function getArt(name, options, cb) {
|
function getArt(name, options, cb) {
|
||||||
const ext = paths.extname(name);
|
const ext = paths.extname(name);
|
||||||
|
|
||||||
options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
|
options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
|
||||||
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
|
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
|
||||||
|
|
||||||
// :TODO: make use of asAnsi option and convert from supported -> ansi
|
// :TODO: make use of asAnsi option and convert from supported -> ansi
|
||||||
|
|
||||||
if('' !== ext) {
|
if ('' !== ext) {
|
||||||
options.types = [ ext.toLowerCase() ];
|
options.types = [ext.toLowerCase()];
|
||||||
} else {
|
} else {
|
||||||
if(_.isUndefined(options.types)) {
|
if (_.isUndefined(options.types)) {
|
||||||
options.types = Object.keys(SUPPORTED_ART_TYPES);
|
options.types = Object.keys(SUPPORTED_ART_TYPES);
|
||||||
} else if(_.isString(options.types)) {
|
} else if (_.isString(options.types)) {
|
||||||
options.types = [ options.types.toLowerCase() ];
|
options.types = [options.types.toLowerCase()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an extension is provided, just read the file now
|
// If an extension is provided, just read the file now
|
||||||
if('' !== ext) {
|
if ('' !== ext) {
|
||||||
const directPath = paths.isAbsolute(name) ? name : paths.join(options.basePath, name);
|
const directPath = paths.isAbsolute(name)
|
||||||
|
? name
|
||||||
|
: paths.join(options.basePath, name);
|
||||||
return getArtFromPath(directPath, options, cb);
|
return getArtFromPath(directPath, options, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.readdir(options.basePath, (err, files) => {
|
fs.readdir(options.basePath, (err, files) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = files.filter( file => {
|
const filtered = files.filter(file => {
|
||||||
//
|
//
|
||||||
// Ignore anything not allowed in |options.types|
|
// Ignore anything not allowed in |options.types|
|
||||||
//
|
//
|
||||||
const fext = paths.extname(file);
|
const fext = paths.extname(file);
|
||||||
if(!options.types.includes(fext.toLowerCase())) {
|
if (!options.types.includes(fext.toLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bn = paths.basename(file, fext).toLowerCase();
|
const bn = paths.basename(file, fext).toLowerCase();
|
||||||
if(options.random) {
|
if (options.random) {
|
||||||
const suppliedBn = paths.basename(name, fext).toLowerCase();
|
const suppliedBn = paths.basename(name, fext).toLowerCase();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Random selection enabled. We'll allow for
|
// Random selection enabled. We'll allow for
|
||||||
// basename1.ext, basename2.ext, ...
|
// basename1.ext, basename2.ext, ...
|
||||||
//
|
//
|
||||||
if(!bn.startsWith(suppliedBn)) {
|
if (!bn.startsWith(suppliedBn)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const num = bn.substr(suppliedBn.length);
|
const num = bn.substr(suppliedBn.length);
|
||||||
if(num.length > 0) {
|
if (num.length > 0) {
|
||||||
if(isNaN(parseInt(num, 10))) {
|
if (isNaN(parseInt(num, 10))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +199,7 @@ function getArt(name, options, cb) {
|
|||||||
// We've already validated the extension (above). Must be an exact
|
// We've already validated the extension (above). Must be an exact
|
||||||
// match to basename here
|
// match to basename here
|
||||||
//
|
//
|
||||||
if(bn != paths.basename(name, fext).toLowerCase()) {
|
if (bn != paths.basename(name, fext).toLowerCase()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,15 +207,18 @@ function getArt(name, options, cb) {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if(filtered.length > 0) {
|
if (filtered.length > 0) {
|
||||||
//
|
//
|
||||||
// We should now have:
|
// We should now have:
|
||||||
// - Exactly (1) item in |filtered| if non-random
|
// - Exactly (1) item in |filtered| if non-random
|
||||||
// - 1:n items in |filtered| to choose from if random
|
// - 1:n items in |filtered| to choose from if random
|
||||||
//
|
//
|
||||||
let readPath;
|
let readPath;
|
||||||
if(options.random) {
|
if (options.random) {
|
||||||
readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]);
|
readPath = paths.join(
|
||||||
|
options.basePath,
|
||||||
|
filtered[Math.floor(Math.random() * filtered.length)]
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
assert(1 === filtered.length);
|
assert(1 === filtered.length);
|
||||||
readPath = paths.join(options.basePath, filtered[0]);
|
readPath = paths.join(options.basePath, filtered[0]);
|
||||||
@@ -230,7 +238,7 @@ function defaultEncodingFromExtension(ext) {
|
|||||||
|
|
||||||
function defaultEofFromExtension(ext) {
|
function defaultEofFromExtension(ext) {
|
||||||
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
|
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
|
||||||
if(artType) {
|
if (artType) {
|
||||||
return artType.eof;
|
return artType.eof;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,12 +248,12 @@ function defaultEofFromExtension(ext) {
|
|||||||
// * Cancel (disabled | <keys> )
|
// * Cancel (disabled | <keys> )
|
||||||
// * Resume from pause -> continous (disabled | <keys>)
|
// * Resume from pause -> continous (disabled | <keys>)
|
||||||
function display(client, art, options, cb) {
|
function display(client, art, options, cb) {
|
||||||
if(_.isFunction(options) && !cb) {
|
if (_.isFunction(options) && !cb) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!art || !art.length) {
|
if (!art || !art.length) {
|
||||||
return cb(Errors.Invalid('No art supplied!'));
|
return cb(Errors.Invalid('No art supplied!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,19 +263,19 @@ function display(client, art, options, cb) {
|
|||||||
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
|
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
|
||||||
// 2) CPR driven
|
// 2) CPR driven
|
||||||
|
|
||||||
if(!_.isBoolean(options.iceColors)) {
|
if (!_.isBoolean(options.iceColors)) {
|
||||||
// try to detect from SAUCE
|
// try to detect from SAUCE
|
||||||
if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) {
|
if (_.has(options, 'sauce.ansiFlags') && options.sauce.ansiFlags & (1 << 0)) {
|
||||||
options.iceColors = true;
|
options.iceColors = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ansiParser = new aep.ANSIEscapeParser({
|
const ansiParser = new aep.ANSIEscapeParser({
|
||||||
mciReplaceChar : options.mciReplaceChar,
|
mciReplaceChar: options.mciReplaceChar,
|
||||||
termHeight : client.term.termHeight,
|
termHeight: client.term.termHeight,
|
||||||
termWidth : client.term.termWidth,
|
termWidth: client.term.termWidth,
|
||||||
trailingLF : options.trailingLF,
|
trailingLF: options.trailingLF,
|
||||||
startRow : options.startRow,
|
startRow: options.startRow,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mciMap = {};
|
const mciMap = {};
|
||||||
@@ -275,37 +283,36 @@ function display(client, art, options, cb) {
|
|||||||
|
|
||||||
ansiParser.on('mci', mciInfo => {
|
ansiParser.on('mci', mciInfo => {
|
||||||
// :TODO: ensure generatedId's do not conflict with any existing |id|
|
// :TODO: ensure generatedId's do not conflict with any existing |id|
|
||||||
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
|
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
|
||||||
const mapKey = `${mciInfo.mci}${id}`;
|
const mapKey = `${mciInfo.mci}${id}`;
|
||||||
const mapEntry = mciMap[mapKey];
|
const mapEntry = mciMap[mapKey];
|
||||||
|
|
||||||
if(mapEntry) {
|
if (mapEntry) {
|
||||||
mapEntry.focusSGR = mciInfo.SGR;
|
mapEntry.focusSGR = mciInfo.SGR;
|
||||||
mapEntry.focusArgs = mciInfo.args;
|
mapEntry.focusArgs = mciInfo.args;
|
||||||
} else {
|
} else {
|
||||||
mciMap[mapKey] = {
|
mciMap[mapKey] = {
|
||||||
position : mciInfo.position,
|
position: mciInfo.position,
|
||||||
args : mciInfo.args,
|
args: mciInfo.args,
|
||||||
SGR : mciInfo.SGR,
|
SGR: mciInfo.SGR,
|
||||||
code : mciInfo.mci,
|
code: mciInfo.mci,
|
||||||
id : id,
|
id: id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(!mciInfo.id) {
|
if (!mciInfo.id) {
|
||||||
++generatedId;
|
++generatedId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ansiParser.on('literal', literal => client.term.write(literal, false) );
|
ansiParser.on('literal', literal => client.term.write(literal, false));
|
||||||
ansiParser.on('control', control => client.term.rawWrite(control) );
|
ansiParser.on('control', control => client.term.rawWrite(control));
|
||||||
|
|
||||||
ansiParser.on('complete', () => {
|
ansiParser.on('complete', () => {
|
||||||
ansiParser.removeAllListeners();
|
ansiParser.removeAllListeners();
|
||||||
|
|
||||||
const extraInfo = {
|
const extraInfo = {
|
||||||
height : ansiParser.row - 1,
|
height: ansiParser.row - 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
return cb(null, mciMap, extraInfo);
|
return cb(null, mciMap, extraInfo);
|
||||||
@@ -313,11 +320,11 @@ function display(client, art, options, cb) {
|
|||||||
|
|
||||||
let initSeq = '';
|
let initSeq = '';
|
||||||
if (client.term.syncTermFontsEnabled) {
|
if (client.term.syncTermFontsEnabled) {
|
||||||
if(options.font) {
|
if (options.font) {
|
||||||
initSeq = ansi.setSyncTermFontWithAlias(options.font);
|
initSeq = ansi.setSyncTermFontWithAlias(options.font);
|
||||||
} else if(options.sauce) {
|
} else if (options.sauce) {
|
||||||
let fontName = getFontNameFromSAUCE(options.sauce);
|
let fontName = getFontNameFromSAUCE(options.sauce);
|
||||||
if(fontName) {
|
if (fontName) {
|
||||||
fontName = ansi.getSyncTermFontFromAlias(fontName);
|
fontName = ansi.getSyncTermFontFromAlias(fontName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,18 +334,18 @@ function display(client, art, options, cb) {
|
|||||||
// at a time. This applies to detection only (e.g. SAUCE).
|
// at a time. This applies to detection only (e.g. SAUCE).
|
||||||
// If explicit, we'll set it no matter what (above)
|
// If explicit, we'll set it no matter what (above)
|
||||||
//
|
//
|
||||||
if(fontName && client.term.currentSyncFont != fontName) {
|
if (fontName && client.term.currentSyncFont != fontName) {
|
||||||
client.term.currentSyncFont = fontName;
|
client.term.currentSyncFont = fontName;
|
||||||
initSeq = ansi.setSyncTermFont(fontName);
|
initSeq = ansi.setSyncTermFont(fontName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.iceColors) {
|
if (options.iceColors) {
|
||||||
initSeq += ansi.blinkToBrightIntensity();
|
initSeq += ansi.blinkToBrightIntensity();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(initSeq) {
|
if (initSeq) {
|
||||||
client.term.rawWrite(initSeq);
|
client.term.rawWrite(initSeq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
exports.parseAsset = parseAsset;
|
exports.parseAsset = parseAsset;
|
||||||
exports.getAssetWithShorthand = getAssetWithShorthand;
|
exports.getAssetWithShorthand = getAssetWithShorthand;
|
||||||
exports.getArtAsset = getArtAsset;
|
exports.getArtAsset = getArtAsset;
|
||||||
exports.getModuleAsset = getModuleAsset;
|
exports.getModuleAsset = getModuleAsset;
|
||||||
exports.resolveConfigAsset = resolveConfigAsset;
|
exports.resolveConfigAsset = resolveConfigAsset;
|
||||||
exports.resolveSystemStatAsset = resolveSystemStatAsset;
|
exports.resolveSystemStatAsset = resolveSystemStatAsset;
|
||||||
exports.getViewPropertyAsset = getViewPropertyAsset;
|
exports.getViewPropertyAsset = getViewPropertyAsset;
|
||||||
|
|
||||||
const ALL_ASSETS = [
|
const ALL_ASSETS = [
|
||||||
'art',
|
'art',
|
||||||
@@ -30,18 +30,17 @@ const ALL_ASSETS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ASSET_RE = new RegExp(
|
const ASSET_RE = new RegExp(
|
||||||
'^@(' + ALL_ASSETS.join('|') + ')' +
|
'^@(' + ALL_ASSETS.join('|') + ')' + /:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source
|
||||||
/:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function parseAsset(s) {
|
function parseAsset(s) {
|
||||||
const m = ASSET_RE.exec(s);
|
const m = ASSET_RE.exec(s);
|
||||||
if(m) {
|
if (m) {
|
||||||
const result = { type : m[1] };
|
const result = { type: m[1] };
|
||||||
|
|
||||||
if(m[3]) {
|
if (m[3]) {
|
||||||
result.asset = m[3];
|
result.asset = m[3];
|
||||||
if(m[2]) {
|
if (m[2]) {
|
||||||
result.location = m[2];
|
result.location = m[2];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -53,11 +52,11 @@ function parseAsset(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAssetWithShorthand(spec, defaultType) {
|
function getAssetWithShorthand(spec, defaultType) {
|
||||||
if(!_.isString(spec)) {
|
if (!_.isString(spec)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if('@' === spec[0]) {
|
if ('@' === spec[0]) {
|
||||||
const asset = parseAsset(spec);
|
const asset = parseAsset(spec);
|
||||||
assert(_.isString(asset.type));
|
assert(_.isString(asset.type));
|
||||||
|
|
||||||
@@ -65,43 +64,43 @@ function getAssetWithShorthand(spec, defaultType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type : defaultType,
|
type: defaultType,
|
||||||
asset : spec,
|
asset: spec,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArtAsset(spec) {
|
function getArtAsset(spec) {
|
||||||
const asset = getAssetWithShorthand(spec, 'art');
|
const asset = getAssetWithShorthand(spec, 'art');
|
||||||
|
|
||||||
if(!asset) {
|
if (!asset) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert( ['art', 'method' ].indexOf(asset.type) > -1);
|
assert(['art', 'method'].indexOf(asset.type) > -1);
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModuleAsset(spec) {
|
function getModuleAsset(spec) {
|
||||||
const asset = getAssetWithShorthand(spec, 'systemModule');
|
const asset = getAssetWithShorthand(spec, 'systemModule');
|
||||||
|
|
||||||
if(!asset) {
|
if (!asset) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert( ['userModule', 'systemModule' ].includes(asset.type) );
|
assert(['userModule', 'systemModule'].includes(asset.type));
|
||||||
|
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveConfigAsset(spec) {
|
function resolveConfigAsset(spec) {
|
||||||
const asset = parseAsset(spec);
|
const asset = parseAsset(spec);
|
||||||
if(asset) {
|
if (asset) {
|
||||||
assert('config' === asset.type);
|
assert('config' === asset.type);
|
||||||
|
|
||||||
const path = asset.asset.split('.');
|
const path = asset.asset.split('.');
|
||||||
let conf = Config();
|
let conf = Config();
|
||||||
for(let i = 0; i < path.length; ++i) {
|
for (let i = 0; i < path.length; ++i) {
|
||||||
if(_.isUndefined(conf[path[i]])) {
|
if (_.isUndefined(conf[path[i]])) {
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
conf = conf[path[i]];
|
conf = conf[path[i]];
|
||||||
@@ -114,7 +113,7 @@ function resolveConfigAsset(spec) {
|
|||||||
|
|
||||||
function resolveSystemStatAsset(spec) {
|
function resolveSystemStatAsset(spec) {
|
||||||
const asset = parseAsset(spec);
|
const asset = parseAsset(spec);
|
||||||
if(!asset) {
|
if (!asset) {
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +123,7 @@ function resolveSystemStatAsset(spec) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getViewPropertyAsset(src) {
|
function getViewPropertyAsset(src) {
|
||||||
if(!_.isString(src) || '@' !== src.charAt(0)) {
|
if (!_.isString(src) || '@' !== src.charAt(0)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,60 +2,68 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'User Auto-Sig Editor',
|
name: 'User Auto-Sig Editor',
|
||||||
desc : 'Module for editing auto-sigs',
|
desc: 'Module for editing auto-sigs',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
edit : 0,
|
edit: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
editor : 1,
|
editor: 1,
|
||||||
save : 2,
|
save: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class UserAutoSigEditorModule extends MenuModule {
|
exports.getModule = class UserAutoSigEditorModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs });
|
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
|
||||||
|
extraArgs: options.extraArgs,
|
||||||
|
});
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
saveChanges : (formData, extraArgs, cb) => {
|
saveChanges: (formData, extraArgs, cb) => {
|
||||||
return this.saveChanges(cb);
|
return this.saveChanges(cb);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
return this.prepViewController('edit', FormIds.edit, mciData.menu, callback);
|
return this.prepViewController(
|
||||||
|
'edit',
|
||||||
|
FormIds.edit,
|
||||||
|
mciData.menu,
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
const requiredCodes = [ MciViewIds.editor, MciViewIds.save ];
|
const requiredCodes = [MciViewIds.editor, MciViewIds.save];
|
||||||
return this.validateMCIByViewIds('edit', requiredCodes, callback);
|
return this.validateMCIByViewIds('edit', requiredCodes, callback);
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
const sig = this.client.user.getProperty(UserProps.AutoSignature) || '';
|
const sig =
|
||||||
|
this.client.user.getProperty(UserProps.AutoSignature) || '';
|
||||||
this.setViewText('edit', MciViewIds.editor, sig);
|
this.setViewText('edit', MciViewIds.editor, sig);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -67,8 +75,8 @@ exports.getModule = class UserAutoSigEditorModule extends MenuModule {
|
|||||||
saveChanges(cb) {
|
saveChanges(cb) {
|
||||||
const sig = this.getView('edit', MciViewIds.editor).getData().trim();
|
const sig = this.getView('edit', MciViewIds.editor).getData().trim();
|
||||||
this.client.user.persistProperty(UserProps.AutoSignature, sig, err => {
|
this.client.user.persistProperty(UserProps.AutoSignature, sig, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
this.client.log.error( { error : err.message }, 'Could not save auto-sig');
|
this.client.log.error({ error: err.message }, 'Could not save auto-sig');
|
||||||
}
|
}
|
||||||
return this.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
});
|
});
|
||||||
|
|||||||
174
core/bbs.js
174
core/bbs.js
@@ -16,26 +16,27 @@ const SysLogKeys = require('./system_log.js');
|
|||||||
const UserLogNames = require('./user_log_name');
|
const UserLogNames = require('./user_log_name');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const mkdirs = require('fs-extra').mkdirs;
|
const mkdirs = require('fs-extra').mkdirs;
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
// our main entry point
|
// our main entry point
|
||||||
exports.main = main;
|
exports.main = main;
|
||||||
|
|
||||||
// object with various services we want to de-init/shutdown cleanly if possible
|
// object with various services we want to de-init/shutdown cleanly if possible
|
||||||
const initServices = {};
|
const initServices = {};
|
||||||
|
|
||||||
// only include bbs.js once @ startup; this should be fine
|
// only include bbs.js once @ startup; this should be fine
|
||||||
const COPYRIGHT = fs.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8').split(/\r?\n/g)[0];
|
const COPYRIGHT = fs
|
||||||
|
.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8')
|
||||||
|
.split(/\r?\n/g)[0];
|
||||||
|
|
||||||
const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`;
|
const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`;
|
||||||
const HELP =
|
const HELP = `${FULL_COPYRIGHT}
|
||||||
`${FULL_COPYRIGHT}
|
|
||||||
usage: main.js <args>
|
usage: main.js <args>
|
||||||
eg : main.js --config /enigma_install_path/config/
|
eg : main.js --config /enigma_install_path/config/
|
||||||
|
|
||||||
@@ -62,17 +63,21 @@ function main() {
|
|||||||
function processArgs(callback) {
|
function processArgs(callback) {
|
||||||
const argv = require('minimist')(process.argv.slice(2));
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
|
|
||||||
if(argv.help) {
|
if (argv.help) {
|
||||||
return printHelpAndExit();
|
return printHelpAndExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(argv.version) {
|
if (argv.version) {
|
||||||
return printVersionAndExit();
|
return printVersionAndExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
const configOverridePath = argv.config;
|
const configOverridePath = argv.config;
|
||||||
|
|
||||||
return callback(null, configOverridePath || conf.Config.getDefaultPath(), _.isString(configOverridePath));
|
return callback(
|
||||||
|
null,
|
||||||
|
configOverridePath || conf.Config.getDefaultPath(),
|
||||||
|
_.isString(configOverridePath)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function initConfig(configPath, configPathSupplied, callback) {
|
function initConfig(configPath, configPathSupplied, callback) {
|
||||||
const configFile = configPath + 'config.hjson';
|
const configFile = configPath + 'config.hjson';
|
||||||
@@ -82,12 +87,14 @@ function main() {
|
|||||||
// If the user supplied a path and we can't read/parse it
|
// If the user supplied a path and we can't read/parse it
|
||||||
// then it's a fatal error
|
// then it's a fatal error
|
||||||
//
|
//
|
||||||
if(err) {
|
if (err) {
|
||||||
if('ENOENT' === err.code) {
|
if ('ENOENT' === err.code) {
|
||||||
if(configPathSupplied) {
|
if (configPathSupplied) {
|
||||||
console.error('Configuration file does not exist: ' + configFile);
|
console.error(
|
||||||
|
'Configuration file does not exist: ' + configFile
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
configPathSupplied = null; // make non-fatal; we'll go with defaults
|
configPathSupplied = null; // make non-fatal; we'll go with defaults
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorDisplayed = true;
|
errorDisplayed = true;
|
||||||
@@ -105,26 +112,30 @@ function main() {
|
|||||||
},
|
},
|
||||||
function initSystem(callback) {
|
function initSystem(callback) {
|
||||||
initialize(function init(err) {
|
initialize(function init(err) {
|
||||||
if(err) {
|
if (err) {
|
||||||
console.error('Error initializing: ' + util.inspect(err));
|
console.error('Error initializing: ' + util.inspect(err));
|
||||||
}
|
}
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
// note this is escaped:
|
// note this is escaped:
|
||||||
fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => {
|
fs.readFile(
|
||||||
console.info(FULL_COPYRIGHT);
|
paths.join(__dirname, '../misc/startup_banner.asc'),
|
||||||
if(!err) {
|
'utf8',
|
||||||
console.info(banner);
|
(err, banner) => {
|
||||||
|
console.info(FULL_COPYRIGHT);
|
||||||
|
if (!err) {
|
||||||
|
console.info(banner);
|
||||||
|
}
|
||||||
|
console.info('System started!');
|
||||||
}
|
}
|
||||||
console.info('System started!');
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(err && !errorDisplayed) {
|
if (err && !errorDisplayed) {
|
||||||
console.error('Error initializing: ' + util.inspect(err));
|
console.error('Error initializing: ' + util.inspect(err));
|
||||||
return process.exit();
|
return process.exit();
|
||||||
}
|
}
|
||||||
@@ -143,37 +154,39 @@ function shutdownSystem() {
|
|||||||
const ClientConns = require('./client_connections.js');
|
const ClientConns = require('./client_connections.js');
|
||||||
const activeConnections = ClientConns.getActiveConnections(ClientConns.AllConnections);
|
const activeConnections = ClientConns.getActiveConnections(ClientConns.AllConnections);
|
||||||
let i = activeConnections.length;
|
let i = activeConnections.length;
|
||||||
while(i--) {
|
while (i--) {
|
||||||
const activeTerm = activeConnections[i].term;
|
const activeTerm = activeConnections[i].term;
|
||||||
if(activeTerm) {
|
if (activeTerm) {
|
||||||
activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
|
activeTerm.write(
|
||||||
|
'\n\nServer is shutting down NOW! Disconnecting...\n\n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ClientConns.removeClient(activeConnections[i]);
|
ClientConns.removeClient(activeConnections[i]);
|
||||||
}
|
}
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
function stopListeningServers(callback) {
|
function stopListeningServers(callback) {
|
||||||
return require('./listening_server.js').shutdown( () => {
|
return require('./listening_server.js').shutdown(() => {
|
||||||
return callback(null); // ignore err
|
return callback(null); // ignore err
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function stopEventScheduler(callback) {
|
function stopEventScheduler(callback) {
|
||||||
if(initServices.eventScheduler) {
|
if (initServices.eventScheduler) {
|
||||||
return initServices.eventScheduler.shutdown( () => {
|
return initServices.eventScheduler.shutdown(() => {
|
||||||
return callback(null); // ignore err
|
return callback(null); // ignore err
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function stopFileAreaWeb(callback) {
|
function stopFileAreaWeb(callback) {
|
||||||
require('./file_area_web.js').startup( () => {
|
require('./file_area_web.js').startup(() => {
|
||||||
return callback(null); // ignore err
|
return callback(null); // ignore err
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function stopMsgNetwork(callback) {
|
function stopMsgNetwork(callback) {
|
||||||
require('./msg_network.js').shutdown(callback);
|
require('./msg_network.js').shutdown(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
console.info('Goodbye!');
|
console.info('Goodbye!');
|
||||||
@@ -187,30 +200,39 @@ function initialize(cb) {
|
|||||||
[
|
[
|
||||||
function createMissingDirectories(callback) {
|
function createMissingDirectories(callback) {
|
||||||
const Config = conf.get();
|
const Config = conf.get();
|
||||||
async.each(Object.keys(Config.paths), function entry(pathKey, next) {
|
async.each(
|
||||||
mkdirs(Config.paths[pathKey], function dirCreated(err) {
|
Object.keys(Config.paths),
|
||||||
if(err) {
|
function entry(pathKey, next) {
|
||||||
console.error('Could not create path: ' + Config.paths[pathKey] + ': ' + err.toString());
|
mkdirs(Config.paths[pathKey], function dirCreated(err) {
|
||||||
}
|
if (err) {
|
||||||
return next(err);
|
console.error(
|
||||||
});
|
'Could not create path: ' +
|
||||||
}, function dirCreationComplete(err) {
|
Config.paths[pathKey] +
|
||||||
return callback(err);
|
': ' +
|
||||||
});
|
err.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function dirCreationComplete(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function basicInit(callback) {
|
function basicInit(callback) {
|
||||||
logger.init();
|
logger.init();
|
||||||
logger.log.info(
|
logger.log.info(
|
||||||
{
|
{
|
||||||
version : require('../package.json').version,
|
version: require('../package.json').version,
|
||||||
nodeVersion : process.version,
|
nodeVersion: process.version,
|
||||||
},
|
},
|
||||||
'**** ENiGMA½ Bulletin Board System Starting Up! ****'
|
'**** ENiGMA½ Bulletin Board System Starting Up! ****'
|
||||||
);
|
);
|
||||||
|
|
||||||
process.on('SIGINT', shutdownSystem);
|
process.on('SIGINT', shutdownSystem);
|
||||||
|
|
||||||
require('@breejs/later').date.localTime(); // use local times for later.js/scheduling
|
require('@breejs/later').date.localTime(); // use local times for later.js/scheduling
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
@@ -239,9 +261,12 @@ function initialize(cb) {
|
|||||||
// :TODO: use User.getUserInfo() for this!
|
// :TODO: use User.getUserInfo() for this!
|
||||||
|
|
||||||
const propLoadOpts = {
|
const propLoadOpts = {
|
||||||
names : [
|
names: [
|
||||||
UserProps.RealName, UserProps.Sex, UserProps.EmailAddress,
|
UserProps.RealName,
|
||||||
UserProps.Location, UserProps.Affiliations,
|
UserProps.Sex,
|
||||||
|
UserProps.EmailAddress,
|
||||||
|
UserProps.Location,
|
||||||
|
UserProps.Affiliations,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,15 +276,19 @@ function initialize(cb) {
|
|||||||
return User.getUserName(User.RootUserID, next);
|
return User.getUserName(User.RootUserID, next);
|
||||||
},
|
},
|
||||||
function getOpProps(opUserName, next) {
|
function getOpProps(opUserName, next) {
|
||||||
User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => {
|
User.loadProperties(
|
||||||
return next(err, opUserName, opProps);
|
User.RootUserID,
|
||||||
});
|
propLoadOpts,
|
||||||
|
(err, opProps) => {
|
||||||
|
return next(err, opUserName, opProps);
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
(err, opUserName, opProps) => {
|
(err, opUserName, opProps) => {
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
|
|
||||||
if(err) {
|
if (err) {
|
||||||
propLoadOpts.names.concat('username').forEach(v => {
|
propLoadOpts.names.concat('username').forEach(v => {
|
||||||
StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A');
|
StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A');
|
||||||
});
|
});
|
||||||
@@ -279,14 +308,17 @@ function initialize(cb) {
|
|||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
logName : SysLogKeys.UserLoginHistory,
|
logName: SysLogKeys.UserLoginHistory,
|
||||||
resultType : 'count',
|
resultType: 'count',
|
||||||
date : moment(),
|
date: moment(),
|
||||||
};
|
};
|
||||||
|
|
||||||
StatLog.findSystemLogEntries(filter, (err, callsToday) => {
|
StatLog.findSystemLogEntries(filter, (err, callsToday) => {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
StatLog.setNonPersistentSystemStat(SysProps.LoginsToday, callsToday);
|
StatLog.setNonPersistentSystemStat(
|
||||||
|
SysProps.LoginsToday,
|
||||||
|
callsToday
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(null);
|
return callback(null);
|
||||||
});
|
});
|
||||||
@@ -390,7 +422,8 @@ function initialize(cb) {
|
|||||||
return require('./file_area_web.js').startup(callback);
|
return require('./file_area_web.js').startup(callback);
|
||||||
},
|
},
|
||||||
function readyPasswordReset(callback) {
|
function readyPasswordReset(callback) {
|
||||||
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset;
|
const WebPasswordReset =
|
||||||
|
require('./web_password_reset.js').WebPasswordReset;
|
||||||
return WebPasswordReset.startup(callback);
|
return WebPasswordReset.startup(callback);
|
||||||
},
|
},
|
||||||
function ready2FA_OTPRegister(callback) {
|
function ready2FA_OTPRegister(callback) {
|
||||||
@@ -398,15 +431,16 @@ function initialize(cb) {
|
|||||||
return User2FA_OTPWebRegister.startup(callback);
|
return User2FA_OTPWebRegister.startup(callback);
|
||||||
},
|
},
|
||||||
function readyEventScheduler(callback) {
|
function readyEventScheduler(callback) {
|
||||||
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
const EventSchedulerModule =
|
||||||
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
require('./event_scheduler.js').EventSchedulerModule;
|
||||||
|
EventSchedulerModule.loadAndStart((err, modInst) => {
|
||||||
initServices.eventScheduler = modInst;
|
initServices.eventScheduler = modInst;
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function listenUserEventsForStatLog(callback) {
|
function listenUserEventsForStatLog(callback) {
|
||||||
return require('./stat_log.js').initUserEvents(callback);
|
return require('./stat_log.js').initUserEvents(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function onComplete(err) {
|
function onComplete(err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|||||||
190
core/bbs_link.js
190
core/bbs_link.js
@@ -1,21 +1,18 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const { resetScreen } = require('./ansi_term.js');
|
const { resetScreen } = require('./ansi_term.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const {
|
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
||||||
trackDoorRunBegin,
|
|
||||||
trackDoorRunEnd
|
|
||||||
} = require('./door_util.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Expected configuration block:
|
Expected configuration block:
|
||||||
@@ -42,18 +39,18 @@ const packageJson = require('../package.json');
|
|||||||
// :TODO: ENH: Support nodeMax and tooManyArt
|
// :TODO: ENH: Support nodeMax and tooManyArt
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'BBSLink',
|
name: 'BBSLink',
|
||||||
desc : 'BBSLink Access Module',
|
desc: 'BBSLink Access Module',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class BBSLinkModule extends MenuModule {
|
exports.getModule = class BBSLinkModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.config = options.menuConfig.config;
|
this.config = options.menuConfig.config;
|
||||||
this.config.host = this.config.host || 'games.bbslink.net';
|
this.config.host = this.config.host || 'games.bbslink.net';
|
||||||
this.config.port = this.config.port || 23;
|
this.config.port = this.config.port || 23;
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
@@ -67,12 +64,12 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
return self.validateConfigFields(
|
return self.validateConfigFields(
|
||||||
{
|
{
|
||||||
host : 'string',
|
host: 'string',
|
||||||
sysCode : 'string',
|
sysCode: 'string',
|
||||||
authCode : 'string',
|
authCode: 'string',
|
||||||
schemeCode : 'string',
|
schemeCode: 'string',
|
||||||
door : 'string',
|
door: 'string',
|
||||||
port : 'number',
|
port: 'number',
|
||||||
},
|
},
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
@@ -82,19 +79,26 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||||||
// Acquire an authentication token
|
// Acquire an authentication token
|
||||||
//
|
//
|
||||||
crypto.randomBytes(16, function rand(ex, buf) {
|
crypto.randomBytes(16, function rand(ex, buf) {
|
||||||
if(ex) {
|
if (ex) {
|
||||||
callback(ex);
|
callback(ex);
|
||||||
} else {
|
} else {
|
||||||
randomKey = buf.toString('base64').substr(0, 6);
|
randomKey = buf.toString('base64').substr(0, 6);
|
||||||
self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) {
|
self.simpleHttpRequest(
|
||||||
if(err) {
|
'/token.php?key=' + randomKey,
|
||||||
callback(err);
|
null,
|
||||||
} else {
|
function resp(err, body) {
|
||||||
token = body.trim();
|
if (err) {
|
||||||
self.client.log.trace( { token : token }, 'BBSLink token');
|
callback(err);
|
||||||
callback(null);
|
} else {
|
||||||
|
token = body.trim();
|
||||||
|
self.client.log.trace(
|
||||||
|
{ token: token },
|
||||||
|
'BBSLink token'
|
||||||
|
);
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -103,26 +107,40 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||||||
// Authenticate the token we acquired previously
|
// Authenticate the token we acquired previously
|
||||||
//
|
//
|
||||||
const headers = {
|
const headers = {
|
||||||
'X-User' : self.client.user.userId.toString(),
|
'X-User': self.client.user.userId.toString(),
|
||||||
'X-System' : self.config.sysCode,
|
'X-System': self.config.sysCode,
|
||||||
'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'),
|
'X-Auth': crypto
|
||||||
'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'),
|
.createHash('md5')
|
||||||
'X-Rows' : self.client.term.termHeight.toString(),
|
.update(self.config.authCode + token)
|
||||||
'X-Key' : randomKey,
|
.digest('hex'),
|
||||||
'X-Door' : self.config.door,
|
'X-Code': crypto
|
||||||
'X-Token' : token,
|
.createHash('md5')
|
||||||
'X-Type' : 'enigma-bbs',
|
.update(self.config.schemeCode + token)
|
||||||
'X-Version' : packageJson.version,
|
.digest('hex'),
|
||||||
|
'X-Rows': self.client.term.termHeight.toString(),
|
||||||
|
'X-Key': randomKey,
|
||||||
|
'X-Door': self.config.door,
|
||||||
|
'X-Token': token,
|
||||||
|
'X-Type': 'enigma-bbs',
|
||||||
|
'X-Version': packageJson.version,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) {
|
self.simpleHttpRequest(
|
||||||
var status = body.trim();
|
'/auth.php?key=' + randomKey,
|
||||||
|
headers,
|
||||||
|
function resp(err, body) {
|
||||||
|
var status = body.trim();
|
||||||
|
|
||||||
if('complete' === status) {
|
if ('complete' === status) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
|
}
|
||||||
|
return callback(
|
||||||
|
Errors.AccessDenied(
|
||||||
|
`Bad authentication status: ${status}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(Errors.AccessDenied(`Bad authentication status: ${status}`));
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
function createTelnetBridge(callback) {
|
function createTelnetBridge(callback) {
|
||||||
//
|
//
|
||||||
@@ -130,35 +148,48 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||||||
// bridge from us to them
|
// bridge from us to them
|
||||||
//
|
//
|
||||||
const connectOpts = {
|
const connectOpts = {
|
||||||
port : self.config.port,
|
port: self.config.port,
|
||||||
host : self.config.host,
|
host: self.config.host,
|
||||||
};
|
};
|
||||||
|
|
||||||
let dataOut;
|
let dataOut;
|
||||||
|
|
||||||
self.client.term.write(resetScreen());
|
self.client.term.write(resetScreen());
|
||||||
self.client.term.write(` Connecting to ${self.config.host}, please wait...\n`);
|
self.client.term.write(
|
||||||
|
` Connecting to ${self.config.host}, please wait...\n`
|
||||||
|
);
|
||||||
|
|
||||||
const doorTracking = trackDoorRunBegin(self.client, `bbslink_${self.config.door}`);
|
const doorTracking = trackDoorRunBegin(
|
||||||
|
self.client,
|
||||||
|
`bbslink_${self.config.door}`
|
||||||
|
);
|
||||||
|
|
||||||
const bridgeConnection = net.createConnection(connectOpts, function connected() {
|
const bridgeConnection = net.createConnection(
|
||||||
self.client.log.info(connectOpts, 'BBSLink bridge connection established');
|
connectOpts,
|
||||||
|
function connected() {
|
||||||
|
self.client.log.info(
|
||||||
|
connectOpts,
|
||||||
|
'BBSLink bridge connection established'
|
||||||
|
);
|
||||||
|
|
||||||
dataOut = (data) => {
|
dataOut = data => {
|
||||||
return bridgeConnection.write(data);
|
return bridgeConnection.write(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.client.term.output.on('data', dataOut);
|
self.client.term.output.on('data', dataOut);
|
||||||
|
|
||||||
self.client.once('end', function clientEnd() {
|
self.client.once('end', function clientEnd() {
|
||||||
self.client.log.info('Connection ended. Terminating BBSLink connection');
|
self.client.log.info(
|
||||||
clientTerminated = true;
|
'Connection ended. Terminating BBSLink connection'
|
||||||
bridgeConnection.end();
|
);
|
||||||
});
|
clientTerminated = true;
|
||||||
});
|
bridgeConnection.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const restore = () => {
|
const restore = () => {
|
||||||
if(dataOut && self.client.term.output) {
|
if (dataOut && self.client.term.output) {
|
||||||
self.client.term.output.removeListener('data', dataOut);
|
self.client.term.output.removeListener('data', dataOut);
|
||||||
dataOut = null;
|
dataOut = null;
|
||||||
}
|
}
|
||||||
@@ -174,22 +205,31 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||||||
|
|
||||||
bridgeConnection.on('end', function connectionEnd() {
|
bridgeConnection.on('end', function connectionEnd() {
|
||||||
restore();
|
restore();
|
||||||
return callback(clientTerminated ? Errors.General('Client connection terminated') : null);
|
return callback(
|
||||||
|
clientTerminated
|
||||||
|
? Errors.General('Client connection terminated')
|
||||||
|
: null
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
bridgeConnection.on('error', function error(err) {
|
bridgeConnection.on('error', function error(err) {
|
||||||
self.client.log.info('BBSLink bridge connection error: ' + err.message);
|
self.client.log.info(
|
||||||
|
'BBSLink bridge connection error: ' + err.message
|
||||||
|
);
|
||||||
restore();
|
restore();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.toString() }, 'BBSLink connection error');
|
self.client.log.warn(
|
||||||
|
{ error: err.toString() },
|
||||||
|
'BBSLink connection error'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!clientTerminated) {
|
if (!clientTerminated) {
|
||||||
self.prevMenu();
|
self.prevMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,9 +238,9 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||||||
|
|
||||||
simpleHttpRequest(path, headers, cb) {
|
simpleHttpRequest(path, headers, cb) {
|
||||||
const getOpts = {
|
const getOpts = {
|
||||||
host : this.config.host,
|
host: this.config.host,
|
||||||
path : path,
|
path: path,
|
||||||
headers : headers,
|
headers: headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
const req = http.get(getOpts, function response(resp) {
|
const req = http.get(getOpts, function response(resp) {
|
||||||
|
|||||||
310
core/bbs_list.js
310
core/bbs_list.js
@@ -2,72 +2,69 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
|
|
||||||
const {
|
const { getModDatabasePath, getTransactionDatabase } = require('./database.js');
|
||||||
getModDatabasePath,
|
|
||||||
getTransactionDatabase
|
|
||||||
} = require('./database.js');
|
|
||||||
|
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const theme = require('./theme.js');
|
const theme = require('./theme.js');
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const sqlite3 = require('sqlite3');
|
const sqlite3 = require('sqlite3');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
// :TODO: add notes field
|
// :TODO: add notes field
|
||||||
|
|
||||||
const moduleInfo = exports.moduleInfo = {
|
const moduleInfo = (exports.moduleInfo = {
|
||||||
name : 'BBS List',
|
name: 'BBS List',
|
||||||
desc : 'List of other BBSes',
|
desc: 'List of other BBSes',
|
||||||
author : 'Andrew Pamment',
|
author: 'Andrew Pamment',
|
||||||
packageName : 'com.magickabbs.enigma.bbslist'
|
packageName: 'com.magickabbs.enigma.bbslist',
|
||||||
};
|
});
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
view : {
|
view: {
|
||||||
BBSList : 1,
|
BBSList: 1,
|
||||||
SelectedBBSName : 2,
|
SelectedBBSName: 2,
|
||||||
SelectedBBSSysOp : 3,
|
SelectedBBSSysOp: 3,
|
||||||
SelectedBBSTelnet : 4,
|
SelectedBBSTelnet: 4,
|
||||||
SelectedBBSWww : 5,
|
SelectedBBSWww: 5,
|
||||||
SelectedBBSLoc : 6,
|
SelectedBBSLoc: 6,
|
||||||
SelectedBBSSoftware : 7,
|
SelectedBBSSoftware: 7,
|
||||||
SelectedBBSNotes : 8,
|
SelectedBBSNotes: 8,
|
||||||
SelectedBBSSubmitter : 9,
|
SelectedBBSSubmitter: 9,
|
||||||
|
},
|
||||||
|
add: {
|
||||||
|
BBSName: 1,
|
||||||
|
Sysop: 2,
|
||||||
|
Telnet: 3,
|
||||||
|
Www: 4,
|
||||||
|
Location: 5,
|
||||||
|
Software: 6,
|
||||||
|
Notes: 7,
|
||||||
|
Error: 8,
|
||||||
},
|
},
|
||||||
add : {
|
|
||||||
BBSName : 1,
|
|
||||||
Sysop : 2,
|
|
||||||
Telnet : 3,
|
|
||||||
Www : 4,
|
|
||||||
Location : 5,
|
|
||||||
Software : 6,
|
|
||||||
Notes : 7,
|
|
||||||
Error : 8,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
View : 0,
|
View: 0,
|
||||||
Add : 1,
|
Add: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SELECTED_MCI_NAME_TO_ENTRY = {
|
const SELECTED_MCI_NAME_TO_ENTRY = {
|
||||||
SelectedBBSName : 'bbsName',
|
SelectedBBSName: 'bbsName',
|
||||||
SelectedBBSSysOp : 'sysOp',
|
SelectedBBSSysOp: 'sysOp',
|
||||||
SelectedBBSTelnet : 'telnet',
|
SelectedBBSTelnet: 'telnet',
|
||||||
SelectedBBSWww : 'www',
|
SelectedBBSWww: 'www',
|
||||||
SelectedBBSLoc : 'location',
|
SelectedBBSLoc: 'location',
|
||||||
SelectedBBSSoftware : 'software',
|
SelectedBBSSoftware: 'software',
|
||||||
SelectedBBSSubmitter : 'submitter',
|
SelectedBBSSubmitter: 'submitter',
|
||||||
SelectedBBSSubmitterId : 'submitterUserId',
|
SelectedBBSSubmitterId: 'submitterUserId',
|
||||||
SelectedBBSNotes : 'notes',
|
SelectedBBSNotes: 'notes',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class BBSListModule extends MenuModule {
|
exports.getModule = class BBSListModule extends MenuModule {
|
||||||
@@ -79,10 +76,10 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
//
|
//
|
||||||
// Validators
|
// Validators
|
||||||
//
|
//
|
||||||
viewValidationListener : function(err, cb) {
|
viewValidationListener: function (err, cb) {
|
||||||
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
|
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
|
||||||
if(errMsgView) {
|
if (errMsgView) {
|
||||||
if(err) {
|
if (err) {
|
||||||
errMsgView.setText(err.message);
|
errMsgView.setText(err.message);
|
||||||
} else {
|
} else {
|
||||||
errMsgView.clearText();
|
errMsgView.clearText();
|
||||||
@@ -95,39 +92,48 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
//
|
//
|
||||||
// Key & submit handlers
|
// Key & submit handlers
|
||||||
//
|
//
|
||||||
addBBS : function(formData, extraArgs, cb) {
|
addBBS: function (formData, extraArgs, cb) {
|
||||||
self.displayAddScreen(cb);
|
self.displayAddScreen(cb);
|
||||||
},
|
},
|
||||||
deleteBBS : function(formData, extraArgs, cb) {
|
deleteBBS: function (formData, extraArgs, cb) {
|
||||||
if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) {
|
if (!_.isNumber(self.selectedBBS) || 0 === self.entries.length) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
const entriesView = self.viewControllers.view.getView(
|
||||||
|
MciViewIds.view.BBSList
|
||||||
|
);
|
||||||
|
|
||||||
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
|
if (
|
||||||
|
self.entries[self.selectedBBS].submitterUserId !==
|
||||||
|
self.client.user.userId &&
|
||||||
|
!self.client.user.isSysOp()
|
||||||
|
) {
|
||||||
// must be owner or +op
|
// must be owner or +op
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = self.entries[self.selectedBBS];
|
const entry = self.entries[self.selectedBBS];
|
||||||
if(!entry) {
|
if (!entry) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.database.run(
|
self.database.run(
|
||||||
`DELETE FROM bbs_list
|
`DELETE FROM bbs_list
|
||||||
WHERE id=?;`,
|
WHERE id=?;`,
|
||||||
[ entry.id ],
|
[entry.id],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
self.client.log.error( { err : err }, 'Error deleting from BBS list');
|
self.client.log.error(
|
||||||
|
{ err: err },
|
||||||
|
'Error deleting from BBS list'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
self.entries.splice(self.selectedBBS, 1);
|
self.entries.splice(self.selectedBBS, 1);
|
||||||
|
|
||||||
self.setEntries(entriesView);
|
self.setEntries(entriesView);
|
||||||
|
|
||||||
if(self.entries.length > 0) {
|
if (self.entries.length > 0) {
|
||||||
entriesView.focusPrevious();
|
entriesView.focusPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +144,19 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
submitBBS : function(formData, extraArgs, cb) {
|
submitBBS: function (formData, extraArgs, cb) {
|
||||||
|
|
||||||
let ok = true;
|
let ok = true;
|
||||||
[ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => {
|
['BBSName', 'Sysop', 'Telnet'].forEach(mciName => {
|
||||||
if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) {
|
if (
|
||||||
|
'' ===
|
||||||
|
self.viewControllers.add
|
||||||
|
.getView(MciViewIds.add[mciName])
|
||||||
|
.getData()
|
||||||
|
) {
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(!ok) {
|
if (!ok) {
|
||||||
// validators should prevent this!
|
// validators should prevent this!
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
@@ -155,12 +165,21 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
|
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||||
[
|
[
|
||||||
formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www,
|
formData.value.name,
|
||||||
formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes
|
formData.value.sysop,
|
||||||
|
formData.value.telnet,
|
||||||
|
formData.value.www,
|
||||||
|
formData.value.location,
|
||||||
|
formData.value.software,
|
||||||
|
self.client.user.userId,
|
||||||
|
formData.value.notes,
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.error( { err : err }, 'Error adding to BBS list');
|
self.client.log.error(
|
||||||
|
{ err: err },
|
||||||
|
'Error adding to BBS list'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clearAddForm();
|
self.clearAddForm();
|
||||||
@@ -168,10 +187,10 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cancelSubmit : function(formData, extraArgs, cb) {
|
cancelSubmit: function (formData, extraArgs, cb) {
|
||||||
self.clearAddForm();
|
self.clearAddForm();
|
||||||
self.displayBBSList(true, cb);
|
self.displayBBSList(true, cb);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,10 +203,10 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
},
|
},
|
||||||
function display(callback) {
|
function display(callback) {
|
||||||
self.displayBBSList(false, callback);
|
self.displayBBSList(false, callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
// :TODO: Handle me -- initSequence() should really take a completion callback
|
// :TODO: Handle me -- initSequence() should really take a completion callback
|
||||||
}
|
}
|
||||||
self.finishedLoading();
|
self.finishedLoading();
|
||||||
@@ -196,21 +215,28 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawSelectedEntry(entry) {
|
drawSelectedEntry(entry) {
|
||||||
if(!entry) {
|
if (!entry) {
|
||||||
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
||||||
this.setViewText('view', MciViewIds.view[mciName], '');
|
this.setViewText('view', MciViewIds.view[mciName], '');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
|
const youSubmittedFormat =
|
||||||
|
this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
|
||||||
|
|
||||||
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
||||||
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
|
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
|
||||||
if(MciViewIds.view[mciName]) {
|
if (MciViewIds.view[mciName]) {
|
||||||
|
if (
|
||||||
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) {
|
'SelectedBBSSubmitter' == mciName &&
|
||||||
this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
|
entry.submitterUserId == this.client.user.userId
|
||||||
|
) {
|
||||||
|
this.setViewText(
|
||||||
|
'view',
|
||||||
|
MciViewIds.view.SelectedBBSSubmitter,
|
||||||
|
stringFormat(youSubmittedFormat, entry)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.setViewText('view',MciViewIds.view[mciName], t);
|
this.setViewText('view', MciViewIds.view[mciName], t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -227,7 +253,7 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function clearAndDisplayArt(callback) {
|
function clearAndDisplayArt(callback) {
|
||||||
if(self.viewControllers.add) {
|
if (self.viewControllers.add) {
|
||||||
self.viewControllers.add.setFocus(false);
|
self.viewControllers.add.setFocus(false);
|
||||||
}
|
}
|
||||||
if (clearScreen) {
|
if (clearScreen) {
|
||||||
@@ -236,34 +262,41 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
theme.displayThemedAsset(
|
theme.displayThemedAsset(
|
||||||
self.menuConfig.config.art.entries,
|
self.menuConfig.config.art.entries,
|
||||||
self.client,
|
self.client,
|
||||||
{ font : self.menuConfig.font, trailingLF : false },
|
{ font: self.menuConfig.font, trailingLF: false },
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
return callback(err, artData);
|
return callback(err, artData);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function initOrRedrawViewController(artData, callback) {
|
function initOrRedrawViewController(artData, callback) {
|
||||||
if(_.isUndefined(self.viewControllers.add)) {
|
if (_.isUndefined(self.viewControllers.add)) {
|
||||||
const vc = self.addViewController(
|
const vc = self.addViewController(
|
||||||
'view',
|
'view',
|
||||||
new ViewController( { client : self.client, formId : FormIds.View } )
|
new ViewController({
|
||||||
|
client: self.client,
|
||||||
|
formId: FormIds.View,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadOpts = {
|
const loadOpts = {
|
||||||
callingMenu : self,
|
callingMenu: self,
|
||||||
mciMap : artData.mciMap,
|
mciMap: artData.mciMap,
|
||||||
formId : FormIds.View,
|
formId: FormIds.View,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
} else {
|
} else {
|
||||||
self.viewControllers.view.setFocus(true);
|
self.viewControllers.view.setFocus(true);
|
||||||
self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw();
|
self.viewControllers.view
|
||||||
|
.getView(MciViewIds.view.BBSList)
|
||||||
|
.redraw();
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function fetchEntries(callback) {
|
function fetchEntries(callback) {
|
||||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
const entriesView = self.viewControllers.view.getView(
|
||||||
|
MciViewIds.view.BBSList
|
||||||
|
);
|
||||||
self.entries = [];
|
self.entries = [];
|
||||||
|
|
||||||
self.database.each(
|
self.database.each(
|
||||||
@@ -272,16 +305,16 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
(err, row) => {
|
(err, row) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
self.entries.push({
|
self.entries.push({
|
||||||
text : row.bbs_name, // standard field
|
text: row.bbs_name, // standard field
|
||||||
id : row.id,
|
id: row.id,
|
||||||
bbsName : row.bbs_name,
|
bbsName: row.bbs_name,
|
||||||
sysOp : row.sysop,
|
sysOp: row.sysop,
|
||||||
telnet : row.telnet,
|
telnet: row.telnet,
|
||||||
www : row.www,
|
www: row.www,
|
||||||
location : row.location,
|
location: row.location,
|
||||||
software : row.software,
|
software: row.software,
|
||||||
submitterUserId : row.submitter_user_id,
|
submitterUserId: row.submitter_user_id,
|
||||||
notes : row.notes,
|
notes: row.notes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -291,18 +324,22 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
function getUserNames(entriesView, callback) {
|
function getUserNames(entriesView, callback) {
|
||||||
async.each(self.entries, (entry, next) => {
|
async.each(
|
||||||
User.getUserName(entry.submitterUserId, (err, username) => {
|
self.entries,
|
||||||
if(username) {
|
(entry, next) => {
|
||||||
entry.submitter = username;
|
User.getUserName(entry.submitterUserId, (err, username) => {
|
||||||
} else {
|
if (username) {
|
||||||
entry.submitter = 'N/A';
|
entry.submitter = username;
|
||||||
}
|
} else {
|
||||||
return next();
|
entry.submitter = 'N/A';
|
||||||
});
|
}
|
||||||
}, () => {
|
return next();
|
||||||
return callback(null, entriesView);
|
});
|
||||||
});
|
},
|
||||||
|
() => {
|
||||||
|
return callback(null, entriesView);
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function populateEntries(entriesView, callback) {
|
function populateEntries(entriesView, callback) {
|
||||||
self.setEntries(entriesView);
|
self.setEntries(entriesView);
|
||||||
@@ -312,7 +349,7 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
|
|
||||||
self.drawSelectedEntry(entry);
|
self.drawSelectedEntry(entry);
|
||||||
|
|
||||||
if(!entry) {
|
if (!entry) {
|
||||||
self.selectedBBS = -1;
|
self.selectedBBS = -1;
|
||||||
} else {
|
} else {
|
||||||
self.selectedBBS = idx;
|
self.selectedBBS = idx;
|
||||||
@@ -331,10 +368,10 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
entriesView.redraw();
|
entriesView.redraw();
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,23 +390,26 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
theme.displayThemedAsset(
|
theme.displayThemedAsset(
|
||||||
self.menuConfig.config.art.add,
|
self.menuConfig.config.art.add,
|
||||||
self.client,
|
self.client,
|
||||||
{ font : self.menuConfig.font },
|
{ font: self.menuConfig.font },
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
return callback(err, artData);
|
return callback(err, artData);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function initOrRedrawViewController(artData, callback) {
|
function initOrRedrawViewController(artData, callback) {
|
||||||
if(_.isUndefined(self.viewControllers.add)) {
|
if (_.isUndefined(self.viewControllers.add)) {
|
||||||
const vc = self.addViewController(
|
const vc = self.addViewController(
|
||||||
'add',
|
'add',
|
||||||
new ViewController( { client : self.client, formId : FormIds.Add } )
|
new ViewController({
|
||||||
|
client: self.client,
|
||||||
|
formId: FormIds.Add,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadOpts = {
|
const loadOpts = {
|
||||||
callingMenu : self,
|
callingMenu: self,
|
||||||
mciMap : artData.mciMap,
|
mciMap: artData.mciMap,
|
||||||
formId : FormIds.Add,
|
formId: FormIds.Add,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
@@ -379,10 +419,10 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
self.viewControllers.add.switchFocus(MciViewIds.add.BBSName);
|
self.viewControllers.add.switchFocus(MciViewIds.add.BBSName);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +430,16 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearAddForm() {
|
clearAddForm() {
|
||||||
[ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => {
|
[
|
||||||
|
'BBSName',
|
||||||
|
'Sysop',
|
||||||
|
'Telnet',
|
||||||
|
'Www',
|
||||||
|
'Location',
|
||||||
|
'Software',
|
||||||
|
'Error',
|
||||||
|
'Notes',
|
||||||
|
].forEach(mciName => {
|
||||||
this.setViewText('add', MciViewIds.add[mciName], '');
|
this.setViewText('add', MciViewIds.add[mciName], '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -401,13 +450,12 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function openDatabase(callback) {
|
function openDatabase(callback) {
|
||||||
self.database = getTransactionDatabase(new sqlite3.Database(
|
self.database = getTransactionDatabase(
|
||||||
getModDatabasePath(moduleInfo),
|
new sqlite3.Database(getModDatabasePath(moduleInfo), callback)
|
||||||
callback
|
);
|
||||||
));
|
|
||||||
},
|
},
|
||||||
function createTables(callback) {
|
function createTables(callback) {
|
||||||
self.database.serialize( () => {
|
self.database.serialize(() => {
|
||||||
self.database.run(
|
self.database.run(
|
||||||
`CREATE TABLE IF NOT EXISTS bbs_list (
|
`CREATE TABLE IF NOT EXISTS bbs_list (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
@@ -423,7 +471,7 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const TextView = require('./text_view.js').TextView;
|
const TextView = require('./text_view.js').TextView;
|
||||||
const miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
|
||||||
exports.ButtonView = ButtonView;
|
exports.ButtonView = ButtonView;
|
||||||
|
|
||||||
function ButtonView(options) {
|
function ButtonView(options) {
|
||||||
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
||||||
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
||||||
options.justify = miscUtil.valueWithDefault(options.justify, 'center');
|
options.justify = miscUtil.valueWithDefault(options.justify, 'center');
|
||||||
options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide');
|
options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide');
|
||||||
|
|
||||||
TextView.call(this, options);
|
TextView.call(this, options);
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ function ButtonView(options) {
|
|||||||
|
|
||||||
util.inherits(ButtonView, TextView);
|
util.inherits(ButtonView, TextView);
|
||||||
|
|
||||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
ButtonView.prototype.onKeyPress = function (ch, key) {
|
||||||
if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) {
|
if (this.isKeyMapped('accept', key ? key.name : ch) || ' ' === ch) {
|
||||||
this.submitData = 'accept';
|
this.submitData = 'accept';
|
||||||
this.emit('action', 'accept');
|
this.emit('action', 'accept');
|
||||||
delete this.submitData;
|
delete this.submitData;
|
||||||
@@ -30,6 +30,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ButtonView.prototype.getData = function() {
|
ButtonView.prototype.getData = function () {
|
||||||
return this.submitData || null;
|
return this.submitData || null;
|
||||||
};
|
};
|
||||||
|
|||||||
488
core/client.js
488
core/client.js
@@ -32,22 +32,22 @@
|
|||||||
----/snip/----------------------
|
----/snip/----------------------
|
||||||
*/
|
*/
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const term = require('./client_term.js');
|
const term = require('./client_term.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const MenuStack = require('./menu_stack.js');
|
const MenuStack = require('./menu_stack.js');
|
||||||
const ACS = require('./acs.js');
|
const ACS = require('./acs.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.Client = Client;
|
exports.Client = Client;
|
||||||
|
|
||||||
// :TODO: Move all of the key stuff to it's own module
|
// :TODO: Move all of the key stuff to it's own module
|
||||||
|
|
||||||
@@ -56,86 +56,93 @@ exports.Client = Client;
|
|||||||
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html
|
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html
|
||||||
//
|
//
|
||||||
/* eslint-disable no-control-regex */
|
/* eslint-disable no-control-regex */
|
||||||
const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/;
|
const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/;
|
||||||
const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/;
|
const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/;
|
||||||
const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/;
|
const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/;
|
||||||
const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$');
|
const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$');
|
||||||
const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [
|
const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp(
|
||||||
'(\\d+)(?:;(\\d+))?([~^$])',
|
'(?:\u001b+)(O|N|\\[|\\[\\[)(?:' +
|
||||||
'(?:M([@ #!a`])(.)(.))', // mouse stuff
|
[
|
||||||
'(?:1;)?(\\d+)?([a-zA-Z@])'
|
'(\\d+)(?:;(\\d+))?([~^$])',
|
||||||
].join('|') + ')');
|
'(?:M([@ #!a`])(.)(.))', // mouse stuff
|
||||||
|
'(?:1;)?(\\d+)?([a-zA-Z@])',
|
||||||
|
].join('|') +
|
||||||
|
')'
|
||||||
|
);
|
||||||
/* eslint-enable no-control-regex */
|
/* eslint-enable no-control-regex */
|
||||||
|
|
||||||
const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
|
const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
|
||||||
const RE_ESC_CODE_ANYWHERE = new RegExp( [
|
const RE_ESC_CODE_ANYWHERE = new RegExp(
|
||||||
RE_FUNCTION_KEYCODE_ANYWHERE.source,
|
[
|
||||||
RE_META_KEYCODE_ANYWHERE.source,
|
RE_FUNCTION_KEYCODE_ANYWHERE.source,
|
||||||
RE_DSR_RESPONSE_ANYWHERE.source,
|
RE_META_KEYCODE_ANYWHERE.source,
|
||||||
RE_DEV_ATTR_RESPONSE_ANYWHERE.source,
|
RE_DSR_RESPONSE_ANYWHERE.source,
|
||||||
/\u001b./.source // eslint-disable-line no-control-regex
|
RE_DEV_ATTR_RESPONSE_ANYWHERE.source,
|
||||||
].join('|'));
|
/\u001b./.source, // eslint-disable-line no-control-regex
|
||||||
|
].join('|')
|
||||||
|
);
|
||||||
|
|
||||||
function Client(/*input, output*/) {
|
function Client(/*input, output*/) {
|
||||||
stream.call(this);
|
stream.call(this);
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
this.user = new User();
|
this.user = new User();
|
||||||
this.currentThemeConfig = { info : { name : 'N/A', description : 'None' } };
|
this.currentThemeConfig = { info: { name: 'N/A', description: 'None' } };
|
||||||
this.lastActivityTime = Date.now();
|
this.lastActivityTime = Date.now();
|
||||||
this.menuStack = new MenuStack(this);
|
this.menuStack = new MenuStack(this);
|
||||||
this.acs = new ACS( { client : this, user : this.user } );
|
this.acs = new ACS({ client: this, user: this.user });
|
||||||
this.interruptQueue = new UserInterruptQueue(this);
|
this.interruptQueue = new UserInterruptQueue(this);
|
||||||
|
|
||||||
Object.defineProperty(this, 'currentTheme', {
|
Object.defineProperty(this, 'currentTheme', {
|
||||||
get : () => {
|
get: () => {
|
||||||
if (this.currentThemeConfig) {
|
if (this.currentThemeConfig) {
|
||||||
return this.currentThemeConfig.get();
|
return this.currentThemeConfig.get();
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
info : {
|
info: {
|
||||||
name : 'N/A',
|
name: 'N/A',
|
||||||
author : 'N/A',
|
author: 'N/A',
|
||||||
description : 'N/A',
|
description: 'N/A',
|
||||||
group : 'N/A',
|
group: 'N/A',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set : (theme) => {
|
set: theme => {
|
||||||
this.currentThemeConfig = theme;
|
this.currentThemeConfig = theme;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(this, 'node', {
|
Object.defineProperty(this, 'node', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return self.session.id;
|
return self.session.id;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(this, 'currentMenuModule', {
|
Object.defineProperty(this, 'currentMenuModule', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return self.menuStack.currentModule;
|
return self.menuStack.currentModule;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setTemporaryDirectDataHandler = function(handler) {
|
this.setTemporaryDirectDataHandler = function (handler) {
|
||||||
this.dataPassthrough = true; // let implementations do with what they will here
|
this.dataPassthrough = true; // let implementations do with what they will here
|
||||||
this.input.removeAllListeners('data');
|
this.input.removeAllListeners('data');
|
||||||
this.input.on('data', handler);
|
this.input.on('data', handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.restoreDataHandler = function() {
|
this.restoreDataHandler = function () {
|
||||||
this.dataPassthrough = false;
|
this.dataPassthrough = false;
|
||||||
this.input.removeAllListeners('data');
|
this.input.removeAllListeners('data');
|
||||||
this.input.on('data', this.dataHandler);
|
this.input.on('data', this.dataHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.themeChangedListener = function( { themeId } ) {
|
this.themeChangedListener = function ({ themeId }) {
|
||||||
if(_.get(self.currentTheme, 'info.themeId') === themeId) {
|
if (_.get(self.currentTheme, 'info.themeId') === themeId) {
|
||||||
self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId);
|
self.currentThemeConfig = require('./theme.js')
|
||||||
|
.getAvailableThemes()
|
||||||
|
.get(themeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,14 +158,14 @@ function Client(/*input, output*/) {
|
|||||||
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html
|
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html
|
||||||
// * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/
|
// * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/
|
||||||
//
|
//
|
||||||
this.getTermClient = function(deviceAttr) {
|
this.getTermClient = function (deviceAttr) {
|
||||||
let termClient = {
|
let termClient = {
|
||||||
'63;1;2' : 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android)
|
'63;1;2': 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android)
|
||||||
'50;86;84;88' : 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
|
'50;86;84;88': 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
|
||||||
}[deviceAttr];
|
}[deviceAttr];
|
||||||
|
|
||||||
if(!termClient) {
|
if (!termClient) {
|
||||||
if(_.startsWith(deviceAttr, '67;84;101;114;109')) {
|
if (_.startsWith(deviceAttr, '67;84;101;114;109')) {
|
||||||
//
|
//
|
||||||
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
|
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
|
||||||
//
|
//
|
||||||
@@ -173,176 +180,178 @@ function Client(/*input, output*/) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable no-control-regex */
|
/* eslint-disable no-control-regex */
|
||||||
this.isMouseInput = function(data) {
|
this.isMouseInput = function (data) {
|
||||||
return /\x1b\[M/.test(data) ||
|
return (
|
||||||
|
/\x1b\[M/.test(data) ||
|
||||||
/\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) ||
|
/\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) ||
|
||||||
/\u001b\[(\d+;\d+;\d+)M/.test(data) ||
|
/\u001b\[(\d+;\d+;\d+)M/.test(data) ||
|
||||||
/\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) ||
|
/\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) ||
|
||||||
/\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) ||
|
/\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) ||
|
||||||
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) ||
|
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) ||
|
||||||
/\u001b\[(O|I)/.test(data);
|
/\u001b\[(O|I)/.test(data)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
/* eslint-enable no-control-regex */
|
/* eslint-enable no-control-regex */
|
||||||
|
|
||||||
this.getKeyComponentsFromCode = function(code) {
|
this.getKeyComponentsFromCode = function (code) {
|
||||||
return {
|
return {
|
||||||
// xterm/gnome
|
// xterm/gnome
|
||||||
'OP' : { name : 'f1' },
|
OP: { name: 'f1' },
|
||||||
'OQ' : { name : 'f2' },
|
OQ: { name: 'f2' },
|
||||||
'OR' : { name : 'f3' },
|
OR: { name: 'f3' },
|
||||||
'OS' : { name : 'f4' },
|
OS: { name: 'f4' },
|
||||||
|
|
||||||
'OA' : { name : 'up arrow' },
|
OA: { name: 'up arrow' },
|
||||||
'OB' : { name : 'down arrow' },
|
OB: { name: 'down arrow' },
|
||||||
'OC' : { name : 'right arrow' },
|
OC: { name: 'right arrow' },
|
||||||
'OD' : { name : 'left arrow' },
|
OD: { name: 'left arrow' },
|
||||||
'OE' : { name : 'clear' },
|
OE: { name: 'clear' },
|
||||||
'OF' : { name : 'end' },
|
OF: { name: 'end' },
|
||||||
'OH' : { name : 'home' },
|
OH: { name: 'home' },
|
||||||
|
|
||||||
// xterm/rxvt
|
// xterm/rxvt
|
||||||
'[11~' : { name : 'f1' },
|
'[11~': { name: 'f1' },
|
||||||
'[12~' : { name : 'f2' },
|
'[12~': { name: 'f2' },
|
||||||
'[13~' : { name : 'f3' },
|
'[13~': { name: 'f3' },
|
||||||
'[14~' : { name : 'f4' },
|
'[14~': { name: 'f4' },
|
||||||
|
|
||||||
'[1~' : { name : 'home' },
|
'[1~': { name: 'home' },
|
||||||
'[2~' : { name : 'insert' },
|
'[2~': { name: 'insert' },
|
||||||
'[3~' : { name : 'delete' },
|
'[3~': { name: 'delete' },
|
||||||
'[4~' : { name : 'end' },
|
'[4~': { name: 'end' },
|
||||||
'[5~' : { name : 'page up' },
|
'[5~': { name: 'page up' },
|
||||||
'[6~' : { name : 'page down' },
|
'[6~': { name: 'page down' },
|
||||||
|
|
||||||
// Cygwin & libuv
|
// Cygwin & libuv
|
||||||
'[[A' : { name : 'f1' },
|
'[[A': { name: 'f1' },
|
||||||
'[[B' : { name : 'f2' },
|
'[[B': { name: 'f2' },
|
||||||
'[[C' : { name : 'f3' },
|
'[[C': { name: 'f3' },
|
||||||
'[[D' : { name : 'f4' },
|
'[[D': { name: 'f4' },
|
||||||
'[[E' : { name : 'f5' },
|
'[[E': { name: 'f5' },
|
||||||
|
|
||||||
// Common impls
|
// Common impls
|
||||||
'[15~' : { name : 'f5' },
|
'[15~': { name: 'f5' },
|
||||||
'[17~' : { name : 'f6' },
|
'[17~': { name: 'f6' },
|
||||||
'[18~' : { name : 'f7' },
|
'[18~': { name: 'f7' },
|
||||||
'[19~' : { name : 'f8' },
|
'[19~': { name: 'f8' },
|
||||||
'[20~' : { name : 'f9' },
|
'[20~': { name: 'f9' },
|
||||||
'[21~' : { name : 'f10' },
|
'[21~': { name: 'f10' },
|
||||||
'[23~' : { name : 'f11' },
|
'[23~': { name: 'f11' },
|
||||||
'[24~' : { name : 'f12' },
|
'[24~': { name: 'f12' },
|
||||||
|
|
||||||
// xterm
|
// xterm
|
||||||
'[A' : { name : 'up arrow' },
|
'[A': { name: 'up arrow' },
|
||||||
'[B' : { name : 'down arrow' },
|
'[B': { name: 'down arrow' },
|
||||||
'[C' : { name : 'right arrow' },
|
'[C': { name: 'right arrow' },
|
||||||
'[D' : { name : 'left arrow' },
|
'[D': { name: 'left arrow' },
|
||||||
'[E' : { name : 'clear' },
|
'[E': { name: 'clear' },
|
||||||
'[F' : { name : 'end' },
|
'[F': { name: 'end' },
|
||||||
'[H' : { name : 'home' },
|
'[H': { name: 'home' },
|
||||||
|
|
||||||
// PuTTY
|
// PuTTY
|
||||||
'[[5~' : { name : 'page up' },
|
'[[5~': { name: 'page up' },
|
||||||
'[[6~' : { name : 'page down' },
|
'[[6~': { name: 'page down' },
|
||||||
|
|
||||||
// rvxt
|
// rvxt
|
||||||
'[7~' : { name : 'home' },
|
'[7~': { name: 'home' },
|
||||||
'[8~' : { name : 'end' },
|
'[8~': { name: 'end' },
|
||||||
|
|
||||||
// rxvt with modifiers
|
// rxvt with modifiers
|
||||||
'[a' : { name : 'up arrow', shift : true },
|
'[a': { name: 'up arrow', shift: true },
|
||||||
'[b' : { name : 'down arrow', shift : true },
|
'[b': { name: 'down arrow', shift: true },
|
||||||
'[c' : { name : 'right arrow', shift : true },
|
'[c': { name: 'right arrow', shift: true },
|
||||||
'[d' : { name : 'left arrow', shift : true },
|
'[d': { name: 'left arrow', shift: true },
|
||||||
'[e' : { name : 'clear', shift : true },
|
'[e': { name: 'clear', shift: true },
|
||||||
|
|
||||||
'[2$' : { name : 'insert', shift : true },
|
'[2$': { name: 'insert', shift: true },
|
||||||
'[3$' : { name : 'delete', shift : true },
|
'[3$': { name: 'delete', shift: true },
|
||||||
'[5$' : { name : 'page up', shift : true },
|
'[5$': { name: 'page up', shift: true },
|
||||||
'[6$' : { name : 'page down', shift : true },
|
'[6$': { name: 'page down', shift: true },
|
||||||
'[7$' : { name : 'home', shift : true },
|
'[7$': { name: 'home', shift: true },
|
||||||
'[8$' : { name : 'end', shift : true },
|
'[8$': { name: 'end', shift: true },
|
||||||
|
|
||||||
'Oa' : { name : 'up arrow', ctrl : true },
|
Oa: { name: 'up arrow', ctrl: true },
|
||||||
'Ob' : { name : 'down arrow', ctrl : true },
|
Ob: { name: 'down arrow', ctrl: true },
|
||||||
'Oc' : { name : 'right arrow', ctrl : true },
|
Oc: { name: 'right arrow', ctrl: true },
|
||||||
'Od' : { name : 'left arrow', ctrl : true },
|
Od: { name: 'left arrow', ctrl: true },
|
||||||
'Oe' : { name : 'clear', ctrl : true },
|
Oe: { name: 'clear', ctrl: true },
|
||||||
|
|
||||||
'[2^' : { name : 'insert', ctrl : true },
|
'[2^': { name: 'insert', ctrl: true },
|
||||||
'[3^' : { name : 'delete', ctrl : true },
|
'[3^': { name: 'delete', ctrl: true },
|
||||||
'[5^' : { name : 'page up', ctrl : true },
|
'[5^': { name: 'page up', ctrl: true },
|
||||||
'[6^' : { name : 'page down', ctrl : true },
|
'[6^': { name: 'page down', ctrl: true },
|
||||||
'[7^' : { name : 'home', ctrl : true },
|
'[7^': { name: 'home', ctrl: true },
|
||||||
'[8^' : { name : 'end', ctrl : true },
|
'[8^': { name: 'end', ctrl: true },
|
||||||
|
|
||||||
// SyncTERM / EtherTerm
|
// SyncTERM / EtherTerm
|
||||||
'[K' : { name : 'end' },
|
'[K': { name: 'end' },
|
||||||
'[@' : { name : 'insert' },
|
'[@': { name: 'insert' },
|
||||||
'[V' : { name : 'page up' },
|
'[V': { name: 'page up' },
|
||||||
'[U' : { name : 'page down' },
|
'[U': { name: 'page down' },
|
||||||
|
|
||||||
// other
|
// other
|
||||||
'[Z' : { name : 'tab', shift : true },
|
'[Z': { name: 'tab', shift: true },
|
||||||
}[code];
|
}[code];
|
||||||
};
|
};
|
||||||
|
|
||||||
this.on('data', function clientData(data) {
|
this.on('data', function clientData(data) {
|
||||||
// create a uniform format that can be parsed below
|
// create a uniform format that can be parsed below
|
||||||
if(data[0] > 127 && undefined === data[1]) {
|
if (data[0] > 127 && undefined === data[1]) {
|
||||||
data[0] -= 128;
|
data[0] -= 128;
|
||||||
data = '\u001b' + data.toString('utf-8');
|
data = '\u001b' + data.toString('utf-8');
|
||||||
} else {
|
} else {
|
||||||
data = data.toString('utf-8');
|
data = data.toString('utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(self.isMouseInput(data)) {
|
if (self.isMouseInput(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf = [];
|
var buf = [];
|
||||||
var m;
|
var m;
|
||||||
while((m = RE_ESC_CODE_ANYWHERE.exec(data))) {
|
while ((m = RE_ESC_CODE_ANYWHERE.exec(data))) {
|
||||||
buf = buf.concat(data.slice(0, m.index).split(''));
|
buf = buf.concat(data.slice(0, m.index).split(''));
|
||||||
buf.push(m[0]);
|
buf.push(m[0]);
|
||||||
data = data.slice(m.index + m[0].length);
|
data = data.slice(m.index + m[0].length);
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = buf.concat(data.split('')); // remainder
|
buf = buf.concat(data.split('')); // remainder
|
||||||
|
|
||||||
buf.forEach(function bufPart(s) {
|
buf.forEach(function bufPart(s) {
|
||||||
var key = {
|
var key = {
|
||||||
seq : s,
|
seq: s,
|
||||||
name : undefined,
|
name: undefined,
|
||||||
ctrl : false,
|
ctrl: false,
|
||||||
meta : false,
|
meta: false,
|
||||||
shift : false,
|
shift: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
var parts;
|
var parts;
|
||||||
|
|
||||||
if((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) {
|
if ((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) {
|
||||||
if('R' === parts[2]) {
|
if ('R' === parts[2]) {
|
||||||
const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) );
|
const cprArgs = parts[1].split(';').map(v => parseInt(v, 10) || 0);
|
||||||
if(2 === cprArgs.length) {
|
if (2 === cprArgs.length) {
|
||||||
if(self.cprOffset) {
|
if (self.cprOffset) {
|
||||||
cprArgs[0] = cprArgs[0] + self.cprOffset;
|
cprArgs[0] = cprArgs[0] + self.cprOffset;
|
||||||
cprArgs[1] = cprArgs[1] + self.cprOffset;
|
cprArgs[1] = cprArgs[1] + self.cprOffset;
|
||||||
}
|
}
|
||||||
self.emit('cursor position report', cprArgs);
|
self.emit('cursor position report', cprArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) {
|
} else if ((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) {
|
||||||
assert('c' === parts[2]);
|
assert('c' === parts[2]);
|
||||||
var termClient = self.getTermClient(parts[1]);
|
var termClient = self.getTermClient(parts[1]);
|
||||||
if(termClient) {
|
if (termClient) {
|
||||||
self.term.termClient = termClient;
|
self.term.termClient = termClient;
|
||||||
}
|
}
|
||||||
} else if('\r' === s) {
|
} else if ('\r' === s) {
|
||||||
key.name = 'return';
|
key.name = 'return';
|
||||||
} else if('\n' === s) {
|
} else if ('\n' === s) {
|
||||||
key.name = 'line feed';
|
key.name = 'line feed';
|
||||||
} else if('\t' === s) {
|
} else if ('\t' === s) {
|
||||||
key.name = 'tab';
|
key.name = 'tab';
|
||||||
} else if('\x7f' === s) {
|
} else if ('\x7f' === s) {
|
||||||
//
|
//
|
||||||
// Backspace vs delete is a crazy thing, especially in *nix.
|
// Backspace vs delete is a crazy thing, especially in *nix.
|
||||||
// - ANSI-BBS uses 0x7f for DEL
|
// - ANSI-BBS uses 0x7f for DEL
|
||||||
@@ -351,61 +360,63 @@ function Client(/*input, output*/) {
|
|||||||
// See http://www.hypexr.org/linux_ruboff.php
|
// See http://www.hypexr.org/linux_ruboff.php
|
||||||
// And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
|
// And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
|
||||||
//
|
//
|
||||||
if(self.term.isNixTerm()) {
|
if (self.term.isNixTerm()) {
|
||||||
key.name = 'backspace';
|
key.name = 'backspace';
|
||||||
} else {
|
} else {
|
||||||
key.name = 'delete';
|
key.name = 'delete';
|
||||||
}
|
}
|
||||||
} else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
|
} else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
|
||||||
// backspace, CTRL-H
|
// backspace, CTRL-H
|
||||||
key.name = 'backspace';
|
key.name = 'backspace';
|
||||||
key.meta = ('\x1b' === s.charAt(0));
|
key.meta = '\x1b' === s.charAt(0);
|
||||||
} else if('\x1b' === s || '\x1b\x1b' === s) {
|
} else if ('\x1b' === s || '\x1b\x1b' === s) {
|
||||||
key.name = 'escape';
|
key.name = 'escape';
|
||||||
key.meta = (2 === s.length);
|
key.meta = 2 === s.length;
|
||||||
} else if (' ' === s || '\x1b ' === s) {
|
} else if (' ' === s || '\x1b ' === s) {
|
||||||
// rather annoying that space can come in other than just " "
|
// rather annoying that space can come in other than just " "
|
||||||
key.name = 'space';
|
key.name = 'space';
|
||||||
key.meta = (2 === s.length);
|
key.meta = 2 === s.length;
|
||||||
} else if(1 === s.length && s <= '\x1a') {
|
} else if (1 === s.length && s <= '\x1a') {
|
||||||
// CTRL-<letter>
|
// CTRL-<letter>
|
||||||
key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
|
key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
|
||||||
key.ctrl = true;
|
key.ctrl = true;
|
||||||
} else if(1 === s.length && s >= 'a' && s <= 'z') {
|
} else if (1 === s.length && s >= 'a' && s <= 'z') {
|
||||||
// normal, lowercased letter
|
// normal, lowercased letter
|
||||||
key.name = s;
|
key.name = s;
|
||||||
} else if(1 === s.length && s >= 'A' && s <= 'Z') {
|
} else if (1 === s.length && s >= 'A' && s <= 'Z') {
|
||||||
key.name = s.toLowerCase();
|
key.name = s.toLowerCase();
|
||||||
key.shift = true;
|
key.shift = true;
|
||||||
} else if ((parts = RE_META_KEYCODE.exec(s))) {
|
} else if ((parts = RE_META_KEYCODE.exec(s))) {
|
||||||
// meta with character key
|
// meta with character key
|
||||||
key.name = parts[1].toLowerCase();
|
key.name = parts[1].toLowerCase();
|
||||||
key.meta = true;
|
key.meta = true;
|
||||||
key.shift = /^[A-Z]$/.test(parts[1]);
|
key.shift = /^[A-Z]$/.test(parts[1]);
|
||||||
} else if((parts = RE_FUNCTION_KEYCODE.exec(s))) {
|
} else if ((parts = RE_FUNCTION_KEYCODE.exec(s))) {
|
||||||
var code =
|
var code =
|
||||||
(parts[1] || '') + (parts[2] || '') +
|
(parts[1] || '') +
|
||||||
(parts[4] || '') + (parts[9] || '');
|
(parts[2] || '') +
|
||||||
|
(parts[4] || '') +
|
||||||
|
(parts[9] || '');
|
||||||
|
|
||||||
var modifier = (parts[3] || parts[8] || 1) - 1;
|
var modifier = (parts[3] || parts[8] || 1) - 1;
|
||||||
|
|
||||||
key.ctrl = !!(modifier & 4);
|
key.ctrl = !!(modifier & 4);
|
||||||
key.meta = !!(modifier & 10);
|
key.meta = !!(modifier & 10);
|
||||||
key.shift = !!(modifier & 1);
|
key.shift = !!(modifier & 1);
|
||||||
key.code = code;
|
key.code = code;
|
||||||
|
|
||||||
_.assign(key, self.getKeyComponentsFromCode(code));
|
_.assign(key, self.getKeyComponentsFromCode(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
var ch;
|
var ch;
|
||||||
if(1 === s.length) {
|
if (1 === s.length) {
|
||||||
ch = s;
|
ch = s;
|
||||||
} else if('space' === key.name) {
|
} else if ('space' === key.name) {
|
||||||
// stupid hack to always get space as a regular char
|
// stupid hack to always get space as a regular char
|
||||||
ch = ' ';
|
ch = ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isUndefined(key.name)) {
|
if (_.isUndefined(key.name)) {
|
||||||
key = undefined;
|
key = undefined;
|
||||||
} else {
|
} else {
|
||||||
//
|
//
|
||||||
@@ -418,14 +429,14 @@ function Client(/*input, output*/) {
|
|||||||
key.name;
|
key.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key || ch) {
|
if (key || ch) {
|
||||||
if(Config().logging.traceUserKeyboardInput) {
|
if (Config().logging.traceUserKeyboardInput) {
|
||||||
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
|
self.log.trace({ key: key, ch: escape(ch) }, 'User keyboard input'); // jshint ignore:line
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lastActivityTime = Date.now();
|
self.lastActivityTime = Date.now();
|
||||||
|
|
||||||
if(!self.ignoreInput) {
|
if (!self.ignoreInput) {
|
||||||
self.emit('key press', ch, key);
|
self.emit('key press', ch, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,23 +446,23 @@ function Client(/*input, output*/) {
|
|||||||
|
|
||||||
require('util').inherits(Client, stream);
|
require('util').inherits(Client, stream);
|
||||||
|
|
||||||
Client.prototype.setInputOutput = function(input, output) {
|
Client.prototype.setInputOutput = function (input, output) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.output = output;
|
this.output = output;
|
||||||
|
|
||||||
this.term = new term.ClientTerminal(this.output);
|
this.term = new term.ClientTerminal(this.output);
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.setTermType = function(termType) {
|
Client.prototype.setTermType = function (termType) {
|
||||||
this.term.env.TERM = termType;
|
this.term.env.TERM = termType;
|
||||||
this.term.termType = termType;
|
this.term.termType = termType;
|
||||||
|
|
||||||
this.log.debug( { termType : termType }, 'Set terminal type');
|
this.log.debug({ termType: termType }, 'Set terminal type');
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.startIdleMonitor = function() {
|
Client.prototype.startIdleMonitor = function () {
|
||||||
// clear existing, if any
|
// clear existing, if any
|
||||||
if(this.idleCheck) {
|
if (this.idleCheck) {
|
||||||
this.stopIdleMonitor();
|
this.stopIdleMonitor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,11 +473,11 @@ Client.prototype.startIdleMonitor = function() {
|
|||||||
// We also update minutes spent online the system here,
|
// We also update minutes spent online the system here,
|
||||||
// if we have a authenticated user.
|
// if we have a authenticated user.
|
||||||
//
|
//
|
||||||
this.idleCheck = setInterval( () => {
|
this.idleCheck = setInterval(() => {
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
|
|
||||||
let idleLogoutSeconds;
|
let idleLogoutSeconds;
|
||||||
if(this.user.isAuthenticated()) {
|
if (this.user.isAuthenticated()) {
|
||||||
idleLogoutSeconds = Config().users.idleLogoutSeconds;
|
idleLogoutSeconds = Config().users.idleLogoutSeconds;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -474,17 +485,17 @@ Client.prototype.startIdleMonitor = function() {
|
|||||||
// every user, but want at least some updates for various things
|
// every user, but want at least some updates for various things
|
||||||
// such as achievements. Send off every 5m.
|
// such as achievements. Send off every 5m.
|
||||||
//
|
//
|
||||||
const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1);
|
const minOnline = this.user.incrementProperty(
|
||||||
if(0 === (minOnline % 5)) {
|
UserProps.MinutesOnlineTotalCount,
|
||||||
Events.emit(
|
1
|
||||||
Events.getSystemEvents().UserStatIncrement,
|
);
|
||||||
{
|
if (0 === minOnline % 5) {
|
||||||
user : this.user,
|
Events.emit(Events.getSystemEvents().UserStatIncrement, {
|
||||||
statName : UserProps.MinutesOnlineTotalCount,
|
user: this.user,
|
||||||
statIncrementBy : 1,
|
statName: UserProps.MinutesOnlineTotalCount,
|
||||||
statValue : minOnline
|
statIncrementBy: 1,
|
||||||
}
|
statValue: minOnline,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds;
|
idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds;
|
||||||
@@ -493,46 +504,52 @@ Client.prototype.startIdleMonitor = function() {
|
|||||||
// use override value if set
|
// use override value if set
|
||||||
idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds;
|
idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds;
|
||||||
|
|
||||||
if(idleLogoutSeconds > 0 && (nowMs - this.lastActivityTime >= (idleLogoutSeconds * 1000))) {
|
if (
|
||||||
|
idleLogoutSeconds > 0 &&
|
||||||
|
nowMs - this.lastActivityTime >= idleLogoutSeconds * 1000
|
||||||
|
) {
|
||||||
this.emit('idle timeout');
|
this.emit('idle timeout');
|
||||||
}
|
}
|
||||||
}, 1000 * 60);
|
}, 1000 * 60);
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.stopIdleMonitor = function() {
|
Client.prototype.stopIdleMonitor = function () {
|
||||||
if(this.idleCheck) {
|
if (this.idleCheck) {
|
||||||
clearInterval(this.idleCheck);
|
clearInterval(this.idleCheck);
|
||||||
delete this.idleCheck;
|
delete this.idleCheck;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.explicitActivityTimeUpdate = function() {
|
Client.prototype.explicitActivityTimeUpdate = function () {
|
||||||
this.lastActivityTime = Date.now();
|
this.lastActivityTime = Date.now();
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.overrideIdleLogoutSeconds = function(seconds) {
|
Client.prototype.overrideIdleLogoutSeconds = function (seconds) {
|
||||||
this.idleLogoutSecondsOverride = seconds;
|
this.idleLogoutSecondsOverride = seconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.restoreIdleLogoutSeconds = function() {
|
Client.prototype.restoreIdleLogoutSeconds = function () {
|
||||||
delete this.idleLogoutSecondsOverride;
|
delete this.idleLogoutSecondsOverride;
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.end = function () {
|
Client.prototype.end = function () {
|
||||||
if(this.term) {
|
if (this.term) {
|
||||||
this.term.disconnect();
|
this.term.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.removeListener(Events.getSystemEvents().ThemeChanged, this.themeChangedListener);
|
Events.removeListener(
|
||||||
|
Events.getSystemEvents().ThemeChanged,
|
||||||
|
this.themeChangedListener
|
||||||
|
);
|
||||||
|
|
||||||
const currentModule = this.menuStack.getCurrentModule;
|
const currentModule = this.menuStack.getCurrentModule;
|
||||||
|
|
||||||
if(currentModule) {
|
if (currentModule) {
|
||||||
currentModule.leave();
|
currentModule.leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist time online for authenticated users
|
// persist time online for authenticated users
|
||||||
if(this.user.isAuthenticated()) {
|
if (this.user.isAuthenticated()) {
|
||||||
this.user.persistProperty(
|
this.user.persistProperty(
|
||||||
UserProps.MinutesOnlineTotalCount,
|
UserProps.MinutesOnlineTotalCount,
|
||||||
this.user.getProperty(UserProps.MinutesOnlineTotalCount)
|
this.user.getProperty(UserProps.MinutesOnlineTotalCount)
|
||||||
@@ -545,13 +562,13 @@ Client.prototype.end = function () {
|
|||||||
//
|
//
|
||||||
// We can end up calling 'end' before TTY/etc. is established, e.g. with SSH
|
// We can end up calling 'end' before TTY/etc. is established, e.g. with SSH
|
||||||
//
|
//
|
||||||
if(_.isFunction(this.disconnect)) {
|
if (_.isFunction(this.disconnect)) {
|
||||||
return this.disconnect();
|
return this.disconnect();
|
||||||
} else {
|
} else {
|
||||||
// legacy fallback
|
// legacy fallback
|
||||||
return this.output.end.apply(this.output, arguments);
|
return this.output.end.apply(this.output, arguments);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// ie TypeError
|
// ie TypeError
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -564,15 +581,15 @@ Client.prototype.destroySoon = function () {
|
|||||||
return this.output.destroySoon.apply(this.output, arguments);
|
return this.output.destroySoon.apply(this.output, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.waitForKeyPress = function(cb) {
|
Client.prototype.waitForKeyPress = function (cb) {
|
||||||
this.once('key press', function kp(ch, key) {
|
this.once('key press', function kp(ch, key) {
|
||||||
cb(ch, key);
|
cb(ch, key);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.isLocal = function() {
|
Client.prototype.isLocal = function () {
|
||||||
// :TODO: Handle ipv6 better
|
// :TODO: Handle ipv6 better
|
||||||
return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress);
|
return ['127.0.0.1', '::ffff:127.0.0.1'].includes(this.remoteAddress);
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.friendlyRemoteAddress = function() {
|
Client.prototype.friendlyRemoteAddress = function() {
|
||||||
@@ -587,7 +604,7 @@ Client.prototype.friendlyRemoteAddress = function() {
|
|||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something
|
// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something
|
||||||
Client.prototype.defaultHandlerMissingMod = function() {
|
Client.prototype.defaultHandlerMissingMod = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
function handler(err) {
|
function handler(err) {
|
||||||
@@ -598,7 +615,6 @@ Client.prototype.defaultHandlerMissingMod = function() {
|
|||||||
self.term.write('This has been logged for your SysOp to review.\n');
|
self.term.write('This has been logged for your SysOp to review.\n');
|
||||||
self.term.write('\nGoodbye!\n');
|
self.term.write('\nGoodbye!\n');
|
||||||
|
|
||||||
|
|
||||||
//self.term.write(err);
|
//self.term.write(err);
|
||||||
|
|
||||||
//if(miscUtil.isDevelopment() && err.stack) {
|
//if(miscUtil.isDevelopment() && err.stack) {
|
||||||
@@ -611,18 +627,18 @@ Client.prototype.defaultHandlerMissingMod = function() {
|
|||||||
return handler;
|
return handler;
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.terminalSupports = function(query) {
|
Client.prototype.terminalSupports = function (query) {
|
||||||
const termClient = this.term.termClient;
|
const termClient = this.term.termClient;
|
||||||
|
|
||||||
switch(query) {
|
switch (query) {
|
||||||
case 'vtx_audio' :
|
case 'vtx_audio':
|
||||||
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
|
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
|
||||||
return 'vtx' === termClient;
|
return 'vtx' === termClient;
|
||||||
|
|
||||||
case 'vtx_hyperlink' :
|
case 'vtx_hyperlink':
|
||||||
return 'vtx' === termClient;
|
return 'vtx' === termClient;
|
||||||
|
|
||||||
default :
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,21 +2,21 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const logger = require('./logger.js');
|
const logger = require('./logger.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const hashids = require('hashids/cjs');
|
const hashids = require('hashids/cjs');
|
||||||
|
|
||||||
exports.getActiveConnections = getActiveConnections;
|
exports.getActiveConnections = getActiveConnections;
|
||||||
exports.getActiveConnectionList = getActiveConnectionList;
|
exports.getActiveConnectionList = getActiveConnectionList;
|
||||||
exports.addNewClient = addNewClient;
|
exports.addNewClient = addNewClient;
|
||||||
exports.removeClient = removeClient;
|
exports.removeClient = removeClient;
|
||||||
exports.getConnectionByUserId = getConnectionByUserId;
|
exports.getConnectionByUserId = getConnectionByUserId;
|
||||||
exports.getConnectionByNodeId = getConnectionByNodeId;
|
exports.getConnectionByNodeId = getConnectionByNodeId;
|
||||||
|
|
||||||
const clientConnections = [];
|
const clientConnections = [];
|
||||||
exports.clientConnections = clientConnections;
|
exports.clientConnections = clientConnections;
|
||||||
@@ -95,21 +95,24 @@ function addNewClient(client, clientSock) {
|
|||||||
for (nodeId = 1; nodeId < Number.MAX_SAFE_INTEGER; ++nodeId) {
|
for (nodeId = 1; nodeId < Number.MAX_SAFE_INTEGER; ++nodeId) {
|
||||||
const existing = clientConnections.find(client => nodeId === client.node);
|
const existing = clientConnections.find(client => nodeId === client.node);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
break; // available slot
|
break; // available slot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.session.id = nodeId;
|
client.session.id = nodeId;
|
||||||
const remoteAddress = client.remoteAddress = clientSock.remoteAddress;
|
const remoteAddress = (client.remoteAddress = clientSock.remoteAddress);
|
||||||
// create a unique identifier one-time ID for this session
|
// create a unique identifier one-time ID for this session
|
||||||
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ nodeId, moment().valueOf() ]);
|
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([
|
||||||
|
nodeId,
|
||||||
|
moment().valueOf(),
|
||||||
|
]);
|
||||||
|
|
||||||
clientConnections.push(client);
|
clientConnections.push(client);
|
||||||
clientConnections.sort( (c1, c2) => c1.session.id - c2.session.id);
|
clientConnections.sort((c1, c2) => c1.session.id - c2.session.id);
|
||||||
|
|
||||||
// Create a client specific logger
|
// Create a client specific logger
|
||||||
// Note that this will be updated @ login with additional information
|
// Note that this will be updated @ login with additional information
|
||||||
client.log = logger.log.child( { nodeId, sessionId : client.session.uniqueId } );
|
client.log = logger.log.child({ nodeId, sessionId: client.session.uniqueId });
|
||||||
|
|
||||||
const connInfo = {
|
const connInfo = {
|
||||||
remoteAddress : remoteAddress,
|
remoteAddress : remoteAddress,
|
||||||
@@ -118,17 +121,17 @@ function addNewClient(client, clientSock) {
|
|||||||
isSecure : client.session.isSecure,
|
isSecure : client.session.isSecure,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(client.log.debug()) {
|
if (client.log.debug()) {
|
||||||
connInfo.port = clientSock.localPort;
|
connInfo.port = clientSock.localPort;
|
||||||
connInfo.family = clientSock.localFamily;
|
connInfo.family = clientSock.localFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.log.info(connInfo, `Client connected (${connInfo.serverName}/${connInfo.port})`);
|
client.log.info(connInfo, `Client connected (${connInfo.serverName}/${connInfo.port})`);
|
||||||
|
|
||||||
Events.emit(
|
Events.emit(Events.getSystemEvents().ClientConnected, {
|
||||||
Events.getSystemEvents().ClientConnected,
|
client: client,
|
||||||
{ client : client, connectionCount : clientConnections.length }
|
connectionCount: clientConnections.length,
|
||||||
);
|
});
|
||||||
|
|
||||||
return nodeId;
|
return nodeId;
|
||||||
}
|
}
|
||||||
@@ -137,26 +140,32 @@ function removeClient(client) {
|
|||||||
client.end();
|
client.end();
|
||||||
|
|
||||||
const i = clientConnections.indexOf(client);
|
const i = clientConnections.indexOf(client);
|
||||||
if(i > -1) {
|
if (i > -1) {
|
||||||
clientConnections.splice(i, 1);
|
clientConnections.splice(i, 1);
|
||||||
|
|
||||||
logger.log.info(
|
logger.log.info(
|
||||||
{
|
{
|
||||||
connectionCount : clientConnections.length,
|
connectionCount: clientConnections.length,
|
||||||
nodeId : client.node,
|
nodeId: client.node,
|
||||||
},
|
},
|
||||||
'Client disconnected'
|
'Client disconnected'
|
||||||
);
|
);
|
||||||
|
|
||||||
if(client.user && client.user.isValid()) {
|
if (client.user && client.user.isValid()) {
|
||||||
const minutesOnline = moment().diff(moment(client.user.properties[UserProps.LastLoginTs]), 'minutes');
|
const minutesOnline = moment().diff(
|
||||||
Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user, minutesOnline } );
|
moment(client.user.properties[UserProps.LastLoginTs]),
|
||||||
|
'minutes'
|
||||||
|
);
|
||||||
|
Events.emit(Events.getSystemEvents().UserLogoff, {
|
||||||
|
user: client.user,
|
||||||
|
minutesOnline,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.emit(
|
Events.emit(Events.getSystemEvents().ClientDisconnected, {
|
||||||
Events.getSystemEvents().ClientDisconnected,
|
client: client,
|
||||||
{ client : client, connectionCount : clientConnections.length }
|
connectionCount: clientConnections.length,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,23 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
var Log = require('./logger.js').log;
|
var Log = require('./logger.js').log;
|
||||||
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
var iconv = require('iconv-lite');
|
var iconv = require('iconv-lite');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
exports.ClientTerminal = ClientTerminal;
|
||||||
exports.ClientTerminal = ClientTerminal;
|
|
||||||
|
|
||||||
function ClientTerminal(output) {
|
function ClientTerminal(output) {
|
||||||
this.output = output;
|
this.output = output;
|
||||||
|
|
||||||
var outputEncoding = 'cp437';
|
var outputEncoding = 'cp437';
|
||||||
assert(iconv.encodingExists(outputEncoding));
|
assert(iconv.encodingExists(outputEncoding));
|
||||||
|
|
||||||
// convert line feeds such as \n -> \r\n
|
// convert line feeds such as \n -> \r\n
|
||||||
this.convertLF = true;
|
this.convertLF = true;
|
||||||
|
|
||||||
this.syncTermFontsEnabled = false;
|
this.syncTermFontsEnabled = false;
|
||||||
|
|
||||||
@@ -27,37 +26,37 @@ function ClientTerminal(output) {
|
|||||||
// Some terminal we handle specially
|
// Some terminal we handle specially
|
||||||
// They can also be found in this.env{}
|
// They can also be found in this.env{}
|
||||||
//
|
//
|
||||||
var termType = 'unknown';
|
var termType = 'unknown';
|
||||||
var termHeight = 0;
|
var termHeight = 0;
|
||||||
var termWidth = 0;
|
var termWidth = 0;
|
||||||
var termClient = 'unknown';
|
var termClient = 'unknown';
|
||||||
|
|
||||||
this.currentSyncFont = 'not_set';
|
this.currentSyncFont = 'not_set';
|
||||||
|
|
||||||
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
|
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
|
||||||
this.env = {};
|
this.env = {};
|
||||||
|
|
||||||
Object.defineProperty(this, 'outputEncoding', {
|
Object.defineProperty(this, 'outputEncoding', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return outputEncoding;
|
return outputEncoding;
|
||||||
},
|
},
|
||||||
set : function(enc) {
|
set: function (enc) {
|
||||||
if(iconv.encodingExists(enc)) {
|
if (iconv.encodingExists(enc)) {
|
||||||
outputEncoding = enc;
|
outputEncoding = enc;
|
||||||
} else {
|
} else {
|
||||||
Log.warn({ encoding : enc }, 'Unknown encoding');
|
Log.warn({ encoding: enc }, 'Unknown encoding');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(this, 'termType', {
|
Object.defineProperty(this, 'termType', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return termType;
|
return termType;
|
||||||
},
|
},
|
||||||
set : function(ttype) {
|
set: function (ttype) {
|
||||||
termType = ttype.toLowerCase();
|
termType = ttype.toLowerCase();
|
||||||
|
|
||||||
if(this.isANSI()) {
|
if (this.isANSI()) {
|
||||||
this.outputEncoding = 'cp437';
|
this.outputEncoding = 'cp437';
|
||||||
} else {
|
} else {
|
||||||
// :TODO: See how x84 does this -- only set if local/remote are binary
|
// :TODO: See how x84 does this -- only set if local/remote are binary
|
||||||
@@ -68,53 +67,56 @@ function ClientTerminal(output) {
|
|||||||
// Windows telnet will send "VTNT". If so, set termClient='windows'
|
// Windows telnet will send "VTNT". If so, set termClient='windows'
|
||||||
// there are some others on the page as well
|
// there are some others on the page as well
|
||||||
|
|
||||||
Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change');
|
Log.debug(
|
||||||
}
|
{ encoding: this.outputEncoding },
|
||||||
|
'Set output encoding due to terminal type change'
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(this, 'termWidth', {
|
Object.defineProperty(this, 'termWidth', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return termWidth;
|
return termWidth;
|
||||||
},
|
},
|
||||||
set : function(width) {
|
set: function (width) {
|
||||||
if(width > 0) {
|
if (width > 0) {
|
||||||
termWidth = width;
|
termWidth = width;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(this, 'termHeight', {
|
Object.defineProperty(this, 'termHeight', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return termHeight;
|
return termHeight;
|
||||||
},
|
},
|
||||||
set : function(height) {
|
set: function (height) {
|
||||||
if(height > 0) {
|
if (height > 0) {
|
||||||
termHeight = height;
|
termHeight = height;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(this, 'termClient', {
|
Object.defineProperty(this, 'termClient', {
|
||||||
get : function() {
|
get: function () {
|
||||||
return termClient;
|
return termClient;
|
||||||
},
|
},
|
||||||
set : function(tc) {
|
set: function (tc) {
|
||||||
termClient = tc;
|
termClient = tc;
|
||||||
|
|
||||||
Log.debug( { termClient : this.termClient }, 'Set known terminal client');
|
Log.debug({ termClient: this.termClient }, 'Set known terminal client');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientTerminal.prototype.disconnect = function() {
|
ClientTerminal.prototype.disconnect = function () {
|
||||||
this.output = null;
|
this.output = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.isNixTerm = function() {
|
ClientTerminal.prototype.isNixTerm = function () {
|
||||||
//
|
//
|
||||||
// Standard *nix type terminals
|
// Standard *nix type terminals
|
||||||
//
|
//
|
||||||
if(this.termType.startsWith('xterm')) {
|
if (this.termType.startsWith('xterm')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ ClientTerminal.prototype.isNixTerm = function() {
|
|||||||
return utf8TermList.includes(this.termType);
|
return utf8TermList.includes(this.termType);
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.isANSI = function() {
|
ClientTerminal.prototype.isANSI = function () {
|
||||||
//
|
//
|
||||||
// ANSI terminals should be encoded to CP437
|
// ANSI terminals should be encoded to CP437
|
||||||
//
|
//
|
||||||
@@ -163,35 +165,33 @@ ClientTerminal.prototype.isANSI = function() {
|
|||||||
|
|
||||||
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
|
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
|
||||||
|
|
||||||
ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) {
|
ClientTerminal.prototype.write = function (s, convertLineFeeds, cb) {
|
||||||
this.rawWrite(this.encode(s, convertLineFeeds), cb);
|
this.rawWrite(this.encode(s, convertLineFeeds), cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.rawWrite = function(s, cb) {
|
ClientTerminal.prototype.rawWrite = function (s, cb) {
|
||||||
if(this.output && this.output.writable) {
|
if (this.output && this.output.writable) {
|
||||||
this.output.write(s, err => {
|
this.output.write(s, err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(err) {
|
if (err) {
|
||||||
Log.warn( { error : err.message }, 'Failed writing to socket');
|
Log.warn({ error: err.message }, 'Failed writing to socket');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.pipeWrite = function(s, cb) {
|
ClientTerminal.prototype.pipeWrite = function (s, cb) {
|
||||||
this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds|
|
this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds|
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.encode = function(s, convertLineFeeds) {
|
ClientTerminal.prototype.encode = function (s, convertLineFeeds) {
|
||||||
convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF;
|
convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF;
|
||||||
|
|
||||||
if(convertLineFeeds && _.isString(s)) {
|
if (convertLineFeeds && _.isString(s)) {
|
||||||
s = s.replace(/\n/g, '\r\n');
|
s = s.replace(/\n/g, '\r\n');
|
||||||
}
|
}
|
||||||
return iconv.encode(s, this.outputEncoding);
|
return iconv.encode(s, this.outputEncoding);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const ANSI = require('./ansi_term.js');
|
const ANSI = require('./ansi_term.js');
|
||||||
const { getPredefinedMCIValue } = require('./predefined_mci.js');
|
const { getPredefinedMCIValue } = require('./predefined_mci.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.stripMciColorCodes = stripMciColorCodes;
|
exports.stripMciColorCodes = stripMciColorCodes;
|
||||||
exports.pipeStringLength = pipeStringLength;
|
exports.pipeStringLength = pipeStringLength;
|
||||||
exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
|
exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
|
||||||
exports.controlCodesToAnsi = controlCodesToAnsi;
|
exports.controlCodesToAnsi = controlCodesToAnsi;
|
||||||
|
|
||||||
// :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string?
|
// :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string?
|
||||||
|
|
||||||
@@ -23,97 +23,101 @@ function pipeStringLength(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ansiSgrFromRenegadeColorCode(cc) {
|
function ansiSgrFromRenegadeColorCode(cc) {
|
||||||
return ANSI.sgr({
|
return ANSI.sgr(
|
||||||
0 : [ 'reset', 'black' ],
|
{
|
||||||
1 : [ 'reset', 'blue' ],
|
0: ['reset', 'black'],
|
||||||
2 : [ 'reset', 'green' ],
|
1: ['reset', 'blue'],
|
||||||
3 : [ 'reset', 'cyan' ],
|
2: ['reset', 'green'],
|
||||||
4 : [ 'reset', 'red' ],
|
3: ['reset', 'cyan'],
|
||||||
5 : [ 'reset', 'magenta' ],
|
4: ['reset', 'red'],
|
||||||
6 : [ 'reset', 'yellow' ],
|
5: ['reset', 'magenta'],
|
||||||
7 : [ 'reset', 'white' ],
|
6: ['reset', 'yellow'],
|
||||||
|
7: ['reset', 'white'],
|
||||||
|
|
||||||
8 : [ 'bold', 'black' ],
|
8: ['bold', 'black'],
|
||||||
9 : [ 'bold', 'blue' ],
|
9: ['bold', 'blue'],
|
||||||
10 : [ 'bold', 'green' ],
|
10: ['bold', 'green'],
|
||||||
11 : [ 'bold', 'cyan' ],
|
11: ['bold', 'cyan'],
|
||||||
12 : [ 'bold', 'red' ],
|
12: ['bold', 'red'],
|
||||||
13 : [ 'bold', 'magenta' ],
|
13: ['bold', 'magenta'],
|
||||||
14 : [ 'bold', 'yellow' ],
|
14: ['bold', 'yellow'],
|
||||||
15 : [ 'bold', 'white' ],
|
15: ['bold', 'white'],
|
||||||
|
|
||||||
16 : [ 'blackBG' ],
|
16: ['blackBG'],
|
||||||
17 : [ 'blueBG' ],
|
17: ['blueBG'],
|
||||||
18 : [ 'greenBG' ],
|
18: ['greenBG'],
|
||||||
19 : [ 'cyanBG' ],
|
19: ['cyanBG'],
|
||||||
20 : [ 'redBG' ],
|
20: ['redBG'],
|
||||||
21 : [ 'magentaBG' ],
|
21: ['magentaBG'],
|
||||||
22 : [ 'yellowBG' ],
|
22: ['yellowBG'],
|
||||||
23 : [ 'whiteBG' ],
|
23: ['whiteBG'],
|
||||||
|
|
||||||
24 : [ 'blink', 'blackBG' ],
|
24: ['blink', 'blackBG'],
|
||||||
25 : [ 'blink', 'blueBG' ],
|
25: ['blink', 'blueBG'],
|
||||||
26 : [ 'blink', 'greenBG' ],
|
26: ['blink', 'greenBG'],
|
||||||
27 : [ 'blink', 'cyanBG' ],
|
27: ['blink', 'cyanBG'],
|
||||||
28 : [ 'blink', 'redBG' ],
|
28: ['blink', 'redBG'],
|
||||||
29 : [ 'blink', 'magentaBG' ],
|
29: ['blink', 'magentaBG'],
|
||||||
30 : [ 'blink', 'yellowBG' ],
|
30: ['blink', 'yellowBG'],
|
||||||
31 : [ 'blink', 'whiteBG' ],
|
31: ['blink', 'whiteBG'],
|
||||||
}[cc] || 'normal');
|
}[cc] || 'normal'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ansiSgrFromCnetStyleColorCode(cc) {
|
function ansiSgrFromCnetStyleColorCode(cc) {
|
||||||
return ANSI.sgr({
|
return ANSI.sgr(
|
||||||
c0 : [ 'reset', 'black' ],
|
{
|
||||||
c1 : [ 'reset', 'red' ],
|
c0: ['reset', 'black'],
|
||||||
c2 : [ 'reset', 'green' ],
|
c1: ['reset', 'red'],
|
||||||
c3 : [ 'reset', 'yellow' ],
|
c2: ['reset', 'green'],
|
||||||
c4 : [ 'reset', 'blue' ],
|
c3: ['reset', 'yellow'],
|
||||||
c5 : [ 'reset', 'magenta' ],
|
c4: ['reset', 'blue'],
|
||||||
c6 : [ 'reset', 'cyan' ],
|
c5: ['reset', 'magenta'],
|
||||||
c7 : [ 'reset', 'white' ],
|
c6: ['reset', 'cyan'],
|
||||||
|
c7: ['reset', 'white'],
|
||||||
|
|
||||||
c8 : [ 'bold', 'black' ],
|
c8: ['bold', 'black'],
|
||||||
c9 : [ 'bold', 'red' ],
|
c9: ['bold', 'red'],
|
||||||
ca : [ 'bold', 'green' ],
|
ca: ['bold', 'green'],
|
||||||
cb : [ 'bold', 'yellow' ],
|
cb: ['bold', 'yellow'],
|
||||||
cc : [ 'bold', 'blue' ],
|
cc: ['bold', 'blue'],
|
||||||
cd : [ 'bold', 'magenta' ],
|
cd: ['bold', 'magenta'],
|
||||||
ce : [ 'bold', 'cyan' ],
|
ce: ['bold', 'cyan'],
|
||||||
cf : [ 'bold', 'white' ],
|
cf: ['bold', 'white'],
|
||||||
|
|
||||||
z0 : [ 'blackBG' ],
|
z0: ['blackBG'],
|
||||||
z1 : [ 'redBG' ],
|
z1: ['redBG'],
|
||||||
z2 : [ 'greenBG' ],
|
z2: ['greenBG'],
|
||||||
z3 : [ 'yellowBG' ],
|
z3: ['yellowBG'],
|
||||||
z4 : [ 'blueBG' ],
|
z4: ['blueBG'],
|
||||||
z5 : [ 'magentaBG' ],
|
z5: ['magentaBG'],
|
||||||
z6 : [ 'cyanBG' ],
|
z6: ['cyanBG'],
|
||||||
z7 : [ 'whiteBG' ],
|
z7: ['whiteBG'],
|
||||||
}[cc] || 'normal');
|
}[cc] || 'normal'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renegadeToAnsi(s, client) {
|
function renegadeToAnsi(s, client) {
|
||||||
if(-1 == s.indexOf('|')) {
|
if (-1 == s.indexOf('|')) {
|
||||||
return s; // no pipe codes present
|
return s; // no pipe codes present
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = '';
|
let result = '';
|
||||||
const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g;
|
const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g;
|
||||||
let m;
|
let m;
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
while((m = re.exec(s))) {
|
while ((m = re.exec(s))) {
|
||||||
if(m[3]) {
|
if (m[3]) {
|
||||||
// |## color
|
// |## color
|
||||||
const val = parseInt(m[3], 10);
|
const val = parseInt(m[3], 10);
|
||||||
const attr = ansiSgrFromRenegadeColorCode(val);
|
const attr = ansiSgrFromRenegadeColorCode(val);
|
||||||
result += s.substr(lastIndex, m.index - lastIndex) + attr;
|
result += s.substr(lastIndex, m.index - lastIndex) + attr;
|
||||||
} else if(m[4] || m[1]) {
|
} else if (m[4] || m[1]) {
|
||||||
// |AA MCI code or |Cx## movement where ## is in m[1]
|
// |AA MCI code or |Cx## movement where ## is in m[1]
|
||||||
let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]);
|
let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]);
|
||||||
val = _.isString(val) ? val : m[0]; // value itself or literal
|
val = _.isString(val) ? val : m[0]; // value itself or literal
|
||||||
result += s.substr(lastIndex, m.index - lastIndex) + val;
|
result += s.substr(lastIndex, m.index - lastIndex) + val;
|
||||||
} else if(m[5]) {
|
} else if (m[5]) {
|
||||||
// || -- literal '|', that is.
|
// || -- literal '|', that is.
|
||||||
result += '|';
|
result += '|';
|
||||||
}
|
}
|
||||||
@@ -121,7 +125,7 @@ function renegadeToAnsi(s, client) {
|
|||||||
lastIndex = re.lastIndex;
|
lastIndex = re.lastIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (0 === result.length ? s : result + s.substr(lastIndex));
|
return 0 === result.length ? s : result + s.substr(lastIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -144,26 +148,27 @@ function renegadeToAnsi(s, client) {
|
|||||||
// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt
|
// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt
|
||||||
//
|
//
|
||||||
function controlCodesToAnsi(s, client) {
|
function controlCodesToAnsi(s, client) {
|
||||||
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex
|
const RE =
|
||||||
|
/(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
let m;
|
let m;
|
||||||
let result = '';
|
let result = '';
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let v;
|
let v;
|
||||||
let fg;
|
let fg;
|
||||||
let bg;
|
let bg;
|
||||||
|
|
||||||
while((m = RE.exec(s))) {
|
while ((m = RE.exec(s))) {
|
||||||
switch(m[0].charAt(0)) {
|
switch (m[0].charAt(0)) {
|
||||||
case '|' :
|
case '|':
|
||||||
// Renegade |##
|
// Renegade |##
|
||||||
v = parseInt(m[2], 10);
|
v = parseInt(m[2], 10);
|
||||||
|
|
||||||
if(isNaN(v)) {
|
if (isNaN(v)) {
|
||||||
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
|
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(v)) {
|
if (_.isString(v)) {
|
||||||
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
||||||
} else {
|
} else {
|
||||||
v = ansiSgrFromRenegadeColorCode(v);
|
v = ansiSgrFromRenegadeColorCode(v);
|
||||||
@@ -171,9 +176,9 @@ function controlCodesToAnsi(s, client) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '@' :
|
case '@':
|
||||||
// PCBoard @X## or Wildcat! @##@
|
// PCBoard @X## or Wildcat! @##@
|
||||||
if('@' === m[0].substr(-1)) {
|
if ('@' === m[0].substr(-1)) {
|
||||||
// Wildcat!
|
// Wildcat!
|
||||||
v = m[6];
|
v = m[6];
|
||||||
} else {
|
} else {
|
||||||
@@ -181,81 +186,83 @@ function controlCodesToAnsi(s, client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bg = {
|
bg = {
|
||||||
0 : [ 'blackBG' ],
|
0: ['blackBG'],
|
||||||
1 : [ 'blueBG' ],
|
1: ['blueBG'],
|
||||||
2 : [ 'greenBG' ],
|
2: ['greenBG'],
|
||||||
3 : [ 'cyanBG' ],
|
3: ['cyanBG'],
|
||||||
4 : [ 'redBG' ],
|
4: ['redBG'],
|
||||||
5 : [ 'magentaBG' ],
|
5: ['magentaBG'],
|
||||||
6 : [ 'yellowBG' ],
|
6: ['yellowBG'],
|
||||||
7 : [ 'whiteBG' ],
|
7: ['whiteBG'],
|
||||||
|
|
||||||
8 : [ 'bold', 'blackBG' ],
|
8: ['bold', 'blackBG'],
|
||||||
9 : [ 'bold', 'blueBG' ],
|
9: ['bold', 'blueBG'],
|
||||||
A : [ 'bold', 'greenBG' ],
|
A: ['bold', 'greenBG'],
|
||||||
B : [ 'bold', 'cyanBG' ],
|
B: ['bold', 'cyanBG'],
|
||||||
C : [ 'bold', 'redBG' ],
|
C: ['bold', 'redBG'],
|
||||||
D : [ 'bold', 'magentaBG' ],
|
D: ['bold', 'magentaBG'],
|
||||||
E : [ 'bold', 'yellowBG' ],
|
E: ['bold', 'yellowBG'],
|
||||||
F : [ 'bold', 'whiteBG' ],
|
F: ['bold', 'whiteBG'],
|
||||||
}[v.charAt(0)] || [ 'normal' ];
|
}[v.charAt(0)] || ['normal'];
|
||||||
|
|
||||||
fg = {
|
fg = {
|
||||||
0 : [ 'reset', 'black' ],
|
0: ['reset', 'black'],
|
||||||
1 : [ 'reset', 'blue' ],
|
1: ['reset', 'blue'],
|
||||||
2 : [ 'reset', 'green' ],
|
2: ['reset', 'green'],
|
||||||
3 : [ 'reset', 'cyan' ],
|
3: ['reset', 'cyan'],
|
||||||
4 : [ 'reset', 'red' ],
|
4: ['reset', 'red'],
|
||||||
5 : [ 'reset', 'magenta' ],
|
5: ['reset', 'magenta'],
|
||||||
6 : [ 'reset', 'yellow' ],
|
6: ['reset', 'yellow'],
|
||||||
7 : [ 'reset', 'white' ],
|
7: ['reset', 'white'],
|
||||||
|
|
||||||
8 : [ 'blink', 'black' ],
|
8: ['blink', 'black'],
|
||||||
9 : [ 'blink', 'blue' ],
|
9: ['blink', 'blue'],
|
||||||
A : [ 'blink', 'green' ],
|
A: ['blink', 'green'],
|
||||||
B : [ 'blink', 'cyan' ],
|
B: ['blink', 'cyan'],
|
||||||
C : [ 'blink', 'red' ],
|
C: ['blink', 'red'],
|
||||||
D : [ 'blink', 'magenta' ],
|
D: ['blink', 'magenta'],
|
||||||
E : [ 'blink', 'yellow' ],
|
E: ['blink', 'yellow'],
|
||||||
F : [ 'blink', 'white' ],
|
F: ['blink', 'white'],
|
||||||
}[v.charAt(1)] || ['normal'];
|
}[v.charAt(1)] || ['normal'];
|
||||||
|
|
||||||
v = ANSI.sgr(fg.concat(bg));
|
v = ANSI.sgr(fg.concat(bg));
|
||||||
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '\x03' :
|
case '\x03':
|
||||||
// WWIV
|
// WWIV
|
||||||
v = parseInt(m[8], 10);
|
v = parseInt(m[8], 10);
|
||||||
|
|
||||||
if(isNaN(v)) {
|
if (isNaN(v)) {
|
||||||
v += m[0];
|
v += m[0];
|
||||||
} else {
|
} else {
|
||||||
v = ANSI.sgr({
|
v = ANSI.sgr(
|
||||||
0 : [ 'reset', 'black' ],
|
{
|
||||||
1 : [ 'bold', 'cyan' ],
|
0: ['reset', 'black'],
|
||||||
2 : [ 'bold', 'yellow' ],
|
1: ['bold', 'cyan'],
|
||||||
3 : [ 'reset', 'magenta' ],
|
2: ['bold', 'yellow'],
|
||||||
4 : [ 'bold', 'white', 'blueBG' ],
|
3: ['reset', 'magenta'],
|
||||||
5 : [ 'reset', 'green' ],
|
4: ['bold', 'white', 'blueBG'],
|
||||||
6 : [ 'bold', 'blink', 'red' ],
|
5: ['reset', 'green'],
|
||||||
7 : [ 'bold', 'blue' ],
|
6: ['bold', 'blink', 'red'],
|
||||||
8 : [ 'reset', 'blue' ],
|
7: ['bold', 'blue'],
|
||||||
9 : [ 'reset', 'cyan' ],
|
8: ['reset', 'blue'],
|
||||||
}[v] || 'normal');
|
9: ['reset', 'cyan'],
|
||||||
|
}[v] || 'normal'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '\x19' :
|
case '\x19':
|
||||||
case '\0x11' :
|
case '\0x11':
|
||||||
// CNET "Y-Style" & "Q-Style"
|
// CNET "Y-Style" & "Q-Style"
|
||||||
v = m[9] || m[11];
|
v = m[9] || m[11];
|
||||||
if(v) {
|
if (v) {
|
||||||
if('n1' === v) {
|
if ('n1' === v) {
|
||||||
v = '\n';
|
v = '\n';
|
||||||
} else if('f1' === v) {
|
} else if ('f1' === v) {
|
||||||
v = ANSI.clearScreen();
|
v = ANSI.clearScreen();
|
||||||
} else {
|
} else {
|
||||||
v = ansiSgrFromCnetStyleColorCode(v);
|
v = ansiSgrFromCnetStyleColorCode(v);
|
||||||
@@ -270,5 +277,5 @@ function controlCodesToAnsi(s, client) {
|
|||||||
lastIndex = RE.lastIndex;
|
lastIndex = RE.lastIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (0 === result.length ? s : result + s.substr(lastIndex));
|
return 0 === result.length ? s : result + s.substr(lastIndex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const { MenuModule } = require('../core/menu_module.js');
|
const { MenuModule } = require('../core/menu_module.js');
|
||||||
const { resetScreen } = require('../core/ansi_term.js');
|
const { resetScreen } = require('../core/ansi_term.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const {
|
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
||||||
trackDoorRunBegin,
|
|
||||||
trackDoorRunEnd
|
|
||||||
} = require('./door_util.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const RLogin = require('rlogin');
|
const RLogin = require('rlogin');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'CombatNet',
|
name: 'CombatNet',
|
||||||
desc : 'CombatNet Access Module',
|
desc: 'CombatNet Access Module',
|
||||||
author : 'Dave Stephens',
|
author: 'Dave Stephens',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class CombatNetModule extends MenuModule {
|
exports.getModule = class CombatNetModule extends MenuModule {
|
||||||
@@ -25,9 +22,9 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
// establish defaults
|
// establish defaults
|
||||||
this.config = options.menuConfig.config;
|
this.config = options.menuConfig.config;
|
||||||
this.config.host = this.config.host || 'bbs.combatnet.us';
|
this.config.host = this.config.host || 'bbs.combatnet.us';
|
||||||
this.config.rloginPort = this.config.rloginPort || 4513;
|
this.config.rloginPort = this.config.rloginPort || 4513;
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
@@ -38,10 +35,10 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
return self.validateConfigFields(
|
return self.validateConfigFields(
|
||||||
{
|
{
|
||||||
host : 'string',
|
host: 'string',
|
||||||
password : 'string',
|
password: 'string',
|
||||||
bbsTag : 'string',
|
bbsTag: 'string',
|
||||||
rloginPort : 'number',
|
rloginPort: 'number',
|
||||||
},
|
},
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
@@ -52,30 +49,33 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||||||
|
|
||||||
let doorTracking;
|
let doorTracking;
|
||||||
|
|
||||||
const restorePipeToNormal = function() {
|
const restorePipeToNormal = function () {
|
||||||
if(self.client.term.output) {
|
if (self.client.term.output) {
|
||||||
self.client.term.output.removeListener('data', sendToRloginBuffer);
|
self.client.term.output.removeListener(
|
||||||
|
'data',
|
||||||
|
sendToRloginBuffer
|
||||||
|
);
|
||||||
|
|
||||||
if(doorTracking) {
|
if (doorTracking) {
|
||||||
trackDoorRunEnd(doorTracking);
|
trackDoorRunEnd(doorTracking);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rlogin = new RLogin(
|
const rlogin = new RLogin({
|
||||||
{
|
clientUsername: self.config.password,
|
||||||
clientUsername : self.config.password,
|
serverUsername: `${self.config.bbsTag}${self.client.user.username}`,
|
||||||
serverUsername : `${self.config.bbsTag}${self.client.user.username}`,
|
host: self.config.host,
|
||||||
host : self.config.host,
|
port: self.config.rloginPort,
|
||||||
port : self.config.rloginPort,
|
terminalType: self.client.term.termClient,
|
||||||
terminalType : self.client.term.termClient,
|
terminalSpeed: 57600,
|
||||||
terminalSpeed : 57600
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// If there was an error ...
|
// If there was an error ...
|
||||||
rlogin.on('error', err => {
|
rlogin.on('error', err => {
|
||||||
self.client.log.info(`CombatNet rlogin client error: ${err.message}`);
|
self.client.log.info(
|
||||||
|
`CombatNet rlogin client error: ${err.message}`
|
||||||
|
);
|
||||||
restorePipeToNormal();
|
restorePipeToNormal();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
@@ -91,24 +91,29 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||||||
rlogin.send(buffer);
|
rlogin.send(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
rlogin.on('connect',
|
rlogin.on(
|
||||||
|
'connect',
|
||||||
/* The 'connect' event handler will be supplied with one argument,
|
/* The 'connect' event handler will be supplied with one argument,
|
||||||
a boolean indicating whether or not the connection was established. */
|
a boolean indicating whether or not the connection was established. */
|
||||||
|
|
||||||
function(state) {
|
function (state) {
|
||||||
if(state) {
|
if (state) {
|
||||||
self.client.log.info('Connected to CombatNet');
|
self.client.log.info('Connected to CombatNet');
|
||||||
self.client.term.output.on('data', sendToRloginBuffer);
|
self.client.term.output.on('data', sendToRloginBuffer);
|
||||||
|
|
||||||
doorTracking = trackDoorRunBegin(self.client);
|
doorTracking = trackDoorRunBegin(self.client);
|
||||||
} else {
|
} else {
|
||||||
return callback(Errors.General('Failed to establish establish CombatNet connection'));
|
return callback(
|
||||||
|
Errors.General(
|
||||||
|
'Failed to establish establish CombatNet connection'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// If data (a Buffer) has been received from the server ...
|
// If data (a Buffer) has been received from the server ...
|
||||||
rlogin.on('data', (data) => {
|
rlogin.on('data', data => {
|
||||||
self.client.term.rawWrite(data);
|
self.client.term.rawWrite(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,11 +121,11 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||||||
rlogin.connect();
|
rlogin.connect();
|
||||||
|
|
||||||
// note: no explicit callback() until we're finished!
|
// note: no explicit callback() until we're finished!
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.message }, 'CombatNet error');
|
self.client.log.warn({ error: err.message }, 'CombatNet error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the client is still here, go to previous
|
// if the client is still here, go to previous
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.sortAreasOrConfs = sortAreasOrConfs;
|
exports.sortAreasOrConfs = sortAreasOrConfs;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Method for sorting message, file, etc. areas and confs
|
// Method for sorting message, file, etc. areas and confs
|
||||||
@@ -19,12 +19,12 @@ function sortAreasOrConfs(areasOrConfs, type) {
|
|||||||
entryA = type ? a[type] : a;
|
entryA = type ? a[type] : a;
|
||||||
entryB = type ? b[type] : b;
|
entryB = type ? b[type] : b;
|
||||||
|
|
||||||
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
|
if (_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
|
||||||
return entryA.sort - entryB.sort;
|
return entryA.sort - entryB.sort;
|
||||||
} else {
|
} else {
|
||||||
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
|
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
|
||||||
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
|
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
|
||||||
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare
|
return keyA.localeCompare(keyB, { sensitivity: false, numeric: true }); // "natural" compare
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,11 @@ exports.Config = class Config extends ConfigLoader {
|
|||||||
'loginServers.ssh.algorithms.compress',
|
'loginServers.ssh.algorithms.compress',
|
||||||
];
|
];
|
||||||
|
|
||||||
const replaceKeys = [
|
const replaceKeys = ['args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch'];
|
||||||
'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch',
|
|
||||||
];
|
|
||||||
|
|
||||||
const configOptions = Object.assign({}, options, {
|
const configOptions = Object.assign({}, options, {
|
||||||
defaultConfig : DefaultConfig,
|
defaultConfig: DefaultConfig,
|
||||||
defaultsCustomizer : (defaultVal, configVal, key, path) => {
|
defaultsCustomizer: (defaultVal, configVal, key, path) => {
|
||||||
if (Array.isArray(defaultVal) && Array.isArray(configVal)) {
|
if (Array.isArray(defaultVal) && Array.isArray(configVal)) {
|
||||||
if (replacePaths.includes(path) || replaceKeys.includes(key)) {
|
if (replacePaths.includes(path) || replaceKeys.includes(key)) {
|
||||||
// full replacement using user config value
|
// full replacement using user config value
|
||||||
@@ -42,7 +40,7 @@ exports.Config = class Config extends ConfigLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onReload : err => {
|
onReload: err => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
Events.emit(Events.getSystemEvents().ConfigChanged);
|
Events.emit(Events.getSystemEvents().ConfigChanged);
|
||||||
|
|||||||
@@ -2,43 +2,49 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const hjson = require('hjson');
|
const hjson = require('hjson');
|
||||||
const sane = require('sane');
|
const sane = require('sane');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = new class ConfigCache
|
module.exports = new (class ConfigCache {
|
||||||
{
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cache = new Map(); // path->parsed config
|
this.cache = new Map(); // path->parsed config
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfigWithOptions(options, cb) {
|
getConfigWithOptions(options, cb) {
|
||||||
options.hotReload = _.get(options, 'hotReload', true);
|
options.hotReload = _.get(options, 'hotReload', true);
|
||||||
const cached = this.cache.has(options.filePath);
|
const cached = this.cache.has(options.filePath);
|
||||||
|
|
||||||
if(options.forceReCache || !cached) {
|
if (options.forceReCache || !cached) {
|
||||||
this.recacheConfigFromFile(options.filePath, (err, config) => {
|
this.recacheConfigFromFile(options.filePath, (err, config) => {
|
||||||
if(!err && !cached) {
|
if (!err && !cached) {
|
||||||
if(options.hotReload) {
|
if (options.hotReload) {
|
||||||
const watcher = sane(
|
const watcher = sane(paths.dirname(options.filePath), {
|
||||||
paths.dirname(options.filePath),
|
glob: `**/${paths.basename(options.filePath)}`,
|
||||||
{
|
});
|
||||||
glob : `**/${paths.basename(options.filePath)}`
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watcher.on('change', (fileName, fileRoot) => {
|
watcher.on('change', (fileName, fileRoot) => {
|
||||||
require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching');
|
require('./logger.js').log.info(
|
||||||
|
{ fileName, fileRoot },
|
||||||
|
'Configuration file changed; re-caching'
|
||||||
|
);
|
||||||
|
|
||||||
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => {
|
this.recacheConfigFromFile(
|
||||||
if(!err) {
|
paths.join(fileRoot, fileName),
|
||||||
if(options.callback) {
|
err => {
|
||||||
options.callback( { fileName, fileRoot, configCache : this } );
|
if (!err) {
|
||||||
|
if (options.callback) {
|
||||||
|
options.callback({
|
||||||
|
fileName,
|
||||||
|
fileRoot,
|
||||||
|
configCache: this,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,12 +56,12 @@ module.exports = new class ConfigCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
getConfig(filePath, cb) {
|
getConfig(filePath, cb) {
|
||||||
return this.getConfigWithOptions( { filePath }, cb);
|
return this.getConfigWithOptions({ filePath }, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
recacheConfigFromFile(path, cb) {
|
recacheConfigFromFile(path, cb) {
|
||||||
fs.readFile(path, { encoding : 'utf-8' }, (err, data) => {
|
fs.readFile(path, { encoding: 'utf-8' }, (err, data) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +69,13 @@ module.exports = new class ConfigCache
|
|||||||
try {
|
try {
|
||||||
parsed = hjson.parse(data);
|
parsed = hjson.parse(data);
|
||||||
this.cache.set(path, parsed);
|
this.cache.set(path, parsed);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' );
|
require('./logger.js').log.error(
|
||||||
} catch(ignored) {
|
{ filePath: path, error: e.message },
|
||||||
|
'Failed to re-cache'
|
||||||
|
);
|
||||||
|
} catch (ignored) {
|
||||||
// nothing - we may be failing to parse the config in which we can't log here!
|
// nothing - we may be failing to parse the config in which we can't log here!
|
||||||
}
|
}
|
||||||
return cb(e);
|
return cb(e);
|
||||||
@@ -75,4 +84,4 @@ module.exports = new class ConfigCache
|
|||||||
return cb(null, parsed);
|
return cb(null, parsed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,23 +14,21 @@ module.exports = class ConfigLoader {
|
|||||||
defaultsCustomizer = null,
|
defaultsCustomizer = null,
|
||||||
onReload = null,
|
onReload = null,
|
||||||
keepWsc = false,
|
keepWsc = false,
|
||||||
} =
|
} = {
|
||||||
{
|
hotReload: true,
|
||||||
hotReload : true,
|
defaultConfig: {},
|
||||||
defaultConfig : {},
|
defaultsCustomizer: null,
|
||||||
defaultsCustomizer : null,
|
onReload: null,
|
||||||
onReload : null,
|
keepWsc: false,
|
||||||
keepWsc : false,
|
|
||||||
}
|
}
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
this.current = {};
|
this.current = {};
|
||||||
|
|
||||||
this.hotReload = hotReload;
|
this.hotReload = hotReload;
|
||||||
this.defaultConfig = defaultConfig;
|
this.defaultConfig = defaultConfig;
|
||||||
this.defaultsCustomizer = defaultsCustomizer;
|
this.defaultsCustomizer = defaultsCustomizer;
|
||||||
this.onReload = onReload;
|
this.onReload = onReload;
|
||||||
this.keepWsc = keepWsc;
|
this.keepWsc = keepWsc;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(baseConfigPath, cb) {
|
init(baseConfigPath, cb) {
|
||||||
@@ -61,7 +59,7 @@ module.exports = class ConfigLoader {
|
|||||||
//
|
//
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
return this._loadConfigFile(baseConfigPath, callback);
|
return this._loadConfigFile(baseConfigPath, callback);
|
||||||
},
|
},
|
||||||
(config, callback) => {
|
(config, callback) => {
|
||||||
@@ -72,16 +70,17 @@ module.exports = class ConfigLoader {
|
|||||||
config,
|
config,
|
||||||
(defaultVal, configVal, key, target, source) => {
|
(defaultVal, configVal, key, target, source) => {
|
||||||
var path;
|
var path;
|
||||||
while (true) { // eslint-disable-line no-constant-condition
|
while (true) {
|
||||||
|
// eslint-disable-line no-constant-condition
|
||||||
if (!stack.length) {
|
if (!stack.length) {
|
||||||
stack.push({source, path : []});
|
stack.push({ source, path: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prev = stack[stack.length - 1];
|
const prev = stack[stack.length - 1];
|
||||||
|
|
||||||
if (source === prev.source) {
|
if (source === prev.source) {
|
||||||
path = prev.path.concat(key);
|
path = prev.path.concat(key);
|
||||||
stack.push({source : configVal, path});
|
stack.push({ source: configVal, path });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +88,12 @@ module.exports = class ConfigLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path = path.join('.');
|
path = path.join('.');
|
||||||
return this.defaultsCustomizer(defaultVal, configVal, key, path);
|
return this.defaultsCustomizer(
|
||||||
|
defaultVal,
|
||||||
|
configVal,
|
||||||
|
key,
|
||||||
|
path
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,12 +122,12 @@ module.exports = class ConfigLoader {
|
|||||||
|
|
||||||
_convertTo(value, type) {
|
_convertTo(value, type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'bool' :
|
case 'bool':
|
||||||
case 'boolean' :
|
case 'boolean':
|
||||||
value = ('1' === value || 'true' === value.toLowerCase());
|
value = '1' === value || 'true' === value.toLowerCase();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'number' :
|
case 'number':
|
||||||
{
|
{
|
||||||
const num = parseInt(value);
|
const num = parseInt(value);
|
||||||
if (!isNaN(num)) {
|
if (!isNaN(num)) {
|
||||||
@@ -132,15 +136,15 @@ module.exports = class ConfigLoader {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'object' :
|
case 'object':
|
||||||
try {
|
try {
|
||||||
value = JSON.parse(value);
|
value = JSON.parse(value);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'timestamp' :
|
case 'timestamp':
|
||||||
{
|
{
|
||||||
const m = moment(value);
|
const m = moment(value);
|
||||||
if (m.isValid()) {
|
if (m.isValid()) {
|
||||||
@@ -162,7 +166,9 @@ module.exports = class ConfigLoader {
|
|||||||
let value = process.env[varName];
|
let value = process.env[varName];
|
||||||
if (!value) {
|
if (!value) {
|
||||||
// console is about as good as we can do here
|
// console is about as good as we can do here
|
||||||
return console.info(`WARNING: environment variable "${varName}" from spec "${spec}" not found!`);
|
return console.info(
|
||||||
|
`WARNING: environment variable "${varName}" from spec "${spec}" not found!`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('array' === array) {
|
if ('array' === array) {
|
||||||
@@ -179,9 +185,9 @@ module.exports = class ConfigLoader {
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
filePath,
|
filePath,
|
||||||
hotReload : this.hotReload,
|
hotReload: this.hotReload,
|
||||||
keepWsc : this.keepWsc,
|
keepWsc: this.keepWsc,
|
||||||
callback : this._configFileChanged.bind(this),
|
callback: this._configFileChanged.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
ConfigCache.getConfigWithOptions(options, (err, config) => {
|
ConfigCache.getConfigWithOptions(options, (err, config) => {
|
||||||
@@ -192,7 +198,7 @@ module.exports = class ConfigLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_configFileChanged({fileName, fileRoot}) {
|
_configFileChanged({ fileName, fileRoot }) {
|
||||||
const reCachedPath = paths.join(fileRoot, fileName);
|
const reCachedPath = paths.join(fileRoot, fileName);
|
||||||
if (this.configPaths.includes(reCachedPath)) {
|
if (this.configPaths.includes(reCachedPath)) {
|
||||||
this._reload(this.baseConfigPath, err => {
|
this._reload(this.baseConfigPath, err => {
|
||||||
@@ -205,44 +211,44 @@ module.exports = class ConfigLoader {
|
|||||||
|
|
||||||
_resolveIncludes(configRoot, config, cb) {
|
_resolveIncludes(configRoot, config, cb) {
|
||||||
if (!Array.isArray(config.includes)) {
|
if (!Array.isArray(config.includes)) {
|
||||||
this.configPaths = [ this.baseConfigPath ];
|
this.configPaths = [this.baseConfigPath];
|
||||||
return cb(null, config);
|
return cb(null, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a included file is changed, we need to re-cache, so this
|
// If a included file is changed, we need to re-cache, so this
|
||||||
// must be tracked...
|
// must be tracked...
|
||||||
const includePaths = config.includes.map(inc => paths.join(configRoot, inc));
|
const includePaths = config.includes.map(inc => paths.join(configRoot, inc));
|
||||||
async.eachSeries(includePaths, (includePath, nextIncludePath) => {
|
async.eachSeries(
|
||||||
this._loadConfigFile(includePath, (err, includedConfig) => {
|
includePaths,
|
||||||
if (err) {
|
(includePath, nextIncludePath) => {
|
||||||
return nextIncludePath(err);
|
this._loadConfigFile(includePath, (err, includedConfig) => {
|
||||||
}
|
if (err) {
|
||||||
|
return nextIncludePath(err);
|
||||||
_.defaultsDeep(config, includedConfig);
|
|
||||||
return nextIncludePath(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
this.configPaths = [ this.baseConfigPath, ...includePaths ];
|
|
||||||
return cb(err, config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_resolveAtSpecs(config) {
|
|
||||||
return mapValuesDeep(
|
|
||||||
config,
|
|
||||||
value => {
|
|
||||||
if (_.isString(value) && '@' === value.charAt(0)) {
|
|
||||||
if (value.startsWith('@reference:')) {
|
|
||||||
const refPath = value.slice(11);
|
|
||||||
value = _.get(config, refPath, value);
|
|
||||||
} else if (value.startsWith('@environment:')) {
|
|
||||||
value = this._resolveEnvironmentVariable(value) || value;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
_.defaultsDeep(config, includedConfig);
|
||||||
|
return nextIncludePath(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.configPaths = [this.baseConfigPath, ...includePaths];
|
||||||
|
return cb(err, config);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resolveAtSpecs(config) {
|
||||||
|
return mapValuesDeep(config, value => {
|
||||||
|
if (_.isString(value) && '@' === value.charAt(0)) {
|
||||||
|
if (value.startsWith('@reference:')) {
|
||||||
|
const refPath = value.slice(11);
|
||||||
|
value = _.get(config, refPath, value);
|
||||||
|
} else if (value.startsWith('@environment:')) {
|
||||||
|
value = this._resolveEnvironmentVariable(value) || value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,26 +2,26 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
exports.connectEntry = connectEntry;
|
exports.connectEntry = connectEntry;
|
||||||
|
|
||||||
const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
|
const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
|
||||||
let giveUpTimer;
|
let giveUpTimer;
|
||||||
|
|
||||||
const done = function(err) {
|
const done = function (err) {
|
||||||
client.removeListener('cursor position report', cprListener);
|
client.removeListener('cursor position report', cprListener);
|
||||||
clearTimeout(giveUpTimer);
|
clearTimeout(giveUpTimer);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cprListener = (pos) => {
|
const cprListener = pos => {
|
||||||
cprHandler(pos);
|
cprHandler(pos);
|
||||||
return done(null);
|
return done(null);
|
||||||
};
|
};
|
||||||
@@ -29,10 +29,10 @@ const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
|
|||||||
client.once('cursor position report', cprListener);
|
client.once('cursor position report', cprListener);
|
||||||
|
|
||||||
// give up after 2s
|
// give up after 2s
|
||||||
giveUpTimer = setTimeout( () => {
|
giveUpTimer = setTimeout(() => {
|
||||||
return done(Errors.General(failMessage));
|
return done(Errors.General(failMessage));
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
};
|
||||||
|
|
||||||
function ansiDiscoverHomePosition(client, cb) {
|
function ansiDiscoverHomePosition(client, cb) {
|
||||||
//
|
//
|
||||||
@@ -41,7 +41,7 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||||||
// think of home as 0,0. If this is the case, we need to offset
|
// think of home as 0,0. If this is the case, we need to offset
|
||||||
// our positioning to accommodate for such.
|
// our positioning to accommodate for such.
|
||||||
//
|
//
|
||||||
if( !Config().term.checkAnsiHomePosition ) {
|
if (!Config().term.checkAnsiHomePosition) {
|
||||||
// Skip (and assume 1,1) if the home position check is disabled.
|
// Skip (and assume 1,1) if the home position check is disabled.
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
@@ -54,11 +54,14 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||||||
//
|
//
|
||||||
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
|
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
|
||||||
//
|
//
|
||||||
if(h > 1 || w > 1) {
|
if (h > 1 || w > 1) {
|
||||||
return client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values');
|
return client.log.warn(
|
||||||
|
{ height: h, width: w },
|
||||||
|
'Ignoring ANSI home position CPR due to unexpected values'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(0 === h & 0 === w) {
|
if ((0 === h) & (0 === w)) {
|
||||||
//
|
//
|
||||||
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount
|
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount
|
||||||
//
|
//
|
||||||
@@ -70,7 +73,7 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
|
|
||||||
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
|
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
|
||||||
}
|
}
|
||||||
|
|
||||||
function ansiAttemptDetectUTF8(client, cb) {
|
function ansiAttemptDetectUTF8(client, cb) {
|
||||||
@@ -87,7 +90,7 @@ function ansiAttemptDetectUTF8(client, cb) {
|
|||||||
// "*nix" terminal -- that is, xterm, etc.
|
// "*nix" terminal -- that is, xterm, etc.
|
||||||
// Also skip this check if checkUtf8Encoding is disabled in the config
|
// Also skip this check if checkUtf8Encoding is disabled in the config
|
||||||
|
|
||||||
if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
|
if (!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,20 +102,24 @@ function ansiAttemptDetectUTF8(client, cb) {
|
|||||||
pos => {
|
pos => {
|
||||||
initialPosition = pos;
|
initialPosition = pos;
|
||||||
|
|
||||||
withCursorPositionReport(client,
|
withCursorPositionReport(
|
||||||
|
client,
|
||||||
pos => {
|
pos => {
|
||||||
const [_, w] = pos;
|
const [_, w] = pos;
|
||||||
const len = w - initialPosition[1];
|
const len = w - initialPosition[1];
|
||||||
if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull
|
if (!isNaN(len) && len >= ASCIIPortion.length + 6) {
|
||||||
client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".');
|
// CP437 displays 3 chars each Unicode skull
|
||||||
|
client.log.info(
|
||||||
|
'Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'
|
||||||
|
);
|
||||||
client.setTermType('ansi');
|
client.setTermType('ansi');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Detect UTF-8 stage 2 timed out',
|
'Detect UTF-8 stage 2 timed out',
|
||||||
cb,
|
cb
|
||||||
);
|
);
|
||||||
|
|
||||||
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
|
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
|
||||||
client.term.rawWrite(ansi.queryPos());
|
client.term.rawWrite(ansi.queryPos());
|
||||||
},
|
},
|
||||||
'Detect UTF-8 stage 1 timed out',
|
'Detect UTF-8 stage 1 timed out',
|
||||||
@@ -158,11 +165,13 @@ const ansiQuerySyncTermFontSupport = (client, cb) => {
|
|||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
|
|
||||||
client.term.rawWrite(`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`);
|
client.term.rawWrite(
|
||||||
}
|
`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function ansiQueryTermSizeIfNeeded(client, cb) {
|
function ansiQueryTermSizeIfNeeded(client, cb) {
|
||||||
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
|
if (client.term.termHeight > 0 || client.term.termWidth > 0) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +181,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
|
|||||||
//
|
//
|
||||||
// If we've already found out, disregard
|
// If we've already found out, disregard
|
||||||
//
|
//
|
||||||
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
|
if (client.term.termHeight > 0 || client.term.termWidth > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,20 +191,21 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
|
|||||||
// 999x999 values we asked to move to.
|
// 999x999 values we asked to move to.
|
||||||
//
|
//
|
||||||
const [h, w] = pos;
|
const [h, w] = pos;
|
||||||
if(h < 10 || h === 999 || w < 10 || w === 999) {
|
if (h < 10 || h === 999 || w < 10 || w === 999) {
|
||||||
return client.log.warn(
|
return client.log.warn(
|
||||||
{ height : h, width : w },
|
{ height: h, width: w },
|
||||||
'Ignoring ANSI CPR screen size query response due to non-sane values');
|
'Ignoring ANSI CPR screen size query response due to non-sane values'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.term.termHeight = h;
|
client.term.termHeight = h;
|
||||||
client.term.termWidth = w;
|
client.term.termWidth = w;
|
||||||
|
|
||||||
client.log.debug(
|
client.log.debug(
|
||||||
{
|
{
|
||||||
termWidth : client.term.termWidth,
|
termWidth: client.term.termWidth,
|
||||||
termHeight : client.term.termHeight,
|
termHeight: client.term.termHeight,
|
||||||
source : 'ANSI CPR'
|
source: 'ANSI CPR',
|
||||||
},
|
},
|
||||||
'Window size updated'
|
'Window size updated'
|
||||||
);
|
);
|
||||||
@@ -226,8 +236,7 @@ function displayBanner(term) {
|
|||||||
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
||||||
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|
||||||
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
||||||
|00`
|
|00`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectEntry(client, nextMenu) {
|
function connectEntry(client, nextMenu) {
|
||||||
@@ -245,20 +254,23 @@ function connectEntry(client, nextMenu) {
|
|||||||
},
|
},
|
||||||
function queryTermSizeByNonStandardAnsi(callback) {
|
function queryTermSizeByNonStandardAnsi(callback) {
|
||||||
ansiQueryTermSizeIfNeeded(client, err => {
|
ansiQueryTermSizeIfNeeded(client, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
//
|
//
|
||||||
// Check again; We may have got via NAWS/similar before CPR completed.
|
// Check again; We may have got via NAWS/similar before CPR completed.
|
||||||
//
|
//
|
||||||
if(0 === term.termHeight || 0 === term.termWidth) {
|
if (0 === term.termHeight || 0 === term.termWidth) {
|
||||||
//
|
//
|
||||||
// We still don't have something good for term height/width.
|
// We still don't have something good for term height/width.
|
||||||
// Default to DOS size 80x25.
|
// Default to DOS size 80x25.
|
||||||
//
|
//
|
||||||
// :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
|
// :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
|
||||||
client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!');
|
client.log.warn(
|
||||||
|
{ reason: err.message },
|
||||||
|
'Failed to negotiate term size; Defaulting to 80x25!'
|
||||||
|
);
|
||||||
|
|
||||||
term.termHeight = 25;
|
term.termHeight = 25;
|
||||||
term.termWidth = 80;
|
term.termWidth = 80;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +280,7 @@ function connectEntry(client, nextMenu) {
|
|||||||
function checkUtf8IfNeeded(callback) {
|
function checkUtf8IfNeeded(callback) {
|
||||||
return ansiAttemptDetectUTF8(client, callback);
|
return ansiAttemptDetectUTF8(client, callback);
|
||||||
},
|
},
|
||||||
function querySyncTERMFontSupport(callback) {
|
function querySyncTERMFontSupport(callback) {
|
||||||
return ansiQuerySyncTermFontSupport(client, callback);
|
return ansiQuerySyncTermFontSupport(client, callback);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -281,9 +293,9 @@ function connectEntry(client, nextMenu) {
|
|||||||
displayBanner(term);
|
displayBanner(term);
|
||||||
|
|
||||||
// fire event
|
// fire event
|
||||||
Events.emit(Events.getSystemEvents().TermDetected, { client : client } );
|
Events.emit(Events.getSystemEvents().TermDetected, { client: client });
|
||||||
|
|
||||||
setTimeout( () => {
|
setTimeout(() => {
|
||||||
return client.menuStack.goto(nextMenu);
|
return client.menuStack.goto(nextMenu);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,265 @@
|
|||||||
|
|
||||||
|
|
||||||
const CP437UnicodeTable = [
|
const CP437UnicodeTable = [
|
||||||
'\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006',
|
'\u0000',
|
||||||
'\u0007', '\u0008', '\u0009', '\u000A', '\u000B', '\u000C', '\u000D',
|
'\u0001',
|
||||||
'\u000E', '\u000F', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014',
|
'\u0002',
|
||||||
'\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001A', '\u001B',
|
'\u0003',
|
||||||
'\u001C', '\u001D', '\u001E', '\u001F', '\u0020', '\u0021', '\u0022',
|
'\u0004',
|
||||||
'\u0023', '\u0024', '\u0025', '\u0026', '\u0027', '\u0028', '\u0029',
|
'\u0005',
|
||||||
'\u002A', '\u002B', '\u002C', '\u002D', '\u002E', '\u002F', '\u0030',
|
'\u0006',
|
||||||
'\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037',
|
'\u0007',
|
||||||
'\u0038', '\u0039', '\u003A', '\u003B', '\u003C', '\u003D', '\u003E',
|
'\u0008',
|
||||||
'\u003F', '\u0040', '\u0041', '\u0042', '\u0043', '\u0044', '\u0045',
|
'\u0009',
|
||||||
'\u0046', '\u0047', '\u0048', '\u0049', '\u004A', '\u004B', '\u004C',
|
'\u000A',
|
||||||
'\u004D', '\u004E', '\u004F', '\u0050', '\u0051', '\u0052', '\u0053',
|
'\u000B',
|
||||||
'\u0054', '\u0055', '\u0056', '\u0057', '\u0058', '\u0059', '\u005A',
|
'\u000C',
|
||||||
'\u005B', '\u005C', '\u005D', '\u005E', '\u005F', '\u0060', '\u0061',
|
'\u000D',
|
||||||
'\u0062', '\u0063', '\u0064', '\u0065', '\u0066', '\u0067', '\u0068',
|
'\u000E',
|
||||||
'\u0069', '\u006A', '\u006B', '\u006C', '\u006D', '\u006E', '\u006F',
|
'\u000F',
|
||||||
'\u0070', '\u0071', '\u0072', '\u0073', '\u0074', '\u0075', '\u0076',
|
'\u0010',
|
||||||
'\u0077', '\u0078', '\u0079', '\u007A', '\u007B', '\u007C', '\u007D',
|
'\u0011',
|
||||||
'\u007E', '\u007F', '\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4',
|
'\u0012',
|
||||||
'\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB', '\u00E8', '\u00EF',
|
'\u0013',
|
||||||
'\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6',
|
'\u0014',
|
||||||
'\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9', '\u00FF', '\u00D6',
|
'\u0015',
|
||||||
'\u00DC', '\u00A2', '\u00A3', '\u00A5', '\u20A7', '\u0192', '\u00E1',
|
'\u0016',
|
||||||
'\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1', '\u00AA', '\u00BA',
|
'\u0017',
|
||||||
'\u00BF', '\u2310', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB',
|
'\u0018',
|
||||||
'\u00BB', '\u2591', '\u2592', '\u2593', '\u2502', '\u2524', '\u2561',
|
'\u0019',
|
||||||
'\u2562', '\u2556', '\u2555', '\u2563', '\u2551', '\u2557', '\u255D',
|
'\u001A',
|
||||||
'\u255C', '\u255B', '\u2510', '\u2514', '\u2534', '\u252C', '\u251C',
|
'\u001B',
|
||||||
'\u2500', '\u253C', '\u255E', '\u255F', '\u255A', '\u2554', '\u2569',
|
'\u001C',
|
||||||
'\u2566', '\u2560', '\u2550', '\u256C', '\u2567', '\u2568', '\u2564',
|
'\u001D',
|
||||||
'\u2565', '\u2559', '\u2558', '\u2552', '\u2553', '\u256B', '\u256A',
|
'\u001E',
|
||||||
'\u2518', '\u250C', '\u2588', '\u2584', '\u258C', '\u2590', '\u2580',
|
'\u001F',
|
||||||
'\u03B1', '\u00DF', '\u0393', '\u03C0', '\u03A3', '\u03C3', '\u00B5',
|
'\u0020',
|
||||||
'\u03C4', '\u03A6', '\u0398', '\u03A9', '\u03B4', '\u221E', '\u03C6',
|
'\u0021',
|
||||||
'\u03B5', '\u2229', '\u2261', '\u00B1', '\u2265', '\u2264', '\u2320',
|
'\u0022',
|
||||||
'\u2321', '\u00F7', '\u2248', '\u00B0', '\u2219', '\u00B7', '\u221A',
|
'\u0023',
|
||||||
'\u207F', '\u00B2', '\u25A0', '\u00A0'
|
'\u0024',
|
||||||
|
'\u0025',
|
||||||
|
'\u0026',
|
||||||
|
'\u0027',
|
||||||
|
'\u0028',
|
||||||
|
'\u0029',
|
||||||
|
'\u002A',
|
||||||
|
'\u002B',
|
||||||
|
'\u002C',
|
||||||
|
'\u002D',
|
||||||
|
'\u002E',
|
||||||
|
'\u002F',
|
||||||
|
'\u0030',
|
||||||
|
'\u0031',
|
||||||
|
'\u0032',
|
||||||
|
'\u0033',
|
||||||
|
'\u0034',
|
||||||
|
'\u0035',
|
||||||
|
'\u0036',
|
||||||
|
'\u0037',
|
||||||
|
'\u0038',
|
||||||
|
'\u0039',
|
||||||
|
'\u003A',
|
||||||
|
'\u003B',
|
||||||
|
'\u003C',
|
||||||
|
'\u003D',
|
||||||
|
'\u003E',
|
||||||
|
'\u003F',
|
||||||
|
'\u0040',
|
||||||
|
'\u0041',
|
||||||
|
'\u0042',
|
||||||
|
'\u0043',
|
||||||
|
'\u0044',
|
||||||
|
'\u0045',
|
||||||
|
'\u0046',
|
||||||
|
'\u0047',
|
||||||
|
'\u0048',
|
||||||
|
'\u0049',
|
||||||
|
'\u004A',
|
||||||
|
'\u004B',
|
||||||
|
'\u004C',
|
||||||
|
'\u004D',
|
||||||
|
'\u004E',
|
||||||
|
'\u004F',
|
||||||
|
'\u0050',
|
||||||
|
'\u0051',
|
||||||
|
'\u0052',
|
||||||
|
'\u0053',
|
||||||
|
'\u0054',
|
||||||
|
'\u0055',
|
||||||
|
'\u0056',
|
||||||
|
'\u0057',
|
||||||
|
'\u0058',
|
||||||
|
'\u0059',
|
||||||
|
'\u005A',
|
||||||
|
'\u005B',
|
||||||
|
'\u005C',
|
||||||
|
'\u005D',
|
||||||
|
'\u005E',
|
||||||
|
'\u005F',
|
||||||
|
'\u0060',
|
||||||
|
'\u0061',
|
||||||
|
'\u0062',
|
||||||
|
'\u0063',
|
||||||
|
'\u0064',
|
||||||
|
'\u0065',
|
||||||
|
'\u0066',
|
||||||
|
'\u0067',
|
||||||
|
'\u0068',
|
||||||
|
'\u0069',
|
||||||
|
'\u006A',
|
||||||
|
'\u006B',
|
||||||
|
'\u006C',
|
||||||
|
'\u006D',
|
||||||
|
'\u006E',
|
||||||
|
'\u006F',
|
||||||
|
'\u0070',
|
||||||
|
'\u0071',
|
||||||
|
'\u0072',
|
||||||
|
'\u0073',
|
||||||
|
'\u0074',
|
||||||
|
'\u0075',
|
||||||
|
'\u0076',
|
||||||
|
'\u0077',
|
||||||
|
'\u0078',
|
||||||
|
'\u0079',
|
||||||
|
'\u007A',
|
||||||
|
'\u007B',
|
||||||
|
'\u007C',
|
||||||
|
'\u007D',
|
||||||
|
'\u007E',
|
||||||
|
'\u007F',
|
||||||
|
'\u00C7',
|
||||||
|
'\u00FC',
|
||||||
|
'\u00E9',
|
||||||
|
'\u00E2',
|
||||||
|
'\u00E4',
|
||||||
|
'\u00E0',
|
||||||
|
'\u00E5',
|
||||||
|
'\u00E7',
|
||||||
|
'\u00EA',
|
||||||
|
'\u00EB',
|
||||||
|
'\u00E8',
|
||||||
|
'\u00EF',
|
||||||
|
'\u00EE',
|
||||||
|
'\u00EC',
|
||||||
|
'\u00C4',
|
||||||
|
'\u00C5',
|
||||||
|
'\u00C9',
|
||||||
|
'\u00E6',
|
||||||
|
'\u00C6',
|
||||||
|
'\u00F4',
|
||||||
|
'\u00F6',
|
||||||
|
'\u00F2',
|
||||||
|
'\u00FB',
|
||||||
|
'\u00F9',
|
||||||
|
'\u00FF',
|
||||||
|
'\u00D6',
|
||||||
|
'\u00DC',
|
||||||
|
'\u00A2',
|
||||||
|
'\u00A3',
|
||||||
|
'\u00A5',
|
||||||
|
'\u20A7',
|
||||||
|
'\u0192',
|
||||||
|
'\u00E1',
|
||||||
|
'\u00ED',
|
||||||
|
'\u00F3',
|
||||||
|
'\u00FA',
|
||||||
|
'\u00F1',
|
||||||
|
'\u00D1',
|
||||||
|
'\u00AA',
|
||||||
|
'\u00BA',
|
||||||
|
'\u00BF',
|
||||||
|
'\u2310',
|
||||||
|
'\u00AC',
|
||||||
|
'\u00BD',
|
||||||
|
'\u00BC',
|
||||||
|
'\u00A1',
|
||||||
|
'\u00AB',
|
||||||
|
'\u00BB',
|
||||||
|
'\u2591',
|
||||||
|
'\u2592',
|
||||||
|
'\u2593',
|
||||||
|
'\u2502',
|
||||||
|
'\u2524',
|
||||||
|
'\u2561',
|
||||||
|
'\u2562',
|
||||||
|
'\u2556',
|
||||||
|
'\u2555',
|
||||||
|
'\u2563',
|
||||||
|
'\u2551',
|
||||||
|
'\u2557',
|
||||||
|
'\u255D',
|
||||||
|
'\u255C',
|
||||||
|
'\u255B',
|
||||||
|
'\u2510',
|
||||||
|
'\u2514',
|
||||||
|
'\u2534',
|
||||||
|
'\u252C',
|
||||||
|
'\u251C',
|
||||||
|
'\u2500',
|
||||||
|
'\u253C',
|
||||||
|
'\u255E',
|
||||||
|
'\u255F',
|
||||||
|
'\u255A',
|
||||||
|
'\u2554',
|
||||||
|
'\u2569',
|
||||||
|
'\u2566',
|
||||||
|
'\u2560',
|
||||||
|
'\u2550',
|
||||||
|
'\u256C',
|
||||||
|
'\u2567',
|
||||||
|
'\u2568',
|
||||||
|
'\u2564',
|
||||||
|
'\u2565',
|
||||||
|
'\u2559',
|
||||||
|
'\u2558',
|
||||||
|
'\u2552',
|
||||||
|
'\u2553',
|
||||||
|
'\u256B',
|
||||||
|
'\u256A',
|
||||||
|
'\u2518',
|
||||||
|
'\u250C',
|
||||||
|
'\u2588',
|
||||||
|
'\u2584',
|
||||||
|
'\u258C',
|
||||||
|
'\u2590',
|
||||||
|
'\u2580',
|
||||||
|
'\u03B1',
|
||||||
|
'\u00DF',
|
||||||
|
'\u0393',
|
||||||
|
'\u03C0',
|
||||||
|
'\u03A3',
|
||||||
|
'\u03C3',
|
||||||
|
'\u00B5',
|
||||||
|
'\u03C4',
|
||||||
|
'\u03A6',
|
||||||
|
'\u0398',
|
||||||
|
'\u03A9',
|
||||||
|
'\u03B4',
|
||||||
|
'\u221E',
|
||||||
|
'\u03C6',
|
||||||
|
'\u03B5',
|
||||||
|
'\u2229',
|
||||||
|
'\u2261',
|
||||||
|
'\u00B1',
|
||||||
|
'\u2265',
|
||||||
|
'\u2264',
|
||||||
|
'\u2320',
|
||||||
|
'\u2321',
|
||||||
|
'\u00F7',
|
||||||
|
'\u2248',
|
||||||
|
'\u00B0',
|
||||||
|
'\u2219',
|
||||||
|
'\u00B7',
|
||||||
|
'\u221A',
|
||||||
|
'\u207F',
|
||||||
|
'\u00B2',
|
||||||
|
'\u25A0',
|
||||||
|
'\u00A0',
|
||||||
];
|
];
|
||||||
|
|
||||||
const NonCP437EncodableRegExp = /[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; // eslint-disable-line no-control-regex
|
const NonCP437EncodableRegExp =
|
||||||
const isCP437Encodable = (s) => {
|
/[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; // eslint-disable-line no-control-regex
|
||||||
|
const isCP437Encodable = s => {
|
||||||
if (!s.length) {
|
if (!s.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
48
core/crc.js
48
core/crc.js
@@ -38,7 +38,7 @@ const CRC32_TABLE = new Int32Array([
|
|||||||
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
||||||
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
||||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
exports.CRC32 = class CRC32 {
|
exports.CRC32 = class CRC32 {
|
||||||
@@ -52,40 +52,40 @@ exports.CRC32 = class CRC32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_4(input) {
|
update_4(input) {
|
||||||
const len = input.length - 3;
|
const len = input.length - 3;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for(i = 0; i < len;) {
|
for (i = 0; i < len; ) {
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
}
|
}
|
||||||
while(i < len + 3) {
|
while (i < len + 3) {
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_8(input) {
|
update_8(input) {
|
||||||
const len = input.length - 7;
|
const len = input.length - 7;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for(i = 0; i < len;) {
|
for (i = 0; i < len; ) {
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
}
|
}
|
||||||
while(i < len + 7) {
|
while (i < len + 7) {
|
||||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ];
|
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalize() {
|
finalize() {
|
||||||
return (this.crc ^ (-1)) >>> 0;
|
return (this.crc ^ -1) >>> 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
127
core/database.js
127
core/database.js
@@ -5,25 +5,25 @@
|
|||||||
const conf = require('./config');
|
const conf = require('./config');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const sqlite3 = require('sqlite3');
|
const sqlite3 = require('sqlite3');
|
||||||
const sqlite3Trans = require('sqlite3-trans');
|
const sqlite3Trans = require('sqlite3-trans');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
// database handles
|
// database handles
|
||||||
const dbs = {};
|
const dbs = {};
|
||||||
|
|
||||||
exports.getTransactionDatabase = getTransactionDatabase;
|
exports.getTransactionDatabase = getTransactionDatabase;
|
||||||
exports.getModDatabasePath = getModDatabasePath;
|
exports.getModDatabasePath = getModDatabasePath;
|
||||||
exports.loadDatabaseForMod = loadDatabaseForMod;
|
exports.loadDatabaseForMod = loadDatabaseForMod;
|
||||||
exports.getISOTimestampString = getISOTimestampString;
|
exports.getISOTimestampString = getISOTimestampString;
|
||||||
exports.sanitizeString = sanitizeString;
|
exports.sanitizeString = sanitizeString;
|
||||||
exports.initializeDatabases = initializeDatabases;
|
exports.initializeDatabases = initializeDatabases;
|
||||||
|
|
||||||
exports.dbs = dbs;
|
exports.dbs = dbs;
|
||||||
|
|
||||||
function getTransactionDatabase(db) {
|
function getTransactionDatabase(db) {
|
||||||
return sqlite3Trans.wrap(db);
|
return sqlite3Trans.wrap(db);
|
||||||
@@ -40,37 +40,38 @@ function getModDatabasePath(moduleInfo, suffix) {
|
|||||||
// We expect that moduleInfo defines packageName which will be the base of the modules
|
// We expect that moduleInfo defines packageName which will be the base of the modules
|
||||||
// filename. An optional suffix may be supplied as well.
|
// filename. An optional suffix may be supplied as well.
|
||||||
//
|
//
|
||||||
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
const HOST_RE =
|
||||||
|
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||||
|
|
||||||
assert(_.isObject(moduleInfo));
|
assert(_.isObject(moduleInfo));
|
||||||
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
|
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
|
||||||
|
|
||||||
let full = moduleInfo.packageName;
|
let full = moduleInfo.packageName;
|
||||||
if(suffix) {
|
if (suffix) {
|
||||||
full += `.${suffix}`;
|
full += `.${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
(full.split('.').length > 1 && HOST_RE.test(full)),
|
full.split('.').length > 1 && HOST_RE.test(full),
|
||||||
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation');
|
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'
|
||||||
|
);
|
||||||
|
|
||||||
const Config = conf.get();
|
const Config = conf.get();
|
||||||
return paths.join(Config.paths.modsDb, `${full}.sqlite3`);
|
return paths.join(Config.paths.modsDb, `${full}.sqlite3`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDatabaseForMod(modInfo, cb) {
|
function loadDatabaseForMod(modInfo, cb) {
|
||||||
const db = getTransactionDatabase(new sqlite3.Database(
|
const db = getTransactionDatabase(
|
||||||
getModDatabasePath(modInfo),
|
new sqlite3.Database(getModDatabasePath(modInfo), err => {
|
||||||
err => {
|
|
||||||
return cb(err, db);
|
return cb(err, db);
|
||||||
}
|
})
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getISOTimestampString(ts) {
|
function getISOTimestampString(ts) {
|
||||||
ts = ts || moment();
|
ts = ts || moment();
|
||||||
if(!moment.isMoment(ts)) {
|
if (!moment.isMoment(ts)) {
|
||||||
if(_.isString(ts)) {
|
if (_.isString(ts)) {
|
||||||
ts = ts.replace(/\//g, '-');
|
ts = ts.replace(/\//g, '-');
|
||||||
}
|
}
|
||||||
ts = moment(ts);
|
ts = moment(ts);
|
||||||
@@ -79,42 +80,55 @@ function getISOTimestampString(ts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeString(s) {
|
function sanitizeString(s) {
|
||||||
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex
|
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => {
|
||||||
|
// eslint-disable-line no-control-regex
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case '\0' : return '\\0';
|
case '\0':
|
||||||
case '\x08' : return '\\b';
|
return '\\0';
|
||||||
case '\x09' : return '\\t';
|
case '\x08':
|
||||||
case '\x1a' : return '\\z';
|
return '\\b';
|
||||||
case '\n' : return '\\n';
|
case '\x09':
|
||||||
case '\r' : return '\\r';
|
return '\\t';
|
||||||
|
case '\x1a':
|
||||||
|
return '\\z';
|
||||||
|
case '\n':
|
||||||
|
return '\\n';
|
||||||
|
case '\r':
|
||||||
|
return '\\r';
|
||||||
|
|
||||||
case '"' :
|
case '"':
|
||||||
case '\'' :
|
case "'":
|
||||||
return `${c}${c}`;
|
return `${c}${c}`;
|
||||||
|
|
||||||
case '\\' :
|
case '\\':
|
||||||
case '%' :
|
case '%':
|
||||||
return `\\${c}`;
|
return `\\${c}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeDatabases(cb) {
|
function initializeDatabases(cb) {
|
||||||
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
|
async.eachSeries(
|
||||||
dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => {
|
['system', 'user', 'message', 'file'],
|
||||||
if(err) {
|
(dbName, next) => {
|
||||||
return cb(err);
|
dbs[dbName] = sqlite3Trans.wrap(
|
||||||
}
|
new sqlite3.Database(getDatabasePath(dbName), err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
dbs[dbName].serialize( () => {
|
dbs[dbName].serialize(() => {
|
||||||
DB_INIT_TABLE[dbName]( () => {
|
DB_INIT_TABLE[dbName](() => {
|
||||||
return next(null);
|
return next(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}));
|
})
|
||||||
}, err => {
|
);
|
||||||
return cb(err);
|
},
|
||||||
});
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableForeignKeys(db) {
|
function enableForeignKeys(db) {
|
||||||
@@ -122,7 +136,7 @@ function enableForeignKeys(db) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DB_INIT_TABLE = {
|
const DB_INIT_TABLE = {
|
||||||
system : (cb) => {
|
system: cb => {
|
||||||
enableForeignKeys(dbs.system);
|
enableForeignKeys(dbs.system);
|
||||||
|
|
||||||
// Various stat/event logging - see stat_log.js
|
// Various stat/event logging - see stat_log.js
|
||||||
@@ -160,7 +174,7 @@ const DB_INIT_TABLE = {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
user : (cb) => {
|
user: cb => {
|
||||||
enableForeignKeys(dbs.user);
|
enableForeignKeys(dbs.user);
|
||||||
|
|
||||||
dbs.user.run(
|
dbs.user.run(
|
||||||
@@ -229,7 +243,7 @@ const DB_INIT_TABLE = {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
message : (cb) => {
|
message: cb => {
|
||||||
enableForeignKeys(dbs.message);
|
enableForeignKeys(dbs.message);
|
||||||
|
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
@@ -296,7 +310,6 @@ const DB_INIT_TABLE = {
|
|||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// :TODO: need SQL to ensure cleaned up if delete from message?
|
// :TODO: need SQL to ensure cleaned up if delete from message?
|
||||||
/*
|
/*
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
@@ -337,7 +350,7 @@ const DB_INIT_TABLE = {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
file : (cb) => {
|
file: cb => {
|
||||||
enableForeignKeys(dbs.file);
|
enableForeignKeys(dbs.file);
|
||||||
|
|
||||||
dbs.file.run(
|
dbs.file.run(
|
||||||
@@ -457,5 +470,5 @@ const DB_INIT_TABLE = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
module.exports = class DescriptIonFile {
|
module.exports = class DescriptIonFile {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -19,14 +19,14 @@ module.exports = class DescriptIonFile {
|
|||||||
|
|
||||||
getDescription(fileName) {
|
getDescription(fileName) {
|
||||||
const entry = this.get(fileName);
|
const entry = this.get(fileName);
|
||||||
if(entry) {
|
if (entry) {
|
||||||
return entry.desc;
|
return entry.desc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static createFromFile(path, cb) {
|
static createFromFile(path, cb) {
|
||||||
fs.readFile(path, (err, descData) => {
|
fs.readFile(path, (err, descData) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,43 +35,48 @@ module.exports = class DescriptIonFile {
|
|||||||
// DESCRIPT.ION entries are terminated with a CR and/or LF
|
// DESCRIPT.ION entries are terminated with a CR and/or LF
|
||||||
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
|
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
|
||||||
|
|
||||||
async.each(lines, (entryData, nextLine) => {
|
async.each(
|
||||||
//
|
lines,
|
||||||
// We allow quoted (long) filenames or non-quoted filenames.
|
(entryData, nextLine) => {
|
||||||
// FILENAME<SPC>DESC<0x04><program data><CR/LF>
|
//
|
||||||
//
|
// We allow quoted (long) filenames or non-quoted filenames.
|
||||||
const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex
|
// FILENAME<SPC>DESC<0x04><program data><CR/LF>
|
||||||
if(!parts) {
|
//
|
||||||
return nextLine(null);
|
const parts = entryData.match(
|
||||||
}
|
/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/
|
||||||
|
); // eslint-disable-line no-control-regex
|
||||||
const fileName = parts[1] || parts[2];
|
if (!parts) {
|
||||||
|
return nextLine(null);
|
||||||
//
|
|
||||||
// Un-escape CR/LF's
|
|
||||||
// - escapped \r and/or \n
|
|
||||||
// - BBBS style @n - See https://www.bbbs.net/sysop.html
|
|
||||||
//
|
|
||||||
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
|
|
||||||
|
|
||||||
descIonFile.entries.set(
|
|
||||||
fileName,
|
|
||||||
{
|
|
||||||
desc : desc,
|
|
||||||
programId : parts[4],
|
|
||||||
programData : parts[5],
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return nextLine(null);
|
const fileName = parts[1] || parts[2];
|
||||||
},
|
|
||||||
() => {
|
//
|
||||||
return cb(
|
// Un-escape CR/LF's
|
||||||
descIonFile.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized DESCRIPT.ION format'),
|
// - escapped \r and/or \n
|
||||||
descIonFile
|
// - BBBS style @n - See https://www.bbbs.net/sysop.html
|
||||||
);
|
//
|
||||||
});
|
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
|
||||||
|
|
||||||
|
descIonFile.entries.set(fileName, {
|
||||||
|
desc: desc,
|
||||||
|
programId: parts[4],
|
||||||
|
programData: parts[5],
|
||||||
|
});
|
||||||
|
|
||||||
|
return nextLine(null);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return cb(
|
||||||
|
descIonFile.entries.size > 0
|
||||||
|
? null
|
||||||
|
: Errors.Invalid(
|
||||||
|
'Invalid or unrecognized DESCRIPT.ION format'
|
||||||
|
),
|
||||||
|
descIonFile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
89
core/door.js
89
core/door.js
@@ -1,28 +1,28 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const Events = require('./events');
|
const Events = require('./events');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const pty = require('node-pty');
|
const pty = require('node-pty');
|
||||||
const decode = require('iconv-lite').decode;
|
const decode = require('iconv-lite').decode;
|
||||||
const createServer = require('net').createServer;
|
const createServer = require('net').createServer;
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = class Door {
|
module.exports = class Door {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.restored = false;
|
this.restored = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare(ioType, cb) {
|
prepare(ioType, cb) {
|
||||||
this.io = ioType;
|
this.io = ioType;
|
||||||
|
|
||||||
// we currently only have to do any real setup for 'socket'
|
// we currently only have to do any real setup for 'socket'
|
||||||
if('socket' !== ioType) {
|
if ('socket' !== ioType) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ module.exports = class Door {
|
|||||||
return this.restoreIo(conn);
|
return this.restoreIo(conn);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sockServer.getConnections( (err, count) => {
|
this.sockServer.getConnections((err, count) => {
|
||||||
// We expect only one connection from our DOOR/emulator/etc.
|
// We expect only one connection from our DOOR/emulator/etc.
|
||||||
if(!err && count <= 1) {
|
if (!err && count <= 1) {
|
||||||
this.client.term.output.pipe(conn);
|
this.client.term.output.pipe(conn);
|
||||||
conn.on('data', this.doorDataHandler.bind(this));
|
conn.on('data', this.doorDataHandler.bind(this));
|
||||||
}
|
}
|
||||||
@@ -53,24 +53,24 @@ module.exports = class Door {
|
|||||||
run(exeInfo, cb) {
|
run(exeInfo, cb) {
|
||||||
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
||||||
|
|
||||||
if('socket' === this.io && !this.sockServer) {
|
if ('socket' === this.io && !this.sockServer) {
|
||||||
return cb(Errors.UnexpectedState('Socket server is not running'));
|
return cb(Errors.UnexpectedState('Socket server is not running'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
|
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
|
||||||
|
|
||||||
const formatObj = {
|
const formatObj = {
|
||||||
dropFile : exeInfo.dropFile,
|
dropFile: exeInfo.dropFile,
|
||||||
dropFilePath : exeInfo.dropFilePath,
|
dropFilePath: exeInfo.dropFilePath,
|
||||||
node : exeInfo.node.toString(),
|
node: exeInfo.node.toString(),
|
||||||
srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1',
|
srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1',
|
||||||
userId : this.client.user.userId.toString(),
|
userId: this.client.user.userId.toString(),
|
||||||
userName : this.client.user.getSanitizedName(),
|
userName: this.client.user.getSanitizedName(),
|
||||||
userNameRaw : this.client.user.username,
|
userNameRaw: this.client.user.username,
|
||||||
cwd : cwd,
|
cwd: cwd,
|
||||||
};
|
};
|
||||||
|
|
||||||
const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) );
|
const args = exeInfo.args.map(arg => stringFormat(arg, formatObj));
|
||||||
|
|
||||||
this.client.log.info(
|
this.client.log.info(
|
||||||
{ cmd : exeInfo.cmd, args, io : this.io },
|
{ cmd : exeInfo.cmd, args, io : this.io },
|
||||||
@@ -79,13 +79,13 @@ module.exports = class Door {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.doorPty = pty.spawn(exeInfo.cmd, args, {
|
this.doorPty = pty.spawn(exeInfo.cmd, args, {
|
||||||
cols : this.client.term.termWidth,
|
cols: this.client.term.termWidth,
|
||||||
rows : this.client.term.termHeight,
|
rows: this.client.term.termHeight,
|
||||||
cwd : cwd,
|
cwd: cwd,
|
||||||
env : exeInfo.env,
|
env: exeInfo.env,
|
||||||
encoding : null, // we want to handle all encoding ourself
|
encoding: null, // we want to handle all encoding ourself
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +93,12 @@ module.exports = class Door {
|
|||||||
// PID is launched. Make sure it's killed off if the user disconnects.
|
// PID is launched. Make sure it's killed off if the user disconnects.
|
||||||
//
|
//
|
||||||
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
||||||
if (this.doorPty && this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')) {
|
if (
|
||||||
|
this.doorPty &&
|
||||||
|
this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')
|
||||||
|
) {
|
||||||
this.client.log.info(
|
this.client.log.info(
|
||||||
{ pid : this.doorPty.pid },
|
{ pid: this.doorPty.pid },
|
||||||
'User has disconnected; Killing door process.'
|
'User has disconnected; Killing door process.'
|
||||||
);
|
);
|
||||||
this.doorPty.kill();
|
this.doorPty.kill();
|
||||||
@@ -103,10 +106,11 @@ module.exports = class Door {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.client.log.debug(
|
this.client.log.debug(
|
||||||
{ processId : this.doorPty.pid }, 'External door process spawned'
|
{ processId: this.doorPty.pid },
|
||||||
|
'External door process spawned'
|
||||||
);
|
);
|
||||||
|
|
||||||
if('stdio' === this.io) {
|
if ('stdio' === this.io) {
|
||||||
this.client.log.debug('Using stdio for door I/O');
|
this.client.log.debug('Using stdio for door I/O');
|
||||||
|
|
||||||
this.client.term.output.pipe(this.doorPty);
|
this.client.term.output.pipe(this.doorPty);
|
||||||
@@ -116,22 +120,25 @@ module.exports = class Door {
|
|||||||
this.doorPty.once('close', () => {
|
this.doorPty.once('close', () => {
|
||||||
return this.restoreIo(this.doorPty);
|
return this.restoreIo(this.doorPty);
|
||||||
});
|
});
|
||||||
} else if('socket' === this.io) {
|
} else if ('socket' === this.io) {
|
||||||
this.client.log.debug(
|
this.client.log.debug(
|
||||||
{ srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket },
|
{
|
||||||
|
srvPort: this.sockServer.address().port,
|
||||||
|
srvSocket: this.sockServerSocket,
|
||||||
|
},
|
||||||
'Using temporary socket server for door I/O'
|
'Using temporary socket server for door I/O'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.doorPty.once('exit', exitCode => {
|
this.doorPty.once('exit', exitCode => {
|
||||||
this.client.log.info( { exitCode : exitCode }, 'Door exited');
|
this.client.log.info({ exitCode: exitCode }, 'Door exited');
|
||||||
|
|
||||||
if(this.sockServer) {
|
if (this.sockServer) {
|
||||||
this.sockServer.close();
|
this.sockServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// we may not get a close
|
// we may not get a close
|
||||||
if('stdio' === this.io) {
|
if ('stdio' === this.io) {
|
||||||
this.restoreIo(this.doorPty);
|
this.restoreIo(this.doorPty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,13 +154,13 @@ module.exports = class Door {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restoreIo(piped) {
|
restoreIo(piped) {
|
||||||
if(!this.restored) {
|
if (!this.restored) {
|
||||||
if(this.doorPty) {
|
if (this.doorPty) {
|
||||||
this.doorPty.kill();
|
this.doorPty.kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = this.client.term.output;
|
const output = this.client.term.output;
|
||||||
if(output) {
|
if (output) {
|
||||||
output.unpipe(piped);
|
output.unpipe(piped);
|
||||||
output.resume();
|
output.resume();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const { resetScreen } = require('./ansi_term.js');
|
const { resetScreen } = require('./ansi_term.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const {
|
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
||||||
trackDoorRunBegin,
|
|
||||||
trackDoorRunEnd
|
|
||||||
} = require('./door_util.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const SSHClient = require('ssh2').Client;
|
const SSHClient = require('ssh2').Client;
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'DoorParty',
|
name: 'DoorParty',
|
||||||
desc : 'DoorParty Access Module',
|
desc: 'DoorParty Access Module',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class DoorPartyModule extends MenuModule {
|
exports.getModule = class DoorPartyModule extends MenuModule {
|
||||||
@@ -25,10 +22,10 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
// establish defaults
|
// establish defaults
|
||||||
this.config = options.menuConfig.config;
|
this.config = options.menuConfig.config;
|
||||||
this.config.host = this.config.host || 'dp.throwbackbbs.com';
|
this.config.host = this.config.host || 'dp.throwbackbbs.com';
|
||||||
this.config.sshPort = this.config.sshPort || 2022;
|
this.config.sshPort = this.config.sshPort || 2022;
|
||||||
this.config.rloginPort = this.config.rloginPort || 513;
|
this.config.rloginPort = this.config.rloginPort || 513;
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
@@ -40,12 +37,12 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
return self.validateConfigFields(
|
return self.validateConfigFields(
|
||||||
{
|
{
|
||||||
host : 'string',
|
host: 'string',
|
||||||
username : 'string',
|
username: 'string',
|
||||||
password : 'string',
|
password: 'string',
|
||||||
bbsTag : 'string',
|
bbsTag: 'string',
|
||||||
sshPort : 'number',
|
sshPort: 'number',
|
||||||
rloginPort : 'number',
|
rloginPort: 'number',
|
||||||
},
|
},
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
@@ -60,12 +57,12 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||||||
let pipedStream;
|
let pipedStream;
|
||||||
let doorTracking;
|
let doorTracking;
|
||||||
|
|
||||||
const restorePipe = function() {
|
const restorePipe = function () {
|
||||||
if(pipedStream && !pipeRestored && !clientTerminated) {
|
if (pipedStream && !pipeRestored && !clientTerminated) {
|
||||||
self.client.term.output.unpipe(pipedStream);
|
self.client.term.output.unpipe(pipedStream);
|
||||||
self.client.term.output.resume();
|
self.client.term.output.resume();
|
||||||
|
|
||||||
if(doorTracking) {
|
if (doorTracking) {
|
||||||
trackDoorRunEnd(doorTracking);
|
trackDoorRunEnd(doorTracking);
|
||||||
doorTracking = null;
|
doorTracking = null;
|
||||||
}
|
}
|
||||||
@@ -75,48 +72,60 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||||||
sshClient.on('ready', () => {
|
sshClient.on('ready', () => {
|
||||||
// track client termination so we can clean up early
|
// track client termination so we can clean up early
|
||||||
self.client.once('end', () => {
|
self.client.once('end', () => {
|
||||||
self.client.log.info('Connection ended. Terminating DoorParty connection');
|
self.client.log.info(
|
||||||
|
'Connection ended. Terminating DoorParty connection'
|
||||||
|
);
|
||||||
clientTerminated = true;
|
clientTerminated = true;
|
||||||
sshClient.end();
|
sshClient.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
// establish tunnel for rlogin
|
// establish tunnel for rlogin
|
||||||
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
|
sshClient.forwardOut(
|
||||||
if(err) {
|
'127.0.0.1',
|
||||||
return callback(Errors.General('Failed to establish tunnel'));
|
self.config.sshPort,
|
||||||
|
self.config.host,
|
||||||
|
self.config.rloginPort,
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(
|
||||||
|
Errors.General('Failed to establish tunnel')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
doorTracking = trackDoorRunBegin(self.client);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Send rlogin
|
||||||
|
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
|
||||||
|
// [XA]nuskooler
|
||||||
|
//
|
||||||
|
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
|
||||||
|
stream.write(rlogin);
|
||||||
|
|
||||||
|
pipedStream = stream; // :TODO: this is hacky...
|
||||||
|
self.client.term.output.pipe(stream);
|
||||||
|
|
||||||
|
stream.on('data', d => {
|
||||||
|
// :TODO: we should just pipe this...
|
||||||
|
self.client.term.rawWrite(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
sshClient.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('close', () => {
|
||||||
|
restorePipe();
|
||||||
|
sshClient.end();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
);
|
||||||
doorTracking = trackDoorRunBegin(self.client);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Send rlogin
|
|
||||||
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
|
|
||||||
// [XA]nuskooler
|
|
||||||
//
|
|
||||||
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
|
|
||||||
stream.write(rlogin);
|
|
||||||
|
|
||||||
pipedStream = stream; // :TODO: this is hacky...
|
|
||||||
self.client.term.output.pipe(stream);
|
|
||||||
|
|
||||||
stream.on('data', d => {
|
|
||||||
// :TODO: we should just pipe this...
|
|
||||||
self.client.term.rawWrite(d);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', () => {
|
|
||||||
sshClient.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('close', () => {
|
|
||||||
restorePipe();
|
|
||||||
sshClient.end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sshClient.on('error', err => {
|
sshClient.on('error', err => {
|
||||||
self.client.log.info(`DoorParty SSH client error: ${err.message}`);
|
self.client.log.info(
|
||||||
|
`DoorParty SSH client error: ${err.message}`
|
||||||
|
);
|
||||||
trackDoorRunEnd(doorTracking);
|
trackDoorRunEnd(doorTracking);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,23 +134,23 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||||||
callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
sshClient.connect( {
|
sshClient.connect({
|
||||||
host : self.config.host,
|
host: self.config.host,
|
||||||
port : self.config.sshPort,
|
port: self.config.sshPort,
|
||||||
username : self.config.username,
|
username: self.config.username,
|
||||||
password : self.config.password,
|
password: self.config.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
// note: no explicit callback() until we're finished!
|
// note: no explicit callback() until we're finished!
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.message }, 'DoorParty error');
|
self.client.log.warn({ error: err.message }, 'DoorParty error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the client is still here, go to previous
|
// if the client is still here, go to previous
|
||||||
if(!clientTerminated) {
|
if (!clientTerminated) {
|
||||||
self.prevMenu();
|
self.prevMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
|
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.trackDoorRunBegin = trackDoorRunBegin;
|
exports.trackDoorRunBegin = trackDoorRunBegin;
|
||||||
exports.trackDoorRunEnd = trackDoorRunEnd;
|
exports.trackDoorRunEnd = trackDoorRunEnd;
|
||||||
|
|
||||||
function trackDoorRunBegin(client, doorTag) {
|
function trackDoorRunBegin(client, doorTag) {
|
||||||
const startTime = moment();
|
const startTime = moment();
|
||||||
@@ -23,20 +23,24 @@ function trackDoorRunEnd(trackInfo) {
|
|||||||
const { startTime, client, doorTag } = trackInfo;
|
const { startTime, client, doorTag } = trackInfo;
|
||||||
|
|
||||||
const diff = moment.duration(moment().diff(startTime));
|
const diff = moment.duration(moment().diff(startTime));
|
||||||
if(diff.asSeconds() >= 45) {
|
if (diff.asSeconds() >= 45) {
|
||||||
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1);
|
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const runTimeMinutes = Math.floor(diff.asMinutes());
|
const runTimeMinutes = Math.floor(diff.asMinutes());
|
||||||
if(runTimeMinutes > 0) {
|
if (runTimeMinutes > 0) {
|
||||||
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes);
|
StatLog.incrementUserStat(
|
||||||
|
client.user,
|
||||||
|
UserProps.DoorRunTotalMinutes,
|
||||||
|
runTimeMinutes
|
||||||
|
);
|
||||||
|
|
||||||
const eventInfo = {
|
const eventInfo = {
|
||||||
runTimeMinutes,
|
runTimeMinutes,
|
||||||
user : client.user,
|
user: client.user,
|
||||||
doorTag : doorTag || 'unknown',
|
doorTag: doorTag || 'unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo);
|
Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const FileEntry = require('./file_entry');
|
const FileEntry = require('./file_entry');
|
||||||
const UserProps = require('./user_property');
|
const UserProps = require('./user_property');
|
||||||
const Events = require('./events');
|
const Events = require('./events');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
@@ -12,9 +12,11 @@ module.exports = class DownloadQueue {
|
|||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
if(!Array.isArray(this.client.user.downloadQueue)) {
|
if (!Array.isArray(this.client.user.downloadQueue)) {
|
||||||
if(this.client.user.properties[UserProps.DownloadQueue]) {
|
if (this.client.user.properties[UserProps.DownloadQueue]) {
|
||||||
this.loadFromProperty(this.client.user.properties[UserProps.DownloadQueue]);
|
this.loadFromProperty(
|
||||||
|
this.client.user.properties[UserProps.DownloadQueue]
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.client.user.downloadQueue = [];
|
this.client.user.downloadQueue = [];
|
||||||
}
|
}
|
||||||
@@ -33,68 +35,86 @@ module.exports = class DownloadQueue {
|
|||||||
this.client.user.downloadQueue = [];
|
this.client.user.downloadQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(fileEntry, systemFile=false) {
|
toggle(fileEntry, systemFile = false) {
|
||||||
if(this.isQueued(fileEntry)) {
|
if (this.isQueued(fileEntry)) {
|
||||||
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(
|
||||||
|
e => fileEntry.fileId !== e.fileId
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.add(fileEntry, systemFile);
|
this.add(fileEntry, systemFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(fileEntry, systemFile=false) {
|
add(fileEntry, systemFile = false) {
|
||||||
this.client.user.downloadQueue.push({
|
this.client.user.downloadQueue.push({
|
||||||
fileId : fileEntry.fileId,
|
fileId: fileEntry.fileId,
|
||||||
areaTag : fileEntry.areaTag,
|
areaTag: fileEntry.areaTag,
|
||||||
fileName : fileEntry.fileName,
|
fileName: fileEntry.fileName,
|
||||||
path : fileEntry.filePath,
|
path: fileEntry.filePath,
|
||||||
byteSize : fileEntry.meta.byte_size || 0,
|
byteSize: fileEntry.meta.byte_size || 0,
|
||||||
systemFile : systemFile,
|
systemFile: systemFile,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItems(fileIds) {
|
removeItems(fileIds) {
|
||||||
if(!Array.isArray(fileIds)) {
|
if (!Array.isArray(fileIds)) {
|
||||||
fileIds = [ fileIds ];
|
fileIds = [fileIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ remain, removed ] = _.partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) ));
|
const [remain, removed] = _.partition(
|
||||||
|
this.client.user.downloadQueue,
|
||||||
|
e => -1 === fileIds.indexOf(e.fileId)
|
||||||
|
);
|
||||||
this.client.user.downloadQueue = remain;
|
this.client.user.downloadQueue = remain;
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
isQueued(entryOrId) {
|
isQueued(entryOrId) {
|
||||||
if(entryOrId instanceof FileEntry) {
|
if (entryOrId instanceof FileEntry) {
|
||||||
entryOrId = entryOrId.fileId;
|
entryOrId = entryOrId.fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false;
|
return this.client.user.downloadQueue.find(e => entryOrId === e.fileId)
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toProperty() { return JSON.stringify(this.client.user.downloadQueue); }
|
toProperty() {
|
||||||
|
return JSON.stringify(this.client.user.downloadQueue);
|
||||||
|
}
|
||||||
|
|
||||||
loadFromProperty(prop) {
|
loadFromProperty(prop) {
|
||||||
try {
|
try {
|
||||||
this.client.user.downloadQueue = JSON.parse(prop);
|
this.client.user.downloadQueue = JSON.parse(prop);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
this.client.user.downloadQueue = [];
|
this.client.user.downloadQueue = [];
|
||||||
|
|
||||||
this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property');
|
this.client.log.error(
|
||||||
|
{ error: e.message, property: prop },
|
||||||
|
'Failed parsing download queue property'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addTemporaryDownload(entry) {
|
addTemporaryDownload(entry) {
|
||||||
this.add(entry, true); // true=systemFile
|
this.add(entry, true); // true=systemFile
|
||||||
|
|
||||||
// clean up after ourselves when the session ends
|
// clean up after ourselves when the session ends
|
||||||
const thisUniqueId = this.client.session.uniqueId;
|
const thisUniqueId = this.client.session.uniqueId;
|
||||||
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
||||||
if(thisUniqueId === _.get(evt, 'client.session.uniqueId')) {
|
if (thisUniqueId === _.get(evt, 'client.session.uniqueId')) {
|
||||||
FileEntry.removeEntry(entry, { removePhysFile : true }, err => {
|
FileEntry.removeEntry(entry, { removePhysFile: true }, err => {
|
||||||
const Log = require('./logger').log;
|
const Log = require('./logger').log;
|
||||||
if(err) {
|
if (err) {
|
||||||
Log.warn( { fileId : entry.fileId, path : entry.filePath }, 'Failed removing temporary session download' );
|
Log.warn(
|
||||||
|
{ fileId: entry.fileId, path: entry.filePath },
|
||||||
|
'Failed removing temporary session download'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.debug( { fileId : entry.fileId, path : entry.filePath }, 'Removed temporary session download item' );
|
Log.debug(
|
||||||
|
{ fileId: entry.fileId, path: entry.filePath },
|
||||||
|
'Removed temporary session download item'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
284
core/dropfile.js
284
core/dropfile.js
@@ -2,18 +2,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const { mkdirs } = require('fs-extra');
|
const { mkdirs } = require('fs-extra');
|
||||||
|
|
||||||
//
|
//
|
||||||
// Resources
|
// Resources
|
||||||
@@ -25,31 +25,34 @@ const { mkdirs } = require('fs-extra');
|
|||||||
// * http://lord.lordlegacy.com/dosemu/
|
// * http://lord.lordlegacy.com/dosemu/
|
||||||
//
|
//
|
||||||
module.exports = class DropFile {
|
module.exports = class DropFile {
|
||||||
constructor(client, { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {} ) {
|
constructor(
|
||||||
this.client = client;
|
client,
|
||||||
this.fileType = fileType.toUpperCase();
|
{ fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {}
|
||||||
this.baseDir = baseDir;
|
) {
|
||||||
|
this.client = client;
|
||||||
|
this.fileType = fileType.toUpperCase();
|
||||||
|
this.baseDir = baseDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fullPath() {
|
get fullPath() {
|
||||||
return paths.join(this.baseDir, ('node' + this.client.node), this.fileName);
|
return paths.join(this.baseDir, 'node' + this.client.node, this.fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
get fileName() {
|
get fileName() {
|
||||||
return {
|
return {
|
||||||
DOOR : 'DOOR.SYS', // GAP BBS, many others
|
DOOR: 'DOOR.SYS', // GAP BBS, many others
|
||||||
DOOR32 : 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec)
|
DOOR32: 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec)
|
||||||
CALLINFO : 'CALLINFO.BBS', // Citadel?
|
CALLINFO: 'CALLINFO.BBS', // Citadel?
|
||||||
DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
|
DORINFO: this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
|
||||||
CHAIN : 'CHAIN.TXT', // WWIV
|
CHAIN: 'CHAIN.TXT', // WWIV
|
||||||
CURRUSER : 'CURRUSER.BBS', // RyBBS
|
CURRUSER: 'CURRUSER.BBS', // RyBBS
|
||||||
SFDOORS : 'SFDOORS.DAT', // Spitfire
|
SFDOORS: 'SFDOORS.DAT', // Spitfire
|
||||||
PCBOARD : 'PCBOARD.SYS', // PCBoard
|
PCBOARD: 'PCBOARD.SYS', // PCBoard
|
||||||
TRIBBS : 'TRIBBS.SYS', // TriBBS
|
TRIBBS: 'TRIBBS.SYS', // TriBBS
|
||||||
USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+
|
USERINFO: 'USERINFO.DAT', // Wildcat! 3.0+
|
||||||
JUMPER : 'JUMPER.DAT', // 2AM BBS
|
JUMPER: 'JUMPER.DAT', // 2AM BBS
|
||||||
SXDOOR : 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE
|
SXDOOR: 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE
|
||||||
INFO : 'INFO.BBS', // Phoenix BBS
|
INFO: 'INFO.BBS', // Phoenix BBS
|
||||||
}[this.fileType];
|
}[this.fileType];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,9 +62,9 @@ module.exports = class DropFile {
|
|||||||
|
|
||||||
getHandler() {
|
getHandler() {
|
||||||
return {
|
return {
|
||||||
DOOR : this.getDoorSysBuffer,
|
DOOR: this.getDoorSysBuffer,
|
||||||
DOOR32 : this.getDoor32Buffer,
|
DOOR32: this.getDoor32Buffer,
|
||||||
DORINFO : this.getDoorInfoDefBuffer,
|
DORINFO: this.getDoorInfoDefBuffer,
|
||||||
}[this.fileType];
|
}[this.fileType];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +76,9 @@ module.exports = class DropFile {
|
|||||||
getDoorInfoFileName() {
|
getDoorInfoFileName() {
|
||||||
let x;
|
let x;
|
||||||
const node = this.client.node;
|
const node = this.client.node;
|
||||||
if(10 === node) {
|
if (10 === node) {
|
||||||
x = 0;
|
x = 0;
|
||||||
} else if(node < 10) {
|
} else if (node < 10) {
|
||||||
x = node;
|
x = node;
|
||||||
} else {
|
} else {
|
||||||
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
|
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
|
||||||
@@ -84,75 +87,82 @@ module.exports = class DropFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDoorSysBuffer() {
|
getDoorSysBuffer() {
|
||||||
const prop = this.client.user.properties;
|
const prop = this.client.user.properties;
|
||||||
const now = moment();
|
const now = moment();
|
||||||
const secLevel = this.client.user.getLegacySecurityLevel().toString();
|
const secLevel = this.client.user.getLegacySecurityLevel().toString();
|
||||||
const fullName = this.client.user.getSanitizedName('real');
|
const fullName = this.client.user.getSanitizedName('real');
|
||||||
const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY');
|
const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY');
|
||||||
|
|
||||||
const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024);
|
const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024);
|
||||||
const downK = Math.floor((parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024);
|
const downK = Math.floor(
|
||||||
|
(parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024
|
||||||
|
);
|
||||||
|
|
||||||
const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format('hh:mm');
|
const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format(
|
||||||
|
'hh:mm'
|
||||||
|
);
|
||||||
|
|
||||||
// :TODO: fix time remaining
|
// :TODO: fix time remaining
|
||||||
// :TODO: fix default protocol -- user prop: transfer_protocol
|
// :TODO: fix default protocol -- user prop: transfer_protocol
|
||||||
return iconv.encode( [
|
return iconv.encode(
|
||||||
'COM1:', // "Comm Port - COM0: = LOCAL MODE"
|
[
|
||||||
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
|
'COM1:', // "Comm Port - COM0: = LOCAL MODE"
|
||||||
'8', // "Parity - 7 or 8"
|
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
|
||||||
this.client.node.toString(), // "Node Number - 1 to 99"
|
'8', // "Parity - 7 or 8"
|
||||||
'57600', // "DTE Rate. Actual BPS rate to use. (kg)"
|
this.client.node.toString(), // "Node Number - 1 to 99"
|
||||||
'Y', // "Screen Display - Y=On N=Off (Default to Y)"
|
'57600', // "DTE Rate. Actual BPS rate to use. (kg)"
|
||||||
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
|
'Y', // "Screen Display - Y=On N=Off (Default to Y)"
|
||||||
'Y', // "Page Bell - Y=On N=Off (Default to Y)"
|
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
|
||||||
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
|
'Y', // "Page Bell - Y=On N=Off (Default to Y)"
|
||||||
fullName, // "User Full Name"
|
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
|
||||||
prop[UserProps.Location]|| 'Anywhere', // "Calling From"
|
fullName, // "User Full Name"
|
||||||
'123-456-7890', // "Home Phone"
|
prop[UserProps.Location] || 'Anywhere', // "Calling From"
|
||||||
'123-456-7890', // "Work/Data Phone"
|
'123-456-7890', // "Home Phone"
|
||||||
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
|
'123-456-7890', // "Work/Data Phone"
|
||||||
secLevel, // "Security Level"
|
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
|
||||||
prop[UserProps.LoginCount].toString(), // "Total Times On"
|
secLevel, // "Security Level"
|
||||||
now.format('MM/DD/YY'), // "Last Date Called"
|
prop[UserProps.LoginCount].toString(), // "Total Times On"
|
||||||
'15360', // "Seconds Remaining THIS call (for those that particular)"
|
now.format('MM/DD/YY'), // "Last Date Called"
|
||||||
'256', // "Minutes Remaining THIS call"
|
'15360', // "Seconds Remaining THIS call (for those that particular)"
|
||||||
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
|
'256', // "Minutes Remaining THIS call"
|
||||||
this.client.term.termHeight.toString(), // "Page Length"
|
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
|
||||||
'N', // "User Mode - Y = Expert, N = Novice"
|
this.client.term.termHeight.toString(), // "Page Length"
|
||||||
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
|
'N', // "User Mode - Y = Expert, N = Novice"
|
||||||
'1', // "Conference Exited To DOOR From (G)"
|
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
|
||||||
'01/01/99', // "User Expiration Date (mm/dd/yy)"
|
'1', // "Conference Exited To DOOR From (G)"
|
||||||
this.client.user.userId.toString(), // "User File's Record Number"
|
'01/01/99', // "User Expiration Date (mm/dd/yy)"
|
||||||
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
|
this.client.user.userId.toString(), // "User File's Record Number"
|
||||||
// :TODO: fix up, down, etc. form user properties
|
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
|
||||||
'0', // "Total Uploads"
|
// :TODO: fix up, down, etc. form user properties
|
||||||
'0', // "Total Downloads"
|
'0', // "Total Uploads"
|
||||||
'0', // "Daily Download "K" Total"
|
'0', // "Total Downloads"
|
||||||
'999999', // "Daily Download Max. "K" Limit"
|
'0', // "Daily Download "K" Total"
|
||||||
bd, // "Caller's Birthdate"
|
'999999', // "Daily Download Max. "K" Limit"
|
||||||
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
|
bd, // "Caller's Birthdate"
|
||||||
'X:\\GEN\\', // "Path to the GEN directory"
|
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
|
||||||
StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)"
|
'X:\\GEN\\', // "Path to the GEN directory"
|
||||||
this.client.user.getSanitizedName(), // "Alias name"
|
StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)"
|
||||||
'00:05', // "Event time (hh:mm)" (note: wat?)
|
this.client.user.getSanitizedName(), // "Alias name"
|
||||||
'Y', // "If its an error correcting connection (Y/N)"
|
'00:05', // "Event time (hh:mm)" (note: wat?)
|
||||||
'Y', // "ANSI supported & caller using NG mode (Y/N)"
|
'Y', // "If its an error correcting connection (Y/N)"
|
||||||
'Y', // "Use Record Locking (Y/N)"
|
'Y', // "ANSI supported & caller using NG mode (Y/N)"
|
||||||
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
|
'Y', // "Use Record Locking (Y/N)"
|
||||||
// :TODO: fix minutes here also:
|
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
|
||||||
'256', // "Time Credits In Minutes (positive/negative)"
|
// :TODO: fix minutes here also:
|
||||||
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
|
'256', // "Time Credits In Minutes (positive/negative)"
|
||||||
timeOfCall, // "Time of This Call"
|
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
|
||||||
timeOfCall, // "Time of Last Call (hh:mm)"
|
timeOfCall, // "Time of This Call"
|
||||||
'9999', // "Maximum daily files available"
|
timeOfCall, // "Time of Last Call (hh:mm)"
|
||||||
'0', // "Files d/led so far today"
|
'9999', // "Maximum daily files available"
|
||||||
upK.toString(), // "Total "K" Bytes Uploaded"
|
'0', // "Files d/led so far today"
|
||||||
downK.toString(), // "Total "K" Bytes Downloaded"
|
upK.toString(), // "Total "K" Bytes Uploaded"
|
||||||
prop[UserProps.UserComment] || 'None', // "User Comment"
|
downK.toString(), // "Total "K" Bytes Downloaded"
|
||||||
'0', // "Total Doors Opened"
|
prop[UserProps.UserComment] || 'None', // "User Comment"
|
||||||
'0', // "Total Messages Left"
|
'0', // "Total Doors Opened"
|
||||||
].join('\r\n') + '\r\n', 'cp437');
|
'0', // "Total Messages Left"
|
||||||
|
].join('\r\n') + '\r\n',
|
||||||
|
'cp437'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDoor32Buffer() {
|
getDoor32Buffer() {
|
||||||
@@ -163,26 +173,29 @@ module.exports = class DropFile {
|
|||||||
//
|
//
|
||||||
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
|
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
|
||||||
const Door32CommTypes = {
|
const Door32CommTypes = {
|
||||||
Local : 0,
|
Local: 0,
|
||||||
Serial : 1,
|
Serial: 1,
|
||||||
Telnet : 2,
|
Telnet: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const commType = Door32CommTypes.Telnet;
|
const commType = Door32CommTypes.Telnet;
|
||||||
|
|
||||||
return iconv.encode([
|
return iconv.encode(
|
||||||
commType.toString(),
|
[
|
||||||
'-1',
|
commType.toString(),
|
||||||
'115200',
|
'-1',
|
||||||
Config().general.boardName,
|
'115200',
|
||||||
this.client.user.userId.toString(),
|
Config().general.boardName,
|
||||||
this.client.user.getSanitizedName('real'),
|
this.client.user.userId.toString(),
|
||||||
this.client.user.getSanitizedName(),
|
this.client.user.getSanitizedName('real'),
|
||||||
this.client.user.getLegacySecurityLevel().toString(),
|
this.client.user.getSanitizedName(),
|
||||||
'546', // :TODO: Minutes left!
|
this.client.user.getLegacySecurityLevel().toString(),
|
||||||
'1', // ANSI
|
'546', // :TODO: Minutes left!
|
||||||
this.client.node.toString(),
|
'1', // ANSI
|
||||||
].join('\r\n') + '\r\n', 'cp437');
|
this.client.node.toString(),
|
||||||
|
].join('\r\n') + '\r\n',
|
||||||
|
'cp437'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDoorInfoDefBuffer() {
|
getDoorInfoDefBuffer() {
|
||||||
@@ -194,31 +207,36 @@ module.exports = class DropFile {
|
|||||||
//
|
//
|
||||||
// Note that usernames are just used for first/last names here
|
// Note that usernames are just used for first/last names here
|
||||||
//
|
//
|
||||||
const opUserName = /[^\s]*/.exec(StatLog.getSystemStat(SysProps.SysOpUsername))[0];
|
const opUserName = /[^\s]*/.exec(
|
||||||
const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0];
|
StatLog.getSystemStat(SysProps.SysOpUsername)
|
||||||
const secLevel = this.client.user.getLegacySecurityLevel().toString();
|
)[0];
|
||||||
const location = this.client.user.properties[UserProps.Location];
|
const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0];
|
||||||
|
const secLevel = this.client.user.getLegacySecurityLevel().toString();
|
||||||
|
const location = this.client.user.properties[UserProps.Location];
|
||||||
|
|
||||||
return iconv.encode( [
|
return iconv.encode(
|
||||||
Config().general.boardName, // "The name of the system."
|
[
|
||||||
opUserName, // "The sysop's name up to the first space."
|
Config().general.boardName, // "The name of the system."
|
||||||
opUserName, // "The sysop's name following the first space."
|
opUserName, // "The sysop's name up to the first space."
|
||||||
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
|
opUserName, // "The sysop's name following the first space."
|
||||||
'57600', // "The current port (DTE) rate."
|
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
|
||||||
'0', // "The number "0""
|
'57600', // "The current port (DTE) rate."
|
||||||
userName, // "The current user's name, up to the first space."
|
'0', // "The number "0""
|
||||||
userName, // "The current user's name, following the first space."
|
userName, // "The current user's name, up to the first space."
|
||||||
location || '', // "Where the user lives, or a blank line if unknown."
|
userName, // "The current user's name, following the first space."
|
||||||
'1', // "The number "0" if TTY, or "1" if ANSI."
|
location || '', // "Where the user lives, or a blank line if unknown."
|
||||||
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
|
'1', // "The number "0" if TTY, or "1" if ANSI."
|
||||||
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
|
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
|
||||||
'-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
|
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
|
||||||
].join('\r\n') + '\r\n', 'cp437');
|
'-1', // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
|
||||||
|
].join('\r\n') + '\r\n',
|
||||||
|
'cp437'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createFile(cb) {
|
createFile(cb) {
|
||||||
mkdirs(paths.dirname(this.fullPath), err => {
|
mkdirs(paths.dirname(this.fullPath), err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
return fs.writeFile(this.fullPath, this.getContents(), cb);
|
return fs.writeFile(this.fullPath, this.getContents(), cb);
|
||||||
|
|||||||
@@ -2,57 +2,59 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const TextView = require('./text_view.js').TextView;
|
const TextView = require('./text_view.js').TextView;
|
||||||
const miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
const strUtil = require('./string_util.js');
|
const strUtil = require('./string_util.js');
|
||||||
|
|
||||||
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
|
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.EditTextView = EditTextView;
|
exports.EditTextView = EditTextView;
|
||||||
|
|
||||||
const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, {
|
const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, {
|
||||||
delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
delete: ['delete', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||||
});
|
});
|
||||||
|
|
||||||
function EditTextView(options) {
|
function EditTextView(options) {
|
||||||
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
||||||
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
||||||
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
||||||
options.resizable = false;
|
options.resizable = false;
|
||||||
|
|
||||||
if(!_.isObject(options.specialKeyMap)) {
|
if (!_.isObject(options.specialKeyMap)) {
|
||||||
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
|
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextView.call(this, options);
|
TextView.call(this, options);
|
||||||
|
|
||||||
this.initDefaultWidth();
|
this.initDefaultWidth();
|
||||||
this.cursorPos = { row : 0, col : 0 };
|
this.cursorPos = { row: 0, col: 0 };
|
||||||
|
|
||||||
this.clientBackspace = function() {
|
this.clientBackspace = function () {
|
||||||
this.text = this.text.substr(0, this.text.length - 1);
|
this.text = this.text.substr(0, this.text.length - 1);
|
||||||
|
|
||||||
if(this.text.length >= this.dimens.width) {
|
if (this.text.length >= this.dimens.width) {
|
||||||
this.redraw();
|
this.redraw();
|
||||||
} else {
|
} else {
|
||||||
this.cursorPos.col -= 1;
|
this.cursorPos.col -= 1;
|
||||||
if(this.cursorPos.col >= 0) {
|
if (this.cursorPos.col >= 0) {
|
||||||
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
|
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
|
||||||
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
|
this.client.term.write(
|
||||||
|
`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
require('util').inherits(EditTextView, TextView);
|
require('util').inherits(EditTextView, TextView);
|
||||||
|
|
||||||
EditTextView.prototype.onKeyPress = function(ch, key) {
|
EditTextView.prototype.onKeyPress = function (ch, key) {
|
||||||
if(key) {
|
if (key) {
|
||||||
if(this.isKeyMapped('backspace', key.name)) {
|
if (this.isKeyMapped('backspace', key.name)) {
|
||||||
if(this.text.length > 0) {
|
if (this.text.length > 0) {
|
||||||
this.clientBackspace();
|
this.clientBackspace();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,29 +65,29 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
|
|||||||
if (this.text.length > 0 && this.cursorPos.col === this.text.length) {
|
if (this.text.length > 0 && this.cursorPos.col === this.text.length) {
|
||||||
this.clientBackspace();
|
this.clientBackspace();
|
||||||
}
|
}
|
||||||
} else if(this.isKeyMapped('clearLine', key.name)) {
|
} else if (this.isKeyMapped('clearLine', key.name)) {
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.cursorPos.col = 0;
|
this.cursorPos.col = 0;
|
||||||
this.setFocus(true); // resetting focus will redraw & adjust cursor
|
this.setFocus(true); // resetting focus will redraw & adjust cursor
|
||||||
|
|
||||||
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ch && strUtil.isPrintable(ch)) {
|
if (ch && strUtil.isPrintable(ch)) {
|
||||||
if(this.text.length < this.maxLength) {
|
if (this.text.length < this.maxLength) {
|
||||||
ch = strUtil.stylizeString(ch, this.textStyle);
|
ch = strUtil.stylizeString(ch, this.textStyle);
|
||||||
|
|
||||||
this.text += ch;
|
this.text += ch;
|
||||||
|
|
||||||
if(this.text.length > this.dimens.width) {
|
if (this.text.length > this.dimens.width) {
|
||||||
// no shortcuts - redraw the view
|
// no shortcuts - redraw the view
|
||||||
this.redraw();
|
this.redraw();
|
||||||
} else {
|
} else {
|
||||||
this.cursorPos.col += 1;
|
this.cursorPos.col += 1;
|
||||||
|
|
||||||
if(_.isString(this.textMaskChar)) {
|
if (_.isString(this.textMaskChar)) {
|
||||||
if(this.textMaskChar.length > 0) {
|
if (this.textMaskChar.length > 0) {
|
||||||
this.client.term.write(this.textMaskChar);
|
this.client.term.write(this.textMaskChar);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -98,10 +100,10 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
|
|||||||
EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
EditTextView.prototype.setText = function(text) {
|
EditTextView.prototype.setText = function (text) {
|
||||||
// draw & set |text|
|
// draw & set |text|
|
||||||
EditTextView.super_.prototype.setText.call(this, text);
|
EditTextView.super_.prototype.setText.call(this, text);
|
||||||
|
|
||||||
// adjust local cursor tracking
|
// adjust local cursor tracking
|
||||||
this.cursorPos = { row : 0, col : text.length };
|
this.cursorPos = { row: 0, col: text.length };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,26 +2,26 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const nodeMailer = require('nodemailer');
|
const nodeMailer = require('nodemailer');
|
||||||
|
|
||||||
exports.sendMail = sendMail;
|
exports.sendMail = sendMail;
|
||||||
|
|
||||||
function sendMail(message, cb) {
|
function sendMail(message, cb) {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(!_.has(config, 'email.transport')) {
|
if (!_.has(config, 'email.transport')) {
|
||||||
return cb(Errors.MissingConfig('Email "email.transport" configuration missing'));
|
return cb(Errors.MissingConfig('Email "email.transport" configuration missing'));
|
||||||
}
|
}
|
||||||
|
|
||||||
message.from = message.from || config.email.defaultFrom;
|
message.from = message.from || config.email.defaultFrom;
|
||||||
|
|
||||||
const transportOptions = Object.assign( {}, config.email.transport, {
|
const transportOptions = Object.assign({}, config.email.transport, {
|
||||||
logger : Log,
|
logger: Log,
|
||||||
});
|
});
|
||||||
|
|
||||||
const transport = nodeMailer.createTransport(transportOptions);
|
const transport = nodeMailer.createTransport(transportOptions);
|
||||||
|
|||||||
@@ -5,53 +5,65 @@ class EnigError extends Error {
|
|||||||
constructor(message, code, reason, reasonCode) {
|
constructor(message, code, reason, reasonCode) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
this.reasonCode = reasonCode;
|
this.reasonCode = reasonCode;
|
||||||
|
|
||||||
if(this.reason) {
|
if (this.reason) {
|
||||||
this.message += `: ${this.reason}`;
|
this.message += `: ${this.reason}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof Error.captureStackTrace === 'function') {
|
if (typeof Error.captureStackTrace === 'function') {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
} else {
|
} else {
|
||||||
this.stack = (new Error(message)).stack;
|
this.stack = new Error(message).stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.EnigError = EnigError;
|
exports.EnigError = EnigError;
|
||||||
|
|
||||||
exports.Errors = {
|
exports.Errors = {
|
||||||
General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode),
|
General: (reason, reasonCode) =>
|
||||||
MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode),
|
new EnigError('An error occurred', -33000, reason, reasonCode),
|
||||||
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode),
|
MenuStack: (reason, reasonCode) =>
|
||||||
AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode),
|
new EnigError('Menu stack error', -33001, reason, reasonCode),
|
||||||
Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
|
DoesNotExist: (reason, reasonCode) =>
|
||||||
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
|
new EnigError('Object does not exist', -33002, reason, reasonCode),
|
||||||
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
|
AccessDenied: (reason, reasonCode) =>
|
||||||
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode),
|
new EnigError('Access denied', -32003, reason, reasonCode),
|
||||||
MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode),
|
Invalid: (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
|
||||||
MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode),
|
ExternalProcess: (reason, reasonCode) =>
|
||||||
BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode),
|
new EnigError('External process error', -32005, reason, reasonCode),
|
||||||
UserInterrupt : (reason, reasonCode) => new EnigError('User interrupted', -32011, reason, reasonCode),
|
MissingConfig: (reason, reasonCode) =>
|
||||||
NothingToDo : (reason, reasonCode) => new EnigError('Nothing to do', -32012, reason, reasonCode),
|
new EnigError('Missing configuration', -32006, reason, reasonCode),
|
||||||
|
UnexpectedState: (reason, reasonCode) =>
|
||||||
|
new EnigError('Unexpected state', -32007, reason, reasonCode),
|
||||||
|
MissingParam: (reason, reasonCode) =>
|
||||||
|
new EnigError('Missing paramter(s)', -32008, reason, reasonCode),
|
||||||
|
MissingMci: (reason, reasonCode) =>
|
||||||
|
new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode),
|
||||||
|
BadLogin: (reason, reasonCode) =>
|
||||||
|
new EnigError('Bad login attempt', -32010, reason, reasonCode),
|
||||||
|
UserInterrupt: (reason, reasonCode) =>
|
||||||
|
new EnigError('User interrupted', -32011, reason, reasonCode),
|
||||||
|
NothingToDo: (reason, reasonCode) =>
|
||||||
|
new EnigError('Nothing to do', -32012, reason, reasonCode),
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.ErrorReasons = {
|
exports.ErrorReasons = {
|
||||||
AlreadyThere : 'ALREADYTHERE',
|
AlreadyThere: 'ALREADYTHERE',
|
||||||
InvalidNextMenu : 'BADNEXT',
|
InvalidNextMenu: 'BADNEXT',
|
||||||
NoPreviousMenu : 'NOPREV',
|
NoPreviousMenu: 'NOPREV',
|
||||||
NoConditionMatch : 'NOCONDMATCH',
|
NoConditionMatch: 'NOCONDMATCH',
|
||||||
NotEnabled : 'NOTENABLED',
|
NotEnabled: 'NOTENABLED',
|
||||||
AlreadyLoggedIn : 'ALREADYLOGGEDIN',
|
AlreadyLoggedIn: 'ALREADYLOGGEDIN',
|
||||||
TooMany : 'TOOMANY',
|
TooMany: 'TOOMANY',
|
||||||
Disabled : 'DISABLED',
|
Disabled: 'DISABLED',
|
||||||
Inactive : 'INACTIVE',
|
Inactive: 'INACTIVE',
|
||||||
Locked : 'LOCKED',
|
Locked: 'LOCKED',
|
||||||
NotAllowed : 'NOTALLOWED',
|
NotAllowed: 'NOTALLOWED',
|
||||||
Invalid2FA : 'INVALID2FA',
|
Invalid2FA: 'INVALID2FA',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
module.exports = function(condition, message) {
|
module.exports = function (condition, message) {
|
||||||
if(Config().debug.assertsEnabled) {
|
if (Config().debug.assertsEnabled) {
|
||||||
assert.apply(this, arguments);
|
assert.apply(this, arguments);
|
||||||
} else if(!(condition)) {
|
} else if (!condition) {
|
||||||
const stack = new Error().stack;
|
const stack = new Error().stack;
|
||||||
Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' );
|
Log.error({ condition: condition, stack: stack }, message || 'Assertion failed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,48 +2,52 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const PluginModule = require('./plugin_module.js').PluginModule;
|
const PluginModule = require('./plugin_module.js').PluginModule;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const later = require('@breejs/later');
|
const later = require('@breejs/later');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const pty = require('node-pty');
|
const pty = require('node-pty');
|
||||||
const sane = require('sane');
|
const sane = require('sane');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
|
|
||||||
exports.getModule = EventSchedulerModule;
|
exports.getModule = EventSchedulerModule;
|
||||||
exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart
|
exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Event Scheduler',
|
name: 'Event Scheduler',
|
||||||
desc : 'Support for scheduling arbritary events',
|
desc: 'Support for scheduling arbritary events',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
|
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
|
||||||
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
|
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
|
||||||
|
|
||||||
class ScheduledEvent {
|
class ScheduledEvent {
|
||||||
constructor(events, name) {
|
constructor(events, name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.schedule = this.parseScheduleString(events[name].schedule);
|
this.schedule = this.parseScheduleString(events[name].schedule);
|
||||||
this.action = this.parseActionSpec(events[name].action);
|
this.action = this.parseActionSpec(events[name].action);
|
||||||
if(this.action) {
|
if (this.action) {
|
||||||
this.action.args = events[name].args || [];
|
this.action.args = events[name].args || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isValid() {
|
get isValid() {
|
||||||
if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) {
|
if (
|
||||||
|
!this.schedule ||
|
||||||
|
(!this.schedule.sched && !this.schedule.watchFile) ||
|
||||||
|
!this.action
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if('method' === this.action.type && !this.action.location) {
|
if ('method' === this.action.type && !this.action.location) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,57 +55,57 @@ class ScheduledEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseScheduleString(schedStr) {
|
parseScheduleString(schedStr) {
|
||||||
if(!schedStr) {
|
if (!schedStr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let schedule = {};
|
let schedule = {};
|
||||||
|
|
||||||
const m = SCHEDULE_REGEXP.exec(schedStr);
|
const m = SCHEDULE_REGEXP.exec(schedStr);
|
||||||
if(m) {
|
if (m) {
|
||||||
schedStr = schedStr.substr(0, m.index).trim();
|
schedStr = schedStr.substr(0, m.index).trim();
|
||||||
|
|
||||||
if('@watch:' === m[1]) {
|
if ('@watch:' === m[1]) {
|
||||||
schedule.watchFile = m[2];
|
schedule.watchFile = m[2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(schedStr.length > 0) {
|
if (schedStr.length > 0) {
|
||||||
const sched = later.parse.text(schedStr);
|
const sched = later.parse.text(schedStr);
|
||||||
if(-1 === sched.error) {
|
if (-1 === sched.error) {
|
||||||
schedule.sched = sched;
|
schedule.sched = sched;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return undefined if we couldn't parse out anything useful
|
// return undefined if we couldn't parse out anything useful
|
||||||
if(!_.isEmpty(schedule)) {
|
if (!_.isEmpty(schedule)) {
|
||||||
return schedule;
|
return schedule;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseActionSpec(actionSpec) {
|
parseActionSpec(actionSpec) {
|
||||||
if(actionSpec) {
|
if (actionSpec) {
|
||||||
if('@' === actionSpec[0]) {
|
if ('@' === actionSpec[0]) {
|
||||||
const m = ACTION_REGEXP.exec(actionSpec);
|
const m = ACTION_REGEXP.exec(actionSpec);
|
||||||
if(m) {
|
if (m) {
|
||||||
if(m[2].indexOf(':') > -1) {
|
if (m[2].indexOf(':') > -1) {
|
||||||
const parts = m[2].split(':');
|
const parts = m[2].split(':');
|
||||||
return {
|
return {
|
||||||
type : m[1],
|
type: m[1],
|
||||||
location : parts[0],
|
location: parts[0],
|
||||||
what : parts[1],
|
what: parts[1],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
type : m[1],
|
type: m[1],
|
||||||
what : m[2],
|
what: m[2],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
type : 'execute',
|
type: 'execute',
|
||||||
what : actionSpec,
|
what: actionSpec,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,59 +114,70 @@ class ScheduledEvent {
|
|||||||
executeAction(reason, cb) {
|
executeAction(reason, cb) {
|
||||||
Log.info( { eventName : this.name, action : this.action, reason : reason }, `Executing scheduled event "${this.name}"...`);
|
Log.info( { eventName : this.name, action : this.action, reason : reason }, `Executing scheduled event "${this.name}"...`);
|
||||||
|
|
||||||
if('method' === this.action.type) {
|
if ('method' === this.action.type) {
|
||||||
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
|
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
|
||||||
try {
|
try {
|
||||||
const methodModule = require(modulePath);
|
const methodModule = require(modulePath);
|
||||||
methodModule[this.action.what](this.action.args, err => {
|
methodModule[this.action.what](this.action.args, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
Log.debug(
|
Log.debug(
|
||||||
{ error : err.message, eventName : this.name, action : this.action },
|
{
|
||||||
'Error performing scheduled event action');
|
error: err.message,
|
||||||
|
eventName: this.name,
|
||||||
|
action: this.action,
|
||||||
|
},
|
||||||
|
'Error performing scheduled event action'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(err);
|
return cb(err);
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
{ error : e.message, eventName : this.name, action : this.action },
|
{ error: e.message, eventName: this.name, action: this.action },
|
||||||
'Failed to perform scheduled event action');
|
'Failed to perform scheduled event action'
|
||||||
|
);
|
||||||
|
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
} else if('execute' === this.action.type) {
|
} else if ('execute' === this.action.type) {
|
||||||
const opts = {
|
const opts = {
|
||||||
// :TODO: cwd
|
// :TODO: cwd
|
||||||
name : this.name,
|
name: this.name,
|
||||||
cols : 80,
|
cols: 80,
|
||||||
rows : 24,
|
rows: 24,
|
||||||
env : process.env,
|
env: process.env,
|
||||||
};
|
};
|
||||||
|
|
||||||
let proc;
|
let proc;
|
||||||
try {
|
try {
|
||||||
proc = pty.spawn(this.action.what, this.action.args, opts);
|
proc = pty.spawn(this.action.what, this.action.args, opts);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
Log.warn(
|
Log.warn({
|
||||||
{
|
error: 'Failed to spawn @execute process',
|
||||||
error : 'Failed to spawn @execute process',
|
reason: e.message,
|
||||||
reason : e.message,
|
eventName: this.name,
|
||||||
eventName : this.name,
|
action: this.action,
|
||||||
action : this.action,
|
what: this.action.what,
|
||||||
what : this.action.what,
|
args: this.action.args,
|
||||||
args : this.action.args
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.once('exit', exitCode => {
|
proc.once('exit', exitCode => {
|
||||||
if(exitCode) {
|
if (exitCode) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
{ eventName : this.name, action : this.action, exitCode : exitCode },
|
{ eventName: this.name, action: this.action, exitCode: exitCode },
|
||||||
'Bad exit code while performing scheduled event action');
|
'Bad exit code while performing scheduled event action'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cb(exitCode ? Errors.ExternalProcess(`Bad exit code while performing scheduled event action: ${exitCode}`) : null);
|
return cb(
|
||||||
|
exitCode
|
||||||
|
? Errors.ExternalProcess(
|
||||||
|
`Bad exit code while performing scheduled event action: ${exitCode}`
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,15 +187,15 @@ function EventSchedulerModule(options) {
|
|||||||
PluginModule.call(this, options);
|
PluginModule.call(this, options);
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(_.has(config, 'eventScheduler')) {
|
if (_.has(config, 'eventScheduler')) {
|
||||||
this.moduleConfig = config.eventScheduler;
|
this.moduleConfig = config.eventScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this.runningActions = new Set();
|
this.runningActions = new Set();
|
||||||
|
|
||||||
this.performAction = function(schedEvent, reason) {
|
this.performAction = function (schedEvent, reason) {
|
||||||
if(self.runningActions.has(schedEvent.name)) {
|
if (self.runningActions.has(schedEvent.name)) {
|
||||||
return; // already running
|
return; // already running
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,80 +208,85 @@ function EventSchedulerModule(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convienence static method for direct load + start
|
// convienence static method for direct load + start
|
||||||
EventSchedulerModule.loadAndStart = function(cb) {
|
EventSchedulerModule.loadAndStart = function (cb) {
|
||||||
const loadModuleEx = require('./module_util.js').loadModuleEx;
|
const loadModuleEx = require('./module_util.js').loadModuleEx;
|
||||||
|
|
||||||
const loadOpts = {
|
const loadOpts = {
|
||||||
name : path.basename(__filename, '.js'),
|
name: path.basename(__filename, '.js'),
|
||||||
path : __dirname,
|
path: __dirname,
|
||||||
};
|
};
|
||||||
|
|
||||||
loadModuleEx(loadOpts, (err, mod) => {
|
loadModuleEx(loadOpts, (err, mod) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modInst = new mod.getModule();
|
const modInst = new mod.getModule();
|
||||||
modInst.startup( err => {
|
modInst.startup(err => {
|
||||||
return cb(err, modInst);
|
return cb(err, modInst);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
EventSchedulerModule.prototype.startup = function(cb) {
|
EventSchedulerModule.prototype.startup = function (cb) {
|
||||||
|
this.eventTimers = [];
|
||||||
this.eventTimers = [];
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if(this.moduleConfig && _.has(this.moduleConfig, 'events')) {
|
if (this.moduleConfig && _.has(this.moduleConfig, 'events')) {
|
||||||
const events = Object.keys(this.moduleConfig.events).map( name => {
|
const events = Object.keys(this.moduleConfig.events).map(name => {
|
||||||
return new ScheduledEvent(this.moduleConfig.events, name);
|
return new ScheduledEvent(this.moduleConfig.events, name);
|
||||||
});
|
});
|
||||||
|
|
||||||
events.forEach( schedEvent => {
|
events.forEach(schedEvent => {
|
||||||
if(!schedEvent.isValid) {
|
if (!schedEvent.isValid) {
|
||||||
Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry');
|
Log.warn({ eventName: schedEvent.name }, 'Invalid scheduled event entry');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.debug(
|
Log.debug(
|
||||||
{
|
{
|
||||||
eventName : schedEvent.name,
|
eventName: schedEvent.name,
|
||||||
schedule : this.moduleConfig.events[schedEvent.name].schedule,
|
schedule: this.moduleConfig.events[schedEvent.name].schedule,
|
||||||
action : schedEvent.action,
|
action: schedEvent.action,
|
||||||
next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A',
|
next: schedEvent.schedule.sched
|
||||||
|
? moment(
|
||||||
|
later.schedule(schedEvent.schedule.sched).next(1)
|
||||||
|
).format('ddd, MMM Do, YYYY @ h:m:ss a')
|
||||||
|
: 'N/A',
|
||||||
},
|
},
|
||||||
'Scheduled event loaded'
|
'Scheduled event loaded'
|
||||||
);
|
);
|
||||||
|
|
||||||
if(schedEvent.schedule.sched) {
|
if (schedEvent.schedule.sched) {
|
||||||
this.eventTimers.push(later.setInterval( () => {
|
this.eventTimers.push(
|
||||||
self.performAction(schedEvent, 'Schedule');
|
later.setInterval(() => {
|
||||||
}, schedEvent.schedule.sched));
|
self.performAction(schedEvent, 'Schedule');
|
||||||
|
}, schedEvent.schedule.sched)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(schedEvent.schedule.watchFile) {
|
if (schedEvent.schedule.watchFile) {
|
||||||
const watcher = sane(
|
const watcher = sane(paths.dirname(schedEvent.schedule.watchFile), {
|
||||||
paths.dirname(schedEvent.schedule.watchFile),
|
glob: `**/${paths.basename(schedEvent.schedule.watchFile)}`,
|
||||||
{
|
});
|
||||||
glob : `**/${paths.basename(schedEvent.schedule.watchFile)}`
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// :TODO: should track watched files & stop watching @ shutdown?
|
// :TODO: should track watched files & stop watching @ shutdown?
|
||||||
|
|
||||||
[ 'change', 'add', 'delete' ].forEach(event => {
|
['change', 'add', 'delete'].forEach(event => {
|
||||||
watcher.on(event, (fileName, fileRoot) => {
|
watcher.on(event, (fileName, fileRoot) => {
|
||||||
const eventPath = paths.join(fileRoot, fileName);
|
const eventPath = paths.join(fileRoot, fileName);
|
||||||
if(schedEvent.schedule.watchFile === eventPath) {
|
if (schedEvent.schedule.watchFile === eventPath) {
|
||||||
self.performAction(schedEvent, `Watch file: ${eventPath}`);
|
self.performAction(schedEvent, `Watch file: ${eventPath}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
fse.exists(schedEvent.schedule.watchFile, exists => {
|
fse.exists(schedEvent.schedule.watchFile, exists => {
|
||||||
if(exists) {
|
if (exists) {
|
||||||
self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`);
|
self.performAction(
|
||||||
|
schedEvent,
|
||||||
|
`Watch file: ${schedEvent.schedule.watchFile}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -276,9 +296,9 @@ EventSchedulerModule.prototype.startup = function(cb) {
|
|||||||
cb(null);
|
cb(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
EventSchedulerModule.prototype.shutdown = function(cb) {
|
EventSchedulerModule.prototype.shutdown = function (cb) {
|
||||||
if(this.eventTimers) {
|
if (this.eventTimers) {
|
||||||
this.eventTimers.forEach( et => et.clear() );
|
this.eventTimers.forEach(et => et.clear());
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(null);
|
cb(null);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const events = require('events');
|
const events = require('events');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const SystemEvents = require('./system_events.js');
|
const SystemEvents = require('./system_events.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = new class Events extends events.EventEmitter {
|
module.exports = new (class Events extends events.EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.setMaxListeners(64); // :TODO: play with this...
|
this.setMaxListeners(64); // :TODO: play with this...
|
||||||
}
|
}
|
||||||
|
|
||||||
getSystemEvents() {
|
getSystemEvents() {
|
||||||
@@ -19,22 +19,22 @@ module.exports = new class Events extends events.EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addListener(event, listener) {
|
addListener(event, listener) {
|
||||||
Log.trace( { event : event }, 'Registering event listener');
|
Log.trace({ event: event }, 'Registering event listener');
|
||||||
return super.addListener(event, listener);
|
return super.addListener(event, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event, ...args) {
|
emit(event, ...args) {
|
||||||
Log.trace( { event : event }, 'Emitting event');
|
Log.trace({ event: event }, 'Emitting event');
|
||||||
return super.emit(event, ...args);
|
return super.emit(event, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
on(event, listener) {
|
on(event, listener) {
|
||||||
Log.trace( { event : event }, 'Registering event listener');
|
Log.trace({ event: event }, 'Registering event listener');
|
||||||
return super.on(event, listener);
|
return super.on(event, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
once(event, listener) {
|
once(event, listener) {
|
||||||
Log.trace( { event : event }, 'Registering single use event listener');
|
Log.trace({ event: event }, 'Registering single use event listener');
|
||||||
return super.once(event, listener);
|
return super.once(event, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,32 +45,32 @@ module.exports = new class Events extends events.EventEmitter {
|
|||||||
// The returned object must be used with removeMultipleEventListener()
|
// The returned object must be used with removeMultipleEventListener()
|
||||||
//
|
//
|
||||||
addMultipleEventListener(events, listener) {
|
addMultipleEventListener(events, listener) {
|
||||||
Log.trace( { events }, 'Registering event listeners');
|
Log.trace({ events }, 'Registering event listeners');
|
||||||
|
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
|
|
||||||
events.forEach(eventName => {
|
events.forEach(eventName => {
|
||||||
const listenWrapper = _.partial(listener, _, eventName);
|
const listenWrapper = _.partial(listener, _, eventName);
|
||||||
this.on(eventName, listenWrapper);
|
this.on(eventName, listenWrapper);
|
||||||
listeners.push( { eventName, listenWrapper } );
|
listeners.push({ eventName, listenWrapper });
|
||||||
});
|
});
|
||||||
|
|
||||||
return listeners;
|
return listeners;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMultipleEventListener(listeners) {
|
removeMultipleEventListener(listeners) {
|
||||||
Log.trace( { events }, 'Removing listeners');
|
Log.trace({ events }, 'Removing listeners');
|
||||||
listeners.forEach(listener => {
|
listeners.forEach(listener => {
|
||||||
this.removeListener(listener.eventName, listener.listenWrapper);
|
this.removeListener(listener.eventName, listener.listenWrapper);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener(event, listener) {
|
removeListener(event, listener) {
|
||||||
Log.trace( { event : event }, 'Removing listener');
|
Log.trace({ event: event }, 'Removing listener');
|
||||||
return super.removeListener(event, listener);
|
return super.removeListener(event, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
startup(cb) {
|
startup(cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|||||||
183
core/exodus.js
183
core/exodus.js
@@ -2,29 +2,24 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const { resetScreen } = require('./ansi_term.js');
|
const { resetScreen } = require('./ansi_term.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const {
|
const { getEnigmaUserAgent } = require('./misc_util.js');
|
||||||
getEnigmaUserAgent
|
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
||||||
} = require('./misc_util.js');
|
|
||||||
const {
|
|
||||||
trackDoorRunBegin,
|
|
||||||
trackDoorRunEnd
|
|
||||||
} = require('./door_util.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const joinPath = require('path').join;
|
const joinPath = require('path').join;
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const querystring = require('querystring');
|
const querystring = require('querystring');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const SSHClient = require('ssh2').Client;
|
const SSHClient = require('ssh2').Client;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Configuration block:
|
Configuration block:
|
||||||
@@ -55,41 +50,47 @@ const SSHClient = require('ssh2').Client;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Exodus',
|
name: 'Exodus',
|
||||||
desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
|
desc: 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class ExodusModule extends MenuModule {
|
exports.getModule = class ExodusModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.config = options.menuConfig.config || {};
|
this.config = options.menuConfig.config || {};
|
||||||
this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org';
|
this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org';
|
||||||
this.config.ticketPort = this.config.ticketPort || 1984,
|
(this.config.ticketPort = this.config.ticketPort || 1984),
|
||||||
this.config.ticketPath = this.config.ticketPath || '/exodus';
|
(this.config.ticketPath = this.config.ticketPath || '/exodus');
|
||||||
this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true);
|
this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true);
|
||||||
this.config.sshHost = this.config.sshHost || this.config.ticketHost;
|
this.config.sshHost = this.config.sshHost || this.config.ticketHost;
|
||||||
this.config.sshPort = this.config.sshPort || 22;
|
this.config.sshPort = this.config.sshPort || 22;
|
||||||
this.config.sshUser = this.config.sshUser || 'exodus_server';
|
this.config.sshUser = this.config.sshUser || 'exodus_server';
|
||||||
this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa');
|
this.config.sshKeyPem =
|
||||||
|
this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa');
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
|
const self = this;
|
||||||
const self = this;
|
let clientTerminated = false;
|
||||||
let clientTerminated = false;
|
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
// very basic validation on optionals
|
// very basic validation on optionals
|
||||||
async.each( [ 'board', 'key', 'door' ], (key, next) => {
|
async.each(
|
||||||
return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`));
|
['board', 'key', 'door'],
|
||||||
}, callback);
|
(key, next) => {
|
||||||
|
return _.isString(self.config[key])
|
||||||
|
? next(null)
|
||||||
|
: next(Errors.MissingConfig(`Config requires "${key}"!`));
|
||||||
|
},
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function loadCertAuthorities(callback) {
|
function loadCertAuthorities(callback) {
|
||||||
if(!_.isString(self.config.caPem)) {
|
if (!_.isString(self.config.caPem)) {
|
||||||
return callback(null, null);
|
return callback(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,31 +99,34 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function getTicket(certAuthorities, callback) {
|
function getTicket(certAuthorities, callback) {
|
||||||
const now = moment.utc().unix();
|
const now = moment.utc().unix();
|
||||||
const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex');
|
const sha256 = crypto
|
||||||
const token = `${sha256}|${now}`;
|
.createHash('sha256')
|
||||||
|
.update(`${self.config.key}${now}`)
|
||||||
|
.digest('hex');
|
||||||
|
const token = `${sha256}|${now}`;
|
||||||
|
|
||||||
const postData = querystring.stringify({
|
const postData = querystring.stringify({
|
||||||
token : token,
|
token: token,
|
||||||
board : self.config.board,
|
board: self.config.board,
|
||||||
user : self.client.user.username,
|
user: self.client.user.username,
|
||||||
door : self.config.door,
|
door: self.config.door,
|
||||||
});
|
});
|
||||||
|
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
hostname : self.config.ticketHost,
|
hostname: self.config.ticketHost,
|
||||||
port : self.config.ticketPort,
|
port: self.config.ticketPort,
|
||||||
path : self.config.ticketPath,
|
path: self.config.ticketPath,
|
||||||
rejectUnauthorized : self.config.rejectUnauthorized,
|
rejectUnauthorized: self.config.rejectUnauthorized,
|
||||||
method : 'POST',
|
method: 'POST',
|
||||||
headers : {
|
headers: {
|
||||||
'Content-Type' : 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'Content-Length' : postData.length,
|
'Content-Length': postData.length,
|
||||||
'User-Agent' : getEnigmaUserAgent(),
|
'User-Agent': getEnigmaUserAgent(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if(certAuthorities) {
|
if (certAuthorities) {
|
||||||
reqOptions.ca = certAuthorities;
|
reqOptions.ca = certAuthorities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,8 +137,10 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
if(ticket.length !== 36) {
|
if (ticket.length !== 36) {
|
||||||
return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`));
|
return callback(
|
||||||
|
Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null, ticket);
|
return callback(null, ticket);
|
||||||
@@ -154,52 +160,58 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function establishSecureConnection(ticket, privateKey, callback) {
|
function establishSecureConnection(ticket, privateKey, callback) {
|
||||||
|
|
||||||
let pipeRestored = false;
|
let pipeRestored = false;
|
||||||
let pipedStream;
|
let pipedStream;
|
||||||
let doorTracking;
|
let doorTracking;
|
||||||
|
|
||||||
function restorePipe() {
|
function restorePipe() {
|
||||||
if(pipedStream && !pipeRestored && !clientTerminated) {
|
if (pipedStream && !pipeRestored && !clientTerminated) {
|
||||||
self.client.term.output.unpipe(pipedStream);
|
self.client.term.output.unpipe(pipedStream);
|
||||||
self.client.term.output.resume();
|
self.client.term.output.resume();
|
||||||
|
|
||||||
if(doorTracking) {
|
if (doorTracking) {
|
||||||
trackDoorRunEnd(doorTracking);
|
trackDoorRunEnd(doorTracking);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.client.term.write(resetScreen());
|
self.client.term.write(resetScreen());
|
||||||
self.client.term.write('Connecting to Exodus server, please wait...\n');
|
self.client.term.write(
|
||||||
|
'Connecting to Exodus server, please wait...\n'
|
||||||
|
);
|
||||||
|
|
||||||
const sshClient = new SSHClient();
|
const sshClient = new SSHClient();
|
||||||
|
|
||||||
const window = {
|
const window = {
|
||||||
rows : self.client.term.termHeight,
|
rows: self.client.term.termHeight,
|
||||||
cols : self.client.term.termWidth,
|
cols: self.client.term.termWidth,
|
||||||
width : 0,
|
width: 0,
|
||||||
height : 0,
|
height: 0,
|
||||||
term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
|
term: 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
env : {
|
env: {
|
||||||
exodus : ticket,
|
exodus: ticket,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
sshClient.on('ready', () => {
|
sshClient.on('ready', () => {
|
||||||
self.client.once('end', () => {
|
self.client.once('end', () => {
|
||||||
self.client.log.info('Connection ended. Terminating Exodus connection');
|
self.client.log.info(
|
||||||
|
'Connection ended. Terminating Exodus connection'
|
||||||
|
);
|
||||||
clientTerminated = true;
|
clientTerminated = true;
|
||||||
return sshClient.end();
|
return sshClient.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
sshClient.shell(window, options, (err, stream) => {
|
sshClient.shell(window, options, (err, stream) => {
|
||||||
doorTracking = trackDoorRunBegin(self.client, `exodus_${self.config.door}`);
|
doorTracking = trackDoorRunBegin(
|
||||||
|
self.client,
|
||||||
|
`exodus_${self.config.door}`
|
||||||
|
);
|
||||||
|
|
||||||
pipedStream = stream; // :TODO: ewwwwwwwww hack
|
pipedStream = stream; // :TODO: ewwwwwwwww hack
|
||||||
self.client.term.output.pipe(stream);
|
self.client.term.output.pipe(stream);
|
||||||
|
|
||||||
stream.on('data', d => {
|
stream.on('data', d => {
|
||||||
@@ -212,7 +224,10 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
|
|
||||||
stream.on('error', err => {
|
stream.on('error', err => {
|
||||||
Log.warn( { error : err.message }, 'Exodus SSH client stream error');
|
Log.warn(
|
||||||
|
{ error: err.message },
|
||||||
|
'Exodus SSH client stream error'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -223,19 +238,19 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sshClient.connect({
|
sshClient.connect({
|
||||||
host : self.config.sshHost,
|
host: self.config.sshHost,
|
||||||
port : self.config.sshPort,
|
port: self.config.sshPort,
|
||||||
username : self.config.sshUser,
|
username: self.config.sshUser,
|
||||||
privateKey : privateKey,
|
privateKey: privateKey,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.message }, 'Exodus error');
|
self.client.log.warn({ error: err.message }, 'Exodus error');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!clientTerminated) {
|
if (!clientTerminated) {
|
||||||
self.prevMenu();
|
self.prevMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,84 +2,88 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
const getSortedAvailableFileAreas =
|
||||||
const FileBaseFilters = require('./file_base_filter.js');
|
require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||||
const stringFormat = require('./string_format.js');
|
const FileBaseFilters = require('./file_base_filter.js');
|
||||||
const UserProps = require('./user_property.js');
|
const stringFormat = require('./string_format.js');
|
||||||
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Area Filter Editor',
|
name: 'File Area Filter Editor',
|
||||||
desc : 'Module for adding, deleting, and modifying file base filters',
|
desc: 'Module for adding, deleting, and modifying file base filters',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
editor : {
|
editor: {
|
||||||
searchTerms : 1,
|
searchTerms: 1,
|
||||||
tags : 2,
|
tags: 2,
|
||||||
area : 3,
|
area: 3,
|
||||||
sort : 4,
|
sort: 4,
|
||||||
order : 5,
|
order: 5,
|
||||||
filterName : 6,
|
filterName: 6,
|
||||||
navMenu : 7,
|
navMenu: 7,
|
||||||
|
|
||||||
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
|
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
|
||||||
selectedFilterInfo : 10, // { ...filter object ... }
|
selectedFilterInfo: 10, // { ...filter object ... }
|
||||||
activeFilterInfo : 11, // { ...filter object ... }
|
activeFilterInfo: 11, // { ...filter object ... }
|
||||||
error : 12, // validation errors
|
error: 12, // validation errors
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
|
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
|
||||||
this.currentFilterIndex = 0; // into |filtersArray|
|
this.currentFilterIndex = 0; // into |filtersArray|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
|
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
|
||||||
//
|
//
|
||||||
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||||
this.filtersArray.sort( (filterA, filterB) => {
|
this.filtersArray.sort((filterA, filterB) => {
|
||||||
if(activeFilter) {
|
if (activeFilter) {
|
||||||
if(filterA.uuid === activeFilter.uuid) {
|
if (filterA.uuid === activeFilter.uuid) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if(filterB.uuid === activeFilter.uuid) {
|
if (filterB.uuid === activeFilter.uuid) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } );
|
return filterA.name.localeCompare(filterB.name, {
|
||||||
|
sensitivity: false,
|
||||||
|
numeric: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
saveFilter : (formData, extraArgs, cb) => {
|
saveFilter: (formData, extraArgs, cb) => {
|
||||||
return this.saveCurrentFilter(formData, cb);
|
return this.saveCurrentFilter(formData, cb);
|
||||||
},
|
},
|
||||||
prevFilter : (formData, extraArgs, cb) => {
|
prevFilter: (formData, extraArgs, cb) => {
|
||||||
this.currentFilterIndex -= 1;
|
this.currentFilterIndex -= 1;
|
||||||
if(this.currentFilterIndex < 0) {
|
if (this.currentFilterIndex < 0) {
|
||||||
this.currentFilterIndex = this.filtersArray.length - 1;
|
this.currentFilterIndex = this.filtersArray.length - 1;
|
||||||
}
|
}
|
||||||
this.loadDataForFilter(this.currentFilterIndex);
|
this.loadDataForFilter(this.currentFilterIndex);
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
nextFilter : (formData, extraArgs, cb) => {
|
nextFilter: (formData, extraArgs, cb) => {
|
||||||
this.currentFilterIndex += 1;
|
this.currentFilterIndex += 1;
|
||||||
if(this.currentFilterIndex >= this.filtersArray.length) {
|
if (this.currentFilterIndex >= this.filtersArray.length) {
|
||||||
this.currentFilterIndex = 0;
|
this.currentFilterIndex = 0;
|
||||||
}
|
}
|
||||||
this.loadDataForFilter(this.currentFilterIndex);
|
this.loadDataForFilter(this.currentFilterIndex);
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
makeFilterActive : (formData, extraArgs, cb) => {
|
makeFilterActive: (formData, extraArgs, cb) => {
|
||||||
const filters = new FileBaseFilters(this.client);
|
const filters = new FileBaseFilters(this.client);
|
||||||
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
|
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
|
||||||
|
|
||||||
@@ -87,45 +91,49 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
newFilter : (formData, extraArgs, cb) => {
|
newFilter: (formData, extraArgs, cb) => {
|
||||||
this.currentFilterIndex = this.filtersArray.length; // next avail slot
|
this.currentFilterIndex = this.filtersArray.length; // next avail slot
|
||||||
this.clearForm(MciViewIds.editor.searchTerms);
|
this.clearForm(MciViewIds.editor.searchTerms);
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
deleteFilter : (formData, extraArgs, cb) => {
|
deleteFilter: (formData, extraArgs, cb) => {
|
||||||
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
||||||
const filterUuid = selectedFilter.uuid;
|
const filterUuid = selectedFilter.uuid;
|
||||||
|
|
||||||
// cannot delete built-in/system filters
|
// cannot delete built-in/system filters
|
||||||
if(true === selectedFilter.system) {
|
if (true === selectedFilter.system) {
|
||||||
this.showError('Cannot delete built in filters!');
|
this.showError('Cannot delete built in filters!');
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
|
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
|
||||||
|
|
||||||
// remove from stored properties
|
// remove from stored properties
|
||||||
const filters = new FileBaseFilters(this.client);
|
const filters = new FileBaseFilters(this.client);
|
||||||
filters.remove(filterUuid);
|
filters.remove(filterUuid);
|
||||||
filters.persist( () => {
|
filters.persist(() => {
|
||||||
|
|
||||||
//
|
//
|
||||||
// If the item was also the active filter, we need to make a new one active
|
// If the item was also the active filter, we need to make a new one active
|
||||||
//
|
//
|
||||||
if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) {
|
if (
|
||||||
|
filterUuid ===
|
||||||
|
this.client.user.properties[UserProps.FileBaseFilterActiveUuid]
|
||||||
|
) {
|
||||||
const newActive = this.filtersArray[this.currentFilterIndex];
|
const newActive = this.filtersArray[this.currentFilterIndex];
|
||||||
if(newActive) {
|
if (newActive) {
|
||||||
filters.setActive(newActive.uuid);
|
filters.setActive(newActive.uuid);
|
||||||
} else {
|
} else {
|
||||||
// nothing to set active to
|
// nothing to set active to
|
||||||
this.client.user.removeProperty('file_base_filter_active_uuid');
|
this.client.user.removeProperty(
|
||||||
|
'file_base_filter_active_uuid'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update UI
|
// update UI
|
||||||
this.updateActiveLabel();
|
this.updateActiveLabel();
|
||||||
|
|
||||||
if(this.filtersArray.length > 0) {
|
if (this.filtersArray.length > 0) {
|
||||||
this.loadDataForFilter(this.currentFilterIndex);
|
this.loadDataForFilter(this.currentFilterIndex);
|
||||||
} else {
|
} else {
|
||||||
this.clearForm();
|
this.clearForm();
|
||||||
@@ -134,14 +142,16 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
viewValidationListener : (err, cb) => {
|
viewValidationListener: (err, cb) => {
|
||||||
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
|
const errorView = this.viewControllers.editor.getView(
|
||||||
|
MciViewIds.editor.error
|
||||||
|
);
|
||||||
let newFocusId;
|
let newFocusId;
|
||||||
|
|
||||||
if(errorView) {
|
if (errorView) {
|
||||||
if(err) {
|
if (err) {
|
||||||
errorView.setText(err.message);
|
errorView.setText(err.message);
|
||||||
err.view.clearText(); // clear out the invalid data
|
err.view.clearText(); // clear out the invalid data
|
||||||
} else {
|
} else {
|
||||||
errorView.clearText();
|
errorView.clearText();
|
||||||
}
|
}
|
||||||
@@ -154,8 +164,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
|
|
||||||
showError(errMsg) {
|
showError(errMsg) {
|
||||||
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
|
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
|
||||||
if(errorView) {
|
if (errorView) {
|
||||||
if(errMsg) {
|
if (errMsg) {
|
||||||
errorView.setText(errMsg);
|
errorView.setText(errMsg);
|
||||||
} else {
|
} else {
|
||||||
errorView.clearText();
|
errorView.clearText();
|
||||||
@@ -165,31 +175,39 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
|
const vc = self.addViewController(
|
||||||
|
'editor',
|
||||||
|
new ViewController({ client: this.client })
|
||||||
|
);
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function loadFromConfig(callback) {
|
function loadFromConfig(callback) {
|
||||||
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
return vc.loadFromMenuConfig(
|
||||||
|
{ callingMenu: self, mciMap: mciData.menu },
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function populateAreas(callback) {
|
function populateAreas(callback) {
|
||||||
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
self.availAreas = [{ name: '-ALL-' }].concat(
|
||||||
|
getSortedAvailableFileAreas(self.client) || []
|
||||||
|
);
|
||||||
|
|
||||||
const areasView = vc.getView(MciViewIds.editor.area);
|
const areasView = vc.getView(MciViewIds.editor.area);
|
||||||
if(areasView) {
|
if (areasView) {
|
||||||
areasView.setItems( self.availAreas.map( a => a.name ) );
|
areasView.setItems(self.availAreas.map(a => a.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateActiveLabel();
|
self.updateActiveLabel();
|
||||||
self.loadDataForFilter(self.currentFilterIndex);
|
self.loadDataForFilter(self.currentFilterIndex);
|
||||||
self.viewControllers.editor.resetInitialFocus();
|
self.viewControllers.editor.resetInitialFocus();
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -204,36 +222,45 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
|
|
||||||
setText(mciId, text) {
|
setText(mciId, text) {
|
||||||
const view = this.viewControllers.editor.getView(mciId);
|
const view = this.viewControllers.editor.getView(mciId);
|
||||||
if(view) {
|
if (view) {
|
||||||
view.setText(text);
|
view.setText(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActiveLabel() {
|
updateActiveLabel() {
|
||||||
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||||
if(activeFilter) {
|
if (activeFilter) {
|
||||||
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
|
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
|
||||||
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
|
this.setText(
|
||||||
|
MciViewIds.editor.activeFilterInfo,
|
||||||
|
stringFormat(activeFormat, activeFilter)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFocusItemIndex(mciId, index) {
|
setFocusItemIndex(mciId, index) {
|
||||||
const view = this.viewControllers.editor.getView(mciId);
|
const view = this.viewControllers.editor.getView(mciId);
|
||||||
if(view) {
|
if (view) {
|
||||||
view.setFocusItemIndex(index);
|
view.setFocusItemIndex(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearForm(newFocusId) {
|
clearForm(newFocusId) {
|
||||||
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
|
[
|
||||||
|
MciViewIds.editor.searchTerms,
|
||||||
|
MciViewIds.editor.tags,
|
||||||
|
MciViewIds.editor.filterName,
|
||||||
|
].forEach(mciId => {
|
||||||
this.setText(mciId, '');
|
this.setText(mciId, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
|
[MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort].forEach(
|
||||||
this.setFocusItemIndex(mciId, 0);
|
mciId => {
|
||||||
});
|
this.setFocusItemIndex(mciId, 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if(newFocusId) {
|
if (newFocusId) {
|
||||||
this.viewControllers.editor.switchFocus(newFocusId);
|
this.viewControllers.editor.switchFocus(newFocusId);
|
||||||
} else {
|
} else {
|
||||||
this.viewControllers.editor.resetInitialFocus();
|
this.viewControllers.editor.resetInitialFocus();
|
||||||
@@ -241,11 +268,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSelectedAreaTag(index) {
|
getSelectedAreaTag(index) {
|
||||||
if(0 === index) {
|
if (0 === index) {
|
||||||
return ''; // -ALL-
|
return ''; // -ALL-
|
||||||
}
|
}
|
||||||
const area = this.availAreas[index];
|
const area = this.availAreas[index];
|
||||||
if(!area) {
|
if (!area) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return area.areaTag;
|
return area.areaTag;
|
||||||
@@ -258,9 +285,12 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
setAreaIndexFromCurrentFilter() {
|
setAreaIndexFromCurrentFilter() {
|
||||||
let index;
|
let index;
|
||||||
const filter = this.getCurrentFilter();
|
const filter = this.getCurrentFilter();
|
||||||
if(filter) {
|
if (filter) {
|
||||||
// special treatment: areaTag saved as blank ("") if -ALL-
|
// special treatment: areaTag saved as blank ("") if -ALL-
|
||||||
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
|
index =
|
||||||
|
(filter.areaTag &&
|
||||||
|
this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) ||
|
||||||
|
0;
|
||||||
} else {
|
} else {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
@@ -270,8 +300,9 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
setOrderByFromCurrentFilter() {
|
setOrderByFromCurrentFilter() {
|
||||||
let index;
|
let index;
|
||||||
const filter = this.getCurrentFilter();
|
const filter = this.getCurrentFilter();
|
||||||
if(filter) {
|
if (filter) {
|
||||||
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
|
index =
|
||||||
|
FileBaseFilters.OrderByValues.findIndex(ob => filter.order === ob) || 0;
|
||||||
} else {
|
} else {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
@@ -281,8 +312,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
setSortByFromCurrentFilter() {
|
setSortByFromCurrentFilter() {
|
||||||
let index;
|
let index;
|
||||||
const filter = this.getCurrentFilter();
|
const filter = this.getCurrentFilter();
|
||||||
if(filter) {
|
if (filter) {
|
||||||
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
|
index = FileBaseFilters.SortByValues.findIndex(sb => filter.sort === sb) || 0;
|
||||||
} else {
|
} else {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
@@ -294,19 +325,19 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFilterValuesFromFormData(filter, formData) {
|
setFilterValuesFromFormData(filter, formData) {
|
||||||
filter.name = formData.value.name;
|
filter.name = formData.value.name;
|
||||||
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
|
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
|
||||||
filter.terms = formData.value.searchTerms;
|
filter.terms = formData.value.searchTerms;
|
||||||
filter.tags = formData.value.tags;
|
filter.tags = formData.value.tags;
|
||||||
filter.order = this.getOrderBy(formData.value.orderByIndex);
|
filter.order = this.getOrderBy(formData.value.orderByIndex);
|
||||||
filter.sort = this.getSortBy(formData.value.sortByIndex);
|
filter.sort = this.getSortBy(formData.value.sortByIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCurrentFilter(formData, cb) {
|
saveCurrentFilter(formData, cb) {
|
||||||
const filters = new FileBaseFilters(this.client);
|
const filters = new FileBaseFilters(this.client);
|
||||||
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
||||||
|
|
||||||
if(selectedFilter) {
|
if (selectedFilter) {
|
||||||
// *update* currently selected filter
|
// *update* currently selected filter
|
||||||
this.setFilterValuesFromFormData(selectedFilter, formData);
|
this.setFilterValuesFromFormData(selectedFilter, formData);
|
||||||
filters.replace(selectedFilter.uuid, selectedFilter);
|
filters.replace(selectedFilter.uuid, selectedFilter);
|
||||||
@@ -327,10 +358,10 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||||||
|
|
||||||
loadDataForFilter(filterIndex) {
|
loadDataForFilter(filterIndex) {
|
||||||
const filter = this.filtersArray[filterIndex];
|
const filter = this.filtersArray[filterIndex];
|
||||||
if(filter) {
|
if (filter) {
|
||||||
this.setText(MciViewIds.editor.searchTerms, filter.terms);
|
this.setText(MciViewIds.editor.searchTerms, filter.terms);
|
||||||
this.setText(MciViewIds.editor.tags, filter.tags);
|
this.setText(MciViewIds.editor.tags, filter.tags);
|
||||||
this.setText(MciViewIds.editor.filterName, filter.name);
|
this.setText(MciViewIds.editor.filterName, filter.name);
|
||||||
|
|
||||||
this.setAreaIndexFromCurrentFilter();
|
this.setAreaIndexFromCurrentFilter();
|
||||||
this.setSortByFromCurrentFilter();
|
this.setSortByFromCurrentFilter();
|
||||||
|
|||||||
@@ -2,150 +2,149 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const theme = require('./theme.js');
|
const theme = require('./theme.js');
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const FileArea = require('./file_base_area.js');
|
const FileArea = require('./file_base_area.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
||||||
const ArchiveUtil = require('./archive_util.js');
|
const ArchiveUtil = require('./archive_util.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
const FileAreaWeb = require('./file_area_web.js');
|
const FileAreaWeb = require('./file_area_web.js');
|
||||||
const FileBaseFilters = require('./file_base_filter.js');
|
const FileBaseFilters = require('./file_base_filter.js');
|
||||||
const resolveMimeType = require('./mime_util.js').resolveMimeType;
|
const resolveMimeType = require('./mime_util.js').resolveMimeType;
|
||||||
const isAnsi = require('./string_util.js').isAnsi;
|
const isAnsi = require('./string_util.js').isAnsi;
|
||||||
const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi;
|
const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Area List',
|
name: 'File Area List',
|
||||||
desc : 'Lists contents of file an file area',
|
desc: 'Lists contents of file an file area',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
browse : 0,
|
browse: 0,
|
||||||
details : 1,
|
details: 1,
|
||||||
detailsGeneral : 2,
|
detailsGeneral: 2,
|
||||||
detailsNfo : 3,
|
detailsNfo: 3,
|
||||||
detailsFileList : 4,
|
detailsFileList: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
browse : {
|
browse: {
|
||||||
desc : 1,
|
desc: 1,
|
||||||
navMenu : 2,
|
navMenu: 2,
|
||||||
|
|
||||||
customRangeStart : 10, // 10+ = customs
|
customRangeStart: 10, // 10+ = customs
|
||||||
},
|
},
|
||||||
details : {
|
details: {
|
||||||
navMenu : 1,
|
navMenu: 1,
|
||||||
infoXyTop : 2, // %XY starting position for info area
|
infoXyTop: 2, // %XY starting position for info area
|
||||||
infoXyBottom : 3,
|
infoXyBottom: 3,
|
||||||
|
|
||||||
customRangeStart : 10, // 10+ = customs
|
customRangeStart: 10, // 10+ = customs
|
||||||
},
|
},
|
||||||
detailsGeneral : {
|
detailsGeneral: {
|
||||||
customRangeStart : 10, // 10+ = customs
|
customRangeStart: 10, // 10+ = customs
|
||||||
},
|
},
|
||||||
detailsNfo : {
|
detailsNfo: {
|
||||||
nfo : 1,
|
nfo: 1,
|
||||||
|
|
||||||
customRangeStart : 10, // 10+ = customs
|
customRangeStart: 10, // 10+ = customs
|
||||||
},
|
},
|
||||||
detailsFileList : {
|
detailsFileList: {
|
||||||
fileList : 1,
|
fileList: 1,
|
||||||
|
|
||||||
customRangeStart : 10, // 10+ = customs
|
customRangeStart: 10, // 10+ = customs
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileAreaList extends MenuModule {
|
exports.getModule = class FileAreaList extends MenuModule {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.filterCriteria = _.get(options, 'extraArgs.filterCriteria');
|
this.filterCriteria = _.get(options, 'extraArgs.filterCriteria');
|
||||||
this.fileList = _.get(options, 'extraArgs.fileList');
|
this.fileList = _.get(options, 'extraArgs.fileList');
|
||||||
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
|
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
|
||||||
|
|
||||||
if(this.fileList) {
|
if (this.fileList) {
|
||||||
// we'll need to adjust position as well!
|
// we'll need to adjust position as well!
|
||||||
this.fileListPosition = 0;
|
this.fileListPosition = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dlQueue = new DownloadQueue(this.client);
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
if(!this.filterCriteria) {
|
if (!this.filterCriteria) {
|
||||||
this.filterCriteria = FileBaseFilters.getActiveFilter(this.client);
|
this.filterCriteria = FileBaseFilters.getActiveFilter(this.client);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(this.filterCriteria)) {
|
if (_.isString(this.filterCriteria)) {
|
||||||
this.filterCriteria = JSON.parse(this.filterCriteria);
|
this.filterCriteria = JSON.parse(this.filterCriteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.has(options, 'lastMenuResult.value')) {
|
if (_.has(options, 'lastMenuResult.value')) {
|
||||||
this.lastMenuResultValue = options.lastMenuResult.value;
|
this.lastMenuResultValue = options.lastMenuResult.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
nextFile : (formData, extraArgs, cb) => {
|
nextFile: (formData, extraArgs, cb) => {
|
||||||
if(this.fileListPosition + 1 < this.fileList.length) {
|
if (this.fileListPosition + 1 < this.fileList.length) {
|
||||||
this.fileListPosition += 1;
|
this.fileListPosition += 1;
|
||||||
|
|
||||||
return this.displayBrowsePage(true, cb); // true=clerarScreen
|
return this.displayBrowsePage(true, cb); // true=clerarScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.lastFileNextExit) {
|
if (this.lastFileNextExit) {
|
||||||
return this.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
prevFile : (formData, extraArgs, cb) => {
|
prevFile: (formData, extraArgs, cb) => {
|
||||||
if(this.fileListPosition > 0) {
|
if (this.fileListPosition > 0) {
|
||||||
--this.fileListPosition;
|
--this.fileListPosition;
|
||||||
|
|
||||||
return this.displayBrowsePage(true, cb); // true=clearScreen
|
return this.displayBrowsePage(true, cb); // true=clearScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
viewDetails : (formData, extraArgs, cb) => {
|
viewDetails: (formData, extraArgs, cb) => {
|
||||||
this.viewControllers.browse.setFocus(false);
|
this.viewControllers.browse.setFocus(false);
|
||||||
return this.displayDetailsPage(cb);
|
return this.displayDetailsPage(cb);
|
||||||
},
|
},
|
||||||
detailsQuit : (formData, extraArgs, cb) => {
|
detailsQuit: (formData, extraArgs, cb) => {
|
||||||
[ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => {
|
['detailsNfo', 'detailsFileList', 'details'].forEach(n => {
|
||||||
const vc = this.viewControllers[n];
|
const vc = this.viewControllers[n];
|
||||||
if(vc) {
|
if (vc) {
|
||||||
vc.detachClientEvents();
|
vc.detachClientEvents();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.displayBrowsePage(true, cb); // true=clearScreen
|
return this.displayBrowsePage(true, cb); // true=clearScreen
|
||||||
},
|
},
|
||||||
toggleQueue : (formData, extraArgs, cb) => {
|
toggleQueue: (formData, extraArgs, cb) => {
|
||||||
this.dlQueue.toggle(this.currentFileEntry);
|
this.dlQueue.toggle(this.currentFileEntry);
|
||||||
this.updateQueueIndicator();
|
this.updateQueueIndicator();
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
showWebDownloadLink : (formData, extraArgs, cb) => {
|
showWebDownloadLink: (formData, extraArgs, cb) => {
|
||||||
return this.fetchAndDisplayWebDownloadLink(cb);
|
return this.fetchAndDisplayWebDownloadLink(cb);
|
||||||
},
|
},
|
||||||
displayHelp : (formData, extraArgs, cb) => {
|
displayHelp: (formData, extraArgs, cb) => {
|
||||||
return this.displayHelpPage(cb);
|
return this.displayHelpPage(cb);
|
||||||
},
|
},
|
||||||
movementKeyPressed : (formData, extraArgs, cb) => {
|
movementKeyPressed: (formData, extraArgs, cb) => {
|
||||||
return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb);
|
return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -161,31 +160,39 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
|
|
||||||
getSaveState() {
|
getSaveState() {
|
||||||
return {
|
return {
|
||||||
fileList : this.fileList,
|
fileList: this.fileList,
|
||||||
fileListPosition : this.fileListPosition,
|
fileListPosition: this.fileListPosition,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreSavedState(savedState) {
|
restoreSavedState(savedState) {
|
||||||
if(savedState) {
|
if (savedState) {
|
||||||
this.fileList = savedState.fileList;
|
this.fileList = savedState.fileList;
|
||||||
this.fileListPosition = savedState.fileListPosition;
|
this.fileListPosition = savedState.fileListPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFileEntryWithMenuResult(cb) {
|
updateFileEntryWithMenuResult(cb) {
|
||||||
if(!this.lastMenuResultValue) {
|
if (!this.lastMenuResultValue) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isNumber(this.lastMenuResultValue.rating)) {
|
if (_.isNumber(this.lastMenuResultValue.rating)) {
|
||||||
const fileId = this.fileList[this.fileListPosition];
|
const fileId = this.fileList[this.fileListPosition];
|
||||||
FileEntry.persistUserRating(fileId, this.client.user.userId, this.lastMenuResultValue.rating, err => {
|
FileEntry.persistUserRating(
|
||||||
if(err) {
|
fileId,
|
||||||
this.client.log.warn( { error : err.message, fileId : fileId }, 'Failed to persist file rating' );
|
this.client.user.userId,
|
||||||
|
this.lastMenuResultValue.rating,
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
this.client.log.warn(
|
||||||
|
{ error: err.message, fileId: fileId },
|
||||||
|
'Failed to persist file rating'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cb(null);
|
||||||
}
|
}
|
||||||
return cb(null);
|
);
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
@@ -204,12 +211,15 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
},
|
},
|
||||||
function display(callback) {
|
function display(callback) {
|
||||||
return self.displayBrowsePage(false, err => {
|
return self.displayBrowsePage(false, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults');
|
self.gotoMenu(
|
||||||
|
self.menuConfig.config.noResultsMenu ||
|
||||||
|
'fileBaseListEntriesNoResults'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
self.finishedLoading();
|
self.finishedLoading();
|
||||||
@@ -218,31 +228,37 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
populateCurrentEntryInfo(cb) {
|
populateCurrentEntryInfo(cb) {
|
||||||
const config = this.menuConfig.config;
|
const config = this.menuConfig.config;
|
||||||
const currEntry = this.currentFileEntry;
|
const currEntry = this.currentFileEntry;
|
||||||
|
|
||||||
const uploadTimestampFormat = config.uploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short');
|
const uploadTimestampFormat =
|
||||||
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
|
config.uploadTimestampFormat ||
|
||||||
const hashTagsSep = config.hashTagsSep || ', ';
|
this.client.currentTheme.helpers.getDateFormat('short');
|
||||||
const isQueuedIndicator = config.isQueuedIndicator || 'Y';
|
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
|
||||||
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
|
const hashTagsSep = config.hashTagsSep || ', ';
|
||||||
|
const isQueuedIndicator = config.isQueuedIndicator || 'Y';
|
||||||
|
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
|
||||||
|
|
||||||
const entryInfo = currEntry.entryInfo = {
|
const entryInfo = (currEntry.entryInfo = {
|
||||||
fileId : currEntry.fileId,
|
fileId: currEntry.fileId,
|
||||||
areaTag : currEntry.areaTag,
|
areaTag: currEntry.areaTag,
|
||||||
areaName : _.get(area, 'name') || 'N/A',
|
areaName: _.get(area, 'name') || 'N/A',
|
||||||
areaDesc : _.get(area, 'desc') || 'N/A',
|
areaDesc: _.get(area, 'desc') || 'N/A',
|
||||||
fileSha256 : currEntry.fileSha256,
|
fileSha256: currEntry.fileSha256,
|
||||||
fileName : currEntry.fileName,
|
fileName: currEntry.fileName,
|
||||||
desc : currEntry.desc || '',
|
desc: currEntry.desc || '',
|
||||||
descLong : currEntry.descLong || '',
|
descLong: currEntry.descLong || '',
|
||||||
userRating : currEntry.userRating,
|
userRating: currEntry.userRating,
|
||||||
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
|
uploadTimestamp: moment(currEntry.uploadTimestamp).format(
|
||||||
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
|
uploadTimestampFormat
|
||||||
isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator,
|
),
|
||||||
webDlLink : '', // :TODO: fetch web any existing web d/l link
|
hashTags: Array.from(currEntry.hashTags).join(hashTagsSep),
|
||||||
webDlExpire : '', // :TODO: fetch web d/l link expire time
|
isQueued: this.dlQueue.isQueued(currEntry)
|
||||||
};
|
? isQueuedIndicator
|
||||||
|
: isNotQueuedIndicator,
|
||||||
|
webDlLink: '', // :TODO: fetch web any existing web d/l link
|
||||||
|
webDlExpire: '', // :TODO: fetch web d/l link expire time
|
||||||
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// We need the entry object to contain meta keys even if they are empty as
|
// We need the entry object to contain meta keys even if they are empty as
|
||||||
@@ -250,19 +266,23 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
//
|
//
|
||||||
const metaValues = FileEntry.WellKnownMetaValues;
|
const metaValues = FileEntry.WellKnownMetaValues;
|
||||||
metaValues.forEach(name => {
|
metaValues.forEach(name => {
|
||||||
const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A';
|
const value = !_.isUndefined(currEntry.meta[name])
|
||||||
|
? currEntry.meta[name]
|
||||||
|
: 'N/A';
|
||||||
entryInfo[_.camelCase(name)] = value;
|
entryInfo[_.camelCase(name)] = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
if(entryInfo.archiveType) {
|
if (entryInfo.archiveType) {
|
||||||
const mimeType = resolveMimeType(entryInfo.archiveType);
|
const mimeType = resolveMimeType(entryInfo.archiveType);
|
||||||
let desc;
|
let desc;
|
||||||
if(mimeType) {
|
if (mimeType) {
|
||||||
let fileType = _.get(Config(), [ 'fileTypes', mimeType ] );
|
let fileType = _.get(Config(), ['fileTypes', mimeType]);
|
||||||
|
|
||||||
if(Array.isArray(fileType)) {
|
if (Array.isArray(fileType)) {
|
||||||
// further refine by extention
|
// further refine by extention
|
||||||
fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext);
|
fileType = fileType.find(
|
||||||
|
ft => paths.extname(currEntry.fileName) === ft.ext
|
||||||
|
);
|
||||||
}
|
}
|
||||||
desc = fileType && fileType.desc;
|
desc = fileType && fileType.desc;
|
||||||
}
|
}
|
||||||
@@ -271,39 +291,57 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
entryInfo.archiveTypeDesc = 'N/A';
|
entryInfo.archiveTypeDesc = 'N/A';
|
||||||
}
|
}
|
||||||
|
|
||||||
entryInfo.uploadByUsername = entryInfo.uploadByUserName = entryInfo.uploadByUsername || 'N/A'; // may be imported
|
entryInfo.uploadByUsername = entryInfo.uploadByUserName =
|
||||||
entryInfo.hashTags = entryInfo.hashTags || '(none)';
|
entryInfo.uploadByUsername || 'N/A'; // may be imported
|
||||||
|
entryInfo.hashTags = entryInfo.hashTags || '(none)';
|
||||||
|
|
||||||
// create a rating string, e.g. "**---"
|
// create a rating string, e.g. "**---"
|
||||||
const userRatingTicked = config.userRatingTicked || '*';
|
const userRatingTicked = config.userRatingTicked || '*';
|
||||||
const userRatingUnticked = config.userRatingUnticked || '';
|
const userRatingUnticked = config.userRatingUnticked || '';
|
||||||
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
|
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
|
||||||
entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating);
|
entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating);
|
||||||
if(entryInfo.userRating < 5) {
|
if (entryInfo.userRating < 5) {
|
||||||
entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) );
|
entryInfo.userRatingString += userRatingUnticked.repeat(
|
||||||
|
5 - entryInfo.userRating
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
|
FileAreaWeb.getExistingTempDownloadServeItem(
|
||||||
if(err) {
|
this.client,
|
||||||
entryInfo.webDlExpire = '';
|
this.currentFileEntry,
|
||||||
if(ErrNotEnabled === err.reasonCode) {
|
(err, serveItem) => {
|
||||||
entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled';
|
if (err) {
|
||||||
|
entryInfo.webDlExpire = '';
|
||||||
|
if (ErrNotEnabled === err.reasonCode) {
|
||||||
|
entryInfo.webDlExpire =
|
||||||
|
config.webDlLinkNoWebserver || 'Web server is not enabled';
|
||||||
|
} else {
|
||||||
|
entryInfo.webDlLink =
|
||||||
|
config.webDlLinkNeedsGenerated || 'Not yet generated';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
|
const webDlExpireTimeFormat =
|
||||||
|
config.webDlExpireTimeFormat ||
|
||||||
|
this.client.currentTheme.helpers.getDateTimeFormat('short');
|
||||||
|
|
||||||
|
entryInfo.webDlLink =
|
||||||
|
ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
||||||
|
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(
|
||||||
|
webDlExpireTimeFormat
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat('short');
|
|
||||||
|
|
||||||
entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
return cb(null);
|
||||||
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return cb(null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
populateCustomLabels(category, startId) {
|
populateCustomLabels(category, startId) {
|
||||||
return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo);
|
return this.updateCustomViewTextsWithFilter(
|
||||||
|
category,
|
||||||
|
startId,
|
||||||
|
this.currentFileEntry.entryInfo
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
displayArtDataPrepCallback(name, artData, viewController) {
|
displayArtDataPrepCallback(name, artData, viewController) {
|
||||||
@@ -320,19 +358,21 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayBrowsePage(clearScreen, cb) {
|
displayBrowsePage(clearScreen, cb) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function fetchEntryData(callback) {
|
function fetchEntryData(callback) {
|
||||||
if(self.fileList) {
|
if (self.fileList) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
return self.loadFileIds(false, callback); // false=do not force
|
return self.loadFileIds(false, callback); // false=do not force
|
||||||
},
|
},
|
||||||
function checkEmptyResults(callback) {
|
function checkEmptyResults(callback) {
|
||||||
if(0 === self.fileList.length) {
|
if (0 === self.fileList.length) {
|
||||||
return callback(Errors.General('No results for criteria', 'NORESULTS'));
|
return callback(
|
||||||
|
Errors.General('No results for criteria', 'NORESULTS')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
@@ -347,18 +387,23 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
function loadCurrentFileInfo(callback) {
|
function loadCurrentFileInfo(callback) {
|
||||||
self.currentFileEntry = new FileEntry();
|
self.currentFileEntry = new FileEntry();
|
||||||
|
|
||||||
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
|
self.currentFileEntry.load(
|
||||||
if(err) {
|
self.fileList[self.fileListPosition],
|
||||||
return callback(err);
|
err => {
|
||||||
}
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
return self.populateCurrentEntryInfo(callback);
|
return self.populateCurrentEntryInfo(callback);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function populateDesc(callback) {
|
function populateDesc(callback) {
|
||||||
if(_.isString(self.currentFileEntry.desc)) {
|
if (_.isString(self.currentFileEntry.desc)) {
|
||||||
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
|
const descView = self.viewControllers.browse.getView(
|
||||||
if(descView) {
|
MciViewIds.browse.desc
|
||||||
|
);
|
||||||
|
if (descView) {
|
||||||
//
|
//
|
||||||
// For descriptions we want to support as many color code systems
|
// For descriptions we want to support as many color code systems
|
||||||
// as we can for coverage of what is found in the while (e.g. Renegade
|
// as we can for coverage of what is found in the while (e.g. Renegade
|
||||||
@@ -369,18 +414,24 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
// it as text.
|
// it as text.
|
||||||
//
|
//
|
||||||
const desc = controlCodesToAnsi(self.currentFileEntry.desc);
|
const desc = controlCodesToAnsi(self.currentFileEntry.desc);
|
||||||
if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) {
|
if (
|
||||||
|
desc.length != self.currentFileEntry.desc.length ||
|
||||||
|
isAnsi(desc)
|
||||||
|
) {
|
||||||
const opts = {
|
const opts = {
|
||||||
prepped : false,
|
prepped: false,
|
||||||
forceLineTerm : true
|
forceLineTerm: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// if SAUCE states a term width, honor it else we may see
|
// if SAUCE states a term width, honor it else we may see
|
||||||
// display corruption
|
// display corruption
|
||||||
//
|
//
|
||||||
const sauceTermWidth = _.get(self.currentFileEntry.meta, 'desc_sauce.Character.characterWidth');
|
const sauceTermWidth = _.get(
|
||||||
if(_.isNumber(sauceTermWidth)) {
|
self.currentFileEntry.meta,
|
||||||
|
'desc_sauce.Character.characterWidth'
|
||||||
|
);
|
||||||
|
if (_.isNumber(sauceTermWidth)) {
|
||||||
opts.termWidth = sauceTermWidth;
|
opts.termWidth = sauceTermWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,12 +449,15 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
},
|
},
|
||||||
function populateAdditionalViews(callback) {
|
function populateAdditionalViews(callback) {
|
||||||
self.updateQueueIndicator();
|
self.updateQueueIndicator();
|
||||||
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
|
self.populateCustomLabels(
|
||||||
|
'browse',
|
||||||
|
MciViewIds.browse.customRangeStart
|
||||||
|
);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,30 +478,35 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
function populateViews(callback) {
|
function populateViews(callback) {
|
||||||
self.populateCustomLabels('details', MciViewIds.details.customRangeStart);
|
self.populateCustomLabels(
|
||||||
|
'details',
|
||||||
|
MciViewIds.details.customRangeStart
|
||||||
|
);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function prepSection(callback) {
|
function prepSection(callback) {
|
||||||
return self.displayDetailsSection('general', false, callback);
|
return self.displayDetailsSection('general', false, callback);
|
||||||
},
|
},
|
||||||
function listenNavChanges(callback) {
|
function listenNavChanges(callback) {
|
||||||
const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu);
|
const navMenu = self.viewControllers.details.getView(
|
||||||
|
MciViewIds.details.navMenu
|
||||||
|
);
|
||||||
navMenu.setFocusItemIndex(0);
|
navMenu.setFocusItemIndex(0);
|
||||||
|
|
||||||
navMenu.on('index update', index => {
|
navMenu.on('index update', index => {
|
||||||
const sectionName = {
|
const sectionName = {
|
||||||
0 : 'general',
|
0: 'general',
|
||||||
1 : 'nfo',
|
1: 'nfo',
|
||||||
2 : 'fileList',
|
2: 'fileList',
|
||||||
}[index];
|
}[index];
|
||||||
|
|
||||||
if(sectionName) {
|
if (sectionName) {
|
||||||
self.displayDetailsSection(sectionName, true);
|
self.displayDetailsSection(sectionName, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -456,28 +515,32 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayHelpPage(cb) {
|
displayHelpPage(cb) {
|
||||||
this.displayAsset(
|
this.displayAsset(this.menuConfig.config.art.help, { clearScreen: true }, () => {
|
||||||
this.menuConfig.config.art.help,
|
this.client.waitForKeyPress(() => {
|
||||||
{ clearScreen : true },
|
return this.displayBrowsePage(true, cb);
|
||||||
() => {
|
});
|
||||||
this.client.waitForKeyPress( () => {
|
});
|
||||||
return this.displayBrowsePage(true, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMovementKeyPress(keyName, cb) {
|
_handleMovementKeyPress(keyName, cb) {
|
||||||
const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc);
|
const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc);
|
||||||
if (!descView) {
|
if (!descView) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyName) {
|
switch (keyName) {
|
||||||
case 'down arrow' : descView.scrollDocumentUp(); break;
|
case 'down arrow':
|
||||||
case 'up arrow' : descView.scrollDocumentDown(); break;
|
descView.scrollDocumentUp();
|
||||||
case 'page up' : descView.keyPressPageUp(); break;
|
break;
|
||||||
case 'page down' : descView.keyPressPageDown(); break;
|
case 'up arrow':
|
||||||
|
descView.scrollDocumentDown();
|
||||||
|
break;
|
||||||
|
case 'page up':
|
||||||
|
descView.keyPressPageUp();
|
||||||
|
break;
|
||||||
|
case 'page down':
|
||||||
|
descView.keyPressPageDown();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu);
|
this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu);
|
||||||
@@ -490,28 +553,34 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function generateLinkIfNeeded(callback) {
|
function generateLinkIfNeeded(callback) {
|
||||||
|
if (self.currentFileEntry.webDlExpireTime < moment()) {
|
||||||
if(self.currentFileEntry.webDlExpireTime < moment()) {
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes');
|
const expireTime = moment().add(
|
||||||
|
Config().fileBase.web.expireMinutes,
|
||||||
|
'minutes'
|
||||||
|
);
|
||||||
|
|
||||||
FileAreaWeb.createAndServeTempDownload(
|
FileAreaWeb.createAndServeTempDownload(
|
||||||
self.client,
|
self.client,
|
||||||
self.currentFileEntry,
|
self.currentFileEntry,
|
||||||
{ expireTime : expireTime },
|
{ expireTime: expireTime },
|
||||||
(err, url) => {
|
(err, url) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.currentFileEntry.webDlExpireTime = expireTime;
|
self.currentFileEntry.webDlExpireTime = expireTime;
|
||||||
|
|
||||||
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
const webDlExpireTimeFormat =
|
||||||
|
self.menuConfig.config.webDlExpireTimeFormat ||
|
||||||
|
'YYYY-MMM-DD @ h:mm';
|
||||||
|
|
||||||
self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
|
self.currentFileEntry.entryInfo.webDlLink =
|
||||||
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
|
ansi.vtxHyperlink(self.client, url) + url;
|
||||||
|
self.currentFileEntry.entryInfo.webDlExpire =
|
||||||
|
expireTime.format(webDlExpireTimeFormat);
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
@@ -520,11 +589,12 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
function updateActiveViews(callback) {
|
function updateActiveViews(callback) {
|
||||||
self.updateCustomViewTextsWithFilter(
|
self.updateCustomViewTextsWithFilter(
|
||||||
'browse',
|
'browse',
|
||||||
MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo,
|
MciViewIds.browse.customRangeStart,
|
||||||
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
|
self.currentFileEntry.entryInfo,
|
||||||
|
{ filter: ['{webDlLink}', '{webDlExpire}'] }
|
||||||
);
|
);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -533,104 +603,118 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateQueueIndicator() {
|
updateQueueIndicator() {
|
||||||
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
|
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
|
||||||
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
|
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
|
||||||
|
|
||||||
this.currentFileEntry.entryInfo.isQueued = stringFormat(
|
this.currentFileEntry.entryInfo.isQueued = stringFormat(
|
||||||
this.dlQueue.isQueued(this.currentFileEntry) ?
|
this.dlQueue.isQueued(this.currentFileEntry)
|
||||||
isQueuedIndicator :
|
? isQueuedIndicator
|
||||||
isNotQueuedIndicator
|
: isNotQueuedIndicator
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updateCustomViewTextsWithFilter(
|
this.updateCustomViewTextsWithFilter(
|
||||||
'browse',
|
'browse',
|
||||||
MciViewIds.browse.customRangeStart,
|
MciViewIds.browse.customRangeStart,
|
||||||
this.currentFileEntry.entryInfo,
|
this.currentFileEntry.entryInfo,
|
||||||
{ filter : [ '{isQueued}' ] }
|
{ filter: ['{isQueued}'] }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheArchiveEntries(cb) {
|
cacheArchiveEntries(cb) {
|
||||||
// check cache
|
// check cache
|
||||||
if(this.currentFileEntry.archiveEntries) {
|
if (this.currentFileEntry.archiveEntries) {
|
||||||
return cb(null, 'cache');
|
return cb(null, 'cache');
|
||||||
}
|
}
|
||||||
|
|
||||||
const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag);
|
const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag);
|
||||||
if(!areaInfo) {
|
if (!areaInfo) {
|
||||||
return cb(Errors.Invalid('Invalid area tag'));
|
return cb(Errors.Invalid('Invalid area tag'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = this.currentFileEntry.filePath;
|
const filePath = this.currentFileEntry.filePath;
|
||||||
const archiveUtil = ArchiveUtil.getInstance();
|
const archiveUtil = ArchiveUtil.getInstance();
|
||||||
|
|
||||||
archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => {
|
archiveUtil.listEntries(
|
||||||
if(err) {
|
filePath,
|
||||||
return cb(err);
|
this.currentFileEntry.entryInfo.archiveType,
|
||||||
|
(err, entries) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign and add standard "text" member for itemFormat
|
||||||
|
this.currentFileEntry.archiveEntries = entries.map(e =>
|
||||||
|
Object.assign(e, { text: `${e.fileName} (${e.byteSize})` })
|
||||||
|
);
|
||||||
|
return cb(null, 're-cached');
|
||||||
}
|
}
|
||||||
|
);
|
||||||
// assign and add standard "text" member for itemFormat
|
|
||||||
this.currentFileEntry.archiveEntries = entries.map(e => Object.assign(e, { text : `${e.fileName} (${e.byteSize})` } ));
|
|
||||||
return cb(null, 're-cached');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFileListNoListing(text) {
|
setFileListNoListing(text) {
|
||||||
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
|
const fileListView = this.viewControllers.detailsFileList.getView(
|
||||||
if(fileListView) {
|
MciViewIds.detailsFileList.fileList
|
||||||
|
);
|
||||||
|
if (fileListView) {
|
||||||
fileListView.complexItems = false;
|
fileListView.complexItems = false;
|
||||||
fileListView.setItems( [ text ] );
|
fileListView.setItems([text]);
|
||||||
fileListView.redraw();
|
fileListView.redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
populateFileListing() {
|
populateFileListing() {
|
||||||
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
|
const fileListView = this.viewControllers.detailsFileList.getView(
|
||||||
|
MciViewIds.detailsFileList.fileList
|
||||||
|
);
|
||||||
|
|
||||||
if(this.currentFileEntry.entryInfo.archiveType) {
|
if (this.currentFileEntry.entryInfo.archiveType) {
|
||||||
this.cacheArchiveEntries( (err, cacheStatus) => {
|
this.cacheArchiveEntries((err, cacheStatus) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return this.setFileListNoListing('Failed to get file listing');
|
return this.setFileListNoListing('Failed to get file listing');
|
||||||
}
|
}
|
||||||
|
|
||||||
if('re-cached' === cacheStatus) {
|
if ('re-cached' === cacheStatus) {
|
||||||
fileListView.setItems(this.currentFileEntry.archiveEntries);
|
fileListView.setItems(this.currentFileEntry.archiveEntries);
|
||||||
fileListView.redraw();
|
fileListView.redraw();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const notAnArchiveFileName = stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } );
|
const notAnArchiveFileName = stringFormat(
|
||||||
|
this.menuConfig.config.notAnArchiveFormat || 'Not an archive',
|
||||||
|
{ fileName: this.currentFileEntry.fileName }
|
||||||
|
);
|
||||||
this.setFileListNoListing(notAnArchiveFileName);
|
this.setFileListNoListing(notAnArchiveFileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
displayDetailsSection(sectionName, clearArea, cb) {
|
displayDetailsSection(sectionName, clearArea, cb) {
|
||||||
const self = this;
|
const self = this;
|
||||||
const name = `details${_.upperFirst(sectionName)}`;
|
const name = `details${_.upperFirst(sectionName)}`;
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function detachPrevious(callback) {
|
function detachPrevious(callback) {
|
||||||
if(self.lastDetailsViewController) {
|
if (self.lastDetailsViewController) {
|
||||||
self.lastDetailsViewController.detachClientEvents();
|
self.lastDetailsViewController.detachClientEvents();
|
||||||
}
|
}
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function prepArtAndViewController(callback) {
|
function prepArtAndViewController(callback) {
|
||||||
|
|
||||||
function gotoTopPos() {
|
function gotoTopPos() {
|
||||||
self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1));
|
self.client.term.rawWrite(
|
||||||
|
ansi.goto(self.detailsInfoArea.top[0], 1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
gotoTopPos();
|
gotoTopPos();
|
||||||
|
|
||||||
if(clearArea) {
|
if (clearArea) {
|
||||||
self.client.term.rawWrite(ansi.reset());
|
self.client.term.rawWrite(ansi.reset());
|
||||||
|
|
||||||
let pos = self.detailsInfoArea.top[0];
|
let pos = self.detailsInfoArea.top[0];
|
||||||
const bottom = self.detailsInfoArea.bottom[0];
|
const bottom = self.detailsInfoArea.bottom[0];
|
||||||
|
|
||||||
while(pos++ <= bottom) {
|
while (pos++ <= bottom) {
|
||||||
self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
|
self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,47 +735,51 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
function populateViews(callback) {
|
function populateViews(callback) {
|
||||||
self.lastDetailsViewController = self.viewControllers[name];
|
self.lastDetailsViewController = self.viewControllers[name];
|
||||||
|
|
||||||
switch(sectionName) {
|
switch (sectionName) {
|
||||||
case 'nfo' :
|
case 'nfo':
|
||||||
{
|
{
|
||||||
const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo);
|
const nfoView = self.viewControllers.detailsNfo.getView(
|
||||||
if(!nfoView) {
|
MciViewIds.detailsNfo.nfo
|
||||||
|
);
|
||||||
|
if (!nfoView) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isAnsi(self.currentFileEntry.entryInfo.descLong)) {
|
if (isAnsi(self.currentFileEntry.entryInfo.descLong)) {
|
||||||
nfoView.setAnsi(
|
nfoView.setAnsi(
|
||||||
self.currentFileEntry.entryInfo.descLong,
|
self.currentFileEntry.entryInfo.descLong,
|
||||||
{
|
{
|
||||||
prepped : false,
|
prepped: false,
|
||||||
forceLineTerm : true,
|
forceLineTerm: true,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
nfoView.setText(self.currentFileEntry.entryInfo.descLong);
|
nfoView.setText(
|
||||||
|
self.currentFileEntry.entryInfo.descLong
|
||||||
|
);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fileList' :
|
case 'fileList':
|
||||||
self.populateFileListing();
|
self.populateFileListing();
|
||||||
return callback(null);
|
return callback(null);
|
||||||
|
|
||||||
default :
|
default:
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function setLabels(callback) {
|
function setLabels(callback) {
|
||||||
self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
|
self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -699,11 +787,15 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadFileIds(force, cb) {
|
loadFileIds(force, cb) {
|
||||||
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) {
|
if (
|
||||||
this.fileListPosition = 0;
|
force ||
|
||||||
|
_.isUndefined(this.fileList) ||
|
||||||
|
_.isUndefined(this.fileListPosition)
|
||||||
|
) {
|
||||||
|
this.fileListPosition = 0;
|
||||||
|
|
||||||
const filterCriteria = Object.assign({}, this.filterCriteria);
|
const filterCriteria = Object.assign({}, this.filterCriteria);
|
||||||
if(!filterCriteria.areaTag) {
|
if (!filterCriteria.areaTag) {
|
||||||
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client);
|
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,30 +2,30 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const FileDb = require('./database.js').dbs.file;
|
const FileDb = require('./database.js').dbs.file;
|
||||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const getServer = require('./listening_server.js').getServer;
|
const getServer = require('./listening_server.js').getServer;
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
|
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
|
||||||
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const SysProps = require('./system_menu_method.js');
|
const SysProps = require('./system_menu_method.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const hashids = require('hashids/cjs');
|
const hashids = require('hashids/cjs');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const mimeTypes = require('mime-types');
|
const mimeTypes = require('mime-types');
|
||||||
const yazl = require('yazl');
|
const yazl = require('yazl');
|
||||||
|
|
||||||
function notEnabledError() {
|
function notEnabledError() {
|
||||||
return Errors.General('Web server is not enabled', ErrNotEnabled);
|
return Errors.General('Web server is not enabled', ErrNotEnabled);
|
||||||
@@ -33,8 +33,8 @@ function notEnabledError() {
|
|||||||
|
|
||||||
class FileAreaWebAccess {
|
class FileAreaWebAccess {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.hashids = new hashids(Config().general.boardName);
|
this.hashids = new hashids(Config().general.boardName);
|
||||||
this.expireTimers = {}; // hashId->timer
|
this.expireTimers = {}; // hashId->timer
|
||||||
}
|
}
|
||||||
|
|
||||||
startup(cb) {
|
startup(cb) {
|
||||||
@@ -47,21 +47,27 @@ class FileAreaWebAccess {
|
|||||||
},
|
},
|
||||||
function addWebRoute(callback) {
|
function addWebRoute(callback) {
|
||||||
self.webServer = getServer(webServerPackageName);
|
self.webServer = getServer(webServerPackageName);
|
||||||
if(!self.webServer) {
|
if (!self.webServer) {
|
||||||
return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`));
|
return callback(
|
||||||
|
Errors.DoesNotExist(
|
||||||
|
`Server with package name "${webServerPackageName}" does not exist`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(self.isEnabled()) {
|
if (self.isEnabled()) {
|
||||||
const routeAdded = self.webServer.instance.addRoute({
|
const routeAdded = self.webServer.instance.addRoute({
|
||||||
method : 'GET',
|
method: 'GET',
|
||||||
path : Config().fileBase.web.routePath,
|
path: Config().fileBase.web.routePath,
|
||||||
handler : self.routeWebRequest.bind(self),
|
handler: self.routeWebRequest.bind(self),
|
||||||
});
|
});
|
||||||
return callback(routeAdded ? null : Errors.General('Failed adding route'));
|
return callback(
|
||||||
|
routeAdded ? null : Errors.General('Failed adding route')
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return callback(null); // not enabled, but no error
|
return callback(null); // not enabled, but no error
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -79,8 +85,8 @@ class FileAreaWebAccess {
|
|||||||
|
|
||||||
static getHashIdTypes() {
|
static getHashIdTypes() {
|
||||||
return {
|
return {
|
||||||
SingleFile : 0,
|
SingleFile: 0,
|
||||||
BatchArchive : 1,
|
BatchArchive: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +98,7 @@ class FileAreaWebAccess {
|
|||||||
`SELECT hash_id, expire_timestamp
|
`SELECT hash_id, expire_timestamp
|
||||||
FROM file_web_serve;`,
|
FROM file_web_serve;`,
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if(row) {
|
if (row) {
|
||||||
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
|
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -109,29 +115,28 @@ class FileAreaWebAccess {
|
|||||||
FileDb.run(
|
FileDb.run(
|
||||||
`DELETE FROM file_web_serve
|
`DELETE FROM file_web_serve
|
||||||
WHERE hash_id = ?;`,
|
WHERE hash_id = ?;`,
|
||||||
[ hashId ]
|
[hashId]
|
||||||
);
|
);
|
||||||
|
|
||||||
delete this.expireTimers[hashId];
|
delete this.expireTimers[hashId];
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleExpire(hashId, expireTime) {
|
scheduleExpire(hashId, expireTime) {
|
||||||
|
|
||||||
// remove any previous entry for this hashId
|
// remove any previous entry for this hashId
|
||||||
const previous = this.expireTimers[hashId];
|
const previous = this.expireTimers[hashId];
|
||||||
if(previous) {
|
if (previous) {
|
||||||
clearTimeout(previous);
|
clearTimeout(previous);
|
||||||
delete this.expireTimers[hashId];
|
delete this.expireTimers[hashId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMs = expireTime.diff(moment());
|
const timeoutMs = expireTime.diff(moment());
|
||||||
|
|
||||||
if(timeoutMs <= 0) {
|
if (timeoutMs <= 0) {
|
||||||
setImmediate( () => {
|
setImmediate(() => {
|
||||||
this.removeEntry(hashId);
|
this.removeEntry(hashId);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.expireTimers[hashId] = setTimeout( () => {
|
this.expireTimers[hashId] = setTimeout(() => {
|
||||||
this.removeEntry(hashId);
|
this.removeEntry(hashId);
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
}
|
}
|
||||||
@@ -142,27 +147,32 @@ class FileAreaWebAccess {
|
|||||||
`SELECT expire_timestamp FROM
|
`SELECT expire_timestamp FROM
|
||||||
file_web_serve
|
file_web_serve
|
||||||
WHERE hash_id = ?`,
|
WHERE hash_id = ?`,
|
||||||
[ hashId ],
|
[hashId],
|
||||||
(err, result) => {
|
(err, result) => {
|
||||||
if(err || !result) {
|
if (err || !result) {
|
||||||
return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID'));
|
return cb(
|
||||||
|
err ? err : Errors.DoesNotExist('Invalid or missing hash ID')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const decoded = this.hashids.decode(hashId);
|
const decoded = this.hashids.decode(hashId);
|
||||||
|
|
||||||
// decode() should provide an array of [ userId, hashIdType, id, ... ]
|
// decode() should provide an array of [ userId, hashIdType, id, ... ]
|
||||||
if(!Array.isArray(decoded) || decoded.length < 3) {
|
if (!Array.isArray(decoded) || decoded.length < 3) {
|
||||||
return cb(Errors.Invalid('Invalid or unknown hash ID'));
|
return cb(Errors.Invalid('Invalid or unknown hash ID'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const servedItem = {
|
const servedItem = {
|
||||||
hashId : hashId,
|
hashId: hashId,
|
||||||
userId : decoded[0],
|
userId: decoded[0],
|
||||||
hashIdType : decoded[1],
|
hashIdType: decoded[1],
|
||||||
expireTimestamp : moment(result.expire_timestamp),
|
expireTimestamp: moment(result.expire_timestamp),
|
||||||
};
|
};
|
||||||
|
|
||||||
if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) {
|
if (
|
||||||
|
FileAreaWebAccess.getHashIdTypes().SingleFile ===
|
||||||
|
servedItem.hashIdType
|
||||||
|
) {
|
||||||
servedItem.fileIds = decoded.slice(2);
|
servedItem.fileIds = decoded.slice(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,11 +182,17 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSingleFileHashId(client, fileEntry) {
|
getSingleFileHashId(client, fileEntry) {
|
||||||
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] );
|
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [
|
||||||
|
fileEntry.fileId,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBatchArchiveHashId(client, batchId) {
|
getBatchArchiveHashId(client, batchId) {
|
||||||
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId);
|
return this.getHashId(
|
||||||
|
client,
|
||||||
|
FileAreaWebAccess.getHashIdTypes().BatchArchive,
|
||||||
|
batchId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHashId(client, hashIdType, identifier) {
|
getHashId(client, hashIdType, identifier) {
|
||||||
@@ -194,13 +210,13 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||||
if(!this.isEnabled()) {
|
if (!this.isEnabled()) {
|
||||||
return cb(notEnabledError());
|
return cb(notEnabledError());
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashId = this.getSingleFileHashId(client, fileEntry);
|
const hashId = this.getSingleFileHashId(client, fileEntry);
|
||||||
this.loadServedHashId(hashId, (err, servedItem) => {
|
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +231,9 @@ class FileAreaWebAccess {
|
|||||||
dbOrTrans.run(
|
dbOrTrans.run(
|
||||||
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
|
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
|
||||||
VALUES (?, ?);`,
|
VALUES (?, ?);`,
|
||||||
[ hashId, getISOTimestampString(expireTime) ],
|
[hashId, getISOTimestampString(expireTime)],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,13 +245,13 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createAndServeTempDownload(client, fileEntry, options, cb) {
|
createAndServeTempDownload(client, fileEntry, options, cb) {
|
||||||
if(!this.isEnabled()) {
|
if (!this.isEnabled()) {
|
||||||
return cb(notEnabledError());
|
return cb(notEnabledError());
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashId = this.getSingleFileHashId(client, fileEntry);
|
const hashId = this.getSingleFileHashId(client, fileEntry);
|
||||||
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
|
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
|
||||||
options.expireTime = options.expireTime || moment().add(2, 'days');
|
options.expireTime = options.expireTime || moment().add(2, 'days');
|
||||||
|
|
||||||
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
|
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
|
||||||
return cb(err, url);
|
return cb(err, url);
|
||||||
@@ -243,41 +259,45 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createAndServeTempBatchDownload(client, fileEntries, options, cb) {
|
createAndServeTempBatchDownload(client, fileEntries, options, cb) {
|
||||||
if(!this.isEnabled()) {
|
if (!this.isEnabled()) {
|
||||||
return cb(notEnabledError());
|
return cb(notEnabledError());
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchId = moment().utc().unix();
|
const batchId = moment().utc().unix();
|
||||||
const hashId = this.getBatchArchiveHashId(client, batchId);
|
const hashId = this.getBatchArchiveHashId(client, batchId);
|
||||||
const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
|
const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
|
||||||
options.expireTime = options.expireTime || moment().add(2, 'days');
|
options.expireTime = options.expireTime || moment().add(2, 'days');
|
||||||
|
|
||||||
FileDb.beginTransaction( (err, trans) => {
|
FileDb.beginTransaction((err, trans) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
|
this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return trans.rollback( () => {
|
return trans.rollback(() => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async.eachSeries(fileEntries, (entry, nextEntry) => {
|
async.eachSeries(
|
||||||
trans.run(
|
fileEntries,
|
||||||
`INSERT INTO file_web_serve_batch (hash_id, file_id)
|
(entry, nextEntry) => {
|
||||||
|
trans.run(
|
||||||
|
`INSERT INTO file_web_serve_batch (hash_id, file_id)
|
||||||
VALUES (?, ?);`,
|
VALUES (?, ?);`,
|
||||||
[ hashId, entry.fileId ],
|
[hashId, entry.fileId],
|
||||||
err => {
|
err => {
|
||||||
return nextEntry(err);
|
return nextEntry(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, err => {
|
},
|
||||||
trans[err ? 'rollback' : 'commit']( () => {
|
err => {
|
||||||
return cb(err, url);
|
trans[err ? 'rollback' : 'commit'](() => {
|
||||||
});
|
return cb(err, url);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -289,47 +309,46 @@ class FileAreaWebAccess {
|
|||||||
routeWebRequest(req, resp) {
|
routeWebRequest(req, resp) {
|
||||||
const hashId = paths.basename(req.url);
|
const hashId = paths.basename(req.url);
|
||||||
|
|
||||||
Log.debug( { hashId : hashId, url : req.url }, 'File area web request');
|
Log.debug({ hashId: hashId, url: req.url }, 'File area web request');
|
||||||
|
|
||||||
this.loadServedHashId(hashId, (err, servedItem) => {
|
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||||
|
if (err) {
|
||||||
if(err) {
|
|
||||||
return this.fileNotFound(resp);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
|
const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
|
||||||
switch(servedItem.hashIdType) {
|
switch (servedItem.hashIdType) {
|
||||||
case hashIdTypes.SingleFile :
|
case hashIdTypes.SingleFile:
|
||||||
return this.routeWebRequestForSingleFile(servedItem, req, resp);
|
return this.routeWebRequestForSingleFile(servedItem, req, resp);
|
||||||
|
|
||||||
case hashIdTypes.BatchArchive :
|
case hashIdTypes.BatchArchive:
|
||||||
return this.routeWebRequestForBatchArchive(servedItem, req, resp);
|
return this.routeWebRequestForBatchArchive(servedItem, req, resp);
|
||||||
|
|
||||||
default :
|
default:
|
||||||
return this.fileNotFound(resp);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
routeWebRequestForSingleFile(servedItem, req, resp) {
|
routeWebRequestForSingleFile(servedItem, req, resp) {
|
||||||
Log.debug( { servedItem : servedItem }, 'Single file web request');
|
Log.debug({ servedItem: servedItem }, 'Single file web request');
|
||||||
|
|
||||||
const fileEntry = new FileEntry();
|
const fileEntry = new FileEntry();
|
||||||
|
|
||||||
servedItem.fileId = servedItem.fileIds[0];
|
servedItem.fileId = servedItem.fileIds[0];
|
||||||
|
|
||||||
fileEntry.load(servedItem.fileId, err => {
|
fileEntry.load(servedItem.fileId, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return this.fileNotFound(resp);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = fileEntry.filePath;
|
const filePath = fileEntry.filePath;
|
||||||
if(!filePath) {
|
if (!filePath) {
|
||||||
return this.fileNotFound(resp);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.stat(filePath, (err, stats) => {
|
fs.stat(filePath, (err, stats) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return this.fileNotFound(resp);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,13 +359,18 @@ class FileAreaWebAccess {
|
|||||||
|
|
||||||
resp.on('finish', () => {
|
resp.on('finish', () => {
|
||||||
// transfer completed fully
|
// transfer completed fully
|
||||||
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]);
|
this.updateDownloadStatsForUserIdAndSystem(
|
||||||
|
servedItem.userId,
|
||||||
|
stats.size,
|
||||||
|
[fileEntry]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
|
'Content-Type':
|
||||||
'Content-Length' : stats.size,
|
mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
|
||||||
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
|
'Content-Length': stats.size,
|
||||||
|
'Content-Disposition': `attachment; filename="${fileEntry.fileName}"`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const readStream = fs.createReadStream(filePath);
|
const readStream = fs.createReadStream(filePath);
|
||||||
@@ -357,7 +381,7 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routeWebRequestForBatchArchive(servedItem, req, resp) {
|
routeWebRequestForBatchArchive(servedItem, req, resp) {
|
||||||
Log.debug( { servedItem : servedItem }, 'Batch file web request');
|
Log.debug({ servedItem: servedItem }, 'Batch file web request');
|
||||||
|
|
||||||
//
|
//
|
||||||
// We are going to build an on-the-fly zip file stream of 1:n
|
// We are going to build an on-the-fly zip file stream of 1:n
|
||||||
@@ -374,53 +398,80 @@ class FileAreaWebAccess {
|
|||||||
`SELECT file_id
|
`SELECT file_id
|
||||||
FROM file_web_serve_batch
|
FROM file_web_serve_batch
|
||||||
WHERE hash_id = ?;`,
|
WHERE hash_id = ?;`,
|
||||||
[ servedItem.hashId ],
|
[servedItem.hashId],
|
||||||
(err, fileIdRows) => {
|
(err, fileIdRows) => {
|
||||||
if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) {
|
if (
|
||||||
return callback(Errors.DoesNotExist('Could not get file IDs for batch'));
|
err ||
|
||||||
|
!Array.isArray(fileIdRows) ||
|
||||||
|
0 === fileIdRows.length
|
||||||
|
) {
|
||||||
|
return callback(
|
||||||
|
Errors.DoesNotExist(
|
||||||
|
'Could not get file IDs for batch'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null, fileIdRows.map(r => r.file_id));
|
return callback(
|
||||||
|
null,
|
||||||
|
fileIdRows.map(r => r.file_id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function loadFileEntries(fileIds, callback) {
|
function loadFileEntries(fileIds, callback) {
|
||||||
async.map(fileIds, (fileId, nextFileId) => {
|
async.map(
|
||||||
const fileEntry = new FileEntry();
|
fileIds,
|
||||||
fileEntry.load(fileId, err => {
|
(fileId, nextFileId) => {
|
||||||
return nextFileId(err, fileEntry);
|
const fileEntry = new FileEntry();
|
||||||
});
|
fileEntry.load(fileId, err => {
|
||||||
}, (err, fileEntries) => {
|
return nextFileId(err, fileEntry);
|
||||||
if(err) {
|
});
|
||||||
return callback(Errors.DoesNotExist('Could not load file IDs for batch'));
|
},
|
||||||
}
|
(err, fileEntries) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(
|
||||||
|
Errors.DoesNotExist(
|
||||||
|
'Could not load file IDs for batch'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return callback(null, fileEntries);
|
return callback(null, fileEntries);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function createAndServeStream(fileEntries, callback) {
|
function createAndServeStream(fileEntries, callback) {
|
||||||
const filePaths = fileEntries.map(fe => fe.filePath);
|
const filePaths = fileEntries.map(fe => fe.filePath);
|
||||||
Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request');
|
Log.trace(
|
||||||
|
{ filePaths: filePaths },
|
||||||
|
'Creating zip archive for batch web request'
|
||||||
|
);
|
||||||
|
|
||||||
const zipFile = new yazl.ZipFile();
|
const zipFile = new yazl.ZipFile();
|
||||||
|
|
||||||
zipFile.on('error', err => {
|
zipFile.on('error', err => {
|
||||||
Log.warn( { error : err.message }, 'Error adding file to batch web request archive');
|
Log.warn(
|
||||||
|
{ error: err.message },
|
||||||
|
'Error adding file to batch web request archive'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
filePaths.forEach(fp => {
|
filePaths.forEach(fp => {
|
||||||
zipFile.addFile(
|
zipFile.addFile(
|
||||||
fp, // path to physical file
|
fp, // path to physical file
|
||||||
paths.basename(fp), // filename/path *stored in archive*
|
paths.basename(fp), // filename/path *stored in archive*
|
||||||
{
|
{
|
||||||
compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us.
|
compress: false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us.
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
zipFile.end( finalZipSize => {
|
zipFile.end(finalZipSize => {
|
||||||
if(-1 === finalZipSize) {
|
if (-1 === finalZipSize) {
|
||||||
return callback(Errors.UnexpectedState('Unable to acquire final zip size'));
|
return callback(
|
||||||
|
Errors.UnexpectedState('Unable to acquire final zip size')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.on('close', () => {
|
resp.on('close', () => {
|
||||||
@@ -430,24 +481,30 @@ class FileAreaWebAccess {
|
|||||||
|
|
||||||
resp.on('finish', () => {
|
resp.on('finish', () => {
|
||||||
// transfer completed fully
|
// transfer completed fully
|
||||||
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries);
|
self.updateDownloadStatsForUserIdAndSystem(
|
||||||
|
servedItem.userId,
|
||||||
|
finalZipSize,
|
||||||
|
fileEntries
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const batchFileName = `batch_${servedItem.hashId}.zip`;
|
const batchFileName = `batch_${servedItem.hashId}.zip`;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'),
|
'Content-Type':
|
||||||
'Content-Length' : finalZipSize,
|
mimeTypes.contentType(batchFileName) ||
|
||||||
'Content-Disposition' : `attachment; filename="${batchFileName}"`,
|
mimeTypes.contentType('.bin'),
|
||||||
|
'Content-Length': finalZipSize,
|
||||||
|
'Content-Disposition': `attachment; filename="${batchFileName}"`,
|
||||||
};
|
};
|
||||||
|
|
||||||
resp.writeHead(200, headers);
|
resp.writeHead(200, headers);
|
||||||
return zipFile.outputStream.pipe(resp);
|
return zipFile.outputStream.pipe(resp);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
// :TODO: Log me!
|
// :TODO: Log me!
|
||||||
return this.fileNotFound(resp);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
@@ -458,25 +515,24 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
|
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
|
||||||
async.waterfall(
|
async.waterfall([
|
||||||
[
|
function fetchActiveUser(callback) {
|
||||||
function fetchActiveUser(callback) {
|
const clientForUserId = getConnectionByUserId(userId);
|
||||||
const clientForUserId = getConnectionByUserId(userId);
|
if (clientForUserId) {
|
||||||
if(clientForUserId) {
|
return callback(null, clientForUserId.user);
|
||||||
return callback(null, clientForUserId.user);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// not online now - look 'em up
|
// not online now - look 'em up
|
||||||
User.getUser(userId, (err, assocUser) => {
|
User.getUser(userId, (err, assocUser) => {
|
||||||
return callback(err, assocUser);
|
return callback(err, assocUser);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function updateStats(user, callback) {
|
function updateStats(user, callback) {
|
||||||
StatLog.incrementUserStat(user, UserProps.FileDlTotalCount, 1);
|
StatLog.incrementUserStat(user, UserProps.FileDlTotalCount, 1);
|
||||||
StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes);
|
StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes);
|
||||||
|
|
||||||
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
|
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
|
||||||
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
|
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
|
||||||
|
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1);
|
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1);
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, dlBytes);
|
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, dlBytes);
|
||||||
@@ -498,4 +554,4 @@ class FileAreaWebAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new FileAreaWebAccess();
|
module.exports = new FileAreaWebAccess();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,22 +2,22 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
|
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Area Selector',
|
name: 'File Area Selector',
|
||||||
desc : 'Select from available file areas',
|
desc: 'Select from available file areas',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
areaList : 1,
|
areaList: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileAreaSelectModule extends MenuModule {
|
exports.getModule = class FileAreaSelectModule extends MenuModule {
|
||||||
@@ -25,26 +25,31 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
selectArea : (formData, extraArgs, cb) => {
|
selectArea: (formData, extraArgs, cb) => {
|
||||||
const filterCriteria = {
|
const filterCriteria = {
|
||||||
areaTag : formData.value.areaTag,
|
areaTag: formData.value.areaTag,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuOpts = {
|
const menuOpts = {
|
||||||
extraArgs : {
|
extraArgs: {
|
||||||
filterCriteria : filterCriteria,
|
filterCriteria: filterCriteria,
|
||||||
},
|
},
|
||||||
menuFlags : [ 'popParent', 'mergeFlags' ],
|
menuFlags: ['popParent', 'mergeFlags'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
|
return this.gotoMenu(
|
||||||
}
|
this.menuConfig.config.fileBaseListEntriesMenu ||
|
||||||
|
'fileBaseListEntries',
|
||||||
|
menuOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +58,9 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
|||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function mergeAreaStats(callback) {
|
function mergeAreaStats(callback) {
|
||||||
const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || { areas : {} };
|
const areaStats = StatLog.getSystemStat(
|
||||||
|
SysProps.FileBaseAreaStats
|
||||||
|
) || { areas: {} };
|
||||||
|
|
||||||
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
|
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
|
||||||
const availAreas = getSortedAvailableFileAreas(self.client);
|
const availAreas = getSortedAvailableFileAreas(self.client);
|
||||||
@@ -66,18 +73,30 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
|||||||
return callback(null, availAreas);
|
return callback(null, availAreas);
|
||||||
},
|
},
|
||||||
function prepView(availAreas, callback) {
|
function prepView(availAreas, callback) {
|
||||||
self.prepViewController('allViews', 0, mciData.menu, (err, vc) => {
|
self.prepViewController(
|
||||||
if(err) {
|
'allViews',
|
||||||
return callback(err);
|
0,
|
||||||
|
mciData.menu,
|
||||||
|
(err, vc) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaListView = vc.getView(MciViewIds.areaList);
|
||||||
|
areaListView.setItems(
|
||||||
|
availAreas.map(area =>
|
||||||
|
Object.assign(area, {
|
||||||
|
text: area.name,
|
||||||
|
data: area.areaTag,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
areaListView.redraw();
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const areaListView = vc.getView(MciViewIds.areaList);
|
},
|
||||||
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
|
|
||||||
areaListView.redraw();
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|||||||
@@ -2,91 +2,101 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
const theme = require('./theme.js');
|
const theme = require('./theme.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const FileAreaWeb = require('./file_area_web.js');
|
const FileAreaWeb = require('./file_area_web.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Base Download Queue Manager',
|
name: 'File Base Download Queue Manager',
|
||||||
desc : 'Module for interacting with download queue/batch',
|
desc: 'Module for interacting with download queue/batch',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
queueManager : 0,
|
queueManager: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
queueManager : {
|
queueManager: {
|
||||||
queue : 1,
|
queue: 1,
|
||||||
navMenu : 2,
|
navMenu: 2,
|
||||||
|
|
||||||
customRangeStart : 10,
|
customRangeStart: 10,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.dlQueue = new DownloadQueue(this.client);
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
if (_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
downloadAll : (formData, extraArgs, cb) => {
|
downloadAll: (formData, extraArgs, cb) => {
|
||||||
const modOpts = {
|
const modOpts = {
|
||||||
extraArgs : {
|
extraArgs: {
|
||||||
sendQueue : this.dlQueue.items,
|
sendQueue: this.dlQueue.items,
|
||||||
direction : 'send',
|
direction: 'send',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.fileTransferProtocolSelection ||
|
||||||
|
'fileTransferProtocolSelection',
|
||||||
|
modOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
},
|
},
|
||||||
removeItem : (formData, extraArgs, cb) => {
|
removeItem: (formData, extraArgs, cb) => {
|
||||||
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
||||||
if(!selectedItem) {
|
if (!selectedItem) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dlQueue.removeItems(selectedItem.fileId);
|
this.dlQueue.removeItems(selectedItem.fileId);
|
||||||
|
|
||||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||||
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
|
return this.removeItemsFromDownloadQueueView(
|
||||||
|
formData.value.queueItem,
|
||||||
|
cb
|
||||||
|
);
|
||||||
},
|
},
|
||||||
clearQueue : (formData, extraArgs, cb) => {
|
clearQueue: (formData, extraArgs, cb) => {
|
||||||
this.dlQueue.clear();
|
this.dlQueue.clear();
|
||||||
|
|
||||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||||
return this.removeItemsFromDownloadQueueView('all', cb);
|
return this.removeItemsFromDownloadQueueView('all', cb);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
if(0 === this.dlQueue.items.length) {
|
if (0 === this.dlQueue.items.length) {
|
||||||
if(this.sendFileIds) {
|
if (this.sendFileIds) {
|
||||||
// we've finished everything up - just fall back
|
// we've finished everything up - just fall back
|
||||||
return this.prevMenu();
|
return this.prevMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simply an empty D/L queue: Present a specialized "empty queue" page
|
// Simply an empty D/L queue: Present a specialized "empty queue" page
|
||||||
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.emptyQueueMenu ||
|
||||||
|
'fileBaseDownloadManagerEmptyQueue'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -98,7 +108,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||||||
},
|
},
|
||||||
function display(callback) {
|
function display(callback) {
|
||||||
return self.displayQueueManagerPage(false, callback);
|
return self.displayQueueManagerPage(false, callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
return self.finishedLoading();
|
return self.finishedLoading();
|
||||||
@@ -107,12 +117,14 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
||||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
const queueView = this.viewControllers.queueManager.getView(
|
||||||
if(!queueView) {
|
MciViewIds.queueManager.queue
|
||||||
|
);
|
||||||
|
if (!queueView) {
|
||||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if('all' === itemIndex) {
|
if ('all' === itemIndex) {
|
||||||
queueView.setItems([]);
|
queueView.setItems([]);
|
||||||
queueView.setFocusItems([]);
|
queueView.setFocusItems([]);
|
||||||
} else {
|
} else {
|
||||||
@@ -124,28 +136,40 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayWebDownloadLinkForFileEntry(fileEntry) {
|
displayWebDownloadLinkForFileEntry(fileEntry) {
|
||||||
FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => {
|
FileAreaWeb.getExistingTempDownloadServeItem(
|
||||||
if(serveItem && serveItem.url) {
|
this.client,
|
||||||
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
fileEntry,
|
||||||
|
(err, serveItem) => {
|
||||||
|
if (serveItem && serveItem.url) {
|
||||||
|
const webDlExpireTimeFormat =
|
||||||
|
this.menuConfig.config.webDlExpireTimeFormat ||
|
||||||
|
'YYYY-MMM-DD @ h:mm';
|
||||||
|
|
||||||
fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
fileEntry.webDlLink =
|
||||||
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
||||||
} else {
|
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(
|
||||||
fileEntry.webDlLink = '';
|
webDlExpireTimeFormat
|
||||||
fileEntry.webDlExpire = '';
|
);
|
||||||
|
} else {
|
||||||
|
fileEntry.webDlLink = '';
|
||||||
|
fileEntry.webDlExpire = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'queueManager',
|
||||||
|
MciViewIds.queueManager.customRangeStart,
|
||||||
|
fileEntry,
|
||||||
|
{ filter: ['{webDlLink}', '{webDlExpire}'] }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
this.updateCustomViewTextsWithFilter(
|
|
||||||
'queueManager',
|
|
||||||
MciViewIds.queueManager.customRangeStart, fileEntry,
|
|
||||||
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadQueueView(cb) {
|
updateDownloadQueueView(cb) {
|
||||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
const queueView = this.viewControllers.queueManager.getView(
|
||||||
if(!queueView) {
|
MciViewIds.queueManager.queue
|
||||||
|
);
|
||||||
|
if (!queueView) {
|
||||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,10 +200,10 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||||||
},
|
},
|
||||||
function populateViews(callback) {
|
function populateViews(callback) {
|
||||||
return self.updateDownloadQueueView(callback);
|
return self.updateDownloadQueueView(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { v4 : UUIDv4 } = require('uuid');
|
const { v4: UUIDv4 } = require('uuid');
|
||||||
|
|
||||||
module.exports = class FileBaseFilters {
|
module.exports = class FileBaseFilters {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
@@ -15,7 +15,7 @@ module.exports = class FileBaseFilters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get OrderByValues() {
|
static get OrderByValues() {
|
||||||
return [ 'descending', 'ascending' ];
|
return ['descending', 'ascending'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get SortByValues() {
|
static get SortByValues() {
|
||||||
@@ -32,7 +32,7 @@ module.exports = class FileBaseFilters {
|
|||||||
|
|
||||||
toArray() {
|
toArray() {
|
||||||
return _.map(this.filters, (filter, uuid) => {
|
return _.map(this.filters, (filter, uuid) => {
|
||||||
return Object.assign( { uuid : uuid }, filter );
|
return Object.assign({ uuid: uuid }, filter);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ module.exports = class FileBaseFilters {
|
|||||||
|
|
||||||
replace(filterUuid, filterInfo) {
|
replace(filterUuid, filterInfo) {
|
||||||
const filter = this.get(filterUuid);
|
const filter = this.get(filterUuid);
|
||||||
if(!filter) {
|
if (!filter) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,22 +68,25 @@ module.exports = class FileBaseFilters {
|
|||||||
load() {
|
load() {
|
||||||
let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters];
|
let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters];
|
||||||
let defaulted;
|
let defaulted;
|
||||||
if(!filtersProperty) {
|
if (!filtersProperty) {
|
||||||
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
|
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
|
||||||
defaulted = true;
|
defaulted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.filters = JSON.parse(filtersProperty);
|
this.filters = JSON.parse(filtersProperty);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
|
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
|
||||||
defaulted = true;
|
defaulted = true;
|
||||||
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
|
this.client.log.error(
|
||||||
|
{ error: e.message, property: filtersProperty },
|
||||||
|
'Failed parsing file base filters property'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(defaulted) {
|
if (defaulted) {
|
||||||
this.persist( err => {
|
this.persist(err => {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
const defaultActiveUuid = this.toArray()[0].uuid;
|
const defaultActiveUuid = this.toArray()[0].uuid;
|
||||||
this.setActive(defaultActiveUuid);
|
this.setActive(defaultActiveUuid);
|
||||||
}
|
}
|
||||||
@@ -92,19 +95,29 @@ module.exports = class FileBaseFilters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
persist(cb) {
|
persist(cb) {
|
||||||
return this.client.user.persistProperty(UserProps.FileBaseFilters, JSON.stringify(this.filters), cb);
|
return this.client.user.persistProperty(
|
||||||
|
UserProps.FileBaseFilters,
|
||||||
|
JSON.stringify(this.filters),
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanTags(tags) {
|
cleanTags(tags) {
|
||||||
return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim();
|
return tags
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/,?\s+|,/g, ' ')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
setActive(filterUuid) {
|
setActive(filterUuid) {
|
||||||
const activeFilter = this.get(filterUuid);
|
const activeFilter = this.get(filterUuid);
|
||||||
|
|
||||||
if(activeFilter) {
|
if (activeFilter) {
|
||||||
this.activeFilter = activeFilter;
|
this.activeFilter = activeFilter;
|
||||||
this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid);
|
this.client.user.persistProperty(
|
||||||
|
UserProps.FileBaseFilterActiveUuid,
|
||||||
|
filterUuid
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,41 +125,43 @@ module.exports = class FileBaseFilters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getBuiltInSystemFilters() {
|
static getBuiltInSystemFilters() {
|
||||||
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
|
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
[ U_LATEST ] : {
|
[U_LATEST]: {
|
||||||
name : 'By Date Added',
|
name: 'By Date Added',
|
||||||
areaTag : '', // all
|
areaTag: '', // all
|
||||||
terms : '', // *
|
terms: '', // *
|
||||||
tags : '', // *
|
tags: '', // *
|
||||||
order : 'descending',
|
order: 'descending',
|
||||||
sort : 'upload_timestamp',
|
sort: 'upload_timestamp',
|
||||||
uuid : U_LATEST,
|
uuid: U_LATEST,
|
||||||
system : true,
|
system: true,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getActiveFilter(client) {
|
static getActiveFilter(client) {
|
||||||
return new FileBaseFilters(client).get(client.user.properties[UserProps.FileBaseFilterActiveUuid]);
|
return new FileBaseFilters(client).get(
|
||||||
|
client.user.properties[UserProps.FileBaseFilterActiveUuid]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getFileBaseLastViewedFileIdByUser(user) {
|
static getFileBaseLastViewedFileIdByUser(user) {
|
||||||
return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0));
|
return parseInt(user.properties[UserProps.FileBaseLastViewedId] || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
|
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
|
||||||
if(!cb && _.isFunction(allowOlder)) {
|
if (!cb && _.isFunction(allowOlder)) {
|
||||||
cb = allowOlder;
|
cb = allowOlder;
|
||||||
allowOlder = false;
|
allowOlder = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
|
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
|
||||||
if(!allowOlder && fileId < current) {
|
if (!allowOlder && fileId < current) {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
cb(null);
|
cb(null);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,235 +2,283 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const FileArea = require('./file_base_area.js');
|
const FileArea = require('./file_base_area.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const {
|
const { splitTextAtTerms, isAnsi } = require('./string_util.js');
|
||||||
splitTextAtTerms,
|
const AnsiPrep = require('./ansi_prep.js');
|
||||||
isAnsi,
|
const Log = require('./logger.js').log;
|
||||||
} = require('./string_util.js');
|
|
||||||
const AnsiPrep = require('./ansi_prep.js');
|
|
||||||
const Log = require('./logger.js').log;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.exportFileList = exportFileList;
|
exports.exportFileList = exportFileList;
|
||||||
exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent;
|
exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent;
|
||||||
|
|
||||||
function exportFileList(filterCriteria, options, cb) {
|
function exportFileList(filterCriteria, options, cb) {
|
||||||
options.templateEncoding = options.templateEncoding || 'utf8';
|
options.templateEncoding = options.templateEncoding || 'utf8';
|
||||||
options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc';
|
options.entryTemplate =
|
||||||
options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
|
options.entryTemplate || 'descript_ion_export_entry_template.asc';
|
||||||
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
|
options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
|
||||||
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
|
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
|
||||||
|
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
|
||||||
|
|
||||||
if(true === options.escapeDesc) {
|
if (true === options.escapeDesc) {
|
||||||
options.escapeDesc = '\\n';
|
options.escapeDesc = '\\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
total : 0,
|
total: 0,
|
||||||
current : 0,
|
current: 0,
|
||||||
step : 'preparing',
|
step: 'preparing',
|
||||||
status : 'Preparing',
|
status: 'Preparing',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProgress = _.isFunction(options.progress) ?
|
const updateProgress = _.isFunction(options.progress)
|
||||||
progCb => {
|
? progCb => {
|
||||||
return options.progress(state, progCb);
|
return options.progress(state, progCb);
|
||||||
} :
|
}
|
||||||
progCb => {
|
: progCb => {
|
||||||
return progCb(null);
|
return progCb(null);
|
||||||
}
|
};
|
||||||
;
|
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function readTemplateFiles(callback) {
|
function readTemplateFiles(callback) {
|
||||||
updateProgress(err => {
|
updateProgress(err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateFiles = [
|
const templateFiles = [
|
||||||
{ name : options.headerTemplate, req : false },
|
{ name: options.headerTemplate, req: false },
|
||||||
{ name : options.entryTemplate, req : true }
|
{ name: options.entryTemplate, req: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
async.map(templateFiles, (template, nextTemplate) => {
|
async.map(
|
||||||
if(!template.name && !template.req) {
|
templateFiles,
|
||||||
return nextTemplate(null, Buffer.from([]));
|
(template, nextTemplate) => {
|
||||||
}
|
if (!template.name && !template.req) {
|
||||||
|
return nextTemplate(null, Buffer.from([]));
|
||||||
|
}
|
||||||
|
|
||||||
template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name);
|
template.name = paths.isAbsolute(template.name)
|
||||||
fs.readFile(template.name, (err, data) => {
|
? template.name
|
||||||
return nextTemplate(err, data);
|
: paths.join(config.paths.misc, template.name);
|
||||||
});
|
fs.readFile(template.name, (err, data) => {
|
||||||
}, (err, templates) => {
|
return nextTemplate(err, data);
|
||||||
if(err) {
|
|
||||||
return callback(Errors.General(err.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode + ensure DOS style CRLF
|
|
||||||
templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') );
|
|
||||||
|
|
||||||
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
|
|
||||||
let descIndent = 0;
|
|
||||||
if(!options.escapeDesc) {
|
|
||||||
splitTextAtTerms(templates[1]).some(line => {
|
|
||||||
const pos = line.indexOf('{fileDesc}');
|
|
||||||
if(pos > -1) {
|
|
||||||
descIndent = pos;
|
|
||||||
return true; // found it!
|
|
||||||
}
|
|
||||||
return false; // keep looking
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
(err, templates) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(Errors.General(err.message));
|
||||||
|
}
|
||||||
|
|
||||||
return callback(null, templates[0], templates[1], descIndent);
|
// decode + ensure DOS style CRLF
|
||||||
});
|
templates = templates.map(tmp =>
|
||||||
|
iconv
|
||||||
|
.decode(tmp, options.templateEncoding)
|
||||||
|
.replace(/\r?\n/g, '\r\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
|
||||||
|
let descIndent = 0;
|
||||||
|
if (!options.escapeDesc) {
|
||||||
|
splitTextAtTerms(templates[1]).some(line => {
|
||||||
|
const pos = line.indexOf('{fileDesc}');
|
||||||
|
if (pos > -1) {
|
||||||
|
descIndent = pos;
|
||||||
|
return true; // found it!
|
||||||
|
}
|
||||||
|
return false; // keep looking
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null, templates[0], templates[1], descIndent);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
|
function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
|
||||||
state.step = 'gathering';
|
state.step = 'gathering';
|
||||||
state.status = 'Gathering files for supplied criteria';
|
state.status = 'Gathering files for supplied criteria';
|
||||||
updateProgress(err => {
|
updateProgress(err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
|
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
|
||||||
if(0 === fileIds.length) {
|
if (0 === fileIds.length) {
|
||||||
return callback(Errors.General('No results for criteria', 'NORESULTS'));
|
return callback(
|
||||||
|
Errors.General('No results for criteria', 'NORESULTS')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(err, headerTemplate, entryTemplate, descIndent, fileIds);
|
return callback(
|
||||||
|
err,
|
||||||
|
headerTemplate,
|
||||||
|
entryTemplate,
|
||||||
|
descIndent,
|
||||||
|
fileIds
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) {
|
function buildListEntries(
|
||||||
|
headerTemplate,
|
||||||
|
entryTemplate,
|
||||||
|
descIndent,
|
||||||
|
fileIds,
|
||||||
|
callback
|
||||||
|
) {
|
||||||
const formatObj = {
|
const formatObj = {
|
||||||
totalFileCount : fileIds.length,
|
totalFileCount: fileIds.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
let current = 0;
|
let current = 0;
|
||||||
let listBody = '';
|
let listBody = '';
|
||||||
const totals = { fileCount : fileIds.length, bytes : 0 };
|
const totals = { fileCount: fileIds.length, bytes: 0 };
|
||||||
state.total = fileIds.length;
|
state.total = fileIds.length;
|
||||||
|
|
||||||
state.step = 'file';
|
state.step = 'file';
|
||||||
|
|
||||||
async.eachSeries(fileIds, (fileId, nextFileId) => {
|
async.eachSeries(
|
||||||
const fileInfo = new FileEntry();
|
fileIds,
|
||||||
current += 1;
|
(fileId, nextFileId) => {
|
||||||
|
const fileInfo = new FileEntry();
|
||||||
|
current += 1;
|
||||||
|
|
||||||
fileInfo.load(fileId, err => {
|
fileInfo.load(fileId, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return nextFileId(null); // failed, but try the next
|
return nextFileId(null); // failed, but try the next
|
||||||
}
|
|
||||||
|
|
||||||
totals.bytes += fileInfo.meta.byte_size;
|
|
||||||
|
|
||||||
const appendFileInfo = () => {
|
|
||||||
if(options.escapeDesc) {
|
|
||||||
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.maxDescLen) {
|
totals.bytes += fileInfo.meta.byte_size;
|
||||||
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
listBody += stringFormat(entryTemplate, formatObj);
|
const appendFileInfo = () => {
|
||||||
|
if (options.escapeDesc) {
|
||||||
state.current = current;
|
formatObj.fileDesc = formatObj.fileDesc.replace(
|
||||||
state.status = `Processing ${fileInfo.fileName}`;
|
/\r?\n/g,
|
||||||
state.fileInfo = formatObj;
|
options.escapeDesc
|
||||||
|
);
|
||||||
updateProgress(err => {
|
|
||||||
return nextFileId(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
|
|
||||||
|
|
||||||
formatObj.fileId = fileId;
|
|
||||||
formatObj.areaName = _.get(area, 'name') || 'N/A';
|
|
||||||
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
|
|
||||||
formatObj.userRating = fileInfo.userRating || 0;
|
|
||||||
formatObj.fileName = fileInfo.fileName;
|
|
||||||
formatObj.fileSize = fileInfo.meta.byte_size;
|
|
||||||
formatObj.fileDesc = fileInfo.desc || '';
|
|
||||||
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth);
|
|
||||||
formatObj.fileSha256 = fileInfo.fileSha256;
|
|
||||||
formatObj.fileCrc32 = fileInfo.meta.file_crc32;
|
|
||||||
formatObj.fileMd5 = fileInfo.meta.file_md5;
|
|
||||||
formatObj.fileSha1 = fileInfo.meta.file_sha1;
|
|
||||||
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A';
|
|
||||||
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat);
|
|
||||||
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A';
|
|
||||||
formatObj.currentFile = current;
|
|
||||||
formatObj.progress = Math.floor( (current / fileIds.length) * 100 );
|
|
||||||
|
|
||||||
if(isAnsi(fileInfo.desc)) {
|
|
||||||
AnsiPrep(
|
|
||||||
fileInfo.desc,
|
|
||||||
{
|
|
||||||
cols : Math.min(options.descWidth, 79 - descIndent),
|
|
||||||
forceLineTerm : true, // ensure each line is term'd
|
|
||||||
asciiMode : true, // export to ASCII
|
|
||||||
fillLines : false, // don't fill up to |cols|
|
|
||||||
indent : descIndent,
|
|
||||||
},
|
|
||||||
(err, desc) => {
|
|
||||||
if(desc) {
|
|
||||||
formatObj.fileDesc = desc;
|
|
||||||
}
|
|
||||||
return appendFileInfo();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.maxDescLen) {
|
||||||
|
formatObj.fileDesc = formatObj.fileDesc.slice(
|
||||||
|
0,
|
||||||
|
options.maxDescLen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listBody += stringFormat(entryTemplate, formatObj);
|
||||||
|
|
||||||
|
state.current = current;
|
||||||
|
state.status = `Processing ${fileInfo.fileName}`;
|
||||||
|
state.fileInfo = formatObj;
|
||||||
|
|
||||||
|
updateProgress(err => {
|
||||||
|
return nextFileId(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
|
||||||
|
|
||||||
|
formatObj.fileId = fileId;
|
||||||
|
formatObj.areaName = _.get(area, 'name') || 'N/A';
|
||||||
|
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
|
||||||
|
formatObj.userRating = fileInfo.userRating || 0;
|
||||||
|
formatObj.fileName = fileInfo.fileName;
|
||||||
|
formatObj.fileSize = fileInfo.meta.byte_size;
|
||||||
|
formatObj.fileDesc = fileInfo.desc || '';
|
||||||
|
formatObj.fileDescShort = formatObj.fileDesc.slice(
|
||||||
|
0,
|
||||||
|
options.descWidth
|
||||||
);
|
);
|
||||||
} else {
|
formatObj.fileSha256 = fileInfo.fileSha256;
|
||||||
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : '';
|
formatObj.fileCrc32 = fileInfo.meta.file_crc32;
|
||||||
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n';
|
formatObj.fileMd5 = fileInfo.meta.file_md5;
|
||||||
return appendFileInfo();
|
formatObj.fileSha1 = fileInfo.meta.file_sha1;
|
||||||
}
|
formatObj.uploadBy =
|
||||||
});
|
fileInfo.meta.upload_by_username || 'N/A';
|
||||||
}, err => {
|
formatObj.fileUploadTs = moment(
|
||||||
return callback(err, listBody, headerTemplate, totals);
|
fileInfo.uploadTimestamp
|
||||||
});
|
).format(options.tsFormat);
|
||||||
|
formatObj.fileHashTags =
|
||||||
|
fileInfo.hashTags.size > 0
|
||||||
|
? Array.from(fileInfo.hashTags).join(', ')
|
||||||
|
: 'N/A';
|
||||||
|
formatObj.currentFile = current;
|
||||||
|
formatObj.progress = Math.floor(
|
||||||
|
(current / fileIds.length) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAnsi(fileInfo.desc)) {
|
||||||
|
AnsiPrep(
|
||||||
|
fileInfo.desc,
|
||||||
|
{
|
||||||
|
cols: Math.min(
|
||||||
|
options.descWidth,
|
||||||
|
79 - descIndent
|
||||||
|
),
|
||||||
|
forceLineTerm: true, // ensure each line is term'd
|
||||||
|
asciiMode: true, // export to ASCII
|
||||||
|
fillLines: false, // don't fill up to |cols|
|
||||||
|
indent: descIndent,
|
||||||
|
},
|
||||||
|
(err, desc) => {
|
||||||
|
if (desc) {
|
||||||
|
formatObj.fileDesc = desc;
|
||||||
|
}
|
||||||
|
return appendFileInfo();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const indentSpc =
|
||||||
|
descIndent > 0 ? ' '.repeat(descIndent) : '';
|
||||||
|
formatObj.fileDesc =
|
||||||
|
splitTextAtTerms(formatObj.fileDesc).join(
|
||||||
|
`\r\n${indentSpc}`
|
||||||
|
) + '\r\n';
|
||||||
|
return appendFileInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return callback(err, listBody, headerTemplate, totals);
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function buildHeader(listBody, headerTemplate, totals, callback) {
|
function buildHeader(listBody, headerTemplate, totals, callback) {
|
||||||
// header is built last such that we can have totals/etc.
|
// header is built last such that we can have totals/etc.
|
||||||
|
|
||||||
let filterAreaName;
|
let filterAreaName;
|
||||||
let filterAreaDesc;
|
let filterAreaDesc;
|
||||||
if(filterCriteria.areaTag) {
|
if (filterCriteria.areaTag) {
|
||||||
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
|
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
|
||||||
filterAreaName = _.get(area, 'name') || 'N/A';
|
filterAreaName = _.get(area, 'name') || 'N/A';
|
||||||
filterAreaDesc = _.get(area, 'desc') || 'N/A';
|
filterAreaDesc = _.get(area, 'desc') || 'N/A';
|
||||||
} else {
|
} else {
|
||||||
filterAreaName = '-ALL-';
|
filterAreaName = '-ALL-';
|
||||||
filterAreaDesc = 'All areas';
|
filterAreaDesc = 'All areas';
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerFormatObj = {
|
const headerFormatObj = {
|
||||||
nowTs : moment().format(options.tsFormat),
|
nowTs: moment().format(options.tsFormat),
|
||||||
boardName : Config().general.boardName,
|
boardName: Config().general.boardName,
|
||||||
totalFileCount : totals.fileCount,
|
totalFileCount: totals.fileCount,
|
||||||
totalFileSize : totals.bytes,
|
totalFileSize: totals.bytes,
|
||||||
filterAreaTag : filterCriteria.areaTag || '-ALL-',
|
filterAreaTag: filterCriteria.areaTag || '-ALL-',
|
||||||
filterAreaName : filterAreaName,
|
filterAreaName: filterAreaName,
|
||||||
filterAreaDesc : filterAreaDesc,
|
filterAreaDesc: filterAreaDesc,
|
||||||
filterTerms : filterCriteria.terms || '(none)',
|
filterTerms: filterCriteria.terms || '(none)',
|
||||||
filterHashTags : filterCriteria.tags || '(none)',
|
filterHashTags: filterCriteria.tags || '(none)',
|
||||||
};
|
};
|
||||||
|
|
||||||
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
|
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
|
||||||
@@ -238,13 +286,14 @@ function exportFileList(filterCriteria, options, cb) {
|
|||||||
},
|
},
|
||||||
function done(listBody, callback) {
|
function done(listBody, callback) {
|
||||||
delete state.fileInfo;
|
delete state.fileInfo;
|
||||||
state.step = 'finished';
|
state.step = 'finished';
|
||||||
state.status = 'Finished processing';
|
state.status = 'Finished processing';
|
||||||
updateProgress( () => {
|
updateProgress(() => {
|
||||||
return callback(null, listBody);
|
return callback(null, listBody);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
], (err, listBody) => {
|
],
|
||||||
|
(err, listBody) => {
|
||||||
return cb(err, listBody);
|
return cb(err, listBody);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -260,42 +309,59 @@ function updateFileBaseDescFilesScheduledEvent(args, cb) {
|
|||||||
// * Multi line descriptions are stored with *escaped* \r\n pairs
|
// * Multi line descriptions are stored with *escaped* \r\n pairs
|
||||||
// * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec
|
// * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec
|
||||||
//
|
//
|
||||||
const entryTemplate = args[0];
|
const entryTemplate = args[0];
|
||||||
const headerTemplate = args[1];
|
const headerTemplate = args[1];
|
||||||
|
|
||||||
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true });
|
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck: true });
|
||||||
async.each(areas, (area, nextArea) => {
|
async.each(
|
||||||
const storageLocations = FileArea.getAreaStorageLocations(area);
|
areas,
|
||||||
|
(area, nextArea) => {
|
||||||
|
const storageLocations = FileArea.getAreaStorageLocations(area);
|
||||||
|
|
||||||
async.each(storageLocations, (storageLoc, nextStorageLoc) => {
|
async.each(
|
||||||
const filterCriteria = {
|
storageLocations,
|
||||||
areaTag : area.areaTag,
|
(storageLoc, nextStorageLoc) => {
|
||||||
storageTag : storageLoc.storageTag,
|
const filterCriteria = {
|
||||||
};
|
areaTag: area.areaTag,
|
||||||
|
storageTag: storageLoc.storageTag,
|
||||||
|
};
|
||||||
|
|
||||||
const exportOpts = {
|
const exportOpts = {
|
||||||
headerTemplate : headerTemplate,
|
headerTemplate: headerTemplate,
|
||||||
entryTemplate : entryTemplate,
|
entryTemplate: entryTemplate,
|
||||||
escapeDesc : true, // escape CRLF's
|
escapeDesc: true, // escape CRLF's
|
||||||
maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes"
|
maxDescLen: 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes"
|
||||||
};
|
};
|
||||||
|
|
||||||
exportFileList(filterCriteria, exportOpts, (err, listBody) => {
|
exportFileList(filterCriteria, exportOpts, (err, listBody) => {
|
||||||
|
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION');
|
||||||
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION');
|
fs.writeFile(
|
||||||
fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => {
|
descIonPath,
|
||||||
if(err) {
|
iconv.encode(listBody, 'cp437'),
|
||||||
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION');
|
err => {
|
||||||
} else {
|
if (err) {
|
||||||
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION');
|
Log.warn(
|
||||||
}
|
{ error: err.message, path: descIonPath },
|
||||||
return nextStorageLoc(null);
|
'Failed (re)creating DESCRIPT.ION'
|
||||||
});
|
);
|
||||||
});
|
} else {
|
||||||
}, () => {
|
Log.debug(
|
||||||
return nextArea(null);
|
{ path: descIonPath },
|
||||||
});
|
'(Re)generated DESCRIPT.ION'
|
||||||
}, () => {
|
);
|
||||||
return cb(null);
|
}
|
||||||
});
|
return nextStorageLoc(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return nextArea(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,31 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
const getSortedAvailableFileAreas =
|
||||||
const FileBaseFilters = require('./file_base_filter.js');
|
require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||||
|
const FileBaseFilters = require('./file_base_filter.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Base Search',
|
name: 'File Base Search',
|
||||||
desc : 'Module for quickly searching the file base',
|
desc: 'Module for quickly searching the file base',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
search : {
|
search: {
|
||||||
searchTerms : 1,
|
searchTerms: 1,
|
||||||
search : 2,
|
search: 2,
|
||||||
tags : 3,
|
tags: 3,
|
||||||
area : 4,
|
area: 4,
|
||||||
orderBy : 5,
|
orderBy: 5,
|
||||||
sort : 6,
|
sort: 6,
|
||||||
advSearch : 7,
|
advSearch: 7,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileBaseSearch extends MenuModule {
|
exports.getModule = class FileBaseSearch extends MenuModule {
|
||||||
@@ -33,7 +34,7 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
search : (formData, extraArgs, cb) => {
|
search: (formData, extraArgs, cb) => {
|
||||||
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
|
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
|
||||||
return this.searchNow(formData, isAdvanced, cb);
|
return this.searchNow(formData, isAdvanced, cb);
|
||||||
},
|
},
|
||||||
@@ -42,28 +43,36 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
|||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) );
|
const vc = self.addViewController(
|
||||||
|
'search',
|
||||||
|
new ViewController({ client: this.client })
|
||||||
|
);
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function loadFromConfig(callback) {
|
function loadFromConfig(callback) {
|
||||||
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
return vc.loadFromMenuConfig(
|
||||||
|
{ callingMenu: self, mciMap: mciData.menu },
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function populateAreas(callback) {
|
function populateAreas(callback) {
|
||||||
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
self.availAreas = [{ name: '-ALL-' }].concat(
|
||||||
|
getSortedAvailableFileAreas(self.client) || []
|
||||||
|
);
|
||||||
|
|
||||||
const areasView = vc.getView(MciViewIds.search.area);
|
const areasView = vc.getView(MciViewIds.search.area);
|
||||||
areasView.setItems( self.availAreas.map( a => a.name ) );
|
areasView.setItems(self.availAreas.map(a => a.name));
|
||||||
areasView.redraw();
|
areasView.redraw();
|
||||||
vc.switchFocus(MciViewIds.search.searchTerms);
|
vc.switchFocus(MciViewIds.search.searchTerms);
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -73,11 +82,11 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSelectedAreaTag(index) {
|
getSelectedAreaTag(index) {
|
||||||
if(0 === index) {
|
if (0 === index) {
|
||||||
return ''; // -ALL-
|
return ''; // -ALL-
|
||||||
}
|
}
|
||||||
const area = this.availAreas[index];
|
const area = this.availAreas[index];
|
||||||
if(!area) {
|
if (!area) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return area.areaTag;
|
return area.areaTag;
|
||||||
@@ -92,16 +101,16 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFilterValuesFromFormData(formData, isAdvanced) {
|
getFilterValuesFromFormData(formData, isAdvanced) {
|
||||||
const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
|
const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
|
||||||
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
|
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
|
||||||
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
|
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
areaTag : this.getSelectedAreaTag(areaIndex),
|
areaTag: this.getSelectedAreaTag(areaIndex),
|
||||||
terms : formData.value.searchTerms,
|
terms: formData.value.searchTerms,
|
||||||
tags : isAdvanced ? formData.value.tags : '',
|
tags: isAdvanced ? formData.value.tags : '',
|
||||||
order : this.getOrderBy(orderByIndex),
|
order: this.getOrderBy(orderByIndex),
|
||||||
sort : this.getSortBy(sortByIndex),
|
sort: this.getSortBy(sortByIndex),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +118,16 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
|||||||
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
|
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
|
||||||
|
|
||||||
const menuOpts = {
|
const menuOpts = {
|
||||||
extraArgs : {
|
extraArgs: {
|
||||||
filterCriteria : filterCriteria,
|
filterCriteria: filterCriteria,
|
||||||
},
|
},
|
||||||
menuFlags : [ 'popParent' ],
|
menuFlags: ['popParent'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries',
|
||||||
|
menuOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,23 +2,23 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const FileArea = require('./file_base_area.js');
|
const FileArea = require('./file_base_area.js');
|
||||||
const { renderSubstr } = require('./string_util.js');
|
const { renderSubstr } = require('./string_util.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
const { exportFileList } = require('./file_base_list_export.js');
|
const { exportFileList } = require('./file_base_list_export.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const { v4 : UUIDv4 } = require('uuid');
|
const { v4: UUIDv4 } = require('uuid');
|
||||||
const yazl = require('yazl');
|
const yazl = require('yazl');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Module config block can contain the following:
|
Module config block can contain the following:
|
||||||
@@ -44,52 +44,66 @@ const yazl = require('yazl');
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Base List Export',
|
name: 'File Base List Export',
|
||||||
desc : 'Exports file base listings for download',
|
desc: 'Exports file base listings for download',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
main : 0,
|
main: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
main : {
|
main: {
|
||||||
status : 1,
|
status: 1,
|
||||||
progressBar : 2,
|
progressBar: 2,
|
||||||
|
|
||||||
customRangeStart : 10,
|
customRangeStart: 10,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileBaseListExport extends MenuModule {
|
exports.getModule = class FileBaseListExport extends MenuModule {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
|
this.config = Object.assign(
|
||||||
|
{},
|
||||||
|
_.get(options, 'menuConfig.config'),
|
||||||
|
options.extraArgs
|
||||||
|
);
|
||||||
|
|
||||||
this.config.templateEncoding = this.config.templateEncoding || 'utf8';
|
this.config.templateEncoding = this.config.templateEncoding || 'utf8';
|
||||||
this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short');
|
this.config.tsFormat =
|
||||||
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
|
this.config.tsFormat ||
|
||||||
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1);
|
this.client.currentTheme.helpers.getDateTimeFormat('short');
|
||||||
this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :)
|
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
|
||||||
|
this.config.progBarChar = renderSubstr(this.config.progBarChar || '▒', 0, 1);
|
||||||
|
this.config.compressThreshold = this.config.compressThreshold || 1440000; // >= 1.44M by default :)
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
(callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback),
|
callback =>
|
||||||
(callback) => this.prepareList(callback),
|
this.prepViewController(
|
||||||
|
'main',
|
||||||
|
FormIds.main,
|
||||||
|
mciData.menu,
|
||||||
|
callback
|
||||||
|
),
|
||||||
|
callback => this.prepareList(callback),
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
if('NORESULTS' === err.reasonCode) {
|
if ('NORESULTS' === err.reasonCode) {
|
||||||
return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults');
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.noResultsMenu ||
|
||||||
|
'fileBaseExportListNoResults'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prevMenu();
|
return this.prevMenu();
|
||||||
@@ -108,16 +122,18 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const statusView = self.viewControllers.main.getView(MciViewIds.main.status);
|
const statusView = self.viewControllers.main.getView(MciViewIds.main.status);
|
||||||
const updateStatus = (status) => {
|
const updateStatus = status => {
|
||||||
if(statusView) {
|
if (statusView) {
|
||||||
statusView.setText(status);
|
statusView.setText(status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar);
|
const progBarView = self.viewControllers.main.getView(
|
||||||
|
MciViewIds.main.progressBar
|
||||||
|
);
|
||||||
const updateProgressBar = (curr, total) => {
|
const updateProgressBar = (curr, total) => {
|
||||||
if(progBarView) {
|
if (progBarView) {
|
||||||
const prog = Math.floor( (curr / total) * progBarView.dimens.width );
|
const prog = Math.floor((curr / total) * progBarView.dimens.width);
|
||||||
progBarView.setText(self.config.progBarChar.repeat(prog));
|
progBarView.setText(self.config.progBarChar.repeat(prog));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -125,17 +141,21 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
let cancel = false;
|
let cancel = false;
|
||||||
|
|
||||||
const exportListProgress = (state, progNext) => {
|
const exportListProgress = (state, progNext) => {
|
||||||
switch(state.step) {
|
switch (state.step) {
|
||||||
case 'preparing' :
|
case 'preparing':
|
||||||
case 'gathering' :
|
case 'gathering':
|
||||||
updateStatus(state.status);
|
updateStatus(state.status);
|
||||||
break;
|
break;
|
||||||
case 'file' :
|
case 'file':
|
||||||
updateStatus(state.status);
|
updateStatus(state.status);
|
||||||
updateProgressBar(state.current, state.total);
|
updateProgressBar(state.current, state.total);
|
||||||
self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo);
|
self.updateCustomViewTextsWithFilter(
|
||||||
|
'main',
|
||||||
|
MciViewIds.main.customRangeStart,
|
||||||
|
state.fileInfo
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default :
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +163,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const keyPressHandler = (ch, key) => {
|
const keyPressHandler = (ch, key) => {
|
||||||
if('escape' === key.name) {
|
if ('escape' === key.name) {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
self.client.removeListener('key press', keyPressHandler);
|
self.client.removeListener('key press', keyPressHandler);
|
||||||
}
|
}
|
||||||
@@ -158,17 +178,27 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
self.client.on('key press', keyPressHandler);
|
self.client.on('key press', keyPressHandler);
|
||||||
|
|
||||||
const filterCriteria = Object.assign({}, self.config.filterCriteria);
|
const filterCriteria = Object.assign({}, self.config.filterCriteria);
|
||||||
if(!filterCriteria.areaTag) {
|
if (!filterCriteria.areaTag) {
|
||||||
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client);
|
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(
|
||||||
|
self.client
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
templateEncoding : self.config.templateEncoding,
|
templateEncoding: self.config.templateEncoding,
|
||||||
headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'),
|
headerTemplate: _.get(
|
||||||
entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'),
|
self.config,
|
||||||
tsFormat : self.config.tsFormat,
|
'templates.header',
|
||||||
descWidth : self.config.descWidth,
|
'file_list_header.asc'
|
||||||
progress : exportListProgress,
|
),
|
||||||
|
entryTemplate: _.get(
|
||||||
|
self.config,
|
||||||
|
'templates.entry',
|
||||||
|
'file_list_entry.asc'
|
||||||
|
),
|
||||||
|
tsFormat: self.config.tsFormat,
|
||||||
|
descWidth: self.config.descWidth,
|
||||||
|
progress: exportListProgress,
|
||||||
};
|
};
|
||||||
|
|
||||||
exportFileList(filterCriteria, opts, (err, listBody) => {
|
exportFileList(filterCriteria, opts, (err, listBody) => {
|
||||||
@@ -178,47 +208,65 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
function persistList(listBody, callback) {
|
function persistList(listBody, callback) {
|
||||||
updateStatus('Persisting list');
|
updateStatus('Persisting list');
|
||||||
|
|
||||||
const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads);
|
const sysTempDownloadArea = FileArea.getFileAreaByTag(
|
||||||
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
|
FileArea.WellKnownAreaTags.TempDownloads
|
||||||
|
);
|
||||||
|
const sysTempDownloadDir =
|
||||||
|
FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
|
||||||
|
|
||||||
fse.mkdirs(sysTempDownloadDir, err => {
|
fse.mkdirs(sysTempDownloadDir, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputFileName = paths.join(
|
const outputFileName = paths.join(
|
||||||
sysTempDownloadDir,
|
sysTempDownloadDir,
|
||||||
`file_list_${UUIDv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt`
|
`file_list_${UUIDv4().substr(-8)}_${moment().format(
|
||||||
|
'YYYY-MM-DD'
|
||||||
|
)}.txt`
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.writeFile(outputFileName, listBody, 'utf8', err => {
|
fs.writeFile(outputFileName, listBody, 'utf8', err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => {
|
self.getSizeAndCompressIfMeetsSizeThreshold(
|
||||||
return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea);
|
outputFileName,
|
||||||
});
|
(err, finalOutputFileName, fileSize) => {
|
||||||
|
return callback(
|
||||||
|
err,
|
||||||
|
finalOutputFileName,
|
||||||
|
fileSize,
|
||||||
|
sysTempDownloadArea
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) {
|
function persistFileEntry(
|
||||||
|
outputFileName,
|
||||||
|
fileSize,
|
||||||
|
sysTempDownloadArea,
|
||||||
|
callback
|
||||||
|
) {
|
||||||
const newEntry = new FileEntry({
|
const newEntry = new FileEntry({
|
||||||
areaTag : sysTempDownloadArea.areaTag,
|
areaTag: sysTempDownloadArea.areaTag,
|
||||||
fileName : paths.basename(outputFileName),
|
fileName: paths.basename(outputFileName),
|
||||||
storageTag : sysTempDownloadArea.storageTags[0],
|
storageTag: sysTempDownloadArea.storageTags[0],
|
||||||
meta : {
|
meta: {
|
||||||
upload_by_username : self.client.user.username,
|
upload_by_username: self.client.user.username,
|
||||||
upload_by_user_id : self.client.user.userId,
|
upload_by_user_id: self.client.user.userId,
|
||||||
byte_size : fileSize,
|
byte_size: fileSize,
|
||||||
session_temp_dl : 1, // download is valid until session is over
|
session_temp_dl: 1, // download is valid until session is over
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
newEntry.desc = 'File List Export';
|
newEntry.desc = 'File List Export';
|
||||||
|
|
||||||
newEntry.persist(err => {
|
newEntry.persist(err => {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
// queue it!
|
// queue it!
|
||||||
DownloadQueue.get(self.client).addTemporaryDownload(newEntry);
|
DownloadQueue.get(self.client).addTemporaryDownload(newEntry);
|
||||||
}
|
}
|
||||||
@@ -232,7 +280,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
|
|
||||||
updateStatus('Exported list has been added to your download queue');
|
updateStatus('Exported list has been added to your download queue');
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
self.client.removeListener('key press', keyPressHandler);
|
self.client.removeListener('key press', keyPressHandler);
|
||||||
@@ -243,11 +291,11 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
|
|
||||||
getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) {
|
getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) {
|
||||||
fse.stat(filePath, (err, stats) => {
|
fse.stat(filePath, (err, stats) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stats.size < this.config.compressThreshold) {
|
if (stats.size < this.config.compressThreshold) {
|
||||||
// small enough, keep orig
|
// small enough, keep orig
|
||||||
return cb(null, filePath, stats.size);
|
return cb(null, filePath, stats.size);
|
||||||
}
|
}
|
||||||
@@ -256,13 +304,13 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
|
|
||||||
const zipFile = new yazl.ZipFile();
|
const zipFile = new yazl.ZipFile();
|
||||||
zipFile.addFile(filePath, paths.basename(filePath));
|
zipFile.addFile(filePath, paths.basename(filePath));
|
||||||
zipFile.end( () => {
|
zipFile.end(() => {
|
||||||
const outZipFile = fs.createWriteStream(zipFilePath);
|
const outZipFile = fs.createWriteStream(zipFilePath);
|
||||||
zipFile.outputStream.pipe(outZipFile);
|
zipFile.outputStream.pipe(outZipFile);
|
||||||
zipFile.outputStream.on('finish', () => {
|
zipFile.outputStream.on('finish', () => {
|
||||||
// delete the original
|
// delete the original
|
||||||
fse.unlink(filePath, err => {
|
fse.unlink(filePath, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,4 +323,4 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,74 +2,79 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
const theme = require('./theme.js');
|
const theme = require('./theme.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const FileAreaWeb = require('./file_area_web.js');
|
const FileAreaWeb = require('./file_area_web.js');
|
||||||
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Base Download Web Queue Manager',
|
name: 'File Base Download Web Queue Manager',
|
||||||
desc : 'Module for interacting with web backed download queue/batch',
|
desc: 'Module for interacting with web backed download queue/batch',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
queueManager : 0
|
queueManager: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
queueManager : {
|
queueManager: {
|
||||||
queue : 1,
|
queue: 1,
|
||||||
navMenu : 2,
|
navMenu: 2,
|
||||||
|
|
||||||
customRangeStart : 10,
|
customRangeStart: 10,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.dlQueue = new DownloadQueue(this.client);
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
removeItem : (formData, extraArgs, cb) => {
|
removeItem: (formData, extraArgs, cb) => {
|
||||||
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
||||||
if(!selectedItem) {
|
if (!selectedItem) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dlQueue.removeItems(selectedItem.fileId);
|
this.dlQueue.removeItems(selectedItem.fileId);
|
||||||
|
|
||||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||||
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
|
return this.removeItemsFromDownloadQueueView(
|
||||||
|
formData.value.queueItem,
|
||||||
|
cb
|
||||||
|
);
|
||||||
},
|
},
|
||||||
clearQueue : (formData, extraArgs, cb) => {
|
clearQueue: (formData, extraArgs, cb) => {
|
||||||
this.dlQueue.clear();
|
this.dlQueue.clear();
|
||||||
|
|
||||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||||
return this.removeItemsFromDownloadQueueView('all', cb);
|
return this.removeItemsFromDownloadQueueView('all', cb);
|
||||||
},
|
},
|
||||||
getBatchLink : (formData, extraArgs, cb) => {
|
getBatchLink: (formData, extraArgs, cb) => {
|
||||||
return this.generateAndDisplayBatchLink(cb);
|
return this.generateAndDisplayBatchLink(cb);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
if(0 === this.dlQueue.items.length) {
|
if (0 === this.dlQueue.items.length) {
|
||||||
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.emptyQueueMenu ||
|
||||||
|
'fileBaseDownloadManagerEmptyQueue'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -81,7 +86,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||||||
},
|
},
|
||||||
function display(callback) {
|
function display(callback) {
|
||||||
return self.displayQueueManagerPage(false, callback);
|
return self.displayQueueManagerPage(false, callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
return self.finishedLoading();
|
return self.finishedLoading();
|
||||||
@@ -90,12 +95,14 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
||||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
const queueView = this.viewControllers.queueManager.getView(
|
||||||
if(!queueView) {
|
MciViewIds.queueManager.queue
|
||||||
|
);
|
||||||
|
if (!queueView) {
|
||||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if('all' === itemIndex) {
|
if ('all' === itemIndex) {
|
||||||
queueView.setItems([]);
|
queueView.setItems([]);
|
||||||
queueView.setFocusItems([]);
|
queueView.setFocusItems([]);
|
||||||
} else {
|
} else {
|
||||||
@@ -109,14 +116,17 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||||||
displayFileInfoForFileEntry(fileEntry) {
|
displayFileInfoForFileEntry(fileEntry) {
|
||||||
this.updateCustomViewTextsWithFilter(
|
this.updateCustomViewTextsWithFilter(
|
||||||
'queueManager',
|
'queueManager',
|
||||||
MciViewIds.queueManager.customRangeStart, fileEntry,
|
MciViewIds.queueManager.customRangeStart,
|
||||||
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
|
fileEntry,
|
||||||
|
{ filter: ['{webDlLink}', '{webDlExpire}', '{fileName}'] } // :TODO: Others....
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadQueueView(cb) {
|
updateDownloadQueueView(cb) {
|
||||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
const queueView = this.viewControllers.queueManager.getView(
|
||||||
if(!queueView) {
|
MciViewIds.queueManager.queue
|
||||||
|
);
|
||||||
|
if (!queueView) {
|
||||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,26 +150,28 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||||||
this.client,
|
this.client,
|
||||||
this.dlQueue.items,
|
this.dlQueue.items,
|
||||||
{
|
{
|
||||||
expireTime : expireTime
|
expireTime: expireTime,
|
||||||
},
|
},
|
||||||
(err, webBatchDlLink) => {
|
(err, webBatchDlLink) => {
|
||||||
// :TODO: handle not enabled -> display such
|
// :TODO: handle not enabled -> display such
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
const webDlExpireTimeFormat =
|
||||||
|
this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||||
|
|
||||||
const formatObj = {
|
const formatObj = {
|
||||||
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
|
webBatchDlLink:
|
||||||
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
|
ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
|
||||||
|
webBatchDlExpire: expireTime.format(webDlExpireTimeFormat),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateCustomViewTextsWithFilter(
|
this.updateCustomViewTextsWithFilter(
|
||||||
'queueManager',
|
'queueManager',
|
||||||
MciViewIds.queueManager.customRangeStart,
|
MciViewIds.queueManager.customRangeStart,
|
||||||
formatObj,
|
formatObj,
|
||||||
{ filter : Object.keys(formatObj).map(k => '{' + k + '}' ) }
|
{ filter: Object.keys(formatObj).map(k => '{' + k + '}') }
|
||||||
);
|
);
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
@@ -181,51 +193,75 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
function prepareQueueDownloadLinks(callback) {
|
function prepareQueueDownloadLinks(callback) {
|
||||||
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
const webDlExpireTimeFormat =
|
||||||
|
self.menuConfig.config.webDlExpireTimeFormat ||
|
||||||
|
'YYYY-MMM-DD @ h:mm';
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
|
async.each(
|
||||||
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
|
self.dlQueue.items,
|
||||||
if(err) {
|
(fileEntry, nextFileEntry) => {
|
||||||
if(ErrNotEnabled === err.reasonCode) {
|
FileAreaWeb.getExistingTempDownloadServeItem(
|
||||||
return nextFileEntry(err); // we should have caught this prior
|
self.client,
|
||||||
}
|
fileEntry,
|
||||||
|
(err, serveItem) => {
|
||||||
const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes');
|
if (err) {
|
||||||
|
if (ErrNotEnabled === err.reasonCode) {
|
||||||
FileAreaWeb.createAndServeTempDownload(
|
return nextFileEntry(err); // we should have caught this prior
|
||||||
self.client,
|
|
||||||
fileEntry,
|
|
||||||
{ expireTime : expireTime },
|
|
||||||
(err, url) => {
|
|
||||||
if(err) {
|
|
||||||
return nextFileEntry(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileEntry.webDlLinkRaw = url;
|
const expireTime = moment().add(
|
||||||
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
|
config.fileBase.web.expireMinutes,
|
||||||
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat);
|
'minutes'
|
||||||
|
);
|
||||||
|
|
||||||
|
FileAreaWeb.createAndServeTempDownload(
|
||||||
|
self.client,
|
||||||
|
fileEntry,
|
||||||
|
{ expireTime: expireTime },
|
||||||
|
(err, url) => {
|
||||||
|
if (err) {
|
||||||
|
return nextFileEntry(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEntry.webDlLinkRaw = url;
|
||||||
|
fileEntry.webDlLink =
|
||||||
|
ansi.vtxHyperlink(self.client, url) +
|
||||||
|
url;
|
||||||
|
fileEntry.webDlExpire =
|
||||||
|
expireTime.format(
|
||||||
|
webDlExpireTimeFormat
|
||||||
|
);
|
||||||
|
|
||||||
|
return nextFileEntry(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fileEntry.webDlLinkRaw = serveItem.url;
|
||||||
|
fileEntry.webDlLink =
|
||||||
|
ansi.vtxHyperlink(
|
||||||
|
self.client,
|
||||||
|
serveItem.url
|
||||||
|
) + serveItem.url;
|
||||||
|
fileEntry.webDlExpire = moment(
|
||||||
|
serveItem.expireTimestamp
|
||||||
|
).format(webDlExpireTimeFormat);
|
||||||
return nextFileEntry(null);
|
return nextFileEntry(null);
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
} else {
|
);
|
||||||
fileEntry.webDlLinkRaw = serveItem.url;
|
},
|
||||||
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url;
|
err => {
|
||||||
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
return callback(err);
|
||||||
return nextFileEntry(null);
|
}
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
return callback(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
function populateViews(callback) {
|
function populateViews(callback) {
|
||||||
return self.updateDownloadQueueView(callback);
|
return self.updateDownloadQueueView(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,60 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fileDb = require('./database.js').dbs.file;
|
const fileDb = require('./database.js').dbs.file;
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const {
|
const { getISOTimestampString, sanitizeString } = require('./database.js');
|
||||||
getISOTimestampString,
|
const Config = require('./config.js').get;
|
||||||
sanitizeString
|
|
||||||
} = require('./database.js');
|
|
||||||
const Config = require('./config.js').get;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
const { unlink, readFile } = require('graceful-fs');
|
const { unlink, readFile } = require('graceful-fs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
const FILE_TABLE_MEMBERS = [
|
const FILE_TABLE_MEMBERS = [
|
||||||
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag',
|
'file_id',
|
||||||
'desc', 'desc_long', 'upload_timestamp'
|
'area_tag',
|
||||||
|
'file_sha256',
|
||||||
|
'file_name',
|
||||||
|
'storage_tag',
|
||||||
|
'desc',
|
||||||
|
'desc_long',
|
||||||
|
'upload_timestamp',
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILE_WELL_KNOWN_META = {
|
const FILE_WELL_KNOWN_META = {
|
||||||
// name -> *read* converter, if any
|
// name -> *read* converter, if any
|
||||||
upload_by_username : null,
|
upload_by_username: null,
|
||||||
upload_by_user_id : (u) => parseInt(u) || 0,
|
upload_by_user_id: u => parseInt(u) || 0,
|
||||||
file_md5 : null,
|
file_md5: null,
|
||||||
file_sha1 : null,
|
file_sha1: null,
|
||||||
file_crc32 : null,
|
file_crc32: null,
|
||||||
est_release_year : (y) => parseInt(y) || new Date().getFullYear(),
|
est_release_year: y => parseInt(y) || new Date().getFullYear(),
|
||||||
dl_count : (d) => parseInt(d) || 0,
|
dl_count: d => parseInt(d) || 0,
|
||||||
byte_size : (b) => parseInt(b) || 0,
|
byte_size: b => parseInt(b) || 0,
|
||||||
archive_type : null,
|
archive_type: null,
|
||||||
short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import
|
short_file_name: null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import
|
||||||
tic_origin : null, // TIC "Origin"
|
tic_origin: null, // TIC "Origin"
|
||||||
tic_desc : null, // TIC "Desc"
|
tic_desc: null, // TIC "Desc"
|
||||||
tic_ldesc : null, // TIC "Ldesc" joined by '\n'
|
tic_ldesc: null, // TIC "Ldesc" joined by '\n'
|
||||||
session_temp_dl : (v) => parseInt(v) ? true : false,
|
session_temp_dl: v => (parseInt(v) ? true : false),
|
||||||
desc_sauce : (s) => JSON.parse(s) || {},
|
desc_sauce: s => JSON.parse(s) || {},
|
||||||
desc_long_sauce : (s) => JSON.parse(s) || {},
|
desc_long_sauce: s => JSON.parse(s) || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = class FileEntry {
|
module.exports = class FileEntry {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
this.fileId = options.fileId || 0;
|
this.fileId = options.fileId || 0;
|
||||||
this.areaTag = options.areaTag || '';
|
this.areaTag = options.areaTag || '';
|
||||||
this.meta = Object.assign( { dl_count : 0 }, options.meta);
|
this.meta = Object.assign({ dl_count: 0 }, options.meta);
|
||||||
this.hashTags = options.hashTags || new Set();
|
this.hashTags = options.hashTags || new Set();
|
||||||
this.fileName = options.fileName;
|
this.fileName = options.fileName;
|
||||||
this.storageTag = options.storageTag;
|
this.storageTag = options.storageTag;
|
||||||
this.fileSha256 = options.fileSha256;
|
this.fileSha256 = options.fileSha256;
|
||||||
}
|
}
|
||||||
@@ -64,13 +67,13 @@ module.exports = class FileEntry {
|
|||||||
FROM file
|
FROM file
|
||||||
WHERE file_id=?
|
WHERE file_id=?
|
||||||
LIMIT 1;`,
|
LIMIT 1;`,
|
||||||
[ fileId ],
|
[fileId],
|
||||||
(err, file) => {
|
(err, file) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!file) {
|
if (!file) {
|
||||||
return cb(Errors.DoesNotExist('No file is available by that ID'));
|
return cb(Errors.DoesNotExist('No file is available by that ID'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ module.exports = class FileEntry {
|
|||||||
},
|
},
|
||||||
function loadUserRating(callback) {
|
function loadUserRating(callback) {
|
||||||
return self.loadRating(callback);
|
return self.loadRating(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -109,7 +112,7 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
persist(isUpdate, cb) {
|
persist(isUpdate, cb) {
|
||||||
if(!cb && _.isFunction(isUpdate)) {
|
if (!cb && _.isFunction(isUpdate)) {
|
||||||
cb = isUpdate;
|
cb = isUpdate;
|
||||||
isUpdate = false;
|
isUpdate = false;
|
||||||
}
|
}
|
||||||
@@ -119,22 +122,30 @@ module.exports = class FileEntry {
|
|||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function check(callback) {
|
function check(callback) {
|
||||||
if(isUpdate && !self.fileId) {
|
if (isUpdate && !self.fileId) {
|
||||||
return callback(Errors.Invalid('Cannot update file entry without an existing "fileId" member'));
|
return callback(
|
||||||
|
Errors.Invalid(
|
||||||
|
'Cannot update file entry without an existing "fileId" member'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function calcSha256IfNeeded(callback) {
|
function calcSha256IfNeeded(callback) {
|
||||||
if(self.fileSha256) {
|
if (self.fileSha256) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isUpdate) {
|
if (isUpdate) {
|
||||||
return callback(Errors.MissingParam('fileSha256 property must be set for updates!'));
|
return callback(
|
||||||
|
Errors.MissingParam(
|
||||||
|
'fileSha256 property must be set for updates!'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
readFile(self.filePath, (err, data) => {
|
readFile(self.filePath, (err, data) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,11 +159,20 @@ module.exports = class FileEntry {
|
|||||||
return fileDb.beginTransaction(callback);
|
return fileDb.beginTransaction(callback);
|
||||||
},
|
},
|
||||||
function storeEntry(trans, callback) {
|
function storeEntry(trans, callback) {
|
||||||
if(isUpdate) {
|
if (isUpdate) {
|
||||||
trans.run(
|
trans.run(
|
||||||
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||||
[ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
|
[
|
||||||
|
self.fileId,
|
||||||
|
self.areaTag,
|
||||||
|
self.fileSha256,
|
||||||
|
self.fileName,
|
||||||
|
self.storageTag,
|
||||||
|
self.desc,
|
||||||
|
self.descLong,
|
||||||
|
getISOTimestampString(),
|
||||||
|
],
|
||||||
err => {
|
err => {
|
||||||
return callback(err, trans);
|
return callback(err, trans);
|
||||||
}
|
}
|
||||||
@@ -161,9 +181,18 @@ module.exports = class FileEntry {
|
|||||||
trans.run(
|
trans.run(
|
||||||
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?);`,
|
VALUES(?, ?, ?, ?, ?, ?, ?);`,
|
||||||
[ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
|
[
|
||||||
function inserted(err) { // use non-arrow func for 'this' scope / lastID
|
self.areaTag,
|
||||||
if(!err) {
|
self.fileSha256,
|
||||||
|
self.fileName,
|
||||||
|
self.storageTag,
|
||||||
|
self.desc,
|
||||||
|
self.descLong,
|
||||||
|
getISOTimestampString(),
|
||||||
|
],
|
||||||
|
function inserted(err) {
|
||||||
|
// use non-arrow func for 'this' scope / lastID
|
||||||
|
if (!err) {
|
||||||
self.fileId = this.lastID;
|
self.fileId = this.lastID;
|
||||||
}
|
}
|
||||||
return callback(err, trans);
|
return callback(err, trans);
|
||||||
@@ -172,27 +201,44 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
function storeMeta(trans, callback) {
|
function storeMeta(trans, callback) {
|
||||||
async.each(Object.keys(self.meta), (n, next) => {
|
async.each(
|
||||||
const v = self.meta[n];
|
Object.keys(self.meta),
|
||||||
return FileEntry.persistMetaValue(self.fileId, n, v, trans, next);
|
(n, next) => {
|
||||||
},
|
const v = self.meta[n];
|
||||||
err => {
|
return FileEntry.persistMetaValue(
|
||||||
return callback(err, trans);
|
self.fileId,
|
||||||
});
|
n,
|
||||||
|
v,
|
||||||
|
trans,
|
||||||
|
next
|
||||||
|
);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return callback(err, trans);
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function storeHashTags(trans, callback) {
|
function storeHashTags(trans, callback) {
|
||||||
const hashTagsArray = Array.from(self.hashTags);
|
const hashTagsArray = Array.from(self.hashTags);
|
||||||
async.each(hashTagsArray, (hashTag, next) => {
|
async.each(
|
||||||
return FileEntry.persistHashTag(self.fileId, hashTag, trans, next);
|
hashTagsArray,
|
||||||
},
|
(hashTag, next) => {
|
||||||
err => {
|
return FileEntry.persistHashTag(
|
||||||
return callback(err, trans);
|
self.fileId,
|
||||||
});
|
hashTag,
|
||||||
}
|
trans,
|
||||||
|
next
|
||||||
|
);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return callback(err, trans);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
],
|
],
|
||||||
(err, trans) => {
|
(err, trans) => {
|
||||||
// :TODO: Log orig err
|
// :TODO: Log orig err
|
||||||
if(trans) {
|
if (trans) {
|
||||||
trans[err ? 'rollback' : 'commit'](transErr => {
|
trans[err ? 'rollback' : 'commit'](transErr => {
|
||||||
return cb(transErr ? transErr : err);
|
return cb(transErr ? transErr : err);
|
||||||
});
|
});
|
||||||
@@ -205,10 +251,10 @@ module.exports = class FileEntry {
|
|||||||
|
|
||||||
static getAreaStorageDirectoryByTag(storageTag) {
|
static getAreaStorageDirectoryByTag(storageTag) {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]);
|
const storageLocation = storageTag && config.fileBase.storageTags[storageTag];
|
||||||
|
|
||||||
// absolute paths as-is
|
// absolute paths as-is
|
||||||
if(storageLocation && '/' === storageLocation.charAt(0)) {
|
if (storageLocation && '/' === storageLocation.charAt(0)) {
|
||||||
return storageLocation;
|
return storageLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +273,7 @@ module.exports = class FileEntry {
|
|||||||
FROM file
|
FROM file
|
||||||
WHERE file_name = ?
|
WHERE file_name = ?
|
||||||
LIMIT 1;`,
|
LIMIT 1;`,
|
||||||
[ paths.basename(fullPath) ],
|
[paths.basename(fullPath)],
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
return err ? cb(err) : cb(null, rows.count > 0 ? true : false);
|
return err ? cb(err) : cb(null, rows.count > 0 ? true : false);
|
||||||
}
|
}
|
||||||
@@ -238,7 +284,7 @@ module.exports = class FileEntry {
|
|||||||
return fileDb.run(
|
return fileDb.run(
|
||||||
`REPLACE INTO file_user_rating (file_id, user_id, rating)
|
`REPLACE INTO file_user_rating (file_id, user_id, rating)
|
||||||
VALUES (?, ?, ?);`,
|
VALUES (?, ?, ?);`,
|
||||||
[ fileId, userId, rating ],
|
[fileId, userId, rating],
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -247,13 +293,13 @@ module.exports = class FileEntry {
|
|||||||
return fileDb.run(
|
return fileDb.run(
|
||||||
`DELETE FROM file_user_rating
|
`DELETE FROM file_user_rating
|
||||||
WHERE user_id = ?;`,
|
WHERE user_id = ?;`,
|
||||||
[ userId ],
|
[userId],
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static persistMetaValue(fileId, name, value, transOrDb, cb) {
|
static persistMetaValue(fileId, name, value, transOrDb, cb) {
|
||||||
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
||||||
cb = transOrDb;
|
cb = transOrDb;
|
||||||
transOrDb = fileDb;
|
transOrDb = fileDb;
|
||||||
}
|
}
|
||||||
@@ -261,7 +307,7 @@ module.exports = class FileEntry {
|
|||||||
return transOrDb.run(
|
return transOrDb.run(
|
||||||
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
|
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
|
||||||
VALUES (?, ?, ?);`,
|
VALUES (?, ?, ?);`,
|
||||||
[ fileId, name, value ],
|
[fileId, name, value],
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -272,9 +318,9 @@ module.exports = class FileEntry {
|
|||||||
`UPDATE file_meta
|
`UPDATE file_meta
|
||||||
SET meta_value = meta_value + ?
|
SET meta_value = meta_value + ?
|
||||||
WHERE file_id = ? AND meta_name = ?;`,
|
WHERE file_id = ? AND meta_name = ?;`,
|
||||||
[ incrementBy, fileId, name ],
|
[incrementBy, fileId, name],
|
||||||
err => {
|
err => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,11 +332,13 @@ module.exports = class FileEntry {
|
|||||||
`SELECT meta_name, meta_value
|
`SELECT meta_name, meta_value
|
||||||
FROM file_meta
|
FROM file_meta
|
||||||
WHERE file_id=?;`,
|
WHERE file_id=?;`,
|
||||||
[ this.fileId ],
|
[this.fileId],
|
||||||
(err, meta) => {
|
(err, meta) => {
|
||||||
if(meta) {
|
if (meta) {
|
||||||
const conv = FILE_WELL_KNOWN_META[meta.meta_name];
|
const conv = FILE_WELL_KNOWN_META[meta.meta_name];
|
||||||
this.meta[meta.meta_name] = conv ? conv(meta.meta_value) : meta.meta_value;
|
this.meta[meta.meta_name] = conv
|
||||||
|
? conv(meta.meta_value)
|
||||||
|
: meta.meta_value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
@@ -300,16 +348,16 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static persistHashTag(fileId, hashTag, transOrDb, cb) {
|
static persistHashTag(fileId, hashTag, transOrDb, cb) {
|
||||||
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
||||||
cb = transOrDb;
|
cb = transOrDb;
|
||||||
transOrDb = fileDb;
|
transOrDb = fileDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
transOrDb.serialize( () => {
|
transOrDb.serialize(() => {
|
||||||
transOrDb.run(
|
transOrDb.run(
|
||||||
`INSERT OR IGNORE INTO hash_tag (hash_tag)
|
`INSERT OR IGNORE INTO hash_tag (hash_tag)
|
||||||
VALUES (?);`,
|
VALUES (?);`,
|
||||||
[ hashTag ]
|
[hashTag]
|
||||||
);
|
);
|
||||||
|
|
||||||
transOrDb.run(
|
transOrDb.run(
|
||||||
@@ -320,7 +368,7 @@ module.exports = class FileEntry {
|
|||||||
WHERE hash_tag = ?),
|
WHERE hash_tag = ?),
|
||||||
?
|
?
|
||||||
);`,
|
);`,
|
||||||
[ hashTag, fileId ],
|
[hashTag, fileId],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
@@ -337,9 +385,9 @@ module.exports = class FileEntry {
|
|||||||
FROM file_hash_tag
|
FROM file_hash_tag
|
||||||
WHERE file_id=?
|
WHERE file_id=?
|
||||||
);`,
|
);`,
|
||||||
[ this.fileId ],
|
[this.fileId],
|
||||||
(err, hashTag) => {
|
(err, hashTag) => {
|
||||||
if(hashTag) {
|
if (hashTag) {
|
||||||
this.hashTags.add(hashTag.hash_tag);
|
this.hashTags.add(hashTag.hash_tag);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -356,9 +404,9 @@ module.exports = class FileEntry {
|
|||||||
INNER JOIN file f
|
INNER JOIN file f
|
||||||
ON f.file_id = fur.file_id
|
ON f.file_id = fur.file_id
|
||||||
AND f.file_id = ?`,
|
AND f.file_id = ?`,
|
||||||
[ this.fileId ],
|
[this.fileId],
|
||||||
(err, result) => {
|
(err, result) => {
|
||||||
if(result) {
|
if (result) {
|
||||||
this.userRating = result.avg_rating;
|
this.userRating = result.avg_rating;
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -367,16 +415,16 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHashTags(hashTags) {
|
setHashTags(hashTags) {
|
||||||
if(_.isString(hashTags)) {
|
if (_.isString(hashTags)) {
|
||||||
this.hashTags = new Set(hashTags.split(/[\s,]+/));
|
this.hashTags = new Set(hashTags.split(/[\s,]+/));
|
||||||
} else if(Array.isArray(hashTags)) {
|
} else if (Array.isArray(hashTags)) {
|
||||||
this.hashTags = new Set(hashTags);
|
this.hashTags = new Set(hashTags);
|
||||||
} else if(hashTags instanceof Set) {
|
} else if (hashTags instanceof Set) {
|
||||||
this.hashTags = hashTags;
|
this.hashTags = hashTags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get WellKnownMetaValues() {
|
static get WellKnownMetaValues() {
|
||||||
return Object.keys(FILE_WELL_KNOWN_META);
|
return Object.keys(FILE_WELL_KNOWN_META);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,17 +434,17 @@ module.exports = class FileEntry {
|
|||||||
`SELECT file_id
|
`SELECT file_id
|
||||||
FROM file
|
FROM file
|
||||||
WHERE file_sha256 LIKE "${sha}%"
|
WHERE file_sha256 LIKE "${sha}%"
|
||||||
LIMIT 2;`, // limit 2 such that we can find if there are dupes
|
LIMIT 2;`, // limit 2 such that we can find if there are dupes
|
||||||
(err, fileIdRows) => {
|
(err, fileIdRows) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!fileIdRows || 0 === fileIdRows.length) {
|
if (!fileIdRows || 0 === fileIdRows.length) {
|
||||||
return cb(Errors.DoesNotExist('No matches'));
|
return cb(Errors.DoesNotExist('No matches'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fileIdRows.length > 1) {
|
if (fileIdRows.length > 1) {
|
||||||
return cb(Errors.Invalid('SHA is ambiguous'));
|
return cb(Errors.Invalid('SHA is ambiguous'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,17 +461,17 @@ module.exports = class FileEntry {
|
|||||||
static findByFullPath(fullPath, cb) {
|
static findByFullPath(fullPath, cb) {
|
||||||
// first, basic by-filename lookup.
|
// first, basic by-filename lookup.
|
||||||
FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => {
|
FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if(!entries || !entries.length || entries.length > 1) {
|
if (!entries || !entries.length || entries.length > 1) {
|
||||||
return cb(Errors.DoesNotExist('No matches'));
|
return cb(Errors.DoesNotExist('No matches'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the *full* path has not changed
|
// ensure the *full* path has not changed
|
||||||
// :TODO: if FS is case-insensitive, we probably want a better check here
|
// :TODO: if FS is case-insensitive, we probably want a better check here
|
||||||
const possibleMatch = entries[0];
|
const possibleMatch = entries[0];
|
||||||
if(possibleMatch.fullPath === fullPath) {
|
if (possibleMatch.fullPath === fullPath) {
|
||||||
return cb(null, possibleMatch);
|
return cb(null, possibleMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,27 +489,30 @@ module.exports = class FileEntry {
|
|||||||
WHERE file_name LIKE "${wc}"
|
WHERE file_name LIKE "${wc}"
|
||||||
`,
|
`,
|
||||||
(err, fileIdRows) => {
|
(err, fileIdRows) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!fileIdRows || 0 === fileIdRows.length) {
|
if (!fileIdRows || 0 === fileIdRows.length) {
|
||||||
return cb(Errors.DoesNotExist('No matches'));
|
return cb(Errors.DoesNotExist('No matches'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = [];
|
const entries = [];
|
||||||
async.each(fileIdRows, (row, nextRow) => {
|
async.each(
|
||||||
const fileEntry = new FileEntry();
|
fileIdRows,
|
||||||
fileEntry.load(row.file_id, err => {
|
(row, nextRow) => {
|
||||||
if(!err) {
|
const fileEntry = new FileEntry();
|
||||||
entries.push(fileEntry);
|
fileEntry.load(row.file_id, err => {
|
||||||
}
|
if (!err) {
|
||||||
return nextRow(err);
|
entries.push(fileEntry);
|
||||||
});
|
}
|
||||||
},
|
return nextRow(err);
|
||||||
err => {
|
});
|
||||||
return cb(err, entries);
|
},
|
||||||
});
|
err => {
|
||||||
|
return cb(err, entries);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -484,12 +535,12 @@ module.exports = class FileEntry {
|
|||||||
let sqlOrderBy;
|
let sqlOrderBy;
|
||||||
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
|
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
if(moment.isMoment(filter.newerThanTimestamp)) {
|
if (moment.isMoment(filter.newerThanTimestamp)) {
|
||||||
filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp);
|
filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrderByWithCast(ob) {
|
function getOrderByWithCast(ob) {
|
||||||
if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) {
|
if (['dl_count', 'est_release_year', 'byte_size'].indexOf(filter.sort) > -1) {
|
||||||
return `ORDER BY CAST(${ob} AS INTEGER)`;
|
return `ORDER BY CAST(${ob} AS INTEGER)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +548,7 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendWhereClause(clause) {
|
function appendWhereClause(clause) {
|
||||||
if(sqlWhere) {
|
if (sqlWhere) {
|
||||||
sqlWhere += ' AND ';
|
sqlWhere += ' AND ';
|
||||||
} else {
|
} else {
|
||||||
sqlWhere += ' WHERE ';
|
sqlWhere += ' WHERE ';
|
||||||
@@ -505,20 +556,21 @@ module.exports = class FileEntry {
|
|||||||
sqlWhere += clause;
|
sqlWhere += clause;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter.sort && filter.sort.length > 0) {
|
if (filter.sort && filter.sort.length > 0) {
|
||||||
if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value?
|
if (Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) {
|
||||||
sql =
|
// sorting via a meta value?
|
||||||
`SELECT DISTINCT f.file_id
|
sql = `SELECT DISTINCT f.file_id
|
||||||
FROM file f, file_meta m`;
|
FROM file f, file_meta m`;
|
||||||
|
|
||||||
appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`);
|
appendWhereClause(
|
||||||
|
`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`
|
||||||
|
);
|
||||||
|
|
||||||
sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`;
|
sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`;
|
||||||
} else {
|
} else {
|
||||||
// additional special treatment for user ratings: we need to average them
|
// additional special treatment for user ratings: we need to average them
|
||||||
if('user_rating' === filter.sort) {
|
if ('user_rating' === filter.sort) {
|
||||||
sql =
|
sql = `SELECT DISTINCT f.file_id,
|
||||||
`SELECT DISTINCT f.file_id,
|
|
||||||
(SELECT IFNULL(AVG(rating), 0) rating
|
(SELECT IFNULL(AVG(rating), 0) rating
|
||||||
FROM file_user_rating
|
FROM file_user_rating
|
||||||
WHERE file_id = f.file_id)
|
WHERE file_id = f.file_id)
|
||||||
@@ -527,23 +579,22 @@ module.exports = class FileEntry {
|
|||||||
|
|
||||||
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
|
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
|
||||||
} else {
|
} else {
|
||||||
sql =
|
sql = `SELECT DISTINCT f.file_id
|
||||||
`SELECT DISTINCT f.file_id
|
|
||||||
FROM file f`;
|
FROM file f`;
|
||||||
|
|
||||||
sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
|
sqlOrderBy =
|
||||||
|
getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sql =
|
sql = `SELECT DISTINCT f.file_id
|
||||||
`SELECT DISTINCT f.file_id
|
|
||||||
FROM file f`;
|
FROM file f`;
|
||||||
|
|
||||||
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
|
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter.areaTag && filter.areaTag.length > 0) {
|
if (filter.areaTag && filter.areaTag.length > 0) {
|
||||||
if(Array.isArray(filter.areaTag)) {
|
if (Array.isArray(filter.areaTag)) {
|
||||||
const areaList = filter.areaTag.map(t => `"${t}"`).join(', ');
|
const areaList = filter.areaTag.map(t => `"${t}"`).join(', ');
|
||||||
appendWhereClause(`f.area_tag IN(${areaList})`);
|
appendWhereClause(`f.area_tag IN(${areaList})`);
|
||||||
} else {
|
} else {
|
||||||
@@ -551,10 +602,9 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter.metaPairs && filter.metaPairs.length > 0) {
|
if (filter.metaPairs && filter.metaPairs.length > 0) {
|
||||||
|
|
||||||
filter.metaPairs.forEach(mp => {
|
filter.metaPairs.forEach(mp => {
|
||||||
if(mp.wildcards) {
|
if (mp.wildcards) {
|
||||||
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
|
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
|
||||||
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
|
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
|
||||||
appendWhereClause(
|
appendWhereClause(
|
||||||
@@ -576,11 +626,11 @@ module.exports = class FileEntry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter.storageTag && filter.storageTag.length > 0) {
|
if (filter.storageTag && filter.storageTag.length > 0) {
|
||||||
appendWhereClause(`f.storage_tag="${filter.storageTag}"`);
|
appendWhereClause(`f.storage_tag="${filter.storageTag}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter.terms && filter.terms.length > 0) {
|
if (filter.terms && filter.terms.length > 0) {
|
||||||
const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms);
|
const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms);
|
||||||
|
|
||||||
if ('fts_match' === queryType) {
|
if ('fts_match' === queryType) {
|
||||||
@@ -606,9 +656,14 @@ module.exports = class FileEntry {
|
|||||||
filter.tags = filter.tags.toString();
|
filter.tags = filter.tags.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter.tags && filter.tags.length > 0) {
|
if (filter.tags && filter.tags.length > 0) {
|
||||||
// build list of quoted tags; filter.tags comes in as a space and/or comma separated values
|
// build list of quoted tags; filter.tags comes in as a space and/or comma separated values
|
||||||
const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanitizeString(tag)}"` ).join(',');
|
const tags = filter.tags
|
||||||
|
.replace(/,/g, ' ')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.split(' ')
|
||||||
|
.map(tag => `"${sanitizeString(tag)}"`)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
appendWhereClause(
|
appendWhereClause(
|
||||||
`f.file_id IN (
|
`f.file_id IN (
|
||||||
@@ -623,35 +678,43 @@ module.exports = class FileEntry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) {
|
if (
|
||||||
appendWhereClause(`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`);
|
_.isString(filter.newerThanTimestamp) &&
|
||||||
|
filter.newerThanTimestamp.length > 0
|
||||||
|
) {
|
||||||
|
appendWhereClause(
|
||||||
|
`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isNumber(filter.newerThanFileId)) {
|
if (_.isNumber(filter.newerThanFileId)) {
|
||||||
appendWhereClause(`f.file_id > ${filter.newerThanFileId}`);
|
appendWhereClause(`f.file_id > ${filter.newerThanFileId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += `${sqlWhere} ${sqlOrderBy}`;
|
sql += `${sqlWhere} ${sqlOrderBy}`;
|
||||||
|
|
||||||
if(_.isNumber(filter.limit)) {
|
if (_.isNumber(filter.limit)) {
|
||||||
sql += ` LIMIT ${filter.limit}`;
|
sql += ` LIMIT ${filter.limit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += ';';
|
sql += ';';
|
||||||
|
|
||||||
fileDb.all(sql, (err, rows) => {
|
fileDb.all(sql, (err, rows) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if(!rows || 0 === rows.length) {
|
if (!rows || 0 === rows.length) {
|
||||||
return cb(null, []); // no matches
|
return cb(null, []); // no matches
|
||||||
}
|
}
|
||||||
return cb(null, rows.map(r => r.file_id));
|
return cb(
|
||||||
|
null,
|
||||||
|
rows.map(r => r.file_id)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeEntry(srcFileEntry, options, cb) {
|
static removeEntry(srcFileEntry, options, cb) {
|
||||||
if(!_.isFunction(cb) && _.isFunction(options)) {
|
if (!_.isFunction(cb) && _.isFunction(options)) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
@@ -662,21 +725,21 @@ module.exports = class FileEntry {
|
|||||||
fileDb.run(
|
fileDb.run(
|
||||||
`DELETE FROM file
|
`DELETE FROM file
|
||||||
WHERE file_id = ?;`,
|
WHERE file_id = ?;`,
|
||||||
[ srcFileEntry.fileId ],
|
[srcFileEntry.fileId],
|
||||||
err => {
|
err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function optionallyRemovePhysicalFile(callback) {
|
function optionallyRemovePhysicalFile(callback) {
|
||||||
if(true !== options.removePhysFile) {
|
if (true !== options.removePhysFile) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
unlink(srcFileEntry.filePath, err => {
|
unlink(srcFileEntry.filePath, err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -685,25 +748,25 @@ module.exports = class FileEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) {
|
static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) {
|
||||||
if(!cb && _.isFunction(destFileName)) {
|
if (!cb && _.isFunction(destFileName)) {
|
||||||
cb = destFileName;
|
cb = destFileName;
|
||||||
destFileName = srcFileEntry.fileName;
|
destFileName = srcFileEntry.fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const srcPath = srcFileEntry.filePath;
|
const srcPath = srcFileEntry.filePath;
|
||||||
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
|
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
|
||||||
|
|
||||||
if(!dstDir) {
|
if (!dstDir) {
|
||||||
return cb(Errors.Invalid('Invalid storage tag'));
|
return cb(Errors.Invalid('Invalid storage tag'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const dstPath = paths.join(dstDir, destFileName);
|
const dstPath = paths.join(dstDir, destFileName);
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function movePhysFile(callback) {
|
function movePhysFile(callback) {
|
||||||
if(srcPath === dstPath) {
|
if (srcPath === dstPath) {
|
||||||
return callback(null); // don't need to move file, but may change areas
|
return callback(null); // don't need to move file, but may change areas
|
||||||
}
|
}
|
||||||
|
|
||||||
fse.move(srcPath, dstPath, err => {
|
fse.move(srcPath, dstPath, err => {
|
||||||
@@ -715,12 +778,12 @@ module.exports = class FileEntry {
|
|||||||
`UPDATE file
|
`UPDATE file
|
||||||
SET area_tag = ?, file_name = ?, storage_tag = ?
|
SET area_tag = ?, file_name = ?, storage_tag = ?
|
||||||
WHERE file_id = ?;`,
|
WHERE file_id = ?;`,
|
||||||
[ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ],
|
[destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId],
|
||||||
err => {
|
err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -735,7 +798,7 @@ module.exports = class FileEntry {
|
|||||||
// No wildcards?
|
// No wildcards?
|
||||||
const hasSingleCharWC = terms.indexOf('?') > -1;
|
const hasSingleCharWC = terms.indexOf('?') > -1;
|
||||||
if (terms.indexOf('*') === -1 && !hasSingleCharWC) {
|
if (terms.indexOf('*') === -1 && !hasSingleCharWC) {
|
||||||
return [ terms, 'fts_match' ];
|
return [terms, 'fts_match'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepareLike = () => {
|
const prepareLike = () => {
|
||||||
@@ -746,7 +809,7 @@ module.exports = class FileEntry {
|
|||||||
|
|
||||||
// Any ? wildcards?
|
// Any ? wildcards?
|
||||||
if (hasSingleCharWC) {
|
if (hasSingleCharWC) {
|
||||||
return [ prepareLike(terms), 'like' ];
|
return [prepareLike(terms), 'like'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const split = terms.replace(/\s+/g, ' ').split(' ');
|
const split = terms.replace(/\s+/g, ' ').split(' ');
|
||||||
@@ -764,9 +827,9 @@ module.exports = class FileEntry {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (useLike) {
|
if (useLike) {
|
||||||
return [ prepareLike(terms), 'like' ];
|
return [prepareLike(terms), 'like'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ terms, 'fts_match' ];
|
return [terms, 'fts_match'];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,30 +2,30 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const pty = require('node-pty');
|
const pty = require('node-pty');
|
||||||
const temptmp = require('temptmp').createTrackedSession('transfer_file');
|
const temptmp = require('temptmp').createTrackedSession('transfer_file');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
|
|
||||||
// some consts
|
// some consts
|
||||||
const SYSTEM_EOL = require('os').EOL;
|
const SYSTEM_EOL = require('os').EOL;
|
||||||
const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
|
const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Notes
|
Notes
|
||||||
@@ -44,9 +44,9 @@ const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Transfer file',
|
name: 'Transfer file',
|
||||||
desc : 'Sends or receives a file(s)',
|
desc: 'Sends or receives a file(s)',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class TransferFileModule extends MenuModule {
|
exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
@@ -59,56 +59,58 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
// Most options can be set via extraArgs or config block
|
// Most options can be set via extraArgs or config block
|
||||||
//
|
//
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(options.extraArgs) {
|
if (options.extraArgs) {
|
||||||
if(options.extraArgs.protocol) {
|
if (options.extraArgs.protocol) {
|
||||||
this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol];
|
this.protocolConfig =
|
||||||
|
config.fileTransferProtocols[options.extraArgs.protocol];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.extraArgs.direction) {
|
if (options.extraArgs.direction) {
|
||||||
this.direction = options.extraArgs.direction;
|
this.direction = options.extraArgs.direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.extraArgs.sendQueue) {
|
if (options.extraArgs.sendQueue) {
|
||||||
this.sendQueue = options.extraArgs.sendQueue;
|
this.sendQueue = options.extraArgs.sendQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.extraArgs.recvFileName) {
|
if (options.extraArgs.recvFileName) {
|
||||||
this.recvFileName = options.extraArgs.recvFileName;
|
this.recvFileName = options.extraArgs.recvFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.extraArgs.recvDirectory) {
|
if (options.extraArgs.recvDirectory) {
|
||||||
this.recvDirectory = options.extraArgs.recvDirectory;
|
this.recvDirectory = options.extraArgs.recvDirectory;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(this.config.protocol) {
|
if (this.config.protocol) {
|
||||||
this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
|
this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.config.direction) {
|
if (this.config.direction) {
|
||||||
this.direction = this.config.direction;
|
this.direction = this.config.direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.config.sendQueue) {
|
if (this.config.sendQueue) {
|
||||||
this.sendQueue = this.config.sendQueue;
|
this.sendQueue = this.config.sendQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.config.recvFileName) {
|
if (this.config.recvFileName) {
|
||||||
this.recvFileName = this.config.recvFileName;
|
this.recvFileName = this.config.recvFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.config.recvDirectory) {
|
if (this.config.recvDirectory) {
|
||||||
this.recvDirectory = this.config.recvDirectory;
|
this.recvDirectory = this.config.recvDirectory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
|
this.protocolConfig =
|
||||||
this.direction = this.direction || 'send';
|
this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
|
||||||
this.sendQueue = this.sendQueue || [];
|
this.direction = this.direction || 'send';
|
||||||
|
this.sendQueue = this.sendQueue || [];
|
||||||
|
|
||||||
// Ensure sendQueue is an array of objects that contain at least a 'path' member
|
// Ensure sendQueue is an array of objects that contain at least a 'path' member
|
||||||
this.sendQueue = this.sendQueue.map(item => {
|
this.sendQueue = this.sendQueue.map(item => {
|
||||||
if(_.isString(item)) {
|
if (_.isString(item)) {
|
||||||
return { path : item };
|
return { path: item };
|
||||||
} else {
|
} else {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -118,11 +120,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSending() {
|
isSending() {
|
||||||
return ('send' === this.direction);
|
return 'send' === this.direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
restorePipeAfterExternalProc() {
|
restorePipeAfterExternalProc() {
|
||||||
if(!this.pipeRestored) {
|
if (!this.pipeRestored) {
|
||||||
this.pipeRestored = true;
|
this.pipeRestored = true;
|
||||||
|
|
||||||
this.client.restoreDataHandler();
|
this.client.restoreDataHandler();
|
||||||
@@ -134,14 +136,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
// :TODO: Look into this further
|
// :TODO: Look into this further
|
||||||
const allFiles = this.sendQueue.map(f => f.path);
|
const allFiles = this.sendQueue.map(f => f.path);
|
||||||
this.executeExternalProtocolHandlerForSend(allFiles, err => {
|
this.executeExternalProtocolHandlerForSend(allFiles, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
|
this.client.log.warn(
|
||||||
|
{ files: allFiles, error: err.message },
|
||||||
|
'Error sending file(s)'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const sentFiles = [];
|
const sentFiles = [];
|
||||||
this.sendQueue.forEach(f => {
|
this.sendQueue.forEach(f => {
|
||||||
f.sent = true;
|
f.sent = true;
|
||||||
sentFiles.push(f.path);
|
sentFiles.push(f.path);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.log.info( { sentFiles : sentFiles }, `User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)` );
|
this.client.log.info( { sentFiles : sentFiles }, `User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)` );
|
||||||
@@ -196,29 +200,32 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
|
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
|
||||||
// in the case of collisions.
|
// in the case of collisions.
|
||||||
//
|
//
|
||||||
const dstPath = paths.dirname(dst);
|
const dstPath = paths.dirname(dst);
|
||||||
const dstFileExt = paths.extname(dst);
|
const dstFileExt = paths.extname(dst);
|
||||||
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
||||||
|
|
||||||
let renameIndex = 0;
|
let renameIndex = 0;
|
||||||
let movedOk = false;
|
let movedOk = false;
|
||||||
let tryDstPath;
|
let tryDstPath;
|
||||||
|
|
||||||
async.until(
|
async.until(
|
||||||
(callback) => callback(null, movedOk), // until moved OK
|
callback => callback(null, movedOk), // until moved OK
|
||||||
(cb) => {
|
cb => {
|
||||||
if(0 === renameIndex) {
|
if (0 === renameIndex) {
|
||||||
// try originally supplied path first
|
// try originally supplied path first
|
||||||
tryDstPath = dst;
|
tryDstPath = dst;
|
||||||
} else {
|
} else {
|
||||||
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
|
tryDstPath = paths.join(
|
||||||
|
dstPath,
|
||||||
|
`${dstFileSuffix}(${renameIndex})${dstFileExt}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fse.move(src, tryDstPath, err => {
|
fse.move(src, tryDstPath, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
if('EEXIST' === err.code) {
|
if ('EEXIST' === err.code) {
|
||||||
renameIndex += 1;
|
renameIndex += 1;
|
||||||
return cb(null); // keep trying
|
return cb(null); // keep trying
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -236,25 +243,27 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
|
|
||||||
recvFiles(cb) {
|
recvFiles(cb) {
|
||||||
this.executeExternalProtocolHandlerForRecv(err => {
|
this.executeExternalProtocolHandlerForRecv(err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recvFilePaths = [];
|
this.recvFilePaths = [];
|
||||||
|
|
||||||
if(this.recvFileName) {
|
if (this.recvFileName) {
|
||||||
//
|
//
|
||||||
// file name specified - we expect a single file in |this.recvDirectory|
|
// file name specified - we expect a single file in |this.recvDirectory|
|
||||||
// by the name of |this.recvFileName|
|
// by the name of |this.recvFileName|
|
||||||
//
|
//
|
||||||
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
|
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
|
||||||
fs.stat(recvFullPath, (err, stats) => {
|
fs.stat(recvFullPath, (err, stats) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!stats.isFile()) {
|
if (!stats.isFile()) {
|
||||||
return cb(Errors.Invalid('Expected file entry in recv directory'));
|
return cb(
|
||||||
|
Errors.Invalid('Expected file entry in recv directory')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recvFilePaths.push(recvFullPath);
|
this.recvFilePaths.push(recvFullPath);
|
||||||
@@ -265,83 +274,96 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
|
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
|
||||||
//
|
//
|
||||||
fs.readdir(this.recvDirectory, (err, files) => {
|
fs.readdir(this.recvDirectory, (err, files) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// stat each to grab files only
|
// stat each to grab files only
|
||||||
async.each(files, (fileName, nextFile) => {
|
async.each(
|
||||||
const recvFullPath = paths.join(this.recvDirectory, fileName);
|
files,
|
||||||
|
(fileName, nextFile) => {
|
||||||
|
const recvFullPath = paths.join(this.recvDirectory, fileName);
|
||||||
|
|
||||||
fs.stat(recvFullPath, (err, stats) => {
|
fs.stat(recvFullPath, (err, stats) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
this.client.log.warn('Failed to stat file', { path : recvFullPath } );
|
this.client.log.warn('Failed to stat file', {
|
||||||
return nextFile(null); // just try the next one
|
path: recvFullPath,
|
||||||
}
|
});
|
||||||
|
return nextFile(null); // just try the next one
|
||||||
|
}
|
||||||
|
|
||||||
if(stats.isFile()) {
|
if (stats.isFile()) {
|
||||||
this.recvFilePaths.push(recvFullPath);
|
this.recvFilePaths.push(recvFullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextFile(null);
|
return nextFile(null);
|
||||||
});
|
});
|
||||||
}, () => {
|
},
|
||||||
return cb(null);
|
() => {
|
||||||
});
|
return cb(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pathWithTerminatingSeparator(path) {
|
pathWithTerminatingSeparator(path) {
|
||||||
if(path && paths.sep !== path.charAt(path.length - 1)) {
|
if (path && paths.sep !== path.charAt(path.length - 1)) {
|
||||||
path = path + paths.sep;
|
path = path + paths.sep;
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepAndBuildSendArgs(filePaths, cb) {
|
prepAndBuildSendArgs(filePaths, cb) {
|
||||||
const externalArgs = this.protocolConfig.external['sendArgs'];
|
const externalArgs = this.protocolConfig.external['sendArgs'];
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function getTempFileListPath(callback) {
|
function getTempFileListPath(callback) {
|
||||||
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) );
|
const hasFileList = externalArgs.find(
|
||||||
if(!hasFileList) {
|
ea => ea.indexOf('{fileListPath}') > -1
|
||||||
|
);
|
||||||
|
if (!hasFileList) {
|
||||||
return callback(null, null);
|
return callback(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => {
|
temptmp.open(
|
||||||
if(err) {
|
{ prefix: TEMP_SUFFIX, suffix: '.txt' },
|
||||||
return callback(err); // failed to create it
|
(err, tempFileInfo) => {
|
||||||
}
|
if (err) {
|
||||||
|
return callback(err); // failed to create it
|
||||||
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
}
|
||||||
fs.close(tempFileInfo.fd, err => {
|
|
||||||
return callback(err, tempFileInfo.path);
|
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
fs.close(tempFileInfo.fd, err => {
|
||||||
|
return callback(err, tempFileInfo.path);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
function createArgs(tempFileListPath, callback) {
|
function createArgs(tempFileListPath, callback) {
|
||||||
// initial args: ignore {filePaths} as we must break that into it's own sep array items
|
// initial args: ignore {filePaths} as we must break that into it's own sep array items
|
||||||
const args = externalArgs.map(arg => {
|
const args = externalArgs.map(arg => {
|
||||||
return '{filePaths}' === arg ? arg : stringFormat(arg, {
|
return '{filePaths}' === arg
|
||||||
fileListPath : tempFileListPath || '',
|
? arg
|
||||||
});
|
: stringFormat(arg, {
|
||||||
|
fileListPath: tempFileListPath || '',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const filePathsPos = args.indexOf('{filePaths}');
|
const filePathsPos = args.indexOf('{filePaths}');
|
||||||
if(filePathsPos > -1) {
|
if (filePathsPos > -1) {
|
||||||
// replace {filePaths} with 0:n individual entries in |args|
|
// replace {filePaths} with 0:n individual entries in |args|
|
||||||
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
|
args.splice.apply(args, [filePathsPos, 1].concat(filePaths));
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null, args);
|
return callback(null, args);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
(err, args) => {
|
(err, args) => {
|
||||||
return cb(err, args);
|
return cb(err, args);
|
||||||
@@ -350,47 +372,52 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepAndBuildRecvArgs(cb) {
|
prepAndBuildRecvArgs(cb) {
|
||||||
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
|
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
|
||||||
const externalArgs = this.protocolConfig.external[argsKey];
|
const externalArgs = this.protocolConfig.external[argsKey];
|
||||||
const args = externalArgs.map(arg => stringFormat(arg, {
|
const args = externalArgs.map(arg =>
|
||||||
uploadDir : this.recvDirectory,
|
stringFormat(arg, {
|
||||||
fileName : this.recvFileName || '',
|
uploadDir: this.recvDirectory,
|
||||||
}));
|
fileName: this.recvFileName || '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return cb(null, args);
|
return cb(null, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
executeExternalProtocolHandler(args, cb) {
|
executeExternalProtocolHandler(args, cb) {
|
||||||
const external = this.protocolConfig.external;
|
const external = this.protocolConfig.external;
|
||||||
const cmd = external[`${this.direction}Cmd`];
|
const cmd = external[`${this.direction}Cmd`];
|
||||||
|
|
||||||
// support for handlers that need IACs taken care of over Telnet/etc.
|
// support for handlers that need IACs taken care of over Telnet/etc.
|
||||||
const processIACs =
|
const processIACs = external.processIACs || external.escapeTelnet; // deprecated name
|
||||||
external.processIACs ||
|
|
||||||
external.escapeTelnet; // deprecated name
|
|
||||||
|
|
||||||
// :TODO: we should only do this when over Telnet (or derived, such as WebSockets)?
|
// :TODO: we should only do this when over Telnet (or derived, such as WebSockets)?
|
||||||
|
|
||||||
const IAC = Buffer.from([255]);
|
const IAC = Buffer.from([255]);
|
||||||
const EscapedIAC = Buffer.from([255, 255]);
|
const EscapedIAC = Buffer.from([255, 255]);
|
||||||
|
|
||||||
this.client.log.debug(
|
this.client.log.debug(
|
||||||
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
|
{
|
||||||
|
cmd: cmd,
|
||||||
|
args: args,
|
||||||
|
tempDir: this.recvDirectory,
|
||||||
|
direction: this.direction,
|
||||||
|
},
|
||||||
'Executing external protocol'
|
'Executing external protocol'
|
||||||
);
|
);
|
||||||
|
|
||||||
const spawnOpts = {
|
const spawnOpts = {
|
||||||
cols : this.client.term.termWidth,
|
cols: this.client.term.termWidth,
|
||||||
rows : this.client.term.termHeight,
|
rows: this.client.term.termHeight,
|
||||||
cwd : this.recvDirectory,
|
cwd: this.recvDirectory,
|
||||||
encoding : null, // don't bork our data!
|
encoding: null, // don't bork our data!
|
||||||
};
|
};
|
||||||
|
|
||||||
const externalProc = pty.spawn(cmd, args, spawnOpts);
|
const externalProc = pty.spawn(cmd, args, spawnOpts);
|
||||||
|
|
||||||
let dataHits = 0;
|
let dataHits = 0;
|
||||||
const updateActivity = () => {
|
const updateActivity = () => {
|
||||||
if (0 === (dataHits++ % 4)) {
|
if (0 === dataHits++ % 4) {
|
||||||
this.client.explicitActivityTimeUpdate();
|
this.client.explicitActivityTimeUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -399,7 +426,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
updateActivity();
|
updateActivity();
|
||||||
|
|
||||||
// needed for things like sz/rz
|
// needed for things like sz/rz
|
||||||
if(processIACs) {
|
if (processIACs) {
|
||||||
let iacPos = data.indexOf(EscapedIAC);
|
let iacPos = data.indexOf(EscapedIAC);
|
||||||
if (-1 === iacPos) {
|
if (-1 === iacPos) {
|
||||||
return externalProc.write(data);
|
return externalProc.write(data);
|
||||||
@@ -430,7 +457,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
updateActivity();
|
updateActivity();
|
||||||
|
|
||||||
// needed for things like sz/rz
|
// needed for things like sz/rz
|
||||||
if(processIACs) {
|
if (processIACs) {
|
||||||
let iacPos = data.indexOf(IAC);
|
let iacPos = data.indexOf(IAC);
|
||||||
if (-1 === iacPos) {
|
if (-1 === iacPos) {
|
||||||
return this.client.term.rawWrite(data);
|
return this.client.term.rawWrite(data);
|
||||||
@@ -459,23 +486,33 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
return this.restorePipeAfterExternalProc();
|
return this.restorePipeAfterExternalProc();
|
||||||
});
|
});
|
||||||
|
|
||||||
externalProc.once('exit', (exitCode) => {
|
externalProc.once('exit', exitCode => {
|
||||||
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
|
this.client.log.debug(
|
||||||
|
{ cmd: cmd, args: args, exitCode: exitCode },
|
||||||
|
'Process exited'
|
||||||
|
);
|
||||||
|
|
||||||
this.restorePipeAfterExternalProc();
|
this.restorePipeAfterExternalProc();
|
||||||
externalProc.removeAllListeners();
|
externalProc.removeAllListeners();
|
||||||
|
|
||||||
return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null);
|
return cb(
|
||||||
|
exitCode
|
||||||
|
? Errors.ExternalProcess(
|
||||||
|
`Process exited with exit code ${exitCode}`,
|
||||||
|
'EBADEXIT'
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
executeExternalProtocolHandlerForSend(filePaths, cb) {
|
executeExternalProtocolHandlerForSend(filePaths, cb) {
|
||||||
if(!Array.isArray(filePaths)) {
|
if (!Array.isArray(filePaths)) {
|
||||||
filePaths = [ filePaths ];
|
filePaths = [filePaths];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prepAndBuildSendArgs(filePaths, (err, args) => {
|
this.prepAndBuildSendArgs(filePaths, (err, args) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +523,8 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
executeExternalProtocolHandlerForRecv(cb) {
|
executeExternalProtocolHandlerForRecv(cb) {
|
||||||
this.prepAndBuildRecvArgs( (err, args) => {
|
this.prepAndBuildRecvArgs((err, args) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,91 +535,115 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMenuResult() {
|
getMenuResult() {
|
||||||
if(this.isSending()) {
|
if (this.isSending()) {
|
||||||
return { sentFileIds : this.sentFileIds };
|
return { sentFileIds: this.sentFileIds };
|
||||||
} else {
|
} else {
|
||||||
return { recvFilePaths : this.recvFilePaths };
|
return { recvFilePaths: this.recvFilePaths };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSendStats(cb) {
|
updateSendStats(cb) {
|
||||||
let downloadBytes = 0;
|
let downloadBytes = 0;
|
||||||
let downloadCount = 0;
|
let downloadCount = 0;
|
||||||
let fileIds = [];
|
let fileIds = [];
|
||||||
|
|
||||||
async.each(this.sendQueue, (queueItem, next) => {
|
async.each(
|
||||||
if(!queueItem.sent) {
|
this.sendQueue,
|
||||||
return next(null);
|
(queueItem, next) => {
|
||||||
}
|
if (!queueItem.sent) {
|
||||||
|
return next(null);
|
||||||
if(queueItem.fileId) {
|
|
||||||
fileIds.push(queueItem.fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_.isNumber(queueItem.byteSize)) {
|
|
||||||
downloadCount += 1;
|
|
||||||
downloadBytes += queueItem.byteSize;
|
|
||||||
return next(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we just have a path - figure it out
|
|
||||||
fs.stat(queueItem.path, (err, stats) => {
|
|
||||||
if(err) {
|
|
||||||
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
|
|
||||||
} else {
|
|
||||||
downloadCount += 1;
|
|
||||||
downloadBytes += stats.size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(null);
|
if (queueItem.fileId) {
|
||||||
});
|
fileIds.push(queueItem.fileId);
|
||||||
}, () => {
|
}
|
||||||
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
|
|
||||||
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalCount, downloadCount);
|
|
||||||
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalBytes, downloadBytes);
|
|
||||||
|
|
||||||
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount);
|
if (_.isNumber(queueItem.byteSize)) {
|
||||||
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes);
|
downloadCount += 1;
|
||||||
|
downloadBytes += queueItem.byteSize;
|
||||||
|
return next(null);
|
||||||
|
}
|
||||||
|
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, downloadCount);
|
// we just have a path - figure it out
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, downloadBytes);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
fileIds.forEach(fileId => {
|
return next(null);
|
||||||
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
|
});
|
||||||
});
|
},
|
||||||
|
() => {
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
return cb(null);
|
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount);
|
||||||
});
|
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes);
|
||||||
|
|
||||||
|
fileIds.forEach(fileId => {
|
||||||
|
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRecvStats(cb) {
|
updateRecvStats(cb) {
|
||||||
let uploadBytes = 0;
|
let uploadBytes = 0;
|
||||||
let uploadCount = 0;
|
let uploadCount = 0;
|
||||||
|
|
||||||
async.each(this.recvFilePaths, (filePath, next) => {
|
async.each(
|
||||||
// we just have a path - figure it out
|
this.recvFilePaths,
|
||||||
fs.stat(filePath, (err, stats) => {
|
(filePath, next) => {
|
||||||
if(err) {
|
// we just have a path - figure it out
|
||||||
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
|
fs.stat(filePath, (err, stats) => {
|
||||||
} else {
|
if (err) {
|
||||||
uploadCount += 1;
|
this.client.log.warn(
|
||||||
uploadBytes += stats.size;
|
{ error: err.message, path: filePath },
|
||||||
}
|
'File stat failed'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
uploadCount += 1;
|
||||||
|
uploadBytes += stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
return next(null);
|
return next(null);
|
||||||
});
|
});
|
||||||
}, () => {
|
},
|
||||||
StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalCount, uploadCount);
|
() => {
|
||||||
StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalBytes, uploadBytes);
|
StatLog.incrementUserStat(
|
||||||
|
this.client.user,
|
||||||
|
UserProps.FileUlTotalCount,
|
||||||
|
uploadCount
|
||||||
|
);
|
||||||
|
StatLog.incrementUserStat(
|
||||||
|
this.client.user,
|
||||||
|
UserProps.FileUlTotalBytes,
|
||||||
|
uploadBytes
|
||||||
|
);
|
||||||
|
|
||||||
StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount);
|
StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount);
|
||||||
StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes);
|
StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes);
|
||||||
|
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileUlTodayCount, uploadCount);
|
return cb(null);
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileUlTodayBytes, uploadBytes);
|
}
|
||||||
|
);
|
||||||
return cb(null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
@@ -593,41 +654,38 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
if(self.isSending()) {
|
if (self.isSending()) {
|
||||||
if(!Array.isArray(self.sendQueue)) {
|
if (!Array.isArray(self.sendQueue)) {
|
||||||
self.sendQueue = [ self.sendQueue ];
|
self.sendQueue = [self.sendQueue];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function transferFiles(callback) {
|
function transferFiles(callback) {
|
||||||
if(self.isSending()) {
|
if (self.isSending()) {
|
||||||
self.sendFiles( err => {
|
self.sendFiles(err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentFileIds = [];
|
const sentFileIds = [];
|
||||||
self.sendQueue.forEach(queueItem => {
|
self.sendQueue.forEach(queueItem => {
|
||||||
if(queueItem.sent && queueItem.fileId) {
|
if (queueItem.sent && queueItem.fileId) {
|
||||||
sentFileIds.push(queueItem.fileId);
|
sentFileIds.push(queueItem.fileId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(sentFileIds.length > 0) {
|
if (sentFileIds.length > 0) {
|
||||||
// remove items we sent from the D/L queue
|
// remove items we sent from the D/L queue
|
||||||
const dlQueue = new DownloadQueue(self.client);
|
const dlQueue = new DownloadQueue(self.client);
|
||||||
const dlFileEntries = dlQueue.removeItems(sentFileIds);
|
const dlFileEntries = dlQueue.removeItems(sentFileIds);
|
||||||
|
|
||||||
// fire event for downloaded entries
|
// fire event for downloaded entries
|
||||||
Events.emit(
|
Events.emit(Events.getSystemEvents().UserDownload, {
|
||||||
Events.getSystemEvents().UserDownload,
|
user: self.client.user,
|
||||||
{
|
files: dlFileEntries,
|
||||||
user : self.client.user,
|
});
|
||||||
files : dlFileEntries
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
self.sentFileIds = sentFileIds;
|
self.sentFileIds = sentFileIds;
|
||||||
}
|
}
|
||||||
@@ -635,29 +693,32 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||||||
return callback(null);
|
return callback(null);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.recvFiles( err => {
|
self.recvFiles(err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function cleanupTempFiles(callback) {
|
function cleanupTempFiles(callback) {
|
||||||
temptmp.cleanup( paths => {
|
temptmp.cleanup(paths => {
|
||||||
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' );
|
Log.debug(
|
||||||
|
{ paths: paths, sessionId: temptmp.sessionId },
|
||||||
|
'Temporary files cleaned up'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function updateUserAndSystemStats(callback) {
|
function updateUserAndSystemStats(callback) {
|
||||||
if(self.isSending()) {
|
if (self.isSending()) {
|
||||||
return self.updateSendStats(callback);
|
return self.updateSendStats(callback);
|
||||||
} else {
|
} else {
|
||||||
return self.updateRecvStats(callback);
|
return self.updateRecvStats(callback);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn( { error : err.message }, 'File transfer error');
|
self.client.log.warn({ error: err.message }, 'File transfer error');
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.prevMenu();
|
return self.prevMenu();
|
||||||
|
|||||||
@@ -2,84 +2,95 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File transfer protocol selection',
|
name: 'File transfer protocol selection',
|
||||||
desc : 'Select protocol / method for file transfer',
|
desc: 'Select protocol / method for file transfer',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
protList : 1,
|
protList: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.config = this.menuConfig.config || {};
|
this.config = this.menuConfig.config || {};
|
||||||
|
|
||||||
if(options.extraArgs) {
|
if (options.extraArgs) {
|
||||||
if(options.extraArgs.direction) {
|
if (options.extraArgs.direction) {
|
||||||
this.config.direction = options.extraArgs.direction;
|
this.config.direction = options.extraArgs.direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.config.direction = this.config.direction || 'send';
|
this.config.direction = this.config.direction || 'send';
|
||||||
|
|
||||||
this.extraArgs = options.extraArgs;
|
this.extraArgs = options.extraArgs;
|
||||||
|
|
||||||
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
if (_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
|
if (_.has(options, 'lastMenuResult.recvFilePaths')) {
|
||||||
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||||
|
|
||||||
this.loadAvailProtocols();
|
this.loadAvailProtocols();
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
selectProtocol : (formData, extraArgs, cb) => {
|
selectProtocol: (formData, extraArgs, cb) => {
|
||||||
const protocol = this.protocols[formData.value.protocol];
|
const protocol = this.protocols[formData.value.protocol];
|
||||||
const finalExtraArgs = this.extraArgs || {};
|
const finalExtraArgs = this.extraArgs || {};
|
||||||
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
|
Object.assign(
|
||||||
|
finalExtraArgs,
|
||||||
|
{ protocol: protocol.protocol, direction: this.config.direction },
|
||||||
|
extraArgs
|
||||||
|
);
|
||||||
|
|
||||||
const modOpts = {
|
const modOpts = {
|
||||||
extraArgs : finalExtraArgs,
|
extraArgs: finalExtraArgs,
|
||||||
};
|
};
|
||||||
|
|
||||||
if('send' === this.config.direction) {
|
if ('send' === this.config.direction) {
|
||||||
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb);
|
return this.gotoMenu(
|
||||||
|
this.config.downloadFilesMenu || 'sendFilesToUser',
|
||||||
|
modOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb);
|
return this.gotoMenu(
|
||||||
|
this.config.uploadFilesMenu || 'recvFilesFromUser',
|
||||||
|
modOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getMenuResult() {
|
getMenuResult() {
|
||||||
if(this.sentFileIds) {
|
if (this.sentFileIds) {
|
||||||
return { sentFileIds : this.sentFileIds };
|
return { sentFileIds: this.sentFileIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.recvFilePaths) {
|
if (this.recvFilePaths) {
|
||||||
return { recvFilePaths : this.recvFilePaths };
|
return { recvFilePaths: this.recvFilePaths };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
if(this.sentFileIds || this.recvFilePaths) {
|
if (this.sentFileIds || this.recvFilePaths) {
|
||||||
// nothing to do here; move along (we're just falling through)
|
// nothing to do here; move along (we're just falling through)
|
||||||
this.prevMenu();
|
this.prevMenu();
|
||||||
} else {
|
} else {
|
||||||
@@ -89,19 +100,21 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
const vc = (self.viewControllers.allViews = new ViewController({
|
||||||
|
client: self.client,
|
||||||
|
}));
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function loadFromConfig(callback) {
|
function loadFromConfig(callback) {
|
||||||
const loadOpts = {
|
const loadOpts = {
|
||||||
callingMenu : self,
|
callingMenu: self,
|
||||||
mciMap : mciData.menu
|
mciMap: mciData.menu,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
@@ -113,7 +126,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||||||
protListView.redraw();
|
protListView.redraw();
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -125,28 +138,32 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||||||
loadAvailProtocols() {
|
loadAvailProtocols() {
|
||||||
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
|
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
|
||||||
return {
|
return {
|
||||||
text : protInfo.name, // standard
|
text: protInfo.name, // standard
|
||||||
protocol : protocol,
|
protocol: protocol,
|
||||||
name : protInfo.name,
|
name: protInfo.name,
|
||||||
hasBatch : _.has(protInfo, 'external.recvArgs'),
|
hasBatch: _.has(protInfo, 'external.recvArgs'),
|
||||||
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
|
hasNonBatch: _.has(protInfo, 'external.recvArgsNonBatch'),
|
||||||
sort : protInfo.sort,
|
sort: protInfo.sort,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter out batch vs non-batch only protocols
|
// Filter out batch vs non-batch only protocols
|
||||||
if(this.extraArgs.recvFileName) { // non-batch aka non-blind
|
if (this.extraArgs.recvFileName) {
|
||||||
this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
|
// non-batch aka non-blind
|
||||||
|
this.protocols = this.protocols.filter(prot => prot.hasNonBatch);
|
||||||
} else {
|
} else {
|
||||||
this.protocols = this.protocols.filter( prot => prot.hasBatch );
|
this.protocols = this.protocols.filter(prot => prot.hasBatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// natural sort taking explicit orders into consideration
|
// natural sort taking explicit orders into consideration
|
||||||
this.protocols.sort( (a, b) => {
|
this.protocols.sort((a, b) => {
|
||||||
if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
|
if (_.isNumber(a.sort) && _.isNumber(b.sort)) {
|
||||||
return a.sort - b.sort;
|
return a.sort - b.sort;
|
||||||
} else {
|
} else {
|
||||||
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
|
return a.name.localeCompare(b.name, {
|
||||||
|
sensitivity: false,
|
||||||
|
numeric: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,58 +2,61 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const EnigAssert = require('./enigma_assert.js');
|
const EnigAssert = require('./enigma_assert.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
|
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
|
||||||
exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
|
exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
|
||||||
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
|
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
|
||||||
|
|
||||||
function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
|
function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
|
||||||
operation = operation || 'copy';
|
operation = operation || 'copy';
|
||||||
const dstPath = paths.dirname(dst);
|
const dstPath = paths.dirname(dst);
|
||||||
const dstFileExt = paths.extname(dst);
|
const dstFileExt = paths.extname(dst);
|
||||||
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
||||||
|
|
||||||
EnigAssert('move' === operation || 'copy' === operation);
|
EnigAssert('move' === operation || 'copy' === operation);
|
||||||
|
|
||||||
let renameIndex = 0;
|
let renameIndex = 0;
|
||||||
let opOk = false;
|
let opOk = false;
|
||||||
let tryDstPath;
|
let tryDstPath;
|
||||||
|
|
||||||
function tryOperation(src, dst, callback) {
|
function tryOperation(src, dst, callback) {
|
||||||
if('move' === operation) {
|
if ('move' === operation) {
|
||||||
fse.move(src, tryDstPath, err => {
|
fse.move(src, tryDstPath, err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
} else if('copy' === operation) {
|
} else if ('copy' === operation) {
|
||||||
fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => {
|
fse.copy(src, tryDstPath, { overwrite: false, errorOnExist: true }, err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async.until(
|
async.until(
|
||||||
(callback) => callback(null, opOk), // until moved OK
|
callback => callback(null, opOk), // until moved OK
|
||||||
(cb) => {
|
cb => {
|
||||||
if(0 === renameIndex) {
|
if (0 === renameIndex) {
|
||||||
// try originally supplied path first
|
// try originally supplied path first
|
||||||
tryDstPath = dst;
|
tryDstPath = dst;
|
||||||
} else {
|
} else {
|
||||||
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
|
tryDstPath = paths.join(
|
||||||
|
dstPath,
|
||||||
|
`${dstFileSuffix}(${renameIndex})${dstFileExt}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tryOperation(src, tryDstPath, err => {
|
tryOperation(src, tryDstPath, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
// for some reason fs-extra copy doesn't pass err.code
|
// for some reason fs-extra copy doesn't pass err.code
|
||||||
// :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST
|
// :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST
|
||||||
if('EEXIST' === err.code || 'dest already exists.' === err.message) {
|
if ('EEXIST' === err.code || 'dest already exists.' === err.message) {
|
||||||
renameIndex += 1;
|
renameIndex += 1;
|
||||||
return cb(null); // keep trying
|
return cb(null); // keep trying
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -82,7 +85,7 @@ function copyFileWithCollisionHandling(src, dst, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pathWithTerminatingSeparator(path) {
|
function pathWithTerminatingSeparator(path) {
|
||||||
if(path && paths.sep !== path.charAt(path.length - 1)) {
|
if (path && paths.sep !== path.charAt(path.length - 1)) {
|
||||||
path = path + paths.sep;
|
path = path + paths.sep;
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
// Descriptions found in the wild that mean "no description" /facepalm.
|
// Descriptions found in the wild that mean "no description" /facepalm.
|
||||||
const IgnoredDescriptions = [
|
const IgnoredDescriptions = [
|
||||||
@@ -25,14 +25,14 @@ module.exports = class FilesBBSFile {
|
|||||||
|
|
||||||
getDescription(fileName) {
|
getDescription(fileName) {
|
||||||
const entry = this.get(fileName);
|
const entry = this.get(fileName);
|
||||||
if(entry) {
|
if (entry) {
|
||||||
return entry.desc;
|
return entry.desc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static createFromFile(path, cb) {
|
static createFromFile(path, cb) {
|
||||||
fs.readFile(path, (err, descData) => {
|
fs.readFile(path, (err, descData) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ module.exports = class FilesBBSFile {
|
|||||||
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
|
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
|
||||||
const filesBbs = new FilesBBSFile();
|
const filesBbs = new FilesBBSFile();
|
||||||
|
|
||||||
const isBadDescription = (desc) => {
|
const isBadDescription = desc => {
|
||||||
return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false;
|
return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,9 +59,7 @@ module.exports = class FilesBBSFile {
|
|||||||
const detectDecoder = () => {
|
const detectDecoder = () => {
|
||||||
// helpers
|
// helpers
|
||||||
const regExpTestUpTo = (n, re) => {
|
const regExpTestUpTo = (n, re) => {
|
||||||
return lines
|
return lines.slice(0, n).some(l => re.test(l));
|
||||||
.slice(0, n)
|
|
||||||
.some(l => re.test(l));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -70,36 +68,37 @@ module.exports = class FilesBBSFile {
|
|||||||
const decoders = [
|
const decoders = [
|
||||||
{
|
{
|
||||||
// I've been told this is what Syncrhonet uses
|
// I've been told this is what Syncrhonet uses
|
||||||
lineRegExp : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/,
|
lineRegExp:
|
||||||
detect : function() {
|
/^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/,
|
||||||
|
detect: function () {
|
||||||
return regExpTestUpTo(10, this.lineRegExp);
|
return regExpTestUpTo(10, this.lineRegExp);
|
||||||
},
|
},
|
||||||
extract : function() {
|
extract: function () {
|
||||||
for(let i = 0; i < lines.length; ++i) {
|
for (let i = 0; i < lines.length; ++i) {
|
||||||
let line = lines[i];
|
let line = lines[i];
|
||||||
const hdr = line.match(this.lineRegExp);
|
const hdr = line.match(this.lineRegExp);
|
||||||
if(!hdr) {
|
if (!hdr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const long = [];
|
const long = [];
|
||||||
for(let j = i + 1; j < lines.length; ++j) {
|
for (let j = i + 1; j < lines.length; ++j) {
|
||||||
line = lines[j];
|
line = lines[j];
|
||||||
if(!line.startsWith(' ')) {
|
if (!line.startsWith(' ')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
long.push(line.trim());
|
long.push(line.trim());
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
const desc = long.join('\r\n') || hdr[3] || '';
|
const desc = long.join('\r\n') || hdr[3] || '';
|
||||||
const fileName = hdr[1];
|
const fileName = hdr[1];
|
||||||
const timestamp = moment(hdr[2], 'MM/DD/YY');
|
const timestamp = moment(hdr[2], 'MM/DD/YY');
|
||||||
|
|
||||||
if(isBadDescription(desc) || !timestamp.isValid()) {
|
if (isBadDescription(desc) || !timestamp.isValid()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
filesBbs.entries.set(fileName, { timestamp, desc } );
|
filesBbs.entries.set(fileName, { timestamp, desc });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -107,37 +106,41 @@ module.exports = class FilesBBSFile {
|
|||||||
// Examples:
|
// Examples:
|
||||||
// - Night Owl CD #7, 1992
|
// - Night Owl CD #7, 1992
|
||||||
//
|
//
|
||||||
lineRegExp : /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/,
|
lineRegExp: /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/,
|
||||||
detect : function() {
|
detect: function () {
|
||||||
return regExpTestUpTo(10, this.lineRegExp);
|
return regExpTestUpTo(10, this.lineRegExp);
|
||||||
},
|
},
|
||||||
extract : function() {
|
extract: function () {
|
||||||
for(let i = 0; i < lines.length; ++i) {
|
for (let i = 0; i < lines.length; ++i) {
|
||||||
let line = lines[i];
|
let line = lines[i];
|
||||||
const hdr = line.match(this.lineRegExp);
|
const hdr = line.match(this.lineRegExp);
|
||||||
if(!hdr) {
|
if (!hdr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const long = [ hdr[2].trim() ];
|
const long = [hdr[2].trim()];
|
||||||
for(let j = i + 1; j < lines.length; ++j) {
|
for (let j = i + 1; j < lines.length; ++j) {
|
||||||
line = lines[j];
|
line = lines[j];
|
||||||
// -------------------------------------------------v 32
|
// -------------------------------------------------v 32
|
||||||
if(!line.startsWith(' | ')) {
|
if (
|
||||||
|
!line.startsWith(
|
||||||
|
' | '
|
||||||
|
)
|
||||||
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
long.push(line.substr(33));
|
long.push(line.substr(33));
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
const desc = long.join('\r\n');
|
const desc = long.join('\r\n');
|
||||||
const fileName = hdr[1];
|
const fileName = hdr[1];
|
||||||
|
|
||||||
if(isBadDescription(desc)) {
|
if (isBadDescription(desc)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
filesBbs.entries.set(fileName, { desc } );
|
filesBbs.entries.set(fileName, { desc });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -148,36 +151,36 @@ module.exports = class FilesBBSFile {
|
|||||||
// Examples
|
// Examples
|
||||||
// - GUS archive @ dk.toastednet.org
|
// - GUS archive @ dk.toastednet.org
|
||||||
//
|
//
|
||||||
lineRegExp : /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/,
|
lineRegExp: /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/,
|
||||||
detect : function() {
|
detect: function () {
|
||||||
return regExpTestUpTo(10, this.lineRegExp);
|
return regExpTestUpTo(10, this.lineRegExp);
|
||||||
},
|
},
|
||||||
extract : function() {
|
extract: function () {
|
||||||
for(let i = 0; i < lines.length; ++i) {
|
for (let i = 0; i < lines.length; ++i) {
|
||||||
let line = lines[i];
|
let line = lines[i];
|
||||||
const hdr = line.match(this.lineRegExp);
|
const hdr = line.match(this.lineRegExp);
|
||||||
if(!hdr) {
|
if (!hdr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const long = [ hdr[2].trimRight() ];
|
const long = [hdr[2].trimRight()];
|
||||||
for(let j = i + 1; j < lines.length; ++j) {
|
for (let j = i + 1; j < lines.length; ++j) {
|
||||||
line = lines[j];
|
line = lines[j];
|
||||||
if(!line.startsWith('\t\t ')) {
|
if (!line.startsWith('\t\t ')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
long.push(line.substr(4));
|
long.push(line.substr(4));
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
const desc = long.join('\r\n');
|
const desc = long.join('\r\n');
|
||||||
const fileName = hdr[1];
|
const fileName = hdr[1];
|
||||||
|
|
||||||
if(isBadDescription(desc)) {
|
if (isBadDescription(desc)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
filesBbs.entries.set(fileName, { desc } );
|
filesBbs.entries.set(fileName, { desc });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -187,41 +190,46 @@ module.exports = class FilesBBSFile {
|
|||||||
// Examples:
|
// Examples:
|
||||||
// - Expanding Your BBS CD by David Wolfe, 1995
|
// - Expanding Your BBS CD by David Wolfe, 1995
|
||||||
//
|
//
|
||||||
lineRegExp : /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/,
|
lineRegExp:
|
||||||
detect : function() {
|
/^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/,
|
||||||
|
detect: function () {
|
||||||
return regExpTestUpTo(10, this.lineRegExp);
|
return regExpTestUpTo(10, this.lineRegExp);
|
||||||
},
|
},
|
||||||
extract : function() {
|
extract: function () {
|
||||||
for(let i = 0; i < lines.length; ++i) {
|
for (let i = 0; i < lines.length; ++i) {
|
||||||
let line = lines[i];
|
let line = lines[i];
|
||||||
const hdr = line.match(this.lineRegExp);
|
const hdr = line.match(this.lineRegExp);
|
||||||
if(!hdr) {
|
if (!hdr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstDescLine = hdr[4].trimRight();
|
const firstDescLine = hdr[4].trimRight();
|
||||||
const long = [ firstDescLine ];
|
const long = [firstDescLine];
|
||||||
for(let j = i + 1; j < lines.length; ++j) {
|
for (let j = i + 1; j < lines.length; ++j) {
|
||||||
line = lines[j];
|
line = lines[j];
|
||||||
if(!line.startsWith(' '.repeat(34))) {
|
if (!line.startsWith(' '.repeat(34))) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
long.push(line.substr(34).trimRight());
|
long.push(line.substr(34).trimRight());
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|
||||||
const desc = long.join('\r\n');
|
const desc = long.join('\r\n');
|
||||||
const fileName = hdr[1];
|
const fileName = hdr[1];
|
||||||
const size = parseInt(hdr[2]);
|
const size = parseInt(hdr[2]);
|
||||||
const timestamp = moment(hdr[3], 'MM-DD-YY');
|
const timestamp = moment(hdr[3], 'MM-DD-YY');
|
||||||
|
|
||||||
if(isBadDescription(desc) || isNaN(size) || !timestamp.isValid()) {
|
if (
|
||||||
|
isBadDescription(desc) ||
|
||||||
|
isNaN(size) ||
|
||||||
|
!timestamp.isValid()
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
filesBbs.entries.set(fileName, { desc, size, timestamp });
|
filesBbs.entries.set(fileName, { desc, size, timestamp });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -235,25 +243,25 @@ module.exports = class FilesBBSFile {
|
|||||||
//
|
//
|
||||||
// May contain headers, but we'll just skip 'em.
|
// May contain headers, but we'll just skip 'em.
|
||||||
//
|
//
|
||||||
lineRegExp : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/,
|
lineRegExp: /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/,
|
||||||
detect : function() {
|
detect: function () {
|
||||||
return regExpTestUpTo(10, this.lineRegExp);
|
return regExpTestUpTo(10, this.lineRegExp);
|
||||||
},
|
},
|
||||||
extract : function() {
|
extract: function () {
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
const hdr = line.match(this.lineRegExp);
|
const hdr = line.match(this.lineRegExp);
|
||||||
if(!hdr) {
|
if (!hdr) {
|
||||||
return; // forEach
|
return; // forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = hdr[1].trim();
|
const fileName = hdr[1].trim();
|
||||||
const desc = hdr[2].trim();
|
const desc = hdr[2].trim();
|
||||||
|
|
||||||
if(desc && !isBadDescription(desc)) {
|
if (desc && !isBadDescription(desc)) {
|
||||||
filesBbs.entries.set(fileName, { desc } );
|
filesBbs.entries.set(fileName, { desc });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -261,31 +269,32 @@ module.exports = class FilesBBSFile {
|
|||||||
// Examples:
|
// Examples:
|
||||||
// - AMINET CD's & similar
|
// - AMINET CD's & similar
|
||||||
//
|
//
|
||||||
lineRegExp : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/,
|
lineRegExp: /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/,
|
||||||
detect : function() {
|
detect: function () {
|
||||||
return regExpTestUpTo(10, this.lineRegExp);
|
return regExpTestUpTo(10, this.lineRegExp);
|
||||||
},
|
},
|
||||||
extract : function() {
|
extract: function () {
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
const hdr = line.match(this.tester);
|
const hdr = line.match(this.tester);
|
||||||
if(!hdr) {
|
if (!hdr) {
|
||||||
return; // forEach
|
return; // forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = hdr[1].trim();
|
const fileName = hdr[1].trim();
|
||||||
let size = parseInt(hdr[2]);
|
let size = parseInt(hdr[2]);
|
||||||
const desc = hdr[3].trim();
|
const desc = hdr[3].trim();
|
||||||
|
|
||||||
if(isNaN(size)) {
|
if (isNaN(size)) {
|
||||||
return; // forEach
|
return; // forEach
|
||||||
}
|
}
|
||||||
size *= 1024; // K->bytes.
|
size *= 1024; // K->bytes.
|
||||||
|
|
||||||
if(desc) { // omit empty entries
|
if (desc) {
|
||||||
filesBbs.entries.set(fileName, { size, desc } );
|
// omit empty entries
|
||||||
|
filesBbs.entries.set(fileName, { size, desc });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -294,18 +303,18 @@ module.exports = class FilesBBSFile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const decoder = detectDecoder();
|
const decoder = detectDecoder();
|
||||||
if(!decoder) {
|
if (!decoder) {
|
||||||
return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format'));
|
return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format'));
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder.extract(decoder);
|
decoder.extract(decoder);
|
||||||
|
|
||||||
return cb(
|
return cb(
|
||||||
filesBbs.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized FILES.BBS format'),
|
filesBbs.entries.size > 0
|
||||||
|
? null
|
||||||
|
: Errors.Invalid('Invalid or unrecognized FILES.BBS format'),
|
||||||
filesBbs
|
filesBbs
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,36 +3,39 @@
|
|||||||
|
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
// FNV-1a based on work here: https://github.com/wiedi/node-fnv
|
// FNV-1a based on work here: https://github.com/wiedi/node-fnv
|
||||||
module.exports = class FNV1a {
|
module.exports = class FNV1a {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
this.hash = 0x811c9dc5;
|
this.hash = 0x811c9dc5;
|
||||||
|
|
||||||
if(!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
this.update(data);
|
this.update(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(data) {
|
update(data) {
|
||||||
if(_.isNumber(data)) {
|
if (_.isNumber(data)) {
|
||||||
data = data.toString();
|
data = data.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(data)) {
|
if (_.isString(data)) {
|
||||||
data = Buffer.from(data);
|
data = Buffer.from(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Buffer.isBuffer(data)) {
|
if (!Buffer.isBuffer(data)) {
|
||||||
throw Errors.Invalid('data must be String or Buffer!');
|
throw Errors.Invalid('data must be String or Buffer!');
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let b of data) {
|
for (let b of data) {
|
||||||
this.hash = this.hash ^ b;
|
this.hash = this.hash ^ b;
|
||||||
this.hash +=
|
this.hash +=
|
||||||
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
|
(this.hash << 24) +
|
||||||
(this.hash << 4) + (this.hash << 1);
|
(this.hash << 8) +
|
||||||
|
(this.hash << 7) +
|
||||||
|
(this.hash << 4) +
|
||||||
|
(this.hash << 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -49,4 +52,3 @@ module.exports = class FNV1a {
|
|||||||
return this.hash & 0xffffffff;
|
return this.hash & 0xffffffff;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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 */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i;
|
const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i;
|
||||||
const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i;
|
const FTN_PATTERN_REGEXP =
|
||||||
|
/^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i;
|
||||||
|
|
||||||
module.exports = class Address {
|
module.exports = class Address {
|
||||||
constructor(addr) {
|
constructor(addr) {
|
||||||
if(addr) {
|
if (addr) {
|
||||||
if(_.isObject(addr)) {
|
if (_.isObject(addr)) {
|
||||||
Object.assign(this, addr);
|
Object.assign(this, addr);
|
||||||
} else if(_.isString(addr)) {
|
} else if (_.isString(addr)) {
|
||||||
const temp = Address.fromString(addr);
|
const temp = Address.fromString(addr);
|
||||||
if(temp) {
|
if (temp) {
|
||||||
Object.assign(this, temp);
|
Object.assign(this, temp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,7 @@ module.exports = class Address {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isEqual(other) {
|
isEqual(other) {
|
||||||
if(_.isString(other)) {
|
if (_.isString(other)) {
|
||||||
other = Address.fromString(other);
|
other = Address.fromString(other);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,46 +46,46 @@ module.exports = class Address {
|
|||||||
|
|
||||||
getMatchAddr(pattern) {
|
getMatchAddr(pattern) {
|
||||||
const m = FTN_PATTERN_REGEXP.exec(pattern);
|
const m = FTN_PATTERN_REGEXP.exec(pattern);
|
||||||
if(m) {
|
if (m) {
|
||||||
let addr = { };
|
let addr = {};
|
||||||
|
|
||||||
if(m[1]) {
|
if (m[1]) {
|
||||||
addr.zone = m[1].slice(0, -1);
|
addr.zone = m[1].slice(0, -1);
|
||||||
if('*' !== addr.zone) {
|
if ('*' !== addr.zone) {
|
||||||
addr.zone = parseInt(addr.zone);
|
addr.zone = parseInt(addr.zone);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addr.zone = '*';
|
addr.zone = '*';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(m[2]) {
|
if (m[2]) {
|
||||||
addr.net = m[2];
|
addr.net = m[2];
|
||||||
if('*' !== addr.net) {
|
if ('*' !== addr.net) {
|
||||||
addr.net = parseInt(addr.net);
|
addr.net = parseInt(addr.net);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addr.net = '*';
|
addr.net = '*';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(m[3]) {
|
if (m[3]) {
|
||||||
addr.node = m[3].substr(1);
|
addr.node = m[3].substr(1);
|
||||||
if('*' !== addr.node) {
|
if ('*' !== addr.node) {
|
||||||
addr.node = parseInt(addr.node);
|
addr.node = parseInt(addr.node);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addr.node = '*';
|
addr.node = '*';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(m[4]) {
|
if (m[4]) {
|
||||||
addr.point = m[4].substr(1);
|
addr.point = m[4].substr(1);
|
||||||
if('*' !== addr.point) {
|
if ('*' !== addr.point) {
|
||||||
addr.point = parseInt(addr.point);
|
addr.point = parseInt(addr.point);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addr.point = '*';
|
addr.point = '*';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(m[5]) {
|
if (m[5]) {
|
||||||
addr.domain = m[5].substr(1);
|
addr.domain = m[5].substr(1);
|
||||||
} else {
|
} else {
|
||||||
addr.domain = '*';
|
addr.domain = '*';
|
||||||
@@ -118,7 +119,7 @@ module.exports = class Address {
|
|||||||
|
|
||||||
isPatternMatch(pattern) {
|
isPatternMatch(pattern) {
|
||||||
const addr = this.getMatchAddr(pattern);
|
const addr = this.getMatchAddr(pattern);
|
||||||
if(addr) {
|
if (addr) {
|
||||||
return (
|
return (
|
||||||
('*' === addr.net || this.net === addr.net) &&
|
('*' === addr.net || this.net === addr.net) &&
|
||||||
('*' === addr.node || this.node === addr.node) &&
|
('*' === addr.node || this.node === addr.node) &&
|
||||||
@@ -134,25 +135,25 @@ module.exports = class Address {
|
|||||||
static fromString(addrStr) {
|
static fromString(addrStr) {
|
||||||
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
||||||
|
|
||||||
if(m) {
|
if (m) {
|
||||||
// start with a 2D
|
// start with a 2D
|
||||||
let addr = {
|
let addr = {
|
||||||
net : parseInt(m[2]),
|
net: parseInt(m[2]),
|
||||||
node : parseInt(m[3].substr(1)),
|
node: parseInt(m[3].substr(1)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3D: Addition of zone if present
|
// 3D: Addition of zone if present
|
||||||
if(m[1]) {
|
if (m[1]) {
|
||||||
addr.zone = parseInt(m[1].slice(0, -1));
|
addr.zone = parseInt(m[1].slice(0, -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4D if optional point is present
|
// 4D if optional point is present
|
||||||
if(m[4]) {
|
if (m[4]) {
|
||||||
addr.point = parseInt(m[4].substr(1));
|
addr.point = parseInt(m[4].substr(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5D with @domain
|
// 5D with @domain
|
||||||
if(m[5]) {
|
if (m[5]) {
|
||||||
addr.domain = m[5].substr(1);
|
addr.domain = m[5].substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,16 +169,16 @@ module.exports = class Address {
|
|||||||
// allow for e.g. '4D' or 5
|
// allow for e.g. '4D' or 5
|
||||||
const dim = parseInt(dimensions.toString()[0]);
|
const dim = parseInt(dimensions.toString()[0]);
|
||||||
|
|
||||||
if(dim >= 3) {
|
if (dim >= 3) {
|
||||||
addrStr += `/${this.node}`;
|
addrStr += `/${this.node}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// missing & .0 are equiv for point
|
// missing & .0 are equiv for point
|
||||||
if(dim >= 4 && this.point) {
|
if (dim >= 4 && this.point) {
|
||||||
addrStr += `.${this.point}`;
|
addrStr += `.${this.point}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(5 === dim && this.domain) {
|
if (5 === dim && this.domain) {
|
||||||
addrStr += `@${this.domain.toLowerCase()}`;
|
addrStr += `@${this.domain.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,19 +186,19 @@ module.exports = class Address {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getComparator() {
|
static getComparator() {
|
||||||
return function(left, right) {
|
return function (left, right) {
|
||||||
let c = (left.zone || 0) - (right.zone || 0);
|
let c = (left.zone || 0) - (right.zone || 0);
|
||||||
if(0 !== c) {
|
if (0 !== c) {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
c = (left.net || 0) - (right.net || 0);
|
c = (left.net || 0) - (right.net || 0);
|
||||||
if(0 !== c) {
|
if (0 !== c) {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
c = (left.node || 0) - (right.node || 0);
|
c = (left.node || 0) - (right.node || 0);
|
||||||
if(0 !== c) {
|
if (0 !== c) {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
222
core/ftn_util.js
222
core/ftn_util.js
@@ -1,40 +1,40 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const Address = require('./ftn_address.js');
|
const Address = require('./ftn_address.js');
|
||||||
const FNV1a = require('./fnv1a.js');
|
const FNV1a = require('./fnv1a.js');
|
||||||
const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion;
|
const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion;
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
|
||||||
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
||||||
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
||||||
exports.getMessageSerialNumber = getMessageSerialNumber;
|
exports.getMessageSerialNumber = getMessageSerialNumber;
|
||||||
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
||||||
exports.getDateTimeString = getDateTimeString;
|
exports.getDateTimeString = getDateTimeString;
|
||||||
|
|
||||||
exports.getMessageIdentifier = getMessageIdentifier;
|
exports.getMessageIdentifier = getMessageIdentifier;
|
||||||
exports.getProductIdentifier = getProductIdentifier;
|
exports.getProductIdentifier = getProductIdentifier;
|
||||||
exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
|
exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
|
||||||
exports.getOrigin = getOrigin;
|
exports.getOrigin = getOrigin;
|
||||||
exports.getTearLine = getTearLine;
|
exports.getTearLine = getTearLine;
|
||||||
exports.getVia = getVia;
|
exports.getVia = getVia;
|
||||||
exports.getIntl = getIntl;
|
exports.getIntl = getIntl;
|
||||||
exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList;
|
exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList;
|
||||||
exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList;
|
exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList;
|
||||||
exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries;
|
exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries;
|
||||||
exports.getUpdatedPathEntries = getUpdatedPathEntries;
|
exports.getUpdatedPathEntries = getUpdatedPathEntries;
|
||||||
|
|
||||||
exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding;
|
exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding;
|
||||||
exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier;
|
exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier;
|
||||||
|
|
||||||
exports.getQuotePrefix = getQuotePrefix;
|
exports.getQuotePrefix = getQuotePrefix;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Namespace for RFC-4122 name based UUIDs generated from
|
// Namespace for RFC-4122 name based UUIDs generated from
|
||||||
@@ -45,9 +45,9 @@ exports.getQuotePrefix = getQuotePrefix;
|
|||||||
// See list here: https://github.com/Mithgol/node-fidonet-jam
|
// See list here: https://github.com/Mithgol/node-fidonet-jam
|
||||||
|
|
||||||
function stringToNullPaddedBuffer(s, bufLen) {
|
function stringToNullPaddedBuffer(s, bufLen) {
|
||||||
let buffer = Buffer.alloc(bufLen);
|
let buffer = Buffer.alloc(bufLen);
|
||||||
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
|
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
|
||||||
for(let i = 0; i < enc.length; ++i) {
|
for (let i = 0; i < enc.length; ++i) {
|
||||||
buffer[i] = enc[i];
|
buffer[i] = enc[i];
|
||||||
}
|
}
|
||||||
return buffer;
|
return buffer;
|
||||||
@@ -65,7 +65,7 @@ function getDateFromFtnDateTime(dateTime) {
|
|||||||
// "27 Feb 15 00:00:03"
|
// "27 Feb 15 00:00:03"
|
||||||
//
|
//
|
||||||
// :TODO: Use moment.js here
|
// :TODO: Use moment.js here
|
||||||
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
|
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDateTimeString(m) {
|
function getDateTimeString(m) {
|
||||||
@@ -85,7 +85,7 @@ function getDateTimeString(m) {
|
|||||||
// MM = "00" | .. | "59"
|
// MM = "00" | .. | "59"
|
||||||
// SS = "00" | .. | "59"
|
// SS = "00" | .. | "59"
|
||||||
//
|
//
|
||||||
if(!moment.isMoment(m)) {
|
if (!moment.isMoment(m)) {
|
||||||
m = moment(m);
|
m = moment(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +93,8 @@ function getDateTimeString(m) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMessageSerialNumber(messageId) {
|
function getMessageSerialNumber(messageId) {
|
||||||
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
|
const msSinceEnigmaEpoc = Date.now() - Date.UTC(2016, 1, 1);
|
||||||
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
|
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
|
||||||
return `00000000${hash}`.substr(-8);
|
return `00000000${hash}`.substr(-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +143,13 @@ function getMessageSerialNumber(messageId) {
|
|||||||
//
|
//
|
||||||
function getMessageIdentifier(message, address, isNetMail = false) {
|
function getMessageIdentifier(message, address, isNetMail = false) {
|
||||||
const addrStr = new Address(address).toString('5D');
|
const addrStr = new Address(address).toString('5D');
|
||||||
return isNetMail ?
|
return isNetMail
|
||||||
`${addrStr} ${getMessageSerialNumber(message.messageId)}` :
|
? `${addrStr} ${getMessageSerialNumber(message.messageId)}`
|
||||||
`${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`
|
: `${
|
||||||
;
|
message.messageId
|
||||||
|
}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(
|
||||||
|
message.messageId
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -158,7 +161,7 @@ function getMessageIdentifier(message, address, isNetMail = false) {
|
|||||||
//
|
//
|
||||||
function getProductIdentifier() {
|
function getProductIdentifier() {
|
||||||
const version = getCleanEnigmaVersion();
|
const version = getCleanEnigmaVersion();
|
||||||
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||||
|
|
||||||
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||||
}
|
}
|
||||||
@@ -181,9 +184,12 @@ function getQuotePrefix(name) {
|
|||||||
let initials;
|
let initials;
|
||||||
|
|
||||||
const parts = name.split(' ');
|
const parts = name.split(' ');
|
||||||
if(parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
// First & Last initials - (Bryan Ashby -> BA)
|
// First & Last initials - (Bryan Ashby -> BA)
|
||||||
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase();
|
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)}`.toUpperCase();
|
||||||
} else {
|
} else {
|
||||||
// Just use the first two - (NuSkooler -> Nu)
|
// Just use the first two - (NuSkooler -> Nu)
|
||||||
initials = _.capitalize(name.slice(0, 2));
|
initials = _.capitalize(name.slice(0, 2));
|
||||||
@@ -198,17 +204,19 @@ function getQuotePrefix(name) {
|
|||||||
//
|
//
|
||||||
function getOrigin(address) {
|
function getOrigin(address) {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
const origin = _.has(config, 'messageNetworks.originLine') ?
|
const origin = _.has(config, 'messageNetworks.originLine')
|
||||||
config.messageNetworks.originLine :
|
? config.messageNetworks.originLine
|
||||||
config.general.boardName;
|
: config.general.boardName;
|
||||||
|
|
||||||
const addrStr = new Address(address).toString('5D');
|
const addrStr = new Address(address).toString('5D');
|
||||||
return ` * Origin: ${origin} (${addrStr})`;
|
return ` * Origin: ${origin} (${addrStr})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTearLine() {
|
function getTearLine() {
|
||||||
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||||
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
return `--- ENiGMA 1/2 v${
|
||||||
|
packageJson.version
|
||||||
|
} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -222,9 +230,9 @@ function getVia(address) {
|
|||||||
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
|
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
|
||||||
<Program Name> <Version> [Serial Number]<CR>
|
<Program Name> <Version> [Serial Number]<CR>
|
||||||
*/
|
*/
|
||||||
const addrStr = new Address(address).toString('5D');
|
const addrStr = new Address(address).toString('5D');
|
||||||
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
|
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
|
||||||
const version = getCleanEnigmaVersion();
|
const version = getCleanEnigmaVersion();
|
||||||
|
|
||||||
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
|
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
|
||||||
}
|
}
|
||||||
@@ -247,10 +255,10 @@ function getAbbreviatedNetNodeList(netNodes) {
|
|||||||
let abbrList = '';
|
let abbrList = '';
|
||||||
let currNet;
|
let currNet;
|
||||||
netNodes.forEach(netNode => {
|
netNodes.forEach(netNode => {
|
||||||
if(_.isString(netNode)) {
|
if (_.isString(netNode)) {
|
||||||
netNode = Address.fromString(netNode);
|
netNode = Address.fromString(netNode);
|
||||||
}
|
}
|
||||||
if(currNet !== netNode.net) {
|
if (currNet !== netNode.net) {
|
||||||
abbrList += `${netNode.net}/`;
|
abbrList += `${netNode.net}/`;
|
||||||
currNet = netNode.net;
|
currNet = netNode.net;
|
||||||
}
|
}
|
||||||
@@ -268,12 +276,12 @@ function parseAbbreviatedNetNodeList(netNodes) {
|
|||||||
let net;
|
let net;
|
||||||
let m;
|
let m;
|
||||||
let results = [];
|
let results = [];
|
||||||
while(null !== (m = re.exec(netNodes))) {
|
while (null !== (m = re.exec(netNodes))) {
|
||||||
if(m[1] && m[2]) {
|
if (m[1] && m[2]) {
|
||||||
net = parseInt(m[1]);
|
net = parseInt(m[1]);
|
||||||
results.push(new Address( { net : net, node : parseInt(m[2]) } ));
|
results.push(new Address({ net: net, node: parseInt(m[2]) }));
|
||||||
} else if(net) {
|
} else if (net) {
|
||||||
results.push(new Address( { net : net, node : parseInt(m[3]) } ));
|
results.push(new Address({ net: net, node: parseInt(m[3]) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,11 +324,11 @@ function getUpdatedSeenByEntries(existingEntries, additions) {
|
|||||||
programs."
|
programs."
|
||||||
*/
|
*/
|
||||||
existingEntries = existingEntries || [];
|
existingEntries = existingEntries || [];
|
||||||
if(!_.isArray(existingEntries)) {
|
if (!_.isArray(existingEntries)) {
|
||||||
existingEntries = [ existingEntries ];
|
existingEntries = [existingEntries];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_.isString(additions)) {
|
if (!_.isString(additions)) {
|
||||||
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
|
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,12 +346,13 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
|
|||||||
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
|
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
|
||||||
|
|
||||||
existingEntries = existingEntries || [];
|
existingEntries = existingEntries || [];
|
||||||
if(!_.isArray(existingEntries)) {
|
if (!_.isArray(existingEntries)) {
|
||||||
existingEntries = [ existingEntries ];
|
existingEntries = [existingEntries];
|
||||||
}
|
}
|
||||||
|
|
||||||
existingEntries.push(getAbbreviatedNetNodeList(
|
existingEntries.push(
|
||||||
parseAbbreviatedNetNodeList(localAddress)));
|
getAbbreviatedNetNodeList(parseAbbreviatedNetNodeList(localAddress))
|
||||||
|
);
|
||||||
|
|
||||||
return existingEntries;
|
return existingEntries;
|
||||||
}
|
}
|
||||||
@@ -354,69 +363,68 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
|
|||||||
//
|
//
|
||||||
const ENCODING_TO_FTS_5003_001_CHARS = {
|
const ENCODING_TO_FTS_5003_001_CHARS = {
|
||||||
// level 1 - generally should not be used
|
// level 1 - generally should not be used
|
||||||
ascii : [ 'ASCII', 1 ],
|
ascii: ['ASCII', 1],
|
||||||
'us-ascii' : [ 'ASCII', 1 ],
|
'us-ascii': ['ASCII', 1],
|
||||||
|
|
||||||
// level 2 - 8 bit, ASCII based
|
// level 2 - 8 bit, ASCII based
|
||||||
cp437 : [ 'CP437', 2 ],
|
cp437: ['CP437', 2],
|
||||||
cp850 : [ 'CP850', 2 ],
|
cp850: ['CP850', 2],
|
||||||
|
|
||||||
// level 3 - reserved
|
// level 3 - reserved
|
||||||
|
|
||||||
// level 4
|
// level 4
|
||||||
utf8 : [ 'UTF-8', 4 ],
|
utf8: ['UTF-8', 4],
|
||||||
'utf-8' : [ 'UTF-8', 4 ],
|
'utf-8': ['UTF-8', 4],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function getCharacterSetIdentifierByEncoding(encodingName) {
|
function getCharacterSetIdentifierByEncoding(encodingName) {
|
||||||
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
|
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
|
||||||
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
|
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHRSToEncodingTable = {
|
const CHRSToEncodingTable = {
|
||||||
Level1 : {
|
Level1: {
|
||||||
'ASCII' : 'ascii', // ISO-646-1
|
ASCII: 'ascii', // ISO-646-1
|
||||||
'DUTCH' : 'ascii', // ISO-646
|
DUTCH: 'ascii', // ISO-646
|
||||||
'FINNISH' : 'ascii', // ISO-646-10
|
FINNISH: 'ascii', // ISO-646-10
|
||||||
'FRENCH' : 'ascii', // ISO-646
|
FRENCH: 'ascii', // ISO-646
|
||||||
'CANADIAN' : 'ascii', // ISO-646
|
CANADIAN: 'ascii', // ISO-646
|
||||||
'GERMAN' : 'ascii', // ISO-646
|
GERMAN: 'ascii', // ISO-646
|
||||||
'ITALIAN' : 'ascii', // ISO-646
|
ITALIAN: 'ascii', // ISO-646
|
||||||
'NORWEIG' : 'ascii', // ISO-646
|
NORWEIG: 'ascii', // ISO-646
|
||||||
'PORTU' : 'ascii', // ISO-646
|
PORTU: 'ascii', // ISO-646
|
||||||
'SPANISH' : 'iso-656',
|
SPANISH: 'iso-656',
|
||||||
'SWEDISH' : 'ascii', // ISO-646-10
|
SWEDISH: 'ascii', // ISO-646-10
|
||||||
'SWISS' : 'ascii', // ISO-646
|
SWISS: 'ascii', // ISO-646
|
||||||
'UK' : 'ascii', // ISO-646
|
UK: 'ascii', // ISO-646
|
||||||
'ISO-10' : 'ascii', // ISO-646-10
|
'ISO-10': 'ascii', // ISO-646-10
|
||||||
},
|
},
|
||||||
Level2 : {
|
Level2: {
|
||||||
'CP437' : 'cp437',
|
CP437: 'cp437',
|
||||||
'CP850' : 'cp850',
|
CP850: 'cp850',
|
||||||
'CP852' : 'cp852',
|
CP852: 'cp852',
|
||||||
'CP866' : 'cp866',
|
CP866: 'cp866',
|
||||||
'CP848' : 'cp848',
|
CP848: 'cp848',
|
||||||
'CP1250' : 'cp1250',
|
CP1250: 'cp1250',
|
||||||
'CP1251' : 'cp1251',
|
CP1251: 'cp1251',
|
||||||
'CP1252' : 'cp1252',
|
CP1252: 'cp1252',
|
||||||
'CP10000' : 'macroman',
|
CP10000: 'macroman',
|
||||||
'LATIN-1' : 'iso-8859-1',
|
'LATIN-1': 'iso-8859-1',
|
||||||
'LATIN-2' : 'iso-8859-2',
|
'LATIN-2': 'iso-8859-2',
|
||||||
'LATIN-5' : 'iso-8859-9',
|
'LATIN-5': 'iso-8859-9',
|
||||||
'LATIN-9' : 'iso-8859-15',
|
'LATIN-9': 'iso-8859-15',
|
||||||
},
|
},
|
||||||
|
|
||||||
Level4 : {
|
Level4: {
|
||||||
'UTF-8' : 'utf8',
|
'UTF-8': 'utf8',
|
||||||
},
|
},
|
||||||
|
|
||||||
DeprecatedMisc : {
|
DeprecatedMisc: {
|
||||||
'IBMPC' : 'cp1250', // :TODO: validate
|
IBMPC: 'cp1250', // :TODO: validate
|
||||||
'+7_FIDO' : 'cp866',
|
'+7_FIDO': 'cp866',
|
||||||
'+7' : 'cp866',
|
'+7': 'cp866',
|
||||||
'MAC' : 'macroman', // :TODO: validate
|
MAC: 'macroman', // :TODO: validate
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Given 1:N CHRS kludge IDs, try to pick the best encoding we can
|
// Given 1:N CHRS kludge IDs, try to pick the best encoding we can
|
||||||
@@ -424,7 +432,7 @@ const CHRSToEncodingTable = {
|
|||||||
// http://www.unicode.org/L2/L1999/99325-N.htm
|
// http://www.unicode.org/L2/L1999/99325-N.htm
|
||||||
function getEncodingFromCharacterSetIdentifier(chrs) {
|
function getEncodingFromCharacterSetIdentifier(chrs) {
|
||||||
if (!Array.isArray(chrs)) {
|
if (!Array.isArray(chrs)) {
|
||||||
chrs = [ chrs ];
|
chrs = [chrs];
|
||||||
}
|
}
|
||||||
|
|
||||||
const encLevel = (ident, table, level) => {
|
const encLevel = (ident, table, level) => {
|
||||||
@@ -448,7 +456,7 @@ function getEncodingFromCharacterSetIdentifier(chrs) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mapping.sort( (l, r) => {
|
mapping.sort((l, r) => {
|
||||||
return l.level - r.level;
|
return l.level - r.level;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,497 +15,513 @@ const _ = require('lodash');
|
|||||||
exports.FullMenuView = FullMenuView;
|
exports.FullMenuView = FullMenuView;
|
||||||
|
|
||||||
function FullMenuView(options) {
|
function FullMenuView(options) {
|
||||||
options.cursor = options.cursor || 'hide';
|
options.cursor = options.cursor || 'hide';
|
||||||
options.justify = options.justify || 'left';
|
options.justify = options.justify || 'left';
|
||||||
|
|
||||||
|
MenuView.call(this, options);
|
||||||
|
|
||||||
MenuView.call(this, options);
|
// Initialize paging
|
||||||
|
this.pages = [];
|
||||||
|
this.currentPage = 0;
|
||||||
|
|
||||||
|
this.initDefaultWidth();
|
||||||
|
|
||||||
// Initialize paging
|
// we want page up/page down by default
|
||||||
this.pages = [];
|
if (!_.isObject(options.specialKeyMap)) {
|
||||||
this.currentPage = 0;
|
Object.assign(this.specialKeyMap, {
|
||||||
|
'page up': ['page up'],
|
||||||
this.initDefaultWidth();
|
'page down': ['page down'],
|
||||||
|
});
|
||||||
// we want page up/page down by default
|
|
||||||
if (!_.isObject(options.specialKeyMap)) {
|
|
||||||
Object.assign(this.specialKeyMap, {
|
|
||||||
'page up': ['page up'],
|
|
||||||
'page down': ['page down'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autoAdjustHeightIfEnabled = () => {
|
|
||||||
if (this.autoAdjustHeight) {
|
|
||||||
this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing);
|
|
||||||
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
this.autoAdjustHeightIfEnabled = () => {
|
||||||
};
|
if (this.autoAdjustHeight) {
|
||||||
|
this.dimens.height =
|
||||||
this.autoAdjustHeightIfEnabled();
|
this.items.length * (this.itemSpacing + 1) - this.itemSpacing;
|
||||||
|
this.dimens.height = Math.min(
|
||||||
this.clearPage = () => {
|
this.dimens.height,
|
||||||
let width = this.dimens.width;
|
this.client.term.termHeight - this.position.row
|
||||||
if (this.oldDimens) {
|
);
|
||||||
if (this.oldDimens.width > width) {
|
|
||||||
width = this.oldDimens.width;
|
|
||||||
}
|
|
||||||
delete this.oldDimens;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < this.dimens.height; i++) {
|
|
||||||
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
|
|
||||||
this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachePositions = () => {
|
|
||||||
if (this.positionCacheExpired) {
|
|
||||||
// first, clear the page
|
|
||||||
this.clearPage();
|
|
||||||
|
|
||||||
|
|
||||||
this.autoAdjustHeightIfEnabled();
|
|
||||||
|
|
||||||
this.pages = []; // reset
|
|
||||||
|
|
||||||
// Calculate number of items visible per column
|
|
||||||
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
|
|
||||||
// handle case where one can fit at the end
|
|
||||||
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) {
|
|
||||||
this.itemsPerRow++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final check to make sure we don't try to display more than we have
|
|
||||||
if (this.itemsPerRow > this.items.length) {
|
|
||||||
this.itemsPerRow = this.items.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
let col = this.position.col;
|
|
||||||
let row = this.position.row;
|
|
||||||
const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
|
|
||||||
|
|
||||||
let itemInRow = 0;
|
|
||||||
let itemInCol = 0;
|
|
||||||
|
|
||||||
let pageStart = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.items.length; ++i) {
|
|
||||||
itemInRow++;
|
|
||||||
this.items[i].row = row;
|
|
||||||
this.items[i].col = col;
|
|
||||||
this.items[i].itemInRow = itemInRow;
|
|
||||||
|
|
||||||
row += this.itemSpacing + 1;
|
|
||||||
|
|
||||||
// have to calculate the max length on the last entry
|
|
||||||
if (i == this.items.length - 1) {
|
|
||||||
let maxLength = 0;
|
|
||||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
|
||||||
if (this.items[i - j].col != this.items[i].col) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const itemLength = this.items[i - j].text.length;
|
|
||||||
if (itemLength > maxLength) {
|
|
||||||
maxLength = itemLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set length on each item in the column
|
|
||||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
|
||||||
if (this.items[i - j].col != this.items[i].col) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.items[i - j].fixedLength = maxLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Check if we have room for this column
|
|
||||||
// skip for column 0, we need at least one
|
|
||||||
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
|
|
||||||
// save previous page
|
|
||||||
this.pages.push({ start: pageStart, end: i - itemInRow });
|
|
||||||
|
|
||||||
// fix the last column processed
|
|
||||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
|
||||||
if (this.items[i - j].col != col) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.items[i - j].col = this.position.col;
|
|
||||||
pageStart = i - j;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since this is the last page, save the current page as well
|
|
||||||
this.pages.push({ start: pageStart, end: i });
|
|
||||||
|
|
||||||
}
|
|
||||||
// also handle going to next column
|
|
||||||
else if (itemInRow == this.itemsPerRow) {
|
|
||||||
itemInRow = 0;
|
|
||||||
|
|
||||||
// restart row for next column
|
|
||||||
row = this.position.row;
|
|
||||||
let maxLength = 0;
|
|
||||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
|
||||||
// TODO: handle complex items
|
|
||||||
let itemLength = this.items[i - j].text.length;
|
|
||||||
if (itemLength > maxLength) {
|
|
||||||
maxLength = itemLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set length on each item in the column
|
|
||||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
|
||||||
this.items[i - j].fixedLength = maxLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have room for this column in the current page
|
|
||||||
// skip for first column, we need at least one
|
|
||||||
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
|
|
||||||
// save previous page
|
|
||||||
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
|
|
||||||
|
|
||||||
// restart page start for next page
|
|
||||||
pageStart = i - this.itemsPerRow + 1;
|
|
||||||
|
|
||||||
// reset
|
|
||||||
col = this.position.col;
|
|
||||||
itemInRow = 0;
|
|
||||||
|
|
||||||
// fix the last column processed
|
|
||||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
|
||||||
this.items[i - j].col = col;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// increment the column
|
|
||||||
col += maxLength + spacer.length;
|
|
||||||
itemInCol++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.positionCacheExpired = true;
|
||||||
|
};
|
||||||
|
|
||||||
// Set the current page if the current item is focused.
|
this.autoAdjustHeightIfEnabled();
|
||||||
if (this.focusedItemIndex === i) {
|
|
||||||
this.currentPage = this.pages.length;
|
this.clearPage = () => {
|
||||||
|
let width = this.dimens.width;
|
||||||
|
if (this.oldDimens) {
|
||||||
|
if (this.oldDimens.width > width) {
|
||||||
|
width = this.oldDimens.width;
|
||||||
|
}
|
||||||
|
delete this.oldDimens;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.positionCacheExpired = false;
|
for (let i = 0; i < this.dimens.height; i++) {
|
||||||
};
|
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
|
||||||
|
this.client.term.write(
|
||||||
|
`${ansi.goto(
|
||||||
|
this.position.row + i,
|
||||||
|
this.position.col
|
||||||
|
)}${this.getSGR()}${text}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.drawItem = (index) => {
|
this.cachePositions = () => {
|
||||||
const item = this.items[index];
|
if (this.positionCacheExpired) {
|
||||||
if (!item) {
|
// first, clear the page
|
||||||
return;
|
this.clearPage();
|
||||||
}
|
|
||||||
|
|
||||||
const cached = this.getRenderCacheItem(index, item.focused);
|
this.autoAdjustHeightIfEnabled();
|
||||||
if (cached) {
|
|
||||||
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text;
|
this.pages = []; // reset
|
||||||
let sgr;
|
|
||||||
if (item.focused && this.hasFocusItems()) {
|
|
||||||
const focusItem = this.focusItems[index];
|
|
||||||
text = focusItem ? focusItem.text : item.text;
|
|
||||||
sgr = '';
|
|
||||||
} else if (this.complexItems) {
|
|
||||||
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
|
|
||||||
sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
|
|
||||||
} else {
|
|
||||||
text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle);
|
|
||||||
sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
|
|
||||||
}
|
|
||||||
|
|
||||||
let renderLength = strUtil.renderStringLength(text);
|
// Calculate number of items visible per column
|
||||||
if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) {
|
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
|
||||||
text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow;
|
// handle case where one can fit at the end
|
||||||
}
|
if (this.dimens.height > this.itemsPerRow * (this.itemSpacing + 1)) {
|
||||||
|
this.itemsPerRow++;
|
||||||
|
}
|
||||||
|
|
||||||
let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
|
// Final check to make sure we don't try to display more than we have
|
||||||
|
if (this.itemsPerRow > this.items.length) {
|
||||||
|
this.itemsPerRow = this.items.length;
|
||||||
|
}
|
||||||
|
|
||||||
text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`;
|
let col = this.position.col;
|
||||||
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
|
let row = this.position.row;
|
||||||
this.setRenderCacheItem(index, text, item.focused);
|
const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
|
||||||
};
|
|
||||||
|
let itemInRow = 0;
|
||||||
|
let itemInCol = 0;
|
||||||
|
|
||||||
|
let pageStart = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.items.length; ++i) {
|
||||||
|
itemInRow++;
|
||||||
|
this.items[i].row = row;
|
||||||
|
this.items[i].col = col;
|
||||||
|
this.items[i].itemInRow = itemInRow;
|
||||||
|
|
||||||
|
row += this.itemSpacing + 1;
|
||||||
|
|
||||||
|
// have to calculate the max length on the last entry
|
||||||
|
if (i == this.items.length - 1) {
|
||||||
|
let maxLength = 0;
|
||||||
|
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||||
|
if (this.items[i - j].col != this.items[i].col) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const itemLength = this.items[i - j].text.length;
|
||||||
|
if (itemLength > maxLength) {
|
||||||
|
maxLength = itemLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set length on each item in the column
|
||||||
|
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||||
|
if (this.items[i - j].col != this.items[i].col) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.items[i - j].fixedLength = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have room for this column
|
||||||
|
// skip for column 0, we need at least one
|
||||||
|
if (itemInCol != 0 && col + maxLength > this.dimens.width) {
|
||||||
|
// save previous page
|
||||||
|
this.pages.push({ start: pageStart, end: i - itemInRow });
|
||||||
|
|
||||||
|
// fix the last column processed
|
||||||
|
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||||
|
if (this.items[i - j].col != col) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.items[i - j].col = this.position.col;
|
||||||
|
pageStart = i - j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since this is the last page, save the current page as well
|
||||||
|
this.pages.push({ start: pageStart, end: i });
|
||||||
|
}
|
||||||
|
// also handle going to next column
|
||||||
|
else if (itemInRow == this.itemsPerRow) {
|
||||||
|
itemInRow = 0;
|
||||||
|
|
||||||
|
// restart row for next column
|
||||||
|
row = this.position.row;
|
||||||
|
let maxLength = 0;
|
||||||
|
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||||
|
// TODO: handle complex items
|
||||||
|
let itemLength = this.items[i - j].text.length;
|
||||||
|
if (itemLength > maxLength) {
|
||||||
|
maxLength = itemLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set length on each item in the column
|
||||||
|
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||||
|
this.items[i - j].fixedLength = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have room for this column in the current page
|
||||||
|
// skip for first column, we need at least one
|
||||||
|
if (itemInCol != 0 && col + maxLength > this.dimens.width) {
|
||||||
|
// save previous page
|
||||||
|
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
|
||||||
|
|
||||||
|
// restart page start for next page
|
||||||
|
pageStart = i - this.itemsPerRow + 1;
|
||||||
|
|
||||||
|
// reset
|
||||||
|
col = this.position.col;
|
||||||
|
itemInRow = 0;
|
||||||
|
|
||||||
|
// fix the last column processed
|
||||||
|
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||||
|
this.items[i - j].col = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment the column
|
||||||
|
col += maxLength + spacer.length;
|
||||||
|
itemInCol++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the current page if the current item is focused.
|
||||||
|
if (this.focusedItemIndex === i) {
|
||||||
|
this.currentPage = this.pages.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.positionCacheExpired = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.drawItem = index => {
|
||||||
|
const item = this.items[index];
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.getRenderCacheItem(index, item.focused);
|
||||||
|
if (cached) {
|
||||||
|
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text;
|
||||||
|
let sgr;
|
||||||
|
if (item.focused && this.hasFocusItems()) {
|
||||||
|
const focusItem = this.focusItems[index];
|
||||||
|
text = focusItem ? focusItem.text : item.text;
|
||||||
|
sgr = '';
|
||||||
|
} else if (this.complexItems) {
|
||||||
|
text = pipeToAnsi(
|
||||||
|
formatString(
|
||||||
|
item.focused && this.focusItemFormat
|
||||||
|
? this.focusItemFormat
|
||||||
|
: this.itemFormat,
|
||||||
|
item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
sgr = this.focusItemFormat
|
||||||
|
? ''
|
||||||
|
: index === this.focusedItemIndex
|
||||||
|
? this.getFocusSGR()
|
||||||
|
: this.getSGR();
|
||||||
|
} else {
|
||||||
|
text = strUtil.stylizeString(
|
||||||
|
item.text,
|
||||||
|
item.focused ? this.focusTextStyle : this.textStyle
|
||||||
|
);
|
||||||
|
sgr = index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR();
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderLength = strUtil.renderStringLength(text);
|
||||||
|
if (this.hasTextOverflow() && item.col + renderLength > this.dimens.width) {
|
||||||
|
text =
|
||||||
|
strUtil.renderSubstr(
|
||||||
|
text,
|
||||||
|
0,
|
||||||
|
this.dimens.width - (item.col + this.textOverflow.length)
|
||||||
|
) + this.textOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
|
||||||
|
|
||||||
|
text = `${sgr}${strUtil.pad(
|
||||||
|
text,
|
||||||
|
padLength,
|
||||||
|
this.fillChar,
|
||||||
|
this.justify
|
||||||
|
)}${this.getSGR()}`;
|
||||||
|
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
|
||||||
|
this.setRenderCacheItem(index, text, item.focused);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(FullMenuView, MenuView);
|
util.inherits(FullMenuView, MenuView);
|
||||||
|
|
||||||
FullMenuView.prototype.redraw = function() {
|
FullMenuView.prototype.redraw = function () {
|
||||||
FullMenuView.super_.prototype.redraw.call(this);
|
FullMenuView.super_.prototype.redraw.call(this);
|
||||||
|
|
||||||
this.cachePositions();
|
this.cachePositions();
|
||||||
|
|
||||||
if (this.items.length) {
|
if (this.items.length) {
|
||||||
for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) {
|
for (
|
||||||
this.items[i].focused = this.focusedItemIndex === i;
|
let i = this.pages[this.currentPage].start;
|
||||||
this.drawItem(i);
|
i <= this.pages[this.currentPage].end;
|
||||||
|
++i
|
||||||
|
) {
|
||||||
|
this.items[i].focused = this.focusedItemIndex === i;
|
||||||
|
this.drawItem(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.setHeight = function(height) {
|
FullMenuView.prototype.setHeight = function (height) {
|
||||||
this.oldDimens = Object.assign({}, this.dimens);
|
|
||||||
|
|
||||||
FullMenuView.super_.prototype.setHeight.call(this, height);
|
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
this.autoAdjustHeight = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.setWidth = function(width) {
|
|
||||||
this.oldDimens = Object.assign({}, this.dimens);
|
|
||||||
|
|
||||||
FullMenuView.super_.prototype.setWidth.call(this, width);
|
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.setTextOverflow = function(overflow) {
|
|
||||||
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
|
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
FullMenuView.prototype.setPosition = function(pos) {
|
|
||||||
FullMenuView.super_.prototype.setPosition.call(this, pos);
|
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.setFocus = function(focused) {
|
|
||||||
FullMenuView.super_.prototype.setFocus.call(this, focused);
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
this.autoAdjustHeight = false;
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.setFocusItemIndex = function(index) {
|
|
||||||
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.onKeyPress = function(ch, key) {
|
|
||||||
if (key) {
|
|
||||||
if (this.isKeyMapped('up', key.name)) {
|
|
||||||
this.focusPrevious();
|
|
||||||
} else if (this.isKeyMapped('down', key.name)) {
|
|
||||||
this.focusNext();
|
|
||||||
} else if (this.isKeyMapped('left', key.name)) {
|
|
||||||
this.focusPreviousColumn();
|
|
||||||
} else if (this.isKeyMapped('right', key.name)) {
|
|
||||||
this.focusNextColumn();
|
|
||||||
} else if (this.isKeyMapped('page up', key.name)) {
|
|
||||||
this.focusPreviousPageItem();
|
|
||||||
} else if (this.isKeyMapped('page down', key.name)) {
|
|
||||||
this.focusNextPageItem();
|
|
||||||
} else if (this.isKeyMapped('home', key.name)) {
|
|
||||||
this.focusFirst();
|
|
||||||
} else if (this.isKeyMapped('end', key.name)) {
|
|
||||||
this.focusLast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.getData = function() {
|
|
||||||
const item = this.getItem(this.focusedItemIndex);
|
|
||||||
return _.isString(item.data) ? item.data : this.focusedItemIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.setItems = function(items) {
|
|
||||||
// if we have items already, save off their drawing area so we don't leave fragments at redraw
|
|
||||||
if (this.items && this.items.length) {
|
|
||||||
this.oldDimens = Object.assign({}, this.dimens);
|
this.oldDimens = Object.assign({}, this.dimens);
|
||||||
}
|
|
||||||
|
|
||||||
FullMenuView.super_.prototype.setItems.call(this, items);
|
FullMenuView.super_.prototype.setHeight.call(this, height);
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
|
this.autoAdjustHeight = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.removeItem = function(index) {
|
FullMenuView.prototype.setWidth = function (width) {
|
||||||
if (this.items && this.items.length) {
|
|
||||||
this.oldDimens = Object.assign({}, this.dimens);
|
this.oldDimens = Object.assign({}, this.dimens);
|
||||||
}
|
|
||||||
|
|
||||||
FullMenuView.super_.prototype.removeItem.call(this, index);
|
FullMenuView.super_.prototype.setWidth.call(this, width);
|
||||||
this.positionCacheExpired = true;
|
|
||||||
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusNext = function() {
|
FullMenuView.prototype.setTextOverflow = function (overflow) {
|
||||||
if (this.items.length - 1 === this.focusedItemIndex) {
|
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
|
||||||
this.clearPage();
|
|
||||||
this.focusedItemIndex = 0;
|
|
||||||
this.currentPage = 0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.focusedItemIndex++;
|
|
||||||
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
|
||||||
this.clearPage();
|
|
||||||
this.currentPage++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.redraw();
|
this.positionCacheExpired = true;
|
||||||
|
|
||||||
FullMenuView.super_.prototype.focusNext.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusPrevious = function() {
|
FullMenuView.prototype.setPosition = function (pos) {
|
||||||
if (0 === this.focusedItemIndex) {
|
FullMenuView.super_.prototype.setPosition.call(this, pos);
|
||||||
this.clearPage();
|
|
||||||
this.focusedItemIndex = this.items.length - 1;
|
|
||||||
this.currentPage = this.pages.length - 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.focusedItemIndex--;
|
|
||||||
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
|
||||||
this.clearPage();
|
|
||||||
this.currentPage--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.redraw();
|
this.positionCacheExpired = true;
|
||||||
|
|
||||||
FullMenuView.super_.prototype.focusPrevious.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusPreviousColumn = function() {
|
FullMenuView.prototype.setFocus = function (focused) {
|
||||||
|
FullMenuView.super_.prototype.setFocus.call(this, focused);
|
||||||
|
this.positionCacheExpired = true;
|
||||||
|
this.autoAdjustHeight = false;
|
||||||
|
|
||||||
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
this.redraw();
|
||||||
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
|
|
||||||
if (this.focusedItemIndex < 0) {
|
|
||||||
this.clearPage();
|
|
||||||
const lastItemRow = this.items[this.items.length - 1].itemInRow;
|
|
||||||
if (lastItemRow > currentRow) {
|
|
||||||
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// can't go to same column, so go to last item
|
|
||||||
this.focusedItemIndex = this.items.length - 1;
|
|
||||||
}
|
|
||||||
// set to last page
|
|
||||||
this.currentPage = this.pages.length - 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
|
||||||
this.clearPage();
|
|
||||||
this.currentPage--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
|
|
||||||
// TODO: This isn't specific to Previous, may want to replace in the future
|
|
||||||
FullMenuView.super_.prototype.focusPrevious.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusNextColumn = function() {
|
FullMenuView.prototype.setFocusItemIndex = function (index) {
|
||||||
|
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
||||||
|
};
|
||||||
|
|
||||||
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
FullMenuView.prototype.onKeyPress = function (ch, key) {
|
||||||
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
|
if (key) {
|
||||||
if (this.focusedItemIndex > this.items.length - 1) {
|
if (this.isKeyMapped('up', key.name)) {
|
||||||
this.focusedItemIndex = currentRow - 1;
|
this.focusPrevious();
|
||||||
this.currentPage = 0;
|
} else if (this.isKeyMapped('down', key.name)) {
|
||||||
this.clearPage();
|
this.focusNext();
|
||||||
}
|
} else if (this.isKeyMapped('left', key.name)) {
|
||||||
else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
this.focusPreviousColumn();
|
||||||
|
} else if (this.isKeyMapped('right', key.name)) {
|
||||||
|
this.focusNextColumn();
|
||||||
|
} else if (this.isKeyMapped('page up', key.name)) {
|
||||||
|
this.focusPreviousPageItem();
|
||||||
|
} else if (this.isKeyMapped('page down', key.name)) {
|
||||||
|
this.focusNextPageItem();
|
||||||
|
} else if (this.isKeyMapped('home', key.name)) {
|
||||||
|
this.focusFirst();
|
||||||
|
} else if (this.isKeyMapped('end', key.name)) {
|
||||||
|
this.focusLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.getData = function () {
|
||||||
|
const item = this.getItem(this.focusedItemIndex);
|
||||||
|
return _.isString(item.data) ? item.data : this.focusedItemIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.setItems = function (items) {
|
||||||
|
// if we have items already, save off their drawing area so we don't leave fragments at redraw
|
||||||
|
if (this.items && this.items.length) {
|
||||||
|
this.oldDimens = Object.assign({}, this.dimens);
|
||||||
|
}
|
||||||
|
|
||||||
|
FullMenuView.super_.prototype.setItems.call(this, items);
|
||||||
|
|
||||||
|
this.positionCacheExpired = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.removeItem = function (index) {
|
||||||
|
if (this.items && this.items.length) {
|
||||||
|
this.oldDimens = Object.assign({}, this.dimens);
|
||||||
|
}
|
||||||
|
|
||||||
|
FullMenuView.super_.prototype.removeItem.call(this, index);
|
||||||
|
this.positionCacheExpired = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.focusNext = function () {
|
||||||
|
if (this.items.length - 1 === this.focusedItemIndex) {
|
||||||
|
this.clearPage();
|
||||||
|
this.focusedItemIndex = 0;
|
||||||
|
this.currentPage = 0;
|
||||||
|
} else {
|
||||||
|
this.focusedItemIndex++;
|
||||||
|
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
||||||
|
this.clearPage();
|
||||||
|
this.currentPage++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
FullMenuView.super_.prototype.focusNext.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.focusPrevious = function () {
|
||||||
|
if (0 === this.focusedItemIndex) {
|
||||||
|
this.clearPage();
|
||||||
|
this.focusedItemIndex = this.items.length - 1;
|
||||||
|
this.currentPage = this.pages.length - 1;
|
||||||
|
} else {
|
||||||
|
this.focusedItemIndex--;
|
||||||
|
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
||||||
|
this.clearPage();
|
||||||
|
this.currentPage--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
FullMenuView.super_.prototype.focusPrevious.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.focusPreviousColumn = function () {
|
||||||
|
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
||||||
|
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
|
||||||
|
if (this.focusedItemIndex < 0) {
|
||||||
|
this.clearPage();
|
||||||
|
const lastItemRow = this.items[this.items.length - 1].itemInRow;
|
||||||
|
if (lastItemRow > currentRow) {
|
||||||
|
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
|
||||||
|
} else {
|
||||||
|
// can't go to same column, so go to last item
|
||||||
|
this.focusedItemIndex = this.items.length - 1;
|
||||||
|
}
|
||||||
|
// set to last page
|
||||||
|
this.currentPage = this.pages.length - 1;
|
||||||
|
} else {
|
||||||
|
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
||||||
|
this.clearPage();
|
||||||
|
this.currentPage--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
// TODO: This isn't specific to Previous, may want to replace in the future
|
||||||
|
FullMenuView.super_.prototype.focusPrevious.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.focusNextColumn = function () {
|
||||||
|
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
||||||
|
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
|
||||||
|
if (this.focusedItemIndex > this.items.length - 1) {
|
||||||
|
this.focusedItemIndex = currentRow - 1;
|
||||||
|
this.currentPage = 0;
|
||||||
|
this.clearPage();
|
||||||
|
} else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
||||||
|
this.clearPage();
|
||||||
|
this.currentPage++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
// TODO: This isn't specific to Next, may want to replace in the future
|
||||||
|
FullMenuView.super_.prototype.focusNext.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.focusPreviousPageItem = function () {
|
||||||
|
// handle first page
|
||||||
|
if (this.currentPage == 0) {
|
||||||
|
// Do nothing, page up shouldn't go down on last page
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentPage--;
|
||||||
|
this.focusedItemIndex = this.pages[this.currentPage].start;
|
||||||
this.clearPage();
|
this.clearPage();
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullMenuView.prototype.focusNextPageItem = function () {
|
||||||
|
// handle last page
|
||||||
|
if (this.currentPage == this.pages.length - 1) {
|
||||||
|
// Do nothing, page up shouldn't go down on last page
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.currentPage++;
|
this.currentPage++;
|
||||||
}
|
this.focusedItemIndex = this.pages[this.currentPage].start;
|
||||||
|
this.clearPage();
|
||||||
|
|
||||||
this.redraw();
|
this.redraw();
|
||||||
|
|
||||||
// TODO: This isn't specific to Next, may want to replace in the future
|
return FullMenuView.super_.prototype.focusNextPageItem.call(this);
|
||||||
FullMenuView.super_.prototype.focusNext.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusPreviousPageItem = function() {
|
FullMenuView.prototype.focusFirst = function () {
|
||||||
|
this.currentPage = 0;
|
||||||
|
this.focusedItemIndex = 0;
|
||||||
|
this.clearPage();
|
||||||
|
|
||||||
// handle first page
|
this.redraw();
|
||||||
if (this.currentPage == 0) {
|
return FullMenuView.super_.prototype.focusFirst.call(this);
|
||||||
// Do nothing, page up shouldn't go down on last page
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPage--;
|
|
||||||
this.focusedItemIndex = this.pages[this.currentPage].start;
|
|
||||||
this.clearPage();
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
|
|
||||||
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusNextPageItem = function() {
|
FullMenuView.prototype.focusLast = function () {
|
||||||
|
this.currentPage = this.pages.length - 1;
|
||||||
|
this.focusedItemIndex = this.pages[this.currentPage].end;
|
||||||
|
this.clearPage();
|
||||||
|
|
||||||
// handle last page
|
this.redraw();
|
||||||
if (this.currentPage == this.pages.length - 1) {
|
return FullMenuView.super_.prototype.focusLast.call(this);
|
||||||
// Do nothing, page up shouldn't go down on last page
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPage++;
|
|
||||||
this.focusedItemIndex = this.pages[this.currentPage].start;
|
|
||||||
this.clearPage();
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
|
|
||||||
return FullMenuView.super_.prototype.focusNextPageItem.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusFirst = function() {
|
FullMenuView.prototype.setFocusItems = function (items) {
|
||||||
|
FullMenuView.super_.prototype.setFocusItems.call(this, items);
|
||||||
|
|
||||||
this.currentPage = 0;
|
this.positionCacheExpired = true;
|
||||||
this.focusedItemIndex = 0;
|
|
||||||
this.clearPage();
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
return FullMenuView.super_.prototype.focusFirst.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.focusLast = function() {
|
FullMenuView.prototype.setItemSpacing = function (itemSpacing) {
|
||||||
|
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
|
||||||
|
|
||||||
this.currentPage = this.pages.length - 1;
|
this.positionCacheExpired = true;
|
||||||
this.focusedItemIndex = this.pages[this.currentPage].end;
|
|
||||||
this.clearPage();
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
return FullMenuView.super_.prototype.focusLast.call(this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.setFocusItems = function(items) {
|
FullMenuView.prototype.setJustify = function (justify) {
|
||||||
FullMenuView.super_.prototype.setFocusItems.call(this, items);
|
FullMenuView.super_.prototype.setJustify.call(this, justify);
|
||||||
|
this.positionCacheExpired = true;
|
||||||
this.positionCacheExpired = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullMenuView.prototype.setItemSpacing = function(itemSpacing) {
|
FullMenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) {
|
||||||
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
|
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
|
||||||
|
|
||||||
FullMenuView.prototype.setJustify = function(justify) {
|
|
||||||
FullMenuView.super_.prototype.setJustify.call(this, justify);
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
|
|
||||||
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
|
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const MenuView = require('./menu_view.js').MenuView;
|
const MenuView = require('./menu_view.js').MenuView;
|
||||||
const strUtil = require('./string_util.js');
|
const strUtil = require('./string_util.js');
|
||||||
const formatString = require('./string_format');
|
const formatString = require('./string_format');
|
||||||
const { pipeToAnsi } = require('./color_codes.js');
|
const { pipeToAnsi } = require('./color_codes.js');
|
||||||
const { goto } = require('./ansi_term.js');
|
const { goto } = require('./ansi_term.js');
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.HorizontalMenuView = HorizontalMenuView;
|
exports.HorizontalMenuView = HorizontalMenuView;
|
||||||
|
|
||||||
// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView)
|
// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView)
|
||||||
|
|
||||||
function HorizontalMenuView(options) {
|
function HorizontalMenuView(options) {
|
||||||
options.cursor = options.cursor || 'hide';
|
options.cursor = options.cursor || 'hide';
|
||||||
|
|
||||||
if(!_.isNumber(options.itemSpacing)) {
|
if (!_.isNumber(options.itemSpacing)) {
|
||||||
options.itemSpacing = 1;
|
options.itemSpacing = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,16 +27,16 @@ function HorizontalMenuView(options) {
|
|||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.getSpacer = function() {
|
this.getSpacer = function () {
|
||||||
return new Array(self.itemSpacing + 1).join(' ');
|
return new Array(self.itemSpacing + 1).join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cachePositions = function() {
|
this.cachePositions = function () {
|
||||||
if(this.positionCacheExpired) {
|
if (this.positionCacheExpired) {
|
||||||
var col = self.position.col;
|
var col = self.position.col;
|
||||||
var spacer = self.getSpacer();
|
var spacer = self.getSpacer();
|
||||||
|
|
||||||
for(var i = 0; i < self.items.length; ++i) {
|
for (var i = 0; i < self.items.length; ++i) {
|
||||||
self.items[i].col = col;
|
self.items[i].col = col;
|
||||||
col += spacer.length + self.items[i].text.length + spacer.length;
|
col += spacer.length + self.items[i].text.length + spacer.length;
|
||||||
}
|
}
|
||||||
@@ -45,75 +45,94 @@ function HorizontalMenuView(options) {
|
|||||||
this.positionCacheExpired = false;
|
this.positionCacheExpired = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.drawItem = function(index) {
|
this.drawItem = function (index) {
|
||||||
assert(!this.positionCacheExpired);
|
assert(!this.positionCacheExpired);
|
||||||
|
|
||||||
const item = self.items[index];
|
const item = self.items[index];
|
||||||
if(!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
let sgr;
|
let sgr;
|
||||||
if(item.focused && self.hasFocusItems()) {
|
if (item.focused && self.hasFocusItems()) {
|
||||||
const focusItem = self.focusItems[index];
|
const focusItem = self.focusItems[index];
|
||||||
text = focusItem ? focusItem.text : item.text;
|
text = focusItem ? focusItem.text : item.text;
|
||||||
sgr = '';
|
sgr = '';
|
||||||
} else if(this.complexItems) {
|
} else if (this.complexItems) {
|
||||||
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
|
text = pipeToAnsi(
|
||||||
sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
|
formatString(
|
||||||
|
item.focused && this.focusItemFormat
|
||||||
|
? this.focusItemFormat
|
||||||
|
: this.itemFormat,
|
||||||
|
item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
sgr = this.focusItemFormat
|
||||||
|
? ''
|
||||||
|
: index === self.focusedItemIndex
|
||||||
|
? self.getFocusSGR()
|
||||||
|
: self.getSGR();
|
||||||
} else {
|
} else {
|
||||||
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle);
|
text = strUtil.stylizeString(
|
||||||
sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
|
item.text,
|
||||||
|
item.focused ? self.focusTextStyle : self.textStyle
|
||||||
|
);
|
||||||
|
sgr = index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR();
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2);
|
const drawWidth = strUtil.renderStringLength(text) + self.getSpacer().length * 2;
|
||||||
|
|
||||||
self.client.term.write(
|
self.client.term.write(
|
||||||
`${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}`
|
`${goto(self.position.row, item.col)}${sgr}${strUtil.pad(
|
||||||
|
text,
|
||||||
|
drawWidth,
|
||||||
|
self.fillChar,
|
||||||
|
'center'
|
||||||
|
)}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
require('util').inherits(HorizontalMenuView, MenuView);
|
require('util').inherits(HorizontalMenuView, MenuView);
|
||||||
|
|
||||||
HorizontalMenuView.prototype.setHeight = function(height) {
|
HorizontalMenuView.prototype.setHeight = function (height) {
|
||||||
height = parseInt(height, 10);
|
height = parseInt(height, 10);
|
||||||
assert(1 === height); // nothing else allowed here
|
assert(1 === height); // nothing else allowed here
|
||||||
HorizontalMenuView.super_.prototype.setHeight(this, height);
|
HorizontalMenuView.super_.prototype.setHeight(this, height);
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.redraw = function() {
|
HorizontalMenuView.prototype.redraw = function () {
|
||||||
HorizontalMenuView.super_.prototype.redraw.call(this);
|
HorizontalMenuView.super_.prototype.redraw.call(this);
|
||||||
|
|
||||||
this.cachePositions();
|
this.cachePositions();
|
||||||
|
|
||||||
for(var i = 0; i < this.items.length; ++i) {
|
for (var i = 0; i < this.items.length; ++i) {
|
||||||
this.items[i].focused = this.focusedItemIndex === i;
|
this.items[i].focused = this.focusedItemIndex === i;
|
||||||
this.drawItem(i);
|
this.drawItem(i);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.setPosition = function(pos) {
|
HorizontalMenuView.prototype.setPosition = function (pos) {
|
||||||
HorizontalMenuView.super_.prototype.setPosition.call(this, pos);
|
HorizontalMenuView.super_.prototype.setPosition.call(this, pos);
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.setFocus = function(focused) {
|
HorizontalMenuView.prototype.setFocus = function (focused) {
|
||||||
HorizontalMenuView.super_.prototype.setFocus.call(this, focused);
|
HorizontalMenuView.super_.prototype.setFocus.call(this, focused);
|
||||||
|
|
||||||
this.redraw();
|
this.redraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.setItems = function(items) {
|
HorizontalMenuView.prototype.setItems = function (items) {
|
||||||
HorizontalMenuView.super_.prototype.setItems.call(this, items);
|
HorizontalMenuView.super_.prototype.setItems.call(this, items);
|
||||||
|
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.focusNext = function() {
|
HorizontalMenuView.prototype.focusNext = function () {
|
||||||
if(this.items.length - 1 === this.focusedItemIndex) {
|
if (this.items.length - 1 === this.focusedItemIndex) {
|
||||||
this.focusedItemIndex = 0;
|
this.focusedItemIndex = 0;
|
||||||
} else {
|
} else {
|
||||||
this.focusedItemIndex++;
|
this.focusedItemIndex++;
|
||||||
@@ -125,9 +144,8 @@ HorizontalMenuView.prototype.focusNext = function() {
|
|||||||
HorizontalMenuView.super_.prototype.focusNext.call(this);
|
HorizontalMenuView.super_.prototype.focusNext.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.focusPrevious = function() {
|
HorizontalMenuView.prototype.focusPrevious = function () {
|
||||||
|
if (0 === this.focusedItemIndex) {
|
||||||
if(0 === this.focusedItemIndex) {
|
|
||||||
this.focusedItemIndex = this.items.length - 1;
|
this.focusedItemIndex = this.items.length - 1;
|
||||||
} else {
|
} else {
|
||||||
this.focusedItemIndex--;
|
this.focusedItemIndex--;
|
||||||
@@ -139,11 +157,11 @@ HorizontalMenuView.prototype.focusPrevious = function() {
|
|||||||
HorizontalMenuView.super_.prototype.focusPrevious.call(this);
|
HorizontalMenuView.super_.prototype.focusPrevious.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
|
HorizontalMenuView.prototype.onKeyPress = function (ch, key) {
|
||||||
if(key) {
|
if (key) {
|
||||||
if(this.isKeyMapped('left', key.name)) {
|
if (this.isKeyMapped('left', key.name)) {
|
||||||
this.focusPrevious();
|
this.focusPrevious();
|
||||||
} else if(this.isKeyMapped('right', key.name)) {
|
} else if (this.isKeyMapped('right', key.name)) {
|
||||||
this.focusNext();
|
this.focusNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +169,7 @@ HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
|
|||||||
HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
HorizontalMenuView.prototype.getData = function() {
|
HorizontalMenuView.prototype.getData = function () {
|
||||||
const item = this.getItem(this.focusedItemIndex);
|
const item = this.getItem(this.focusedItemIndex);
|
||||||
return _.isString(item.data) ? item.data : this.focusedItemIndex;
|
return _.isString(item.data) ? item.data : this.focusedItemIndex;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const View = require('./view.js').View;
|
const View = require('./view.js').View;
|
||||||
const valueWithDefault = require('./misc_util.js').valueWithDefault;
|
const valueWithDefault = require('./misc_util.js').valueWithDefault;
|
||||||
const isPrintable = require('./string_util.js').isPrintable;
|
const isPrintable = require('./string_util.js').isPrintable;
|
||||||
const stylizeString = require('./string_util.js').stylizeString;
|
const stylizeString = require('./string_util.js').stylizeString;
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = class KeyEntryView extends View {
|
module.exports = class KeyEntryView extends View {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
@@ -15,12 +15,12 @@ module.exports = class KeyEntryView extends View {
|
|||||||
|
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.eatTabKey = options.eatTabKey || true;
|
this.eatTabKey = options.eatTabKey || true;
|
||||||
this.caseInsensitive = options.caseInsensitive || true;
|
this.caseInsensitive = options.caseInsensitive || true;
|
||||||
|
|
||||||
if(Array.isArray(options.keys)) {
|
if (Array.isArray(options.keys)) {
|
||||||
if(this.caseInsensitive) {
|
if (this.caseInsensitive) {
|
||||||
this.keys = options.keys.map( k => k.toUpperCase() );
|
this.keys = options.keys.map(k => k.toUpperCase());
|
||||||
} else {
|
} else {
|
||||||
this.keys = options.keys;
|
this.keys = options.keys;
|
||||||
}
|
}
|
||||||
@@ -30,18 +30,22 @@ module.exports = class KeyEntryView extends View {
|
|||||||
onKeyPress(ch, key) {
|
onKeyPress(ch, key) {
|
||||||
const drawKey = ch;
|
const drawKey = ch;
|
||||||
|
|
||||||
if(ch && this.caseInsensitive) {
|
if (ch && this.caseInsensitive) {
|
||||||
ch = ch.toUpperCase();
|
ch = ch.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
|
if (
|
||||||
this.redraw(); // sets position
|
drawKey &&
|
||||||
|
isPrintable(drawKey) &&
|
||||||
|
(!this.keys || this.keys.indexOf(ch) > -1)
|
||||||
|
) {
|
||||||
|
this.redraw(); // sets position
|
||||||
this.client.term.write(stylizeString(ch, this.textStyle));
|
this.client.term.write(stylizeString(ch, this.textStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keyEntered = ch || key.name;
|
this.keyEntered = ch || key.name;
|
||||||
|
|
||||||
if(key && 'tab' === key.name && !this.eatTabKey) {
|
if (key && 'tab' === key.name && !this.eatTabKey) {
|
||||||
return this.emit('action', 'next', key);
|
return this.emit('action', 'next', key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,21 +54,21 @@ module.exports = class KeyEntryView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPropertyValue(propName, propValue) {
|
setPropertyValue(propName, propValue) {
|
||||||
switch(propName) {
|
switch (propName) {
|
||||||
case 'eatTabKey' :
|
case 'eatTabKey':
|
||||||
if(_.isBoolean(propValue)) {
|
if (_.isBoolean(propValue)) {
|
||||||
this.eatTabKey = propValue;
|
this.eatTabKey = propValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'caseInsensitive' :
|
case 'caseInsensitive':
|
||||||
if(_.isBoolean(propValue)) {
|
if (_.isBoolean(propValue)) {
|
||||||
this.caseInsensitive = propValue;
|
this.caseInsensitive = propValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'keys' :
|
case 'keys':
|
||||||
if(Array.isArray(propValue)) {
|
if (Array.isArray(propValue)) {
|
||||||
this.keys = propValue;
|
this.keys = propValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -73,5 +77,7 @@ module.exports = class KeyEntryView extends View {
|
|||||||
super.setPropertyValue(propName, propValue);
|
super.setPropertyValue(propName, propValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() { return this.keyEntered; }
|
getData() {
|
||||||
};
|
return this.keyEntered;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,74 +2,90 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
const sysDb = require('./database.js').dbs.system;
|
const sysDb = require('./database.js').dbs.system;
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const SysLogKeys = require('./system_log.js');
|
const SysLogKeys = require('./system_log.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Last Callers',
|
name: 'Last Callers',
|
||||||
desc : 'Last callers to the system',
|
desc: 'Last callers to the system',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
packageName : 'codes.l33t.enigma.lastcallers'
|
packageName: 'codes.l33t.enigma.lastcallers',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
callerList : 1,
|
callerList: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class LastCallersModule extends MenuModule {
|
exports.getModule = class LastCallersModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {});
|
this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {});
|
||||||
this.actionIndicatorDefault = _.get(options, 'menuConfig.config.actionIndicatorDefault', '-');
|
this.actionIndicatorDefault = _.get(
|
||||||
|
options,
|
||||||
|
'menuConfig.config.actionIndicatorDefault',
|
||||||
|
'-'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
this.prepViewController('callers', 0, mciData.menu, err => {
|
this.prepViewController('callers', 0, mciData.menu, err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
this.fetchHistory( (err, loginHistory) => {
|
this.fetchHistory((err, loginHistory) => {
|
||||||
return callback(err, loginHistory);
|
return callback(err, loginHistory);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(loginHistory, callback) => {
|
(loginHistory, callback) => {
|
||||||
this.loadUserForHistoryItems(loginHistory, (err, updatedHistory) => {
|
this.loadUserForHistoryItems(
|
||||||
return callback(err, updatedHistory);
|
loginHistory,
|
||||||
});
|
(err, updatedHistory) => {
|
||||||
|
return callback(err, updatedHistory);
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(loginHistory, callback) => {
|
(loginHistory, callback) => {
|
||||||
const callersView = this.viewControllers.callers.getView(MciViewIds.callerList);
|
const callersView = this.viewControllers.callers.getView(
|
||||||
if(!callersView) {
|
MciViewIds.callerList
|
||||||
return cb(Errors.MissingMci(`Missing caller list MCI ${MciViewIds.callerList}`));
|
);
|
||||||
|
if (!callersView) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingMci(
|
||||||
|
`Missing caller list MCI ${MciViewIds.callerList}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
callersView.setItems(loginHistory);
|
callersView.setItems(loginHistory);
|
||||||
callersView.redraw();
|
callersView.redraw();
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
this.client.log.warn( { error : err.message }, 'Error loading last callers');
|
this.client.log.warn(
|
||||||
|
{ error: err.message },
|
||||||
|
'Error loading last callers'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
@@ -79,65 +95,74 @@ exports.getModule = class LastCallersModule extends MenuModule {
|
|||||||
|
|
||||||
getCollapse(conf) {
|
getCollapse(conf) {
|
||||||
let collapse = _.get(this, conf);
|
let collapse = _.get(this, conf);
|
||||||
collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/);
|
collapse =
|
||||||
if(collapse) {
|
collapse &&
|
||||||
|
collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/);
|
||||||
|
if (collapse) {
|
||||||
return moment.duration(parseInt(collapse[1]), collapse[2]);
|
return moment.duration(parseInt(collapse[1]), collapse[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchHistory(cb) {
|
fetchHistory(cb) {
|
||||||
const callersView = this.viewControllers.callers.getView(MciViewIds.callerList);
|
const callersView = this.viewControllers.callers.getView(MciViewIds.callerList);
|
||||||
if(!callersView || 0 === callersView.dimens.height) {
|
if (!callersView || 0 === callersView.dimens.height) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
StatLog.getSystemLogEntries(
|
StatLog.getSystemLogEntries(
|
||||||
SysLogKeys.UserLoginHistory,
|
SysLogKeys.UserLoginHistory,
|
||||||
StatLog.Order.TimestampDesc,
|
StatLog.Order.TimestampDesc,
|
||||||
200, // max items to fetch - we need more than max displayed for filtering/etc.
|
200, // max items to fetch - we need more than max displayed for filtering/etc.
|
||||||
(err, loginHistory) => {
|
(err, loginHistory) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateTimeFormat = _.get(
|
const dateTimeFormat = _.get(
|
||||||
this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short'));
|
this,
|
||||||
|
'menuConfig.config.dateTimeFormat',
|
||||||
|
this.client.currentTheme.helpers.getDateFormat('short')
|
||||||
|
);
|
||||||
|
|
||||||
loginHistory = loginHistory.map(item => {
|
loginHistory = loginHistory.map(item => {
|
||||||
try {
|
try {
|
||||||
const historyItem = JSON.parse(item.log_value);
|
const historyItem = JSON.parse(item.log_value);
|
||||||
if(_.isObject(historyItem)) {
|
if (_.isObject(historyItem)) {
|
||||||
item.userId = historyItem.userId;
|
item.userId = historyItem.userId;
|
||||||
item.sessionId = historyItem.sessionId;
|
item.sessionId = historyItem.sessionId;
|
||||||
} else {
|
} else {
|
||||||
item.userId = historyItem; // older format
|
item.userId = historyItem; // older format
|
||||||
item.sessionId = '-none-';
|
item.sessionId = '-none-';
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return null; // we'll filter this out
|
return null; // we'll filter this out
|
||||||
}
|
}
|
||||||
|
|
||||||
item.timestamp = moment(item.timestamp);
|
item.timestamp = moment(item.timestamp);
|
||||||
|
|
||||||
return Object.assign(
|
return Object.assign(item, {
|
||||||
item,
|
ts: moment(item.timestamp).format(dateTimeFormat),
|
||||||
{
|
});
|
||||||
ts : moment(item.timestamp).format(dateTimeFormat)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide');
|
const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide');
|
||||||
const sysOpCollapse = this.getCollapse('menuConfig.config.sysop.collapse');
|
const sysOpCollapse = this.getCollapse(
|
||||||
|
'menuConfig.config.sysop.collapse'
|
||||||
|
);
|
||||||
|
|
||||||
const collapseList = (withUserId, minAge) => {
|
const collapseList = (withUserId, minAge) => {
|
||||||
let lastUserId;
|
let lastUserId;
|
||||||
let lastTimestamp;
|
let lastTimestamp;
|
||||||
loginHistory = loginHistory.filter(item => {
|
loginHistory = loginHistory.filter(item => {
|
||||||
const secApart = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).asSeconds() : 0;
|
const secApart = lastTimestamp
|
||||||
const collapse = (null === withUserId ? true : withUserId === item.userId) &&
|
? moment
|
||||||
(lastUserId === item.userId) &&
|
.duration(lastTimestamp.diff(item.timestamp))
|
||||||
(secApart < minAge);
|
.asSeconds()
|
||||||
|
: 0;
|
||||||
|
const collapse =
|
||||||
|
(null === withUserId ? true : withUserId === item.userId) &&
|
||||||
|
lastUserId === item.userId &&
|
||||||
|
secApart < minAge;
|
||||||
|
|
||||||
lastUserId = item.userId;
|
lastUserId = item.userId;
|
||||||
lastTimestamp = item.timestamp;
|
lastTimestamp = item.timestamp;
|
||||||
@@ -146,20 +171,22 @@ exports.getModule = class LastCallersModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if(hideSysOp) {
|
if (hideSysOp) {
|
||||||
loginHistory = loginHistory.filter(item => false === User.isRootUserId(item.userId));
|
loginHistory = loginHistory.filter(
|
||||||
} else if(sysOpCollapse) {
|
item => false === User.isRootUserId(item.userId)
|
||||||
|
);
|
||||||
|
} else if (sysOpCollapse) {
|
||||||
collapseList(User.RootUserID, sysOpCollapse.asSeconds());
|
collapseList(User.RootUserID, sysOpCollapse.asSeconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
const userCollapse = this.getCollapse('menuConfig.config.user.collapse');
|
const userCollapse = this.getCollapse('menuConfig.config.user.collapse');
|
||||||
if(userCollapse) {
|
if (userCollapse) {
|
||||||
collapseList(null, userCollapse.asSeconds());
|
collapseList(null, userCollapse.asSeconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(
|
return cb(
|
||||||
null,
|
null,
|
||||||
loginHistory.slice(0, callersView.dimens.height) // trim the fat
|
loginHistory.slice(0, callersView.dimens.height) // trim the fat
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -167,57 +194,70 @@ exports.getModule = class LastCallersModule extends MenuModule {
|
|||||||
|
|
||||||
loadUserForHistoryItems(loginHistory, cb) {
|
loadUserForHistoryItems(loginHistory, cb) {
|
||||||
const getPropOpts = {
|
const getPropOpts = {
|
||||||
names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ]
|
names: [UserProps.RealName, UserProps.Location, UserProps.Affiliations],
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k);
|
const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k);
|
||||||
let indicatorSumsSql;
|
let indicatorSumsSql;
|
||||||
if(actionIndicatorNames.length > 0) {
|
if (actionIndicatorNames.length > 0) {
|
||||||
indicatorSumsSql = actionIndicatorNames.map(i => {
|
indicatorSumsSql = actionIndicatorNames.map(i => {
|
||||||
return `SUM(CASE WHEN log_name='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`;
|
return `SUM(CASE WHEN log_name='${_.snakeCase(
|
||||||
|
i
|
||||||
|
)}' THEN 1 ELSE 0 END) AS ${i}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async.map(loginHistory, (item, nextHistoryItem) => {
|
async.map(
|
||||||
User.getUserName(item.userId, (err, userName) => {
|
loginHistory,
|
||||||
if(err) {
|
(item, nextHistoryItem) => {
|
||||||
return nextHistoryItem(null, null);
|
User.getUserName(item.userId, (err, userName) => {
|
||||||
}
|
if (err) {
|
||||||
|
return nextHistoryItem(null, null);
|
||||||
item.userName = item.text = userName;
|
|
||||||
|
|
||||||
User.loadProperties(item.userId, getPropOpts, (err, props) => {
|
|
||||||
item.location = (props && props[UserProps.Location]) || '';
|
|
||||||
item.affiliation = item.affils = (props && props[UserProps.Affiliations]) || '';
|
|
||||||
item.realName = (props && props[UserProps.RealName]) || '';
|
|
||||||
|
|
||||||
if(!indicatorSumsSql) {
|
|
||||||
return nextHistoryItem(null, item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sysDb.get(
|
item.userName = item.text = userName;
|
||||||
`SELECT ${indicatorSumsSql.join(', ')}
|
|
||||||
|
User.loadProperties(item.userId, getPropOpts, (err, props) => {
|
||||||
|
item.location = (props && props[UserProps.Location]) || '';
|
||||||
|
item.affiliation = item.affils =
|
||||||
|
(props && props[UserProps.Affiliations]) || '';
|
||||||
|
item.realName = (props && props[UserProps.RealName]) || '';
|
||||||
|
|
||||||
|
if (!indicatorSumsSql) {
|
||||||
|
return nextHistoryItem(null, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
sysDb.get(
|
||||||
|
`SELECT ${indicatorSumsSql.join(', ')}
|
||||||
FROM user_event_log
|
FROM user_event_log
|
||||||
WHERE user_id=? AND session_id=?
|
WHERE user_id=? AND session_id=?
|
||||||
LIMIT 1;`,
|
LIMIT 1;`,
|
||||||
[ item.userId, item.sessionId ],
|
[item.userId, item.sessionId],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if(_.isObject(results)) {
|
if (_.isObject(results)) {
|
||||||
item.actions = '';
|
item.actions = '';
|
||||||
Object.keys(results).forEach(n => {
|
Object.keys(results).forEach(n => {
|
||||||
const indicator = results[n] > 0 ? this.actionIndicators[n] || this.actionIndicatorDefault : this.actionIndicatorDefault;
|
const indicator =
|
||||||
item[n] = indicator;
|
results[n] > 0
|
||||||
item.actions += indicator;
|
? this.actionIndicators[n] ||
|
||||||
});
|
this.actionIndicatorDefault
|
||||||
|
: this.actionIndicatorDefault;
|
||||||
|
item[n] = indicator;
|
||||||
|
item.actions += indicator;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nextHistoryItem(null, item);
|
||||||
}
|
}
|
||||||
return nextHistoryItem(null, item);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
},
|
(err, mapped) => {
|
||||||
(err, mapped) => {
|
return cb(
|
||||||
return cb(err, mapped.filter(item => item)); // remove deleted
|
err,
|
||||||
});
|
mapped.filter(item => item)
|
||||||
|
); // remove deleted
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const logger = require('./logger.js');
|
const logger = require('./logger.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
const listeningServers = {}; // packageName -> info
|
const listeningServers = {}; // packageName -> info
|
||||||
|
|
||||||
exports.startup = startup;
|
exports.startup = startup;
|
||||||
exports.shutdown = shutdown;
|
exports.shutdown = shutdown;
|
||||||
exports.getServer = getServer;
|
exports.getServer = getServer;
|
||||||
|
|
||||||
function startup(cb) {
|
function startup(cb) {
|
||||||
return startListening(cb);
|
return startListening(cb);
|
||||||
@@ -28,36 +28,44 @@ function getServer(packageName) {
|
|||||||
function startListening(cb) {
|
function startListening(cb) {
|
||||||
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
||||||
|
|
||||||
async.each( [ 'login', 'content', 'chat' ], (category, next) => {
|
async.each(
|
||||||
moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => {
|
['login', 'content', 'chat'],
|
||||||
const moduleInst = new module.getModule();
|
(category, next) => {
|
||||||
try {
|
moduleUtil.loadModulesForCategory(
|
||||||
moduleInst.createServer(err => {
|
`${category}Servers`,
|
||||||
if(err) {
|
(module, nextModule) => {
|
||||||
return nextModule(err);
|
const moduleInst = new module.getModule();
|
||||||
|
try {
|
||||||
|
moduleInst.createServer(err => {
|
||||||
|
if (err) {
|
||||||
|
return nextModule(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleInst.listen(err => {
|
||||||
|
if (err) {
|
||||||
|
return nextModule(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
listeningServers[module.moduleInfo.packageName] = {
|
||||||
|
instance: moduleInst,
|
||||||
|
info: module.moduleInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
return nextModule(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.log.error(e, 'Exception caught creating server!');
|
||||||
|
return nextModule(e);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
moduleInst.listen( err => {
|
err => {
|
||||||
if(err) {
|
return next(err);
|
||||||
return nextModule(err);
|
}
|
||||||
}
|
);
|
||||||
|
},
|
||||||
listeningServers[module.moduleInfo.packageName] = {
|
err => {
|
||||||
instance : moduleInst,
|
return cb(err);
|
||||||
info : module.moduleInfo,
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
return nextModule(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
logger.log.error(e, 'Exception caught creating server!');
|
|
||||||
return nextModule(e);
|
|
||||||
}
|
|
||||||
}, err => {
|
|
||||||
return next(err);
|
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
return cb(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,54 +2,56 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = class Log {
|
module.exports = class Log {
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
const Config = require('./config.js').get();
|
const Config = require('./config.js').get();
|
||||||
const logPath = Config.paths.logs;
|
const logPath = Config.paths.logs;
|
||||||
|
|
||||||
const err = this.checkLogPath(logPath);
|
const err = this.checkLogPath(logPath);
|
||||||
if(err) {
|
if (err) {
|
||||||
console.error(err.message); // eslint-disable-line no-console
|
console.error(err.message); // eslint-disable-line no-console
|
||||||
return process.exit();
|
return process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
const logStreams = [];
|
const logStreams = [];
|
||||||
if(_.isObject(Config.logging.rotatingFile)) {
|
if (_.isObject(Config.logging.rotatingFile)) {
|
||||||
Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName);
|
Config.logging.rotatingFile.path = paths.join(
|
||||||
|
logPath,
|
||||||
|
Config.logging.rotatingFile.fileName
|
||||||
|
);
|
||||||
logStreams.push(Config.logging.rotatingFile);
|
logStreams.push(Config.logging.rotatingFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializers = {
|
const serializers = {
|
||||||
err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc.
|
err: bunyan.stdSerializers.err, // handle 'err' fields with stack/etc.
|
||||||
};
|
};
|
||||||
|
|
||||||
// try to remove sensitive info by default, e.g. 'password' fields
|
// try to remove sensitive info by default, e.g. 'password' fields
|
||||||
[ 'formData', 'formValue' ].forEach(keyName => {
|
['formData', 'formValue'].forEach(keyName => {
|
||||||
serializers[keyName] = (fd) => Log.hideSensitive(fd);
|
serializers[keyName] = fd => Log.hideSensitive(fd);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.log = bunyan.createLogger({
|
this.log = bunyan.createLogger({
|
||||||
name : 'ENiGMA½ BBS',
|
name: 'ENiGMA½ BBS',
|
||||||
streams : logStreams,
|
streams: logStreams,
|
||||||
serializers : serializers,
|
serializers: serializers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static checkLogPath(logPath) {
|
static checkLogPath(logPath) {
|
||||||
try {
|
try {
|
||||||
if(!fs.statSync(logPath).isDirectory()) {
|
if (!fs.statSync(logPath).isDirectory()) {
|
||||||
return new Error(`${logPath} is not a directory`);
|
return new Error(`${logPath} is not a directory`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
if('ENOENT' === e.code) {
|
if ('ENOENT' === e.code) {
|
||||||
return new Error(`${logPath} does not exist`);
|
return new Error(`${logPath} does not exist`);
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
@@ -62,11 +64,14 @@ module.exports = class Log {
|
|||||||
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be
|
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be
|
||||||
//
|
//
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
|
JSON.stringify(obj).replace(
|
||||||
return `"${valueName}":"********"`;
|
/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/,
|
||||||
})
|
(match, valueName) => {
|
||||||
|
return `"${valueName}":"********"`;
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// be safe and return empty obj!
|
// be safe and return empty obj!
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config').get;
|
const Config = require('./config').get;
|
||||||
const logger = require('./logger.js');
|
const logger = require('./logger.js');
|
||||||
const ServerModule = require('./server_module.js').ServerModule;
|
const ServerModule = require('./server_module.js').ServerModule;
|
||||||
const clientConns = require('./client_connections.js');
|
const clientConns = require('./client_connections.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = class LoginServerModule extends ServerModule {
|
module.exports = class LoginServerModule extends ServerModule {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -19,7 +19,7 @@ module.exports = class LoginServerModule extends ServerModule {
|
|||||||
// :TODO: we need to max connections -- e.g. from config 'maxConnections'
|
// :TODO: we need to max connections -- e.g. from config 'maxConnections'
|
||||||
|
|
||||||
prepareClient(client, cb) {
|
prepareClient(client, cb) {
|
||||||
if(client.user.isAuthenticated()) {
|
if (client.user.isAuthenticated()) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ module.exports = class LoginServerModule extends ServerModule {
|
|||||||
// Choose initial theme before we have user context
|
// Choose initial theme before we have user context
|
||||||
//
|
//
|
||||||
const preLoginTheme = _.get(Config(), 'theme.preLogin');
|
const preLoginTheme = _.get(Config(), 'theme.preLogin');
|
||||||
if('*' === preLoginTheme) {
|
if ('*' === preLoginTheme) {
|
||||||
client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || '';
|
client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || '';
|
||||||
} else {
|
} else {
|
||||||
client.user.properties[UserProps.ThemeId] = preLoginTheme;
|
client.user.properties[UserProps.ThemeId] = preLoginTheme;
|
||||||
@@ -41,24 +41,25 @@ module.exports = class LoginServerModule extends ServerModule {
|
|||||||
|
|
||||||
handleNewClient(client, clientSock, modInfo) {
|
handleNewClient(client, clientSock, modInfo) {
|
||||||
clientSock.on('error', err => {
|
clientSock.on('error', err => {
|
||||||
logger.log.warn({ modInfo, error : err.message }, 'Client socket error');
|
logger.log.warn({ modInfo, error: err.message }, 'Client socket error');
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Start tracking the client. A session ID aka client ID
|
// Start tracking the client. A session ID aka client ID
|
||||||
// will be established in addNewClient() below.
|
// will be established in addNewClient() below.
|
||||||
//
|
//
|
||||||
if(_.isUndefined(client.session)) {
|
if (_.isUndefined(client.session)) {
|
||||||
client.session = {};
|
client.session = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
client.session.serverName = modInfo.name;
|
client.session.serverName = modInfo.name;
|
||||||
client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false);
|
client.session.isSecure = _.isBoolean(client.isSecure)
|
||||||
|
? client.isSecure
|
||||||
|
: modInfo.isSecure || false;
|
||||||
|
|
||||||
clientConns.addNewClient(client, clientSock);
|
clientConns.addNewClient(client, clientSock);
|
||||||
|
|
||||||
client.on('ready', readyOptions => {
|
client.on('ready', readyOptions => {
|
||||||
|
|
||||||
client.startIdleMonitor();
|
client.startIdleMonitor();
|
||||||
|
|
||||||
// Go to module -- use default error handler
|
// Go to module -- use default error handler
|
||||||
@@ -72,12 +73,15 @@ module.exports = class LoginServerModule extends ServerModule {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.on('error', err => {
|
client.on('error', err => {
|
||||||
logger.log.info({ nodeId : client.node, error : err.message }, 'Connection error');
|
logger.log.info(
|
||||||
|
{ nodeId: client.node, error: err.message },
|
||||||
|
'Connection error'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('close', err => {
|
client.on('close', err => {
|
||||||
const logFunc = err ? logger.log.info : logger.log.debug;
|
const logFunc = err ? logger.log.info : logger.log.debug;
|
||||||
logFunc( { nodeId : client.node }, 'Connection closed');
|
logFunc({ nodeId: client.node }, 'Connection closed');
|
||||||
|
|
||||||
clientConns.removeClient(client);
|
clientConns.removeClient(client);
|
||||||
});
|
});
|
||||||
@@ -86,7 +90,7 @@ module.exports = class LoginServerModule extends ServerModule {
|
|||||||
client.log.info('User idle timeout expired');
|
client.log.info('User idle timeout expired');
|
||||||
|
|
||||||
client.menuStack.goto('idleLogoff', err => {
|
client.menuStack.goto('idleLogoff', err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
// likely just doesn't exist
|
// likely just doesn't exist
|
||||||
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
||||||
client.end();
|
client.end();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var events = require('events');
|
var events = require('events');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
module.exports = MailPacket;
|
module.exports = MailPacket;
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ function MailPacket(options) {
|
|||||||
|
|
||||||
require('util').inherits(MailPacket, events.EventEmitter);
|
require('util').inherits(MailPacket, events.EventEmitter);
|
||||||
|
|
||||||
MailPacket.prototype.read = function(options) {
|
MailPacket.prototype.read = function (options) {
|
||||||
//
|
//
|
||||||
// options.packetPath | opts.packetBuffer: supplies a path-to-file
|
// options.packetPath | opts.packetBuffer: supplies a path-to-file
|
||||||
// or a buffer containing packet data
|
// or a buffer containing packet data
|
||||||
@@ -26,11 +26,11 @@ MailPacket.prototype.read = function(options) {
|
|||||||
assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer));
|
assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer));
|
||||||
};
|
};
|
||||||
|
|
||||||
MailPacket.prototype.write = function(options) {
|
MailPacket.prototype.write = function (options) {
|
||||||
//
|
//
|
||||||
// options.messages[]: array of message(s) to create packets from
|
// options.messages[]: array of message(s) to create packets from
|
||||||
//
|
//
|
||||||
// emits 'packet' event per packet constructed
|
// emits 'packet' event per packet constructed
|
||||||
//
|
//
|
||||||
assert(_.isArray(options.messages));
|
assert(_.isArray(options.messages));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Address = require('./ftn_address.js');
|
const Address = require('./ftn_address.js');
|
||||||
const Message = require('./message.js');
|
const Message = require('./message.js');
|
||||||
|
|
||||||
exports.getAddressedToInfo = getAddressedToInfo;
|
exports.getAddressedToInfo = getAddressedToInfo;
|
||||||
|
|
||||||
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
const EMAIL_REGEX =
|
||||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Input Output
|
Input Output
|
||||||
@@ -26,56 +27,72 @@ function getAddressedToInfo(input) {
|
|||||||
|
|
||||||
const firstAtPos = input.indexOf('@');
|
const firstAtPos = input.indexOf('@');
|
||||||
|
|
||||||
if(firstAtPos < 0) {
|
if (firstAtPos < 0) {
|
||||||
let addr = Address.fromString(input);
|
let addr = Address.fromString(input);
|
||||||
if(Address.isValidAddress(addr)) {
|
if (Address.isValidAddress(addr)) {
|
||||||
return { flavor : Message.AddressFlavor.FTN, remote : input };
|
return { flavor: Message.AddressFlavor.FTN, remote: input };
|
||||||
}
|
}
|
||||||
|
|
||||||
const lessThanPos = input.indexOf('<');
|
const lessThanPos = input.indexOf('<');
|
||||||
if(lessThanPos < 0) {
|
if (lessThanPos < 0) {
|
||||||
return { name : input, flavor : Message.AddressFlavor.Local };
|
return { name: input, flavor: Message.AddressFlavor.Local };
|
||||||
}
|
}
|
||||||
|
|
||||||
const greaterThanPos = input.indexOf('>');
|
const greaterThanPos = input.indexOf('>');
|
||||||
if(greaterThanPos < lessThanPos) {
|
if (greaterThanPos < lessThanPos) {
|
||||||
return { name : input, flavor : Message.AddressFlavor.Local };
|
return { name: input, flavor: Message.AddressFlavor.Local };
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
|
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
|
||||||
if(Address.isValidAddress(addr)) {
|
if (Address.isValidAddress(addr)) {
|
||||||
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
|
return {
|
||||||
|
name: input.slice(0, lessThanPos).trim(),
|
||||||
|
flavor: Message.AddressFlavor.FTN,
|
||||||
|
remote: addr.toString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name : input, flavor : Message.AddressFlavor.Local };
|
return { name: input, flavor: Message.AddressFlavor.Local };
|
||||||
}
|
}
|
||||||
|
|
||||||
const lessThanPos = input.indexOf('<');
|
const lessThanPos = input.indexOf('<');
|
||||||
const greaterThanPos = input.indexOf('>');
|
const greaterThanPos = input.indexOf('>');
|
||||||
if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
|
if (lessThanPos > 0 && greaterThanPos > lessThanPos) {
|
||||||
const addr = input.slice(lessThanPos + 1, greaterThanPos);
|
const addr = input.slice(lessThanPos + 1, greaterThanPos);
|
||||||
const m = addr.match(EMAIL_REGEX);
|
const m = addr.match(EMAIL_REGEX);
|
||||||
if(m) {
|
if (m) {
|
||||||
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr };
|
return {
|
||||||
|
name: input.slice(0, lessThanPos).trim(),
|
||||||
|
flavor: Message.AddressFlavor.Email,
|
||||||
|
remote: addr,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name : input, flavor : Message.AddressFlavor.Local };
|
return { name: input, flavor: Message.AddressFlavor.Local };
|
||||||
}
|
}
|
||||||
|
|
||||||
let m = input.match(EMAIL_REGEX);
|
let m = input.match(EMAIL_REGEX);
|
||||||
if(m) {
|
if (m) {
|
||||||
return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input };
|
return {
|
||||||
|
name: input.slice(0, firstAtPos),
|
||||||
|
flavor: Message.AddressFlavor.Email,
|
||||||
|
remote: input,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let addr = Address.fromString(input); // 5D?
|
let addr = Address.fromString(input); // 5D?
|
||||||
if(Address.isValidAddress(addr)) {
|
if (Address.isValidAddress(addr)) {
|
||||||
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
|
return { flavor: Message.AddressFlavor.FTN, remote: addr.toString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = Address.fromString(input.slice(firstAtPos + 1).trim());
|
addr = Address.fromString(input.slice(firstAtPos + 1).trim());
|
||||||
if(Address.isValidAddress(addr)) {
|
if (Address.isValidAddress(addr)) {
|
||||||
return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
|
return {
|
||||||
|
name: input.slice(0, firstAtPos).trim(),
|
||||||
|
flavor: Message.AddressFlavor.FTN,
|
||||||
|
remote: addr.toString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name : input, flavor : Message.AddressFlavor.Local };
|
return { name: input, flavor: Message.AddressFlavor.Local };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var TextView = require('./text_view.js').TextView;
|
var TextView = require('./text_view.js').TextView;
|
||||||
var miscUtil = require('./misc_util.js');
|
var miscUtil = require('./misc_util.js');
|
||||||
var strUtil = require('./string_util.js');
|
var strUtil = require('./string_util.js');
|
||||||
var ansi = require('./ansi_term.js');
|
var ansi = require('./ansi_term.js');
|
||||||
|
|
||||||
//var util = require('util');
|
//var util = require('util');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
exports.MaskEditTextView = MaskEditTextView;
|
exports.MaskEditTextView = MaskEditTextView;
|
||||||
|
|
||||||
// ##/##/#### <--styleSGR2 if fillChar
|
// ##/##/#### <--styleSGR2 if fillChar
|
||||||
// ^- styleSGR1
|
// ^- styleSGR1
|
||||||
@@ -29,59 +29,71 @@ exports.MaskEditTextView = MaskEditTextView;
|
|||||||
// * There exists some sort of condition that allows pattern position to get out of sync
|
// * There exists some sort of condition that allows pattern position to get out of sync
|
||||||
|
|
||||||
function MaskEditTextView(options) {
|
function MaskEditTextView(options) {
|
||||||
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
||||||
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
||||||
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
||||||
options.resizable = false;
|
options.resizable = false;
|
||||||
|
|
||||||
TextView.call(this, options);
|
TextView.call(this, options);
|
||||||
|
|
||||||
this.initDefaultWidth();
|
this.initDefaultWidth();
|
||||||
|
|
||||||
this.cursorPos = { x : 0 };
|
this.cursorPos = { x: 0 };
|
||||||
this.patternArrayPos = 0;
|
this.patternArrayPos = 0;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.maskPattern = options.maskPattern || '';
|
this.maskPattern = options.maskPattern || '';
|
||||||
|
|
||||||
this.clientBackspace = function() {
|
this.clientBackspace = function () {
|
||||||
var fillCharSGR = this.getStyleSGR(3) || this.getSGR();
|
var fillCharSGR = this.getStyleSGR(3) || this.getSGR();
|
||||||
this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR());
|
this.client.term.write(
|
||||||
|
'\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.drawText = function(s) {
|
this.drawText = function (s) {
|
||||||
var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle);
|
var textToDraw = strUtil.stylizeString(
|
||||||
|
s,
|
||||||
|
this.hasFocus ? this.focusTextStyle : this.textStyle
|
||||||
|
);
|
||||||
|
|
||||||
assert(textToDraw.length <= self.patternArray.length);
|
assert(textToDraw.length <= self.patternArray.length);
|
||||||
|
|
||||||
// draw out the text we have so far
|
// draw out the text we have so far
|
||||||
var i = 0;
|
var i = 0;
|
||||||
var t = 0;
|
var t = 0;
|
||||||
while(i < self.patternArray.length) {
|
while (i < self.patternArray.length) {
|
||||||
if(_.isRegExp(self.patternArray[i])) {
|
if (_.isRegExp(self.patternArray[i])) {
|
||||||
if(t < textToDraw.length) {
|
if (t < textToDraw.length) {
|
||||||
self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]);
|
self.client.term.write(
|
||||||
|
(self.hasFocus ? self.getFocusSGR() : self.getSGR()) +
|
||||||
|
textToDraw[t]
|
||||||
|
);
|
||||||
t++;
|
t++;
|
||||||
} else {
|
} else {
|
||||||
self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar);
|
self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || '');
|
var styleSgr = this.hasFocus
|
||||||
|
? self.getStyleSGR(2) || ''
|
||||||
|
: self.getStyleSGR(1) || '';
|
||||||
self.client.term.write(styleSgr + self.maskPattern[i]);
|
self.client.term.write(styleSgr + self.maskPattern[i]);
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.buildPattern = function() {
|
this.buildPattern = function () {
|
||||||
self.patternArray = [];
|
self.patternArray = [];
|
||||||
self.maxLength = 0;
|
self.maxLength = 0;
|
||||||
|
|
||||||
for(var i = 0; i < self.maskPattern.length; i++) {
|
for (var i = 0; i < self.maskPattern.length; i++) {
|
||||||
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
|
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
|
||||||
if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
|
if (self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
|
||||||
self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]);
|
self.patternArray.push(
|
||||||
|
MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]
|
||||||
|
);
|
||||||
++self.maxLength;
|
++self.maxLength;
|
||||||
} else {
|
} else {
|
||||||
self.patternArray.push(self.maskPattern[i]);
|
self.patternArray.push(self.maskPattern[i]);
|
||||||
@@ -89,53 +101,58 @@ function MaskEditTextView(options) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getEndOfTextColumn = function() {
|
this.getEndOfTextColumn = function () {
|
||||||
return this.position.col + this.patternArrayPos;
|
return this.position.col + this.patternArrayPos;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.buildPattern();
|
this.buildPattern();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require('util').inherits(MaskEditTextView, TextView);
|
require('util').inherits(MaskEditTextView, TextView);
|
||||||
|
|
||||||
MaskEditTextView.maskPatternCharacterRegEx = {
|
MaskEditTextView.maskPatternCharacterRegEx = {
|
||||||
'#' : /[0-9]/, // Numeric
|
'#': /[0-9]/, // Numeric
|
||||||
'A' : /[a-zA-Z]/, // Alpha
|
A: /[a-zA-Z]/, // Alpha
|
||||||
'@' : /[0-9a-zA-Z]/, // Alphanumeric
|
'@': /[0-9a-zA-Z]/, // Alphanumeric
|
||||||
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255
|
'&': /[\w\d\s]/, // Any "printable" 32-126, 128-255
|
||||||
};
|
};
|
||||||
|
|
||||||
MaskEditTextView.prototype.setText = function(text) {
|
MaskEditTextView.prototype.setText = function (text) {
|
||||||
MaskEditTextView.super_.prototype.setText.call(this, text);
|
MaskEditTextView.super_.prototype.setText.call(this, text);
|
||||||
|
|
||||||
if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText()
|
if (this.patternArray) {
|
||||||
|
// :TODO: This is a hack - see TextView ctor note about setText()
|
||||||
this.patternArrayPos = this.patternArray.length;
|
this.patternArrayPos = this.patternArray.length;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
MaskEditTextView.prototype.setMaskPattern = function(pattern) {
|
MaskEditTextView.prototype.setMaskPattern = function (pattern) {
|
||||||
this.dimens.width = pattern.length;
|
this.dimens.width = pattern.length;
|
||||||
|
|
||||||
this.maskPattern = pattern;
|
this.maskPattern = pattern;
|
||||||
this.buildPattern();
|
this.buildPattern();
|
||||||
};
|
};
|
||||||
|
|
||||||
MaskEditTextView.prototype.onKeyPress = function(ch, key) {
|
MaskEditTextView.prototype.onKeyPress = function (ch, key) {
|
||||||
if(key) {
|
if (key) {
|
||||||
if(this.isKeyMapped('backspace', key.name)) {
|
if (this.isKeyMapped('backspace', key.name)) {
|
||||||
if(this.text.length > 0) {
|
if (this.text.length > 0) {
|
||||||
this.patternArrayPos--;
|
this.patternArrayPos--;
|
||||||
assert(this.patternArrayPos >= 0);
|
assert(this.patternArrayPos >= 0);
|
||||||
|
|
||||||
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
if (_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
||||||
this.text = this.text.substr(0, this.text.length - 1);
|
this.text = this.text.substr(0, this.text.length - 1);
|
||||||
this.clientBackspace();
|
this.clientBackspace();
|
||||||
} else {
|
} else {
|
||||||
while(this.patternArrayPos >= 0) {
|
while (this.patternArrayPos >= 0) {
|
||||||
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
if (_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
||||||
this.text = this.text.substr(0, this.text.length - 1);
|
this.text = this.text.substr(0, this.text.length - 1);
|
||||||
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));
|
this.client.term.write(
|
||||||
|
ansi.goto(
|
||||||
|
this.position.row,
|
||||||
|
this.getEndOfTextColumn() + 1
|
||||||
|
)
|
||||||
|
);
|
||||||
this.clientBackspace();
|
this.clientBackspace();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -145,62 +162,67 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if(this.isKeyMapped('clearLine', key.name)) {
|
} else if (this.isKeyMapped('clearLine', key.name)) {
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.patternArrayPos = 0;
|
this.patternArrayPos = 0;
|
||||||
this.setFocus(true); // redraw + adjust cursor
|
this.setFocus(true); // redraw + adjust cursor
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ch && strUtil.isPrintable(ch)) {
|
if (ch && strUtil.isPrintable(ch)) {
|
||||||
if(this.text.length < this.maxLength) {
|
if (this.text.length < this.maxLength) {
|
||||||
ch = strUtil.stylizeString(ch, this.textStyle);
|
ch = strUtil.stylizeString(ch, this.textStyle);
|
||||||
|
|
||||||
if(!ch.match(this.patternArray[this.patternArrayPos])) {
|
if (!ch.match(this.patternArray[this.patternArrayPos])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.text += ch;
|
this.text += ch;
|
||||||
this.patternArrayPos++;
|
this.patternArrayPos++;
|
||||||
|
|
||||||
while(this.patternArrayPos < this.patternArray.length &&
|
while (
|
||||||
!_.isRegExp(this.patternArray[this.patternArrayPos]))
|
this.patternArrayPos < this.patternArray.length &&
|
||||||
{
|
!_.isRegExp(this.patternArray[this.patternArrayPos])
|
||||||
|
) {
|
||||||
this.patternArrayPos++;
|
this.patternArrayPos++;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.redraw();
|
this.redraw();
|
||||||
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn()));
|
this.client.term.write(
|
||||||
|
ansi.goto(this.position.row, this.getEndOfTextColumn())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
MaskEditTextView.prototype.setPropertyValue = function(propName, value) {
|
MaskEditTextView.prototype.setPropertyValue = function (propName, value) {
|
||||||
switch(propName) {
|
switch (propName) {
|
||||||
case 'maskPattern' : this.setMaskPattern(value); break;
|
case 'maskPattern':
|
||||||
|
this.setMaskPattern(value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
|
MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
MaskEditTextView.prototype.getData = function() {
|
MaskEditTextView.prototype.getData = function () {
|
||||||
var rawData = MaskEditTextView.super_.prototype.getData.call(this);
|
var rawData = MaskEditTextView.super_.prototype.getData.call(this);
|
||||||
|
|
||||||
if(!rawData || 0 === rawData.length) {
|
if (!rawData || 0 === rawData.length) {
|
||||||
return rawData;
|
return rawData;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = '';
|
var data = '';
|
||||||
|
|
||||||
assert(rawData.length <= this.patternArray.length);
|
assert(rawData.length <= this.patternArray.length);
|
||||||
|
|
||||||
var p = 0;
|
var p = 0;
|
||||||
for(var i = 0; i < this.patternArray.length; ++i) {
|
for (var i = 0; i < this.patternArray.length; ++i) {
|
||||||
if(_.isRegExp(this.patternArray[i])) {
|
if (_.isRegExp(this.patternArray[i])) {
|
||||||
data += rawData[p++];
|
data += rawData[p++];
|
||||||
} else {
|
} else {
|
||||||
data += this.patternArray[i];
|
data += this.patternArray[i];
|
||||||
|
|||||||
14
core/mbf.js
14
core/mbf.js
@@ -9,7 +9,7 @@ const { Errors } = require('./enig_error');
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Number to 32bit MBF
|
// Number to 32bit MBF
|
||||||
const numToMbf32 = (v) => {
|
const numToMbf32 = v => {
|
||||||
const mbf = Buffer.alloc(4);
|
const mbf = Buffer.alloc(4);
|
||||||
|
|
||||||
if (0 === v) {
|
if (0 === v) {
|
||||||
@@ -19,8 +19,8 @@ const numToMbf32 = (v) => {
|
|||||||
const ieee = Buffer.alloc(4);
|
const ieee = Buffer.alloc(4);
|
||||||
ieee.writeFloatLE(v, 0);
|
ieee.writeFloatLE(v, 0);
|
||||||
|
|
||||||
const sign = ieee[3] & 0x80;
|
const sign = ieee[3] & 0x80;
|
||||||
let exp = (ieee[3] << 1) | (ieee[2] >> 7);
|
let exp = (ieee[3] << 1) | (ieee[2] >> 7);
|
||||||
|
|
||||||
if (exp === 0xfe) {
|
if (exp === 0xfe) {
|
||||||
throw Errors.Invalid(`${v} cannot be converted to mbf`);
|
throw Errors.Invalid(`${v} cannot be converted to mbf`);
|
||||||
@@ -36,14 +36,14 @@ const numToMbf32 = (v) => {
|
|||||||
return mbf;
|
return mbf;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mbf32ToNum = (mbf) => {
|
const mbf32ToNum = mbf => {
|
||||||
if (0 === mbf[3]) {
|
if (0 === mbf[3]) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ieee = Buffer.alloc(4);
|
const ieee = Buffer.alloc(4);
|
||||||
const sign = mbf[2] & 0x80;
|
const sign = mbf[2] & 0x80;
|
||||||
const exp = mbf[3] - 2;
|
const exp = mbf[3] - 2;
|
||||||
|
|
||||||
ieee[3] = sign | (exp >> 1);
|
ieee[3] = sign | (exp >> 1);
|
||||||
ieee[2] = (exp << 7) | (mbf[2] & 0x7f);
|
ieee[2] = (exp << 7) | (mbf[2] & 0x7f);
|
||||||
|
|||||||
@@ -2,33 +2,45 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const TextView = require('./text_view.js').TextView;
|
const TextView = require('./text_view.js').TextView;
|
||||||
const View = require('./view.js').View;
|
const View = require('./view.js').View;
|
||||||
const EditTextView = require('./edit_text_view.js').EditTextView;
|
const EditTextView = require('./edit_text_view.js').EditTextView;
|
||||||
const ButtonView = require('./button_view.js').ButtonView;
|
const ButtonView = require('./button_view.js').ButtonView;
|
||||||
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
|
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
|
||||||
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
|
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
|
||||||
const FullMenuView = require('./full_menu_view.js').FullMenuView;
|
const FullMenuView = require('./full_menu_view.js').FullMenuView;
|
||||||
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
|
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
|
||||||
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
|
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
|
||||||
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
|
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
|
||||||
const KeyEntryView = require('./key_entry_view.js');
|
const KeyEntryView = require('./key_entry_view.js');
|
||||||
const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView;
|
const MultiLineEditTextView =
|
||||||
|
require('./multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||||
const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue;
|
const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue;
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.MCIViewFactory = MCIViewFactory;
|
exports.MCIViewFactory = MCIViewFactory;
|
||||||
|
|
||||||
function MCIViewFactory(client) {
|
function MCIViewFactory(client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
MCIViewFactory.UserViewCodes = [
|
MCIViewFactory.UserViewCodes = [
|
||||||
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE',
|
'TL',
|
||||||
|
'ET',
|
||||||
|
'ME',
|
||||||
|
'MT',
|
||||||
|
'PL',
|
||||||
|
'BT',
|
||||||
|
'VM',
|
||||||
|
'HM',
|
||||||
|
'FM',
|
||||||
|
'SM',
|
||||||
|
'TM',
|
||||||
|
'KE',
|
||||||
|
|
||||||
//
|
//
|
||||||
// XY is a special MCI code that allows finding positions
|
// XY is a special MCI code that allows finding positions
|
||||||
@@ -38,34 +50,32 @@ MCIViewFactory.UserViewCodes = [
|
|||||||
'XY',
|
'XY',
|
||||||
];
|
];
|
||||||
|
|
||||||
MCIViewFactory.MovementCodes = [
|
MCIViewFactory.MovementCodes = ['CF', 'CB', 'CU', 'CD'];
|
||||||
'CF', 'CB', 'CU', 'CD',
|
|
||||||
];
|
|
||||||
|
|
||||||
MCIViewFactory.prototype.createFromMCI = function(mci) {
|
MCIViewFactory.prototype.createFromMCI = function (mci) {
|
||||||
assert(mci.code);
|
assert(mci.code);
|
||||||
assert(mci.id > 0);
|
assert(mci.id > 0);
|
||||||
assert(mci.position);
|
assert(mci.position);
|
||||||
|
|
||||||
var view;
|
var view;
|
||||||
var options = {
|
var options = {
|
||||||
client : this.client,
|
client: this.client,
|
||||||
id : mci.id,
|
id: mci.id,
|
||||||
ansiSGR : mci.SGR,
|
ansiSGR: mci.SGR,
|
||||||
ansiFocusSGR : mci.focusSGR,
|
ansiFocusSGR: mci.focusSGR,
|
||||||
position : { row : mci.position[0], col : mci.position[1] },
|
position: { row: mci.position[0], col: mci.position[1] },
|
||||||
};
|
};
|
||||||
|
|
||||||
// :TODO: These should use setPropertyValue()!
|
// :TODO: These should use setPropertyValue()!
|
||||||
function setOption(pos, name) {
|
function setOption(pos, name) {
|
||||||
if(mci.args.length > pos && mci.args[pos].length > 0) {
|
if (mci.args.length > pos && mci.args[pos].length > 0) {
|
||||||
options[name] = mci.args[pos];
|
options[name] = mci.args[pos];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWidth(pos) {
|
function setWidth(pos) {
|
||||||
if(mci.args.length > pos && mci.args[pos].length > 0) {
|
if (mci.args.length > pos && mci.args[pos].length > 0) {
|
||||||
if(!_.isObject(options.dimens)) {
|
if (!_.isObject(options.dimens)) {
|
||||||
options.dimens = {};
|
options.dimens = {};
|
||||||
}
|
}
|
||||||
options.dimens.width = parseInt(mci.args[pos], 10);
|
options.dimens.width = parseInt(mci.args[pos], 10);
|
||||||
@@ -73,7 +83,11 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setFocusOption(pos, name) {
|
function setFocusOption(pos, name) {
|
||||||
if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) {
|
if (
|
||||||
|
mci.focusArgs &&
|
||||||
|
mci.focusArgs.length > pos &&
|
||||||
|
mci.focusArgs[pos].length > 0
|
||||||
|
) {
|
||||||
options[name] = mci.focusArgs[pos];
|
options[name] = mci.focusArgs[pos];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,46 +95,46 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
|||||||
//
|
//
|
||||||
// Note: Keep this in sync with UserViewCodes above!
|
// Note: Keep this in sync with UserViewCodes above!
|
||||||
//
|
//
|
||||||
switch(mci.code) {
|
switch (mci.code) {
|
||||||
// Text Label (Text View)
|
// Text Label (Text View)
|
||||||
case 'TL' :
|
case 'TL':
|
||||||
setOption(0, 'textStyle');
|
setOption(0, 'textStyle');
|
||||||
setOption(1, 'justify');
|
setOption(1, 'justify');
|
||||||
setWidth(2);
|
setWidth(2);
|
||||||
|
|
||||||
view = new TextView(options);
|
view = new TextView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Edit Text
|
// Edit Text
|
||||||
case 'ET' :
|
case 'ET':
|
||||||
setWidth(0);
|
setWidth(0);
|
||||||
|
|
||||||
setOption(1, 'textStyle');
|
setOption(1, 'textStyle');
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new EditTextView(options);
|
view = new EditTextView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Masked Edit Text
|
// Masked Edit Text
|
||||||
case 'ME' :
|
case 'ME':
|
||||||
setOption(0, 'textStyle');
|
setOption(0, 'textStyle');
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new MaskEditTextView(options);
|
view = new MaskEditTextView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Multi Line Edit Text
|
// Multi Line Edit Text
|
||||||
case 'MT' :
|
case 'MT':
|
||||||
// :TODO: apply params
|
// :TODO: apply params
|
||||||
view = new MultiLineEditTextView(options);
|
view = new MultiLineEditTextView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Pre-defined Label (Text View)
|
// Pre-defined Label (Text View)
|
||||||
// :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove
|
// :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove
|
||||||
case 'PL' :
|
case 'PL':
|
||||||
if(mci.args.length > 0) {
|
if (mci.args.length > 0) {
|
||||||
options.text = getPredefinedMCIValue(this.client, mci.args[0]);
|
options.text = getPredefinedMCIValue(this.client, mci.args[0]);
|
||||||
if(options.text) {
|
if (options.text) {
|
||||||
setOption(1, 'textStyle');
|
setOption(1, 'textStyle');
|
||||||
setOption(2, 'justify');
|
setOption(2, 'justify');
|
||||||
setWidth(3);
|
setWidth(3);
|
||||||
@@ -130,10 +144,10 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
case 'BT' :
|
case 'BT':
|
||||||
if(mci.args.length > 0) {
|
if (mci.args.length > 0) {
|
||||||
options.dimens = { width : parseInt(mci.args[0], 10) };
|
options.dimens = { width: parseInt(mci.args[0], 10) };
|
||||||
}
|
}
|
||||||
|
|
||||||
setOption(1, 'textStyle');
|
setOption(1, 'textStyle');
|
||||||
@@ -144,78 +158,78 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
|||||||
view = new ButtonView(options);
|
view = new ButtonView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Vertial Menu
|
// Vertial Menu
|
||||||
case 'VM' :
|
case 'VM':
|
||||||
setOption(0, 'itemSpacing');
|
setOption(0, 'itemSpacing');
|
||||||
setOption(1, 'justify');
|
setOption(1, 'justify');
|
||||||
setOption(2, 'textStyle');
|
setOption(2, 'textStyle');
|
||||||
|
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new VerticalMenuView(options);
|
view = new VerticalMenuView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Horizontal Menu
|
// Horizontal Menu
|
||||||
case 'HM' :
|
case 'HM':
|
||||||
setOption(0, 'itemSpacing');
|
setOption(0, 'itemSpacing');
|
||||||
setOption(1, 'textStyle');
|
setOption(1, 'textStyle');
|
||||||
|
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new HorizontalMenuView(options);
|
view = new HorizontalMenuView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Full Menu
|
// Full Menu
|
||||||
case 'FM' :
|
case 'FM':
|
||||||
setOption(0, 'itemSpacing');
|
setOption(0, 'itemSpacing');
|
||||||
setOption(1, 'itemHorizSpacing');
|
setOption(1, 'itemHorizSpacing');
|
||||||
setOption(2, 'justify');
|
setOption(2, 'justify');
|
||||||
setOption(3, 'textStyle');
|
setOption(3, 'textStyle');
|
||||||
|
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new FullMenuView(options);
|
view = new FullMenuView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'SM' :
|
case 'SM':
|
||||||
setOption(0, 'textStyle');
|
setOption(0, 'textStyle');
|
||||||
setOption(1, 'justify');
|
setOption(1, 'justify');
|
||||||
|
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new SpinnerMenuView(options);
|
view = new SpinnerMenuView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'TM' :
|
case 'TM':
|
||||||
if(mci.args.length > 0) {
|
if (mci.args.length > 0) {
|
||||||
var styleSG1 = { fg : parseInt(mci.args[0], 10) };
|
var styleSG1 = { fg: parseInt(mci.args[0], 10) };
|
||||||
if(mci.args.length > 1) {
|
if (mci.args.length > 1) {
|
||||||
styleSG1.bg = parseInt(mci.args[1], 10);
|
styleSG1.bg = parseInt(mci.args[1], 10);
|
||||||
}
|
}
|
||||||
options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true);
|
options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFocusOption(0, 'focusTextStyle');
|
setFocusOption(0, 'focusTextStyle');
|
||||||
|
|
||||||
view = new ToggleMenuView(options);
|
view = new ToggleMenuView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'KE' :
|
case 'KE':
|
||||||
view = new KeyEntryView(options);
|
view = new KeyEntryView(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'XY' :
|
case 'XY':
|
||||||
view = new View(options);
|
view = new View(options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default :
|
default:
|
||||||
if(!MCIViewFactory.MovementCodes.includes(mci.code)) {
|
if (!MCIViewFactory.MovementCodes.includes(mci.code)) {
|
||||||
options.text = getPredefinedMCIValue(this.client, mci.code);
|
options.text = getPredefinedMCIValue(this.client, mci.code);
|
||||||
if(_.isString(options.text)) {
|
if (_.isString(options.text)) {
|
||||||
setWidth(0);
|
setWidth(0);
|
||||||
|
|
||||||
setOption(1, 'textStyle');
|
setOption(1, 'textStyle');
|
||||||
setOption(2, 'justify');
|
setOption(2, 'justify');
|
||||||
|
|
||||||
view = new TextView(options);
|
view = new TextView(options);
|
||||||
}
|
}
|
||||||
@@ -223,7 +237,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(view) {
|
if (view) {
|
||||||
view.mciCode = mci.code;
|
view.mciCode = mci.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,52 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const PluginModule = require('./plugin_module.js').PluginModule;
|
const PluginModule = require('./plugin_module.js').PluginModule;
|
||||||
const theme = require('./theme.js');
|
const theme = require('./theme.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const menuUtil = require('./menu_util.js');
|
const menuUtil = require('./menu_util.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const stringFormat = require('../core/string_format.js');
|
const stringFormat = require('../core/string_format.js');
|
||||||
const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
const MultiLineEditTextView =
|
||||||
const Errors = require('../core/enig_error.js').Errors;
|
require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||||
|
const Errors = require('../core/enig_error.js').Errors;
|
||||||
const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
|
const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
|
||||||
const EnigAssert = require('./enigma_assert');
|
const EnigAssert = require('./enigma_assert');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const iconvDecode = require('iconv-lite').decode;
|
const iconvDecode = require('iconv-lite').decode;
|
||||||
|
|
||||||
exports.MenuModule = class MenuModule extends PluginModule {
|
exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.menuName = options.menuName;
|
this.menuName = options.menuName;
|
||||||
this.menuConfig = options.menuConfig;
|
this.menuConfig = options.menuConfig;
|
||||||
this.client = options.client;
|
this.client = options.client;
|
||||||
this.menuMethods = {}; // methods called from @method's
|
this.menuMethods = {}; // methods called from @method's
|
||||||
this.menuConfig.config = this.menuConfig.config || {};
|
this.menuConfig.config = this.menuConfig.config || {};
|
||||||
this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls);
|
this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls);
|
||||||
this.viewControllers = {};
|
this.viewControllers = {};
|
||||||
this.interrupt = (_.get(this.menuConfig.config, 'interrupt', MenuModule.InterruptTypes.Queued)).toLowerCase();
|
this.interrupt = _.get(
|
||||||
|
this.menuConfig.config,
|
||||||
|
'interrupt',
|
||||||
|
MenuModule.InterruptTypes.Queued
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
if(MenuModule.InterruptTypes.Realtime === this.interrupt) {
|
if (MenuModule.InterruptTypes.Realtime === this.interrupt) {
|
||||||
this.realTimeInterrupt = 'blocked';
|
this.realTimeInterrupt = 'blocked';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get InterruptTypes() {
|
static get InterruptTypes() {
|
||||||
return {
|
return {
|
||||||
Never : 'never',
|
Never: 'never',
|
||||||
Queued : 'queued',
|
Queued: 'queued',
|
||||||
Realtime : 'realtime',
|
Realtime: 'realtime',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,13 +59,16 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
const self = this;
|
const self = this;
|
||||||
const mciData = {};
|
const mciData = {};
|
||||||
let pausePosition = {row: 0, column: 0};
|
let pausePosition = { row: 0, column: 0 };
|
||||||
|
|
||||||
const hasArt = () => {
|
const hasArt = () => {
|
||||||
return _.isString(self.menuConfig.art) ||
|
return (
|
||||||
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
|
_.isString(self.menuConfig.art) ||
|
||||||
|
(Array.isArray(self.menuConfig.art) &&
|
||||||
|
_.has(self.menuConfig.art[0], 'acs'))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
@@ -73,7 +80,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
return self.beforeArt(callback);
|
return self.beforeArt(callback);
|
||||||
},
|
},
|
||||||
function displayMenuArt(callback) {
|
function displayMenuArt(callback) {
|
||||||
if(!hasArt()) {
|
if (!hasArt()) {
|
||||||
return callback(null, null);
|
return callback(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,32 +88,39 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
self.menuConfig.art,
|
self.menuConfig.art,
|
||||||
self.menuConfig.config,
|
self.menuConfig.config,
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } );
|
self.client.log.trace('Could not display art', {
|
||||||
|
art: self.menuConfig.art,
|
||||||
|
reason: err.message,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
mciData.menu = artData.mciMap;
|
mciData.menu = artData.mciMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(artData) {
|
if (artData) {
|
||||||
pausePosition.row = artData.height + 1;
|
pausePosition.row = artData.height + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null, artData); // any errors are non-fatal
|
return callback(null, artData); // any errors are non-fatal
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function displayPromptArt(artData, callback) {
|
function displayPromptArt(artData, callback) {
|
||||||
if(!_.isString(self.menuConfig.prompt)) {
|
if (!_.isString(self.menuConfig.prompt)) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_.isObject(self.menuConfig.promptConfig)) {
|
if (!_.isObject(self.menuConfig.promptConfig)) {
|
||||||
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
|
return callback(
|
||||||
|
Errors.MissingConfig(
|
||||||
|
'Prompt specified but no "promptConfig" block found'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = Object.assign({}, self.menuConfig.config);
|
const options = Object.assign({}, self.menuConfig.config);
|
||||||
|
|
||||||
if(_.isNumber(artData?.height)) {
|
if (_.isNumber(artData?.height)) {
|
||||||
options.startRow = artData.height + 1;
|
options.startRow = artData.height + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +128,12 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
self.menuConfig.promptConfig.art,
|
self.menuConfig.promptConfig.art,
|
||||||
options,
|
options,
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
if(artData) {
|
if (artData) {
|
||||||
mciData.prompt = artData.mciMap;
|
mciData.prompt = artData.mciMap;
|
||||||
pausePosition.row = artData.height + 1;
|
pausePosition.row = artData.height + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(err); // pass err here; prompts *must* have art
|
return callback(err); // pass err here; prompts *must* have art
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -127,11 +141,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
return self.mciReady(mciData, callback);
|
return self.mciReady(mciData, callback);
|
||||||
},
|
},
|
||||||
function displayPauseIfRequested(callback) {
|
function displayPauseIfRequested(callback) {
|
||||||
if(!self.shouldPause()) {
|
if (!self.shouldPause()) {
|
||||||
return callback(null, null);
|
return callback(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) {
|
if (
|
||||||
|
self.client.term.termHeight > 0 &&
|
||||||
|
pausePosition.row > self.client.termHeight
|
||||||
|
) {
|
||||||
// If this scrolled, the prompt will go to the bottom of the screen
|
// If this scrolled, the prompt will go to the bottom of the screen
|
||||||
pausePosition.row = self.client.termHeight;
|
pausePosition.row = self.client.termHeight;
|
||||||
}
|
}
|
||||||
@@ -142,25 +159,31 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
self.finishedLoading();
|
self.finishedLoading();
|
||||||
self.realTimeInterrupt = 'allowed';
|
self.realTimeInterrupt = 'allowed';
|
||||||
return self.autoNextMenu(callback);
|
return self.autoNextMenu(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
self.client.log.warn('Error during init sequence', { error : err.message } );
|
self.client.log.warn('Error during init sequence', {
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
|
||||||
return self.prevMenu( () => { /* dummy */ } );
|
return self.prevMenu(() => {
|
||||||
|
/* dummy */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeArt(cb) {
|
beforeArt(cb) {
|
||||||
if(_.isNumber(this.menuConfig.config.baudRate)) {
|
if (_.isNumber(this.menuConfig.config.baudRate)) {
|
||||||
// :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here
|
// :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here
|
||||||
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate));
|
this.client.term.rawWrite(
|
||||||
|
ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.cls) {
|
if (this.cls) {
|
||||||
this.client.term.rawWrite(ansi.resetScreen());
|
this.client.term.rawWrite(ansi.resetScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,14 +200,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayQueuedInterruptions(cb) {
|
displayQueuedInterruptions(cb) {
|
||||||
if(MenuModule.InterruptTypes.Never === this.interrupt) {
|
if (MenuModule.InterruptTypes.Never === this.interrupt) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let opts = { cls : true }; // clear screen for first message
|
let opts = { cls: true }; // clear screen for first message
|
||||||
|
|
||||||
async.whilst(
|
async.whilst(
|
||||||
(callback) => callback(null, this.client.interruptQueue.hasItems()),
|
callback => callback(null, this.client.interruptQueue.hasItems()),
|
||||||
next => {
|
next => {
|
||||||
this.client.interruptQueue.displayNext(opts, err => {
|
this.client.interruptQueue.displayNext(opts, err => {
|
||||||
opts = {};
|
opts = {};
|
||||||
@@ -198,7 +221,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attemptInterruptNow(interruptItem, cb) {
|
attemptInterruptNow(interruptItem, cb) {
|
||||||
if(this.realTimeInterrupt !== 'allowed' || MenuModule.InterruptTypes.Realtime !== this.interrupt) {
|
if (
|
||||||
|
this.realTimeInterrupt !== 'allowed' ||
|
||||||
|
MenuModule.InterruptTypes.Realtime !== this.interrupt
|
||||||
|
) {
|
||||||
return cb(null, false); // don't eat up the item; queue for later
|
return cb(null, false); // don't eat up the item; queue for later
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,15 +239,16 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.client.interruptQueue.displayWithItem(
|
this.client.interruptQueue.displayWithItem(
|
||||||
Object.assign({}, interruptItem, { cls : true }),
|
Object.assign({}, interruptItem, { cls: true }),
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return done(err, false);
|
return done(err, false);
|
||||||
}
|
}
|
||||||
this.reload(err => {
|
this.reload(err => {
|
||||||
return done(err, err ? false : true);
|
return done(err, err ? false : true);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSaveState() {
|
getSaveState() {
|
||||||
@@ -238,17 +265,17 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextMenu(cb) {
|
nextMenu(cb) {
|
||||||
if(!this.haveNext()) {
|
if (!this.haveNext()) {
|
||||||
return this.prevMenu(cb); // no next, go to prev
|
return this.prevMenu(cb); // no next, go to prev
|
||||||
}
|
}
|
||||||
|
|
||||||
this.displayQueuedInterruptions( () => {
|
this.displayQueuedInterruptions(() => {
|
||||||
return this.client.menuStack.next(cb);
|
return this.client.menuStack.next(cb);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
prevMenu(cb) {
|
prevMenu(cb) {
|
||||||
this.displayQueuedInterruptions( () => {
|
this.displayQueuedInterruptions(() => {
|
||||||
return this.client.menuStack.prev(cb);
|
return this.client.menuStack.prev(cb);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -259,8 +286,8 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
|
|
||||||
gotoMenuOrPrev(name, options, cb) {
|
gotoMenuOrPrev(name, options, cb) {
|
||||||
this.client.menuStack.goto(name, options, err => {
|
this.client.menuStack.goto(name, options, err => {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,7 +297,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gotoMenuOrShowMessage(name, message, options, cb) {
|
gotoMenuOrShowMessage(name, message, options, cb) {
|
||||||
if(!cb && _.isFunction(options)) {
|
if (!cb && _.isFunction(options)) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
@@ -278,18 +305,18 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
options = options || { clearScreen: true };
|
options = options || { clearScreen: true };
|
||||||
|
|
||||||
this.gotoMenu(name, options, err => {
|
this.gotoMenu(name, options, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
if(options.clearScreen) {
|
if (options.clearScreen) {
|
||||||
this.client.term.rawWrite(ansi.resetScreen());
|
this.client.term.rawWrite(ansi.resetScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.term.write(`${message}\n`);
|
this.client.term.write(`${message}\n`);
|
||||||
return this.pausePrompt( () => {
|
return this.pausePrompt(() => {
|
||||||
return this.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -302,33 +329,39 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevMenuOnTimeout(timeout, cb) {
|
prevMenuOnTimeout(timeout, cb) {
|
||||||
setTimeout( () => {
|
setTimeout(() => {
|
||||||
return this.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
addViewController(name, vc) {
|
addViewController(name, vc) {
|
||||||
assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`);
|
assert(
|
||||||
|
!this.viewControllers[name],
|
||||||
|
`ViewController by the name of "${name}" already exists!`
|
||||||
|
);
|
||||||
|
|
||||||
this.viewControllers[name] = vc;
|
this.viewControllers[name] = vc;
|
||||||
return vc;
|
return vc;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeViewController(name) {
|
removeViewController(name) {
|
||||||
if(this.viewControllers[name]) {
|
if (this.viewControllers[name]) {
|
||||||
this.viewControllers[name].detachClientEvents();
|
this.viewControllers[name].detachClientEvents();
|
||||||
delete this.viewControllers[name];
|
delete this.viewControllers[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detachViewControllers() {
|
detachViewControllers() {
|
||||||
Object.keys(this.viewControllers).forEach( name => {
|
Object.keys(this.viewControllers).forEach(name => {
|
||||||
this.viewControllers[name].detachClientEvents();
|
this.viewControllers[name].detachClientEvents();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldPause() {
|
shouldPause() {
|
||||||
return ('end' === this.menuConfig.config.pause || true === this.menuConfig.config.pause);
|
return (
|
||||||
|
'end' === this.menuConfig.config.pause ||
|
||||||
|
true === this.menuConfig.config.pause
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNextTimeout() {
|
hasNextTimeout() {
|
||||||
@@ -336,13 +369,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
haveNext() {
|
haveNext() {
|
||||||
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
|
return _.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next);
|
||||||
}
|
}
|
||||||
|
|
||||||
autoNextMenu(cb) {
|
autoNextMenu(cb) {
|
||||||
const gotoNextMenu = () => {
|
const gotoNextMenu = () => {
|
||||||
if(this.haveNext()) {
|
if (this.haveNext()) {
|
||||||
this.displayQueuedInterruptions( () => {
|
this.displayQueuedInterruptions(() => {
|
||||||
return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb);
|
return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -350,9 +383,12 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
|
if (
|
||||||
if(this.hasNextTimeout()) {
|
_.has(this.menuConfig, 'runtime.autoNext') &&
|
||||||
setTimeout( () => {
|
true === this.menuConfig.runtime.autoNext
|
||||||
|
) {
|
||||||
|
if (this.hasNextTimeout()) {
|
||||||
|
setTimeout(() => {
|
||||||
return gotoNextMenu();
|
return gotoNextMenu();
|
||||||
}, this.menuConfig.config.nextTimeout);
|
}, this.menuConfig.config.nextTimeout);
|
||||||
} else {
|
} else {
|
||||||
@@ -375,20 +411,23 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
function addViewControllers(callback) {
|
function addViewControllers(callback) {
|
||||||
_.forEach(mciData, (mciMap, name) => {
|
_.forEach(mciData, (mciMap, name) => {
|
||||||
assert('menu' === name || 'prompt' === name);
|
assert('menu' === name || 'prompt' === name);
|
||||||
self.addViewController(name, new ViewController( { client : self.client } ) );
|
self.addViewController(
|
||||||
|
name,
|
||||||
|
new ViewController({ client: self.client })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function createMenu(callback) {
|
function createMenu(callback) {
|
||||||
if(!self.viewControllers.menu) {
|
if (!self.viewControllers.menu) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuLoadOpts = {
|
const menuLoadOpts = {
|
||||||
mciMap : mciData.menu,
|
mciMap: mciData.menu,
|
||||||
callingMenu : self,
|
callingMenu: self,
|
||||||
withoutForm : _.isObject(mciData.prompt),
|
withoutForm: _.isObject(mciData.prompt),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
|
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
|
||||||
@@ -396,19 +435,22 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function createPrompt(callback) {
|
function createPrompt(callback) {
|
||||||
if(!self.viewControllers.prompt) {
|
if (!self.viewControllers.prompt) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptLoadOpts = {
|
const promptLoadOpts = {
|
||||||
callingMenu : self,
|
callingMenu: self,
|
||||||
mciMap : mciData.prompt,
|
mciMap: mciData.prompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => {
|
self.viewControllers.prompt.loadFromPromptConfig(
|
||||||
return callback(err);
|
promptLoadOpts,
|
||||||
});
|
err => {
|
||||||
}
|
return callback(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -417,28 +459,27 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayAsset(nameOrData, options, cb) {
|
displayAsset(nameOrData, options, cb) {
|
||||||
if(_.isFunction(options)) {
|
if (_.isFunction(options)) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.clearScreen) {
|
if (options.clearScreen) {
|
||||||
this.client.term.rawWrite(ansi.resetScreen());
|
this.client.term.rawWrite(ansi.resetScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
options = Object.assign( { client : this.client, font : this.menuConfig.config.font }, options );
|
options = Object.assign(
|
||||||
|
{ client: this.client, font: this.menuConfig.config.font },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
if(Buffer.isBuffer(nameOrData)) {
|
if (Buffer.isBuffer(nameOrData)) {
|
||||||
const data = iconvDecode(nameOrData, options.encoding || 'cp437');
|
const data = iconvDecode(nameOrData, options.encoding || 'cp437');
|
||||||
return theme.displayPreparedArt(
|
return theme.displayPreparedArt(options, { data }, (err, artData) => {
|
||||||
options,
|
if (cb) {
|
||||||
{ data },
|
return cb(err, artData);
|
||||||
(err, artData) => {
|
|
||||||
if(cb) {
|
|
||||||
return cb(err, artData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return theme.displayThemedAsset(
|
return theme.displayThemedAsset(
|
||||||
@@ -446,7 +487,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
this.client,
|
this.client,
|
||||||
options,
|
options,
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(err, artData);
|
return cb(err, artData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,18 +496,18 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
|
|
||||||
prepViewController(name, formId, mciMap, cb) {
|
prepViewController(name, formId, mciMap, cb) {
|
||||||
const needsCreated = _.isUndefined(this.viewControllers[name]);
|
const needsCreated = _.isUndefined(this.viewControllers[name]);
|
||||||
if(needsCreated) {
|
if (needsCreated) {
|
||||||
const vcOpts = {
|
const vcOpts = {
|
||||||
client : this.client,
|
client: this.client,
|
||||||
formId : formId,
|
formId: formId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const vc = this.addViewController(name, new ViewController(vcOpts));
|
const vc = this.addViewController(name, new ViewController(vcOpts));
|
||||||
|
|
||||||
const loadOpts = {
|
const loadOpts = {
|
||||||
callingMenu : this,
|
callingMenu: this,
|
||||||
mciMap : mciMap,
|
mciMap: mciMap,
|
||||||
formId : formId,
|
formId: formId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vc.loadFromMenuConfig(loadOpts, err => {
|
return vc.loadFromMenuConfig(loadOpts, err => {
|
||||||
@@ -480,21 +521,17 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepViewControllerWithArt(name, formId, options, cb) {
|
prepViewControllerWithArt(name, formId, options, cb) {
|
||||||
this.displayAsset(
|
this.displayAsset(this.menuConfig.config.art[name], options, (err, artData) => {
|
||||||
this.menuConfig.config.art[name],
|
if (err) {
|
||||||
options,
|
return cb(err);
|
||||||
(err, artData) => {
|
|
||||||
if(err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prepViewController(name, formId, artData.mciMap, cb);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
return this.prepViewController(name, formId, artData.mciMap, cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
optionalMoveToPosition(position) {
|
optionalMoveToPosition(position) {
|
||||||
if(position) {
|
if (position) {
|
||||||
position.x = position.row || position.x || 1;
|
position.x = position.row || position.x || 1;
|
||||||
position.y = position.col || position.y || 1;
|
position.y = position.col || position.y || 1;
|
||||||
|
|
||||||
@@ -503,47 +540,53 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pausePrompt(position, cb) {
|
pausePrompt(position, cb) {
|
||||||
if(!cb && _.isFunction(position)) {
|
if (!cb && _.isFunction(position)) {
|
||||||
cb = position;
|
cb = position;
|
||||||
position = null;
|
position = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.optionalMoveToPosition(position);
|
this.optionalMoveToPosition(position);
|
||||||
|
|
||||||
return theme.displayThemedPause(this.client, {position}, cb);
|
return theme.displayThemedPause(this.client, { position }, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) {
|
promptForInput(
|
||||||
if(!cb && _.isFunction(options)) {
|
{ formName, formId, promptName, prevFormName, position } = {},
|
||||||
|
options,
|
||||||
|
cb
|
||||||
|
) {
|
||||||
|
if (!cb && _.isFunction(options)) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
options.viewController = this.addViewController(
|
options.viewController = this.addViewController(
|
||||||
formName,
|
formName,
|
||||||
new ViewController( { client : this.client, formId } )
|
new ViewController({ client: this.client, formId })
|
||||||
);
|
);
|
||||||
|
|
||||||
options.trailingLF = _.get(options, 'trailingLF', false);
|
options.trailingLF = _.get(options, 'trailingLF', false);
|
||||||
|
|
||||||
let prevVc;
|
let prevVc;
|
||||||
if(prevFormName) {
|
if (prevFormName) {
|
||||||
prevVc = this.viewControllers[prevFormName];
|
prevVc = this.viewControllers[prevFormName];
|
||||||
if(prevVc) {
|
if (prevVc) {
|
||||||
prevVc.setFocus(false);
|
prevVc.setFocus(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//let artHeight;
|
//let artHeight;
|
||||||
options.submitNotify = () => {
|
options.submitNotify = () => {
|
||||||
if(prevVc) {
|
if (prevVc) {
|
||||||
prevVc.setFocus(true);
|
prevVc.setFocus(true);
|
||||||
}
|
}
|
||||||
this.removeViewController(formName);
|
this.removeViewController(formName);
|
||||||
if(options.clearAtSubmit) {
|
if (options.clearAtSubmit) {
|
||||||
this.optionalMoveToPosition(position);
|
this.optionalMoveToPosition(position);
|
||||||
if(options.clearWidth) {
|
if (options.clearWidth) {
|
||||||
this.client.term.rawWrite(`${ansi.reset()}${' '.repeat(options.clearWidth)}`);
|
this.client.term.rawWrite(
|
||||||
|
`${ansi.reset()}${' '.repeat(options.clearWidth)}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// :TODO: handle multi-rows via artHeight
|
// :TODO: handle multi-rows via artHeight
|
||||||
this.client.term.rawWrite(ansi.eraseLine());
|
this.client.term.rawWrite(ansi.eraseLine());
|
||||||
@@ -626,11 +669,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
|
|
||||||
setViewText(formName, mciId, text, appendMultiLine) {
|
setViewText(formName, mciId, text, appendMultiLine) {
|
||||||
const view = this.getView(formName, mciId);
|
const view = this.getView(formName, mciId);
|
||||||
if(!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(appendMultiLine && (view instanceof MultiLineEditTextView)) {
|
if (appendMultiLine && view instanceof MultiLineEditTextView) {
|
||||||
view.addText(text);
|
view.addText(text);
|
||||||
} else {
|
} else {
|
||||||
view.setText(text);
|
view.setText(text);
|
||||||
@@ -647,17 +690,26 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
|
|
||||||
let textView;
|
let textView;
|
||||||
let customMciId = startId;
|
let customMciId = startId;
|
||||||
const config = this.menuConfig.config;
|
const config = this.menuConfig.config;
|
||||||
const endId = options.endId || 99; // we'll fail to get a view before 99
|
const endId = options.endId || 99; // we'll fail to get a view before 99
|
||||||
|
|
||||||
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) {
|
while (
|
||||||
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
|
customMciId <= endId &&
|
||||||
const format = config[key];
|
(textView = this.viewControllers[formName].getView(customMciId))
|
||||||
|
) {
|
||||||
|
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
|
||||||
|
const format = config[key];
|
||||||
|
|
||||||
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) {
|
if (
|
||||||
|
format &&
|
||||||
|
(!options.filter || options.filter.find(f => format.indexOf(f) > -1))
|
||||||
|
) {
|
||||||
const text = stringFormat(format, fmtObj);
|
const text = stringFormat(format, fmtObj);
|
||||||
|
|
||||||
if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) {
|
if (
|
||||||
|
options.appendMultiLine &&
|
||||||
|
textView instanceof MultiLineEditTextView
|
||||||
|
) {
|
||||||
textView.addText(text);
|
textView.addText(text);
|
||||||
} else if (textView.getData() != text) {
|
} else if (textView.getData() != text) {
|
||||||
textView.setText(text);
|
textView.setText(text);
|
||||||
@@ -669,10 +721,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshPredefinedMciViewsByCode(formName, mciCodes) {
|
refreshPredefinedMciViewsByCode(formName, mciCodes) {
|
||||||
const form = _.get(this, [ 'viewControllers', formName] );
|
const form = _.get(this, ['viewControllers', formName]);
|
||||||
if(form) {
|
if (form) {
|
||||||
form.getViewsByMciCode(mciCodes).forEach(v => {
|
form.getViewsByMciCode(mciCodes).forEach(v => {
|
||||||
if(!v.setText) {
|
if (!v.setText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,15 +734,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateMCIByViewIds(formName, viewIds, cb) {
|
validateMCIByViewIds(formName, viewIds, cb) {
|
||||||
if(!Array.isArray(viewIds)) {
|
if (!Array.isArray(viewIds)) {
|
||||||
viewIds = [ viewIds ];
|
viewIds = [viewIds];
|
||||||
}
|
}
|
||||||
const form = _.get(this, [ 'viewControllers', formName ] );
|
const form = _.get(this, ['viewControllers', formName]);
|
||||||
if(!form) {
|
if (!form) {
|
||||||
return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`));
|
return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`));
|
||||||
}
|
}
|
||||||
for(let i = 0; i < viewIds.length; ++i) {
|
for (let i = 0; i < viewIds.length; ++i) {
|
||||||
if(!form.hasView(viewIds[i])) {
|
if (!form.hasView(viewIds[i])) {
|
||||||
return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`));
|
return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -702,7 +754,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
// fields is expected to be { key : type || validator(key, config) }
|
// fields is expected to be { key : type || validator(key, config) }
|
||||||
// where |type| is 'string', 'array', object', 'number'
|
// where |type| is 'string', 'array', object', 'number'
|
||||||
//
|
//
|
||||||
if(!_.isObject(fields)) {
|
if (!_.isObject(fields)) {
|
||||||
return cb(Errors.Invalid('Invalid validator!'));
|
return cb(Errors.Invalid('Invalid validator!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,10 +762,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
let firstBadKey;
|
let firstBadKey;
|
||||||
let badReason;
|
let badReason;
|
||||||
const good = _.every(fields, (type, key) => {
|
const good = _.every(fields, (type, key) => {
|
||||||
if(_.isFunction(type)) {
|
if (_.isFunction(type)) {
|
||||||
if(!type(key, config)) {
|
if (!type(key, config)) {
|
||||||
firstBadKey = key;
|
firstBadKey = key;
|
||||||
badReason = 'Validate failure';
|
badReason = 'Validate failure';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -721,31 +773,45 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||||||
|
|
||||||
const c = config[key];
|
const c = config[key];
|
||||||
let typeOk;
|
let typeOk;
|
||||||
if(_.isUndefined(c)) {
|
if (_.isUndefined(c)) {
|
||||||
typeOk = false;
|
typeOk = false;
|
||||||
badReason = `Missing "${key}", expected ${type}`;
|
badReason = `Missing "${key}", expected ${type}`;
|
||||||
} else {
|
} else {
|
||||||
switch(type) {
|
switch (type) {
|
||||||
case 'string' : typeOk = _.isString(c); break;
|
case 'string':
|
||||||
case 'object' : typeOk = _.isObject(c); break;
|
typeOk = _.isString(c);
|
||||||
case 'array' : typeOk = Array.isArray(c); break;
|
break;
|
||||||
case 'number' : typeOk = !isNaN(parseInt(c)); break;
|
case 'object':
|
||||||
default :
|
typeOk = _.isObject(c);
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
typeOk = Array.isArray(c);
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
typeOk = !isNaN(parseInt(c));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
typeOk = false;
|
typeOk = false;
|
||||||
badReason = `Don't know how to validate ${type}`;
|
badReason = `Don't know how to validate ${type}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!typeOk) {
|
if (!typeOk) {
|
||||||
firstBadKey = key;
|
firstBadKey = key;
|
||||||
if(!badReason) {
|
if (!badReason) {
|
||||||
badReason = `Expected ${type}`;
|
badReason = `Expected ${type}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return typeOk;
|
return typeOk;
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb(good ? null : Errors.Invalid(`Invalid or missing config option "${firstBadKey}" (${badReason})`));
|
return cb(
|
||||||
|
good
|
||||||
|
? null
|
||||||
|
: Errors.Invalid(
|
||||||
|
`Invalid or missing config option "${firstBadKey}" (${badReason})`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Various common helpers
|
// Various common helpers
|
||||||
|
|||||||
@@ -2,25 +2,20 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const loadMenu = require('./menu_util.js').loadMenu;
|
const loadMenu = require('./menu_util.js').loadMenu;
|
||||||
const {
|
const { Errors, ErrorReasons } = require('./enig_error.js');
|
||||||
Errors,
|
const { getResolvedSpec } = require('./menu_util.js');
|
||||||
ErrorReasons
|
|
||||||
} = require('./enig_error.js');
|
|
||||||
const {
|
|
||||||
getResolvedSpec
|
|
||||||
} = require('./menu_util.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
// :TODO: Stack is backwards.... top should be most recent! :)
|
// :TODO: Stack is backwards.... top should be most recent! :)
|
||||||
|
|
||||||
module.exports = class MenuStack {
|
module.exports = class MenuStack {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.stack = [];
|
this.stack = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
push(moduleInfo) {
|
push(moduleInfo) {
|
||||||
@@ -32,13 +27,13 @@ module.exports = class MenuStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
peekPrev() {
|
peekPrev() {
|
||||||
if(this.stackSize > 1) {
|
if (this.stackSize > 1) {
|
||||||
return this.stack[this.stack.length - 2];
|
return this.stack[this.stack.length - 2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
top() {
|
top() {
|
||||||
if(this.stackSize > 0) {
|
if (this.stackSize > 0) {
|
||||||
return this.stack[this.stack.length - 1];
|
return this.stack[this.stack.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,47 +50,61 @@ module.exports = class MenuStack {
|
|||||||
|
|
||||||
next(cb) {
|
next(cb) {
|
||||||
const currentModuleInfo = this.top();
|
const currentModuleInfo = this.top();
|
||||||
const menuConfig = currentModuleInfo.instance.menuConfig;
|
const menuConfig = currentModuleInfo.instance.menuConfig;
|
||||||
const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next');
|
const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next');
|
||||||
if(!nextMenu) {
|
if (!nextMenu) {
|
||||||
return cb(Array.isArray(menuConfig.next) ?
|
return cb(
|
||||||
Errors.MenuStack('No matching condition for "next"', ErrorReasons.NoConditionMatch) :
|
Array.isArray(menuConfig.next)
|
||||||
Errors.MenuStack('Invalid or missing "next" member in menu config', ErrorReasons.InvalidNextMenu)
|
? Errors.MenuStack(
|
||||||
|
'No matching condition for "next"',
|
||||||
|
ErrorReasons.NoConditionMatch
|
||||||
|
)
|
||||||
|
: Errors.MenuStack(
|
||||||
|
'Invalid or missing "next" member in menu config',
|
||||||
|
ErrorReasons.InvalidNextMenu
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(nextMenu === currentModuleInfo.name) {
|
if (nextMenu === currentModuleInfo.name) {
|
||||||
return cb(Errors.MenuStack('Menu config "next" specifies current menu', ErrorReasons.AlreadyThere));
|
return cb(
|
||||||
|
Errors.MenuStack(
|
||||||
|
'Menu config "next" specifies current menu',
|
||||||
|
ErrorReasons.AlreadyThere
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.goto(nextMenu, { }, cb);
|
this.goto(nextMenu, {}, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
prev(cb) {
|
prev(cb) {
|
||||||
const menuResult = this.top().instance.getMenuResult();
|
const menuResult = this.top().instance.getMenuResult();
|
||||||
|
|
||||||
// :TODO: leave() should really take a cb...
|
// :TODO: leave() should really take a cb...
|
||||||
this.pop().instance.leave(); // leave & remove current
|
this.pop().instance.leave(); // leave & remove current
|
||||||
|
|
||||||
const previousModuleInfo = this.pop(); // get previous
|
const previousModuleInfo = this.pop(); // get previous
|
||||||
|
|
||||||
if(previousModuleInfo) {
|
if (previousModuleInfo) {
|
||||||
const opts = {
|
const opts = {
|
||||||
extraArgs : previousModuleInfo.extraArgs,
|
extraArgs: previousModuleInfo.extraArgs,
|
||||||
savedState : previousModuleInfo.savedState,
|
savedState: previousModuleInfo.savedState,
|
||||||
lastMenuResult : menuResult,
|
lastMenuResult: menuResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.goto(previousModuleInfo.name, opts, cb);
|
return this.goto(previousModuleInfo.name, opts, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu));
|
return cb(
|
||||||
|
Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
goto(name, options, cb) {
|
goto(name, options, cb) {
|
||||||
const currentModuleInfo = this.top();
|
const currentModuleInfo = this.top();
|
||||||
|
|
||||||
if(!cb && _.isFunction(options)) {
|
if (!cb && _.isFunction(options)) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
@@ -103,19 +112,24 @@ module.exports = class MenuStack {
|
|||||||
options = options || {};
|
options = options || {};
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if(currentModuleInfo && name === currentModuleInfo.name) {
|
if (currentModuleInfo && name === currentModuleInfo.name) {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
cb(Errors.MenuStack('Already at supplied menu', ErrorReasons.AlreadyThere));
|
cb(
|
||||||
|
Errors.MenuStack(
|
||||||
|
'Already at supplied menu',
|
||||||
|
ErrorReasons.AlreadyThere
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadOpts = {
|
const loadOpts = {
|
||||||
name : name,
|
name: name,
|
||||||
client : self.client,
|
client: self.client,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
|
if (currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
|
||||||
loadOpts.extraArgs = currentModuleInfo.extraArgs;
|
loadOpts.extraArgs = currentModuleInfo.extraArgs;
|
||||||
} else {
|
} else {
|
||||||
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
|
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
|
||||||
@@ -123,15 +137,15 @@ module.exports = class MenuStack {
|
|||||||
loadOpts.lastMenuResult = options.lastMenuResult;
|
loadOpts.lastMenuResult = options.lastMenuResult;
|
||||||
|
|
||||||
loadMenu(loadOpts, (err, modInst) => {
|
loadMenu(loadOpts, (err, modInst) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
// :TODO: probably should just require a cb...
|
// :TODO: probably should just require a cb...
|
||||||
const errCb = cb || self.client.defaultHandlerMissingMod();
|
const errCb = cb || self.client.defaultHandlerMissingMod();
|
||||||
errCb(err);
|
errCb(err);
|
||||||
} else {
|
} else {
|
||||||
self.client.log.debug( { menuName : name }, 'Goto menu module');
|
self.client.log.debug({ menuName: name }, 'Goto menu module');
|
||||||
|
|
||||||
if(!this.client.acs.hasMenuModuleAccess(modInst)) {
|
if (!this.client.acs.hasMenuModuleAccess(modInst)) {
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(Errors.AccessDenied('No access to this menu'));
|
return cb(Errors.AccessDenied('No access to this menu'));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -141,12 +155,15 @@ module.exports = class MenuStack {
|
|||||||
// Handle deprecated 'options' block by merging to config and warning user.
|
// Handle deprecated 'options' block by merging to config and warning user.
|
||||||
// :TODO: Remove in 0.0.10+
|
// :TODO: Remove in 0.0.10+
|
||||||
//
|
//
|
||||||
if(modInst.menuConfig.options) {
|
if (modInst.menuConfig.options) {
|
||||||
self.client.log.warn(
|
self.client.log.warn(
|
||||||
{ options : modInst.menuConfig.options },
|
{ options: modInst.menuConfig.options },
|
||||||
'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions'
|
'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions'
|
||||||
);
|
);
|
||||||
Object.assign(modInst.menuConfig.config || {}, modInst.menuConfig.options);
|
Object.assign(
|
||||||
|
modInst.menuConfig.config || {},
|
||||||
|
modInst.menuConfig.options
|
||||||
|
);
|
||||||
delete modInst.menuConfig.options;
|
delete modInst.menuConfig.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,57 +172,63 @@ module.exports = class MenuStack {
|
|||||||
// anything supplied in code.
|
// anything supplied in code.
|
||||||
//
|
//
|
||||||
let menuFlags;
|
let menuFlags;
|
||||||
if(0 === modInst.menuConfig.config.menuFlags.length) {
|
if (0 === modInst.menuConfig.config.menuFlags.length) {
|
||||||
menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : [];
|
menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : [];
|
||||||
} else {
|
} else {
|
||||||
menuFlags = modInst.menuConfig.config.menuFlags;
|
menuFlags = modInst.menuConfig.config.menuFlags;
|
||||||
|
|
||||||
// in code we can ask to merge in
|
// in code we can ask to merge in
|
||||||
if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) {
|
if (
|
||||||
|
Array.isArray(options.menuFlags) &&
|
||||||
|
options.menuFlags.includes('mergeFlags')
|
||||||
|
) {
|
||||||
menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
|
menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(currentModuleInfo) {
|
if (currentModuleInfo) {
|
||||||
// save stack state
|
// save stack state
|
||||||
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
|
currentModuleInfo.savedState =
|
||||||
|
currentModuleInfo.instance.getSaveState();
|
||||||
|
|
||||||
currentModuleInfo.instance.leave();
|
currentModuleInfo.instance.leave();
|
||||||
|
|
||||||
if(currentModuleInfo.menuFlags.includes('noHistory')) {
|
if (currentModuleInfo.menuFlags.includes('noHistory')) {
|
||||||
this.pop();
|
this.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(menuFlags.includes('popParent')) {
|
if (menuFlags.includes('popParent')) {
|
||||||
this.pop().instance.leave(); // leave & remove current
|
this.pop().instance.leave(); // leave & remove current
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push({
|
self.push({
|
||||||
name : name,
|
name: name,
|
||||||
instance : modInst,
|
instance: modInst,
|
||||||
extraArgs : loadOpts.extraArgs,
|
extraArgs: loadOpts.extraArgs,
|
||||||
menuFlags : menuFlags,
|
menuFlags: menuFlags,
|
||||||
});
|
});
|
||||||
|
|
||||||
// restore previous state if requested
|
// restore previous state if requested
|
||||||
if(options.savedState) {
|
if (options.savedState) {
|
||||||
modInst.restoreSavedState(options.savedState);
|
modInst.restoreSavedState(options.savedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stackEntries = self.stack.map(stackEntry => {
|
const stackEntries = self.stack.map(stackEntry => {
|
||||||
let name = stackEntry.name;
|
let name = stackEntry.name;
|
||||||
if(stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
|
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
|
||||||
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(', ')})`;
|
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
|
||||||
|
', '
|
||||||
|
)})`;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' );
|
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
|
||||||
|
|
||||||
modInst.enter();
|
modInst.enter();
|
||||||
|
|
||||||
if(cb) {
|
if (cb) {
|
||||||
cb(null);
|
cb(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,29 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const moduleUtil = require('./module_util.js');
|
const moduleUtil = require('./module_util.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const asset = require('./asset.js');
|
const asset = require('./asset.js');
|
||||||
const { MCIViewFactory } = require('./mci_view_factory.js');
|
const { MCIViewFactory } = require('./mci_view_factory.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.loadMenu = loadMenu;
|
exports.loadMenu = loadMenu;
|
||||||
exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap;
|
exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap;
|
||||||
exports.handleAction = handleAction;
|
exports.handleAction = handleAction;
|
||||||
exports.getResolvedSpec = getResolvedSpec;
|
exports.getResolvedSpec = getResolvedSpec;
|
||||||
exports.handleNext = handleNext;
|
exports.handleNext = handleNext;
|
||||||
|
|
||||||
function getMenuConfig(client, name, cb) {
|
function getMenuConfig(client, name, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function locateMenuConfig(callback) {
|
function locateMenuConfig(callback) {
|
||||||
const menuConfig = _.get(client.currentTheme, [ 'menus', name ]);
|
const menuConfig = _.get(client.currentTheme, ['menus', name]);
|
||||||
if (menuConfig) {
|
if (menuConfig) {
|
||||||
return callback(null, menuConfig);
|
return callback(null, menuConfig);
|
||||||
}
|
}
|
||||||
@@ -32,15 +32,18 @@ function getMenuConfig(client, name, cb) {
|
|||||||
return callback(Errors.DoesNotExist(`No menu entry for "${name}"`));
|
return callback(Errors.DoesNotExist(`No menu entry for "${name}"`));
|
||||||
},
|
},
|
||||||
function locatePromptConfig(menuConfig, callback) {
|
function locatePromptConfig(menuConfig, callback) {
|
||||||
if(_.isString(menuConfig.prompt)) {
|
if (_.isString(menuConfig.prompt)) {
|
||||||
if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) {
|
if (_.has(client.currentTheme, ['prompts', menuConfig.prompt])) {
|
||||||
menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt];
|
menuConfig.promptConfig =
|
||||||
|
client.currentTheme.prompts[menuConfig.prompt];
|
||||||
return callback(null, menuConfig);
|
return callback(null, menuConfig);
|
||||||
}
|
}
|
||||||
return callback(Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`));
|
return callback(
|
||||||
|
Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(null, menuConfig);
|
return callback(null, menuConfig);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
(err, menuConfig) => {
|
(err, menuConfig) => {
|
||||||
return cb(err, menuConfig);
|
return cb(err, menuConfig);
|
||||||
@@ -50,7 +53,7 @@ function getMenuConfig(client, name, cb) {
|
|||||||
|
|
||||||
// :TODO: name/client should not be part of options - they are required always
|
// :TODO: name/client should not be part of options - they are required always
|
||||||
function loadMenu(options, cb) {
|
function loadMenu(options, cb) {
|
||||||
if(!_.isString(options.name) || !_.isObject(options.client)) {
|
if (!_.isString(options.name) || !_.isObject(options.client)) {
|
||||||
return cb(Errors.MissingParam('Missing required options'));
|
return cb(Errors.MissingParam('Missing required options'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,27 +65,30 @@ function loadMenu(options, cb) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function loadMenuModule(menuConfig, callback) {
|
function loadMenuModule(menuConfig, callback) {
|
||||||
|
|
||||||
menuConfig.config = menuConfig.config || {};
|
menuConfig.config = menuConfig.config || {};
|
||||||
menuConfig.config.menuFlags = menuConfig.config.menuFlags || [];
|
menuConfig.config.menuFlags = menuConfig.config.menuFlags || [];
|
||||||
if(!Array.isArray(menuConfig.config.menuFlags)) {
|
if (!Array.isArray(menuConfig.config.menuFlags)) {
|
||||||
menuConfig.config.menuFlags = [ menuConfig.config.menuFlags ];
|
menuConfig.config.menuFlags = [menuConfig.config.menuFlags];
|
||||||
}
|
}
|
||||||
|
|
||||||
const modAsset = asset.getModuleAsset(menuConfig.module);
|
const modAsset = asset.getModuleAsset(menuConfig.module);
|
||||||
const modSupplied = null !== modAsset;
|
const modSupplied = null !== modAsset;
|
||||||
|
|
||||||
const modLoadOpts = {
|
const modLoadOpts = {
|
||||||
name : modSupplied ? modAsset.asset : 'standard_menu',
|
name: modSupplied ? modAsset.asset : 'standard_menu',
|
||||||
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods,
|
path:
|
||||||
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
|
!modSupplied || 'systemModule' === modAsset.type
|
||||||
|
? __dirname
|
||||||
|
: Config().paths.mods,
|
||||||
|
category:
|
||||||
|
!modSupplied || 'systemModule' === modAsset.type ? null : 'mods',
|
||||||
};
|
};
|
||||||
|
|
||||||
moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => {
|
moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => {
|
||||||
const modData = {
|
const modData = {
|
||||||
name : modLoadOpts.name,
|
name: modLoadOpts.name,
|
||||||
config : menuConfig,
|
config: menuConfig,
|
||||||
mod : mod,
|
mod: mod,
|
||||||
};
|
};
|
||||||
|
|
||||||
return callback(err, modData);
|
return callback(err, modData);
|
||||||
@@ -90,24 +96,30 @@ function loadMenu(options, cb) {
|
|||||||
},
|
},
|
||||||
function createModuleInstance(modData, callback) {
|
function createModuleInstance(modData, callback) {
|
||||||
Log.trace(
|
Log.trace(
|
||||||
{ moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo },
|
{
|
||||||
'Creating menu module instance');
|
moduleName: modData.name,
|
||||||
|
extraArgs: options.extraArgs,
|
||||||
|
config: modData.config,
|
||||||
|
info: modData.mod.modInfo,
|
||||||
|
},
|
||||||
|
'Creating menu module instance'
|
||||||
|
);
|
||||||
|
|
||||||
let moduleInstance;
|
let moduleInstance;
|
||||||
try {
|
try {
|
||||||
moduleInstance = new modData.mod.getModule({
|
moduleInstance = new modData.mod.getModule({
|
||||||
menuName : options.name,
|
menuName: options.name,
|
||||||
menuConfig : modData.config,
|
menuConfig: modData.config,
|
||||||
extraArgs : options.extraArgs,
|
extraArgs: options.extraArgs,
|
||||||
client : options.client,
|
client: options.client,
|
||||||
lastMenuResult : options.lastMenuResult,
|
lastMenuResult: options.lastMenuResult,
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return callback(e);
|
return callback(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null, moduleInstance);
|
return callback(null, moduleInstance);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
(err, modInst) => {
|
(err, modInst) => {
|
||||||
return cb(err, modInst);
|
return cb(err, modInst);
|
||||||
@@ -116,82 +128,99 @@ function loadMenu(options, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
|
function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
|
||||||
if(!_.isObject(menuConfig.form)) {
|
if (!_.isObject(menuConfig.form)) {
|
||||||
return cb(Errors.MissingParam('Invalid or missing "form" member for menu'));
|
return cb(Errors.MissingParam('Invalid or missing "form" member for menu'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_.isObject(menuConfig.form[formId])) {
|
if (!_.isObject(menuConfig.form[formId])) {
|
||||||
return cb(Errors.DoesNotExist(`No form found for formId ${formId}`));
|
return cb(Errors.DoesNotExist(`No form found for formId ${formId}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const formForId = menuConfig.form[formId];
|
const formForId = menuConfig.form[formId];
|
||||||
const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => {
|
const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), mci => {
|
||||||
return MCIViewFactory.UserViewCodes.indexOf(mci) > -1;
|
return MCIViewFactory.UserViewCodes.indexOf(mci) > -1;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key');
|
Log.trace({ mciKey: mciReqKey }, 'Looking for MCI configuration key');
|
||||||
|
|
||||||
//
|
//
|
||||||
// Exact, explicit match?
|
// Exact, explicit match?
|
||||||
//
|
//
|
||||||
if(_.isObject(formForId[mciReqKey])) {
|
if (_.isObject(formForId[mciReqKey])) {
|
||||||
Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match');
|
Log.trace({ mciKey: mciReqKey }, 'Using exact configuration key match');
|
||||||
return cb(null, formForId[mciReqKey]);
|
return cb(null, formForId[mciReqKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Generic match
|
// Generic match
|
||||||
//
|
//
|
||||||
if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
|
if (_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
|
||||||
Log.trace('Using generic configuration');
|
Log.trace('Using generic configuration');
|
||||||
return cb(null, formForId);
|
return cb(null, formForId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`));
|
return cb(
|
||||||
|
Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: Most of this should be moved elsewhere .... DRY...
|
// :TODO: Most of this should be moved elsewhere .... DRY...
|
||||||
function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) {
|
function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) {
|
||||||
if('' === paths.extname(path)) {
|
if ('' === paths.extname(path)) {
|
||||||
path += '.js';
|
path += '.js';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.log.trace(
|
client.log.trace(
|
||||||
{ path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs },
|
{
|
||||||
'Calling menu method');
|
path: path,
|
||||||
|
methodName: asset.asset,
|
||||||
|
formData: formData,
|
||||||
|
extraArgs: extraArgs,
|
||||||
|
},
|
||||||
|
'Calling menu method'
|
||||||
|
);
|
||||||
|
|
||||||
const methodMod = require(path);
|
const methodMod = require(path);
|
||||||
return methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs, cb);
|
return methodMod[asset.asset](
|
||||||
} catch(e) {
|
client.currentMenuModule,
|
||||||
client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method');
|
formData || {},
|
||||||
|
extraArgs,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
client.log.error(
|
||||||
|
{ error: e.toString(), methodName: asset.asset },
|
||||||
|
'Failed to execute asset method'
|
||||||
|
);
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAction(client, formData, conf, cb) {
|
function handleAction(client, formData, conf, cb) {
|
||||||
if(!_.isObject(conf)) {
|
if (!_.isObject(conf)) {
|
||||||
return cb(Errors.MissingParam('Missing config'));
|
return cb(Errors.MissingParam('Missing config'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc.
|
const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc.
|
||||||
const actionAsset = asset.parseAsset(action);
|
const actionAsset = asset.parseAsset(action);
|
||||||
if(!_.isObject(actionAsset)) {
|
if (!_.isObject(actionAsset)) {
|
||||||
return cb(Errors.Invalid('Unable to parse "conf.action"'));
|
return cb(Errors.Invalid('Unable to parse "conf.action"'));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(actionAsset.type) {
|
switch (actionAsset.type) {
|
||||||
case 'method' :
|
case 'method':
|
||||||
case 'systemMethod' :
|
case 'systemMethod':
|
||||||
if(_.isString(actionAsset.location)) {
|
if (_.isString(actionAsset.location)) {
|
||||||
return callModuleMenuMethod(
|
return callModuleMenuMethod(
|
||||||
client,
|
client,
|
||||||
actionAsset,
|
actionAsset,
|
||||||
paths.join(Config().paths.mods, actionAsset.location),
|
paths.join(Config().paths.mods, actionAsset.location),
|
||||||
formData,
|
formData,
|
||||||
conf.extraArgs,
|
conf.extraArgs,
|
||||||
cb);
|
cb
|
||||||
} else if('systemMethod' === actionAsset.type) {
|
);
|
||||||
|
} else if ('systemMethod' === actionAsset.type) {
|
||||||
// :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. ()
|
// :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. ()
|
||||||
// :TODO: Probably better as system_method.js
|
// :TODO: Probably better as system_method.js
|
||||||
return callModuleMenuMethod(
|
return callModuleMenuMethod(
|
||||||
@@ -200,21 +229,30 @@ function handleAction(client, formData, conf, cb) {
|
|||||||
paths.join(__dirname, 'system_menu_method.js'),
|
paths.join(__dirname, 'system_menu_method.js'),
|
||||||
formData,
|
formData,
|
||||||
conf.extraArgs,
|
conf.extraArgs,
|
||||||
cb);
|
cb
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// local to current module
|
// local to current module
|
||||||
const currentModule = client.currentMenuModule;
|
const currentModule = client.currentMenuModule;
|
||||||
if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) {
|
if (_.isFunction(currentModule.menuMethods[actionAsset.asset])) {
|
||||||
return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb);
|
return currentModule.menuMethods[actionAsset.asset](
|
||||||
|
formData,
|
||||||
|
conf.extraArgs,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = Errors.DoesNotExist('Method does not exist');
|
const err = Errors.DoesNotExist('Method does not exist');
|
||||||
client.log.warn( { method : actionAsset.asset }, err.message);
|
client.log.warn({ method: actionAsset.asset }, err.message);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'menu' :
|
case 'menu':
|
||||||
return client.currentMenuModule.gotoMenu(actionAsset.asset, { formData : formData, extraArgs : conf.extraArgs }, cb );
|
return client.currentMenuModule.gotoMenu(
|
||||||
|
actionAsset.asset,
|
||||||
|
{ formData: formData, extraArgs: conf.extraArgs },
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,15 +275,15 @@ function getResolvedSpec(client, spec, memberName) {
|
|||||||
// (3) Simple array of strings. A random selection will be made:
|
// (3) Simple array of strings. A random selection will be made:
|
||||||
// next: [ "foo", "baz", "fizzbang" ]
|
// next: [ "foo", "baz", "fizzbang" ]
|
||||||
//
|
//
|
||||||
if(!Array.isArray(spec)) {
|
if (!Array.isArray(spec)) {
|
||||||
return spec; // (1) simple string, as-is
|
return spec; // (1) simple string, as-is
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isObject(spec[0])) {
|
if (_.isObject(spec[0])) {
|
||||||
return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals
|
return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec[Math.floor(Math.random() * spec.length)]; // (3) random
|
return spec[Math.floor(Math.random() * spec.length)]; // (3) random
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNext(client, nextSpec, conf, cb) {
|
function handleNext(client, nextSpec, conf, cb) {
|
||||||
@@ -257,32 +295,54 @@ function handleNext(client, nextSpec, conf, cb) {
|
|||||||
const extraArgs = conf.extraArgs || {};
|
const extraArgs = conf.extraArgs || {};
|
||||||
|
|
||||||
// :TODO: DRY this with handleAction()
|
// :TODO: DRY this with handleAction()
|
||||||
switch(nextAsset.type) {
|
switch (nextAsset.type) {
|
||||||
case 'method' :
|
case 'method':
|
||||||
case 'systemMethod' :
|
case 'systemMethod':
|
||||||
if(_.isString(nextAsset.location)) {
|
if (_.isString(nextAsset.location)) {
|
||||||
return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb);
|
return callModuleMenuMethod(
|
||||||
} else if('systemMethod' === nextAsset.type) {
|
client,
|
||||||
|
nextAsset,
|
||||||
|
paths.join(Config().paths.mods, nextAsset.location),
|
||||||
|
{},
|
||||||
|
extraArgs,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
} else if ('systemMethod' === nextAsset.type) {
|
||||||
// :TODO: see other notes about system_menu_method.js here
|
// :TODO: see other notes about system_menu_method.js here
|
||||||
return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb);
|
return callModuleMenuMethod(
|
||||||
|
client,
|
||||||
|
nextAsset,
|
||||||
|
paths.join(__dirname, 'system_menu_method.js'),
|
||||||
|
{},
|
||||||
|
extraArgs,
|
||||||
|
cb
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// local to current module
|
// local to current module
|
||||||
const currentModule = client.currentMenuModule;
|
const currentModule = client.currentMenuModule;
|
||||||
if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) {
|
if (_.isFunction(currentModule.menuMethods[nextAsset.asset])) {
|
||||||
const formData = {}; // we don't have any
|
const formData = {}; // we don't have any
|
||||||
return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb );
|
return currentModule.menuMethods[nextAsset.asset](
|
||||||
|
formData,
|
||||||
|
extraArgs,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = Errors.DoesNotExist('Method does not exist');
|
const err = Errors.DoesNotExist('Method does not exist');
|
||||||
client.log.warn( { method : nextAsset.asset }, err.message);
|
client.log.warn({ method: nextAsset.asset }, err.message);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'menu' :
|
case 'menu':
|
||||||
return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb );
|
return client.currentMenuModule.gotoMenu(
|
||||||
|
nextAsset.asset,
|
||||||
|
{ extraArgs: extraArgs },
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = Errors.Invalid('Invalid asset type for "next"');
|
const err = Errors.Invalid('Invalid asset type for "next"');
|
||||||
client.log.error( { nextSpec : nextSpec }, err.message);
|
client.log.error({ nextSpec: nextSpec }, err.message);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const View = require('./view.js').View;
|
const View = require('./view.js').View;
|
||||||
const miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
|
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.MenuView = MenuView;
|
exports.MenuView = MenuView;
|
||||||
|
|
||||||
function MenuView(options) {
|
function MenuView(options) {
|
||||||
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
||||||
@@ -23,7 +23,7 @@ function MenuView(options) {
|
|||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if(options.items) {
|
if (options.items) {
|
||||||
this.setItems(options.items);
|
this.setItems(options.items);
|
||||||
} else {
|
} else {
|
||||||
this.items = [];
|
this.items = [];
|
||||||
@@ -31,54 +31,61 @@ function MenuView(options) {
|
|||||||
|
|
||||||
this.renderCache = {};
|
this.renderCache = {};
|
||||||
|
|
||||||
this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true);
|
this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(
|
||||||
|
options.caseInsensitiveHotKeys,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
this.setHotKeys(options.hotKeys);
|
this.setHotKeys(options.hotKeys);
|
||||||
|
|
||||||
this.focusedItemIndex = options.focusedItemIndex || 0;
|
this.focusedItemIndex = options.focusedItemIndex || 0;
|
||||||
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
|
this.focusedItemIndex =
|
||||||
|
this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
|
||||||
|
|
||||||
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
|
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
|
||||||
this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0;
|
this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing)
|
||||||
|
? options.itemHorizSpacing
|
||||||
|
: 0;
|
||||||
|
|
||||||
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
|
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
|
||||||
this.focusPrefix = options.focusPrefix || '';
|
this.focusPrefix = options.focusPrefix || '';
|
||||||
this.focusSuffix = options.focusSuffix || '';
|
this.focusSuffix = options.focusSuffix || '';
|
||||||
|
|
||||||
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
|
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
|
||||||
|
|
||||||
this.hasFocusItems = function() {
|
this.hasFocusItems = function () {
|
||||||
return !_.isUndefined(self.focusItems);
|
return !_.isUndefined(self.focusItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getHotKeyItemIndex = function(ch) {
|
this.getHotKeyItemIndex = function (ch) {
|
||||||
if(ch && self.hotKeys) {
|
if (ch && self.hotKeys) {
|
||||||
const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
|
const keyIndex =
|
||||||
if(_.isNumber(keyIndex)) {
|
self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
|
||||||
|
if (_.isNumber(keyIndex)) {
|
||||||
return keyIndex;
|
return keyIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emitIndexUpdate = function() {
|
this.emitIndexUpdate = function () {
|
||||||
self.emit('index update', self.focusedItemIndex);
|
self.emit('index update', self.focusedItemIndex);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(MenuView, View);
|
util.inherits(MenuView, View);
|
||||||
|
|
||||||
MenuView.prototype.setTextOverflow = function(overflow) {
|
MenuView.prototype.setTextOverflow = function (overflow) {
|
||||||
this.textOverflow = overflow;
|
this.textOverflow = overflow;
|
||||||
this.invalidateRenderCache();
|
this.invalidateRenderCache();
|
||||||
}
|
};
|
||||||
|
|
||||||
MenuView.prototype.hasTextOverflow = function() {
|
MenuView.prototype.hasTextOverflow = function () {
|
||||||
return this.textOverflow != undefined;
|
return this.textOverflow != undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
MenuView.prototype.setItems = function(items) {
|
MenuView.prototype.setItems = function (items) {
|
||||||
if(Array.isArray(items)) {
|
if (Array.isArray(items)) {
|
||||||
this.sorted = false;
|
this.sorted = false;
|
||||||
this.renderCache = {};
|
this.renderCache = {};
|
||||||
|
|
||||||
@@ -97,7 +104,7 @@ MenuView.prototype.setItems = function(items) {
|
|||||||
let stringItem;
|
let stringItem;
|
||||||
this.items = items.map(item => {
|
this.items = items.map(item => {
|
||||||
stringItem = _.isString(item);
|
stringItem = _.isString(item);
|
||||||
if(stringItem) {
|
if (stringItem) {
|
||||||
text = item;
|
text = item;
|
||||||
} else {
|
} else {
|
||||||
text = item.text || '';
|
text = item.text || '';
|
||||||
@@ -105,10 +112,10 @@ MenuView.prototype.setItems = function(items) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
text = this.disablePipe ? text : pipeToAnsi(text, this.client);
|
text = this.disablePipe ? text : pipeToAnsi(text, this.client);
|
||||||
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
|
return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
|
||||||
});
|
});
|
||||||
|
|
||||||
if(this.complexItems) {
|
if (this.complexItems) {
|
||||||
this.itemFormat = this.itemFormat || '{text}';
|
this.itemFormat = this.itemFormat || '{text}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,58 +123,58 @@ MenuView.prototype.setItems = function(items) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) {
|
MenuView.prototype.getRenderCacheItem = function (index, focusItem = false) {
|
||||||
const item = this.renderCache[index];
|
const item = this.renderCache[index];
|
||||||
return item && item[focusItem ? 'focus' : 'standard'];
|
return item && item[focusItem ? 'focus' : 'standard'];
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.removeRenderCacheItem = function(index) {
|
MenuView.prototype.removeRenderCacheItem = function (index) {
|
||||||
delete this.renderCache[index];
|
delete this.renderCache[index];
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) {
|
MenuView.prototype.setRenderCacheItem = function (index, rendered, focusItem = false) {
|
||||||
this.renderCache[index] = this.renderCache[index] || {};
|
this.renderCache[index] = this.renderCache[index] || {};
|
||||||
this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered;
|
this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.invalidateRenderCache = function() {
|
MenuView.prototype.invalidateRenderCache = function () {
|
||||||
this.renderCache = {};
|
this.renderCache = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setSort = function(sort) {
|
MenuView.prototype.setSort = function (sort) {
|
||||||
if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
|
if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = true === sort ? 'text' : sort;
|
const key = true === sort ? 'text' : sort;
|
||||||
if('text' !== sort && !this.complexItems) {
|
if ('text' !== sort && !this.complexItems) {
|
||||||
return; // need a valid sort key
|
return; // need a valid sort key
|
||||||
}
|
}
|
||||||
|
|
||||||
this.items.sort( (a, b) => {
|
this.items.sort((a, b) => {
|
||||||
const a1 = a[key];
|
const a1 = a[key];
|
||||||
const b1 = b[key];
|
const b1 = b[key];
|
||||||
if(!a1) {
|
if (!a1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if(!b1) {
|
if (!b1) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return a1.localeCompare( b1, { sensitivity : false, numeric : true } );
|
return a1.localeCompare(b1, { sensitivity: false, numeric: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sorted = true;
|
this.sorted = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.removeItem = function(index) {
|
MenuView.prototype.removeItem = function (index) {
|
||||||
this.sorted = false;
|
this.sorted = false;
|
||||||
this.items.splice(index, 1);
|
this.items.splice(index, 1);
|
||||||
|
|
||||||
if(this.focusItems) {
|
if (this.focusItems) {
|
||||||
this.focusItems.splice(index, 1);
|
this.focusItems.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.focusedItemIndex >= index) {
|
if (this.focusedItemIndex >= index) {
|
||||||
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
|
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,53 +183,53 @@ MenuView.prototype.removeItem = function(index) {
|
|||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.getCount = function() {
|
MenuView.prototype.getCount = function () {
|
||||||
return this.items.length;
|
return this.items.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.getItems = function() {
|
MenuView.prototype.getItems = function () {
|
||||||
if(this.complexItems) {
|
if (this.complexItems) {
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.items.map( item => {
|
return this.items.map(item => {
|
||||||
return item.text;
|
return item.text;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.getItem = function(index) {
|
MenuView.prototype.getItem = function (index) {
|
||||||
if(this.complexItems) {
|
if (this.complexItems) {
|
||||||
return this.items[index];
|
return this.items[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.items[index].text;
|
return this.items[index].text;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.focusNext = function() {
|
MenuView.prototype.focusNext = function () {
|
||||||
this.emitIndexUpdate();
|
this.emitIndexUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.focusPrevious = function() {
|
MenuView.prototype.focusPrevious = function () {
|
||||||
this.emitIndexUpdate();
|
this.emitIndexUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.focusNextPageItem = function() {
|
MenuView.prototype.focusNextPageItem = function () {
|
||||||
this.emitIndexUpdate();
|
this.emitIndexUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.focusPreviousPageItem = function() {
|
MenuView.prototype.focusPreviousPageItem = function () {
|
||||||
this.emitIndexUpdate();
|
this.emitIndexUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.focusFirst = function() {
|
MenuView.prototype.focusFirst = function () {
|
||||||
this.emitIndexUpdate();
|
this.emitIndexUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.focusLast = function() {
|
MenuView.prototype.focusLast = function () {
|
||||||
this.emitIndexUpdate();
|
this.emitIndexUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setFocusItemIndex = function(index) {
|
MenuView.prototype.setFocusItemIndex = function (index) {
|
||||||
this.focusedItemIndex = index;
|
this.focusedItemIndex = index;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,10 +239,10 @@ MenuView.prototype.getFocusItemIndex = function() {
|
|||||||
|
|
||||||
MenuView.prototype.onKeyPress = function(ch, key) {
|
MenuView.prototype.onKeyPress = function(ch, key) {
|
||||||
const itemIndex = this.getHotKeyItemIndex(ch);
|
const itemIndex = this.getHotKeyItemIndex(ch);
|
||||||
if(itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
this.setFocusItemIndex(itemIndex);
|
this.setFocusItemIndex(itemIndex);
|
||||||
|
|
||||||
if(true === this.hotKeySubmit) {
|
if (true === this.hotKeySubmit) {
|
||||||
this.emit('action', 'accept');
|
this.emit('action', 'accept');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,79 +250,99 @@ MenuView.prototype.onKeyPress = function(ch, key) {
|
|||||||
MenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
MenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setFocusItems = function(items) {
|
MenuView.prototype.setFocusItems = function (items) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if(items) {
|
if (items) {
|
||||||
this.focusItems = [];
|
this.focusItems = [];
|
||||||
items.forEach( itemText => {
|
items.forEach(itemText => {
|
||||||
this.focusItems.push(
|
this.focusItems.push({
|
||||||
{
|
text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client),
|
||||||
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setItemSpacing = function(itemSpacing) {
|
MenuView.prototype.setItemSpacing = function (itemSpacing) {
|
||||||
itemSpacing = parseInt(itemSpacing);
|
itemSpacing = parseInt(itemSpacing);
|
||||||
assert(_.isNumber(itemSpacing));
|
assert(_.isNumber(itemSpacing));
|
||||||
|
|
||||||
this.itemSpacing = itemSpacing;
|
this.itemSpacing = itemSpacing;
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
|
MenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) {
|
||||||
itemHorizSpacing = parseInt(itemHorizSpacing);
|
itemHorizSpacing = parseInt(itemHorizSpacing);
|
||||||
assert(_.isNumber(itemHorizSpacing));
|
assert(_.isNumber(itemHorizSpacing));
|
||||||
|
|
||||||
this.itemHorizSpacing = itemHorizSpacing;
|
this.itemHorizSpacing = itemHorizSpacing;
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setPropertyValue = function(propName, value) {
|
MenuView.prototype.setPropertyValue = function (propName, value) {
|
||||||
switch(propName) {
|
switch (propName) {
|
||||||
case 'itemSpacing' : this.setItemSpacing(value); break;
|
case 'itemSpacing':
|
||||||
case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break;
|
this.setItemSpacing(value);
|
||||||
case 'items' : this.setItems(value); break;
|
break;
|
||||||
case 'focusItems' : this.setFocusItems(value); break;
|
case 'itemHorizSpacing':
|
||||||
case 'hotKeys' : this.setHotKeys(value); break;
|
this.setItemHorizSpacing(value);
|
||||||
case 'textOverflow' : this.setTextOverflow(value); break;
|
break;
|
||||||
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
case 'items':
|
||||||
case 'justify' : this.setJustify(value); break;
|
this.setItems(value);
|
||||||
case 'fillChar' : this.setFillChar(value); break;
|
break;
|
||||||
case 'focusItemIndex' : this.focusedItemIndex = value; break;
|
case 'focusItems':
|
||||||
|
this.setFocusItems(value);
|
||||||
|
break;
|
||||||
|
case 'hotKeys':
|
||||||
|
this.setHotKeys(value);
|
||||||
|
break;
|
||||||
|
case 'textOverflow':
|
||||||
|
this.setTextOverflow(value);
|
||||||
|
break;
|
||||||
|
case 'hotKeySubmit':
|
||||||
|
this.hotKeySubmit = value;
|
||||||
|
break;
|
||||||
|
case 'justify':
|
||||||
|
this.setJustify(value);
|
||||||
|
break;
|
||||||
|
case 'fillChar':
|
||||||
|
this.setFillChar(value);
|
||||||
|
break;
|
||||||
|
case 'focusItemIndex':
|
||||||
|
this.focusedItemIndex = value;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'itemFormat' :
|
case 'itemFormat':
|
||||||
case 'focusItemFormat' :
|
case 'focusItemFormat':
|
||||||
this[propName] = value;
|
this[propName] = value;
|
||||||
// if there is a cache currently, invalidate it
|
// if there is a cache currently, invalidate it
|
||||||
this.invalidateRenderCache();
|
this.invalidateRenderCache();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'sort' : this.setSort(value); break;
|
case 'sort':
|
||||||
|
this.setSort(value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.setFillChar = function(fillChar) {
|
MenuView.prototype.setFillChar = function (fillChar) {
|
||||||
this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1);
|
this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1);
|
||||||
this.invalidateRenderCache();
|
this.invalidateRenderCache();
|
||||||
}
|
};
|
||||||
|
|
||||||
MenuView.prototype.setJustify = function(justify) {
|
MenuView.prototype.setJustify = function (justify) {
|
||||||
this.justify = justify;
|
this.justify = justify;
|
||||||
this.invalidateRenderCache();
|
this.invalidateRenderCache();
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
MenuView.prototype.setHotKeys = function(hotKeys) {
|
MenuView.prototype.setHotKeys = function (hotKeys) {
|
||||||
if(_.isObject(hotKeys)) {
|
if (_.isObject(hotKeys)) {
|
||||||
if(this.caseInsensitiveHotKeys) {
|
if (this.caseInsensitiveHotKeys) {
|
||||||
this.hotKeys = {};
|
this.hotKeys = {};
|
||||||
for(var key in hotKeys) {
|
for (var key in hotKeys) {
|
||||||
this.hotKeys[key.toLowerCase()] = hotKeys[key];
|
this.hotKeys[key.toLowerCase()] = hotKeys[key];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -323,4 +350,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,27 +2,27 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const msgDb = require('./database.js').dbs.message;
|
const msgDb = require('./database.js').dbs.message;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const Message = require('./message.js');
|
const Message = require('./message.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const msgNetRecord = require('./msg_network.js').recordMessage;
|
const msgNetRecord = require('./msg_network.js').recordMessage;
|
||||||
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
|
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.startup = startup;
|
exports.startup = startup;
|
||||||
exports.shutdown = shutdown;
|
exports.shutdown = shutdown;
|
||||||
exports.getAvailableMessageConferences = getAvailableMessageConferences;
|
exports.getAvailableMessageConferences = getAvailableMessageConferences;
|
||||||
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
|
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
|
||||||
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
|
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
|
||||||
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
|
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
|
||||||
exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags;
|
exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags;
|
||||||
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
|
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
|
||||||
@@ -52,22 +52,31 @@ function startup(cb) {
|
|||||||
// by default, private messages are NOT included
|
// by default, private messages are NOT included
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
Message.findMessages( { resultType : 'count' }, (err, count) => {
|
Message.findMessages({ resultType: 'count' }, (err, count) => {
|
||||||
if(count) {
|
if (count) {
|
||||||
StatLog.setNonPersistentSystemStat(SysProps.MessageTotalCount, count);
|
StatLog.setNonPersistentSystemStat(
|
||||||
|
SysProps.MessageTotalCount,
|
||||||
|
count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
Message.findMessages( { resultType : 'count', date : moment() }, (err, count) => {
|
Message.findMessages(
|
||||||
if(count) {
|
{ resultType: 'count', date: moment() },
|
||||||
StatLog.setNonPersistentSystemStat(SysProps.MessagesToday, count);
|
(err, count) => {
|
||||||
|
if (count) {
|
||||||
|
StatLog.setNonPersistentSystemStat(
|
||||||
|
SysProps.MessagesToday,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return callback(err);
|
||||||
}
|
}
|
||||||
return callback(err);
|
);
|
||||||
});
|
},
|
||||||
}
|
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -80,13 +89,13 @@ function shutdown(cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableMessageConferences(client, options) {
|
function getAvailableMessageConferences(client, options) {
|
||||||
options = options || { includeSystemInternal : false };
|
options = options || { includeSystemInternal: false };
|
||||||
|
|
||||||
assert(client || true === options.noClient);
|
assert(client || true === options.noClient);
|
||||||
|
|
||||||
// perform ACS check per conf & omit system_internal if desired
|
// perform ACS check per conf & omit system_internal if desired
|
||||||
return _.omitBy(Config().messageConferences, (conf, confTag) => {
|
return _.omitBy(Config().messageConferences, (conf, confTag) => {
|
||||||
if(!options.includeSystemInternal && 'system_internal' === confTag) {
|
if (!options.includeSystemInternal && 'system_internal' === confTag) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +106,8 @@ function getAvailableMessageConferences(client, options) {
|
|||||||
function getSortedAvailMessageConferences(client, options) {
|
function getSortedAvailMessageConferences(client, options) {
|
||||||
const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
||||||
return {
|
return {
|
||||||
confTag : k,
|
confTag: k,
|
||||||
conf : v,
|
conf: v,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,10 +123,10 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
|
|||||||
// :TODO: confTag === "" then find default
|
// :TODO: confTag === "" then find default
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(_.has(config.messageConferences, [ confTag, 'areas' ])) {
|
if (_.has(config.messageConferences, [confTag, 'areas'])) {
|
||||||
const areas = config.messageConferences[confTag].areas;
|
const areas = config.messageConferences[confTag].areas;
|
||||||
|
|
||||||
if(!options.client || true === options.noAcsCheck) {
|
if (!options.client || true === options.noAcsCheck) {
|
||||||
// everything - no ACS checks
|
// everything - no ACS checks
|
||||||
return areas;
|
return areas;
|
||||||
} else {
|
} else {
|
||||||
@@ -131,9 +140,9 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
|
|||||||
|
|
||||||
function getSortedAvailMessageAreasByConfTag(confTag, options) {
|
function getSortedAvailMessageAreasByConfTag(confTag, options) {
|
||||||
const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
|
const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
|
||||||
return {
|
return {
|
||||||
areaTag : k,
|
areaTag: k,
|
||||||
area : v,
|
area: v,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,11 +155,13 @@ function getAllAvailableMessageAreaTags(client, options) {
|
|||||||
const areaTags = [];
|
const areaTags = [];
|
||||||
|
|
||||||
// mask over older messy APIs for now
|
// mask over older messy APIs for now
|
||||||
const confOpts = Object.assign({}, options, { noClient : client ? false : true });
|
const confOpts = Object.assign({}, options, { noClient: client ? false : true });
|
||||||
const areaOpts = Object.assign({}, options, { client });
|
const areaOpts = Object.assign({}, options, { client });
|
||||||
|
|
||||||
Object.keys(getAvailableMessageConferences(client, confOpts)).forEach(confTag => {
|
Object.keys(getAvailableMessageConferences(client, confOpts)).forEach(confTag => {
|
||||||
areaTags.push(...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts)));
|
areaTags.push(
|
||||||
|
...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return areaTags;
|
return areaTags;
|
||||||
@@ -171,16 +182,19 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) {
|
|||||||
//
|
//
|
||||||
const config = Config();
|
const config = Config();
|
||||||
let defaultConf = _.findKey(config.messageConferences, o => o.default);
|
let defaultConf = _.findKey(config.messageConferences, o => o.default);
|
||||||
if(defaultConf) {
|
if (defaultConf) {
|
||||||
const conf = config.messageConferences[defaultConf];
|
const conf = config.messageConferences[defaultConf];
|
||||||
if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) {
|
if (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) {
|
||||||
return defaultConf;
|
return defaultConf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// just use anything we can
|
// just use anything we can
|
||||||
defaultConf = _.findKey(config.messageConferences, (conf, confTag) => {
|
defaultConf = _.findKey(config.messageConferences, (conf, confTag) => {
|
||||||
return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf));
|
return (
|
||||||
|
'system_internal' !== confTag &&
|
||||||
|
(true === disableAcsCheck || client.acs.hasMessageConfRead(conf))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return defaultConf;
|
return defaultConf;
|
||||||
@@ -197,21 +211,21 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
|
|||||||
confTag = confTag || getDefaultMessageConferenceTag(client);
|
confTag = confTag || getDefaultMessageConferenceTag(client);
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) {
|
if (confTag && _.has(config.messageConferences, [confTag, 'areas'])) {
|
||||||
const areaPool = config.messageConferences[confTag].areas;
|
const areaPool = config.messageConferences[confTag].areas;
|
||||||
let defaultArea = _.findKey(areaPool, o => o.default);
|
let defaultArea = _.findKey(areaPool, o => o.default);
|
||||||
if(defaultArea) {
|
if (defaultArea) {
|
||||||
const area = areaPool[defaultArea];
|
const area = areaPool[defaultArea];
|
||||||
if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) {
|
if (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) {
|
||||||
return defaultArea;
|
return defaultArea;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultArea = _.findKey(areaPool, (area, areaTag) => {
|
defaultArea = _.findKey(areaPool, (area, areaTag) => {
|
||||||
if(Message.isPrivateAreaTag(areaTag)) {
|
if (Message.isPrivateAreaTag(areaTag)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area));
|
return true === disableAcsCheck || client.acs.hasMessageAreaRead(area);
|
||||||
});
|
});
|
||||||
|
|
||||||
return defaultArea;
|
return defaultArea;
|
||||||
@@ -230,26 +244,29 @@ function getSuitableMessageConfAndAreaTags(client) {
|
|||||||
// if we fail to find something.
|
// if we fail to find something.
|
||||||
//
|
//
|
||||||
let confTag = getDefaultMessageConferenceTag(client);
|
let confTag = getDefaultMessageConferenceTag(client);
|
||||||
if(!confTag) {
|
if (!confTag) {
|
||||||
return ['', '']; // can't have an area without a conf
|
return ['', '']; // can't have an area without a conf
|
||||||
}
|
}
|
||||||
|
|
||||||
let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
||||||
if(!areaTag) {
|
if (!areaTag) {
|
||||||
// OK, perhaps *any* area in *any* conf?
|
// OK, perhaps *any* area in *any* conf?
|
||||||
_.forEach(Config().messageConferences, (conf, ct) => {
|
_.forEach(Config().messageConferences, (conf, ct) => {
|
||||||
if(!client.acs.hasMessageConfRead(conf)) {
|
if (!client.acs.hasMessageConfRead(conf)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_.forEach(conf.areas, (area, at) => {
|
_.forEach(conf.areas, (area, at) => {
|
||||||
if(!_.includes(Message.WellKnownAreaTags, at) && client.acs.hasMessageAreaRead(area)) {
|
if (
|
||||||
|
!_.includes(Message.WellKnownAreaTags, at) &&
|
||||||
|
client.acs.hasMessageAreaRead(area)
|
||||||
|
) {
|
||||||
confTag = ct;
|
confTag = ct;
|
||||||
areaTag = at;
|
areaTag = at;
|
||||||
return false; // stop inner iteration
|
return false; // stop inner iteration
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(areaTag) {
|
if (areaTag) {
|
||||||
return false; // stop iteration
|
return false; // stop iteration
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -263,8 +280,8 @@ function getMessageConferenceByTag(confTag) {
|
|||||||
|
|
||||||
function getMessageConfTagByAreaTag(areaTag) {
|
function getMessageConfTagByAreaTag(areaTag) {
|
||||||
const confs = Config().messageConferences;
|
const confs = Config().messageConferences;
|
||||||
return Object.keys(confs).find( (confTag) => {
|
return Object.keys(confs).find(confTag => {
|
||||||
return _.has(confs, [ confTag, 'areas', areaTag]);
|
return _.has(confs, [confTag, 'areas', areaTag]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,12 +289,12 @@ function getMessageAreaByTag(areaTag, optionalConfTag) {
|
|||||||
const confs = Config().messageConferences;
|
const confs = Config().messageConferences;
|
||||||
|
|
||||||
// :TODO: this could be cached
|
// :TODO: this could be cached
|
||||||
if(_.isString(optionalConfTag)) {
|
if (_.isString(optionalConfTag)) {
|
||||||
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
|
if (_.has(confs, [optionalConfTag, 'areas', areaTag])) {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
{
|
{
|
||||||
areaTag,
|
areaTag,
|
||||||
confTag : optionalConfTag,
|
confTag: optionalConfTag,
|
||||||
},
|
},
|
||||||
confs[optionalConfTag].areas[areaTag]
|
confs[optionalConfTag].areas[areaTag]
|
||||||
);
|
);
|
||||||
@@ -288,9 +305,9 @@ function getMessageAreaByTag(areaTag, optionalConfTag) {
|
|||||||
//
|
//
|
||||||
let area;
|
let area;
|
||||||
_.forEach(confs, (conf, confTag) => {
|
_.forEach(confs, (conf, confTag) => {
|
||||||
if(_.has(conf, [ 'areas', areaTag ])) {
|
if (_.has(conf, ['areas', areaTag])) {
|
||||||
area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]);
|
area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]);
|
||||||
return false; // stop iteration
|
return false; // stop iteration
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -304,33 +321,38 @@ function changeMessageConference(client, confTag, cb) {
|
|||||||
function getConf(callback) {
|
function getConf(callback) {
|
||||||
const conf = getMessageConferenceByTag(confTag);
|
const conf = getMessageConferenceByTag(confTag);
|
||||||
|
|
||||||
if(conf) {
|
if (conf) {
|
||||||
callback(null, conf);
|
callback(null, conf);
|
||||||
} else {
|
} else {
|
||||||
callback(new Error('Invalid message conference tag'));
|
callback(new Error('Invalid message conference tag'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function getDefaultAreaInConf(conf, callback) {
|
function getDefaultAreaInConf(conf, callback) {
|
||||||
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
||||||
const area = getMessageAreaByTag(areaTag, confTag);
|
const area = getMessageAreaByTag(areaTag, confTag);
|
||||||
|
|
||||||
if(area) {
|
if (area) {
|
||||||
callback(null, conf, { areaTag : areaTag, area : area } );
|
callback(null, conf, { areaTag: areaTag, area: area });
|
||||||
} else {
|
} else {
|
||||||
callback(new Error('No available areas for this user in conference'));
|
callback(new Error('No available areas for this user in conference'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function validateAccess(conf, areaInfo, callback) {
|
function validateAccess(conf, areaInfo, callback) {
|
||||||
if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(areaInfo.area)) {
|
if (
|
||||||
return callback(new Error('Access denied to message area and/or conference'));
|
!client.acs.hasMessageConfRead(conf) ||
|
||||||
|
!client.acs.hasMessageAreaRead(areaInfo.area)
|
||||||
|
) {
|
||||||
|
return callback(
|
||||||
|
new Error('Access denied to message area and/or conference')
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return callback(null, conf, areaInfo);
|
return callback(null, conf, areaInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function changeConferenceAndArea(conf, areaInfo, callback) {
|
function changeConferenceAndArea(conf, areaInfo, callback) {
|
||||||
const newProps = {
|
const newProps = {
|
||||||
[ UserProps.MessageConfTag ] : confTag,
|
[UserProps.MessageConfTag]: confTag,
|
||||||
[ UserProps.MessageAreaTag ] : areaInfo.areaTag,
|
[UserProps.MessageAreaTag]: areaInfo.areaTag,
|
||||||
};
|
};
|
||||||
client.user.persistProperties(newProps, err => {
|
client.user.persistProperties(newProps, err => {
|
||||||
callback(err, conf, areaInfo);
|
callback(err, conf, areaInfo);
|
||||||
@@ -338,10 +360,16 @@ function changeMessageConference(client, confTag, cb) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function complete(err, conf, areaInfo) {
|
function complete(err, conf, areaInfo) {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
|
client.log.info(
|
||||||
|
{ confTag: confTag, confName: conf.name, areaTag: areaInfo.areaTag },
|
||||||
|
'Current message conference changed'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
|
client.log.warn(
|
||||||
|
{ confTag: confTag, error: err.message },
|
||||||
|
'Could not change message conference'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
cb(err);
|
cb(err);
|
||||||
}
|
}
|
||||||
@@ -349,7 +377,7 @@ function changeMessageConference(client, confTag, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
||||||
options = options || {}; // :TODO: this is currently pointless... cb is required...
|
options = options || {}; // :TODO: this is currently pointless... cb is required...
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
@@ -361,28 +389,38 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
|||||||
//
|
//
|
||||||
// Need at least *read* to access the area
|
// Need at least *read* to access the area
|
||||||
//
|
//
|
||||||
if(!client.acs.hasMessageAreaRead(area)) {
|
if (!client.acs.hasMessageAreaRead(area)) {
|
||||||
return callback(new Error('Access denied to message area'));
|
return callback(new Error('Access denied to message area'));
|
||||||
} else {
|
} else {
|
||||||
return callback(null, area);
|
return callback(null, area);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function changeArea(area, callback) {
|
function changeArea(area, callback) {
|
||||||
if(true === options.persist) {
|
if (true === options.persist) {
|
||||||
client.user.persistProperty(UserProps.MessageAreaTag, areaTag, function persisted(err) {
|
client.user.persistProperty(
|
||||||
return callback(err, area);
|
UserProps.MessageAreaTag,
|
||||||
});
|
areaTag,
|
||||||
|
function persisted(err) {
|
||||||
|
return callback(err, area);
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
client.user.properties[UserProps.MessageAreaTag] = areaTag;
|
client.user.properties[UserProps.MessageAreaTag] = areaTag;
|
||||||
return callback(null, area);
|
return callback(null, area);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function complete(err, area) {
|
function complete(err, area) {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
|
client.log.info(
|
||||||
|
{ areaTag: areaTag, area: area },
|
||||||
|
'Current message area changed'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
client.log.warn(
|
||||||
|
{ areaTag: areaTag, area: area, error: err.message },
|
||||||
|
'Could not change message area'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -397,16 +435,16 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
|||||||
// This is useful for example when doing a new scan
|
// This is useful for example when doing a new scan
|
||||||
//
|
//
|
||||||
function tempChangeMessageConfAndArea(client, areaTag) {
|
function tempChangeMessageConfAndArea(client, areaTag) {
|
||||||
const area = getMessageAreaByTag(areaTag);
|
const area = getMessageAreaByTag(areaTag);
|
||||||
const confTag = getMessageConfTagByAreaTag(areaTag);
|
const confTag = getMessageConfTagByAreaTag(areaTag);
|
||||||
|
|
||||||
if(!area || !confTag) {
|
if (!area || !confTag) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const conf = getMessageConferenceByTag(confTag);
|
const conf = getMessageConferenceByTag(confTag);
|
||||||
|
|
||||||
if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) {
|
if (!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,31 +455,35 @@ function tempChangeMessageConfAndArea(client, areaTag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeMessageArea(client, areaTag, cb) {
|
function changeMessageArea(client, areaTag, cb) {
|
||||||
changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb);
|
changeMessageAreaWithOptions(client, areaTag, { persist: true }, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMessageConfAndAreaRead(client, areaOrTag) {
|
function hasMessageConfAndAreaRead(client, areaOrTag) {
|
||||||
if(_.isString(areaOrTag)) {
|
if (_.isString(areaOrTag)) {
|
||||||
areaOrTag = getMessageAreaByTag(areaOrTag) || {};
|
areaOrTag = getMessageAreaByTag(areaOrTag) || {};
|
||||||
}
|
}
|
||||||
const conf = getMessageConferenceByTag(areaOrTag.confTag);
|
const conf = getMessageConferenceByTag(areaOrTag.confTag);
|
||||||
return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag);
|
return (
|
||||||
|
client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMessageConfAndAreaWrite(client, areaOrTag) {
|
function hasMessageConfAndAreaWrite(client, areaOrTag) {
|
||||||
if(_.isString(areaOrTag)) {
|
if (_.isString(areaOrTag)) {
|
||||||
areaOrTag = getMessageAreaByTag(areaOrTag) || {};
|
areaOrTag = getMessageAreaByTag(areaOrTag) || {};
|
||||||
}
|
}
|
||||||
const conf = getMessageConferenceByTag(areaOrTag.confTag);
|
const conf = getMessageConferenceByTag(areaOrTag.confTag);
|
||||||
return client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag);
|
return (
|
||||||
|
client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterMessageAreaTagsByReadACS(client, areaTags) {
|
function filterMessageAreaTagsByReadACS(client, areaTags) {
|
||||||
if(!Array.isArray(areaTags)) {
|
if (!Array.isArray(areaTags)) {
|
||||||
areaTags = [ areaTags ];
|
areaTags = [areaTags];
|
||||||
}
|
}
|
||||||
|
|
||||||
return areaTags.filter( areaTag => {
|
return areaTags.filter(areaTag => {
|
||||||
const area = getMessageAreaByTag(areaTag);
|
const area = getMessageAreaByTag(areaTag);
|
||||||
return hasMessageConfAndAreaRead(client, area);
|
return hasMessageConfAndAreaRead(client, area);
|
||||||
});
|
});
|
||||||
@@ -454,14 +496,14 @@ function filterMessageListByReadACS(client, messageList) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Keep a cache around for quick lookup.
|
// Keep a cache around for quick lookup.
|
||||||
const acsCache = new Map(); // areaTag:boolean
|
const acsCache = new Map(); // areaTag:boolean
|
||||||
|
|
||||||
return messageList.filter(msg => {
|
return messageList.filter(msg => {
|
||||||
let cached = acsCache.get(msg.areaTag);
|
let cached = acsCache.get(msg.areaTag);
|
||||||
if(false === cached) {
|
if (false === cached) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(true === cached) {
|
if (true === cached) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
cached = hasMessageConfAndAreaRead(client, msg.areaTag);
|
cached = hasMessageConfAndAreaRead(client, msg.areaTag);
|
||||||
@@ -476,11 +518,11 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) {
|
|||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
areaTag,
|
areaTag,
|
||||||
newerThanMessageId : lastMessageId,
|
newerThanMessageId: lastMessageId,
|
||||||
resultType : 'count',
|
resultType: 'count',
|
||||||
};
|
};
|
||||||
|
|
||||||
if(Message.isPrivateAreaTag(areaTag)) {
|
if (Message.isPrivateAreaTag(areaTag)) {
|
||||||
filter.privateTagUserId = userId;
|
filter.privateTagUserId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,13 +558,13 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
|||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
areaTag,
|
areaTag,
|
||||||
resultType : 'messageList',
|
resultType: 'messageList',
|
||||||
newerThanMessageId : lastMessageId,
|
newerThanMessageId: lastMessageId,
|
||||||
sort : 'messageId',
|
sort: 'messageId',
|
||||||
order : 'ascending',
|
order: 'ascending',
|
||||||
};
|
};
|
||||||
|
|
||||||
if(Message.isPrivateAreaTag(areaTag)) {
|
if (Message.isPrivateAreaTag(areaTag)) {
|
||||||
filter.privateTagUserId = userId;
|
filter.privateTagUserId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,21 +579,21 @@ function getMessageListForArea(client, areaTag, filter, cb)
|
|||||||
cb = filter;
|
cb = filter;
|
||||||
filter = {
|
filter = {
|
||||||
areaTag,
|
areaTag,
|
||||||
resultType : 'messageList',
|
resultType: 'messageList',
|
||||||
sort : 'messageId',
|
sort: 'messageId',
|
||||||
order : 'ascending'
|
order: 'ascending',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
Object.assign(filter, { areaTag } );
|
Object.assign(filter, { areaTag });
|
||||||
}
|
}
|
||||||
|
|
||||||
if(client) {
|
if (client) {
|
||||||
if(!hasMessageConfAndAreaRead(client, areaTag)) {
|
if (!hasMessageConfAndAreaRead(client, areaTag)) {
|
||||||
return cb(null, []);
|
return cb(null, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Message.isPrivateAreaTag(areaTag)) {
|
if (Message.isPrivateAreaTag(areaTag)) {
|
||||||
filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID';
|
filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,12 +605,12 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
|
|||||||
{
|
{
|
||||||
areaTag,
|
areaTag,
|
||||||
newerThanTimestamp,
|
newerThanTimestamp,
|
||||||
sort : 'modTimestamp',
|
sort: 'modTimestamp',
|
||||||
order : 'ascending',
|
order: 'ascending',
|
||||||
limit : 1,
|
limit: 1,
|
||||||
},
|
},
|
||||||
(err, id) => {
|
(err, id) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
return cb(null, id ? id[0] : null);
|
return cb(null, id ? id[0] : null);
|
||||||
@@ -578,10 +620,10 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
|
|||||||
|
|
||||||
function getMessageAreaLastReadId(userId, areaTag, cb) {
|
function getMessageAreaLastReadId(userId, areaTag, cb) {
|
||||||
msgDb.get(
|
msgDb.get(
|
||||||
'SELECT message_id ' +
|
'SELECT message_id ' +
|
||||||
'FROM user_message_area_last_read ' +
|
'FROM user_message_area_last_read ' +
|
||||||
'WHERE user_id = ? AND area_tag = ?;',
|
'WHERE user_id = ? AND area_tag = ?;',
|
||||||
[ userId, areaTag.toLowerCase() ],
|
[userId, areaTag.toLowerCase()],
|
||||||
function complete(err, row) {
|
function complete(err, row) {
|
||||||
cb(err, row ? row.message_id : 0);
|
cb(err, row ? row.message_id : 0);
|
||||||
}
|
}
|
||||||
@@ -589,7 +631,7 @@ function getMessageAreaLastReadId(userId, areaTag, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) {
|
function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) {
|
||||||
if(!cb && _.isFunction(allowOlder)) {
|
if (!cb && _.isFunction(allowOlder)) {
|
||||||
cb = allowOlder;
|
cb = allowOlder;
|
||||||
allowOlder = false;
|
allowOlder = false;
|
||||||
}
|
}
|
||||||
@@ -604,30 +646,37 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb)
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function update(lastId, callback) {
|
function update(lastId, callback) {
|
||||||
if(allowOlder || messageId > lastId) {
|
if (allowOlder || messageId > lastId) {
|
||||||
msgDb.run(
|
msgDb.run(
|
||||||
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
|
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
|
||||||
'VALUES (?, ?, ?);',
|
'VALUES (?, ?, ?);',
|
||||||
[ userId, areaTag, messageId ],
|
[userId, areaTag, messageId],
|
||||||
function written(err) {
|
function written(err) {
|
||||||
callback(err, true); // true=didUpdate
|
callback(err, true); // true=didUpdate
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function complete(err, didUpdate) {
|
function complete(err, didUpdate) {
|
||||||
if(err) {
|
if (err) {
|
||||||
Log.debug(
|
Log.debug(
|
||||||
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId },
|
{
|
||||||
'Failed updating area last read ID');
|
error: err.toString(),
|
||||||
|
userId: userId,
|
||||||
|
areaTag: areaTag,
|
||||||
|
messageId: messageId,
|
||||||
|
},
|
||||||
|
'Failed updating area last read ID'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if(true === didUpdate) {
|
if (true === didUpdate) {
|
||||||
Log.trace(
|
Log.trace(
|
||||||
{ userId : userId, areaTag : areaTag, messageId : messageId },
|
{ userId: userId, areaTag: areaTag, messageId: messageId },
|
||||||
'Area last read ID updated');
|
'Area last read ID updated'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cb(err);
|
cb(err);
|
||||||
@@ -643,7 +692,7 @@ function persistMessage(message, cb) {
|
|||||||
},
|
},
|
||||||
function recordToMessageNetworks(callback) {
|
function recordToMessageNetworks(callback) {
|
||||||
return msgNetRecord(message, callback);
|
return msgNetRecord(message, callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
@@ -651,9 +700,8 @@ function persistMessage(message, cb) {
|
|||||||
|
|
||||||
// method exposed for event scheduler
|
// method exposed for event scheduler
|
||||||
function trimMessageAreasScheduledEvent(args, cb) {
|
function trimMessageAreasScheduledEvent(args, cb) {
|
||||||
|
|
||||||
function trimMessageAreaByMaxMessages(areaInfo, cb) {
|
function trimMessageAreaByMaxMessages(areaInfo, cb) {
|
||||||
if(0 === areaInfo.maxMessages) {
|
if (0 === areaInfo.maxMessages) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,12 +714,19 @@ function trimMessageAreasScheduledEvent(args, cb) {
|
|||||||
ORDER BY message_id DESC
|
ORDER BY message_id DESC
|
||||||
LIMIT -1 OFFSET ${areaInfo.maxMessages}
|
LIMIT -1 OFFSET ${areaInfo.maxMessages}
|
||||||
);`,
|
);`,
|
||||||
[ areaInfo.areaTag.toLowerCase() ],
|
[areaInfo.areaTag.toLowerCase()],
|
||||||
function result(err) { // no arrow func; need this
|
function result(err) {
|
||||||
if(err) {
|
// no arrow func; need this
|
||||||
Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area');
|
if (err) {
|
||||||
|
Log.error(
|
||||||
|
{ areaInfo: areaInfo, error: err.message, type: 'maxMessages' },
|
||||||
|
'Error trimming message area'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully');
|
Log.debug(
|
||||||
|
{ areaInfo: areaInfo, type: 'maxMessages', count: this.changes },
|
||||||
|
'Area trimmed successfully'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
@@ -679,19 +734,26 @@ function trimMessageAreasScheduledEvent(args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function trimMessageAreaByMaxAgeDays(areaInfo, cb) {
|
function trimMessageAreaByMaxAgeDays(areaInfo, cb) {
|
||||||
if(0 === areaInfo.maxAgeDays) {
|
if (0 === areaInfo.maxAgeDays) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
msgDb.run(
|
msgDb.run(
|
||||||
`DELETE FROM message
|
`DELETE FROM message
|
||||||
WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`,
|
WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`,
|
||||||
[ areaInfo.areaTag ],
|
[areaInfo.areaTag],
|
||||||
function result(err) { // no arrow func; need this
|
function result(err) {
|
||||||
if(err) {
|
// no arrow func; need this
|
||||||
Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area');
|
if (err) {
|
||||||
|
Log.warn(
|
||||||
|
{ areaInfo: areaInfo, error: err.message, type: 'maxAgeDays' },
|
||||||
|
'Error trimming message area'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully');
|
Log.debug(
|
||||||
|
{ areaInfo: areaInfo, type: 'maxAgeDays', count: this.changes },
|
||||||
|
'Area trimmed successfully'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
@@ -710,12 +772,12 @@ function trimMessageAreasScheduledEvent(args, cb) {
|
|||||||
`SELECT DISTINCT area_tag
|
`SELECT DISTINCT area_tag
|
||||||
FROM message;`,
|
FROM message;`,
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We treat private mail special
|
// We treat private mail special
|
||||||
if(!Message.isPrivateAreaTag(row.area_tag)) {
|
if (!Message.isPrivateAreaTag(row.area_tag)) {
|
||||||
areaTags.push(row.area_tag);
|
areaTags.push(row.area_tag);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -730,21 +792,20 @@ function trimMessageAreasScheduledEvent(args, cb) {
|
|||||||
// determine maxMessages & maxAgeDays per area
|
// determine maxMessages & maxAgeDays per area
|
||||||
const config = Config();
|
const config = Config();
|
||||||
areaTags.forEach(areaTag => {
|
areaTags.forEach(areaTag => {
|
||||||
|
|
||||||
let maxMessages = config.messageAreaDefaults.maxMessages;
|
let maxMessages = config.messageAreaDefaults.maxMessages;
|
||||||
let maxAgeDays = config.messageAreaDefaults.maxAgeDays;
|
let maxAgeDays = config.messageAreaDefaults.maxAgeDays;
|
||||||
|
|
||||||
const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here
|
const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here
|
||||||
if(area) {
|
if (area) {
|
||||||
maxMessages = area.maxMessages || maxMessages;
|
maxMessages = area.maxMessages || maxMessages;
|
||||||
maxAgeDays = area.maxAgeDays || maxAgeDays;
|
maxAgeDays = area.maxAgeDays || maxAgeDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
areaInfos.push( {
|
areaInfos.push({
|
||||||
areaTag : areaTag,
|
areaTag: areaTag,
|
||||||
maxMessages : maxMessages,
|
maxMessages: maxMessages,
|
||||||
maxAgeDays : maxAgeDays,
|
maxAgeDays: maxAgeDays,
|
||||||
} );
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return callback(null, areaInfos);
|
return callback(null, areaInfos);
|
||||||
@@ -754,7 +815,7 @@ function trimMessageAreasScheduledEvent(args, cb) {
|
|||||||
areaInfos,
|
areaInfos,
|
||||||
(areaInfo, next) => {
|
(areaInfo, next) => {
|
||||||
trimMessageAreaByMaxMessages(areaInfo, err => {
|
trimMessageAreaByMaxMessages(areaInfo, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,20 +856,27 @@ function trimMessageAreasScheduledEvent(args, cb) {
|
|||||||
(mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}')
|
(mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}')
|
||||||
WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days')
|
WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days')
|
||||||
);`,
|
);`,
|
||||||
function results(err) { // no arrow func; need this
|
function results(err) {
|
||||||
if(err) {
|
// no arrow func; need this
|
||||||
Log.warn( { error : err.message }, 'Error trimming private externally sent messages');
|
if (err) {
|
||||||
|
Log.warn(
|
||||||
|
{ error: err.message },
|
||||||
|
'Error trimming private externally sent messages'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully');
|
Log.debug(
|
||||||
|
{ count: this.changes },
|
||||||
|
'Private externally sent messages trimmed successfully'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,44 +22,51 @@ const _ = require('lodash');
|
|||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
const temptmp = require('temptmp');
|
const temptmp = require('temptmp');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const { v4 : UUIDv4 } = require('uuid');
|
const { v4: UUIDv4 } = require('uuid');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
main : 0,
|
main: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
main : {
|
main: {
|
||||||
status : 1,
|
status: 1,
|
||||||
progressBar : 2,
|
progressBar: 2,
|
||||||
|
|
||||||
customRangeStart : 10,
|
customRangeStart: 10,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserProperties = {
|
const UserProperties = {
|
||||||
ExportOptions : 'qwk_export_options',
|
ExportOptions: 'qwk_export_options',
|
||||||
ExportAreas : 'qwk_export_msg_areas',
|
ExportAreas: 'qwk_export_msg_areas',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'QWK Export',
|
name: 'QWK Export',
|
||||||
desc : 'Exports a QWK Packet for download',
|
desc: 'Exports a QWK Packet for download',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
|
this.config = Object.assign(
|
||||||
|
{},
|
||||||
|
_.get(options, 'menuConfig.config'),
|
||||||
|
options.extraArgs
|
||||||
|
);
|
||||||
|
|
||||||
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1);
|
this.config.progBarChar = renderSubstr(this.config.progBarChar || '▒', 0, 1);
|
||||||
this.config.bbsID = this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA');
|
this.config.bbsID =
|
||||||
|
this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA');
|
||||||
|
|
||||||
this.tempName = `${UUIDv4().substr(-8).toUpperCase()}.QWK`;
|
this.tempName = `${UUIDv4().substr(-8).toUpperCase()}.QWK`;
|
||||||
this.sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads);
|
this.sysTempDownloadArea = FileArea.getFileAreaByTag(
|
||||||
|
FileArea.WellKnownAreaTags.TempDownloads
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
@@ -70,27 +77,38 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
this.prepViewController('main', FormIds.main, mciData.menu, err => {
|
this.prepViewController(
|
||||||
return callback(err);
|
'main',
|
||||||
});
|
FormIds.main,
|
||||||
},
|
mciData.menu,
|
||||||
(callback) => {
|
err => {
|
||||||
this.temptmp = temptmp.createTrackedSession('qwkuserexp');
|
|
||||||
this.temptmp.mkdir({ prefix : 'enigqwkwriter-'}, (err, tempDir) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
callback => {
|
||||||
|
this.temptmp = temptmp.createTrackedSession('qwkuserexp');
|
||||||
|
this.temptmp.mkdir(
|
||||||
|
{ prefix: 'enigqwkwriter-' },
|
||||||
|
(err, tempDir) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
this.tempPacketDir = tempDir;
|
this.tempPacketDir = tempDir;
|
||||||
|
|
||||||
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(this.sysTempDownloadArea);
|
const sysTempDownloadDir =
|
||||||
|
FileArea.getAreaDefaultStorageDirectory(
|
||||||
|
this.sysTempDownloadArea
|
||||||
|
);
|
||||||
|
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
fse.mkdirs(sysTempDownloadDir, err => {
|
fse.mkdirs(sysTempDownloadDir, err => {
|
||||||
return callback(err, sysTempDownloadDir);
|
return callback(err, sysTempDownloadDir);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(sysTempDownloadDir, callback) => {
|
(sysTempDownloadDir, callback) => {
|
||||||
this._performExport(sysTempDownloadDir, err => {
|
this._performExport(sysTempDownloadDir, err => {
|
||||||
@@ -104,7 +122,10 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
if (err) {
|
if (err) {
|
||||||
// :TODO: doesn't do anything currently:
|
// :TODO: doesn't do anything currently:
|
||||||
if ('NORESULTS' === err.reasonCode) {
|
if ('NORESULTS' === err.reasonCode) {
|
||||||
return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'qwkExportNoResults');
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.noResultsMenu ||
|
||||||
|
'qwkExportNoResults'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prevMenu();
|
return this.prevMenu();
|
||||||
@@ -123,12 +144,12 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
let qwkOptions = this.client.user.getProperty(UserProperties.ExportOptions);
|
let qwkOptions = this.client.user.getProperty(UserProperties.ExportOptions);
|
||||||
try {
|
try {
|
||||||
qwkOptions = JSON.parse(qwkOptions);
|
qwkOptions = JSON.parse(qwkOptions);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
qwkOptions = {
|
qwkOptions = {
|
||||||
enableQWKE : true,
|
enableQWKE: true,
|
||||||
enableHeadersExtension : true,
|
enableHeadersExtension: true,
|
||||||
enableAtKludges : true,
|
enableAtKludges: true,
|
||||||
archiveFormat : 'application/zip',
|
archiveFormat: 'application/zip',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return qwkOptions;
|
return qwkOptions;
|
||||||
@@ -143,7 +164,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
}
|
}
|
||||||
return exportArea;
|
return exportArea;
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// default to all public and private without 'since'
|
// default to all public and private without 'since'
|
||||||
qwkExportAreas = getAllAvailableMessageAreaTags(this.client).map(areaTag => {
|
qwkExportAreas = getAllAvailableMessageAreaTags(this.client).map(areaTag => {
|
||||||
return { areaTag };
|
return { areaTag };
|
||||||
@@ -151,7 +172,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
|
|
||||||
// Include user's private area
|
// Include user's private area
|
||||||
qwkExportAreas.push({
|
qwkExportAreas.push({
|
||||||
areaTag : Message.WellKnownAreaTags.Private,
|
areaTag: Message.WellKnownAreaTags.Private,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,16 +181,18 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
|
|
||||||
_performExport(sysTempDownloadDir, cb) {
|
_performExport(sysTempDownloadDir, cb) {
|
||||||
const statusView = this.viewControllers.main.getView(MciViewIds.main.status);
|
const statusView = this.viewControllers.main.getView(MciViewIds.main.status);
|
||||||
const updateStatus = (status) => {
|
const updateStatus = status => {
|
||||||
if (statusView) {
|
if (statusView) {
|
||||||
statusView.setText(status);
|
statusView.setText(status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const progBarView = this.viewControllers.main.getView(MciViewIds.main.progressBar);
|
const progBarView = this.viewControllers.main.getView(
|
||||||
|
MciViewIds.main.progressBar
|
||||||
|
);
|
||||||
const updateProgressBar = (curr, total) => {
|
const updateProgressBar = (curr, total) => {
|
||||||
if (progBarView) {
|
if (progBarView) {
|
||||||
const prog = Math.floor( (curr / total) * progBarView.dimens.width );
|
const prog = Math.floor((curr / total) * progBarView.dimens.width);
|
||||||
progBarView.setText(this.config.progBarChar.repeat(prog));
|
progBarView.setText(this.config.progBarChar.repeat(prog));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -181,19 +204,27 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
// we can produce a TON of updates; only update progress at most every 3/4s
|
// we can produce a TON of updates; only update progress at most every 3/4s
|
||||||
if (Date.now() - lastProgUpdate > 750) {
|
if (Date.now() - lastProgUpdate > 750) {
|
||||||
switch (state.step) {
|
switch (state.step) {
|
||||||
case 'next_area' :
|
case 'next_area':
|
||||||
updateStatus(state.status);
|
updateStatus(state.status);
|
||||||
updateProgressBar(0, 0);
|
updateProgressBar(0, 0);
|
||||||
this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state);
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'main',
|
||||||
|
MciViewIds.main.customRangeStart,
|
||||||
|
state
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'message' :
|
case 'message':
|
||||||
updateStatus(state.status);
|
updateStatus(state.status);
|
||||||
updateProgressBar(state.current, state.total);
|
updateProgressBar(state.current, state.total);
|
||||||
this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state);
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'main',
|
||||||
|
MciViewIds.main.customRangeStart,
|
||||||
|
state
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default :
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lastProgUpdate = Date.now();
|
lastProgUpdate = Date.now();
|
||||||
@@ -203,7 +234,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const keyPressHandler = (ch, key) => {
|
const keyPressHandler = (ch, key) => {
|
||||||
if('escape' === key.name) {
|
if ('escape' === key.name) {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
this.client.removeListener('key press', keyPressHandler);
|
this.client.removeListener('key press', keyPressHandler);
|
||||||
}
|
}
|
||||||
@@ -217,54 +248,59 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let current = 1;
|
let current = 1;
|
||||||
async.eachSeries(messageIds, (messageId, nextMessageId) => {
|
async.eachSeries(
|
||||||
const message = new Message();
|
messageIds,
|
||||||
message.load({ messageId }, err => {
|
(messageId, nextMessageId) => {
|
||||||
if (err) {
|
const message = new Message();
|
||||||
return nextMessageId(err);
|
message.load({ messageId }, err => {
|
||||||
}
|
|
||||||
|
|
||||||
const progress = {
|
|
||||||
message,
|
|
||||||
step : 'message',
|
|
||||||
total : ++totalExported,
|
|
||||||
areaCurrent : current,
|
|
||||||
areaCount : messageIds.length,
|
|
||||||
status : `${_.truncate(message.subject, { length : 25 })} (${current} / ${messageIds.length})`,
|
|
||||||
};
|
|
||||||
|
|
||||||
progressHandler(progress, err => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return nextMessageId(err);
|
return nextMessageId(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
packetWriter.appendMessage(message);
|
const progress = {
|
||||||
current += 1;
|
message,
|
||||||
|
step: 'message',
|
||||||
|
total: ++totalExported,
|
||||||
|
areaCurrent: current,
|
||||||
|
areaCount: messageIds.length,
|
||||||
|
status: `${_.truncate(message.subject, {
|
||||||
|
length: 25,
|
||||||
|
})} (${current} / ${messageIds.length})`,
|
||||||
|
};
|
||||||
|
|
||||||
return nextMessageId(null);
|
progressHandler(progress, err => {
|
||||||
|
if (err) {
|
||||||
|
return nextMessageId(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
packetWriter.appendMessage(message);
|
||||||
|
current += 1;
|
||||||
|
|
||||||
|
return nextMessageId(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
},
|
err => {
|
||||||
err => {
|
return cb(err);
|
||||||
return cb(err);
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const packetWriter = new QWKPacketWriter(
|
const packetWriter = new QWKPacketWriter(
|
||||||
Object.assign(this._getUserQWKExportOptions(), {
|
Object.assign(this._getUserQWKExportOptions(), {
|
||||||
user : this.client.user,
|
user: this.client.user,
|
||||||
bbsID : this.config.bbsID,
|
bbsID: this.config.bbsID,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
packetWriter.on('warning', warning => {
|
packetWriter.on('warning', warning => {
|
||||||
this.client.log.warn( { warning }, 'QWK packet writer warning');
|
this.client.log.warn({ warning }, 'QWK packet writer warning');
|
||||||
});
|
});
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
// don't count idle monitor while processing
|
// don't count idle monitor while processing
|
||||||
this.client.stopIdleMonitor();
|
this.client.stopIdleMonitor();
|
||||||
|
|
||||||
@@ -276,77 +312,91 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
});
|
});
|
||||||
|
|
||||||
packetWriter.once('error', err => {
|
packetWriter.once('error', err => {
|
||||||
this.client.log.error( { error : err.message }, 'QWK packet writer error');
|
this.client.log.error(
|
||||||
|
{ error: err.message },
|
||||||
|
'QWK packet writer error'
|
||||||
|
);
|
||||||
cancel = true;
|
cancel = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
packetWriter.init();
|
packetWriter.init();
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
// For each public area -> for each message
|
// For each public area -> for each message
|
||||||
const userExportAreas = this._getUserQWKExportAreas();
|
const userExportAreas = this._getUserQWKExportAreas();
|
||||||
|
|
||||||
const publicExportAreas = userExportAreas
|
const publicExportAreas = userExportAreas.filter(exportArea => {
|
||||||
.filter(exportArea => {
|
return exportArea.areaTag !== Message.WellKnownAreaTags.Private;
|
||||||
return exportArea.areaTag !== Message.WellKnownAreaTags.Private;
|
});
|
||||||
});
|
async.eachSeries(
|
||||||
async.eachSeries(publicExportAreas, (exportArea, nextExportArea) => {
|
publicExportAreas,
|
||||||
const area = getMessageAreaByTag(exportArea.areaTag);
|
(exportArea, nextExportArea) => {
|
||||||
const conf = getMessageConferenceByTag(area.confTag);
|
const area = getMessageAreaByTag(exportArea.areaTag);
|
||||||
if (!area || !conf) {
|
const conf = getMessageConferenceByTag(area.confTag);
|
||||||
// :TODO: remove from user properties - this area does not exist
|
if (!area || !conf) {
|
||||||
this.client.log.warn({ areaTag : exportArea.areaTag }, 'Cannot QWK export area as it does not exist');
|
// :TODO: remove from user properties - this area does not exist
|
||||||
return nextExportArea(null);
|
this.client.log.warn(
|
||||||
}
|
{ areaTag: exportArea.areaTag },
|
||||||
|
'Cannot QWK export area as it does not exist'
|
||||||
if (!hasMessageConfAndAreaRead(this.client, area)) {
|
);
|
||||||
this.client.log.warn({ areaTag : area.areaTag }, 'Cannot QWK export area due to ACS');
|
return nextExportArea(null);
|
||||||
return nextExportArea(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = {
|
|
||||||
conf,
|
|
||||||
area,
|
|
||||||
step : 'next_area',
|
|
||||||
status : `Gathering in ${conf.name} - ${area.name}...`,
|
|
||||||
};
|
|
||||||
|
|
||||||
progressHandler(progress, err => {
|
|
||||||
if (err) {
|
|
||||||
return nextExportArea(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = {
|
if (!hasMessageConfAndAreaRead(this.client, area)) {
|
||||||
resultType : 'id',
|
this.client.log.warn(
|
||||||
areaTag : exportArea.areaTag,
|
{ areaTag: area.areaTag },
|
||||||
newerThanTimestamp : exportArea.newerThanTimestamp
|
'Cannot QWK export area due to ACS'
|
||||||
|
);
|
||||||
|
return nextExportArea(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = {
|
||||||
|
conf,
|
||||||
|
area,
|
||||||
|
step: 'next_area',
|
||||||
|
status: `Gathering in ${conf.name} - ${area.name}...`,
|
||||||
};
|
};
|
||||||
|
|
||||||
processMessagesWithFilter(filter, err => {
|
progressHandler(progress, err => {
|
||||||
return nextExportArea(err);
|
if (err) {
|
||||||
|
return nextExportArea(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
resultType: 'id',
|
||||||
|
areaTag: exportArea.areaTag,
|
||||||
|
newerThanTimestamp: exportArea.newerThanTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
processMessagesWithFilter(filter, err => {
|
||||||
|
return nextExportArea(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
},
|
err => {
|
||||||
err => {
|
return callback(err, userExportAreas);
|
||||||
return callback(err, userExportAreas);
|
}
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
(userExportAreas, callback) => {
|
(userExportAreas, callback) => {
|
||||||
// Private messages to current user if the user has
|
// Private messages to current user if the user has
|
||||||
// elected to export private messages
|
// elected to export private messages
|
||||||
const privateExportArea = userExportAreas.find(exportArea => exportArea.areaTag === Message.WellKnownAreaTags.Private);
|
const privateExportArea = userExportAreas.find(
|
||||||
|
exportArea =>
|
||||||
|
exportArea.areaTag === Message.WellKnownAreaTags.Private
|
||||||
|
);
|
||||||
if (!privateExportArea) {
|
if (!privateExportArea) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
resultType : 'id',
|
resultType: 'id',
|
||||||
privateTagUserId : this.client.user.userId,
|
privateTagUserId: this.client.user.userId,
|
||||||
newerThanTimestamp : privateExportArea.newerThanTimestamp,
|
newerThanTimestamp: privateExportArea.newerThanTimestamp,
|
||||||
};
|
};
|
||||||
return processMessagesWithFilter(filter, callback);
|
return processMessagesWithFilter(filter, callback);
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
let packetInfo;
|
let packetInfo;
|
||||||
packetWriter.once('packet', info => {
|
packetWriter.once('packet', info => {
|
||||||
packetInfo = info;
|
packetInfo = info;
|
||||||
@@ -370,38 +420,40 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
},
|
},
|
||||||
(sysDownloadPath, packetInfo, callback) => {
|
(sysDownloadPath, packetInfo, callback) => {
|
||||||
const newEntry = new FileEntry({
|
const newEntry = new FileEntry({
|
||||||
areaTag : this.sysTempDownloadArea.areaTag,
|
areaTag: this.sysTempDownloadArea.areaTag,
|
||||||
fileName : paths.basename(sysDownloadPath),
|
fileName: paths.basename(sysDownloadPath),
|
||||||
storageTag : this.sysTempDownloadArea.storageTags[0],
|
storageTag: this.sysTempDownloadArea.storageTags[0],
|
||||||
meta : {
|
meta: {
|
||||||
upload_by_username : this.client.user.username,
|
upload_by_username: this.client.user.username,
|
||||||
upload_by_user_id : this.client.user.userId,
|
upload_by_user_id: this.client.user.userId,
|
||||||
byte_size : packetInfo.stats.size,
|
byte_size: packetInfo.stats.size,
|
||||||
session_temp_dl : 1, // download is valid until session is over
|
session_temp_dl: 1, // download is valid until session is over
|
||||||
|
|
||||||
// :TODO: something like this: allow to override the displayed/downloaded as filename
|
// :TODO: something like this: allow to override the displayed/downloaded as filename
|
||||||
// separate from the actual on disk filename. E.g. we could always download as "ENIGMA.QWK"
|
// separate from the actual on disk filename. E.g. we could always download as "ENIGMA.QWK"
|
||||||
//visible_filename : paths.basename(packetInfo.path),
|
//visible_filename : paths.basename(packetInfo.path),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
newEntry.desc = 'QWK Export';
|
newEntry.desc = 'QWK Export';
|
||||||
|
|
||||||
newEntry.persist(err => {
|
newEntry.persist(err => {
|
||||||
if(!err) {
|
if (!err) {
|
||||||
// queue it!
|
// queue it!
|
||||||
DownloadQueue.get(this.client).addTemporaryDownload(newEntry);
|
DownloadQueue.get(this.client).addTemporaryDownload(newEntry);
|
||||||
}
|
}
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
// update user's export area dates; they can always change/reset them again
|
// update user's export area dates; they can always change/reset them again
|
||||||
const updatedUserExportAreas = this._getUserQWKExportAreas().map(exportArea => {
|
const updatedUserExportAreas = this._getUserQWKExportAreas().map(
|
||||||
return Object.assign(exportArea, {
|
exportArea => {
|
||||||
newerThanTimestamp : getISOTimestampString(),
|
return Object.assign(exportArea, {
|
||||||
});
|
newerThanTimestamp: getISOTimestampString(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return this.client.user.persistProperty(
|
return this.client.user.persistProperty(
|
||||||
UserProperties.ExportAreas,
|
UserProperties.ExportAreas,
|
||||||
@@ -425,4 +477,4 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,36 +2,36 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const {
|
const {
|
||||||
getSortedAvailMessageConferences,
|
getSortedAvailMessageConferences,
|
||||||
getAvailableMessageAreasByConfTag,
|
getAvailableMessageAreasByConfTag,
|
||||||
getSortedAvailMessageAreasByConfTag,
|
getSortedAvailMessageAreasByConfTag,
|
||||||
hasMessageConfAndAreaRead,
|
hasMessageConfAndAreaRead,
|
||||||
filterMessageListByReadACS,
|
filterMessageListByReadACS,
|
||||||
} = require('./message_area.js');
|
} = require('./message_area.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const Message = require('./message.js');
|
const Message = require('./message.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Message Base Search',
|
name: 'Message Base Search',
|
||||||
desc : 'Module for quickly searching the message base',
|
desc: 'Module for quickly searching the message base',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
search : {
|
search: {
|
||||||
searchTerms : 1,
|
searchTerms: 1,
|
||||||
search : 2,
|
search: 2,
|
||||||
conf : 3,
|
conf: 3,
|
||||||
area : 4,
|
area: 4,
|
||||||
to : 5,
|
to: 5,
|
||||||
from : 6,
|
from: 6,
|
||||||
advSearch : 7,
|
advSearch: 7,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class MessageBaseSearch extends MenuModule {
|
exports.getModule = class MessageBaseSearch extends MenuModule {
|
||||||
@@ -39,35 +39,37 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
search : (formData, extraArgs, cb) => {
|
search: (formData, extraArgs, cb) => {
|
||||||
return this.searchNow(formData, cb);
|
return this.searchNow(formData, cb);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prepViewController('search', 0, mciData.menu, (err, vc) => {
|
this.prepViewController('search', 0, mciData.menu, (err, vc) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const confView = vc.getView(MciViewIds.search.conf);
|
const confView = vc.getView(MciViewIds.search.conf);
|
||||||
const areaView = vc.getView(MciViewIds.search.area);
|
const areaView = vc.getView(MciViewIds.search.area);
|
||||||
|
|
||||||
if(!confView || !areaView) {
|
if (!confView || !areaView) {
|
||||||
return cb(Errors.DoesNotExist('Missing one or more required views'));
|
return cb(Errors.DoesNotExist('Missing one or more required views'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const availConfs = [ { text : '-ALL-', data : '' } ].concat(
|
const availConfs = [{ text: '-ALL-', data: '' }].concat(
|
||||||
getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || []
|
getSortedAvailMessageConferences(this.client).map(conf =>
|
||||||
|
Object.assign(conf, { text: conf.conf.name, data: conf.confTag })
|
||||||
|
) || []
|
||||||
);
|
);
|
||||||
|
|
||||||
let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL
|
let availAreas = [{ text: '-ALL-', data: '' }]; // note: will populate if conf changes from ALL
|
||||||
|
|
||||||
confView.setItems(availConfs);
|
confView.setItems(availConfs);
|
||||||
areaView.setItems(availAreas);
|
areaView.setItems(availAreas);
|
||||||
@@ -76,9 +78,14 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
|
|||||||
areaView.setFocusItemIndex(0);
|
areaView.setFocusItemIndex(0);
|
||||||
|
|
||||||
confView.on('index update', idx => {
|
confView.on('index update', idx => {
|
||||||
availAreas = [ { text : '-ALL-', data : '' } ].concat(
|
availAreas = [{ text: '-ALL-', data: '' }].concat(
|
||||||
getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map(
|
getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, {
|
||||||
area => Object.assign(area, { text : area.area.name, data : area.areaTag } )
|
client: this.client,
|
||||||
|
}).map(area =>
|
||||||
|
Object.assign(area, {
|
||||||
|
text: area.area.name,
|
||||||
|
data: area.areaTag,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
areaView.setItems(availAreas);
|
areaView.setItems(availAreas);
|
||||||
@@ -92,38 +99,40 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchNow(formData, cb) {
|
searchNow(formData, cb) {
|
||||||
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
|
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
|
||||||
const value = formData.value;
|
const value = formData.value;
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
resultType : 'messageList',
|
resultType: 'messageList',
|
||||||
sort : 'modTimestamp',
|
sort: 'modTimestamp',
|
||||||
terms : value.searchTerms,
|
terms: value.searchTerms,
|
||||||
//extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ],
|
//extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ],
|
||||||
limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned
|
limit: 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned
|
||||||
};
|
};
|
||||||
|
|
||||||
const returnNoResults = () => {
|
const returnNoResults = () => {
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
|
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
|
||||||
{ menuFlags : [ 'popParent' ] },
|
{ menuFlags: ['popParent'] },
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if(isAdvanced) {
|
if (isAdvanced) {
|
||||||
filter.toUserName = value.toUserName;
|
filter.toUserName = value.toUserName;
|
||||||
filter.fromUserName = value.fromUserName;
|
filter.fromUserName = value.fromUserName;
|
||||||
|
|
||||||
if(value.confTag && !value.areaTag) {
|
if (value.confTag && !value.areaTag) {
|
||||||
// areaTag may be a string or array of strings
|
// areaTag may be a string or array of strings
|
||||||
// getAvailableMessageAreasByConfTag() returns a obj - we only need tags
|
// getAvailableMessageAreasByConfTag() returns a obj - we only need tags
|
||||||
filter.areaTag = _.map(
|
filter.areaTag = _.map(
|
||||||
getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ),
|
getAvailableMessageAreasByConfTag(value.confTag, {
|
||||||
|
client: this.client,
|
||||||
|
}),
|
||||||
(area, areaTag) => areaTag
|
(area, areaTag) => areaTag
|
||||||
);
|
);
|
||||||
} else if(value.areaTag) {
|
} else if (value.areaTag) {
|
||||||
if(hasMessageConfAndAreaRead(this.client, value.areaTag)) {
|
if (hasMessageConfAndAreaRead(this.client, value.areaTag)) {
|
||||||
filter.areaTag = value.areaTag; // specific conf + area
|
filter.areaTag = value.areaTag; // specific conf + area
|
||||||
} else {
|
} else {
|
||||||
return returnNoResults();
|
return returnNoResults();
|
||||||
@@ -132,26 +141,26 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message.findMessages(filter, (err, messageList) => {
|
Message.findMessages(filter, (err, messageList) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't include results without ACS -- if the user searched by
|
// don't include results without ACS -- if the user searched by
|
||||||
// explicit conf/area tag, we should have already filtered (above)
|
// explicit conf/area tag, we should have already filtered (above)
|
||||||
if(!value.confTag && !value.areaTag) {
|
if (!value.confTag && !value.areaTag) {
|
||||||
messageList = filterMessageListByReadACS(this.client, messageList);
|
messageList = filterMessageListByReadACS(this.client, messageList);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(0 === messageList.length) {
|
if (0 === messageList.length) {
|
||||||
return returnNoResults();
|
return returnNoResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuOpts = {
|
const menuOpts = {
|
||||||
extraArgs : {
|
extraArgs: {
|
||||||
messageList,
|
messageList,
|
||||||
noUpdateLastReadId : true
|
noUpdateLastReadId: true,
|
||||||
},
|
},
|
||||||
menuFlags : [ 'popParent' ],
|
menuFlags: ['popParent'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|||||||
@@ -2,31 +2,31 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const mimeTypes = require('mime-types');
|
const mimeTypes = require('mime-types');
|
||||||
|
|
||||||
exports.startup = startup;
|
exports.startup = startup;
|
||||||
exports.resolveMimeType = resolveMimeType;
|
exports.resolveMimeType = resolveMimeType;
|
||||||
|
|
||||||
function startup(cb) {
|
function startup(cb) {
|
||||||
//
|
//
|
||||||
// Add in types (not yet) supported by mime-db -- and therefor, mime-types
|
// Add in types (not yet) supported by mime-db -- and therefor, mime-types
|
||||||
//
|
//
|
||||||
const ADDITIONAL_EXT_MIMETYPES = {
|
const ADDITIONAL_EXT_MIMETYPES = {
|
||||||
ans : 'text/x-ansi',
|
ans: 'text/x-ansi',
|
||||||
gz : 'application/gzip', // not in mime-types 2.1.15 :(
|
gz: 'application/gzip', // not in mime-types 2.1.15 :(
|
||||||
lzx : 'application/x-lzx', // :TODO: submit to mime-types
|
lzx: 'application/x-lzx', // :TODO: submit to mime-types
|
||||||
};
|
};
|
||||||
|
|
||||||
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
|
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
|
||||||
// don't override any entries
|
// don't override any entries
|
||||||
if(!_.isString(mimeTypes.types[ext])) {
|
if (!_.isString(mimeTypes.types[ext])) {
|
||||||
mimeTypes[ext] = mimeType;
|
mimeTypes[ext] = mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!mimeTypes.extensions[mimeType]) {
|
if (!mimeTypes.extensions[mimeType]) {
|
||||||
mimeTypes.extensions[mimeType] = [ ext ];
|
mimeTypes.extensions[mimeType] = [ext];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ function startup(cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveMimeType(query) {
|
function resolveMimeType(query) {
|
||||||
if(mimeTypes.extensions[query]) {
|
if (mimeTypes.extensions[query]) {
|
||||||
return query; // already a mime-type
|
return query; // already a mime-type
|
||||||
}
|
}
|
||||||
|
|
||||||
return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined
|
return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
|
|
||||||
exports.dailyMaintenanceScheduledEvent = dailyMaintenanceScheduledEvent;
|
exports.dailyMaintenanceScheduledEvent = dailyMaintenanceScheduledEvent;
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
|
||||||
exports.isProduction = isProduction;
|
exports.isProduction = isProduction;
|
||||||
exports.isDevelopment = isDevelopment;
|
exports.isDevelopment = isDevelopment;
|
||||||
exports.valueWithDefault = valueWithDefault;
|
exports.valueWithDefault = valueWithDefault;
|
||||||
exports.resolvePath = resolvePath;
|
exports.resolvePath = resolvePath;
|
||||||
exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
|
exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
|
||||||
exports.getEnigmaUserAgent = getEnigmaUserAgent;
|
exports.getEnigmaUserAgent = getEnigmaUserAgent;
|
||||||
exports.valueAsArray = valueAsArray;
|
exports.valueAsArray = valueAsArray;
|
||||||
|
|
||||||
function isProduction() {
|
function isProduction() {
|
||||||
var env = process.env.NODE_ENV || 'dev';
|
var env = process.env.NODE_ENV || 'dev';
|
||||||
@@ -21,17 +21,22 @@ function isProduction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isDevelopment() {
|
function isDevelopment() {
|
||||||
return (!(isProduction()));
|
return !isProduction();
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueWithDefault(val, defVal) {
|
function valueWithDefault(val, defVal) {
|
||||||
return (typeof val !== 'undefined' ? val : defVal);
|
return typeof val !== 'undefined' ? val : defVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePath(path) {
|
function resolvePath(path) {
|
||||||
if(path.substr(0, 2) === '~/') {
|
if (path.substr(0, 2) === '~/') {
|
||||||
var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH;
|
var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH;
|
||||||
path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1);
|
path =
|
||||||
|
(process.env.HOME ||
|
||||||
|
mswCombined ||
|
||||||
|
process.env.HOMEPATH ||
|
||||||
|
process.env.HOMEDIR ||
|
||||||
|
process.cwd()) + path.substr(1);
|
||||||
}
|
}
|
||||||
return paths.resolve(path);
|
return paths.resolve(path);
|
||||||
}
|
}
|
||||||
@@ -39,23 +44,22 @@ function resolvePath(path) {
|
|||||||
function getCleanEnigmaVersion() {
|
function getCleanEnigmaVersion() {
|
||||||
return packageJson.version
|
return packageJson.version
|
||||||
.replace(/-/g, '.')
|
.replace(/-/g, '.')
|
||||||
.replace(/alpha/,'a')
|
.replace(/alpha/, 'a')
|
||||||
.replace(/beta/,'b')
|
.replace(/beta/, 'b');
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// See also ftn_util.js getTearLine() & getProductIdentifier()
|
// See also ftn_util.js getTearLine() & getProductIdentifier()
|
||||||
function getEnigmaUserAgent() {
|
function getEnigmaUserAgent() {
|
||||||
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
|
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
|
||||||
const version = getCleanEnigmaVersion();
|
const version = getCleanEnigmaVersion();
|
||||||
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||||
|
|
||||||
return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueAsArray(value) {
|
function valueAsArray(value) {
|
||||||
if(!value) {
|
if (!value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Array.isArray(value) ? value : [ value ];
|
return Array.isArray(value) ? value : [value];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const messageArea = require('../core/message_area.js');
|
const messageArea = require('../core/message_area.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const { get } = require('lodash');
|
const { get } = require('lodash');
|
||||||
|
|
||||||
exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
|
exports.MessageAreaConfTempSwitcher = Sup =>
|
||||||
|
class extends Sup {
|
||||||
|
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
|
||||||
|
messageAreaTag =
|
||||||
|
messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
|
||||||
|
if (!messageAreaTag) {
|
||||||
|
return; // nothing to do!
|
||||||
|
}
|
||||||
|
|
||||||
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
|
if (recordPrevious) {
|
||||||
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
|
this.prevMessageConfAndArea = {
|
||||||
if(!messageAreaTag) {
|
confTag: this.client.user.properties[UserProps.MessageConfTag],
|
||||||
return; // nothing to do!
|
areaTag: this.client.user.properties[UserProps.MessageAreaTag],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
|
||||||
|
this.client.log.warn(
|
||||||
|
{ messageAreaTag: messageArea },
|
||||||
|
'Failed to perform temporary message area/conf switch'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(recordPrevious) {
|
tempMessageConfAndAreaRestore() {
|
||||||
this.prevMessageConfAndArea = {
|
if (this.prevMessageConfAndArea) {
|
||||||
confTag : this.client.user.properties[UserProps.MessageConfTag],
|
this.client.user.properties[UserProps.MessageConfTag] =
|
||||||
areaTag : this.client.user.properties[UserProps.MessageAreaTag],
|
this.prevMessageConfAndArea.confTag;
|
||||||
};
|
this.client.user.properties[UserProps.MessageAreaTag] =
|
||||||
|
this.prevMessageConfAndArea.areaTag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
|
|
||||||
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tempMessageConfAndAreaRestore() {
|
|
||||||
if(this.prevMessageConfAndArea) {
|
|
||||||
this.client.user.properties[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag;
|
|
||||||
this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,37 +2,41 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const {
|
const { Errors, ErrorReasons } = require('./enig_error.js');
|
||||||
Errors,
|
|
||||||
ErrorReasons
|
|
||||||
} = require('./enig_error.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
|
|
||||||
// exports
|
// exports
|
||||||
exports.loadModuleEx = loadModuleEx;
|
exports.loadModuleEx = loadModuleEx;
|
||||||
exports.loadModule = loadModule;
|
exports.loadModule = loadModule;
|
||||||
exports.loadModulesForCategory = loadModulesForCategory;
|
exports.loadModulesForCategory = loadModulesForCategory;
|
||||||
exports.getModulePaths = getModulePaths;
|
exports.getModulePaths = getModulePaths;
|
||||||
exports.initializeModules = initializeModules;
|
exports.initializeModules = initializeModules;
|
||||||
|
|
||||||
function loadModuleEx(options, cb) {
|
function loadModuleEx(options, cb) {
|
||||||
assert(_.isObject(options));
|
assert(_.isObject(options));
|
||||||
assert(_.isString(options.name));
|
assert(_.isString(options.name));
|
||||||
assert(_.isString(options.path));
|
assert(_.isString(options.path));
|
||||||
|
|
||||||
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
|
const modConfig = _.isObject(Config[options.category])
|
||||||
|
? Config[options.category][options.name]
|
||||||
|
: null;
|
||||||
|
|
||||||
if(_.isObject(modConfig) && false === modConfig.enabled) {
|
if (_.isObject(modConfig) && false === modConfig.enabled) {
|
||||||
return cb(Errors.AccessDenied(`Module "${options.name}" is disabled`, ErrorReasons.Disabled));
|
return cb(
|
||||||
|
Errors.AccessDenied(
|
||||||
|
`Module "${options.name}" is disabled`,
|
||||||
|
ErrorReasons.Disabled
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -41,15 +45,15 @@ function loadModuleEx(options, cb) {
|
|||||||
// to have their own containing folder, package.json & dependencies, etc.
|
// to have their own containing folder, package.json & dependencies, etc.
|
||||||
//
|
//
|
||||||
let mod;
|
let mod;
|
||||||
let modPath = paths.join(options.path, `${options.name}.js`); // general case first
|
let modPath = paths.join(options.path, `${options.name}.js`); // general case first
|
||||||
try {
|
try {
|
||||||
mod = require(modPath);
|
mod = require(modPath);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
if('MODULE_NOT_FOUND' === e.code) {
|
if ('MODULE_NOT_FOUND' === e.code) {
|
||||||
modPath = paths.join(options.path, options.name, `${options.name}.js`);
|
modPath = paths.join(options.path, options.name, `${options.name}.js`);
|
||||||
try {
|
try {
|
||||||
mod = require(modPath);
|
mod = require(modPath);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -57,12 +61,16 @@ function loadModuleEx(options, cb) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_.isObject(mod.moduleInfo)) {
|
if (!_.isObject(mod.moduleInfo)) {
|
||||||
return cb(Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`));
|
return cb(
|
||||||
|
Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_.isFunction(mod.getModule)) {
|
if (!_.isFunction(mod.getModule)) {
|
||||||
return cb(Errors.Invalid(`No exported "getModule" method for module ${modPath}!`));
|
return cb(
|
||||||
|
Errors.Invalid(`No exported "getModule" method for module ${modPath}!`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null, mod);
|
return cb(null, mod);
|
||||||
@@ -71,19 +79,25 @@ function loadModuleEx(options, cb) {
|
|||||||
function loadModule(name, category, cb) {
|
function loadModule(name, category, cb) {
|
||||||
const path = Config().paths[category];
|
const path = Config().paths[category];
|
||||||
|
|
||||||
if(!_.isString(path)) {
|
if (!_.isString(path)) {
|
||||||
return cb(Errors.DoesNotExist(`Not sure where to look for module "${name}" of category "${category}"`));
|
return cb(
|
||||||
|
Errors.DoesNotExist(
|
||||||
|
`Not sure where to look for module "${name}" of category "${category}"`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) {
|
loadModuleEx(
|
||||||
return cb(err, mod);
|
{ name: name, path: path, category: category },
|
||||||
});
|
function loaded(err, mod) {
|
||||||
|
return cb(err, mod);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadModulesForCategory(category, iterator, complete) {
|
function loadModulesForCategory(category, iterator, complete) {
|
||||||
|
|
||||||
fs.readdir(Config().paths[category], (err, files) => {
|
fs.readdir(Config().paths[category], (err, files) => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return iterator(err);
|
return iterator(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,23 +105,27 @@ function loadModulesForCategory(category, iterator, complete) {
|
|||||||
return '.js' === paths.extname(file);
|
return '.js' === paths.extname(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
async.each(jsModules, (file, next) => {
|
async.each(
|
||||||
loadModule(paths.basename(file, '.js'), category, (err, mod) => {
|
jsModules,
|
||||||
if(err) {
|
(file, next) => {
|
||||||
if(ErrorReasons.Disabled === err.reasonCode) {
|
loadModule(paths.basename(file, '.js'), category, (err, mod) => {
|
||||||
Log.debug(err.message);
|
if (err) {
|
||||||
} else {
|
if (ErrorReasons.Disabled === err.reasonCode) {
|
||||||
Log.info( { err : err }, 'Failed loading module');
|
Log.debug(err.message);
|
||||||
|
} else {
|
||||||
|
Log.info({ err: err }, 'Failed loading module');
|
||||||
|
}
|
||||||
|
return next(null); // continue no matter what
|
||||||
}
|
}
|
||||||
return next(null); // continue no matter what
|
return iterator(mod, next);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (complete) {
|
||||||
|
return complete(err);
|
||||||
}
|
}
|
||||||
return iterator(mod, next);
|
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
if(complete) {
|
|
||||||
return complete(err);
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,48 +145,63 @@ function initializeModules(cb) {
|
|||||||
|
|
||||||
const modulePaths = getModulePaths().concat(__dirname);
|
const modulePaths = getModulePaths().concat(__dirname);
|
||||||
|
|
||||||
async.each(modulePaths, (modulePath, nextPath) => {
|
async.each(
|
||||||
glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => {
|
modulePaths,
|
||||||
if(err) {
|
(modulePath, nextPath) => {
|
||||||
return nextPath(err);
|
glob('*{.js,/*.js}', { cwd: modulePath }, (err, files) => {
|
||||||
}
|
if (err) {
|
||||||
|
return nextPath(err);
|
||||||
const ourPath = paths.join(__dirname, __filename);
|
|
||||||
|
|
||||||
async.each(files, (moduleName, nextModule) => {
|
|
||||||
const fullModulePath = paths.join(modulePath, moduleName);
|
|
||||||
if(ourPath === fullModulePath) {
|
|
||||||
return nextModule(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const ourPath = paths.join(__dirname, __filename);
|
||||||
const mod = require(fullModulePath);
|
|
||||||
|
|
||||||
if(_.isFunction(mod.moduleInitialize)) {
|
async.each(
|
||||||
const initInfo = {
|
files,
|
||||||
events : Events,
|
(moduleName, nextModule) => {
|
||||||
};
|
const fullModulePath = paths.join(modulePath, moduleName);
|
||||||
|
if (ourPath === fullModulePath) {
|
||||||
mod.moduleInitialize(initInfo, err => {
|
|
||||||
if(err) {
|
|
||||||
Log.warn( { error : err.message, modulePath : fullModulePath }, 'Error during "moduleInitialize"');
|
|
||||||
}
|
|
||||||
return nextModule(null);
|
return nextModule(null);
|
||||||
});
|
}
|
||||||
} else {
|
|
||||||
return nextModule(null);
|
try {
|
||||||
|
const mod = require(fullModulePath);
|
||||||
|
|
||||||
|
if (_.isFunction(mod.moduleInitialize)) {
|
||||||
|
const initInfo = {
|
||||||
|
events: Events,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod.moduleInitialize(initInfo, err => {
|
||||||
|
if (err) {
|
||||||
|
Log.warn(
|
||||||
|
{
|
||||||
|
error: err.message,
|
||||||
|
modulePath: fullModulePath,
|
||||||
|
},
|
||||||
|
'Error during "moduleInitialize"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nextModule(null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return nextModule(null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.warn(
|
||||||
|
{ error: e.message, fullModulePath },
|
||||||
|
'Exception during "moduleInitialize"'
|
||||||
|
);
|
||||||
|
return nextModule(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return nextPath(err);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
);
|
||||||
Log.warn( { error : e.message, fullModulePath }, 'Exception during "moduleInitialize"');
|
|
||||||
return nextModule(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
return nextPath(err);
|
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
},
|
err => {
|
||||||
err => {
|
return cb(err);
|
||||||
return cb(err);
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
313
core/mrc.js
313
core/mrc.js
@@ -2,27 +2,24 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const {
|
const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js');
|
||||||
pipeToAnsi,
|
const stringFormat = require('./string_format.js');
|
||||||
stripMciColorCodes
|
const StringUtil = require('./string_util.js');
|
||||||
} = require('./color_codes.js');
|
const Config = require('./config.js').get;
|
||||||
const stringFormat = require('./string_format.js');
|
|
||||||
const StringUtil = require('./string_util.js');
|
|
||||||
const Config = require('./config.js').get;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'MRC Client',
|
name: 'MRC Client',
|
||||||
desc : 'Connects to an MRC chat server',
|
desc: 'Connects to an MRC chat server',
|
||||||
author : 'RiPuk',
|
author: 'RiPuk',
|
||||||
packageName : 'codes.l33t.enigma.mrc.client',
|
packageName: 'codes.l33t.enigma.mrc.client',
|
||||||
|
|
||||||
// Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were
|
// Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were
|
||||||
// borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together.
|
// borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together.
|
||||||
@@ -30,24 +27,22 @@ exports.moduleInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FormIds = {
|
const FormIds = {
|
||||||
mrcChat : 0,
|
mrcChat: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
mrcChat : {
|
mrcChat: {
|
||||||
chatLog : 1,
|
chatLog: 1,
|
||||||
inputArea : 2,
|
inputArea: 2,
|
||||||
roomName : 3,
|
roomName: 3,
|
||||||
roomTopic : 4,
|
roomTopic: 4,
|
||||||
mrcUsers : 5,
|
mrcUsers: 5,
|
||||||
mrcBbses : 6,
|
mrcBbses: 6,
|
||||||
|
|
||||||
customRangeStart : 20, // 20+ = customs
|
customRangeStart: 20, // 20+ = customs
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: this is a bit shit, could maybe do it with an ansi instead
|
// TODO: this is a bit shit, could maybe do it with an ansi instead
|
||||||
const helpText = `
|
const helpText = `
|
||||||
|15General Chat|08:
|
|15General Chat|08:
|
||||||
@@ -66,13 +61,14 @@ const helpText = `
|
|||||||
|03/|11rainbow |03<your message> |08- |07Crazy rainbow text
|
|03/|11rainbow |03<your message> |08- |07Crazy rainbow text
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
exports.getModule = class mrcModule extends MenuModule {
|
exports.getModule = class mrcModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.log = Log.child( { module : 'MRC' } );
|
this.log = Log.child({ module: 'MRC' });
|
||||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs });
|
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
|
||||||
|
extraArgs: options.extraArgs,
|
||||||
|
});
|
||||||
|
|
||||||
this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500;
|
this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500;
|
||||||
|
|
||||||
@@ -82,27 +78,27 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
room: '',
|
room: '',
|
||||||
room_topic: '',
|
room_topic: '',
|
||||||
nicks: [],
|
nicks: [],
|
||||||
lastSentMsg : {}, // used for latency est.
|
lastSentMsg: {}, // used for latency est.
|
||||||
};
|
};
|
||||||
|
|
||||||
this.customFormatObj = {
|
this.customFormatObj = {
|
||||||
roomName : '',
|
roomName: '',
|
||||||
roomTopic : '',
|
roomTopic: '',
|
||||||
roomUserCount : 0,
|
roomUserCount: 0,
|
||||||
userCount : 0,
|
userCount: 0,
|
||||||
boardCount : 0,
|
boardCount: 0,
|
||||||
roomCount : 0,
|
roomCount: 0,
|
||||||
latencyMs : 0,
|
latencyMs: 0,
|
||||||
activityLevel : 0,
|
activityLevel: 0,
|
||||||
activityLevelIndicator : ' ',
|
activityLevelIndicator: ' ',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
|
sendChatMessage: (formData, extraArgs, cb) => {
|
||||||
sendChatMessage : (formData, extraArgs, cb) => {
|
const inputAreaView = this.viewControllers.mrcChat.getView(
|
||||||
|
MciViewIds.mrcChat.inputArea
|
||||||
const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea);
|
);
|
||||||
const inputData = inputAreaView.getData();
|
const inputData = inputAreaView.getData();
|
||||||
|
|
||||||
this.processOutgoingMessage(inputData);
|
this.processOutgoingMessage(inputData);
|
||||||
inputAreaView.clearText();
|
inputAreaView.clearText();
|
||||||
@@ -110,13 +106,23 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
movementKeyPressed : (formData, extraArgs, cb) => {
|
movementKeyPressed: (formData, extraArgs, cb) => {
|
||||||
const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog);
|
const bodyView = this.viewControllers.mrcChat.getView(
|
||||||
switch(formData.key.name) {
|
MciViewIds.mrcChat.chatLog
|
||||||
case 'down arrow' : bodyView.scrollDocumentUp(); break;
|
);
|
||||||
case 'up arrow' : bodyView.scrollDocumentDown(); break;
|
switch (formData.key.name) {
|
||||||
case 'page up' : bodyView.keyPressPageUp(); break;
|
case 'down arrow':
|
||||||
case 'page down' : bodyView.keyPressPageDown(); break;
|
bodyView.scrollDocumentUp();
|
||||||
|
break;
|
||||||
|
case 'up arrow':
|
||||||
|
bodyView.scrollDocumentDown();
|
||||||
|
break;
|
||||||
|
case 'page up':
|
||||||
|
bodyView.keyPressPageUp();
|
||||||
|
break;
|
||||||
|
case 'page down':
|
||||||
|
bodyView.keyPressPageDown();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea);
|
this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea);
|
||||||
@@ -124,35 +130,48 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
quit : (formData, extraArgs, cb) => {
|
quit: (formData, extraArgs, cb) => {
|
||||||
return this.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearMessages : (formData, extraArgs, cb) => {
|
clearMessages: (formData, extraArgs, cb) => {
|
||||||
this.clearMessages();
|
this.clearMessages();
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
(callback) => {
|
callback => {
|
||||||
return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback);
|
return this.prepViewController(
|
||||||
|
'mrcChat',
|
||||||
|
FormIds.mrcChat,
|
||||||
|
mciData.menu,
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
return this.validateMCIByViewIds('mrcChat', [ MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea ], callback);
|
return this.validateMCIByViewIds(
|
||||||
|
'mrcChat',
|
||||||
|
[MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea],
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(callback) => {
|
callback => {
|
||||||
const connectOpts = {
|
const connectOpts = {
|
||||||
port : _.get(Config(), 'chatServers.mrc.multiplexerPort', 5000),
|
port: _.get(
|
||||||
host : 'localhost',
|
Config(),
|
||||||
|
'chatServers.mrc.multiplexerPort',
|
||||||
|
5000
|
||||||
|
),
|
||||||
|
host: 'localhost',
|
||||||
};
|
};
|
||||||
|
|
||||||
// connect to multiplexer
|
// connect to multiplexer
|
||||||
@@ -167,18 +186,28 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
this.clientConnect();
|
this.clientConnect();
|
||||||
|
|
||||||
// send register to central MRC and get stats every 60s
|
// send register to central MRC and get stats every 60s
|
||||||
this.heartbeat = setInterval( () => {
|
this.heartbeat = setInterval(() => {
|
||||||
this.sendHeartbeat();
|
this.sendHeartbeat();
|
||||||
this.sendServerMessage('STATS');
|
this.sendServerMessage('STATS');
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
// override idle logout seconds if configured
|
// override idle logout seconds if configured
|
||||||
const idleLogoutSeconds = parseInt(this.config.idleLogoutSeconds);
|
const idleLogoutSeconds = parseInt(
|
||||||
if(0 === idleLogoutSeconds) {
|
this.config.idleLogoutSeconds
|
||||||
this.log.debug('Temporary disable idle monitor due to config');
|
);
|
||||||
|
if (0 === idleLogoutSeconds) {
|
||||||
|
this.log.debug(
|
||||||
|
'Temporary disable idle monitor due to config'
|
||||||
|
);
|
||||||
this.client.stopIdleMonitor();
|
this.client.stopIdleMonitor();
|
||||||
} else if (!isNaN(idleLogoutSeconds) && idleLogoutSeconds >= 60) {
|
} else if (
|
||||||
this.log.debug( { idleLogoutSeconds }, 'Temporary override idle logout seconds due to config');
|
!isNaN(idleLogoutSeconds) &&
|
||||||
|
idleLogoutSeconds >= 60
|
||||||
|
) {
|
||||||
|
this.log.debug(
|
||||||
|
{ idleLogoutSeconds },
|
||||||
|
'Temporary override idle logout seconds due to config'
|
||||||
|
);
|
||||||
this.client.overrideIdleLogoutSeconds(idleLogoutSeconds);
|
this.client.overrideIdleLogoutSeconds(idleLogoutSeconds);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -190,7 +219,10 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.state.socket.once('error', err => {
|
this.state.socket.once('error', err => {
|
||||||
this.log.warn( { error : err.message }, 'MRC multiplexer socket error' );
|
this.log.warn(
|
||||||
|
{ error: err.message },
|
||||||
|
'MRC multiplexer socket error'
|
||||||
|
);
|
||||||
this.state.socket.destroy();
|
this.state.socket.destroy();
|
||||||
delete this.state.socket;
|
delete this.state.socket;
|
||||||
|
|
||||||
@@ -198,8 +230,8 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
return(callback);
|
return callback;
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@@ -222,7 +254,7 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
quitServer() {
|
quitServer() {
|
||||||
clearInterval(this.heartbeat);
|
clearInterval(this.heartbeat);
|
||||||
|
|
||||||
if(this.state.socket) {
|
if (this.state.socket) {
|
||||||
this.sendServerMessage('LOGOFF');
|
this.sendServerMessage('LOGOFF');
|
||||||
this.state.socket.destroy();
|
this.state.socket.destroy();
|
||||||
delete this.state.socket;
|
delete this.state.socket;
|
||||||
@@ -233,12 +265,14 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
* Adds a message to the chat log on screen
|
* Adds a message to the chat log on screen
|
||||||
*/
|
*/
|
||||||
addMessageToChatLog(message) {
|
addMessageToChatLog(message) {
|
||||||
if(!Array.isArray(message)) {
|
if (!Array.isArray(message)) {
|
||||||
message = [ message ];
|
message = [message];
|
||||||
}
|
}
|
||||||
|
|
||||||
message.forEach(msg => {
|
message.forEach(msg => {
|
||||||
const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog);
|
const chatLogView = this.viewControllers.mrcChat.getView(
|
||||||
|
MciViewIds.mrcChat.chatLog
|
||||||
|
);
|
||||||
const messageLength = stripMciColorCodes(msg).length;
|
const messageLength = stripMciColorCodes(msg).length;
|
||||||
const chatWidth = chatLogView.dimens.width;
|
const chatWidth = chatLogView.dimens.width;
|
||||||
let padAmount = 0;
|
let padAmount = 0;
|
||||||
@@ -255,7 +289,7 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
const padding = ' |00' + ' '.repeat(padAmount);
|
const padding = ' |00' + ' '.repeat(padAmount);
|
||||||
chatLogView.addText(pipeToAnsi(msg + padding));
|
chatLogView.addText(pipeToAnsi(msg + padding));
|
||||||
|
|
||||||
if(chatLogView.getLineCount() > this.config.maxScrollbackLines) {
|
if (chatLogView.getLineCount() > this.config.maxScrollbackLines) {
|
||||||
chatLogView.deleteLine(0);
|
chatLogView.deleteLine(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -265,8 +299,7 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
* Processes data received from the MRC multiplexer
|
* Processes data received from the MRC multiplexer
|
||||||
*/
|
*/
|
||||||
processReceivedMessage(blob) {
|
processReceivedMessage(blob) {
|
||||||
blob.split('\n').forEach( message => {
|
blob.split('\n').forEach(message => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(message);
|
message = JSON.parse(message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -285,8 +318,8 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`);
|
this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`);
|
||||||
this.setText(MciViewIds.mrcChat.roomTopic, params[2]);
|
this.setText(MciViewIds.mrcChat.roomTopic, params[2]);
|
||||||
|
|
||||||
this.customFormatObj.roomName = params[1];
|
this.customFormatObj.roomName = params[1];
|
||||||
this.customFormatObj.roomTopic = params[2];
|
this.customFormatObj.roomTopic = params[2];
|
||||||
this.updateCustomViews();
|
this.updateCustomViews();
|
||||||
|
|
||||||
this.state.room = params[1];
|
this.state.room = params[1];
|
||||||
@@ -300,22 +333,19 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'STATS': {
|
case 'STATS': {
|
||||||
const [
|
const [boardCount, roomCount, userCount, activityLevel] =
|
||||||
|
params[1].split(' ').map(v => parseInt(v));
|
||||||
|
|
||||||
|
const activityLevelIndicator =
|
||||||
|
this.getActivityLevelIndicator(activityLevel);
|
||||||
|
|
||||||
|
Object.assign(this.customFormatObj, {
|
||||||
boardCount,
|
boardCount,
|
||||||
roomCount,
|
roomCount,
|
||||||
userCount,
|
userCount,
|
||||||
activityLevel
|
activityLevel,
|
||||||
] = params[1].split(' ').map(v => parseInt(v));
|
activityLevelIndicator,
|
||||||
|
});
|
||||||
const activityLevelIndicator = this.getActivityLevelIndicator(activityLevel);
|
|
||||||
|
|
||||||
Object.assign(
|
|
||||||
this.customFormatObj,
|
|
||||||
{
|
|
||||||
boardCount, roomCount, userCount,
|
|
||||||
activityLevel, activityLevelIndicator
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setText(MciViewIds.mrcChat.mrcUsers, userCount);
|
this.setText(MciViewIds.mrcChat.mrcUsers, userCount);
|
||||||
this.setText(MciViewIds.mrcChat.mrcBbses, boardCount);
|
this.setText(MciViewIds.mrcChat.mrcBbses, boardCount);
|
||||||
@@ -328,18 +358,22 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
this.addMessageToChatLog(message.body);
|
this.addMessageToChatLog(message.body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if(message.body === this.state.lastSentMsg.msg) {
|
if (message.body === this.state.lastSentMsg.msg) {
|
||||||
this.customFormatObj.latencyMs =
|
this.customFormatObj.latencyMs = moment
|
||||||
moment.duration(moment().diff(this.state.lastSentMsg.time)).asMilliseconds();
|
.duration(moment().diff(this.state.lastSentMsg.time))
|
||||||
|
.asMilliseconds();
|
||||||
delete this.state.lastSentMsg.msg;
|
delete this.state.lastSentMsg.msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.to_room == this.state.room) {
|
if (message.to_room == this.state.room) {
|
||||||
// if we're here then we want to show it to the user
|
// if we're here then we want to show it to the user
|
||||||
const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat());
|
const currentTime = moment().format(
|
||||||
this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00');
|
this.client.currentTheme.helpers.getTimeFormat()
|
||||||
|
);
|
||||||
|
this.addMessageToChatLog(
|
||||||
|
'|08' + currentTime + '|00 ' + message.body + '|00'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +383,8 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
|
|
||||||
getActivityLevelIndicator(level) {
|
getActivityLevelIndicator(level) {
|
||||||
let indicators = this.config.activityLevelIndicators;
|
let indicators = this.config.activityLevelIndicators;
|
||||||
if(!Array.isArray(indicators) || indicators.length < level + 1) {
|
if (!Array.isArray(indicators) || indicators.length < level + 1) {
|
||||||
indicators = [ ' ', '░', '▒', '▓' ];
|
indicators = [' ', '░', '▒', '▓'];
|
||||||
}
|
}
|
||||||
return indicators[level];
|
return indicators[level];
|
||||||
}
|
}
|
||||||
@@ -382,9 +416,9 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
|
|
||||||
// else just format and send
|
// else just format and send
|
||||||
const textFormatObj = {
|
const textFormatObj = {
|
||||||
fromUserName : this.state.alias,
|
fromUserName: this.state.alias,
|
||||||
toUserName : to_user,
|
toUserName: to_user,
|
||||||
message : message
|
message: message,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageFormat =
|
const messageFormat =
|
||||||
@@ -406,15 +440,19 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.state.lastSentMsg = {
|
this.state.lastSentMsg = {
|
||||||
msg : formattedMessage,
|
msg: formattedMessage,
|
||||||
time : moment(),
|
time: moment(),
|
||||||
};
|
};
|
||||||
this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage);
|
this.sendMessageToMultiplexer(
|
||||||
} catch(e) {
|
to_user || '',
|
||||||
this.client.log.warn( { error : e.message }, 'MRC error');
|
'',
|
||||||
|
this.state.room,
|
||||||
|
formattedMessage
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.client.log.warn({ error: e.message }, 'MRC error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -432,24 +470,35 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
|
|
||||||
case 'rainbow': {
|
case 'rainbow': {
|
||||||
// this is brutal, but i love it
|
// this is brutal, but i love it
|
||||||
const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) {
|
const line = message
|
||||||
const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0');
|
.replace(/^\/rainbow\s/, '')
|
||||||
a += `|${cc}${c}|00 `;
|
.split(' ')
|
||||||
return a;
|
.reduce(function (a, c) {
|
||||||
}, '').substr(0, 140).replace(/\\s\|\d*$/, '');
|
const cc = Math.floor(Math.random() * 31 + 1)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0');
|
||||||
|
a += `|${cc}${c}|00 `;
|
||||||
|
return a;
|
||||||
|
}, '')
|
||||||
|
.substr(0, 140)
|
||||||
|
.replace(/\\s\|\d*$/, '');
|
||||||
|
|
||||||
this.processOutgoingMessage(line);
|
this.processOutgoingMessage(line);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'l33t':
|
case 'l33t':
|
||||||
this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t'));
|
this.processOutgoingMessage(
|
||||||
|
StringUtil.stylizeString(message.substr(6), 'l33t')
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'kewl': {
|
case 'kewl': {
|
||||||
const text_modes = Array('f','v','V','i','M');
|
const text_modes = Array('f', 'v', 'V', 'i', 'M');
|
||||||
const mode = text_modes[Math.floor(Math.random() * text_modes.length)];
|
const mode = text_modes[Math.floor(Math.random() * text_modes.length)];
|
||||||
this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode));
|
this.processOutgoingMessage(
|
||||||
|
StringUtil.stylizeString(message.substr(6), mode)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +519,9 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'topic':
|
case 'topic':
|
||||||
this.sendServerMessage(`NEWTOPIC:${this.state.room}:${message.substr(7)}`);
|
this.sendServerMessage(
|
||||||
|
`NEWTOPIC:${this.state.room}:${message.substr(7)}`
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'info':
|
case 'info':
|
||||||
@@ -489,7 +540,7 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
this.sendServerMessage('LIST');
|
this.sendServerMessage('LIST');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'quit' :
|
case 'quit':
|
||||||
return this.prevMenu();
|
return this.prevMenu();
|
||||||
|
|
||||||
case 'clear':
|
case 'clear':
|
||||||
@@ -501,7 +552,6 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +561,9 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearMessages() {
|
clearMessages() {
|
||||||
const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog);
|
const chatLogView = this.viewControllers.mrcChat.getView(
|
||||||
|
MciViewIds.mrcChat.chatLog
|
||||||
|
);
|
||||||
chatLogView.setText('');
|
chatLogView.setText('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,17 +571,16 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
* Creates a json object, stringifies it and sends it to the MRC multiplexer
|
* Creates a json object, stringifies it and sends it to the MRC multiplexer
|
||||||
*/
|
*/
|
||||||
sendMessageToMultiplexer(to_user, to_site, to_room, body) {
|
sendMessageToMultiplexer(to_user, to_site, to_room, body) {
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
to_user,
|
to_user,
|
||||||
to_site,
|
to_site,
|
||||||
to_room,
|
to_room,
|
||||||
body,
|
body,
|
||||||
from_user : this.state.alias,
|
from_user: this.state.alias,
|
||||||
from_room : this.state.room,
|
from_room: this.state.room,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(this.state.socket) {
|
if (this.state.socket) {
|
||||||
this.state.socket.write(JSON.stringify(message) + '\n');
|
this.state.socket.write(JSON.stringify(message) + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,7 +621,3 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||||||
this.sendHeartbeat();
|
this.sendHeartbeat();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,27 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const messageArea = require('./message_area.js');
|
const messageArea = require('./message_area.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Message Area List',
|
name: 'Message Area List',
|
||||||
desc : 'Module for listing / choosing message areas',
|
desc: 'Module for listing / choosing message areas',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
// :TODO: Obv/2 others can show # of messages in area
|
// :TODO: Obv/2 others can show # of messages in area
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
areaList : 1,
|
areaList: 1,
|
||||||
areaDesc : 2, // area desc updated @ index update
|
areaDesc: 2, // area desc updated @ index update
|
||||||
customRangeStart : 10, // updated @ index update
|
customRangeStart: 10, // updated @ index update
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class MessageAreaListModule extends MenuModule {
|
exports.getModule = class MessageAreaListModule extends MenuModule {
|
||||||
@@ -32,25 +32,32 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
|
|||||||
this.initList();
|
this.initList();
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
changeArea : (formData, extraArgs, cb) => {
|
changeArea: (formData, extraArgs, cb) => {
|
||||||
if(1 === formData.submitId) {
|
if (1 === formData.submitId) {
|
||||||
const area = this.messageAreas[formData.value.area];
|
const area = this.messageAreas[formData.value.area];
|
||||||
|
|
||||||
messageArea.changeMessageArea(this.client, area.areaTag, err => {
|
messageArea.changeMessageArea(this.client, area.areaTag, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
this.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`);
|
this.client.term.pipeWrite(
|
||||||
|
`\n|00Cannot change area: ${err.message}\n`
|
||||||
|
);
|
||||||
return this.prevMenuOnTimeout(1000, cb);
|
return this.prevMenuOnTimeout(1000, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(area.hasArt) {
|
if (area.hasArt) {
|
||||||
const menuOpts = {
|
const menuOpts = {
|
||||||
extraArgs : {
|
extraArgs: {
|
||||||
areaTag : area.areaTag,
|
areaTag: area.areaTag,
|
||||||
},
|
},
|
||||||
menuFlags : [ 'popParent', 'noHistory' ]
|
menuFlags: ['popParent', 'noHistory'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(this.menuConfig.config.changeAreaPreArtMenu || 'changeMessageAreaPreArt', menuOpts, cb);
|
return this.gotoMenu(
|
||||||
|
this.menuConfig.config.changeAreaPreArtMenu ||
|
||||||
|
'changeMessageAreaPreArt',
|
||||||
|
menuOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
@@ -58,25 +65,31 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
|
|||||||
} else {
|
} else {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
(next) => {
|
next => {
|
||||||
return this.prepViewController('areaList', 0, mciData.menu, next);
|
return this.prepViewController('areaList', 0, mciData.menu, next);
|
||||||
},
|
},
|
||||||
(next) => {
|
next => {
|
||||||
const areaListView = this.viewControllers.areaList.getView(MciViewIds.areaList);
|
const areaListView = this.viewControllers.areaList.getView(
|
||||||
if(!areaListView) {
|
MciViewIds.areaList
|
||||||
return cb(Errors.MissingMci(`Missing area list MCI ${MciViewIds.areaList}`));
|
);
|
||||||
|
if (!areaListView) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingMci(
|
||||||
|
`Missing area list MCI ${MciViewIds.areaList}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
areaListView.on('index update', idx => {
|
areaListView.on('index update', idx => {
|
||||||
@@ -87,11 +100,14 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
|
|||||||
areaListView.redraw();
|
areaListView.redraw();
|
||||||
this.selectionIndexUpdate(0);
|
this.selectionIndexUpdate(0);
|
||||||
return next(null);
|
return next(null);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if (err) {
|
||||||
this.client.log.error( { error : err.message }, 'Failed loading message area list');
|
this.client.log.error(
|
||||||
|
{ error: err.message },
|
||||||
|
'Failed loading message area list'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
@@ -101,27 +117,33 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
|
|||||||
|
|
||||||
selectionIndexUpdate(idx) {
|
selectionIndexUpdate(idx) {
|
||||||
const area = this.messageAreas[idx];
|
const area = this.messageAreas[idx];
|
||||||
if(!area) {
|
if (!area) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setViewText('areaList', MciViewIds.areaDesc, area.desc);
|
this.setViewText('areaList', MciViewIds.areaDesc, area.desc);
|
||||||
this.updateCustomViewTextsWithFilter('areaList', MciViewIds.customRangeStart, area);
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'areaList',
|
||||||
|
MciViewIds.customRangeStart,
|
||||||
|
area
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
initList() {
|
initList() {
|
||||||
let index = 1;
|
let index = 1;
|
||||||
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
|
this.messageAreas = messageArea
|
||||||
this.client.user.properties[UserProps.MessageConfTag],
|
.getSortedAvailMessageAreasByConfTag(
|
||||||
{ client : this.client }
|
this.client.user.properties[UserProps.MessageConfTag],
|
||||||
).map(area => {
|
{ client: this.client }
|
||||||
return {
|
)
|
||||||
index : index++,
|
.map(area => {
|
||||||
areaTag : area.areaTag,
|
return {
|
||||||
name : area.area.name,
|
index: index++,
|
||||||
text : area.area.name, // standard
|
areaTag: area.areaTag,
|
||||||
desc : area.area.desc,
|
name: area.area.name,
|
||||||
hasArt : _.isString(area.area.art),
|
text: area.area.name, // standard
|
||||||
};
|
desc: area.area.desc,
|
||||||
});
|
hasArt: _.isString(area.area.art),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
||||||
const persistMessage = require('./message_area.js').persistMessage;
|
const persistMessage = require('./message_area.js').persistMessage;
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const {
|
const { hasMessageConfAndAreaWrite } = require('./message_area.js');
|
||||||
hasMessageConfAndAreaWrite,
|
|
||||||
} = require('./message_area.js');
|
|
||||||
|
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Message Area Post',
|
name: 'Message Area Post',
|
||||||
desc : 'Module for posting a new message to an area',
|
desc: 'Module for posting a new message to an area',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||||
@@ -25,8 +23,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
|||||||
// we're posting, so always start with 'edit' mode
|
// we're posting, so always start with 'edit' mode
|
||||||
this.editorMode = 'edit';
|
this.editorMode = 'edit';
|
||||||
|
|
||||||
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
|
this.menuMethods.editModeMenuSave = function (formData, extraArgs, cb) {
|
||||||
|
|
||||||
var msg;
|
var msg;
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
@@ -41,10 +38,10 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
|||||||
},
|
},
|
||||||
function updateStats(callback) {
|
function updateStats(callback) {
|
||||||
self.updateUserAndSystemStats(callback);
|
self.updateUserAndSystemStats(callback);
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(err) {
|
if (err) {
|
||||||
// :TODO:... sooooo now what?
|
// :TODO:... sooooo now what?
|
||||||
} else {
|
} else {
|
||||||
// note: not logging 'from' here as it's part of client.log.xxxx()
|
// note: not logging 'from' here as it's part of client.log.xxxx()
|
||||||
@@ -62,14 +59,13 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
|||||||
|
|
||||||
enter() {
|
enter() {
|
||||||
this.messageAreaTag =
|
this.messageAreaTag =
|
||||||
this.messageAreaTag ||
|
this.messageAreaTag || this.client.user.getProperty(UserProps.MessageAreaTag);
|
||||||
this.client.user.getProperty(UserProps.MessageAreaTag);
|
|
||||||
|
|
||||||
super.enter();
|
super.enter();
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
if(!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) {
|
if (!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) {
|
||||||
const noAcsMenu =
|
const noAcsMenu =
|
||||||
this.menuConfig.config.messageBasePostMessageNoAccess ||
|
this.menuConfig.config.messageBasePostMessageNoAccess ||
|
||||||
'messageBasePostMessageNoAccess';
|
'messageBasePostMessageNoAccess';
|
||||||
@@ -82,4 +78,4 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
|||||||
|
|
||||||
super.initSequence();
|
super.initSequence();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
||||||
|
|
||||||
exports.getModule = AreaReplyFSEModule;
|
exports.getModule = AreaReplyFSEModule;
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Message Area Reply',
|
name: 'Message Area Reply',
|
||||||
desc : 'Module for replying to an area message',
|
desc: 'Module for replying to an area message',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
function AreaReplyFSEModule(options) {
|
function AreaReplyFSEModule(options) {
|
||||||
|
|||||||
@@ -2,36 +2,36 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
||||||
const Message = require('./message.js');
|
const Message = require('./message.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Message Area View',
|
name: 'Message Area View',
|
||||||
desc : 'Module for viewing an area message',
|
desc: 'Module for viewing an area message',
|
||||||
author : 'NuSkooler',
|
author: 'NuSkooler',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.editorType = 'area';
|
this.editorType = 'area';
|
||||||
this.editorMode = 'view';
|
this.editorMode = 'view';
|
||||||
|
|
||||||
if(_.isObject(options.extraArgs)) {
|
if (_.isObject(options.extraArgs)) {
|
||||||
this.messageList = options.extraArgs.messageList;
|
this.messageList = options.extraArgs.messageList;
|
||||||
this.messageIndex = options.extraArgs.messageIndex;
|
this.messageIndex = options.extraArgs.messageIndex;
|
||||||
this.lastMessageNextExit = options.extraArgs.lastMessageNextExit;
|
this.lastMessageNextExit = options.extraArgs.lastMessageNextExit;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageList = this.messageList || [];
|
this.messageList = this.messageList || [];
|
||||||
this.messageIndex = this.messageIndex || 0;
|
this.messageIndex = this.messageIndex || 0;
|
||||||
this.messageTotal = this.messageList.length;
|
this.messageTotal = this.messageList.length;
|
||||||
|
|
||||||
if(this.messageList.length > 0) {
|
if (this.messageList.length > 0) {
|
||||||
this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
|
this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,18 +39,21 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
|||||||
|
|
||||||
// assign *additional* menuMethods
|
// assign *additional* menuMethods
|
||||||
Object.assign(this.menuMethods, {
|
Object.assign(this.menuMethods, {
|
||||||
nextMessage : (formData, extraArgs, cb) => {
|
nextMessage: (formData, extraArgs, cb) => {
|
||||||
if(self.messageIndex + 1 < self.messageList.length) {
|
if (self.messageIndex + 1 < self.messageList.length) {
|
||||||
self.messageIndex++;
|
self.messageIndex++;
|
||||||
|
|
||||||
this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
|
this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
|
||||||
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
|
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
|
||||||
|
|
||||||
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
|
return self.loadMessageByUuid(
|
||||||
|
self.messageList[self.messageIndex].messageUuid,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto-exit if no more to go?
|
// auto-exit if no more to go?
|
||||||
if(self.lastMessageNextExit) {
|
if (self.lastMessageNextExit) {
|
||||||
self.lastMessageReached = true;
|
self.lastMessageReached = true;
|
||||||
return self.prevMenu(cb);
|
return self.prevMenu(cb);
|
||||||
}
|
}
|
||||||
@@ -58,28 +61,39 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
prevMessage : (formData, extraArgs, cb) => {
|
prevMessage: (formData, extraArgs, cb) => {
|
||||||
if(self.messageIndex > 0) {
|
if (self.messageIndex > 0) {
|
||||||
self.messageIndex--;
|
self.messageIndex--;
|
||||||
|
|
||||||
this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
|
this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
|
||||||
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
|
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
|
||||||
|
|
||||||
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
|
return self.loadMessageByUuid(
|
||||||
|
self.messageList[self.messageIndex].messageUuid,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
movementKeyPressed : (formData, extraArgs, cb) => {
|
movementKeyPressed: (formData, extraArgs, cb) => {
|
||||||
const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic #
|
const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic #
|
||||||
|
|
||||||
// :TODO: Create methods for up/down vs using keyPressXXXXX
|
// :TODO: Create methods for up/down vs using keyPressXXXXX
|
||||||
switch(formData.key.name) {
|
switch (formData.key.name) {
|
||||||
case 'down arrow' : bodyView.scrollDocumentUp(); break;
|
case 'down arrow':
|
||||||
case 'up arrow' : bodyView.scrollDocumentDown(); break;
|
bodyView.scrollDocumentUp();
|
||||||
case 'page up' : bodyView.keyPressPageUp(); break;
|
break;
|
||||||
case 'page down' : bodyView.keyPressPageDown(); break;
|
case 'up arrow':
|
||||||
|
bodyView.scrollDocumentDown();
|
||||||
|
break;
|
||||||
|
case 'page up':
|
||||||
|
bodyView.keyPressPageUp();
|
||||||
|
break;
|
||||||
|
case 'page down':
|
||||||
|
bodyView.keyPressPageDown();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: need to stop down/page down if doing so would push the last
|
// :TODO: need to stop down/page down if doing so would push the last
|
||||||
@@ -88,13 +102,13 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
|||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
replyMessage : (formData, extraArgs, cb) => {
|
replyMessage: (formData, extraArgs, cb) => {
|
||||||
if(_.isString(extraArgs.menu)) {
|
if (_.isString(extraArgs.menu)) {
|
||||||
const modOpts = {
|
const modOpts = {
|
||||||
extraArgs : {
|
extraArgs: {
|
||||||
messageAreaTag : self.messageAreaTag,
|
messageAreaTag: self.messageAreaTag,
|
||||||
replyToMessage : self.message,
|
replyToMessage: self.message,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return self.gotoMenu(extraArgs.menu, modOpts, cb);
|
return self.gotoMenu(extraArgs.menu, modOpts, cb);
|
||||||
@@ -108,10 +122,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
|||||||
|
|
||||||
loadMessageByUuid(uuid, cb) {
|
loadMessageByUuid(uuid, cb) {
|
||||||
const msg = new Message();
|
const msg = new Message();
|
||||||
msg.load( { uuid : uuid, user : this.client.user }, () => {
|
msg.load({ uuid: uuid, user: this.client.user }, () => {
|
||||||
this.setMessage(msg);
|
this.setMessage(msg);
|
||||||
|
|
||||||
if(cb) {
|
if (cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -123,22 +137,22 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
|||||||
|
|
||||||
getSaveState() {
|
getSaveState() {
|
||||||
return {
|
return {
|
||||||
messageList : this.messageList,
|
messageList: this.messageList,
|
||||||
messageIndex : this.messageIndex,
|
messageIndex: this.messageIndex,
|
||||||
messageTotal : this.messageList.length,
|
messageTotal: this.messageList.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreSavedState(savedState) {
|
restoreSavedState(savedState) {
|
||||||
this.messageList = savedState.messageList;
|
this.messageList = savedState.messageList;
|
||||||
this.messageIndex = savedState.messageIndex;
|
this.messageIndex = savedState.messageIndex;
|
||||||
this.messageTotal = savedState.messageTotal;
|
this.messageTotal = savedState.messageTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMenuResult() {
|
getMenuResult() {
|
||||||
return {
|
return {
|
||||||
messageIndex : this.messageIndex,
|
messageIndex: this.messageIndex,
|
||||||
lastMessageReached : this.lastMessageReached,
|
lastMessageReached: this.lastMessageReached,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user