mirror of
https://github.com/NuSkooler/enigma-bbs.git
synced 2025-08-05 01:11:36 +02:00
commit
4e1bbe419b
136 changed files with 211 additions and 213 deletions
197
core/abracadabra.js
Normal file
197
core/abracadabra.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const DropFile = require('./dropfile.js').DropFile;
|
||||
const door = require('./door.js');
|
||||
const theme = require('./theme.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const paths = require('path');
|
||||
const _ = require('lodash');
|
||||
const mkdirs = require('fs-extra').mkdirs;
|
||||
|
||||
// :TODO: This should really be a system module... needs a little work to allow for such
|
||||
|
||||
const activeDoorNodeInstances = {};
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Abracadabra',
|
||||
desc : 'External BBS Door Module',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
/*
|
||||
Example configuration for LORD under DOSEMU:
|
||||
|
||||
{
|
||||
config: {
|
||||
name: PimpWars
|
||||
dropFileType: DORINFO
|
||||
cmd: qemu-system-i386
|
||||
args: [
|
||||
"-localtime",
|
||||
"freedos.img",
|
||||
"-chardev",
|
||||
"socket,port={srvPort},nowait,host=localhost,id=s0",
|
||||
"-device",
|
||||
"isa-serial,chardev=s0"
|
||||
]
|
||||
io: socket
|
||||
}
|
||||
}
|
||||
|
||||
listen: socket | stdio
|
||||
|
||||
{
|
||||
"config" : {
|
||||
"name" : "LORD",
|
||||
"dropFileType" : "DOOR",
|
||||
"cmd" : "/usr/bin/dosemu",
|
||||
"args" : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ],
|
||||
"nodeMax" : 32,
|
||||
"tooManyArt" : "toomany-lord.ans"
|
||||
}
|
||||
}
|
||||
|
||||
:TODO: See Mystic & others for other arg options that we may need to support
|
||||
*/
|
||||
|
||||
exports.getModule = class AbracadabraModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = options.menuConfig.config;
|
||||
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
||||
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
|
||||
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
||||
|
||||
this.config.nodeMax = this.config.nodeMax || 0;
|
||||
this.config.args = this.config.args || [];
|
||||
}
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
* disconnecting wile door is open leaves dosemu
|
||||
* http://bbslink.net/sysop.php support
|
||||
* Font support ala all other menus... or does this just work?
|
||||
*/
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function validateNodeCount(callback) {
|
||||
if(self.config.nodeMax > 0 &&
|
||||
_.isNumber(activeDoorNodeInstances[self.config.name]) &&
|
||||
activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax)
|
||||
{
|
||||
self.client.log.info(
|
||||
{
|
||||
name : self.config.name,
|
||||
activeCount : activeDoorNodeInstances[self.config.name]
|
||||
},
|
||||
'Too many active instances');
|
||||
|
||||
if(_.isString(self.config.tooManyArt)) {
|
||||
theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() {
|
||||
self.pausePrompt( () => {
|
||||
callback(new Error('Too many active instances'));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.client.term.write('\nToo many active instances. Try again later.\n');
|
||||
|
||||
// :TODO: Use MenuModule.pausePrompt()
|
||||
self.pausePrompt( () => {
|
||||
callback(new Error('Too many active instances'));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// :TODO: JS elegant way to do this?
|
||||
if(activeDoorNodeInstances[self.config.name]) {
|
||||
activeDoorNodeInstances[self.config.name] += 1;
|
||||
} else {
|
||||
activeDoorNodeInstances[self.config.name] = 1;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
function generateDropfile(callback) {
|
||||
self.dropFile = new DropFile(self.client, self.config.dropFileType);
|
||||
var fullPath = self.dropFile.fullPath;
|
||||
|
||||
mkdirs(paths.dirname(fullPath), function dirCreated(err) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
self.dropFile.createFile(function created(err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.toString() }, 'Could not start door');
|
||||
self.lastError = err;
|
||||
self.prevMenu();
|
||||
} else {
|
||||
self.finishedLoading();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
runDoor() {
|
||||
|
||||
const exeInfo = {
|
||||
cmd : this.config.cmd,
|
||||
args : this.config.args,
|
||||
io : this.config.io || 'stdio',
|
||||
encoding : this.config.encoding || this.client.term.outputEncoding,
|
||||
dropFile : this.dropFile.fileName,
|
||||
node : this.client.node,
|
||||
//inhSocket : this.client.output._handle.fd,
|
||||
};
|
||||
|
||||
const doorInstance = new door.Door(this.client, exeInfo);
|
||||
|
||||
doorInstance.once('finished', () => {
|
||||
//
|
||||
// Try to clean up various settings such as scroll regions that may
|
||||
// have been set within the door
|
||||
//
|
||||
this.client.term.rawWrite(
|
||||
ansi.normal() +
|
||||
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
|
||||
ansi.setScrollRegion() +
|
||||
ansi.goto(this.client.term.termHeight, 0) +
|
||||
'\r\n\r\n'
|
||||
);
|
||||
|
||||
this.prevMenu();
|
||||
});
|
||||
|
||||
this.client.term.write(ansi.resetScreen());
|
||||
|
||||
doorInstance.run();
|
||||
}
|
||||
|
||||
leave() {
|
||||
super.leave();
|
||||
if(!this.lastError) {
|
||||
activeDoorNodeInstances[this.config.name] -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
finishedLoading() {
|
||||
this.runDoor();
|
||||
}
|
||||
};
|
|
@ -14,7 +14,7 @@ const paths = require('path');
|
|||
const assert = require('assert');
|
||||
const iconv = require('iconv-lite');
|
||||
const _ = require('lodash');
|
||||
const farmhash = require('farmhash');
|
||||
const xxhash = require('xxhash');
|
||||
|
||||
exports.getArt = getArt;
|
||||
exports.getArtFromPath = getArtFromPath;
|
||||
|
@ -288,7 +288,7 @@ function display(client, art, options, cb) {
|
|||
}
|
||||
|
||||
if(!options.disableMciCache) {
|
||||
artHash = farmhash.hash32(art);
|
||||
artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE);
|
||||
|
||||
// see if we have a mciMap cached for this art
|
||||
if(client.mciCache) {
|
||||
|
|
|
@ -29,11 +29,12 @@ const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby';
|
|||
const HELP =
|
||||
`${ENIGMA_COPYRIGHT}
|
||||
usage: main.js <args>
|
||||
eg : main.js --config /enigma_install_path/config/
|
||||
|
||||
valid args:
|
||||
--version : display version
|
||||
--help : displays this help
|
||||
--config PATH : override default config.hjson path
|
||||
--config PATH : override default config path
|
||||
`;
|
||||
|
||||
function printHelpAndExit() {
|
||||
|
@ -56,7 +57,8 @@ function main() {
|
|||
return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath));
|
||||
},
|
||||
function initConfig(configPath, configPathSupplied, callback) {
|
||||
conf.init(resolvePath(configPath), function configInit(err) {
|
||||
const configFile = configPath + 'config.hjson';
|
||||
conf.init(resolvePath(configFile), function configInit(err) {
|
||||
|
||||
//
|
||||
// If the user supplied a path and we can't read/parse it
|
||||
|
@ -65,7 +67,7 @@ function main() {
|
|||
if(err) {
|
||||
if('ENOENT' === err.code) {
|
||||
if(configPathSupplied) {
|
||||
console.error('Configuration file does not exist: ' + configPath);
|
||||
console.error('Configuration file does not exist: ' + configFile);
|
||||
} else {
|
||||
configPathSupplied = null; // make non-fatal; we'll go with defaults
|
||||
}
|
||||
|
|
207
core/bbs_link.js
Normal file
207
core/bbs_link.js
Normal file
|
@ -0,0 +1,207 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const resetScreen = require('./ansi_term.js').resetScreen;
|
||||
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
/*
|
||||
Expected configuration block:
|
||||
|
||||
{
|
||||
module: bbs_link
|
||||
...
|
||||
config: {
|
||||
sysCode: XXXXX
|
||||
authCode: XXXXX
|
||||
schemeCode: XXXX
|
||||
door: lord
|
||||
|
||||
// default hoss: games.bbslink.net
|
||||
host: games.bbslink.net
|
||||
|
||||
// defualt port: 23
|
||||
port: 23
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors
|
||||
// :TODO: ENH: Support nodeMax and tooManyArt
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'BBSLink',
|
||||
desc : 'BBSLink Access Module',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = class BBSLinkModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = options.menuConfig.config;
|
||||
this.config.host = this.config.host || 'games.bbslink.net';
|
||||
this.config.port = this.config.port || 23;
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
let token;
|
||||
let randomKey;
|
||||
let clientTerminated;
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(_.isString(self.config.sysCode) &&
|
||||
_.isString(self.config.authCode) &&
|
||||
_.isString(self.config.schemeCode) &&
|
||||
_.isString(self.config.door))
|
||||
{
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error('Configuration is missing option(s)'));
|
||||
}
|
||||
},
|
||||
function acquireToken(callback) {
|
||||
//
|
||||
// Acquire an authentication token
|
||||
//
|
||||
crypto.randomBytes(16, function rand(ex, buf) {
|
||||
if(ex) {
|
||||
callback(ex);
|
||||
} else {
|
||||
randomKey = buf.toString('base64').substr(0, 6);
|
||||
self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
token = body.trim();
|
||||
self.client.log.trace( { token : token }, 'BBSLink token');
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
function authenticateToken(callback) {
|
||||
//
|
||||
// Authenticate the token we acquired previously
|
||||
//
|
||||
var headers = {
|
||||
'X-User' : self.client.user.userId.toString(),
|
||||
'X-System' : self.config.sysCode,
|
||||
'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'),
|
||||
'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'),
|
||||
'X-Rows' : self.client.term.termHeight.toString(),
|
||||
'X-Key' : randomKey,
|
||||
'X-Door' : self.config.door,
|
||||
'X-Token' : token,
|
||||
'X-Type' : 'enigma-bbs',
|
||||
'X-Version' : packageJson.version,
|
||||
};
|
||||
|
||||
self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) {
|
||||
var status = body.trim();
|
||||
|
||||
if('complete' === status) {
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error('Bad authentication status: ' + status));
|
||||
}
|
||||
});
|
||||
},
|
||||
function createTelnetBridge(callback) {
|
||||
//
|
||||
// Authentication with BBSLink successful. Now, we need to create a telnet
|
||||
// bridge from us to them
|
||||
//
|
||||
var connectOpts = {
|
||||
port : self.config.port,
|
||||
host : self.config.host,
|
||||
};
|
||||
|
||||
var clientTerminated;
|
||||
|
||||
self.client.term.write(resetScreen());
|
||||
self.client.term.write(' Connecting to BBSLink.net, please wait...\n');
|
||||
|
||||
var bridgeConnection = net.createConnection(connectOpts, function connected() {
|
||||
self.client.log.info(connectOpts, 'BBSLink bridge connection established');
|
||||
|
||||
self.client.term.output.pipe(bridgeConnection);
|
||||
|
||||
self.client.once('end', function clientEnd() {
|
||||
self.client.log.info('Connection ended. Terminating BBSLink connection');
|
||||
clientTerminated = true;
|
||||
bridgeConnection.end();
|
||||
});
|
||||
});
|
||||
|
||||
var restorePipe = function() {
|
||||
self.client.term.output.unpipe(bridgeConnection);
|
||||
self.client.term.output.resume();
|
||||
};
|
||||
|
||||
bridgeConnection.on('data', function incomingData(data) {
|
||||
// pass along
|
||||
// :TODO: just pipe this as well
|
||||
self.client.term.rawWrite(data);
|
||||
});
|
||||
|
||||
bridgeConnection.on('end', function connectionEnd() {
|
||||
restorePipe();
|
||||
callback(clientTerminated ? new Error('Client connection terminated') : null);
|
||||
});
|
||||
|
||||
bridgeConnection.on('error', function error(err) {
|
||||
self.client.log.info('BBSLink bridge connection error: ' + err.message);
|
||||
restorePipe();
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.toString() }, 'BBSLink connection error');
|
||||
}
|
||||
|
||||
if(!clientTerminated) {
|
||||
self.prevMenu();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
simpleHttpRequest(path, headers, cb) {
|
||||
const getOpts = {
|
||||
host : this.config.host,
|
||||
path : path,
|
||||
headers : headers,
|
||||
};
|
||||
|
||||
const req = http.get(getOpts, function response(resp) {
|
||||
let data = '';
|
||||
|
||||
resp.on('data', function chunk(c) {
|
||||
data += c;
|
||||
});
|
||||
|
||||
resp.on('end', function respEnd() {
|
||||
cb(null, data);
|
||||
req.end();
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', function reqErr(err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
};
|
435
core/bbs_list.js
Normal file
435
core/bbs_list.js
Normal file
|
@ -0,0 +1,435 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
|
||||
const {
|
||||
getModDatabasePath,
|
||||
getTransactionDatabase
|
||||
} = require('./database.js');
|
||||
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const ansi = require('./ansi_term.js');
|
||||
const theme = require('./theme.js');
|
||||
const User = require('./user.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const sqlite3 = require('sqlite3');
|
||||
const _ = require('lodash');
|
||||
|
||||
// :TODO: add notes field
|
||||
|
||||
const moduleInfo = exports.moduleInfo = {
|
||||
name : 'BBS List',
|
||||
desc : 'List of other BBSes',
|
||||
author : 'Andrew Pamment',
|
||||
packageName : 'com.magickabbs.enigma.bbslist'
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
view : {
|
||||
BBSList : 1,
|
||||
SelectedBBSName : 2,
|
||||
SelectedBBSSysOp : 3,
|
||||
SelectedBBSTelnet : 4,
|
||||
SelectedBBSWww : 5,
|
||||
SelectedBBSLoc : 6,
|
||||
SelectedBBSSoftware : 7,
|
||||
SelectedBBSNotes : 8,
|
||||
SelectedBBSSubmitter : 9,
|
||||
},
|
||||
add : {
|
||||
BBSName : 1,
|
||||
Sysop : 2,
|
||||
Telnet : 3,
|
||||
Www : 4,
|
||||
Location : 5,
|
||||
Software : 6,
|
||||
Notes : 7,
|
||||
Error : 8,
|
||||
}
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
View : 0,
|
||||
Add : 1,
|
||||
};
|
||||
|
||||
const SELECTED_MCI_NAME_TO_ENTRY = {
|
||||
SelectedBBSName : 'bbsName',
|
||||
SelectedBBSSysOp : 'sysOp',
|
||||
SelectedBBSTelnet : 'telnet',
|
||||
SelectedBBSWww : 'www',
|
||||
SelectedBBSLoc : 'location',
|
||||
SelectedBBSSoftware : 'software',
|
||||
SelectedBBSSubmitter : 'submitter',
|
||||
SelectedBBSSubmitterId : 'submitterUserId',
|
||||
SelectedBBSNotes : 'notes',
|
||||
};
|
||||
|
||||
exports.getModule = class BBSListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
this.menuMethods = {
|
||||
//
|
||||
// Validators
|
||||
//
|
||||
viewValidationListener : function(err, cb) {
|
||||
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
|
||||
if(errMsgView) {
|
||||
if(err) {
|
||||
errMsgView.setText(err.message);
|
||||
} else {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
//
|
||||
// Key & submit handlers
|
||||
//
|
||||
addBBS : function(formData, extraArgs, cb) {
|
||||
self.displayAddScreen(cb);
|
||||
},
|
||||
deleteBBS : function(formData, extraArgs, cb) {
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
||||
|
||||
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
|
||||
// must be owner or +op
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
const entry = self.entries[self.selectedBBS];
|
||||
if(!entry) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
self.database.run(
|
||||
`DELETE FROM bbs_list
|
||||
WHERE id=?;`,
|
||||
[ entry.id ],
|
||||
err => {
|
||||
if (err) {
|
||||
self.client.log.error( { err : err }, 'Error deleting from BBS list');
|
||||
} else {
|
||||
self.entries.splice(self.selectedBBS, 1);
|
||||
|
||||
self.setEntries(entriesView);
|
||||
|
||||
if(self.entries.length > 0) {
|
||||
entriesView.focusPrevious();
|
||||
}
|
||||
|
||||
self.viewControllers.view.redrawAll();
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
},
|
||||
submitBBS : function(formData, extraArgs, cb) {
|
||||
|
||||
let ok = true;
|
||||
[ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => {
|
||||
if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) {
|
||||
ok = false;
|
||||
}
|
||||
});
|
||||
if(!ok) {
|
||||
// validators should prevent this!
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
self.database.run(
|
||||
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||
[ formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes ],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.error( { err : err }, 'Error adding to BBS list');
|
||||
}
|
||||
|
||||
self.clearAddForm();
|
||||
self.displayBBSList(true, cb);
|
||||
}
|
||||
);
|
||||
},
|
||||
cancelSubmit : function(formData, extraArgs, cb) {
|
||||
self.clearAddForm();
|
||||
self.displayBBSList(true, cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
async.series(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
self.displayBBSList(false, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
// :TODO: Handle me -- initSequence() should really take a completion callback
|
||||
}
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
drawSelectedEntry(entry) {
|
||||
if(!entry) {
|
||||
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
||||
this.setViewText('view', MciViewIds.view[mciName], '');
|
||||
});
|
||||
} else {
|
||||
const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
|
||||
|
||||
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
||||
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
|
||||
if(MciViewIds.view[mciName]) {
|
||||
|
||||
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) {
|
||||
this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
|
||||
} else {
|
||||
this.setViewText('view',MciViewIds.view[mciName], t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setEntries(entriesView) {
|
||||
const config = this.menuConfig.config;
|
||||
const listFormat = config.listFormat || '{bbsName}';
|
||||
const focusListFormat = config.focusListFormat || '{bbsName}';
|
||||
|
||||
entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) );
|
||||
entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) );
|
||||
}
|
||||
|
||||
displayBBSList(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
if(self.viewControllers.add) {
|
||||
self.viewControllers.add.setFocus(false);
|
||||
}
|
||||
if (clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
theme.displayThemedAsset(
|
||||
self.menuConfig.config.art.entries,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'view',
|
||||
new ViewController( { client : self.client, formId : FormIds.View } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.View,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.view.setFocus(true);
|
||||
self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw();
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function fetchEntries(callback) {
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
||||
self.entries = [];
|
||||
|
||||
self.database.each(
|
||||
`SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes
|
||||
FROM bbs_list;`,
|
||||
(err, row) => {
|
||||
if (!err) {
|
||||
self.entries.push({
|
||||
id : row.id,
|
||||
bbsName : row.bbs_name,
|
||||
sysOp : row.sysop,
|
||||
telnet : row.telnet,
|
||||
www : row.www,
|
||||
location : row.location,
|
||||
software : row.software,
|
||||
submitterUserId : row.submitter_user_id,
|
||||
notes : row.notes,
|
||||
});
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return callback(err, entriesView);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getUserNames(entriesView, callback) {
|
||||
async.each(self.entries, (entry, next) => {
|
||||
User.getUserName(entry.submitterUserId, (err, username) => {
|
||||
if(username) {
|
||||
entry.submitter = username;
|
||||
} else {
|
||||
entry.submitter = 'N/A';
|
||||
}
|
||||
return next();
|
||||
});
|
||||
}, () => {
|
||||
return callback(null, entriesView);
|
||||
});
|
||||
},
|
||||
function populateEntries(entriesView, callback) {
|
||||
self.setEntries(entriesView);
|
||||
|
||||
entriesView.on('index update', idx => {
|
||||
const entry = self.entries[idx];
|
||||
|
||||
self.drawSelectedEntry(entry);
|
||||
|
||||
if(!entry) {
|
||||
self.selectedBBS = -1;
|
||||
} else {
|
||||
self.selectedBBS = idx;
|
||||
}
|
||||
});
|
||||
|
||||
if (self.selectedBBS >= 0) {
|
||||
entriesView.setFocusItemIndex(self.selectedBBS);
|
||||
self.drawSelectedEntry(self.entries[self.selectedBBS]);
|
||||
} else if (self.entries.length > 0) {
|
||||
entriesView.setFocusItemIndex(0);
|
||||
self.drawSelectedEntry(self.entries[0]);
|
||||
}
|
||||
|
||||
entriesView.redraw();
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayAddScreen(cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
self.viewControllers.view.setFocus(false);
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.menuConfig.config.art.add,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'add',
|
||||
new ViewController( { client : self.client, formId : FormIds.Add } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.Add,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.add.setFocus(true);
|
||||
self.viewControllers.add.redrawAll();
|
||||
self.viewControllers.add.switchFocus(MciViewIds.add.BBSName);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
clearAddForm() {
|
||||
[ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => {
|
||||
this.setViewText('add', MciViewIds.add[mciName], '');
|
||||
});
|
||||
}
|
||||
|
||||
initDatabase(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function openDatabase(callback) {
|
||||
self.database = getTransactionDatabase(new sqlite3.Database(
|
||||
getModDatabasePath(moduleInfo),
|
||||
callback
|
||||
));
|
||||
},
|
||||
function createTables(callback) {
|
||||
self.database.serialize( () => {
|
||||
self.database.run(
|
||||
`CREATE TABLE IF NOT EXISTS bbs_list (
|
||||
id INTEGER PRIMARY KEY,
|
||||
bbs_name VARCHAR NOT NULL,
|
||||
sysop VARCHAR NOT NULL,
|
||||
telnet VARCHAR NOT NULL,
|
||||
www VARCHAR,
|
||||
location VARCHAR,
|
||||
software VARCHAR,
|
||||
submitter_user_id INTEGER NOT NULL,
|
||||
notes VARCHAR
|
||||
);`
|
||||
);
|
||||
});
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
beforeArt(cb) {
|
||||
super.beforeArt(err => {
|
||||
return err ? cb(err) : this.initDatabase(cb);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -111,11 +111,8 @@ function init(configPath, options, cb) {
|
|||
}
|
||||
|
||||
function getDefaultPath() {
|
||||
const base = miscUtil.resolvePath('~/');
|
||||
if(base) {
|
||||
// e.g. /home/users/joeuser/.config/enigma-bbs/config.hjson
|
||||
return paths.join(base, '.config', 'enigma-bbs', 'config.hjson');
|
||||
}
|
||||
// e.g. /enigma-bbs-install-path/config/
|
||||
return './config/';
|
||||
}
|
||||
|
||||
function getDefaultConfig() {
|
||||
|
@ -127,8 +124,8 @@ function getDefaultConfig() {
|
|||
|
||||
loginAttempts : 3,
|
||||
|
||||
menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./mods)
|
||||
promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./mods)
|
||||
menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config)
|
||||
promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config)
|
||||
},
|
||||
|
||||
// :TODO: see notes below about 'theme' section - move this!
|
||||
|
@ -193,6 +190,7 @@ function getDefaultConfig() {
|
|||
},
|
||||
|
||||
paths : {
|
||||
config : paths.join(__dirname, './../config/'),
|
||||
mods : paths.join(__dirname, './../mods/'),
|
||||
loginServers : paths.join(__dirname, './servers/login/'),
|
||||
contentServers : paths.join(__dirname, './servers/content/'),
|
||||
|
@ -200,8 +198,8 @@ function getDefaultConfig() {
|
|||
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
|
||||
mailers : paths.join(__dirname, './mailers/') ,
|
||||
|
||||
art : paths.join(__dirname, './../mods/art/'),
|
||||
themes : paths.join(__dirname, './../mods/themes/'),
|
||||
art : paths.join(__dirname, './../art/general/'),
|
||||
themes : paths.join(__dirname, './../art/themes/'),
|
||||
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
||||
db : paths.join(__dirname, './../db/'),
|
||||
modsDb : paths.join(__dirname, './../db/mods/'),
|
||||
|
@ -217,18 +215,18 @@ function getDefaultConfig() {
|
|||
},
|
||||
ssh : {
|
||||
port : 8889,
|
||||
enabled : false, // defualt to false as PK/pass in config.hjson are required
|
||||
enabled : false, // default to false as PK/pass in config.hjson are required
|
||||
|
||||
//
|
||||
// Private key in PEM format
|
||||
//
|
||||
// Generating your PK:
|
||||
// > openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
|
||||
// > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048
|
||||
//
|
||||
// Then, set servers.ssh.privateKeyPass to the password you use above
|
||||
// in your config.hjson
|
||||
//
|
||||
privateKeyPem : paths.join(__dirname, './../misc/ssh_private_key.pem'),
|
||||
privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'),
|
||||
firstMenu : 'sshConnected',
|
||||
firstMenuNewUser : 'sshConnectedNewUser',
|
||||
},
|
||||
|
@ -236,8 +234,8 @@ function getDefaultConfig() {
|
|||
port : 8810, // ws://
|
||||
enabled : false,
|
||||
securePort : 8811, // wss:// - must provide certPem and keyPem
|
||||
certPem : paths.join(__dirname, './../misc/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'),
|
||||
certPem : paths.join(__dirname, './../config/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -273,8 +271,8 @@ function getDefaultConfig() {
|
|||
https : {
|
||||
enabled : false,
|
||||
port : 8443,
|
||||
certPem : paths.join(__dirname, './../misc/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'),
|
||||
certPem : paths.join(__dirname, './../config/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var configCache = require('./config_cache.js');
|
||||
|
||||
var paths = require('path');
|
||||
const Config = require('./config.js').config;
|
||||
const configCache = require('./config_cache.js');
|
||||
const paths = require('path');
|
||||
|
||||
exports.getFullConfig = getFullConfig;
|
||||
|
||||
function getFullConfig(filePath, cb) {
|
||||
// |filePath| is assumed to be in 'mods' if it's only a file name
|
||||
// |filePath| is assumed to be in the config path if it's only a file name
|
||||
if('.' === paths.dirname(filePath)) {
|
||||
filePath = paths.join(__dirname, '../mods', filePath);
|
||||
filePath = paths.join(Config.paths.config, filePath);
|
||||
}
|
||||
|
||||
configCache.getConfig(filePath, function loaded(err, configJson) {
|
||||
|
|
179
core/erc_client.js
Normal file
179
core/erc_client.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const net = require('net');
|
||||
|
||||
/*
|
||||
Expected configuration block example:
|
||||
|
||||
config: {
|
||||
host: 192.168.1.171
|
||||
port: 5001
|
||||
bbsTag: SOME_TAG
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
exports.getModule = ErcClientModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'ENiGMA Relay Chat Client',
|
||||
desc : 'Chat with other ENiGMA BBSes',
|
||||
author : 'Andrew Pamment',
|
||||
};
|
||||
|
||||
var MciViewIds = {
|
||||
ChatDisplay : 1,
|
||||
InputArea : 3,
|
||||
};
|
||||
|
||||
// :TODO: needs converted to ES6 MenuModule subclass
|
||||
function ErcClientModule(options) {
|
||||
MenuModule.prototype.ctorShim.call(this, options);
|
||||
|
||||
const self = this;
|
||||
this.config = options.menuConfig.config;
|
||||
|
||||
this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}';
|
||||
this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}';
|
||||
|
||||
this.finishedLoading = function() {
|
||||
async.waterfall(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(_.isString(self.config.host) &&
|
||||
_.isNumber(self.config.port) &&
|
||||
_.isString(self.config.bbsTag))
|
||||
{
|
||||
return callback(null);
|
||||
} else {
|
||||
return callback(new Error('Configuration is missing required option(s)'));
|
||||
}
|
||||
},
|
||||
function connectToServer(callback) {
|
||||
const connectOpts = {
|
||||
port : self.config.port,
|
||||
host : self.config.host,
|
||||
};
|
||||
|
||||
const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
|
||||
|
||||
chatMessageView.setText('Connecting to server...');
|
||||
chatMessageView.redraw();
|
||||
|
||||
self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
|
||||
|
||||
// :TODO: Track actual client->enig connection for optional prevMenu @ final CB
|
||||
self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host);
|
||||
|
||||
self.chatConnection.on('data', data => {
|
||||
data = data.toString();
|
||||
|
||||
if(data.startsWith('ERCHANDSHAKE')) {
|
||||
self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`);
|
||||
} else if(data.startsWith('{')) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch(e) {
|
||||
return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server');
|
||||
}
|
||||
|
||||
let text;
|
||||
try {
|
||||
if(data.userName) {
|
||||
// user message
|
||||
text = stringFormat(self.chatEntryFormat, data);
|
||||
} else {
|
||||
// system message
|
||||
text = stringFormat(self.systemEntryFormat, data);
|
||||
}
|
||||
} catch(e) {
|
||||
return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error');
|
||||
}
|
||||
|
||||
chatMessageView.addText(text);
|
||||
|
||||
if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height?
|
||||
chatMessageView.deleteLine(0);
|
||||
chatMessageView.scrollDown();
|
||||
}
|
||||
|
||||
chatMessageView.redraw();
|
||||
self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
|
||||
}
|
||||
});
|
||||
|
||||
self.chatConnection.once('end', () => {
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
self.chatConnection.once('error', err => {
|
||||
self.client.log.info(`ERC connection error: ${err.message}`);
|
||||
return callback(new Error('Failed connecting to ERC server!'));
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.message }, 'ERC error');
|
||||
}
|
||||
|
||||
self.prevMenu();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.scrollHandler = function(keyName) {
|
||||
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
|
||||
const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
|
||||
|
||||
if('up arrow' === keyName) {
|
||||
chatDisplayView.scrollUp();
|
||||
} else {
|
||||
chatDisplayView.scrollDown();
|
||||
}
|
||||
|
||||
chatDisplayView.redraw();
|
||||
inputAreaView.setFocus(true);
|
||||
};
|
||||
|
||||
|
||||
this.menuMethods = {
|
||||
inputAreaSubmit : function(formData, extraArgs, cb) {
|
||||
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
|
||||
const inputData = inputAreaView.getData();
|
||||
|
||||
if('/quit' === inputData.toLowerCase()) {
|
||||
self.chatConnection.end();
|
||||
} else {
|
||||
try {
|
||||
self.chatConnection.write(`${inputData}\r\n`);
|
||||
} catch(e) {
|
||||
self.client.log.warn( { error : e.message }, 'ERC error');
|
||||
}
|
||||
inputAreaView.clearText();
|
||||
}
|
||||
return cb(null);
|
||||
},
|
||||
scrollUp : function(formData, extraArgs, cb) {
|
||||
self.scrollHandler(formData.key.name);
|
||||
return cb(null);
|
||||
},
|
||||
scrollDown : function(formData, extraArgs, cb) {
|
||||
self.scrollHandler(formData.key.name);
|
||||
return cb(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(ErcClientModule, MenuModule);
|
||||
|
||||
ErcClientModule.prototype.mciReady = function(mciData, cb) {
|
||||
this.standardMCIReadyHandler(mciData, cb);
|
||||
};
|
339
core/file_area_filter_edit.js
Normal file
339
core/file_area_filter_edit.js
Normal file
|
@ -0,0 +1,339 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||
const FileBaseFilters = require('./file_base_filter.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Area Filter Editor',
|
||||
desc : 'Module for adding, deleting, and modifying file base filters',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
editor : {
|
||||
searchTerms : 1,
|
||||
tags : 2,
|
||||
area : 3,
|
||||
sort : 4,
|
||||
order : 5,
|
||||
filterName : 6,
|
||||
navMenu : 7,
|
||||
|
||||
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
|
||||
selectedFilterInfo : 10, // { ...filter object ... }
|
||||
activeFilterInfo : 11, // { ...filter object ... }
|
||||
error : 12, // validation errors
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
|
||||
this.currentFilterIndex = 0; // into |filtersArray|
|
||||
|
||||
//
|
||||
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
|
||||
//
|
||||
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||
this.filtersArray.sort( (filterA, filterB) => {
|
||||
if(activeFilter) {
|
||||
if(filterA.uuid === activeFilter.uuid) {
|
||||
return -1;
|
||||
}
|
||||
if(filterB.uuid === activeFilter.uuid) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } );
|
||||
});
|
||||
|
||||
this.menuMethods = {
|
||||
saveFilter : (formData, extraArgs, cb) => {
|
||||
return this.saveCurrentFilter(formData, cb);
|
||||
},
|
||||
prevFilter : (formData, extraArgs, cb) => {
|
||||
this.currentFilterIndex -= 1;
|
||||
if(this.currentFilterIndex < 0) {
|
||||
this.currentFilterIndex = this.filtersArray.length - 1;
|
||||
}
|
||||
this.loadDataForFilter(this.currentFilterIndex);
|
||||
return cb(null);
|
||||
},
|
||||
nextFilter : (formData, extraArgs, cb) => {
|
||||
this.currentFilterIndex += 1;
|
||||
if(this.currentFilterIndex >= this.filtersArray.length) {
|
||||
this.currentFilterIndex = 0;
|
||||
}
|
||||
this.loadDataForFilter(this.currentFilterIndex);
|
||||
return cb(null);
|
||||
},
|
||||
makeFilterActive : (formData, extraArgs, cb) => {
|
||||
const filters = new FileBaseFilters(this.client);
|
||||
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
|
||||
|
||||
this.updateActiveLabel();
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
newFilter : (formData, extraArgs, cb) => {
|
||||
this.currentFilterIndex = this.filtersArray.length; // next avail slot
|
||||
this.clearForm(MciViewIds.editor.searchTerms);
|
||||
return cb(null);
|
||||
},
|
||||
deleteFilter : (formData, extraArgs, cb) => {
|
||||
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
||||
const filterUuid = selectedFilter.uuid;
|
||||
|
||||
// cannot delete built-in/system filters
|
||||
if(true === selectedFilter.system) {
|
||||
this.showError('Cannot delete built in filters!');
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
|
||||
|
||||
// remove from stored properties
|
||||
const filters = new FileBaseFilters(this.client);
|
||||
filters.remove(filterUuid);
|
||||
filters.persist( () => {
|
||||
|
||||
//
|
||||
// If the item was also the active filter, we need to make a new one active
|
||||
//
|
||||
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
|
||||
const newActive = this.filtersArray[this.currentFilterIndex];
|
||||
if(newActive) {
|
||||
filters.setActive(newActive.uuid);
|
||||
} else {
|
||||
// nothing to set active to
|
||||
this.client.user.removeProperty('file_base_filter_active_uuid');
|
||||
}
|
||||
}
|
||||
|
||||
// update UI
|
||||
this.updateActiveLabel();
|
||||
|
||||
if(this.filtersArray.length > 0) {
|
||||
this.loadDataForFilter(this.currentFilterIndex);
|
||||
} else {
|
||||
this.clearForm();
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
},
|
||||
|
||||
viewValidationListener : (err, cb) => {
|
||||
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
|
||||
let newFocusId;
|
||||
|
||||
if(errorView) {
|
||||
if(err) {
|
||||
errorView.setText(err.message);
|
||||
err.view.clearText(); // clear out the invalid data
|
||||
} else {
|
||||
errorView.clearText();
|
||||
}
|
||||
}
|
||||
|
||||
return cb(newFocusId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
showError(errMsg) {
|
||||
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
|
||||
if(errorView) {
|
||||
if(errMsg) {
|
||||
errorView.setText(errMsg);
|
||||
} else {
|
||||
errorView.clearText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
||||
},
|
||||
function populateAreas(callback) {
|
||||
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
||||
|
||||
const areasView = vc.getView(MciViewIds.editor.area);
|
||||
if(areasView) {
|
||||
areasView.setItems( self.availAreas.map( a => a.name ) );
|
||||
}
|
||||
|
||||
self.updateActiveLabel();
|
||||
self.loadDataForFilter(self.currentFilterIndex);
|
||||
self.viewControllers.editor.resetInitialFocus();
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentFilter() {
|
||||
return this.filtersArray[this.currentFilterIndex];
|
||||
}
|
||||
|
||||
setText(mciId, text) {
|
||||
const view = this.viewControllers.editor.getView(mciId);
|
||||
if(view) {
|
||||
view.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveLabel() {
|
||||
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||
if(activeFilter) {
|
||||
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
|
||||
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
|
||||
}
|
||||
}
|
||||
|
||||
setFocusItemIndex(mciId, index) {
|
||||
const view = this.viewControllers.editor.getView(mciId);
|
||||
if(view) {
|
||||
view.setFocusItemIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
clearForm(newFocusId) {
|
||||
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
|
||||
this.setText(mciId, '');
|
||||
});
|
||||
|
||||
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
|
||||
this.setFocusItemIndex(mciId, 0);
|
||||
});
|
||||
|
||||
if(newFocusId) {
|
||||
this.viewControllers.editor.switchFocus(newFocusId);
|
||||
} else {
|
||||
this.viewControllers.editor.resetInitialFocus();
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedAreaTag(index) {
|
||||
if(0 === index) {
|
||||
return ''; // -ALL-
|
||||
}
|
||||
const area = this.availAreas[index];
|
||||
if(!area) {
|
||||
return '';
|
||||
}
|
||||
return area.areaTag;
|
||||
}
|
||||
|
||||
getOrderBy(index) {
|
||||
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
|
||||
}
|
||||
|
||||
setAreaIndexFromCurrentFilter() {
|
||||
let index;
|
||||
const filter = this.getCurrentFilter();
|
||||
if(filter) {
|
||||
// special treatment: areaTag saved as blank ("") if -ALL-
|
||||
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
this.setFocusItemIndex(MciViewIds.editor.area, index);
|
||||
}
|
||||
|
||||
setOrderByFromCurrentFilter() {
|
||||
let index;
|
||||
const filter = this.getCurrentFilter();
|
||||
if(filter) {
|
||||
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
this.setFocusItemIndex(MciViewIds.editor.order, index);
|
||||
}
|
||||
|
||||
setSortByFromCurrentFilter() {
|
||||
let index;
|
||||
const filter = this.getCurrentFilter();
|
||||
if(filter) {
|
||||
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
this.setFocusItemIndex(MciViewIds.editor.sort, index);
|
||||
}
|
||||
|
||||
getSortBy(index) {
|
||||
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
|
||||
}
|
||||
|
||||
setFilterValuesFromFormData(filter, formData) {
|
||||
filter.name = formData.value.name;
|
||||
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
|
||||
filter.terms = formData.value.searchTerms;
|
||||
filter.tags = formData.value.tags;
|
||||
filter.order = this.getOrderBy(formData.value.orderByIndex);
|
||||
filter.sort = this.getSortBy(formData.value.sortByIndex);
|
||||
}
|
||||
|
||||
saveCurrentFilter(formData, cb) {
|
||||
const filters = new FileBaseFilters(this.client);
|
||||
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
||||
|
||||
if(selectedFilter) {
|
||||
// *update* currently selected filter
|
||||
this.setFilterValuesFromFormData(selectedFilter, formData);
|
||||
filters.replace(selectedFilter.uuid, selectedFilter);
|
||||
} else {
|
||||
// add a new entry; note that UUID will be generated
|
||||
const newFilter = {};
|
||||
this.setFilterValuesFromFormData(newFilter, formData);
|
||||
|
||||
// set current to what we just saved
|
||||
newFilter.uuid = filters.add(newFilter);
|
||||
|
||||
// add to our array (at current index position)
|
||||
this.filtersArray[this.currentFilterIndex] = newFilter;
|
||||
}
|
||||
|
||||
return filters.persist(cb);
|
||||
}
|
||||
|
||||
loadDataForFilter(filterIndex) {
|
||||
const filter = this.filtersArray[filterIndex];
|
||||
if(filter) {
|
||||
this.setText(MciViewIds.editor.searchTerms, filter.terms);
|
||||
this.setText(MciViewIds.editor.tags, filter.tags);
|
||||
this.setText(MciViewIds.editor.filterName, filter.name);
|
||||
|
||||
this.setAreaIndexFromCurrentFilter();
|
||||
this.setSortByFromCurrentFilter();
|
||||
this.setOrderByFromCurrentFilter();
|
||||
}
|
||||
}
|
||||
};
|
701
core/file_area_list.js
Normal file
701
core/file_area_list.js
Normal file
|
@ -0,0 +1,701 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const ansi = require('./ansi_term.js');
|
||||
const theme = require('./theme.js');
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
const FileArea = require('./file_base_area.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
||||
const ArchiveUtil = require('./archive_util.js');
|
||||
const Config = require('./config.js').config;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const FileAreaWeb = require('./file_area_web.js');
|
||||
const FileBaseFilters = require('./file_base_filter.js');
|
||||
const resolveMimeType = require('./mime_util.js').resolveMimeType;
|
||||
const isAnsi = require('./string_util.js').isAnsi;
|
||||
const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Area List',
|
||||
desc : 'Lists contents of file an file area',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
browse : 0,
|
||||
details : 1,
|
||||
detailsGeneral : 2,
|
||||
detailsNfo : 3,
|
||||
detailsFileList : 4,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
browse : {
|
||||
desc : 1,
|
||||
navMenu : 2,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
details : {
|
||||
navMenu : 1,
|
||||
infoXyTop : 2, // %XY starting position for info area
|
||||
infoXyBottom : 3,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
detailsGeneral : {
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
detailsNfo : {
|
||||
nfo : 1,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
detailsFileList : {
|
||||
fileList : 1,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
};
|
||||
|
||||
exports.getModule = class FileAreaList extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.filterCriteria = _.get(options, 'extraArgs.filterCriteria');
|
||||
this.fileList = _.get(options, 'extraArgs.fileList');
|
||||
|
||||
if(this.fileList) {
|
||||
// we'll need to adjust position as well!
|
||||
this.fileListPosition = 0;
|
||||
}
|
||||
|
||||
this.dlQueue = new DownloadQueue(this.client);
|
||||
|
||||
if(!this.filterCriteria) {
|
||||
this.filterCriteria = FileBaseFilters.getActiveFilter(this.client);
|
||||
}
|
||||
|
||||
if(_.isString(this.filterCriteria)) {
|
||||
this.filterCriteria = JSON.parse(this.filterCriteria);
|
||||
}
|
||||
|
||||
if(_.has(options, 'lastMenuResult.value')) {
|
||||
this.lastMenuResultValue = options.lastMenuResult.value;
|
||||
}
|
||||
|
||||
this.menuMethods = {
|
||||
nextFile : (formData, extraArgs, cb) => {
|
||||
if(this.fileListPosition + 1 < this.fileList.length) {
|
||||
this.fileListPosition += 1;
|
||||
|
||||
return this.displayBrowsePage(true, cb); // true=clerarScreen
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
prevFile : (formData, extraArgs, cb) => {
|
||||
if(this.fileListPosition > 0) {
|
||||
--this.fileListPosition;
|
||||
|
||||
return this.displayBrowsePage(true, cb); // true=clearScreen
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
viewDetails : (formData, extraArgs, cb) => {
|
||||
this.viewControllers.browse.setFocus(false);
|
||||
return this.displayDetailsPage(cb);
|
||||
},
|
||||
detailsQuit : (formData, extraArgs, cb) => {
|
||||
[ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => {
|
||||
const vc = this.viewControllers[n];
|
||||
if(vc) {
|
||||
vc.detachClientEvents();
|
||||
}
|
||||
});
|
||||
|
||||
return this.displayBrowsePage(true, cb); // true=clearScreen
|
||||
},
|
||||
toggleQueue : (formData, extraArgs, cb) => {
|
||||
this.dlQueue.toggle(this.currentFileEntry);
|
||||
this.updateQueueIndicator();
|
||||
return cb(null);
|
||||
},
|
||||
showWebDownloadLink : (formData, extraArgs, cb) => {
|
||||
return this.fetchAndDisplayWebDownloadLink(cb);
|
||||
},
|
||||
displayHelp : (formData, extraArgs, cb) => {
|
||||
return this.displayHelpPage(cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
enter() {
|
||||
super.enter();
|
||||
}
|
||||
|
||||
leave() {
|
||||
super.leave();
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
return {
|
||||
fileList : this.fileList,
|
||||
fileListPosition : this.fileListPosition,
|
||||
};
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
if(savedState) {
|
||||
this.fileList = savedState.fileList;
|
||||
this.fileListPosition = savedState.fileListPosition;
|
||||
}
|
||||
}
|
||||
|
||||
updateFileEntryWithMenuResult(cb) {
|
||||
if(!this.lastMenuResultValue) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
if(_.isNumber(this.lastMenuResultValue.rating)) {
|
||||
const fileId = this.fileList[this.fileListPosition];
|
||||
FileEntry.persistUserRating(fileId, 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);
|
||||
});
|
||||
} else {
|
||||
return cb(null);
|
||||
}
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function preInit(callback) {
|
||||
return self.updateFileEntryWithMenuResult(callback);
|
||||
},
|
||||
function beforeArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayBrowsePage(false, err => {
|
||||
if(err && 'NORESULTS' === err.reasonCode) {
|
||||
self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults');
|
||||
}
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
() => {
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
populateCurrentEntryInfo(cb) {
|
||||
const config = this.menuConfig.config;
|
||||
const currEntry = this.currentFileEntry;
|
||||
|
||||
const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD';
|
||||
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
|
||||
const hashTagsSep = config.hashTagsSep || ', ';
|
||||
const isQueuedIndicator = config.isQueuedIndicator || 'Y';
|
||||
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
|
||||
|
||||
const entryInfo = currEntry.entryInfo = {
|
||||
fileId : currEntry.fileId,
|
||||
areaTag : currEntry.areaTag,
|
||||
areaName : _.get(area, 'name') || 'N/A',
|
||||
areaDesc : _.get(area, 'desc') || 'N/A',
|
||||
fileSha256 : currEntry.fileSha256,
|
||||
fileName : currEntry.fileName,
|
||||
desc : currEntry.desc || '',
|
||||
descLong : currEntry.descLong || '',
|
||||
userRating : currEntry.userRating,
|
||||
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
|
||||
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
|
||||
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
|
||||
// consumers may very likely attempt to use them
|
||||
//
|
||||
const metaValues = FileEntry.WellKnownMetaValues;
|
||||
metaValues.forEach(name => {
|
||||
const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A';
|
||||
entryInfo[_.camelCase(name)] = value;
|
||||
});
|
||||
|
||||
if(entryInfo.archiveType) {
|
||||
const mimeType = resolveMimeType(entryInfo.archiveType);
|
||||
entryInfo.archiveTypeDesc = mimeType ? _.get(Config, [ 'fileTypes', mimeType, 'desc' ] ) || mimeType : entryInfo.archiveType;
|
||||
} else {
|
||||
entryInfo.archiveTypeDesc = 'N/A';
|
||||
}
|
||||
|
||||
entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported
|
||||
entryInfo.hashTags = entryInfo.hashTags || '(none)';
|
||||
|
||||
// create a rating string, e.g. "**---"
|
||||
const userRatingTicked = config.userRatingTicked || '*';
|
||||
const userRatingUnticked = config.userRatingUnticked || '';
|
||||
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
|
||||
entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating);
|
||||
if(entryInfo.userRating < 5) {
|
||||
entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) );
|
||||
}
|
||||
|
||||
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
|
||||
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 {
|
||||
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
||||
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
populateCustomLabels(category, startId) {
|
||||
return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo);
|
||||
}
|
||||
|
||||
displayArtAndPrepViewController(name, options, cb) {
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function readyAndDisplayArt(callback) {
|
||||
if(options.clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art[name],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepeareViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers[name])) {
|
||||
const vcOpts = {
|
||||
client : self.client,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
if(!_.isUndefined(options.noInput)) {
|
||||
vcOpts.noInput = options.noInput;
|
||||
}
|
||||
|
||||
const vc = self.addViewController(name, new ViewController(vcOpts));
|
||||
|
||||
if('details' === name) {
|
||||
try {
|
||||
self.detailsInfoArea = {
|
||||
top : artData.mciMap.XY2.position,
|
||||
bottom : artData.mciMap.XY3.position,
|
||||
};
|
||||
} catch(e) {
|
||||
return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!'));
|
||||
}
|
||||
}
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
}
|
||||
|
||||
self.viewControllers[name].setFocus(true);
|
||||
return callback(null);
|
||||
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayBrowsePage(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function fetchEntryData(callback) {
|
||||
if(self.fileList) {
|
||||
return callback(null);
|
||||
}
|
||||
return self.loadFileIds(false, callback); // false=do not force
|
||||
},
|
||||
function checkEmptyResults(callback) {
|
||||
if(0 === self.fileList.length) {
|
||||
return callback(Errors.General('No results for criteria', 'NORESULTS'));
|
||||
}
|
||||
return callback(null);
|
||||
},
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback);
|
||||
},
|
||||
function loadCurrentFileInfo(callback) {
|
||||
self.currentFileEntry = new FileEntry();
|
||||
|
||||
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return self.populateCurrentEntryInfo(callback);
|
||||
});
|
||||
},
|
||||
function populateDesc(callback) {
|
||||
if(_.isString(self.currentFileEntry.desc)) {
|
||||
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
|
||||
if(descView) {
|
||||
//
|
||||
// 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
|
||||
// pipes, PCB @X##, etc.)
|
||||
//
|
||||
// MLTEV doesn't support all of this, so convert. If we produced ANSI
|
||||
// esc sequences, we'll proceed with specialization, else just treat
|
||||
// it as text.
|
||||
//
|
||||
const desc = controlCodesToAnsi(self.currentFileEntry.desc);
|
||||
if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) {
|
||||
descView.setAnsi(
|
||||
desc,
|
||||
{
|
||||
prepped : false,
|
||||
forceLineTerm : true
|
||||
},
|
||||
() => {
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
descView.setText(self.currentFileEntry.desc);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function populateAdditionalViews(callback) {
|
||||
self.updateQueueIndicator();
|
||||
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayDetailsPage(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
self.populateCustomLabels('details', MciViewIds.details.customRangeStart);
|
||||
return callback(null);
|
||||
},
|
||||
function prepSection(callback) {
|
||||
return self.displayDetailsSection('general', false, callback);
|
||||
},
|
||||
function listenNavChanges(callback) {
|
||||
const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu);
|
||||
navMenu.setFocusItemIndex(0);
|
||||
|
||||
navMenu.on('index update', index => {
|
||||
const sectionName = {
|
||||
0 : 'general',
|
||||
1 : 'nfo',
|
||||
2 : 'fileList',
|
||||
}[index];
|
||||
|
||||
if(sectionName) {
|
||||
self.displayDetailsSection(sectionName, true);
|
||||
}
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayHelpPage(cb) {
|
||||
this.displayAsset(
|
||||
this.menuConfig.config.art.help,
|
||||
{ clearScreen : true },
|
||||
() => {
|
||||
this.client.waitForKeyPress( () => {
|
||||
return this.displayBrowsePage(true, cb);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fetchAndDisplayWebDownloadLink(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function generateLinkIfNeeded(callback) {
|
||||
|
||||
if(self.currentFileEntry.webDlExpireTime < moment()) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempDownload(
|
||||
self.client,
|
||||
self.currentFileEntry,
|
||||
{ expireTime : expireTime },
|
||||
(err, url) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.currentFileEntry.webDlExpireTime = expireTime;
|
||||
|
||||
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
|
||||
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
},
|
||||
function updateActiveViews(callback) {
|
||||
self.updateCustomViewTextsWithFilter(
|
||||
'browse',
|
||||
MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo,
|
||||
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
|
||||
);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateQueueIndicator() {
|
||||
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
|
||||
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
|
||||
|
||||
this.currentFileEntry.entryInfo.isQueued = stringFormat(
|
||||
this.dlQueue.isQueued(this.currentFileEntry) ?
|
||||
isQueuedIndicator :
|
||||
isNotQueuedIndicator
|
||||
);
|
||||
|
||||
this.updateCustomViewTextsWithFilter(
|
||||
'browse',
|
||||
MciViewIds.browse.customRangeStart,
|
||||
this.currentFileEntry.entryInfo,
|
||||
{ filter : [ '{isQueued}' ] }
|
||||
);
|
||||
}
|
||||
|
||||
cacheArchiveEntries(cb) {
|
||||
// check cache
|
||||
if(this.currentFileEntry.archiveEntries) {
|
||||
return cb(null, 'cache');
|
||||
}
|
||||
|
||||
const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag);
|
||||
if(!areaInfo) {
|
||||
return cb(Errors.Invalid('Invalid area tag'));
|
||||
}
|
||||
|
||||
const filePath = this.currentFileEntry.filePath;
|
||||
const archiveUtil = ArchiveUtil.getInstance();
|
||||
|
||||
archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.currentFileEntry.archiveEntries = entries;
|
||||
return cb(null, 're-cached');
|
||||
});
|
||||
}
|
||||
|
||||
populateFileListing() {
|
||||
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
|
||||
|
||||
if(this.currentFileEntry.entryInfo.archiveType) {
|
||||
this.cacheArchiveEntries( (err, cacheStatus) => {
|
||||
if(err) {
|
||||
// :TODO: Handle me!!!
|
||||
fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck
|
||||
return;
|
||||
}
|
||||
|
||||
if('re-cached' === cacheStatus) {
|
||||
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here?
|
||||
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
|
||||
|
||||
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
|
||||
fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) );
|
||||
|
||||
fileListView.redraw();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] );
|
||||
}
|
||||
}
|
||||
|
||||
displayDetailsSection(sectionName, clearArea, cb) {
|
||||
const self = this;
|
||||
const name = `details${_.upperFirst(sectionName)}`;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function detachPrevious(callback) {
|
||||
if(self.lastDetailsViewController) {
|
||||
self.lastDetailsViewController.detachClientEvents();
|
||||
}
|
||||
return callback(null);
|
||||
},
|
||||
function prepArtAndViewController(callback) {
|
||||
|
||||
function gotoTopPos() {
|
||||
self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1));
|
||||
}
|
||||
|
||||
gotoTopPos();
|
||||
|
||||
if(clearArea) {
|
||||
self.client.term.rawWrite(ansi.reset());
|
||||
|
||||
let pos = self.detailsInfoArea.top[0];
|
||||
const bottom = self.detailsInfoArea.bottom[0];
|
||||
|
||||
while(pos++ <= bottom) {
|
||||
self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
|
||||
}
|
||||
|
||||
gotoTopPos();
|
||||
}
|
||||
|
||||
return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
self.lastDetailsViewController = self.viewControllers[name];
|
||||
|
||||
switch(sectionName) {
|
||||
case 'nfo' :
|
||||
{
|
||||
const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo);
|
||||
if(!nfoView) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if(isAnsi(self.currentFileEntry.entryInfo.descLong)) {
|
||||
nfoView.setAnsi(
|
||||
self.currentFileEntry.entryInfo.descLong,
|
||||
{
|
||||
prepped : false,
|
||||
forceLineTerm : true,
|
||||
},
|
||||
() => {
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
nfoView.setText(self.currentFileEntry.entryInfo.descLong);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fileList' :
|
||||
self.populateFileListing();
|
||||
return callback(null);
|
||||
|
||||
default :
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function setLabels(callback) {
|
||||
self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadFileIds(force, cb) {
|
||||
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) {
|
||||
this.fileListPosition = 0;
|
||||
|
||||
const filterCriteria = Object.assign({}, this.filterCriteria);
|
||||
if(!filterCriteria.areaTag) {
|
||||
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client);
|
||||
}
|
||||
|
||||
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
|
||||
this.fileList = fileIds;
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
104
core/file_base_area_select.js
Normal file
104
core/file_base_area_select.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||
const StatLog = require('./stat_log.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Area Selector',
|
||||
desc : 'Select from available file areas',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
areaList : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class FileAreaSelectModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = this.menuConfig.config || {};
|
||||
|
||||
this.loadAvailAreas();
|
||||
|
||||
this.menuMethods = {
|
||||
selectArea : (formData, extraArgs, cb) => {
|
||||
const area = this.availAreas[formData.value.areaSelect] || 0;
|
||||
|
||||
const filterCriteria = {
|
||||
areaTag : area.areaTag,
|
||||
};
|
||||
|
||||
const menuOpts = {
|
||||
extraArgs : {
|
||||
filterCriteria : filterCriteria,
|
||||
},
|
||||
menuFlags : [ 'noHistory' ],
|
||||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadAvailAreas() {
|
||||
this.availAreas = getSortedAvailableFileAreas(this.client);
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function mergeAreaStats(callback) {
|
||||
const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} };
|
||||
|
||||
self.availAreas.forEach(area => {
|
||||
const stats = areaStats.areas[area.areaTag];
|
||||
area.totalFiles = stats ? stats.files : 0;
|
||||
area.totalBytes = stats ? stats.bytes : 0;
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function prepView(callback) {
|
||||
self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const areaListView = vc.getView(MciViewIds.areaList);
|
||||
|
||||
const areaListFormat = self.config.areaListFormat || '{name}';
|
||||
|
||||
areaListView.setItems(self.availAreas.map(a => stringFormat(areaListFormat, a) ) );
|
||||
|
||||
if(self.config.areaListFocusFormat) {
|
||||
areaListView.setFocusItems(self.availAreas.map(a => stringFormat(self.config.areaListFocusFormat, a) ) );
|
||||
}
|
||||
|
||||
areaListView.redraw();
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
244
core/file_base_download_manager.js
Normal file
244
core/file_base_download_manager.js
Normal file
|
@ -0,0 +1,244 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const theme = require('./theme.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const FileAreaWeb = require('./file_area_web.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Base Download Queue Manager',
|
||||
desc : 'Module for interacting with download queue/batch',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
queueManager : 0,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
queueManager : {
|
||||
queue : 1,
|
||||
navMenu : 2,
|
||||
|
||||
customRangeStart : 10,
|
||||
},
|
||||
};
|
||||
|
||||
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.dlQueue = new DownloadQueue(this.client);
|
||||
|
||||
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||
}
|
||||
|
||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||
|
||||
this.menuMethods = {
|
||||
downloadAll : (formData, extraArgs, cb) => {
|
||||
const modOpts = {
|
||||
extraArgs : {
|
||||
sendQueue : this.dlQueue.items,
|
||||
direction : 'send',
|
||||
}
|
||||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
|
||||
},
|
||||
viewItemInfo : (formData, extraArgs, cb) => {
|
||||
},
|
||||
removeItem : (formData, extraArgs, cb) => {
|
||||
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
||||
if(!selectedItem) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
this.dlQueue.removeItems(selectedItem.fileId);
|
||||
|
||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
|
||||
},
|
||||
clearQueue : (formData, extraArgs, cb) => {
|
||||
this.dlQueue.clear();
|
||||
|
||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||
return this.removeItemsFromDownloadQueueView('all', cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
if(0 === this.dlQueue.items.length) {
|
||||
if(this.sendFileIds) {
|
||||
// we've finished everything up - just fall back
|
||||
return this.prevMenu();
|
||||
}
|
||||
|
||||
// Simply an empty D/L queue: Present a specialized "empty queue" page
|
||||
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function beforeArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayQueueManagerPage(false, callback);
|
||||
}
|
||||
],
|
||||
() => {
|
||||
return self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||
if(!queueView) {
|
||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
if('all' === itemIndex) {
|
||||
queueView.setItems([]);
|
||||
queueView.setFocusItems([]);
|
||||
} else {
|
||||
queueView.removeItem(itemIndex);
|
||||
}
|
||||
|
||||
queueView.redraw();
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
displayWebDownloadLinkForFileEntry(fileEntry) {
|
||||
FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => {
|
||||
if(serveItem && serveItem.url) {
|
||||
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
||||
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
||||
} else {
|
||||
fileEntry.webDlLink = '';
|
||||
fileEntry.webDlExpire = '';
|
||||
}
|
||||
|
||||
this.updateCustomViewTextsWithFilter(
|
||||
'queueManager',
|
||||
MciViewIds.queueManager.customRangeStart, fileEntry,
|
||||
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateDownloadQueueView(cb) {
|
||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||
if(!queueView) {
|
||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}';
|
||||
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
|
||||
|
||||
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
|
||||
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
|
||||
|
||||
queueView.on('index update', idx => {
|
||||
const fileEntry = this.dlQueue.items[idx];
|
||||
this.displayWebDownloadLinkForFileEntry(fileEntry);
|
||||
});
|
||||
|
||||
queueView.redraw();
|
||||
this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]);
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
displayQueueManagerPage(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
return self.updateDownloadQueueView(callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayArtAndPrepViewController(name, options, cb) {
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function readyAndDisplayArt(callback) {
|
||||
if(options.clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art[name],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepeareViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers[name])) {
|
||||
const vcOpts = {
|
||||
client : self.client,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
if(!_.isUndefined(options.noInput)) {
|
||||
vcOpts.noInput = options.noInput;
|
||||
}
|
||||
|
||||
const vc = self.addViewController(name, new ViewController(vcOpts));
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
}
|
||||
|
||||
self.viewControllers[name].setFocus(true);
|
||||
return callback(null);
|
||||
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
120
core/file_base_search.js
Normal file
120
core/file_base_search.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||
const FileBaseFilters = require('./file_base_filter.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Base Search',
|
||||
desc : 'Module for quickly searching the file base',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
search : {
|
||||
searchTerms : 1,
|
||||
search : 2,
|
||||
tags : 3,
|
||||
area : 4,
|
||||
orderBy : 5,
|
||||
sort : 6,
|
||||
advSearch : 7,
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileBaseSearch extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.menuMethods = {
|
||||
search : (formData, extraArgs, cb) => {
|
||||
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
|
||||
return this.searchNow(formData, isAdvanced, cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
||||
},
|
||||
function populateAreas(callback) {
|
||||
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
||||
|
||||
const areasView = vc.getView(MciViewIds.search.area);
|
||||
areasView.setItems( self.availAreas.map( a => a.name ) );
|
||||
areasView.redraw();
|
||||
vc.switchFocus(MciViewIds.search.searchTerms);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedAreaTag(index) {
|
||||
if(0 === index) {
|
||||
return ''; // -ALL-
|
||||
}
|
||||
const area = this.availAreas[index];
|
||||
if(!area) {
|
||||
return '';
|
||||
}
|
||||
return area.areaTag;
|
||||
}
|
||||
|
||||
getOrderBy(index) {
|
||||
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
|
||||
}
|
||||
|
||||
getSortBy(index) {
|
||||
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
|
||||
}
|
||||
|
||||
getFilterValuesFromFormData(formData, isAdvanced) {
|
||||
const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
|
||||
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
|
||||
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
|
||||
|
||||
return {
|
||||
areaTag : this.getSelectedAreaTag(areaIndex),
|
||||
terms : formData.value.searchTerms,
|
||||
tags : isAdvanced ? formData.value.tags : '',
|
||||
order : this.getOrderBy(orderByIndex),
|
||||
sort : this.getSortBy(sortByIndex),
|
||||
};
|
||||
}
|
||||
|
||||
searchNow(formData, isAdvanced, cb) {
|
||||
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
|
||||
|
||||
const menuOpts = {
|
||||
extraArgs : {
|
||||
filterCriteria : filterCriteria,
|
||||
},
|
||||
menuFlags : [ 'noHistory' ],
|
||||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
|
||||
}
|
||||
};
|
287
core/file_base_web_download_manager.js
Normal file
287
core/file_base_web_download_manager.js
Normal file
|
@ -0,0 +1,287 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const theme = require('./theme.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const FileAreaWeb = require('./file_area_web.js');
|
||||
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
|
||||
const Config = require('./config.js').config;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Base Download Web Queue Manager',
|
||||
desc : 'Module for interacting with web backed download queue/batch',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
queueManager : 0
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
queueManager : {
|
||||
queue : 1,
|
||||
navMenu : 2,
|
||||
|
||||
customRangeStart : 10,
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.dlQueue = new DownloadQueue(this.client);
|
||||
|
||||
this.menuMethods = {
|
||||
removeItem : (formData, extraArgs, cb) => {
|
||||
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
||||
if(!selectedItem) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
this.dlQueue.removeItems(selectedItem.fileId);
|
||||
|
||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
|
||||
},
|
||||
clearQueue : (formData, extraArgs, cb) => {
|
||||
this.dlQueue.clear();
|
||||
|
||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||
return this.removeItemsFromDownloadQueueView('all', cb);
|
||||
},
|
||||
getBatchLink : (formData, extraArgs, cb) => {
|
||||
return this.generateAndDisplayBatchLink(cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
if(0 === this.dlQueue.items.length) {
|
||||
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function beforeArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayQueueManagerPage(false, callback);
|
||||
}
|
||||
],
|
||||
() => {
|
||||
return self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||
if(!queueView) {
|
||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
if('all' === itemIndex) {
|
||||
queueView.setItems([]);
|
||||
queueView.setFocusItems([]);
|
||||
} else {
|
||||
queueView.removeItem(itemIndex);
|
||||
}
|
||||
|
||||
queueView.redraw();
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
displayFileInfoForFileEntry(fileEntry) {
|
||||
this.updateCustomViewTextsWithFilter(
|
||||
'queueManager',
|
||||
MciViewIds.queueManager.customRangeStart, fileEntry,
|
||||
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
|
||||
);
|
||||
}
|
||||
|
||||
updateDownloadQueueView(cb) {
|
||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||
if(!queueView) {
|
||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}';
|
||||
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
|
||||
|
||||
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
|
||||
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
|
||||
|
||||
queueView.on('index update', idx => {
|
||||
const fileEntry = this.dlQueue.items[idx];
|
||||
this.displayFileInfoForFileEntry(fileEntry);
|
||||
});
|
||||
|
||||
queueView.redraw();
|
||||
this.displayFileInfoForFileEntry(this.dlQueue.items[0]);
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
generateAndDisplayBatchLink(cb) {
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempBatchDownload(
|
||||
this.client,
|
||||
this.dlQueue.items,
|
||||
{
|
||||
expireTime : expireTime
|
||||
},
|
||||
(err, webBatchDlLink) => {
|
||||
// :TODO: handle not enabled -> display such
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
const formatObj = {
|
||||
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
|
||||
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
|
||||
};
|
||||
|
||||
this.updateCustomViewTextsWithFilter(
|
||||
'queueManager',
|
||||
MciViewIds.queueManager.customRangeStart,
|
||||
formatObj,
|
||||
{ filter : Object.keys(formatObj).map(k => '{' + k + '}' ) }
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayQueueManagerPage(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
|
||||
},
|
||||
function prepareQueueDownloadLinks(callback) {
|
||||
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
|
||||
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
|
||||
if(err) {
|
||||
if(ErrNotEnabled === err.reasonCode) {
|
||||
return nextFileEntry(err); // we should have caught this prior
|
||||
}
|
||||
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempDownload(
|
||||
self.client,
|
||||
fileEntry,
|
||||
{ expireTime : expireTime },
|
||||
(err, url) => {
|
||||
if(err) {
|
||||
return nextFileEntry(err);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function populateViews(callback) {
|
||||
return self.updateDownloadQueueView(callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayArtAndPrepViewController(name, options, cb) {
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function readyAndDisplayArt(callback) {
|
||||
if(options.clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art[name],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepeareViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers[name])) {
|
||||
const vcOpts = {
|
||||
client : self.client,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
if(!_.isUndefined(options.noInput)) {
|
||||
vcOpts.noInput = options.noInput;
|
||||
}
|
||||
|
||||
const vc = self.addViewController(name, new ViewController(vcOpts));
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
}
|
||||
|
||||
self.viewControllers[name].setFocus(true);
|
||||
return callback(null);
|
||||
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
158
core/file_transfer_protocol_select.js
Normal file
158
core/file_transfer_protocol_select.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const Config = require('./config.js').config;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File transfer protocol selection',
|
||||
desc : 'Select protocol / method for file transfer',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
protList : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = this.menuConfig.config || {};
|
||||
|
||||
if(options.extraArgs) {
|
||||
if(options.extraArgs.direction) {
|
||||
this.config.direction = options.extraArgs.direction;
|
||||
}
|
||||
}
|
||||
|
||||
this.config.direction = this.config.direction || 'send';
|
||||
|
||||
this.extraArgs = options.extraArgs;
|
||||
|
||||
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||
}
|
||||
|
||||
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
|
||||
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
||||
}
|
||||
|
||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||
|
||||
this.loadAvailProtocols();
|
||||
|
||||
this.menuMethods = {
|
||||
selectProtocol : (formData, extraArgs, cb) => {
|
||||
const protocol = this.protocols[formData.value.protocol];
|
||||
const finalExtraArgs = this.extraArgs || {};
|
||||
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
|
||||
|
||||
const modOpts = {
|
||||
extraArgs : finalExtraArgs,
|
||||
};
|
||||
|
||||
if('send' === this.config.direction) {
|
||||
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb);
|
||||
} else {
|
||||
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getMenuResult() {
|
||||
if(this.sentFileIds) {
|
||||
return { sentFileIds : this.sentFileIds };
|
||||
}
|
||||
|
||||
if(this.recvFilePaths) {
|
||||
return { recvFilePaths : this.recvFilePaths };
|
||||
}
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
if(this.sentFileIds || this.recvFilePaths) {
|
||||
// nothing to do here; move along (we're just falling through)
|
||||
this.prevMenu();
|
||||
} else {
|
||||
super.initSequence();
|
||||
}
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function populateList(callback) {
|
||||
const protListView = vc.getView(MciViewIds.protList);
|
||||
|
||||
const protListFormat = self.config.protListFormat || '{name}';
|
||||
const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
|
||||
|
||||
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
|
||||
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
|
||||
|
||||
protListView.redraw();
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadAvailProtocols() {
|
||||
this.protocols = _.map(Config.fileTransferProtocols, (protInfo, protocol) => {
|
||||
return {
|
||||
protocol : protocol,
|
||||
name : protInfo.name,
|
||||
hasBatch : _.has(protInfo, 'external.recvArgs'),
|
||||
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
|
||||
sort : protInfo.sort,
|
||||
};
|
||||
});
|
||||
|
||||
// Filter out batch vs non-batch only protocols
|
||||
if(this.extraArgs.recvFileName) { // non-batch aka non-blind
|
||||
this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
|
||||
} else {
|
||||
this.protocols = this.protocols.filter( prot => prot.hasBatch );
|
||||
}
|
||||
|
||||
// natural sort taking explicit orders into consideration
|
||||
this.protocols.sort( (a, b) => {
|
||||
if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
|
||||
return a.sort - b.sort;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
151
core/last_callers.js
Normal file
151
core/last_callers.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const StatLog = require('./stat_log.js');
|
||||
const User = require('./user.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const moment = require('moment');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
/*
|
||||
Available listFormat object members:
|
||||
userId
|
||||
userName
|
||||
location
|
||||
affiliation
|
||||
ts
|
||||
|
||||
*/
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Last Callers',
|
||||
desc : 'Last callers to the system',
|
||||
author : 'NuSkooler',
|
||||
packageName : 'codes.l33t.enigma.lastcallers'
|
||||
};
|
||||
|
||||
const MciCodeIds = {
|
||||
CallerList : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class LastCallersModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
let loginHistory;
|
||||
let callersView;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu,
|
||||
noInput : true,
|
||||
};
|
||||
|
||||
vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function fetchHistory(callback) {
|
||||
callersView = vc.getView(MciCodeIds.CallerList);
|
||||
|
||||
// fetch up
|
||||
StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => {
|
||||
loginHistory = lh;
|
||||
|
||||
if(self.menuConfig.config.hideSysOpLogin) {
|
||||
const noOpLoginHistory = loginHistory.filter(lh => {
|
||||
return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId
|
||||
});
|
||||
|
||||
//
|
||||
// If we have enough items to display, or hideSysOpLogin is set to 'always',
|
||||
// then set loginHistory to our filtered list. Else, we'll leave it be.
|
||||
//
|
||||
if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) {
|
||||
loginHistory = noOpLoginHistory;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Finally, we need to trim up the list to the needed size
|
||||
//
|
||||
loginHistory = loginHistory.slice(0, callersView.dimens.height);
|
||||
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function getUserNamesAndProperties(callback) {
|
||||
const getPropOpts = {
|
||||
names : [ 'location', 'affiliation' ]
|
||||
};
|
||||
|
||||
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
|
||||
|
||||
async.each(
|
||||
loginHistory,
|
||||
(item, next) => {
|
||||
item.userId = parseInt(item.log_value);
|
||||
item.ts = moment(item.timestamp).format(dateTimeFormat);
|
||||
|
||||
User.getUserName(item.userId, (err, userName) => {
|
||||
if(err) {
|
||||
item.deleted = true;
|
||||
return next(null);
|
||||
} else {
|
||||
item.userName = userName || 'N/A';
|
||||
|
||||
User.loadProperties(item.userId, getPropOpts, (err, props) => {
|
||||
if(!err && props) {
|
||||
item.location = props.location || 'N/A';
|
||||
item.affiliation = item.affils = (props.affiliation || 'N/A');
|
||||
} else {
|
||||
item.location = 'N/A';
|
||||
item.affiliation = item.affils = 'N/A';
|
||||
}
|
||||
return next(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
err => {
|
||||
loginHistory = loginHistory.filter(lh => true !== lh.deleted);
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function populateList(callback) {
|
||||
const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}';
|
||||
|
||||
callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) );
|
||||
|
||||
callersView.redraw();
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
(err) => {
|
||||
if(err) {
|
||||
self.client.log.error( { error : err.toString() }, 'Error loading last callers');
|
||||
}
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
177
core/msg_area_list.js
Normal file
177
core/msg_area_list.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const messageArea = require('./message_area.js');
|
||||
const displayThemeArt = require('./theme.js').displayThemeArt;
|
||||
const resetScreen = require('./ansi_term.js').resetScreen;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area List',
|
||||
desc : 'Module for listing / choosing message areas',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
|
||||
Obv/2 has the following:
|
||||
CHANGE .ANS - Message base changing ansi
|
||||
|SN Current base name
|
||||
|SS Current base sponsor
|
||||
|NM Number of messages in current base
|
||||
|UP Number of posts current user made (total)
|
||||
|LR Last read message by current user
|
||||
|DT Current date
|
||||
|TI Current time
|
||||
*/
|
||||
|
||||
const MciViewIds = {
|
||||
AreaList : 1,
|
||||
SelAreaInfo1 : 2,
|
||||
SelAreaInfo2 : 3,
|
||||
};
|
||||
|
||||
exports.getModule = class MessageAreaListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
|
||||
this.client.user.properties.message_conf_tag,
|
||||
{ client : this.client }
|
||||
);
|
||||
|
||||
const self = this;
|
||||
this.menuMethods = {
|
||||
changeArea : function(formData, extraArgs, cb) {
|
||||
if(1 === formData.submitId) {
|
||||
let area = self.messageAreas[formData.value.area];
|
||||
const areaTag = area.areaTag;
|
||||
area = area.area; // what we want is actually embedded
|
||||
|
||||
messageArea.changeMessageArea(self.client, areaTag, err => {
|
||||
if(err) {
|
||||
self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`);
|
||||
|
||||
self.prevMenuOnTimeout(1000, cb);
|
||||
} else {
|
||||
if(_.isString(area.art)) {
|
||||
const dispOptions = {
|
||||
client : self.client,
|
||||
name : area.art,
|
||||
};
|
||||
|
||||
self.client.term.rawWrite(resetScreen());
|
||||
|
||||
displayThemeArt(dispOptions, () => {
|
||||
// pause by default, unless explicitly told not to
|
||||
if(_.has(area, 'options.pause') && false === area.options.pause) {
|
||||
return self.prevMenuOnTimeout(1000, cb);
|
||||
} else {
|
||||
self.pausePrompt( () => {
|
||||
return self.prevMenu(cb);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return cb(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prevMenuOnTimeout(timeout, cb) {
|
||||
setTimeout( () => {
|
||||
return this.prevMenu(cb);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
updateGeneralAreaInfoViews(areaIndex) {
|
||||
// :TODO: these concepts have been replaced with the {someKey} style formatting - update me!
|
||||
/* experimental: not yet avail
|
||||
const areaInfo = self.messageAreas[areaIndex];
|
||||
|
||||
[ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => {
|
||||
const v = self.viewControllers.areaList.getView(mciId);
|
||||
if(v) {
|
||||
v.setFormatObject(areaInfo.area);
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu,
|
||||
formId : 0,
|
||||
};
|
||||
|
||||
vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) {
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
function populateAreaListView(callback) {
|
||||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
const areaListView = vc.getView(MciViewIds.AreaList);
|
||||
let i = 1;
|
||||
areaListView.setItems(_.map(self.messageAreas, v => {
|
||||
return stringFormat(listFormat, {
|
||||
index : i++,
|
||||
areaTag : v.area.areaTag,
|
||||
name : v.area.name,
|
||||
desc : v.area.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
i = 1;
|
||||
areaListView.setFocusItems(_.map(self.messageAreas, v => {
|
||||
return stringFormat(focusListFormat, {
|
||||
index : i++,
|
||||
areaTag : v.area.areaTag,
|
||||
name : v.area.name,
|
||||
desc : v.area.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
areaListView.on('index update', areaIndex => {
|
||||
self.updateGeneralAreaInfoViews(areaIndex);
|
||||
});
|
||||
|
||||
areaListView.redraw();
|
||||
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
67
core/msg_area_post_fse.js
Normal file
67
core/msg_area_post_fse.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
||||
const persistMessage = require('./message_area.js').persistMessage;
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area Post',
|
||||
desc : 'Module for posting a new message to an area',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
|
||||
// we're posting, so always start with 'edit' mode
|
||||
this.editorMode = 'edit';
|
||||
|
||||
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
|
||||
|
||||
var msg;
|
||||
async.series(
|
||||
[
|
||||
function getMessageObject(callback) {
|
||||
self.getMessage(function gotMsg(err, msgObj) {
|
||||
msg = msgObj;
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function saveMessage(callback) {
|
||||
return persistMessage(msg, callback);
|
||||
},
|
||||
function updateStats(callback) {
|
||||
self.updateUserStats(callback);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
// :TODO:... sooooo now what?
|
||||
} else {
|
||||
// note: not logging 'from' here as it's part of client.log.xxxx()
|
||||
self.client.log.info(
|
||||
{ to : msg.toUserName, subject : msg.subject, uuid : msg.uuid },
|
||||
'Message persisted'
|
||||
);
|
||||
}
|
||||
|
||||
return self.nextMenu(cb);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
enter() {
|
||||
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
|
||||
super.enter();
|
||||
}
|
||||
};
|
18
core/msg_area_reply_fse.js
Normal file
18
core/msg_area_reply_fse.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
||||
|
||||
exports.getModule = AreaReplyFSEModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area Reply',
|
||||
desc : 'Module for replying to an area message',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
function AreaReplyFSEModule(options) {
|
||||
FullScreenEditorModule.call(this, options);
|
||||
}
|
||||
|
||||
require('util').inherits(AreaReplyFSEModule, FullScreenEditorModule);
|
135
core/msg_area_view_fse.js
Normal file
135
core/msg_area_view_fse.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
|
||||
const Message = require('./message.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area View',
|
||||
desc : 'Module for viewing an area message',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.editorType = 'area';
|
||||
this.editorMode = 'view';
|
||||
|
||||
if(_.isObject(options.extraArgs)) {
|
||||
this.messageList = options.extraArgs.messageList;
|
||||
this.messageIndex = options.extraArgs.messageIndex;
|
||||
this.lastMessageNextExit = options.extraArgs.lastMessageNextExit;
|
||||
}
|
||||
|
||||
this.messageList = this.messageList || [];
|
||||
this.messageIndex = this.messageIndex || 0;
|
||||
this.messageTotal = this.messageList.length;
|
||||
|
||||
const self = this;
|
||||
|
||||
// assign *additional* menuMethods
|
||||
Object.assign(this.menuMethods, {
|
||||
nextMessage : (formData, extraArgs, cb) => {
|
||||
if(self.messageIndex + 1 < self.messageList.length) {
|
||||
self.messageIndex++;
|
||||
|
||||
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
|
||||
}
|
||||
|
||||
// auto-exit if no more to go?
|
||||
if(self.lastMessageNextExit) {
|
||||
self.lastMessageReached = true;
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
prevMessage : (formData, extraArgs, cb) => {
|
||||
if(self.messageIndex > 0) {
|
||||
self.messageIndex--;
|
||||
|
||||
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
movementKeyPressed : (formData, extraArgs, cb) => {
|
||||
const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic #
|
||||
|
||||
// :TODO: Create methods for up/down vs using keyPressXXXXX
|
||||
switch(formData.key.name) {
|
||||
case 'down arrow' : bodyView.scrollDocumentUp(); break;
|
||||
case 'up arrow' : bodyView.scrollDocumentDown(); break;
|
||||
case 'page up' : bodyView.keyPressPageUp(); break;
|
||||
case 'page down' : bodyView.keyPressPageDown(); break;
|
||||
}
|
||||
|
||||
// :TODO: need to stop down/page down if doing so would push the last
|
||||
// visible page off the screen at all .... this should be handled by MLTEV though...
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
replyMessage : (formData, extraArgs, cb) => {
|
||||
if(_.isString(extraArgs.menu)) {
|
||||
const modOpts = {
|
||||
extraArgs : {
|
||||
messageAreaTag : self.messageAreaTag,
|
||||
replyToMessage : self.message,
|
||||
}
|
||||
};
|
||||
|
||||
return self.gotoMenu(extraArgs.menu, modOpts, cb);
|
||||
}
|
||||
|
||||
self.client.log(extraArgs, 'Missing extraArgs.menu');
|
||||
return cb(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
loadMessageByUuid(uuid, cb) {
|
||||
const msg = new Message();
|
||||
msg.load( { uuid : uuid, user : this.client.user }, () => {
|
||||
this.setMessage(msg);
|
||||
|
||||
if(cb) {
|
||||
return cb(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
finishedLoading() {
|
||||
this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid);
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
return {
|
||||
messageList : this.messageList,
|
||||
messageIndex : this.messageIndex,
|
||||
messageTotal : this.messageList.length,
|
||||
};
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
this.messageList = savedState.messageList;
|
||||
this.messageIndex = savedState.messageIndex;
|
||||
this.messageTotal = savedState.messageTotal;
|
||||
}
|
||||
|
||||
getMenuResult() {
|
||||
return {
|
||||
messageIndex : this.messageIndex,
|
||||
lastMessageReached : this.lastMessageReached,
|
||||
};
|
||||
}
|
||||
};
|
148
core/msg_conf_list.js
Normal file
148
core/msg_conf_list.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const messageArea = require('./message_area.js');
|
||||
const displayThemeArt = require('./theme.js').displayThemeArt;
|
||||
const resetScreen = require('./ansi_term.js').resetScreen;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Conference List',
|
||||
desc : 'Module for listing / choosing message conferences',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
ConfList : 1,
|
||||
|
||||
// :TODO:
|
||||
// # areas in conf .... see Obv/2, iNiQ, ...
|
||||
//
|
||||
};
|
||||
|
||||
exports.getModule = class MessageConfListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client);
|
||||
const self = this;
|
||||
|
||||
this.menuMethods = {
|
||||
changeConference : function(formData, extraArgs, cb) {
|
||||
if(1 === formData.submitId) {
|
||||
let conf = self.messageConfs[formData.value.conf];
|
||||
const confTag = conf.confTag;
|
||||
conf = conf.conf; // what we want is embedded
|
||||
|
||||
messageArea.changeMessageConference(self.client, confTag, err => {
|
||||
if(err) {
|
||||
self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`);
|
||||
|
||||
setTimeout( () => {
|
||||
return self.prevMenu(cb);
|
||||
}, 1000);
|
||||
} else {
|
||||
if(_.isString(conf.art)) {
|
||||
const dispOptions = {
|
||||
client : self.client,
|
||||
name : conf.art,
|
||||
};
|
||||
|
||||
self.client.term.rawWrite(resetScreen());
|
||||
|
||||
displayThemeArt(dispOptions, () => {
|
||||
// pause by default, unless explicitly told not to
|
||||
if(_.has(conf, 'options.pause') && false === conf.options.pause) {
|
||||
return self.prevMenuOnTimeout(1000, cb);
|
||||
} else {
|
||||
self.pausePrompt( () => {
|
||||
return self.prevMenu(cb);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return cb(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prevMenuOnTimeout(timeout, cb) {
|
||||
setTimeout( () => {
|
||||
return this.prevMenu(cb);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
let loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu,
|
||||
formId : 0,
|
||||
};
|
||||
|
||||
vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function populateConfListView(callback) {
|
||||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
const confListView = vc.getView(MciViewIds.ConfList);
|
||||
let i = 1;
|
||||
confListView.setItems(_.map(self.messageConfs, v => {
|
||||
return stringFormat(listFormat, {
|
||||
index : i++,
|
||||
confTag : v.conf.confTag,
|
||||
name : v.conf.name,
|
||||
desc : v.conf.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
i = 1;
|
||||
confListView.setFocusItems(_.map(self.messageConfs, v => {
|
||||
return stringFormat(focusListFormat, {
|
||||
index : i++,
|
||||
confTag : v.conf.confTag,
|
||||
name : v.conf.name,
|
||||
desc : v.conf.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
confListView.redraw();
|
||||
|
||||
callback(null);
|
||||
},
|
||||
function populateTextViews(callback) {
|
||||
// :TODO: populate other avail MCI, e.g. current conf name
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
259
core/msg_list.js
Normal file
259
core/msg_list.js
Normal file
|
@ -0,0 +1,259 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const messageArea = require('./message_area.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
/*
|
||||
Available listFormat/focusListFormat members (VM1):
|
||||
|
||||
msgNum : Message number
|
||||
to : To username/handle
|
||||
from : From username/handle
|
||||
subj : Subject
|
||||
ts : Message mod timestamp (format with config.dateTimeFormat)
|
||||
newIndicator : New mark/indicator (config.newIndicator)
|
||||
|
||||
MCI codes:
|
||||
|
||||
VM1 : Message list
|
||||
TL2 : Message info 1: { msgNumSelected, msgNumTotal }
|
||||
*/
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message List',
|
||||
desc : 'Module for listing/browsing available messages',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MCICodesIDs = {
|
||||
MsgList : 1, // VM1
|
||||
MsgInfo1 : 2, // TL2
|
||||
};
|
||||
|
||||
exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
this.messageAreaTag = config.messageAreaTag;
|
||||
|
||||
this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false);
|
||||
|
||||
if(options.extraArgs) {
|
||||
//
|
||||
// |extraArgs| can override |messageAreaTag| provided by config
|
||||
// as well as supply a pre-defined message list
|
||||
//
|
||||
if(options.extraArgs.messageAreaTag) {
|
||||
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||
}
|
||||
|
||||
if(options.extraArgs.messageList) {
|
||||
this.messageList = options.extraArgs.messageList;
|
||||
}
|
||||
}
|
||||
|
||||
this.menuMethods = {
|
||||
selectMessage : function(formData, extraArgs, cb) {
|
||||
if(1 === formData.submitId) {
|
||||
self.initialFocusIndex = formData.value.message;
|
||||
|
||||
const modOpts = {
|
||||
extraArgs : {
|
||||
messageAreaTag : self.messageAreaTag,
|
||||
messageList : self.messageList,
|
||||
messageIndex : formData.value.message,
|
||||
lastMessageNextExit : true,
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Provide a serializer so we don't dump *huge* bits of information to the log
|
||||
// due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189
|
||||
//
|
||||
modOpts.extraArgs.toJSON = function() {
|
||||
const logMsgList = (this.messageList.length <= 4) ?
|
||||
this.messageList :
|
||||
this.messageList.slice(0, 2).concat(this.messageList.slice(-2));
|
||||
|
||||
return {
|
||||
messageAreaTag : this.messageAreaTag,
|
||||
apprevMessageList : logMsgList,
|
||||
messageCount : this.messageList.length,
|
||||
messageIndex : formData.value.message,
|
||||
};
|
||||
};
|
||||
|
||||
return self.gotoMenu(config.menuViewPost || 'messageAreaViewPost', modOpts, cb);
|
||||
} else {
|
||||
return cb(null);
|
||||
}
|
||||
},
|
||||
|
||||
fullExit : function(formData, extraArgs, cb) {
|
||||
self.menuResult = { fullExit : true };
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
enter() {
|
||||
if(this.lastMessageReachedExit) {
|
||||
return this.prevMenu();
|
||||
}
|
||||
|
||||
super.enter();
|
||||
|
||||
//
|
||||
// Config can specify |messageAreaTag| else it comes from
|
||||
// the user's current area
|
||||
//
|
||||
if(this.messageAreaTag) {
|
||||
this.tempMessageConfAndAreaSwitch(this.messageAreaTag);
|
||||
} else {
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
}
|
||||
|
||||
leave() {
|
||||
this.tempMessageConfAndAreaRestore();
|
||||
super.leave();
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function fetchMessagesInArea(callback) {
|
||||
//
|
||||
// Config can supply messages else we'll need to populate the list now
|
||||
//
|
||||
if(_.isArray(self.messageList)) {
|
||||
return callback(0 === self.messageList.length ? new Error('No messages in area') : null);
|
||||
}
|
||||
|
||||
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) {
|
||||
if(!msgList || 0 === msgList.length) {
|
||||
return callback(new Error('No messages in area'));
|
||||
}
|
||||
|
||||
self.messageList = msgList;
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function getLastReadMesageId(callback) {
|
||||
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) {
|
||||
self.lastReadId = lastReadId || 0;
|
||||
return callback(null); // ignore any errors, e.g. missing value
|
||||
});
|
||||
},
|
||||
function updateMessageListObjects(callback) {
|
||||
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM Do';
|
||||
const newIndicator = self.menuConfig.config.newIndicator || '*';
|
||||
const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues
|
||||
|
||||
let msgNum = 1;
|
||||
self.messageList.forEach( (listItem, index) => {
|
||||
listItem.msgNum = msgNum++;
|
||||
listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat);
|
||||
listItem.newIndicator = listItem.messageId > self.lastReadId ? newIndicator : regIndicator;
|
||||
|
||||
if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) {
|
||||
self.initialFocusIndex = index;
|
||||
}
|
||||
});
|
||||
return callback(null);
|
||||
},
|
||||
function populateList(callback) {
|
||||
const msgListView = vc.getView(MCICodesIDs.MsgList);
|
||||
const listFormat = self.menuConfig.config.listFormat || '{msgNum} - {subject} - {toUserName}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default change color here
|
||||
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
|
||||
|
||||
// :TODO: This can take a very long time to load large lists. What we need is to implement the "owner draw" concept in
|
||||
// which items are requested (e.g. their format at least) *as-needed* vs trying to get the format for all of them at once
|
||||
|
||||
msgListView.setItems(_.map(self.messageList, listEntry => {
|
||||
return stringFormat(listFormat, listEntry);
|
||||
}));
|
||||
|
||||
msgListView.setFocusItems(_.map(self.messageList, listEntry => {
|
||||
return stringFormat(focusListFormat, listEntry);
|
||||
}));
|
||||
|
||||
msgListView.on('index update', idx => {
|
||||
self.setViewText(
|
||||
'allViews',
|
||||
MCICodesIDs.MsgInfo1,
|
||||
stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.messageList.length } ));
|
||||
});
|
||||
|
||||
if(self.initialFocusIndex > 0) {
|
||||
// note: causes redraw()
|
||||
msgListView.setFocusItemIndex(self.initialFocusIndex);
|
||||
} else {
|
||||
msgListView.redraw();
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function drawOtherViews(callback) {
|
||||
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
|
||||
self.setViewText(
|
||||
'allViews',
|
||||
MCICodesIDs.MsgInfo1,
|
||||
stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.messageList.length } ));
|
||||
return callback(null);
|
||||
},
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.error( { error : err.message }, 'Error loading message list');
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
return { initialFocusIndex : this.initialFocusIndex };
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
if(savedState) {
|
||||
this.initialFocusIndex = savedState.initialFocusIndex;
|
||||
}
|
||||
}
|
||||
|
||||
getMenuResult() {
|
||||
return this.menuResult;
|
||||
}
|
||||
};
|
144
core/nua.js
Normal file
144
core/nua.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const User = require('./user.js');
|
||||
const theme = require('./theme.js');
|
||||
const login = require('./system_menu_method.js').login;
|
||||
const Config = require('./config.js').config;
|
||||
const messageArea = require('./message_area.js');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'NUA',
|
||||
desc : 'New User Application',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
userName : 1,
|
||||
password : 9,
|
||||
confirm : 10,
|
||||
errMsg : 11,
|
||||
};
|
||||
|
||||
exports.getModule = class NewUserAppModule extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
|
||||
this.menuMethods = {
|
||||
//
|
||||
// Validation stuff
|
||||
//
|
||||
validatePassConfirmMatch : function(data, cb) {
|
||||
const passwordView = self.viewControllers.menu.getView(MciViewIds.password);
|
||||
return cb(passwordView.getData() === data ? null : new Error('Passwords do not match'));
|
||||
},
|
||||
|
||||
viewValidationListener : function(err, cb) {
|
||||
const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg);
|
||||
let newFocusId;
|
||||
|
||||
if(err) {
|
||||
errMsgView.setText(err.message);
|
||||
err.view.clearText();
|
||||
|
||||
if(err.view.getId() === MciViewIds.confirm) {
|
||||
newFocusId = MciViewIds.password;
|
||||
self.viewControllers.menu.getView(MciViewIds.password).clearText();
|
||||
}
|
||||
} else {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
|
||||
return cb(newFocusId);
|
||||
},
|
||||
|
||||
|
||||
//
|
||||
// Submit handlers
|
||||
//
|
||||
submitApplication : function(formData, extraArgs, cb) {
|
||||
const newUser = new User();
|
||||
|
||||
newUser.username = formData.value.username;
|
||||
|
||||
//
|
||||
// We have to disable ACS checks for initial default areas as the user is not yet ready
|
||||
//
|
||||
let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
|
||||
let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
|
||||
|
||||
// can't store undefined!
|
||||
confTag = confTag || '';
|
||||
areaTag = areaTag || '';
|
||||
|
||||
newUser.properties = {
|
||||
real_name : formData.value.realName,
|
||||
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format
|
||||
sex : formData.value.sex,
|
||||
location : formData.value.location,
|
||||
affiliation : formData.value.affils,
|
||||
email_address : formData.value.email,
|
||||
web_address : formData.value.web,
|
||||
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format
|
||||
|
||||
message_conf_tag : confTag,
|
||||
message_area_tag : areaTag,
|
||||
|
||||
term_height : self.client.term.termHeight,
|
||||
term_width : self.client.term.termWidth,
|
||||
|
||||
// :TODO: Other defaults
|
||||
// :TODO: should probably have a place to create defaults/etc.
|
||||
};
|
||||
|
||||
if('*' === Config.defaults.theme) {
|
||||
newUser.properties.theme_id = theme.getRandomTheme();
|
||||
} else {
|
||||
newUser.properties.theme_id = Config.defaults.theme;
|
||||
}
|
||||
|
||||
// :TODO: User.create() should validate email uniqueness!
|
||||
newUser.create(formData.value.password, err => {
|
||||
if(err) {
|
||||
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
|
||||
|
||||
self.gotoMenu(extraArgs.error, err => {
|
||||
if(err) {
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
} else {
|
||||
self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created');
|
||||
|
||||
// Cache SysOp information now
|
||||
// :TODO: Similar to bbs.js. DRY
|
||||
if(newUser.isSysOp()) {
|
||||
Config.general.sysOp = {
|
||||
username : formData.value.username,
|
||||
properties : newUser.properties,
|
||||
};
|
||||
}
|
||||
|
||||
if(User.AccountStatus.inactive === self.client.user.properties.account_status) {
|
||||
return self.gotoMenu(extraArgs.inactive, cb);
|
||||
} else {
|
||||
//
|
||||
// If active now, we need to call login() to authenticate
|
||||
//
|
||||
return login(self, formData, extraArgs, cb);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
return this.standardMCIReadyHandler(mciData, cb);
|
||||
}
|
||||
};
|
338
core/onelinerz.js
Normal file
338
core/onelinerz.js
Normal file
|
@ -0,0 +1,338 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
|
||||
const {
|
||||
getModDatabasePath,
|
||||
getTransactionDatabase
|
||||
} = require('./database.js');
|
||||
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const theme = require('./theme.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const sqlite3 = require('sqlite3');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
/*
|
||||
Module :TODO:
|
||||
* Add pipe code support
|
||||
- override max length & monitor *display* len as user types in order to allow for actual display len with color
|
||||
* Add preview control: Shows preview with pipe codes resolved
|
||||
* Add ability to at least alternate formatStrings -- every other
|
||||
*/
|
||||
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Onelinerz',
|
||||
desc : 'Standard local onelinerz',
|
||||
author : 'NuSkooler',
|
||||
packageName : 'codes.l33t.enigma.onelinerz',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
ViewForm : {
|
||||
Entries : 1,
|
||||
AddPrompt : 2,
|
||||
},
|
||||
AddForm : {
|
||||
NewEntry : 1,
|
||||
EntryPreview : 2,
|
||||
AddPrompt : 3,
|
||||
}
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
View : 0,
|
||||
Add : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class OnelinerzModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
|
||||
this.menuMethods = {
|
||||
viewAddScreen : function(formData, extraArgs, cb) {
|
||||
return self.displayAddScreen(cb);
|
||||
},
|
||||
|
||||
addEntry : function(formData, extraArgs, cb) {
|
||||
if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) {
|
||||
const oneliner = formData.value.oneliner.trim(); // remove any trailing ws
|
||||
|
||||
self.storeNewOneliner(oneliner, err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.message }, 'Failed saving oneliner');
|
||||
}
|
||||
|
||||
self.clearAddForm();
|
||||
return self.displayViewScreen(true, cb); // true=cls
|
||||
});
|
||||
|
||||
} else {
|
||||
// empty message - treat as if cancel was hit
|
||||
return self.displayViewScreen(true, cb); // true=cls
|
||||
}
|
||||
},
|
||||
|
||||
cancelAdd : function(formData, extraArgs, cb) {
|
||||
self.clearAddForm();
|
||||
return self.displayViewScreen(true, cb); // true=cls
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
async.series(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayViewScreen(false, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
// :TODO: Handle me -- initSequence() should really take a completion callback
|
||||
}
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayViewScreen(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
if(self.viewControllers.add) {
|
||||
self.viewControllers.add.setFocus(false);
|
||||
}
|
||||
|
||||
if(clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.menuConfig.config.art.entries,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'view',
|
||||
new ViewController( { client : self.client, formId : FormIds.View } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.View,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.view.setFocus(true);
|
||||
self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw();
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function fetchEntries(callback) {
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries);
|
||||
const limit = entriesView.dimens.height;
|
||||
let entries = [];
|
||||
|
||||
self.db.each(
|
||||
`SELECT *
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM onelinerz
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ${limit}
|
||||
)
|
||||
ORDER BY timestamp ASC;`,
|
||||
(err, row) => {
|
||||
if(!err) {
|
||||
row.timestamp = moment(row.timestamp); // convert -> moment
|
||||
entries.push(row);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return callback(err, entriesView, entries);
|
||||
}
|
||||
);
|
||||
},
|
||||
function populateEntries(entriesView, entries, callback) {
|
||||
const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent
|
||||
const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma';
|
||||
|
||||
entriesView.setItems(entries.map( e => {
|
||||
return stringFormat(listFormat, {
|
||||
userId : e.user_id,
|
||||
username : e.user_name,
|
||||
oneliner : e.oneliner,
|
||||
ts : e.timestamp.format(tsFormat),
|
||||
} );
|
||||
}));
|
||||
|
||||
entriesView.redraw();
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function finalPrep(callback) {
|
||||
const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt);
|
||||
promptView.setFocusItemIndex(1); // default to NO
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayAddScreen(cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
self.viewControllers.view.setFocus(false);
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.menuConfig.config.art.add,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'add',
|
||||
new ViewController( { client : self.client, formId : FormIds.Add } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.Add,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.add.setFocus(true);
|
||||
self.viewControllers.add.redrawAll();
|
||||
self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
clearAddForm() {
|
||||
this.setViewText('add', MciViewIds.AddForm.NewEntry, '');
|
||||
this.setViewText('add', MciViewIds.AddForm.EntryPreview, '');
|
||||
}
|
||||
|
||||
initDatabase(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function openDatabase(callback) {
|
||||
self.db = getTransactionDatabase(new sqlite3.Database(
|
||||
getModDatabasePath(exports.moduleInfo),
|
||||
err => {
|
||||
return callback(err);
|
||||
}
|
||||
));
|
||||
},
|
||||
function createTables(callback) {
|
||||
self.db.run(
|
||||
`CREATE TABLE IF NOT EXISTS onelinerz (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER_NOT NULL,
|
||||
user_name VARCHAR NOT NULL,
|
||||
oneliner VARCHAR NOT NULL,
|
||||
timestamp DATETIME NOT NULL
|
||||
);`
|
||||
,
|
||||
err => {
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
storeNewOneliner(oneliner, cb) {
|
||||
const self = this;
|
||||
const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
|
||||
async.series(
|
||||
[
|
||||
function addRec(callback) {
|
||||
self.db.run(
|
||||
`INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp)
|
||||
VALUES (?, ?, ?, ?);`,
|
||||
[ self.client.user.userId, self.client.user.username, oneliner, ts ],
|
||||
callback
|
||||
);
|
||||
},
|
||||
function removeOld(callback) {
|
||||
// keep 25 max most recent items - remove the older ones
|
||||
self.db.run(
|
||||
`DELETE FROM onelinerz
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM onelinerz
|
||||
ORDER BY id DESC
|
||||
LIMIT -1 OFFSET 25
|
||||
);`,
|
||||
callback
|
||||
);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
beforeArt(cb) {
|
||||
super.beforeArt(err => {
|
||||
return err ? cb(err) : this.initDatabase(cb);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -45,11 +45,12 @@ function printUsageAndSetExitCode(errMsg, exitCode) {
|
|||
}
|
||||
|
||||
function getDefaultConfigPath() {
|
||||
return resolvePath('~/.config/enigma-bbs/config.hjson');
|
||||
return './config/';
|
||||
}
|
||||
|
||||
function getConfigPath() {
|
||||
return argv.config ? argv.config : config.getDefaultPath();
|
||||
const baseConfigPath = argv.config ? argv.config : config.getDefaultPath();
|
||||
return baseConfigPath + 'config.hjson';
|
||||
}
|
||||
|
||||
function initConfig(cb) {
|
||||
|
|
247
core/rumorz.js
Normal file
247
core/rumorz.js
Normal file
|
@ -0,0 +1,247 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const theme = require('./theme.js');
|
||||
const resetScreen = require('./ansi_term.js').resetScreen;
|
||||
const StatLog = require('./stat_log.js');
|
||||
const renderStringLength = require('./string_util.js').renderStringLength;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Rumorz',
|
||||
desc : 'Standard local rumorz',
|
||||
author : 'NuSkooler',
|
||||
packageName : 'codes.l33t.enigma.rumorz',
|
||||
};
|
||||
|
||||
const STATLOG_KEY_RUMORZ = 'system_rumorz';
|
||||
|
||||
const FormIds = {
|
||||
View : 0,
|
||||
Add : 1,
|
||||
};
|
||||
|
||||
const MciCodeIds = {
|
||||
ViewForm : {
|
||||
Entries : 1,
|
||||
AddPrompt : 2,
|
||||
},
|
||||
AddForm : {
|
||||
NewEntry : 1,
|
||||
EntryPreview : 2,
|
||||
AddPrompt : 3,
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class RumorzModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.menuMethods = {
|
||||
viewAddScreen : (formData, extraArgs, cb) => {
|
||||
return this.displayAddScreen(cb);
|
||||
},
|
||||
|
||||
addEntry : (formData, extraArgs, cb) => {
|
||||
if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) {
|
||||
const rumor = formData.value.rumor.trim(); // remove any trailing ws
|
||||
|
||||
StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => {
|
||||
this.clearAddForm();
|
||||
return this.displayViewScreen(true, cb); // true=cls
|
||||
});
|
||||
} else {
|
||||
// empty message - treat as if cancel was hit
|
||||
return this.displayViewScreen(true, cb); // true=cls
|
||||
}
|
||||
},
|
||||
|
||||
cancelAdd : (formData, extraArgs, cb) => {
|
||||
this.clearAddForm();
|
||||
return this.displayViewScreen(true, cb); // true=cls
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get config() { return this.menuConfig.config; }
|
||||
|
||||
clearAddForm() {
|
||||
const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
|
||||
const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
|
||||
|
||||
newEntryView.setText('');
|
||||
|
||||
// preview is optional
|
||||
if(previewView) {
|
||||
previewView.setText('');
|
||||
}
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
self.displayViewScreen(false, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
// :TODO: Handle me -- initSequence() should really take a completion callback
|
||||
}
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayViewScreen(clearScreen, cb) {
|
||||
const self = this;
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
if(self.viewControllers.add) {
|
||||
self.viewControllers.add.setFocus(false);
|
||||
}
|
||||
|
||||
if(clearScreen) {
|
||||
self.client.term.rawWrite(resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.config.art.entries,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'view',
|
||||
new ViewController( { client : self.client, formId : FormIds.View } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.View,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.view.setFocus(true);
|
||||
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function fetchEntries(callback) {
|
||||
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
|
||||
|
||||
StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => {
|
||||
return callback(err, entriesView, entries);
|
||||
});
|
||||
},
|
||||
function populateEntries(entriesView, entries, callback) {
|
||||
const config = self.config;
|
||||
const listFormat = config.listFormat || '{rumor}';
|
||||
const focusListFormat = config.focusListFormat || listFormat;
|
||||
|
||||
entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) );
|
||||
entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) );
|
||||
entriesView.redraw();
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function finalPrep(callback) {
|
||||
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
|
||||
promptView.setFocusItemIndex(1); // default to NO
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayAddScreen(cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
self.viewControllers.view.setFocus(false);
|
||||
self.client.term.rawWrite(resetScreen());
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.config.art.add,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'add',
|
||||
new ViewController( { client : self.client, formId : FormIds.Add } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.Add,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.add.setFocus(true);
|
||||
self.viewControllers.add.redrawAll();
|
||||
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function initPreviewUpdates(callback) {
|
||||
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
|
||||
const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
|
||||
if(previewView) {
|
||||
let timerId;
|
||||
entryView.on('key press', () => {
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout( () => {
|
||||
const focused = self.viewControllers.add.getFocusedView();
|
||||
if(focused === entryView) {
|
||||
previewView.setText(entryView.getData());
|
||||
focused.setFocus(true);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
198
core/telnet_bridge.js
Normal file
198
core/telnet_bridge.js
Normal file
|
@ -0,0 +1,198 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const resetScreen = require('./ansi_term.js').resetScreen;
|
||||
const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const net = require('net');
|
||||
const EventEmitter = require('events');
|
||||
const buffers = require('buffers');
|
||||
|
||||
/*
|
||||
Expected configuration block:
|
||||
|
||||
{
|
||||
module: telnet_bridge
|
||||
...
|
||||
config: {
|
||||
host: somehost.net
|
||||
port: 23
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// :TODO: ENH: Support nodeMax and tooManyArt
|
||||
exports.moduleInfo = {
|
||||
name : 'Telnet Bridge',
|
||||
desc : 'Connect to other Telnet Systems',
|
||||
author : 'Andrew Pamment',
|
||||
};
|
||||
|
||||
const IAC_DO_TERM_TYPE = new Buffer( [ 255, 253, 24 ] );
|
||||
|
||||
class TelnetClientConnection extends EventEmitter {
|
||||
constructor(client) {
|
||||
super();
|
||||
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
|
||||
restorePipe() {
|
||||
if(!this.pipeRestored) {
|
||||
this.pipeRestored = true;
|
||||
|
||||
// client may have bailed
|
||||
if(_.has(this, 'client.term.output')) {
|
||||
if(this.bridgeConnection) {
|
||||
this.client.term.output.unpipe(this.bridgeConnection);
|
||||
}
|
||||
this.client.term.output.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect(connectOpts) {
|
||||
this.bridgeConnection = net.createConnection(connectOpts, () => {
|
||||
this.emit('connected');
|
||||
|
||||
this.pipeRestored = false;
|
||||
this.client.term.output.pipe(this.bridgeConnection);
|
||||
});
|
||||
|
||||
this.bridgeConnection.on('data', data => {
|
||||
this.client.term.rawWrite(data);
|
||||
|
||||
//
|
||||
// Wait for a terminal type request, and send it eactly once.
|
||||
// This is enough (in additional to other negotiations handled in telnet.js)
|
||||
// to get us in on most systems
|
||||
//
|
||||
if(!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) {
|
||||
this.termSent = true;
|
||||
this.bridgeConnection.write(this.getTermTypeNegotiationBuffer());
|
||||
}
|
||||
});
|
||||
|
||||
this.bridgeConnection.once('end', () => {
|
||||
this.restorePipe();
|
||||
this.emit('end');
|
||||
});
|
||||
|
||||
this.bridgeConnection.once('error', err => {
|
||||
this.restorePipe();
|
||||
this.emit('end', err);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if(this.bridgeConnection) {
|
||||
this.bridgeConnection.end();
|
||||
}
|
||||
}
|
||||
|
||||
getTermTypeNegotiationBuffer() {
|
||||
//
|
||||
// Create a TERMINAL-TYPE sub negotiation buffer using the
|
||||
// actual/current terminal type.
|
||||
//
|
||||
let bufs = buffers();
|
||||
|
||||
bufs.push(new Buffer(
|
||||
[
|
||||
255, // IAC
|
||||
250, // SB
|
||||
24, // TERMINAL-TYPE
|
||||
0, // IS
|
||||
]
|
||||
));
|
||||
|
||||
bufs.push(
|
||||
new Buffer(this.client.term.termType), // e.g. "ansi"
|
||||
new Buffer( [ 255, 240 ] ) // IAC, SE
|
||||
);
|
||||
|
||||
return bufs.toBuffer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.getModule = class TelnetBridgeModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = options.menuConfig.config;
|
||||
// defaults
|
||||
this.config.port = this.config.port || 23;
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
let clientTerminated;
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(_.isString(self.config.host) &&
|
||||
_.isNumber(self.config.port))
|
||||
{
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error('Configuration is missing required option(s)'));
|
||||
}
|
||||
},
|
||||
function createTelnetBridge(callback) {
|
||||
const connectOpts = {
|
||||
port : self.config.port,
|
||||
host : self.config.host,
|
||||
};
|
||||
|
||||
let clientTerminated;
|
||||
|
||||
self.client.term.write(resetScreen());
|
||||
self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`);
|
||||
|
||||
const telnetConnection = new TelnetClientConnection(self.client);
|
||||
|
||||
telnetConnection.on('connected', () => {
|
||||
self.client.log.info(connectOpts, 'Telnet bridge connection established');
|
||||
|
||||
if(self.config.font) {
|
||||
self.client.term.rawWrite(setSyncTermFontWithAlias(self.config.font));
|
||||
}
|
||||
|
||||
self.client.once('end', () => {
|
||||
self.client.log.info('Connection ended. Terminating connection');
|
||||
clientTerminated = true;
|
||||
telnetConnection.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
telnetConnection.on('end', err => {
|
||||
if(err) {
|
||||
self.client.log.info(`Telnet bridge connection error: ${err.message}`);
|
||||
}
|
||||
|
||||
callback(clientTerminated ? new Error('Client connection terminated') : null);
|
||||
});
|
||||
|
||||
telnetConnection.connect(connectOpts);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.message }, 'Telnet connection error');
|
||||
}
|
||||
|
||||
if(!clientTerminated) {
|
||||
self.prevMenu();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
727
core/upload.js
Normal file
727
core/upload.js
Normal file
|
@ -0,0 +1,727 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||
const getAreaDefaultStorageDirectory = require('./file_base_area.js').getAreaDefaultStorageDirectory;
|
||||
const scanFile = require('./file_base_area.js').scanFile;
|
||||
const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag;
|
||||
const getDescFromFileName = require('./file_base_area.js').getDescFromFileName;
|
||||
const ansiGoto = require('./ansi_term.js').goto;
|
||||
const moveFileWithCollisionHandling = require('./file_util.js').moveFileWithCollisionHandling;
|
||||
const pathWithTerminatingSeparator = require('./file_util.js').pathWithTerminatingSeparator;
|
||||
const Log = require('./logger.js').log;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const isAnsi = require('./string_util.js').isAnsi;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const temptmp = require('temptmp').createTrackedSession('upload');
|
||||
const paths = require('path');
|
||||
const sanatizeFilename = require('sanitize-filename');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Upload',
|
||||
desc : 'Module for classic file uploads',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
options : 0,
|
||||
processing : 1,
|
||||
fileDetails : 2,
|
||||
dupes : 3,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
options : {
|
||||
area : 1, // area selection
|
||||
uploadType : 2, // blind vs specify filename
|
||||
fileName : 3, // for non-blind; not editable for blind
|
||||
navMenu : 4, // next/cancel/etc.
|
||||
errMsg : 5, // errors (e.g. filename cannot be blank)
|
||||
},
|
||||
|
||||
processing : {
|
||||
calcHashIndicator : 1,
|
||||
archiveListIndicator : 2,
|
||||
descFileIndicator : 3,
|
||||
logStep : 4,
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
|
||||
fileDetails : {
|
||||
desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ)
|
||||
tags : 2, // tag(s) for item
|
||||
estYear : 3,
|
||||
accept : 4, // accept fields & continue
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
|
||||
dupes : {
|
||||
dupeList : 1,
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class UploadModule extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
|
||||
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
||||
}
|
||||
|
||||
this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs : true } );
|
||||
|
||||
this.menuMethods = {
|
||||
optionsNavContinue : (formData, extraArgs, cb) => {
|
||||
return this.performUpload(cb);
|
||||
},
|
||||
|
||||
fileDetailsContinue : (formData, extraArgs, cb) => {
|
||||
// see displayFileDetailsPageForUploadEntry() for this hackery:
|
||||
cb(null);
|
||||
return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any
|
||||
},
|
||||
|
||||
// validation
|
||||
validateNonBlindFileName : (fileName, cb) => {
|
||||
fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc.
|
||||
if(0 === fileName.length) {
|
||||
return cb(new Error('Invalid filename'));
|
||||
}
|
||||
|
||||
if(0 === fileName.length) {
|
||||
return cb(new Error('Filename cannot be empty'));
|
||||
}
|
||||
|
||||
// At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused
|
||||
if(/^[0-9].*$/.test(fileName)) {
|
||||
return cb(new Error('Invalid filename'));
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
viewValidationListener : (err, cb) => {
|
||||
const errView = this.viewControllers.options.getView(MciViewIds.options.errMsg);
|
||||
if(errView) {
|
||||
if(err) {
|
||||
errView.setText(err.message);
|
||||
} else {
|
||||
errView.clearText();
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
// if no areas, we're falling back due to lack of access/areas avail to upload to
|
||||
if(this.availAreas.length > 0) {
|
||||
return {
|
||||
uploadType : this.uploadType,
|
||||
tempRecvDirectory : this.tempRecvDirectory,
|
||||
areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
if(savedState.areaInfo) {
|
||||
this.uploadType = savedState.uploadType;
|
||||
this.areaInfo = savedState.areaInfo;
|
||||
this.tempRecvDirectory = savedState.tempRecvDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
isBlindUpload() { return 'blind' === this.uploadType; }
|
||||
isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); }
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
if(0 === this.availAreas.length) {
|
||||
//
|
||||
return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail');
|
||||
}
|
||||
|
||||
async.series(
|
||||
[
|
||||
function before(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
if(self.isFileTransferComplete()) {
|
||||
return self.displayProcessingPage(callback);
|
||||
} else {
|
||||
return self.displayOptionsPage(callback);
|
||||
}
|
||||
}
|
||||
],
|
||||
() => {
|
||||
return self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
finishedLoading() {
|
||||
if(this.isFileTransferComplete()) {
|
||||
return this.processUploadedFiles();
|
||||
}
|
||||
}
|
||||
|
||||
performUpload(cb) {
|
||||
temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// need a terminator for various external protocols
|
||||
this.tempRecvDirectory = pathWithTerminatingSeparator(tempRecvDirectory);
|
||||
|
||||
const modOpts = {
|
||||
extraArgs : {
|
||||
recvDirectory : this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed
|
||||
direction : 'recv',
|
||||
}
|
||||
};
|
||||
|
||||
if(!this.isBlindUpload()) {
|
||||
// data has been sanatized at this point
|
||||
modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData();
|
||||
}
|
||||
|
||||
//
|
||||
// Move along to protocol selection -> file transfer
|
||||
// Upon completion, we'll re-enter the module with some file paths handed to us
|
||||
//
|
||||
return this.gotoMenu(
|
||||
this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection',
|
||||
modOpts,
|
||||
cb
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
continueNonBlindUpload(cb) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
updateScanStepInfoViews(stepInfo) {
|
||||
// :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC
|
||||
|
||||
const fmtObj = Object.assign( {}, stepInfo);
|
||||
let stepIndicatorFmt = '';
|
||||
let logStepFmt;
|
||||
|
||||
const fmtConfig = this.menuConfig.config;
|
||||
|
||||
const indicatorStates = fmtConfig.indicatorStates || [ '|', '/', '-', '\\' ];
|
||||
const indicatorFinished = fmtConfig.indicatorFinished || '√';
|
||||
|
||||
const indicator = { };
|
||||
const self = this;
|
||||
|
||||
function updateIndicator(mci, isFinished) {
|
||||
indicator.mci = mci;
|
||||
|
||||
if(isFinished) {
|
||||
indicator.text = indicatorFinished;
|
||||
} else {
|
||||
self.scanStatus.indicatorPos += 1;
|
||||
if(self.scanStatus.indicatorPos >= indicatorStates.length) {
|
||||
self.scanStatus.indicatorPos = 0;
|
||||
}
|
||||
indicator.text = indicatorStates[self.scanStatus.indicatorPos];
|
||||
}
|
||||
}
|
||||
|
||||
switch(stepInfo.step) {
|
||||
case 'start' :
|
||||
logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Scanning {fileName}';
|
||||
break;
|
||||
|
||||
case 'hash_update' :
|
||||
stepIndicatorFmt = fmtConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%';
|
||||
updateIndicator(MciViewIds.processing.calcHashIndicator);
|
||||
break;
|
||||
|
||||
case 'hash_finish' :
|
||||
stepIndicatorFmt = fmtConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums';
|
||||
updateIndicator(MciViewIds.processing.calcHashIndicator, true);
|
||||
break;
|
||||
|
||||
case 'archive_list_start' :
|
||||
stepIndicatorFmt = fmtConfig.extractArchiveListFormat || 'Extracting archive list';
|
||||
updateIndicator(MciViewIds.processing.archiveListIndicator);
|
||||
break;
|
||||
|
||||
case 'archive_list_finish' :
|
||||
fmtObj.archivedFileCount = stepInfo.archiveEntries.length;
|
||||
stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)';
|
||||
updateIndicator(MciViewIds.processing.archiveListIndicator, true);
|
||||
break;
|
||||
|
||||
case 'archive_list_failed' :
|
||||
stepIndicatorFmt = fmtConfig.extractArchiveListFailedFormat || 'Archive list extraction failed';
|
||||
break;
|
||||
|
||||
case 'desc_files_start' :
|
||||
stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files';
|
||||
updateIndicator(MciViewIds.processing.descFileIndicator);
|
||||
break;
|
||||
|
||||
case 'desc_files_finish' :
|
||||
stepIndicatorFmt = fmtConfig.processingDescFilesFinishFormat || 'Finished processing description files';
|
||||
updateIndicator(MciViewIds.processing.descFileIndicator, true);
|
||||
break;
|
||||
|
||||
case 'finished' :
|
||||
logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Finished';
|
||||
break;
|
||||
}
|
||||
|
||||
fmtObj.stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj);
|
||||
|
||||
if(this.hasProcessingArt) {
|
||||
this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj, { appendMultiLine : true } );
|
||||
|
||||
if(indicator.mci && indicator.text) {
|
||||
this.setViewText('processing', indicator.mci, indicator.text);
|
||||
}
|
||||
|
||||
if(logStepFmt) {
|
||||
this.setViewText('processing', MciViewIds.processing.logStep, stringFormat(logStepFmt, fmtObj), { appendMultiLine : true } );
|
||||
}
|
||||
} else {
|
||||
this.client.term.pipeWrite(fmtObj.stepIndicatorText);
|
||||
}
|
||||
}
|
||||
|
||||
scanFiles(cb) {
|
||||
const self = this;
|
||||
|
||||
const results = {
|
||||
newEntries : [],
|
||||
dupes : [],
|
||||
};
|
||||
|
||||
self.client.log.debug('Scanning upload(s)', { paths : this.recvFilePaths } );
|
||||
|
||||
let currentFileNum = 0;
|
||||
|
||||
async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => {
|
||||
// :TODO: virus scanning/etc. should occur around here
|
||||
|
||||
currentFileNum += 1;
|
||||
|
||||
self.scanStatus = {
|
||||
indicatorPos : 0,
|
||||
};
|
||||
|
||||
const scanOpts = {
|
||||
areaTag : self.areaInfo.areaTag,
|
||||
storageTag : self.areaInfo.storageTags[0],
|
||||
};
|
||||
|
||||
function handleScanStep(stepInfo, nextScanStep) {
|
||||
stepInfo.totalFileNum = self.recvFilePaths.length;
|
||||
stepInfo.currentFileNum = currentFileNum;
|
||||
|
||||
self.updateScanStepInfoViews(stepInfo);
|
||||
return nextScanStep(null);
|
||||
}
|
||||
|
||||
self.client.log.debug('Scanning file', { filePath : filePath } );
|
||||
|
||||
scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => {
|
||||
if(err) {
|
||||
return nextFilePath(err);
|
||||
}
|
||||
|
||||
// new or dupe?
|
||||
if(dupeEntries.length > 0) {
|
||||
// 1:n dupes found
|
||||
self.client.log.debug('Duplicate file(s) found', { dupeEntries : dupeEntries } );
|
||||
|
||||
results.dupes = results.dupes.concat(dupeEntries);
|
||||
} else {
|
||||
// new one
|
||||
results.newEntries.push(fileEntry);
|
||||
}
|
||||
|
||||
return nextFilePath(null);
|
||||
});
|
||||
}, err => {
|
||||
return cb(err, results);
|
||||
});
|
||||
}
|
||||
|
||||
cleanupTempFiles() {
|
||||
temptmp.cleanup( paths => {
|
||||
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' );
|
||||
});
|
||||
}
|
||||
|
||||
moveAndPersistUploadsToDatabase(newEntries) {
|
||||
|
||||
const areaStorageDir = getAreaDefaultStorageDirectory(this.areaInfo);
|
||||
const self = this;
|
||||
|
||||
async.eachSeries(newEntries, (newEntry, nextEntry) => {
|
||||
const src = paths.join(self.tempRecvDirectory, newEntry.fileName);
|
||||
const dst = paths.join(areaStorageDir, newEntry.fileName);
|
||||
|
||||
moveFileWithCollisionHandling(src, dst, (err, finalPath) => {
|
||||
if(err) {
|
||||
self.client.log.error(
|
||||
'Failed moving physical upload file', { error : err.message, fileName : newEntry.fileName, source : src, dest : dst }
|
||||
);
|
||||
|
||||
if(dst !== finalPath) {
|
||||
// name changed; ajust before persist
|
||||
newEntry.fileName = paths.basename(finalPath);
|
||||
}
|
||||
|
||||
return nextEntry(null); // still try next file
|
||||
}
|
||||
|
||||
self.client.log.debug('Moved upload to area', { path : finalPath } );
|
||||
|
||||
// persist to DB
|
||||
newEntry.persist(err => {
|
||||
if(err) {
|
||||
self.client.log.error('Failed persisting upload to database', { path : finalPath, error : err.message } );
|
||||
}
|
||||
|
||||
return nextEntry(null); // still try next file
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
//
|
||||
// Finally, we can remove any temp files that we may have created
|
||||
//
|
||||
self.cleanupTempFiles();
|
||||
});
|
||||
}
|
||||
|
||||
prepDetailsForUpload(scanResults, cb) {
|
||||
async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => {
|
||||
newEntry.meta.upload_by_username = this.client.user.username;
|
||||
newEntry.meta.upload_by_user_id = this.client.user.userId;
|
||||
|
||||
this.displayFileDetailsPageForUploadEntry(newEntry, (err, newValues) => {
|
||||
if(err) {
|
||||
return nextEntry(err);
|
||||
}
|
||||
|
||||
if(!newEntry.descIsAnsi) {
|
||||
newEntry.desc = _.trimEnd(newValues.shortDesc);
|
||||
}
|
||||
|
||||
if(newValues.estYear.length > 0) {
|
||||
newEntry.meta.est_release_year = newValues.estYear;
|
||||
}
|
||||
|
||||
if(newValues.tags.length > 0) {
|
||||
newEntry.setHashTags(newValues.tags);
|
||||
}
|
||||
|
||||
return nextEntry(err);
|
||||
});
|
||||
}, err => {
|
||||
delete this.fileDetailsCurrentEntrySubmitCallback;
|
||||
return cb(err, scanResults);
|
||||
});
|
||||
}
|
||||
|
||||
displayDupesPage(dupes, cb) {
|
||||
//
|
||||
// If we have custom art to show, use it - else just dump basic info.
|
||||
// Pause at the end in either case.
|
||||
//
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
self.prepViewControllerWithArt(
|
||||
'dupes',
|
||||
FormIds.dupes,
|
||||
{ clearScreen : true, trailingLF : false },
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.term.pipeWrite('|00|07Duplicate upload(s) found:\n');
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
const dupeListView = self.viewControllers.dupes.getView(MciViewIds.dupes.dupeList);
|
||||
return callback(null, dupeListView);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepDupeObjects(dupeListView, callback) {
|
||||
// update dupe objects with additional info that can be used for formatString() and the like
|
||||
async.each(dupes, (dupe, nextDupe) => {
|
||||
FileEntry.loadBasicEntry(dupe.fileId, dupe, err => {
|
||||
if(err) {
|
||||
return nextDupe(err);
|
||||
}
|
||||
|
||||
const areaInfo = getFileAreaByTag(dupe.areaTag);
|
||||
if(areaInfo) {
|
||||
dupe.areaName = areaInfo.name;
|
||||
dupe.areaDesc = areaInfo.desc;
|
||||
}
|
||||
return nextDupe(null);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err, dupeListView);
|
||||
});
|
||||
},
|
||||
function populateDupeInfo(dupeListView, callback) {
|
||||
const dupeInfoFormat = self.menuConfig.config.dupeInfoFormat || '{fileName} @ {areaName}';
|
||||
|
||||
if(dupeListView) {
|
||||
dupeListView.setItems(dupes.map(dupe => stringFormat(dupeInfoFormat, dupe) ) );
|
||||
dupeListView.redraw();
|
||||
} else {
|
||||
dupes.forEach(dupe => {
|
||||
self.client.term.pipeWrite(`${stringFormat(dupeInfoFormat, dupe)}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function pause(callback) {
|
||||
return self.pausePrompt( { row : self.client.term.termHeight }, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
processUploadedFiles() {
|
||||
//
|
||||
// For each file uploaded, we need to process & gather information
|
||||
//
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function prepNonBlind(callback) {
|
||||
if(self.isBlindUpload()) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
//
|
||||
// For non-blind uploads, batch is not supported, we expect a single file
|
||||
// in |recvFilePaths|. If not, it's an error (we don't want to process the wrong thing)
|
||||
//
|
||||
if(self.recvFilePaths.length > 1) {
|
||||
self.client.log.warn( { recvFilePaths : self.recvFilePaths }, 'Non-blind upload received 2:n files' );
|
||||
return callback(Errors.UnexpectedState(`Non-blind upload expected single file but got received ${self.recvFilePaths.length}`));
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function scan(callback) {
|
||||
return self.scanFiles(callback);
|
||||
},
|
||||
function pause(scanResults, callback) {
|
||||
if(self.hasProcessingArt) {
|
||||
self.client.term.rawWrite(ansiGoto(self.client.term.termHeight, 1));
|
||||
} else {
|
||||
self.client.term.write('\n');
|
||||
}
|
||||
|
||||
self.pausePrompt( () => {
|
||||
return callback(null, scanResults);
|
||||
});
|
||||
},
|
||||
function displayDupes(scanResults, callback) {
|
||||
if(0 === scanResults.dupes.length) {
|
||||
return callback(null, scanResults);
|
||||
}
|
||||
|
||||
return self.displayDupesPage(scanResults.dupes, () => {
|
||||
return callback(null, scanResults);
|
||||
});
|
||||
},
|
||||
function prepDetails(scanResults, callback) {
|
||||
return self.prepDetailsForUpload(scanResults, callback);
|
||||
},
|
||||
function startMovingAndPersistingToDatabase(scanResults, callback) {
|
||||
//
|
||||
// *Start* the process of moving files from their current |tempRecvDirectory|
|
||||
// locations -> their final area destinations. Don't make the user wait
|
||||
// here as I/O can take quite a bit of time. Log any failures.
|
||||
//
|
||||
self.moveAndPersistUploadsToDatabase(scanResults.newEntries);
|
||||
return callback(null);
|
||||
},
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn('File upload error encountered', { error : err.message } );
|
||||
self.cleanupTempFiles(); // normally called after moveAndPersistUploadsToDatabase() is completed.
|
||||
}
|
||||
|
||||
return self.prevMenu();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayOptionsPage(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.prepViewControllerWithArt(
|
||||
'options',
|
||||
FormIds.options,
|
||||
{ clearScreen : true, trailingLF : false },
|
||||
callback
|
||||
);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
const areaSelectView = self.viewControllers.options.getView(MciViewIds.options.area);
|
||||
areaSelectView.setItems( self.availAreas.map(areaInfo => areaInfo.name ) );
|
||||
|
||||
const uploadTypeView = self.viewControllers.options.getView(MciViewIds.options.uploadType);
|
||||
const fileNameView = self.viewControllers.options.getView(MciViewIds.options.fileName);
|
||||
|
||||
const blindFileNameText = self.menuConfig.config.blindFileNameText || '(blind - filename ignored)';
|
||||
|
||||
uploadTypeView.on('index update', idx => {
|
||||
self.uploadType = (0 === idx) ? 'blind' : 'non-blind';
|
||||
|
||||
if(self.isBlindUpload()) {
|
||||
fileNameView.setText(blindFileNameText);
|
||||
fileNameView.acceptsFocus = false;
|
||||
} else {
|
||||
fileNameView.clearText();
|
||||
fileNameView.acceptsFocus = true;
|
||||
}
|
||||
});
|
||||
|
||||
// sanatize filename for display when leaving the view
|
||||
self.viewControllers.options.on('leave', prevView => {
|
||||
if(prevView.id === MciViewIds.options.fileName) {
|
||||
fileNameView.setText(sanatizeFilename(fileNameView.getData()));
|
||||
}
|
||||
});
|
||||
|
||||
self.uploadType = 'blind';
|
||||
uploadTypeView.setFocusItemIndex(0); // default to blind
|
||||
fileNameView.setText(blindFileNameText);
|
||||
areaSelectView.redraw();
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayProcessingPage(cb) {
|
||||
return this.prepViewControllerWithArt(
|
||||
'processing',
|
||||
FormIds.processing,
|
||||
{ clearScreen : true, trailingLF : false },
|
||||
err => {
|
||||
// note: this art is not required
|
||||
this.hasProcessingArt = !err;
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fileEntryHasDetectedDesc(fileEntry) {
|
||||
return (fileEntry.desc && fileEntry.desc.length > 0);
|
||||
}
|
||||
|
||||
displayFileDetailsPageForUploadEntry(fileEntry, cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.prepViewControllerWithArt(
|
||||
'fileDetails',
|
||||
FormIds.fileDetails,
|
||||
{ clearScreen : true, trailingLF : false },
|
||||
err => {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
const descView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.desc);
|
||||
const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags);
|
||||
const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear);
|
||||
|
||||
self.updateCustomViewTextsWithFilter('fileDetails', MciViewIds.fileDetails.customRangeStart, fileEntry );
|
||||
|
||||
tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse
|
||||
yearView.setText(fileEntry.meta.est_release_year || '');
|
||||
|
||||
if(isAnsi(fileEntry.desc)) {
|
||||
fileEntry.descIsAnsi = true;
|
||||
|
||||
return descView.setAnsi(
|
||||
fileEntry.desc,
|
||||
{
|
||||
prepped : false,
|
||||
forceLineTerm : true,
|
||||
},
|
||||
() => {
|
||||
return callback(null, descView, 'preview', MciViewIds.fileDetails.tags);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const hasDesc = self.fileEntryHasDetectedDesc(fileEntry);
|
||||
descView.setText(
|
||||
hasDesc ? fileEntry.desc : getDescFromFileName(fileEntry.fileName),
|
||||
{ scrollMode : 'top' } // override scroll mode; we want to be @ top
|
||||
);
|
||||
return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc);
|
||||
}
|
||||
},
|
||||
function finalizeViews(descView, descViewMode, focusId, callback) {
|
||||
descView.setPropertyValue('mode', descViewMode);
|
||||
descView.acceptsFocus = 'preview' === descViewMode ? false : true;
|
||||
self.viewControllers.fileDetails.switchFocus(focusId);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
//
|
||||
// we only call |cb| here if there is an error
|
||||
// else, wait for the current from to be submit - then call -
|
||||
// this way we'll move on to the next file entry when ready
|
||||
//
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
112
core/user_list.js
Normal file
112
core/user_list.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const User = require('./user.js');
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
const moment = require('moment');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
/*
|
||||
Available listFormat/focusListFormat object members:
|
||||
|
||||
userId : User ID
|
||||
userName : User name/handle
|
||||
lastLoginTs : Last login timestamp
|
||||
status : Status: active | inactive
|
||||
location : Location
|
||||
affiliation : Affils
|
||||
note : User note
|
||||
*/
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'User List',
|
||||
desc : 'Lists all system users',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
UserList : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class UserListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
let userList = [];
|
||||
|
||||
const USER_LIST_OPTS = {
|
||||
properties : [ 'location', 'affiliation', 'last_login_timestamp' ],
|
||||
};
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
var loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu,
|
||||
};
|
||||
|
||||
vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function fetchUserList(callback) {
|
||||
// :TODO: Currently fetching all users - probably always OK, but this could be paged
|
||||
User.getUserList(USER_LIST_OPTS, function got(err, ul) {
|
||||
userList = ul;
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
function populateList(callback) {
|
||||
var userListView = vc.getView(MciViewIds.UserList);
|
||||
|
||||
var listFormat = self.menuConfig.config.listFormat || '{userName} - {affils}';
|
||||
var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default changed color!
|
||||
var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
|
||||
|
||||
function getUserFmtObj(ue) {
|
||||
return {
|
||||
userId : ue.userId,
|
||||
userName : ue.userName,
|
||||
affils : ue.affiliation,
|
||||
location : ue.location,
|
||||
// :TODO: the rest!
|
||||
note : ue.note || '',
|
||||
lastLoginTs : moment(ue.last_login_timestamp).format(dateTimeFormat),
|
||||
};
|
||||
}
|
||||
|
||||
userListView.setItems(_.map(userList, function formatUserEntry(ue) {
|
||||
return stringFormat(listFormat, getUserFmtObj(ue));
|
||||
}));
|
||||
|
||||
userListView.setFocusItems(_.map(userList, function formatUserEntry(ue) {
|
||||
return stringFormat(focusListFormat, getUserFmtObj(ue));
|
||||
}));
|
||||
|
||||
userListView.redraw();
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
self.client.log.error( { error : err.toString() }, 'Error loading user list');
|
||||
}
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
84
core/whos_online.js
Normal file
84
core/whos_online.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const getActiveNodeList = require('./client_connections.js').getActiveNodeList;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Who\'s Online',
|
||||
desc : 'Who is currently online',
|
||||
author : 'NuSkooler',
|
||||
packageName : 'codes.l33t.enigma.whosonline'
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
OnlineList : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class WhosOnlineModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu,
|
||||
noInput : true,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function populateList(callback) {
|
||||
const onlineListView = vc.getView(MciViewIds.OnlineList);
|
||||
const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}';
|
||||
const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In';
|
||||
const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A';
|
||||
const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height);
|
||||
|
||||
onlineListView.setItems(_.map(onlineList, oe => {
|
||||
if(oe.authenticated) {
|
||||
oe.timeOn = _.upperFirst(oe.timeOn.humanize());
|
||||
} else {
|
||||
[ 'realName', 'location', 'affils', 'timeOn' ].forEach(m => {
|
||||
oe[m] = otherUnknown;
|
||||
});
|
||||
oe.userName = nonAuthUser;
|
||||
}
|
||||
return stringFormat(listFormat, oe);
|
||||
}));
|
||||
|
||||
onlineListView.focusItems = onlineListView.items;
|
||||
onlineListView.redraw();
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
self.client.log.error( { error : err.message }, 'Error loading who\'s online');
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue