Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs

This commit is contained in:
Bryan Ashby
2017-02-16 21:16:46 -07:00
130 changed files with 11257 additions and 3916 deletions

View File

@@ -1,23 +1,21 @@
/* jslint node: true */
'use strict';
let MenuModule = require('../core/menu_module.js').MenuModule;
let DropFile = require('../core/dropfile.js').DropFile;
let door = require('../core/door.js');
let theme = require('../core/theme.js');
let ansi = require('../core/ansi_term.js');
const MenuModule = require('../core/menu_module.js').MenuModule;
const DropFile = require('../core/dropfile.js').DropFile;
const door = require('../core/door.js');
const theme = require('../core/theme.js');
const ansi = require('../core/ansi_term.js');
let async = require('async');
let assert = require('assert');
let paths = require('path');
let _ = require('lodash');
let mkdirs = require('fs-extra').mkdirs;
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
exports.getModule = AbracadabraModule;
let activeDoorNodeInstances = {};
const activeDoorNodeInstances = {};
exports.moduleInfo = {
name : 'Abracadabra',
@@ -60,20 +58,20 @@ exports.moduleInfo = {
:TODO: See Mystic & others for other arg options that we may need to support
*/
function AbracadabraModule(options) {
MenuModule.call(this, options);
let self = this;
exports.getModule = class AbracadabraModule extends MenuModule {
constructor(options) {
super(options);
this.config = options.menuConfig.config;
this.config = options.menuConfig.config;
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
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'));
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts!
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 || [];
this.config.nodeMax = this.config.nodeMax || 0;
this.config.args = this.config.args || [];
}
/*
:TODO:
@@ -82,7 +80,9 @@ function AbracadabraModule(options) {
* Font support ala all other menus... or does this just work?
*/
this.initSequence = function() {
initSequence() {
const self = this;
async.series(
[
function validateNodeCount(callback) {
@@ -99,14 +99,15 @@ function AbracadabraModule(options) {
if(_.isString(self.config.tooManyArt)) {
theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() {
theme.displayThemedPause( { client : self.client }, function keyPressed() {
self.pausePrompt( () => {
callback(new Error('Too many active instances'));
});
});
} else {
self.client.term.write('\nToo many active instances. Try again later.\n');
theme.displayThemedPause( { client : self.client }, function keyPressed() {
// :TODO: Use MenuModule.pausePrompt()
self.pausePrompt( () => {
callback(new Error('Too many active instances'));
});
}
@@ -146,54 +147,51 @@ function AbracadabraModule(options) {
}
}
);
};
}
this.runDoor = function() {
runDoor() {
const exeInfo = {
cmd : self.config.cmd,
args : self.config.args,
io : self.config.io || 'stdio',
encoding : self.config.encoding || self.client.term.outputEncoding,
dropFile : self.dropFile.fileName,
node : self.client.node,
//inhSocket : self.client.output._handle.fd,
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(self.client, exeInfo);
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
//
self.client.term.rawWrite(
this.client.term.rawWrite(
ansi.normal() +
ansi.goto(self.client.term.termHeight, self.client.term.termWidth) +
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
ansi.setScrollRegion() +
ansi.goto(self.client.term.termHeight, 0) +
ansi.goto(this.client.term.termHeight, 0) +
'\r\n\r\n'
);
self.prevMenu();
this.prevMenu();
});
self.client.term.write(ansi.resetScreen());
this.client.term.write(ansi.resetScreen());
doorInstance.run();
};
}
}
require('util').inherits(AbracadabraModule, MenuModule);
leave() {
super.leave();
if(!this.lastError) {
activeDoorNodeInstances[this.config.name] -= 1;
}
}
AbracadabraModule.prototype.leave = function() {
AbracadabraModule.super_.prototype.leave.call(this);
if(!this.lastError) {
activeDoorNodeInstances[this.config.name] -= 1;
finishedLoading() {
this.runDoor();
}
};
AbracadabraModule.prototype.finishedLoading = function() {
this.runDoor();
};

BIN
mods/art/NEWUSER1.ANS Normal file

Binary file not shown.

View File

@@ -1,33 +0,0 @@
/* jslint node: true */
'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule;
exports.getModule = ArtPoolModule;
exports.moduleInfo = {
name : 'Art Pool',
desc : 'Display art from a pool of options',
author : 'NuSkooler',
};
function ArtPoolModule(options) {
MenuModule.call(this, options);
var config = this.menuConfig.config;
//
// :TODO: General idea
// * Break up some of MenuModule initSequence's calls into methods
// * initSequence here basically has general "clear", "next", etc. as per normal
// * Display art -> ooptinal pause -> display more if requested, etc.
// * Finally exit & move on as per normal
}
require('util').inherits(ArtPoolModule, MenuModule);
MessageAreaModule.prototype.mciReady = function(mciData, cb) {
this.standardMCIReadyHandler(mciData, cb);
};

View File

@@ -36,28 +36,26 @@ const packageJson = require('../package.json');
// :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.getModule = BBSLinkModule;
exports.moduleInfo = {
name : 'BBSLink',
desc : 'BBSLink Access Module',
author : 'NuSkooler',
};
exports.getModule = class BBSLinkModule extends MenuModule {
constructor(options) {
super(options);
function BBSLinkModule(options) {
MenuModule.call(this, options);
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'games.bbslink.net';
this.config.port = this.config.port || 23;
}
var self = this;
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'games.bbslink.net';
this.config.port = this.config.port || 23;
this.initSequence = function() {
var token;
var randomKey;
var clientTerminated;
initSequence() {
let token;
let randomKey;
let clientTerminated;
const self = this;
async.series(
[
@@ -180,17 +178,17 @@ function BBSLinkModule(options) {
}
}
);
};
}
this.simpleHttpRequest = function(path, headers, cb) {
var getOpts = {
simpleHttpRequest(path, headers, cb) {
const getOpts = {
host : this.config.host,
path : path,
headers : headers,
};
var req = http.get(getOpts, function response(resp) {
var data = '';
const req = http.get(getOpts, function response(resp) {
let data = '';
resp.on('data', function chunk(c) {
data += c;
@@ -205,7 +203,5 @@ function BBSLinkModule(options) {
req.on('error', function reqErr(err) {
cb(err);
});
};
}
require('util').inherits(BBSLinkModule, MenuModule);
}
};

View File

@@ -17,17 +17,13 @@ const _ = require('lodash');
// :TODO: add notes field
exports.getModule = BBSListModule;
const moduleInfo = {
const moduleInfo = exports.moduleInfo = {
name : 'BBS List',
desc : 'List of other BBSes',
author : 'Andrew Pamment',
packageName : 'com.magickabbs.enigma.bbslist'
};
exports.moduleInfo = moduleInfo;
const MciViewIds = {
view : {
BBSList : 1,
@@ -69,13 +65,106 @@ const SELECTED_MCI_NAME_TO_ENTRY = {
SelectedBBSNotes : 'notes',
};
function BBSListModule(options) {
MenuModule.call(this, options);
exports.getModule = class BBSListModule extends MenuModule {
constructor(options) {
super(options);
const self = this;
const config = this.menuConfig.config;
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();
}
}
this.initSequence = function() {
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) {
@@ -92,39 +181,42 @@ function BBSListModule(options) {
self.finishedLoading();
}
);
};
}
this.drawSelectedEntry = function(entry) {
drawSelectedEntry(entry) {
if(!entry) {
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
self.setViewText(MciViewIds.view[mciName], '');
this.setViewText('view', MciViewIds.view[mciName], '');
});
} else {
const youSubmittedFormat = config.youSubmittedFormat || '{submitter} (You!)';
const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
if(MciViewIds.view[mciName]) {
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == self.client.user.userId) {
self.setViewText(MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) {
this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
} else {
self.setViewText(MciViewIds.view[mciName], t);
this.setViewText('view',MciViewIds.view[mciName], t);
}
}
});
}
};
}
this.setEntries = function(entriesView) {
setEntries(entriesView) {
const config = this.menuConfig.config;
const listFormat = config.listFormat || '{bbsName}';
const focusListFormat = config.focusListFormat || '{bbsName}';
entriesView.setItems(self.entries.map( e => stringFormat(listFormat, e) ) );
entriesView.setFocusItems(self.entries.map( e => stringFormat(focusListFormat, e) ) );
};
entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) );
entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) );
}
displayBBSList(clearScreen, cb) {
const self = this;
this.displayBBSList = function(clearScreen, cb) {
async.waterfall(
[
function clearAndDisplayArt(callback) {
@@ -135,7 +227,7 @@ function BBSListModule(options) {
self.client.term.rawWrite(ansi.resetScreen());
}
theme.displayThemedAsset(
config.art.entries,
self.menuConfig.config.art.entries,
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
@@ -238,9 +330,11 @@ function BBSListModule(options) {
}
}
);
};
}
displayAddScreen(cb) {
const self = this;
this.displayAddScreen = function(cb) {
async.waterfall(
[
function clearAndDisplayArt(callback) {
@@ -248,7 +342,7 @@ function BBSListModule(options) {
self.client.term.rawWrite(ansi.resetScreen());
theme.displayThemedAsset(
config.art.add,
self.menuConfig.config.art.add,
self.client,
{ font : self.menuConfig.font },
(err, artData) => {
@@ -284,117 +378,17 @@ function BBSListModule(options) {
}
}
);
};
}
this.clearAddForm = function() {
clearAddForm() {
[ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => {
const v = self.viewControllers.add.getView(MciViewIds.add[mciName]);
if(v) {
v.setText('');
}
this.setViewText('add', MciViewIds.add[mciName], '');
});
};
}
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();
}
}
initDatabase(cb) {
const self = this;
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);
}
};
this.setViewText = function(id, text) {
var v = self.viewControllers.view.getView(id);
if(v) {
v.setText(text);
}
};
this.initDatabase = function(cb) {
async.series(
[
function openDatabase(callback) {
@@ -422,15 +416,15 @@ function BBSListModule(options) {
callback(null);
}
],
cb
err => {
return cb(err);
}
);
};
}
}
require('util').inherits(BBSListModule, MenuModule);
BBSListModule.prototype.beforeArt = function(cb) {
BBSListModule.super_.prototype.beforeArt.call(this, err => {
return err ? cb(err) : this.initDatabase(cb);
});
beforeArt(cb) {
super.beforeArt(err => {
return err ? cb(err) : this.initDatabase(cb);
});
}
};

View File

@@ -1,7 +1,7 @@
/* jslint node: true */
'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule;
const MenuModule = require('../core/menu_module.js').MenuModule;
const stringFormat = require('../core/string_format.js');
// deps
@@ -33,8 +33,9 @@ var MciViewIds = {
InputArea : 3,
};
// :TODO: needs converted to ES6 MenuModule subclass
function ErcClientModule(options) {
MenuModule.call(this, options);
MenuModule.prototype.ctorShim.call(this, options);
const self = this;
this.config = options.menuConfig.config;

View File

@@ -0,0 +1,321 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('../core/file_base_filter.js');
const stringFormat = require('../core/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 filterUuid = this.filtersArray[this.currentFilterIndex].uuid;
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);
},
};
}
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();
}
}
};

648
mods/file_area_list.js Normal file
View File

@@ -0,0 +1,648 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const ansi = require('../core/ansi_term.js');
const theme = require('../core/theme.js');
const FileEntry = require('../core/file_entry.js');
const stringFormat = require('../core/string_format.js');
const createCleanAnsi = require('../core/string_util.js').createCleanAnsi;
const FileArea = require('../core/file_base_area.js');
const Errors = require('../core/enig_error.js').Errors;
const ArchiveUtil = require('../core/archive_util.js');
const Config = require('../core/config.js').config;
const DownloadQueue = require('../core/download_queue.js');
const FileAreaWeb = require('../core/file_area_web.js');
const FileBaseFilters = require('../core/file_base_filter.js');
const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
// 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);
if(options.extraArgs) {
this.filterCriteria = options.extraArgs.filterCriteria;
}
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) => {
this.viewControllers.details.setFocus(false);
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 : area.name || 'N/A',
areaDesc : 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.getWellKnownMetaValues();
metaValues.forEach(name => {
const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A';
entryInfo[_.camelCase(name)] = value;
});
if(entryInfo.archiveType) {
entryInfo.archiveTypeDesc = _.has(Config, [ 'archives', 'formats', entryInfo.archiveType, 'desc' ]) ?
Config.archives.formats[entryInfo.archiveType].desc :
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 = new Array(entryInfo.userRating + 1).join(userRatingTicked);
if(entryInfo.userRating < 5) {
entryInfo.userRatingString += new Array( (5 - entryInfo.userRating) + 1).join(userRatingUnticked);
}
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
if(err) {
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
entryInfo.webDlExpire = '';
} else {
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
entryInfo.webDlLink = 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 populateViews(callback) {
if(_.isString(self.currentFileEntry.desc)) {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
if(descView) {
createCleanAnsi(
self.currentFileEntry.desc,
{ height : self.client.termHeight, width : descView.dimens.width },
cleanDesc => {
// :TODO: use cleanDesc -- need to finish createCleanAnsi() !!
//descView.setText(cleanDesc);
descView.setText( self.currentFileEntry.desc );
self.updateQueueIndicator();
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
return callback(null);
}
);
}
} else {
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 = 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) {
nfoView.setText(self.currentFileEntry.entryInfo.descLong);
}
}
break;
case 'fileList' :
self.populateFileListing();
break;
}
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;
FileEntry.findFiles(this.filterCriteria, (err, fileIds) => {
this.fileList = fileIds;
return cb(err);
});
}
}
};

View File

@@ -0,0 +1,218 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const DownloadQueue = require('../core/download_queue.js');
const theme = require('../core/theme.js');
const ansi = require('../core/ansi_term.js');
const Errors = require('../core/enig_error.js').Errors;
const stringFormat = require('../core/string_format.js');
// deps
const async = require('async');
const _ = require('lodash');
exports.moduleInfo = {
name : 'File Base Download Queue Manager',
desc : 'Module for interacting with download queue/batch',
author : 'NuSkooler',
};
const FormIds = {
queueManager : 0,
details : 1,
};
const MciViewIds = {
queueManager : {
queue : 1,
navMenu : 2,
},
details : {
}
};
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);
}
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.redraw();
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
mods/file_base_search.js Normal file
View File

@@ -0,0 +1,120 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('../core/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);
}
};

View File

@@ -0,0 +1,158 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule;
const Config = require('../core/config.js').config;
const stringFormat = require('../core/string_format.js');
const ViewController = require('../core/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 } );
}
});
}
};

View File

@@ -32,111 +32,112 @@ exports.moduleInfo = {
packageName : 'codes.l33t.enigma.lastcallers' // :TODO: concept idea for mods
};
exports.getModule = LastCallersModule;
var MciCodeIds = {
const MciCodeIds = {
CallerList : 1,
};
function LastCallersModule(options) {
MenuModule.call(this, options);
}
exports.getModule = class LastCallersModule extends MenuModule {
constructor(options) {
super(options);
}
require('util').inherits(LastCallersModule, MenuModule);
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
LastCallersModule.prototype.mciReady = function(mciData, cb) {
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
let loginHistory;
let callersView;
let loginHistory;
let callersView;
async.series(
[
function callParentMciReady(callback) {
LastCallersModule.super_.prototype.mciReady.call(self, mciData, callback);
},
function loadFromConfig(callback) {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu,
noInput : true,
};
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);
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;
// 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 === isRootUserId(parseInt(lh.log_value)); // log_value=userId
});
if(self.menuConfig.config.hideSysOpLogin) {
const noOpLoginHistory = loginHistory.filter(lh => {
return false === 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);
getUserName(item.userId, (err, userName) => {
item.userName = userName;
getPropOpts.userId = item.userId;
loadProperties(getPropOpts, (err, props) => {
if(!err) {
item.location = props.location;
item.affiliation = item.affils = props.affiliation;
}
return next();
});
//
// 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);
});
},
callback
);
},
function populateList(callback) {
const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}';
function getUserNamesAndProperties(callback) {
const getPropOpts = {
names : [ 'location', 'affiliation' ]
};
callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) );
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
callersView.redraw();
return callback(null);
}
],
(err) => {
if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading last callers');
}
cb(err);
}
);
async.each(
loginHistory,
(item, next) => {
item.userId = parseInt(item.log_value);
item.ts = moment(item.timestamp).format(dateTimeFormat);
getUserName(item.userId, (err, userName) => {
item.userName = userName;
getPropOpts.userId = item.userId;
loadProperties(getPropOpts, (err, props) => {
if(!err) {
item.location = props.location;
item.affiliation = item.affils = props.affiliation;
}
return next();
});
});
},
callback
);
},
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);
}
);
});
}
};

View File

@@ -1,11 +1,29 @@
{
/*
ENiGMA½ Menu Configuration
./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- -
This configuration is in HJSON format. Strict to-spec JSON is also
perfectly valid. The hjson npm can be used to convert to/from JSON.
_____________________ _____ ____________________ __________\_ /
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
// __|___// | \// |// | \// | | \// \ /___ /_____
/____ _____| __________ ___|__| ____| \ / _____ \
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
/__ _\
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
-------------------------------------------------------------------------------
This configuration is in HJSON (http://hjson.org/) format. Strict to-spec
JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON.
See http://hjson.org/ for more information and syntax.
If you haven't yet, copy the conents of this file to something like
sick_board.hjson. Point to it via config.hjson using the
'general.menuFile' key:
general: { menuFile: "sick_board.hjson" }
*/
menus: {
//
@@ -34,7 +52,7 @@
//
sshConnectedNewUser: {
art: CONNECT
next: newUserApplicationSsh
next: newUserApplicationPreSsh
options: { nextTimeout: 1500 }
}
@@ -60,7 +78,7 @@
}
{
value: { 1: 1 },
action: @menu:newUserApplication
action: @menu:newUserApplicationPre
}
{
value: { 1: 2 },
@@ -162,12 +180,24 @@
desc: Logging Off
next: @systemMethod:logoff
}
/*
TODO: display PRINT before this (Obv/2) or NEWUSER1 (Mystic)
*/
// A quick preamble - defaults to warning about broken terminals
newUserApplicationPre: {
art: NEWUSER1
next: newUserApplication
desc: Applying
options: {
pause: true
cls: true
}
}
newUserApplication: {
module: nua
art: NUA
options: {
menuFlags: [ "noHistory" ]
}
next: [
{
// Initial SysOp does not send feedback to themselves
@@ -268,6 +298,17 @@
}
}
// A quick preamble - defaults to warning about broken terminals (SSH version)
newUserApplicationPreSsh: {
art: NEWUSER1
next: newUserApplicationSsh
desc: Applying
options: {
pause: true
cls: true
}
}
//
// SSH specialization of NUA
// Canceling this form logs off vs falling back to matrix
@@ -275,6 +316,9 @@
newUserApplicationSsh: {
art: NUA
fallback: logoff
options: {
menuFlags: [ "noHistory" ]
}
next: newUserFeedbackToSysOpPreamble
form: {
0: {
@@ -350,7 +394,7 @@
}
{
value: { "submission" : 1 }
action: @systemMethod:prevMenu
action: @systemMethod:logoff
}
]
}
@@ -358,7 +402,7 @@
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
action: @systemMethod:logoff
}
]
}
@@ -708,6 +752,10 @@
value: { command: "D" }
action: @menu:doorMenu
}
{
value: { command: "F" }
action: @menu:fileBase
}
{
value: { command: "U" }
action: @menu:mainMenuUserList
@@ -1696,6 +1744,7 @@
HM1: {
// :TODO: (#)Jump/(L)Index (msg list)/Last
items: [ "prev", "next", "reply", "quit", "help" ]
focusItemIndex: 1
}
}
submit: {
@@ -2226,6 +2275,639 @@
}
}
////////////////////////////////////////////////////////////////////////
// File Area
////////////////////////////////////////////////////////////////////////
fileBase: {
desc: File Base
art: FMENU
prompt: fileMenuCommand
submit: [
{
value: { menuOption: "B" }
action: @menu:fileBaseListEntries
}
{
value: { menuOption: "F" }
action: @menu:fileAreaFilterEditor
}
{
value: { menuOption: "Q" }
action: @systemMethod:prevMenu
}
{
value: { menuOption: "G" }
action: @menu:fullLogoffSequence
}
{
value: { menuOption: "D" }
action: @menu:fileBaseDownloadManager
}
{
value: { menuOption: "U" }
action: @menu:fileBaseUploadFiles
}
{
value: { menuOption: "S" }
action: @menu:fileBaseSearch
}
]
}
fileBaseListEntries: {
module: file_area_list
desc: Browsing Files
config: {
art: {
browse: FBRWSE
details: FDETAIL
detailsGeneral: FDETGEN
detailsNfo: FDETNFO
detailsFileList: FDETLST
help: FBHELP
}
}
form: {
0: {
mci: {
MT1: {
mode: preview
}
HM2: {
focus: true
submit: true
argName: navSelect
items: [
"prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit"
]
focusItemIndex: 1
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:prevFile
}
{
value: { navSelect: 1 }
action: @method:nextFile
}
{
value: { navSelect: 2 }
action: @method:viewDetails
}
{
value: { navSelect: 3 }
action: @method:toggleQueue
}
{
value: { navSelect: 4 }
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
value: { navSelect: 5 }
action: @menu:fileAreaFilterEditor
}
{
value: { navSelect: 6 }
action: @method:displayHelp
}
{
value: { navSelect: 7 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "w", "shift + w" ]
action: @method:showWebDownloadLink
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "t", "shift + t" ]
action: @method:toggleQueue
}
{
keys: [ "f", "shift + f" ]
action: @menu:fileAreaFilterEditor
}
{
keys: [ "v", "shift + v" ]
action: @method:viewDetails
}
{
keys: [ "r", "shift + r" ]
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
keys: [ "?" ]
action: @method:displayHelp
}
]
}
1: {
mci: {
HM1: {
focus: true
submit: true
argName: navSelect
items: [
"general", "nfo/readme", "file listing"
]
}
}
actionKeys: [
{
keys: [ "escape", "q", "shift + q" ]
action: @method:detailsQuit
}
]
}
2: {
// details - general
mci: {}
}
3: {
// details - nfo/readme
mci: {
MT1: {
mode: preview
}
}
}
4: {
// details - file listing
mci: {
VM1: {
}
}
}
}
}
fileBaseGetRatingForSelectedEntry: {
desc: Rating a File
prompt: fileBaseRateEntryPrompt
options: {
cls: true
}
submit: [
// :TODO: handle esc/q
{
// pass data back to caller
value: { rating: null }
action: @systemMethod:prevMenu
}
]
}
fileBaseListEntriesNoResults: {
desc: Browsing Files
art: FBNORES
options: {
pause: true
menuFlags: [ "noHistory" ]
}
}
fileBaseSearch: {
module: file_base_search
desc: Searching Files
art: FSEARCH
form: {
0: {
mci: {
ET1: {
focus: true
argName: searchTerms
}
BT2: {
argName: search
text: search
submit: true
}
ET3: {
maxLength: 64
argName: tags
}
SM4: {
maxLength: 64
argName: areaIndex
}
SM5: {
items: [
"upload date",
"uploaded by",
"downloads",
"rating",
"estimated year",
"size",
]
argName: sortByIndex
}
SM6: {
items: [
"decending",
"ascending"
]
argName: orderByIndex
}
BT7: {
argName: advancedSearch
text: advanced search
submit: true
}
}
submit: {
*: [
{
value: { search: null }
action: @method:search
}
{
value: { advancedSearch: null }
action: @method:search
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileAreaFilterEditor: {
desc: File Filter Editor
module: file_area_filter_edit
art: FFILEDT
form: {
0: {
mci: {
ET1: {
argName: searchTerms
}
ET2: {
maxLength: 64
argName: tags
}
SM3: {
maxLength: 64
argName: areaIndex
}
SM4: {
items: [
"upload date",
"uploaded by",
"downloads",
"rating",
"estimated year",
"size",
]
argName: sortByIndex
}
SM5: {
items: [
"decending",
"ascending"
]
argName: orderByIndex
}
ET6: {
maxLength: 64
argName: name
validate: @systemMethod:validateNonEmpty
}
HM7: {
focus: true
items: [
"prev", "next", "make active", "save", "new", "delete"
]
argName: navSelect
focusItemIndex: 1
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:prevFilter
}
{
value: { navSelect: 1 }
action: @method:nextFilter
}
{
value: { navSelect: 2 }
action: @method:makeFilterActive
}
{
value: { navSelect: 3 }
action: @method:saveFilter
}
{
value: { navSelect: 4 }
action: @method:newFilter
}
{
value: { navSelect: 5 }
action: @method:deleteFilter
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseDownloadManager: {
desc: Download Manager
module: file_base_download_manager
config: {
art: {
queueManager: FDLMGR
/*
NYI
details: FDLDET
*/
}
emptyQueueMenu: fileBaseDownloadManagerEmptyQueue
}
form: {
0: {
mci: {
VM1: {
argName: queueItem
}
HM2: {
focus: true
items: [ "download all", "quit" ]
argName: navSelect
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:downloadAll
}
{
value: { navSelect: 1 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "a", "shift + a" ]
action: @method:downloadAll
}
{
keys: [ "delete", "r", "shift + r" ]
action: @method:removeItem
}
{
keys: [ "c", "shift + c" ]
action: @method:clearQueue
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseDownloadManagerEmptyQueue: {
desc: Empty Download Queue
art: FEMPTYQ
options: {
pause: true
menuFlags: [ "noHistory" ]
}
}
fileTransferProtocolSelection: {
desc: Protocol selection
module: file_transfer_protocol_select
art: FPROSEL
form: {
0: {
mci: {
VM1: {
focus: true
argName: protocol
}
}
submit: {
*: [
{
value: { protocol: null }
action: @method:selectProtocol
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseUploadFiles: {
desc: Uploading
module: upload
config: {
art: {
options: ULOPTS
fileDetails: ULDETAIL
processing: ULCHECK
dupes: ULDUPES
}
}
form: {
// options
0: {
mci: {
SM1: {
argName: areaSelect
focus: true
}
TM2: {
argName: uploadType
items: [ "blind", "supply filename" ]
}
ET3: {
argName: fileName
maxLength: 255
validate: @method:validateNonBlindFileName
}
HM4: {
argName: navSelect
items: [ "continue", "cancel" ]
submit: true
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:optionsNavContinue
}
{
value: { navSelect: 1 }
action: @systemMethod:prevMenu
}
]
}
"actionKeys" : [
{
"keys" : [ "escape" ],
action: @systemMethod:prevMenu
}
]
}
1: {
mci: {
TL1: {}
TL2: {}
TL3: {}
MT4: {}
TL10: {}
}
}
// file details entry
2: {
mci: {
MT1: {
argName: shortDesc
tabSwitchesView: true
focus: true
}
ET2: {
argName: tags
}
ME3: {
argName: estYear
maskPattern: "####"
}
BT4: {
argName: continue
text: continue
submit: true
}
}
submit: {
*: [
{
value: { continue: null }
action: @method:fileDetailsContinue
}
]
}
}
// dupes
3: {
mci: {
VM1: {
/*
Use 'dupeInfoFormat' to custom format:
areaDesc
areaName
areaTag
desc
descLong
fileId
fileName
fileSha256
storageTag
uploadTimestamp
*/
mode: preview
}
}
}
}
}
fileBaseNoUploadAreasAvail: {
desc: File Base
art: ULNOAREA
options: {
pause: true
menuFlags: [ "noHistory" ]
}
}
sendFilesToUser: {
desc: Downloading
module: @systemModule:file_transfer
config: {
// defaults - generally use extraArgs
protocol: zmodem8kSexyz
direction: send
}
}
recvFilesFromUser: {
desc: Uploading
module: @systemModule:file_transfer
config: {
// defaults - generally use extraArgs
protocol: zmodem8kSexyz
direction: recv
}
}
////////////////////////////////////////////////////////////////////////
// Required entries
////////////////////////////////////////////////////////////////////////

View File

@@ -6,7 +6,6 @@ const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const messageArea = require('../core/message_area.js');
const displayThemeArt = require('../core/theme.js').displayThemeArt;
const displayThemedPause = require('../core/theme.js').displayThemedPause;
const resetScreen = require('../core/ansi_term.js').resetScreen;
const stringFormat = require('../core/string_format.js');
@@ -14,8 +13,6 @@ const stringFormat = require('../core/string_format.js');
const async = require('async');
const _ = require('lodash');
exports.getModule = MessageAreaListModule;
exports.moduleInfo = {
name : 'Message Area List',
desc : 'Module for listing / choosing message areas',
@@ -36,152 +33,145 @@ exports.moduleInfo = {
|TI Current time
*/
const MCICodesIDs = {
const MciViewIds = {
AreaList : 1,
SelAreaInfo1 : 2,
SelAreaInfo2 : 3,
};
function MessageAreaListModule(options) {
MenuModule.call(this, options);
exports.getModule = class MessageAreaListModule extends MenuModule {
constructor(options) {
super(options);
var self = this;
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
this.client.user.properties.message_conf_tag,
{ client : this.client }
);
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
self.client.user.properties.message_conf_tag,
{ client : self.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
this.prevMenuOnTimeout = function(timeout, cb) {
setTimeout( () => {
self.prevMenu(cb);
}, timeout);
};
messageArea.changeMessageArea(self.client, areaTag, err => {
if(err) {
self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`);
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
self.prevMenuOnTimeout(1000, cb);
} else {
if(_.isString(area.art)) {
const dispOptions = {
client : self.client,
name : area.art,
};
messageArea.changeMessageArea(self.client, areaTag, err => {
if(err) {
self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`);
self.client.term.rawWrite(resetScreen());
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 {
displayThemedPause( { client : self.client }, () => {
return self.prevMenu(cb);
});
}
});
} else {
return self.prevMenu(cb);
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);
});
} else {
return cb(null);
}
}
}
};
};
}
this.setViewText = function(id, text) {
const v = self.viewControllers.areaList.getView(id);
if(v) {
v.setText(text);
}
};
prevMenuOnTimeout(timeout, cb) {
setTimeout( () => {
return this.prevMenu(cb);
}, timeout);
}
this.updateGeneralAreaInfoViews = function(areaIndex) {
updateGeneralAreaInfoViews(areaIndex) {
// :TODO: these concepts have been replaced with the {someKey} style formatting - update me!
/* experimental: not yet avail
const areaInfo = self.messageAreas[areaIndex];
[ MCICodesIDs.SelAreaInfo1, MCICodesIDs.SelAreaInfo2 ].forEach(mciId => {
[ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => {
const v = self.viewControllers.areaList.getView(mciId);
if(v) {
v.setFormatObject(areaInfo.area);
}
});
*/
};
}
}
require('util').inherits(MessageAreaListModule, MenuModule);
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
const self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series(
[
function callParentMciReady(callback) {
MessageAreaListModule.super_.prototype.mciReady.call(this, mciData, function parentMciReady(err) {
callback(err);
});
},
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(MCICodesIDs.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);
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
],
function complete(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);
}
);
});
}
};

View File

@@ -1,15 +1,11 @@
/* jslint node: true */
'use strict';
let FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
//var Message = require('../core/message.js').Message;
let persistMessage = require('../core/message_area.js').persistMessage;
let user = require('../core/user.js');
const FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
const persistMessage = require('../core/message_area.js').persistMessage;
let _ = require('lodash');
let async = require('async');
exports.getModule = AreaPostFSEModule;
const _ = require('lodash');
const async = require('async');
exports.moduleInfo = {
name : 'Message Area Post',
@@ -17,56 +13,55 @@ exports.moduleInfo = {
author : 'NuSkooler',
};
function AreaPostFSEModule(options) {
FullScreenEditorModule.call(this, options);
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
constructor(options) {
super(options);
var self = this;
const self = this;
// we're posting, so always start with 'edit' mode
this.editorMode = 'edit';
// we're posting, so always start with 'edit' mode
this.editorMode = 'edit';
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) {
var msg;
async.series(
[
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);
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);
}
],
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);
}
);
};
}
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
AreaPostFSEModule.prototype.enter = function() {
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
this.messageAreaTag = this.client.user.properties.message_area_tag;
);
};
}
AreaPostFSEModule.super_.prototype.enter.call(this);
};
enter() {
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
this.messageAreaTag = this.client.user.properties.message_area_tag;
}
super.enter();
}
};

View File

@@ -8,122 +8,118 @@ const Message = require('../core/message.js');
// deps
const _ = require('lodash');
exports.getModule = AreaViewFSEModule;
exports.moduleInfo = {
name : 'Message Area View',
desc : 'Module for viewing an area message',
author : 'NuSkooler',
};
function AreaViewFSEModule(options) {
FullScreenEditorModule.call(this, options);
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
constructor(options) {
super(options);
const self = this;
this.editorType = 'area';
this.editorMode = 'view';
this.editorType = 'area';
this.editorMode = 'view';
if(_.isObject(options.extraArgs)) {
this.messageList = options.extraArgs.messageList;
this.messageIndex = options.extraArgs.messageIndex;
}
if(_.isObject(options.extraArgs)) {
this.messageList = options.extraArgs.messageList;
this.messageIndex = options.extraArgs.messageIndex;
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);
}
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);
}
});
}
this.messageList = this.messageList || [];
this.messageIndex = this.messageIndex || 0;
this.messageTotal = this.messageList.length;
this.menuMethods.nextMessage = function(formData, extraArgs, cb) {
if(self.messageIndex + 1 < self.messageList.length) {
self.messageIndex++;
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
}
return cb(null);
};
this.menuMethods.prevMessage = function(formData, extraArgs, cb) {
if(self.messageIndex > 0) {
self.messageIndex--;
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
}
return cb(null);
};
this.menuMethods.movementKeyPressed = function(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);
};
this.menuMethods.replyMessage = function(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);
};
this.loadMessageByUuid = function(uuid, cb) {
loadMessageByUuid(uuid, cb) {
const msg = new Message();
msg.load( { uuid : uuid, user : self.client.user }, () => {
self.setMessage(msg);
msg.load( { uuid : uuid, user : this.client.user }, () => {
this.setMessage(msg);
if(cb) {
return cb(null);
}
});
};
}
}
require('util').inherits(AreaViewFSEModule, FullScreenEditorModule);
AreaViewFSEModule.prototype.finishedLoading = function() {
if(this.messageList.length) {
finishedLoading() {
this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid);
}
};
AreaViewFSEModule.prototype.getSaveState = function() {
AreaViewFSEModule.super_.prototype.getSaveState.call(this);
return {
messageList : this.messageList,
messageIndex : this.messageIndex,
messageTotal : this.messageList.length,
};
};
AreaViewFSEModule.prototype.restoreSavedState = function(savedState) {
AreaViewFSEModule.super_.prototype.restoreSavedState.call(this, savedState);
this.messageList = savedState.messageList;
this.messageIndex = savedState.messageIndex;
this.messageTotal = savedState.messageTotal;
};
AreaViewFSEModule.prototype.getMenuResult = function() {
return this.messageIndex;
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 this.messageIndex;
}
};

View File

@@ -6,7 +6,6 @@ const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const messageArea = require('../core/message_area.js');
const displayThemeArt = require('../core/theme.js').displayThemeArt;
const displayThemedPause = require('../core/theme.js').displayThemedPause;
const resetScreen = require('../core/ansi_term.js').resetScreen;
const stringFormat = require('../core/string_format.js');
@@ -14,15 +13,13 @@ const stringFormat = require('../core/string_format.js');
const async = require('async');
const _ = require('lodash');
exports.getModule = MessageConfListModule;
exports.moduleInfo = {
name : 'Message Conference List',
desc : 'Module for listing / choosing message conferences',
author : 'NuSkooler',
};
const MCICodeIDs = {
const MciViewIds = {
ConfList : 1,
// :TODO:
@@ -30,127 +27,122 @@ const MCICodeIDs = {
//
};
function MessageConfListModule(options) {
MenuModule.call(this, options);
exports.getModule = class MessageConfListModule extends MenuModule {
constructor(options) {
super(options);
var self = this;
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
this.messageConfs = messageArea.getSortedAvailMessageConferences(self.client);
messageArea.changeMessageConference(self.client, confTag, err => {
if(err) {
self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`);
this.prevMenuOnTimeout = function(timeout, cb) {
setTimeout( () => {
self.prevMenu(cb);
}, timeout);
};
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 {
displayThemedPause( { client : self.client }, () => {
return self.prevMenu(cb);
});
}
});
setTimeout( () => {
return self.prevMenu(cb);
}, 1000);
} else {
return self.prevMenu(cb);
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);
}
});
} else {
return cb(null);
}
}
};
this.setViewText = function(id, text) {
const v = self.viewControllers.areaList.getView(id);
if(v) {
v.setText(text);
}
};
}
require('util').inherits(MessageConfListModule, MenuModule);
MessageConfListModule.prototype.mciReady = function(mciData, cb) {
var self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series(
[
function callParentMciReady(callback) {
MessageConfListModule.super_.prototype.mciReady.call(this, mciData, callback);
},
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(MCICodeIDs.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);
}
);
};
],
function complete(err) {
cb(err);
}
);
});
}
};

View File

@@ -2,10 +2,11 @@
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const messageArea = require('../core/message_area.js');
const stringFormat = require('../core/string_format.js');
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const messageArea = require('../core/message_area.js');
const stringFormat = require('../core/string_format.js');
const MessageAreaConfTempSwitcher = require('../core/mod_mixins.js').MessageAreaConfTempSwitcher;
// deps
const async = require('async');
@@ -28,8 +29,6 @@ const moment = require('moment');
TL2 : Message info 1: { msgNumSelected, msgNumTotal }
*/
exports.getModule = MessageListModule;
exports.moduleInfo = {
name : 'Message List',
desc : 'Module for listing/browsing available messages',
@@ -41,218 +40,213 @@ const MCICodesIDs = {
MsgInfo1 : 2, // TL2
};
function MessageListModule(options) {
MenuModule.call(this, options);
exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) {
constructor(options) {
super(options);
const self = this;
const config = this.menuConfig.config;
const self = this;
const config = this.menuConfig.config;
this.messageAreaTag = config.messageAreaTag;
this.messageAreaTag = config.messageAreaTag;
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) {
//
// |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;
}
}
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;
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,
}
};
//
// 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,
const modOpts = {
extraArgs : {
messageAreaTag : self.messageAreaTag,
messageList : self.messageList,
messageIndex : formData.value.message,
}
};
};
return self.gotoMenu(config.menuViewPost || 'messageAreaViewPost', modOpts, cb);
} else {
return cb(null);
}
},
//
// 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));
fullExit : function(formData, extraArgs, cb) {
self.menuResult = { fullExit : true };
return self.prevMenu(cb);
}
};
return {
messageAreaTag : this.messageAreaTag,
apprevMessageList : logMsgList,
messageCount : this.messageList.length,
messageIndex : formData.value.message,
};
};
this.setViewText = function(id, text) {
const v = self.viewControllers.allViews.getView(id);
if(v) {
v.setText(text);
}
};
}
require('util').inherits(MessageListModule, MenuModule);
require('../core/mod_mixins.js').MessageAreaConfTempSwitcher.call(MessageListModule.prototype);
MessageListModule.prototype.enter = function() {
MessageListModule.super_.prototype.enter.call(this);
//
// Config can specify |messageAreaTag| else it comes from
// the user's current area
//
if(this.messageAreaTag) {
this.tempMessageConfAndAreaSwitch(this.messageAreaTag);
} else {
this.messageAreaTag = this.messageAreaTag = this.client.user.properties.message_area_tag;
}
};
MessageListModule.prototype.leave = function() {
this.tempMessageConfAndAreaRestore();
MessageListModule.super_.prototype.leave.call(this);
};
MessageListModule.prototype.mciReady = function(mciData, cb) {
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
async.series(
[
function callParentMciReady(callback) {
MessageListModule.super_.prototype.mciReady.call(self, mciData, callback);
},
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(
MCICodesIDs.MsgInfo1,
stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.messageList.length } ));
});
if(self.initialFocusIndex > 0) {
// note: causes redraw()
msgListView.setFocusItemIndex(self.initialFocusIndex);
return self.gotoMenu(config.menuViewPost || 'messageAreaViewPost', modOpts, cb);
} else {
msgListView.redraw();
return cb(null);
}
},
return callback(null);
},
function drawOtherViews(callback) {
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
self.setViewText(
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');
fullExit : function(formData, extraArgs, cb) {
self.menuResult = { fullExit : true };
return self.prevMenu(cb);
}
return cb(err);
};
}
enter() {
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;
}
);
};
}
MessageListModule.prototype.getSaveState = function() {
return { initialFocusIndex : this.initialFocusIndex };
};
leave() {
this.tempMessageConfAndAreaRestore();
super.leave();
}
MessageListModule.prototype.restoreSavedState = function(savedState) {
if(savedState) {
this.initialFocusIndex = savedState.initialFocusIndex;
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;
}
};
MessageListModule.prototype.getMenuResult = function() {
return this.menuResult;
};

View File

@@ -9,8 +9,6 @@ const login = require('../core/system_menu_method.js').login;
const Config = require('../core/config.js').config;
const messageArea = require('../core/message_area.js');
exports.getModule = NewUserAppModule;
exports.moduleInfo = {
name : 'NUA',
desc : 'New User Application',
@@ -23,123 +21,124 @@ const MciViewIds = {
errMsg : 11,
};
function NewUserAppModule(options) {
MenuModule.call(this, options);
exports.getModule = class NewUserAppModule extends MenuModule {
constructor(options) {
super(options);
const self = this;
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'));
},
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();
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();
if(err.view.getId() === MciViewIds.confirm) {
newFocusId = MciViewIds.password;
self.viewControllers.menu.getView(MciViewIds.password).clearText();
}
} else {
errMsgView.clearText();
}
} else {
errMsgView.clearText();
}
return cb(newFocusId);
},
return cb(newFocusId);
},
//
// Submit handlers
//
submitApplication : function(formData, extraArgs, cb) {
const newUser = new user.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
// Submit handlers
//
submitApplication : function(formData, extraArgs, cb) {
const newUser = new user.User();
// 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,
newUser.username = formData.value.username;
term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth,
//
// 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
// :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc.
};
// 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,
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( { password : formData.value.password }, err => {
if(err) {
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth,
self.gotoMenu(extraArgs.error, err => {
if(err) {
return self.prevMenu(cb);
}
return cb(null);
});
// :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc.
};
if('*' === Config.defaults.theme) {
newUser.properties.theme_id = theme.getRandomTheme();
} 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.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);
}
newUser.properties.theme_id = Config.defaults.theme;
}
});
},
};
}
// :TODO: User.create() should validate email uniqueness!
newUser.create( { password : formData.value.password }, err => {
if(err) {
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
require('util').inherits(NewUserAppModule, MenuModule);
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');
NewUserAppModule.prototype.mciReady = function(mciData, cb) {
this.standardMCIReadyHandler(mciData, cb);
};
// 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.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);
}
};

View File

@@ -31,9 +31,7 @@ exports.moduleInfo = {
packageName : 'codes.l33t.enigma.onelinerz',
};
exports.getModule = OnelinerzModule;
const MciCodeIds = {
const MciViewIds = {
ViewForm : {
Entries : 1,
AddPrompt : 2,
@@ -50,20 +48,52 @@ const FormIds = {
Add : 1,
};
function OnelinerzModule(options) {
MenuModule.call(this, options);
exports.getModule = class OnelinerzModule extends MenuModule {
constructor(options) {
super(options);
const self = this;
const config = this.menuConfig.config;
const self = this;
this.initSequence = function() {
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) {
self.beforeArt(callback);
return self.beforeArt(callback);
},
function display(callback) {
self.displayViewScreen(false, callback);
return self.displayViewScreen(false, callback);
}
],
err => {
@@ -73,9 +103,11 @@ function OnelinerzModule(options) {
self.finishedLoading();
}
);
};
}
displayViewScreen(clearScreen, cb) {
const self = this;
this.displayViewScreen = function(clearScreen, cb) {
async.waterfall(
[
function clearAndDisplayArt(callback) {
@@ -88,7 +120,7 @@ function OnelinerzModule(options) {
}
theme.displayThemedAsset(
config.art.entries,
self.menuConfig.config.art.entries,
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
@@ -112,12 +144,12 @@ function OnelinerzModule(options) {
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw();
return callback(null);
}
},
function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries);
const limit = entriesView.dimens.height;
let entries = [];
@@ -142,8 +174,8 @@ function OnelinerzModule(options) {
);
},
function populateEntries(entriesView, entries, callback) {
const listFormat = config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent
const tsFormat = config.timestampFormat || 'ddd h:mma';
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, {
@@ -159,7 +191,7 @@ function OnelinerzModule(options) {
return callback(null);
},
function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO
return callback(null);
}
@@ -170,9 +202,11 @@ function OnelinerzModule(options) {
}
}
);
};
}
displayAddScreen(cb) {
const self = this;
this.displayAddScreen = function(cb) {
async.waterfall(
[
function clearAndDisplayArt(callback) {
@@ -180,7 +214,7 @@ function OnelinerzModule(options) {
self.client.term.rawWrite(ansi.resetScreen());
theme.displayThemedAsset(
config.art.add,
self.menuConfig.config.art.add,
self.client,
{ font : self.menuConfig.font },
(err, artData) => {
@@ -205,7 +239,7 @@ function OnelinerzModule(options) {
} else {
self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry);
return callback(null);
}
}
@@ -216,80 +250,50 @@ function OnelinerzModule(options) {
}
}
);
};
}
this.clearAddForm = function() {
const newEntryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
clearAddForm() {
this.setViewText('add', MciViewIds.AddForm.NewEntry, '');
this.setViewText('add', MciViewIds.AddForm.EntryPreview, '');
}
newEntryView.setText('');
// preview is optional
if(previewView) {
previewView.setText('');
}
};
initDatabase(cb) {
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
}
};
this.initDatabase = function(cb) {
async.series(
[
function openDatabase(callback) {
self.db = new sqlite3.Database(
getModDatabasePath(exports.moduleInfo),
callback
err => {
return callback(err);
}
);
},
function createTables(callback) {
self.db.serialize( () => {
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
)`
);
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);
});
callback(null);
}
],
cb
err => {
return cb(err);
}
);
};
}
this.storeNewOneliner = function(oneliner, cb) {
const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ');
storeNewOneliner(oneliner, cb) {
const self = this;
const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ');
async.series(
[
@@ -315,15 +319,15 @@ function OnelinerzModule(options) {
);
}
],
cb
err => {
return cb(err);
}
);
};
}
}
require('util').inherits(OnelinerzModule, MenuModule);
OnelinerzModule.prototype.beforeArt = function(cb) {
OnelinerzModule.super_.prototype.beforeArt.call(this, err => {
return err ? cb(err) : this.initDatabase(cb);
});
beforeArt(cb) {
super.beforeArt(err => {
return err ? cb(err) : this.initDatabase(cb);
});
}
};

View File

@@ -1,8 +1,34 @@
{
/*
./\/\.' ENiGMA½ Prompt Configuration -/--/-------- - -- -
_____________________ _____ ____________________ __________\_ /
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
// __|___// | \// |// | \// | | \// \ /___ /_____
/____ _____| __________ ___|__| ____| \ / _____ \
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
/__ _\
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
-------------------------------------------------------------------------------
This configuration is in HJSON (http://hjson.org/) format. Strict to-spec
JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON.
See http://hjson.org/ for more information and syntax.
If you haven't yet, copy the conents of this file to something like
sick_board_prompt.hjson. Point to it via config.hjson using the
'general.promptFile' key:
general: { promptFile: "sick_board_prompt.hjson" }
*/
// :TODO: this entire file needs cleaned up a LOT
// :TODO: Convert all of this to HJSON
"prompts" : {
"userCredentials" : {
prompts: {
userCredentials: {
"art" : "usercred",
"mci" : {
"ET1" : {
@@ -106,8 +132,47 @@
}
}
},
///////////////////////////////////////////////////////////////////////
// File Base Related
///////////////////////////////////////////////////////////////////////
fileMenuCommand: {
art: FILPMPT
mci: {
TL1: {}
ET2: {
argName: menuOption
width: 20
maxLength: 20
textStyle: upper
focus: true
}
}
}
fileBaseRateEntryPrompt: {
art: RATEFILE
mci: {
SM1: {
argName: rating
items: [ "-----", "*----", "**---", "***--", "****-", "*****" ]
}
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
///////////////////////////////////////////////////////////////////////
// Standard / Required
//
// Prompts in this section are considered "standard" and are required
// to be present
//
///////////////////////////////////////////////////////////////////////
pause: {
//

View File

@@ -27,9 +27,6 @@ const buffers = require('buffers');
*/
// :TODO: ENH: Support nodeMax and tooManyArt
exports.getModule = TelnetBridgeModule;
exports.moduleInfo = {
name : 'Telnet Bridge',
desc : 'Connect to other Telnet Systems',
@@ -52,7 +49,9 @@ class TelnetClientConnection extends EventEmitter {
// client may have bailed
if(_.has(this, 'client.term.output')) {
this.client.term.output.unpipe(this.bridgeConnection);
if(this.bridgeConnection) {
this.client.term.output.unpipe(this.bridgeConnection);
}
this.client.term.output.resume();
}
}
@@ -123,18 +122,18 @@ class TelnetClientConnection extends EventEmitter {
}
exports.getModule = class TelnetBridgeModule extends MenuModule {
constructor(options) {
super(options);
function TelnetBridgeModule(options) {
MenuModule.call(this, options);
const self = this;
this.config = options.menuConfig.config;
this.config = options.menuConfig.config;
// defaults
this.config.port = this.config.port || 23;
}
// defaults
this.config.port = this.config.port || 23;
this.initSequence = function() {
initSequence() {
let clientTerminated;
const self = this;
async.series(
[
@@ -195,7 +194,5 @@ function TelnetBridgeModule(options) {
}
}
);
};
}
require('util').inherits(TelnetBridgeModule, MenuModule);
}
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -478,6 +478,302 @@
}
}
//////////////////// file base ////////////////////////////////
fileBase: {
mci: {
FN4: {
width: 18
textOverflow: ...
}
}
}
fileBaseListEntries: {
config: {
hashTagsSep: "|08, |07"
browseInfoFormat10: "|00|10{fileName} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
browseInfoFormat11: "|00|15{areaName}"
browseInfoFormat12: "|00|07{hashTags}"
browseInfoFormat13: "|00|07{estReleaseYear}"
browseInfoFormat14: "|00|07{dlCount}"
browseInfoFormat15: "{userRatingString}"
browseInfoFormat16: "{isQueued}"
browseInfoFormat17: "{webDlLink}{webDlExpire}"
webDlExpireTimeFormat: " [|08- |07exp] ddd, MMM Do @ h:mm a"
webDlLinkNeedsGenerated: "|08(|07press |10W |07to generate link|08)"
isQueuedIndicator: "|00|10YES"
isNotQueuedIndicator: "|00|07no"
userRatingTicked: "|00|15*"
userRatingUnticked: "|00|07-"
detailsGeneralInfoFormat10: "{fileName}"
detailsGeneralInfoFormat11: "|00|07{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08(|03{byteSize:,} |11B|08)"
detailsGeneralInfoFormat12: "|00|07{hashTags}"
detailsGeneralInfoFormat13: "{estReleaseYear}"
detailsGeneralInfoFormat14: "{dlCount}"
detailsGeneralInfoFormat15: "{userRatingString}"
detailsGeneralInfoFormat16: "{fileCrc32}"
detailsGeneralInfoFormat17: "{fileMd5}"
detailsGeneralInfoFormat18: "{fileSha1}"
detailsGeneralInfoFormat19: "{fileSha256}"
detailsGeneralInfoFormat20: "{uploadByUsername}"
detailsGeneralInfoFormat21: "{uploadTimestamp}"
detailsGeneralInfoFormat22: "{archiveTypeDesc}"
fileListEntryFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusFileListEntryFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
notAnArchiveFormat: "|00|08( |07{fileName} is not an archive |08)"
}
0: {
mci: {
MT1: {
height: 16
width: 45
}
HM2: {
focusTextStyle: first lower
}
TL11: {
width: 21
textOverflow: ...
}
TL12: {
width: 21
textOverflow: ...
}
TL13: { width: 21 }
TL14: { width: 21 }
TL15: { width: 21 }
TL16: { width: 21 }
TL17: { width: 73 }
}
}
1: {
mci: {
HM1: {
focusTextStyle: first lower
}
}
}
2: {
}
3: {
// details - nfo/readme
mci: {
MT1: {
height: 19
width: 79
}
}
}
4: {
mci: {
VM1: {
height: 17
width: 79
}
}
}
}
fileBaseSearch: {
mci: {
ET1: {
width: 42
}
BT2: {
focusTextStyle: first lower
}
ET3: {
width: 42
}
SM4: {
width: 14
justify: right
}
SM5: {
width: 14
justify: right
}
SM6: {
width: 14
justify: right
}
BT7: {
focusTextStyle: first lower
}
}
}
fileAreaFilterEditor: {
mci: {
ET1: {
width: 26
}
ET2: {
width: 26
}
SM3: {
width: 14
justify: right
}
SM4: {
width: 14
justify: right
}
SM5: {
width: 14
justify: right
}
ET6: {
width: 26
}
HM7: {
focusTextStyle: first lower
}
}
}
fileBaseDownloadManager: {
config: {
queueListFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusQueueListFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
}
0: {
mci: {
VM1: {
height: 11
width: 69
}
HM2: {
width: 50
focusTextStyle: first lower
}
}
}
}
fileBaseUploadFiles: {
config: {
// processing
processingInfoFormat10: "{stepIndicatorText}"
processingInfoFormat11: "|00|15{fileName} |08- |11{currentFileNum} |08/ |11{totalFileNum}"
// details entry
fileDetailsInfoFormat10: "{fileName} |02■"
// dupes
dupeInfoFormat: "|00|11{fileName:<53.52}|03{areaName}"
}
// options
0: {
mci: {
SM1: {
width: 14
justify: right
focusTextStyle: first lower
}
TM2: {
focusTextStyle: first lower
styleSGR1: |00|08
}
ET3: {
width: 40
}
HM4: {
focusTextStyle: first lower
}
}
}
// processing/scanning
1: {
mci: {
TL1: { width: 48 }
TL2: { width: 48 }
TL3: { width: 48 }
MT4: {
height: 6
width: 68
mode: preview
}
TL10: { width: 48 }
TL11: { width: 48 }
}
}
// file details
2: {
mci: {
MT1: {
height: 14
width: 45
}
ET2: {
width: 25
}
ME3: {
width: 4
}
BT4: {
focusTextStyle: first lower
}
}
}
// dupes
3: {
mci: {
VM1: {
height: 17
width: 75
}
}
}
}
fileTransferProtocolSelection: {
config: {
protListFormat: "|00|03{name}"
protListFocusFormat: "|00|19|15{name}"
}
0: {
mci: {
VM1: {
height: 15
width: 30
focusTextStyle: first lower
}
}
}
}
//////////////////////////////// ERC ///////////////////////////////
ercClient: {
config: {
//chatEntryFormat: "|00|08[|03{bbsTag}|08] |10{userName}|08: |02{message}"
@@ -493,6 +789,14 @@
}
}
}
fileMenuCommand: {
mci: {
TL1: {
text: "|00|15|MD|08 >> |03active filter|08: |10|FN"
}
}
}
}
}
}

706
mods/upload.js Normal file
View File

@@ -0,0 +1,706 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule;
const stringFormat = require('../core/string_format.js');
const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
const getAreaDefaultStorageDirectory = require('../core/file_base_area.js').getAreaDefaultStorageDirectory;
const scanFile = require('../core/file_base_area.js').scanFile;
const getFileAreaByTag = require('../core/file_base_area.js').getFileAreaByTag;
const ansiGoto = require('../core/ansi_term.js').goto;
const moveFileWithCollisionHandling = require('../core/file_util.js').moveFileWithCollisionHandling;
const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTerminatingSeparator;
const Log = require('../core/logger.js').log;
const Errors = require('../core/enig_error.js').Errors;
const FileEntry = require('../core/file_entry.js');
const enigmaToAnsi = require('../core/color_codes.js').enigmaToAnsi;
// 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 }
);
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 the file entry did *not* have a desc, take the user desc
if(!this.fileEntryHasDetectedDesc(newEntry)) {
newEntry.desc = newValues.shortDesc.trim();
}
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.series(
[
function prepArtAndViewController(callback) {
return self.prepViewControllerWithArt(
'fileDetails',
FormIds.fileDetails,
{ clearScreen : true, trailingLF : false },
callback
);
},
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(self.fileEntryHasDetectedDesc(fileEntry)) {
descView.setPropertyValue('mode', 'preview');
descView.setText(fileEntry.desc);
descView.acceptsFocus = false;
self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.tags);
} else {
descView.setPropertyValue('mode', 'edit');
descView.setText('');
descView.acceptsFocus = true;
self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.desc);
}
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
}
);
}
};

View File

@@ -1,15 +1,14 @@
/* jslint node: true */
'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule;
//var userDb = require('../core/database.js').dbs.user;
var getUserList = require('../core/user.js').getUserList;
var ViewController = require('../core/view_controller.js').ViewController;
const MenuModule = require('../core/menu_module.js').MenuModule;
const getUserList = require('../core/user.js').getUserList;
const ViewController = require('../core/view_controller.js').ViewController;
const stringFormat = require('../core/string_format.js');
var moment = require('moment');
var async = require('async');
var _ = require('lodash');
const moment = require('moment');
const async = require('async');
const _ = require('lodash');
/*
Available listFormat/focusListFormat object members:
@@ -29,85 +28,85 @@ exports.moduleInfo = {
author : 'NuSkooler',
};
exports.getModule = UserListModule;
var MciCodeIds = {
const MciViewIds = {
UserList : 1,
};
function UserListModule(options) {
MenuModule.call(this, options);
}
exports.getModule = class UserListModule extends MenuModule {
constructor(options) {
super(options);
}
require('util').inherits(UserListModule, MenuModule);
UserListModule.prototype.mciReady = function(mciData, cb) {
var self = this;
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
var userList = [];
var USER_LIST_OPTS = {
properties : [ 'location', 'affiliation', 'last_login_timestamp' ],
};
async.series(
[
// :TODO: These two functions repeated all over -- need DRY
function callParentMciReady(callback) {
UserListModule.super_.prototype.mciReady.call(self, mciData, callback);
},
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
getUserList(USER_LIST_OPTS, function got(err, ul) {
userList = ul;
callback(err);
});
},
function populateList(callback) {
var userListView = vc.getView(MciCodeIds.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) {
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading user list');
return cb(err);
}
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
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);
}
);
});
}
};

View File

@@ -18,66 +18,67 @@ exports.moduleInfo = {
packageName : 'codes.l33t.enigma.whosonline'
};
exports.getModule = WhosOnlineModule;
const MciCodeIds = {
const MciViewIds = {
OnlineList : 1,
};
function WhosOnlineModule(options) {
MenuModule.call(this, options);
}
exports.getModule = class WhosOnlineModule extends MenuModule {
constructor(options) {
super(options);
}
require('util').inherits(WhosOnlineModule, MenuModule);
WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
async.series(
[
function callParentMciReady(callback) {
return WhosOnlineModule.super_.prototype.mciReady.call(self, mciData, callback);
},
function loadFromConfig(callback) {
const loadOpts = {
callingMenu : self,
mciMap : mciData.menu,
noInput : true,
};
return vc.loadFromMenuConfig(loadOpts, callback);
},
function populateList(callback) {
const onlineListView = vc.getView(MciCodeIds.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 = _.capitalize(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) {
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
self.client.log.error( { error : err.message }, 'Error loading who\'s online');
return cb(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);
}
);
});
}
};