From 5a0b291a026cf2cd2bf06e7443fbe94cf21c188b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 28 Sep 2016 21:54:25 -0600 Subject: [PATCH 01/86] * Some WIP on file area releated stuff - various partially implemented pieces coming together * Some changes to database.js: Triggers for FTS were not being created properly * Misc fixes & improvements --- core/acs.js | 6 ++ core/bbs.js | 22 +++- core/config.js | 27 +++++ core/database.js | 44 +++++--- core/door.js | 1 + core/enig_error.js | 2 + core/file_area.js | 31 ++++++ core/file_entry.js | 171 +++++++++++++++++++++++++++++++ core/string_format.js | 8 ++ core/string_util.js | 19 ++++ core/transfer_file.js | 132 ++++++++++++++++++++++++ misc/startup_banner.asc | 9 ++ mods/file_area_list.js | 222 ++++++++++++++++++++++++++++++++++++++++ mods/msg_list.js | 2 +- 14 files changed, 675 insertions(+), 21 deletions(-) create mode 100644 core/file_area.js create mode 100644 core/file_entry.js create mode 100644 core/transfer_file.js create mode 100644 misc/startup_banner.asc create mode 100644 mods/file_area_list.js diff --git a/core/acs.js b/core/acs.js index df89be9b..3d2fd678 100644 --- a/core/acs.js +++ b/core/acs.js @@ -33,6 +33,10 @@ class ACS { return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead); } + hasFileAreaRead(area) { + return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); + } + getConditionalValue(condArray, memberName) { assert(_.isArray(condArray)); assert(_.isString(memberName)); @@ -59,6 +63,8 @@ class ACS { ACS.Defaults = { MessageAreaRead : 'GM[users]', MessageConfRead : 'GM[users]', + + FileAreaRead : 'GM[users]', }; module.exports = ACS; \ No newline at end of file diff --git a/core/bbs.js b/core/bbs.js index 36243227..d1ea390f 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -16,6 +16,8 @@ const async = require('async'); const util = require('util'); const _ = require('lodash'); const mkdirs = require('fs-extra').mkdirs; +const fs = require('fs'); +const paths = require('path'); // our main entry point exports.bbsMain = bbsMain; @@ -71,14 +73,23 @@ function bbsMain() { if(err) { console.error('Error initializing: ' + util.inspect(err)); } - callback(err); + return callback(err); }); }, function listenConnections(callback) { - startListening(callback); + return startListening(callback); } ], function complete(err) { + // note this is escaped: + fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { + console.info('ENiGMA½ Copyright (c) 2014-2016 Bryan Ashby'); + if(!err) { + console.info(banner); + } + console.info('System started!'); + }); + if(err) { console.error('Error initializing: ' + util.inspect(err)); } @@ -87,7 +98,9 @@ function bbsMain() { } function shutdownSystem() { - logger.log.info('Process interrupted, shutting down...'); + const msg = 'Process interrupted. Shutting down...'; + console.info(msg); + logger.log.info(msg); async.series( [ @@ -114,7 +127,8 @@ function shutdownSystem() { } ], () => { - process.exit(); + console.info('Goodbye!'); + return process.exit(); } ); } diff --git a/core/config.js b/core/config.js index c53f37f0..31c77275 100644 --- a/core/config.js +++ b/core/config.js @@ -222,6 +222,33 @@ function getDefaultConfig() { } }, + fileTransferProtocols : { + zmodem8kSz : { + name : 'ZModem 8k', + type : 'external', + external : { + sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + sendArgs : [ + '--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePath}' + ], + escapeTelnet : true, // set to true to escape Telnet codes such as IAC + } + }, + + zmodem8kSexyz : { + name : 'ZModem 8k', + type : 'external', + external : { + // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems + sendCmd : 'sexyz', + sendArgs : [ + '-telnet', 'sz', '{filePath}' + ], + escapeTelnet : true, // set to true to escape Telnet codes such as IAC + } + } + + }, messageAreaDefaults : { // diff --git a/core/database.js b/core/database.js index e5e74571..f4bfc752 100644 --- a/core/database.js +++ b/core/database.js @@ -175,17 +175,23 @@ const DB_INIT_TABLE = { dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN DELETE FROM message_fts WHERE docid=old.rowid; - END; + END;` + ); - CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN DELETE FROM message_fts WHERE docid=old.rowid; - END; + END;` + ); - CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); - END; + END;` + ); - CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); END;` ); @@ -250,7 +256,7 @@ const DB_INIT_TABLE = { file_sha1 VARCHAR NOT NULL, file_name, /* FTS @ file_fts */ desc, /* FTS @ file_fts */ - desc_long, /* FTS @ file_fts */ + desc_long, /* FTS @ file_fts */ upload_by_username VARCHAR NOT NULL, upload_timestamp DATETIME NOT NULL );` @@ -273,18 +279,24 @@ const DB_INIT_TABLE = { dbs.file.run( `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN DELETE FROM file_fts WHERE docid=old.rowid; - END; - - CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN + END;` + ); + + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN DELETE FROM file_fts WHERE docid=old.rowid; - END; + END;` + ); - CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN - INSERT INTO file_fts(docid, file_name, desc, long_desc) VALUES(new.rowid, new.file_name, new.desc, new.long_desc); - END; + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN + INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); + END;` + ); - CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN - INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.long_desc); + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN + INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); END;` ); diff --git a/core/door.js b/core/door.js index 302de522..5670db1e 100644 --- a/core/door.js +++ b/core/door.js @@ -100,6 +100,7 @@ Door.prototype.run = function() { } // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS + // :TODO: Use .map() here let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified for(let i = 0; i < args.length; ++i) { diff --git a/core/enig_error.js b/core/enig_error.js index adf5eef6..4695608b 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -19,6 +19,7 @@ class EnigError extends Error { } } +// :TODO: Just use EnigError for all class EnigMenuError extends EnigError { } exports.EnigError = EnigError; @@ -27,4 +28,5 @@ exports.EnigMenuError = EnigMenuError; exports.Errors = { General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), MenuStack : (reason, reasonCode) => new EnigMenuError('Menu stack error', -33001, reason, reasonCode), + DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), }; diff --git a/core/file_area.js b/core/file_area.js new file mode 100644 index 00000000..8026c2ff --- /dev/null +++ b/core/file_area.js @@ -0,0 +1,31 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Config = require('./config.js').config; +const Log = require('./logger.js').log; + +// deps +const _ = require('lodash'); + +exports.getAvailableFileAreas = getAvailableFileAreas; + +exports.getFileAreaByTag = getFileAreaByTag; + +function getAvailableFileAreas(client, options) { + options = options || { includeSystemInternal : false }; + + // perform ACS check per conf & omit system_internal if desired + return _.omit(Config.fileAreas.areas, (area, areaTag) => { + /* if(!options.includeSystemInternal && 'system_internal' === confTag) { + return true; + }*/ + + return !client.acs.hasFileAreaRead(area); + }); +} + + +function getFileAreaByTag(areaTag) { + return Config.fileAreas.areas[areaTag]; +} \ No newline at end of file diff --git a/core/file_entry.js b/core/file_entry.js new file mode 100644 index 00000000..fbb97c78 --- /dev/null +++ b/core/file_entry.js @@ -0,0 +1,171 @@ +/* jslint node: true */ +'use strict'; + +const fileDb = require('./database.js').dbs.file; +const Errors = require('./enig_error.js').Errors; + +// deps +const async = require('async'); +const _ = require('lodash'); + +const FILE_TABLE_MEMBERS = [ + 'file_id', 'area_tag', 'file_sha1', 'file_name', + 'desc', 'desc_long', 'upload_by_username', 'upload_timestamp' +]; + +module.exports = class FileEntry { + constructor(options) { + options = options || {}; + + this.fileId = options.fileId || 0; + this.areaTag = options.areaTag || ''; + this.meta = {}; + this.hashTags = new Set(); + } + + load(fileId, cb) { + const self = this; + + async.series( + [ + function loadBasicEntry(callback) { + fileDb.get( + `SELECT ${FILE_TABLE_MEMBERS.join(', ')} + FROM file + WHERE file_id=? + LIMIT 1;`, + [ fileId ], + (err, file) => { + if(err) { + return callback(err); + } + + if(!file) { + return callback(Errors.DoesNotExist('No file is available by that ID')); + } + + // assign props from |file| + FILE_TABLE_MEMBERS.forEach(prop => { + self[_.camelCase(prop)] = file[prop]; + }); + + return callback(null); + } + ); + }, + function loadMeta(callback) { + return self.loadMeta(callback); + }, + function loadHashTags(callback) { + return self.loadHashTags(callback); + } + ], + err => { + return cb(err); + } + ); + } + + loadMeta(cb) { + fileDb.each( + `SELECT meta_name, meta_value + FROM file_meta + WHERE file_id=?;`, + [ this.fileId ], + (err, meta) => { + if(meta) { + this.meta[meta.meta_name] = meta.meta_value; + } + }, + err => { + return cb(err); + } + ); + } + + loadHashTags(cb) { + fileDb.each( + `SELECT ht.hash_tag_id, ht.hash_tag + FROM hash_tag ht + WHERE ht.hash_tag_id IN ( + SELECT hash_tag_id + FROM file_hash_tag + WHERE file_id=? + );`, + [ this.fileId ], + (err, hashTag) => { + if(hashTag) { + this.hashTags.add(hashTag.hash_tag); + } + }, + err => { + return cb(err); + } + ); + } + + static findFiles(criteria, cb) { + // :TODO: build search here - return [ fileid1, fileid2, ... ] + // free form + // areaTag + // tags + // order by + // sort + + let sql = + `SELECT file_id + FROM file`; + + let sqlWhere = ''; + + function appendWhereClause(clause) { + if(sqlWhere) { + sqlWhere += ' AND '; + } else { + sqlWhere += ' WHERE '; + } + sqlWhere += clause; + } + + if(criteria.areaTag) { + appendWhereClause(`area_tag="${criteria.areaTag}"`); + } + + if(criteria.search) { + appendWhereClause( + `file_id IN ( + SELECT rowid + FROM file_fts + WHERE file_fts MATCH "${criteria.search.replace(/"/g,'""')}" + )` + ); + } + + if(Array.isArray(criteria.hashTags)) { + appendWhereClause( + `file_id IN ( + SELECT file_id + FROM file_hash_tag + WHERE hash_tag_id IN ( + SELECT hash_tag_id + FROM hash_tag + WHERE hash_tag IN (${criteria.hashTags.join(',')}) + ) + )` + ); + } + + // :TODO: criteria.orderBy + // :TODO: criteria.sort + + sql += sqlWhere + ';'; + const matchingFileIds = []; + fileDb.each(sql, (err, fileId) => { + if(fileId) { + matchingFileIds.push(fileId.file_id); + } + }, err => { + return cb(err, matchingFileIds); + }); + } +}; diff --git a/core/string_format.js b/core/string_format.js index 9abac186..dd1ece78 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -6,6 +6,8 @@ const pad = require('./string_util.js').pad; const stylizeString = require('./string_util.js').stylizeString; const renderStringLength = require('./string_util.js').renderStringLength; const renderSubstr = require('./string_util.js').renderSubstr; +const formatByteSize = require('./string_util.js').formatByteSize; +const formatByteSizeAbbr = require('./string_util.js').formatByteSizeAbbr; // deps const _ = require('lodash'); @@ -265,6 +267,12 @@ const transformers = { styleSmallI : (s) => stylizeString(s, 'small i'), styleMixed : (s) => stylizeString(s, 'mixed'), styleL33t : (s) => stylizeString(s, 'l33t'), + + // toMegs(), toKilobytes(), ... + // toList(), toCommaList(), + sizeWithAbbr : (n) => formatByteSize(n, true, 2), + sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), + sizeAbbr : (n) => formatByteSizeAbbr(n), }; function transformValue(transformerName, value) { diff --git a/core/string_util.js b/core/string_util.js index ab543a4e..0ba8dec9 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -13,6 +13,8 @@ exports.debugEscapedString = debugEscapedString; exports.stringFromNullTermBuffer = stringFromNullTermBuffer; exports.renderSubstr = renderSubstr; exports.renderStringLength = renderStringLength; +exports.formatByteSizeAbbr = formatByteSizeAbbr; +exports.formatByteSize = formatByteSize; exports.cleanControlCodes = cleanControlCodes; // :TODO: create Unicode verison of this @@ -286,6 +288,23 @@ function renderStringLength(s) { return len; } +const SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) + +function formatByteSizeAbbr(byteSize) { + return SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; +} + +function formatByteSize(byteSize, withAbbr, decimals) { + withAbbr = withAbbr || false; + decimals = decimals || 3; + const i = Math.floor(Math.log(byteSize) / Math.log(1024)); + let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); + if(withAbbr) { + result += ` ${SIZE_ABBRS[i]}`; + } + return result; +} + // :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's //const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g; diff --git a/core/transfer_file.js b/core/transfer_file.js new file mode 100644 index 00000000..4faf5aa3 --- /dev/null +++ b/core/transfer_file.js @@ -0,0 +1,132 @@ +/* jslint node: true */ +'use strict'; + +// enigma-bbs +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').config; +const stringFormat = require('./string_format.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); +const pty = require('ptyw.js'); + +/* + Resources + + ZModem + * http://gallium.inria.fr/~doligez/zmodem/zmodem.txt + * https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c +*/ + +exports.moduleInfo = { + name : 'Transfer file', + desc : 'Sends or receives a file(s)', + author : 'NuSkooler', +}; + +exports.getModule = class TransferFileModule extends MenuModule { + constructor(options) { + super(options); + + this.config = this.menuConfig.config || {}; + this.config.protocol = this.config.protocol || 'zmodem8kSz'; + this.config.direction = this.config.direction || 'send'; + + this.protocolConfig = Config.fileTransferProtocols[this.config.protocol]; + + // :TODO: bring in extraArgs for path(s) to send when sending; Allow to hard code in config (e.g. for info pack/static downloads) + } + + restorePipeAfterExternalProc(pipe) { + if(!this.pipeRestored) { + this.pipeRestored = true; + + this.client.term.output.unpipe(pipe); + this.client.term.output.resume(); + } + } + + sendFiles(cb) { + async.eachSeries(this.sendQueue, (filePath, next) => { + // :TODO: built in protocols + // :TODO: use protocol passed in + this.executeExternalProtocolHandler(filePath, err => { + return next(err); + }); + }, err => { + return cb(err); + }); + } + + executeExternalProtocolHandler(filePath, cb) { + const external = this.protocolConfig.external; + const cmd = external[`${this.config.direction}Cmd`]; + const args = external[`${this.config.direction}Args`].map(arg => { + return stringFormat(arg, { + filePath : filePath, + }); + }); + + /*this.client.term.rawWrite(new Buffer( + [ + 255, 253, 0, // IAC DO TRANSMIT_BINARY + 255, 251, 0, // IAC WILL TRANSMIT_BINARY + ] + ));*/ + + const externalProc = pty.spawn(cmd, args, { + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + // :TODO: cwd + // :TODO: anything else?? + //env : self.exeInfo.env, + }); + + this.client.term.output.pipe(externalProc); + + /*this.client.term.output.on('data', data => { + // let tmp = data.toString('binary').replace(/\xff\xff/g, '\xff'); + // proc.write(new Buffer(tmp, 'binary')); + proc.write(data); + }); + */ + externalProc.on('data', data => { + // needed for things like sz/rz + if(external.escapeTelnet) { + const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); + this.client.term.rawWrite(new Buffer(tmp, 'binary')); + } else { + this.client.term.rawWrite(data); + } + }); + + externalProc.once('close', () => { + return this.restorePipeAfterExternalProc(externalProc); + }); + + externalProc.once('exit', exitCode => { + this.restorePipeAfterExternalProc(externalProc); + externalProc.removeAllListeners(); + + return cb(null); + }); + } + + initSequence() { + const self = this; + + async.series( + [ + function validateConfig(callback) { + // :TODO: + return callback(null); + }, + function transferFiles(callback) { + self.sendQueue = [ '/home/nuskooler/Downloads/fdoor100.zip' ]; // :TODO: testing of course + return self.sendFiles(callback); + } + ] + ); + } +}; diff --git a/misc/startup_banner.asc b/misc/startup_banner.asc new file mode 100644 index 00000000..b758e066 --- /dev/null +++ b/misc/startup_banner.asc @@ -0,0 +1,9 @@ +_____________________ _____ ____________________ __________\_ / +\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! +// __|___// | \// |// | \// | | \// \ /___ /_____ +/____ _____| __________ ___|__| ____| \ / _____ \ +---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + +------------------------------------------------------------------------------- diff --git a/mods/file_area_list.js b/mods/file_area_list.js new file mode 100644 index 00000000..ea6b1507 --- /dev/null +++ b/mods/file_area_list.js @@ -0,0 +1,222 @@ +/* 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 FileArea = require('../core/file_area.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); + +/* + Misc TODO + * Allow rating to be user defined colors & characters/etc. + * + + + Well known file entry meta values: + * upload_by_username + * upload_by_user_id + * file_md5 + * file_sha256 + * file_crc32 + * est_release_year + * dl_count + * byte_size + * user_rating + * +*/ + +exports.moduleInfo = { + name : 'File Area List', + desc : 'Lists contents of file an file area', + author : 'NuSkooler', +}; + +const FormIds = { + browse : 0, + details : 1, +}; + +const MciViewIds = { + browse : { + desc : 1, + navMenu : 2, + // 10+: customs + }, +}; + +exports.getModule = class FileAreaList extends MenuModule { + + constructor(options) { + super(options); + + const config = this.menuConfig.config; + + if(options.extraArgs) { + this.filterCriteria = options.extraArgs.filterCriteria; + } + + this.filterCriteria = this.filterCriteria || { + // :TODO: set area tag - all in current area by default + }; + } + + enter() { + super.enter(); + } + + leave() { + super.leave(); + } + + initSequence() { + const self = this; + + async.series( + [ + function beforeArt(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayBrowsePage(false, callback); + } + ], + () => { + self.finishedLoading(); + } + ); + } + + displayBrowsePage(clearScreen, cb) { + const self = this; + const config = this.menuConfig.config; + + async.waterfall( + [ + function clearAndDisplayArt(callback) { + if (clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } + + theme.displayThemedAsset( + //config.art.browse, + 'FBRWSE', + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function prepeareViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.browse)) { + const vc = self.addViewController( + 'browse', + new ViewController( { client : self.client, formId : FormIds.browse } ) + ); + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.browse, + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } + + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); + + return callback(null); + }, + function fetchEntryData(callback) { + return self.loadFileIds(callback); + }, + function loadCurrentFileInfo(callback) { + self.currentFileEntry = new FileEntry(); + + self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { + return callback(err); + }); + }, + function populateViews(callback) { + if(_.isString(self.currentFileEntry.desc)) { + const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); + if(descView) { + descView.setText(self.currentFileEntry.desc); + //descView.redraw(); + } + } + + const currEntry = self.currentFileEntry; + const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; + const area = FileArea.getFileAreaByTag(currEntry.areaTag); + const hashTagsSep = config.hashTagsSep || ', '; + const entryInfo = { + fileId : currEntry.fileId, + areaTag : currEntry.areaTag, + areaName : area.name || 'N/A', + areaDesc : area.desc || 'N/A', + fileSha1 : currEntry.fileSha1, + fileName : currEntry.fileName, + desc : currEntry.desc, + descLong : currEntry.descLong, + uploadByUsername : currEntry.uploadByUsername, + uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), + hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), + }; + + const META_NUMBERS = [ 'byte_size', 'dl_count' ]; + _.forEach(self.currentFileEntry.meta, (value, name) => { + if(META_NUMBERS.indexOf(name) > -1) { + value = parseInt(value); + } + entryInfo[_.camelCase(name)] = value; + }); + + + + // entryInfo.fileSize = 1241234; // :TODO: REMOVE ME! + + // 10+ are custom textviews + let textView; + let customMciId = 10; + + while( (textView = self.viewControllers.browse.getView(customMciId)) ) { + const key = `browseInfoFormat${customMciId}`; + const format = config[key]; + + if(format) { + textView.setText(stringFormat(format, entryInfo)); + } + + ++customMciId; + } + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + + loadFileIds(cb) { + this.fileListPosition = 0; + + FileEntry.findFiles(this.filterCriteria, (err, fileIds) => { + this.fileList = fileIds; + return cb(err); + }); + } + +}; diff --git a/mods/msg_list.js b/mods/msg_list.js index 83b4fe90..dc82d437 100644 --- a/mods/msg_list.js +++ b/mods/msg_list.js @@ -127,7 +127,7 @@ MessageListModule.prototype.enter = function() { if(this.messageAreaTag) { this.tempMessageConfAndAreaSwitch(this.messageAreaTag); } else { - this.messageAreaTag = this.messageAreaTag = this.client.user.properties.message_area_tag; + this.messageAreaTag = this.client.user.properties.message_area_tag; } }; From b9ef561058f77d215db7ba6c95b3d95e65844a0c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 28 Sep 2016 22:26:06 -0600 Subject: [PATCH 02/86] Parse file meta values @ load (e.g. to number --- core/file_entry.js | 18 +++++++++++++++++- mods/file_area_list.js | 19 ++++++++----------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index fbb97c78..dadafb1d 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -13,6 +13,19 @@ const FILE_TABLE_MEMBERS = [ 'desc', 'desc_long', 'upload_by_username', 'upload_timestamp' ]; +const FILE_WELL_KNOWN_META = { + // name -> *read* converter, if any + upload_by_username : null, + upload_by_user_id : null, + file_md5 : null, + file_sha256 : null, + file_crc32 : null, + est_release_year : parseInt, + dl_count : parseInt, + byte_size : parseInt, + user_rating : parseInt, +}; + module.exports = class FileEntry { constructor(options) { options = options || {}; @@ -74,7 +87,8 @@ module.exports = class FileEntry { [ this.fileId ], (err, meta) => { if(meta) { - this.meta[meta.meta_name] = meta.meta_value; + const conv = FILE_WELL_KNOWN_META[meta.meta_name]; + this.meta[meta.meta_name] = conv ? conv(meta.meta_value) : meta.meta_value; } }, err => { @@ -104,6 +118,8 @@ module.exports = class FileEntry { ); } + static getWellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); } + static findFiles(criteria, cb) { // :TODO: build search here - return [ fileid1, fileid2, ... ] // free form diff --git a/mods/file_area_list.js b/mods/file_area_list.js index ea6b1507..010aa4b3 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -107,8 +107,7 @@ exports.getModule = class FileAreaList extends MenuModule { } theme.displayThemedAsset( - //config.art.browse, - 'FBRWSE', + config.art.browse, self.client, { font : self.menuConfig.font, trailingLF : false }, (err, artData) => { @@ -174,18 +173,16 @@ exports.getModule = class FileAreaList extends MenuModule { hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), }; - const META_NUMBERS = [ 'byte_size', 'dl_count' ]; - _.forEach(self.currentFileEntry.meta, (value, name) => { - if(META_NUMBERS.indexOf(name) > -1) { - value = parseInt(value); - } + // + // 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 = currEntry.meta[name] || ''; entryInfo[_.camelCase(name)] = value; }); - - - // entryInfo.fileSize = 1241234; // :TODO: REMOVE ME! - // 10+ are custom textviews let textView; let customMciId = 10; From c81b6789f4aa78459e0d04c73c60cdc07b9de9d8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Oct 2016 13:22:34 -0600 Subject: [PATCH 03/86] Add 'pcansi' support for ZOC terminal --- core/client_term.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/client_term.js b/core/client_term.js index 8ef4753d..c9a9e66f 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -131,15 +131,16 @@ ClientTerminal.prototype.isANSI = function() { // ansi-bbs: // * fTelnet // + // pcansi: + // * ZOC + // // screen: // * ConnectBot (Android) // // linux: // * JuiceSSH (note: TERM=linux also) // - - // :TODO: Others?? - return [ 'ansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1; + return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1; }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) From ac35d3506dbcefb8542edcefe40cfcc78dfa1d2f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Oct 2016 13:25:32 -0600 Subject: [PATCH 04/86] File area updates WIP --- core/conf_area_util.js | 30 +++++++++++++++ core/config.js | 4 +- core/enig_error.js | 8 ++-- core/file_area.js | 84 +++++++++++++++++++++++++++++++++++++++--- core/file_entry.js | 8 ++-- core/message_area.js | 38 ++++--------------- mods/file_area_list.js | 11 +++++- 7 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 core/conf_area_util.js diff --git a/core/conf_area_util.js b/core/conf_area_util.js new file mode 100644 index 00000000..6009bb34 --- /dev/null +++ b/core/conf_area_util.js @@ -0,0 +1,30 @@ +/* jslint node: true */ +'use strict'; + +// deps +const _ = require('lodash'); + +exports.sortAreasOrConfs = sortAreasOrConfs; + +// +// Method for sorting message, file, etc. areas and confs +// If the sort key is present and is a number, sort in numerical order; +// Otherwise, use a locale comparison on the sort key or name as a fallback +// +function sortAreasOrConfs(areasOrConfs, type) { + let entryA; + let entryB; + + areasOrConfs.sort((a, b) => { + entryA = a[type]; + entryB = b[type]; + + if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { + return entryA.sort - entryB.sort; + } else { + const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; + const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; + return keyA.localeCompare(keyB); + } + }); +} \ No newline at end of file diff --git a/core/config.js b/core/config.js index 31c77275..78e55c42 100644 --- a/core/config.js +++ b/core/config.js @@ -236,7 +236,7 @@ function getDefaultConfig() { }, zmodem8kSexyz : { - name : 'ZModem 8k', + name : 'ZModem (SEXYZ)', type : 'external', external : { // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems @@ -244,7 +244,7 @@ function getDefaultConfig() { sendArgs : [ '-telnet', 'sz', '{filePath}' ], - escapeTelnet : true, // set to true to escape Telnet codes such as IAC + escapeTelnet : false, // -telnet option does this for us } } diff --git a/core/enig_error.js b/core/enig_error.js index 4695608b..69c6fb3c 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -19,14 +19,12 @@ class EnigError extends Error { } } -// :TODO: Just use EnigError for all -class EnigMenuError extends EnigError { } - exports.EnigError = EnigError; -exports.EnigMenuError = EnigMenuError; exports.Errors = { General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), - MenuStack : (reason, reasonCode) => new EnigMenuError('Menu stack error', -33001, reason, reasonCode), + MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), + AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), + Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), }; diff --git a/core/file_area.js b/core/file_area.js index 8026c2ff..08c600af 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -2,30 +2,102 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; -const Log = require('./logger.js').log; +const Config = require('./config.js').config; +const Errors = require('./enig_error.js').Errors; +const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; // deps const _ = require('lodash'); +const async = require('async'); exports.getAvailableFileAreas = getAvailableFileAreas; - +exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; +exports.getDefaultFileArea = getDefaultFileArea; exports.getFileAreaByTag = getFileAreaByTag; +exports.changeFileAreaWithOptions = changeFileAreaWithOptions; + +const WellKnownAreaTags = exports.WellKnownAreaTags = { + Invalid : '', + MessageAreaAttach : 'message_area_attach', +}; function getAvailableFileAreas(client, options) { options = options || { includeSystemInternal : false }; // perform ACS check per conf & omit system_internal if desired return _.omit(Config.fileAreas.areas, (area, areaTag) => { - /* if(!options.includeSystemInternal && 'system_internal' === confTag) { + if(!options.includeSystemInternal && WellKnownAreaTags.MessageAreaAttach === areaTag) { return true; - }*/ + } return !client.acs.hasFileAreaRead(area); }); } +function getSortedAvailableFileAreas(client, options) { + const areas = _.map(getAvailableFileAreas(client, options), (v, k) => { + return { + areaTag : k, + area : v + }; + }); + + sortAreasOrConfs(areas, 'area'); + return areas; +} + +function getDefaultFileArea(client, disableAcsCheck) { + let defaultArea = _.findKey(Config.fileAreas, o => o.default); + if(defaultArea) { + const area = Config.fileAreas.areas[defaultArea]; + if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) { + return defaultArea; + } + } + + // just use anything we can + defaultArea = _.findKey(Config.fileAreas.areas, (area, areaTag) => { + return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); + }); + + return defaultArea; +} function getFileAreaByTag(areaTag) { return Config.fileAreas.areas[areaTag]; -} \ No newline at end of file +} + +function changeFileAreaWithOptions(client, areaTag, options, cb) { + async.waterfall( + [ + function getArea(callback) { + const area = getFileAreaByTag(areaTag); + return callback(area ? null : Errors.Invalid('Invalid file areaTag'), area); + }, + function validateAccess(area, callback) { + if(!client.acs.hasFileAreaRead(area)) { + return callback(Errors.AccessDenied('No access to this area')); + } + }, + function changeArea(area, callback) { + if(true === options.persist) { + client.user.persistProperty('file_area_tag', areaTag, err => { + return callback(err, area); + }); + } else { + client.user.properties['file_area_tag'] = areaTag; + return callback(null, area); + } + } + ], + (err, area) => { + if(!err) { + client.log.info( { areaTag : areaTag, area : area }, 'Current file area changed'); + } else { + client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change file area'); + } + + return cb(err); + } + ); +} diff --git a/core/file_entry.js b/core/file_entry.js index dadafb1d..ffafbe85 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -20,10 +20,10 @@ const FILE_WELL_KNOWN_META = { file_md5 : null, file_sha256 : null, file_crc32 : null, - est_release_year : parseInt, - dl_count : parseInt, - byte_size : parseInt, - user_rating : parseInt, + est_release_year : (y) => parseInt(y) || new Date().getFullYear(), + dl_count : (d) => parseInt(d) || 0, + byte_size : (b) => parseInt(b) || 0, + user_rating : (r) => Math.min(parseInt(r) || 0, 5), }; module.exports = class FileEntry { diff --git a/core/message_area.js b/core/message_area.js index 2f74357b..95cadf1c 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -2,11 +2,12 @@ 'use strict'; // ENiGMA½ -const msgDb = require('./database.js').dbs.message; -const Config = require('./config.js').config; -const Message = require('./message.js'); -const Log = require('./logger.js').log; -const msgNetRecord = require('./msg_network.js').recordMessage; +const msgDb = require('./database.js').dbs.message; +const Config = require('./config.js').config; +const Message = require('./message.js'); +const Log = require('./logger.js').log; +const msgNetRecord = require('./msg_network.js').recordMessage; +const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; // deps const async = require('async'); @@ -32,29 +33,6 @@ exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.persistMessage = persistMessage; exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; -// -// Method for sorting Message areas and conferences -// If the sort key is present and is a number, sort in numerical order; -// Otherwise, use a locale comparison on the sort key or name as a fallback -// -function sortAreasOrConfs(areasOrConfs, type) { - let entryA; - let entryB; - - areasOrConfs.sort((a, b) => { - entryA = a[type]; - entryB = b[type]; - - if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { - return entryA.sort - entryB.sort; - } else { - const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; - const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; - return keyA.localeCompare(keyB); - } - }); -} - function getAvailableMessageConferences(client, options) { options = options || { includeSystemInternal : false }; @@ -269,7 +247,7 @@ function changeMessageConference(client, confTag, cb) { } function changeMessageAreaWithOptions(client, areaTag, options, cb) { - options = options || {}; + options = options || {}; // :TODO: this is currently pointless... cb is required... async.waterfall( [ @@ -305,7 +283,7 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area'); } - cb(err); + return cb(err); } ); } diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 010aa4b3..a56f0a0c 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -151,7 +151,6 @@ exports.getModule = class FileAreaList extends MenuModule { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); if(descView) { descView.setText(self.currentFileEntry.desc); - //descView.redraw(); } } @@ -159,6 +158,7 @@ exports.getModule = class FileAreaList extends MenuModule { const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; const area = FileArea.getFileAreaByTag(currEntry.areaTag); const hashTagsSep = config.hashTagsSep || ', '; + const entryInfo = { fileId : currEntry.fileId, areaTag : currEntry.areaTag, @@ -183,6 +183,13 @@ exports.getModule = class FileAreaList extends MenuModule { entryInfo[_.camelCase(name)] = value; }); + const userRatingChar = config.userRatingChar ? config.userRatingChar[0] : '*'; + if(_.isNumber(entryInfo.userRating)) { + entryInfo.userRatingString = new Array(entryInfo.userRating).join(userRatingChar); + } else { + entryInfo.userRatingString = ''; + } + // 10+ are custom textviews let textView; let customMciId = 10; @@ -197,6 +204,8 @@ exports.getModule = class FileAreaList extends MenuModule { ++customMciId; } + + return callback(null); } ], err => { From c715bb773134260d8a1df154a0c05b8f950f980b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Oct 2016 13:46:12 -0600 Subject: [PATCH 05/86] Allow tab to process as 'next' in preview mode --- core/multi_line_edit_text_view.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 0b05747a..4ef55590 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -1023,7 +1023,12 @@ MultiLineEditTextView.prototype.getData = function() { MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'mode' : this.mode = value; break; + case 'mode' : + this.mode = value; + if('preview' === value && !this.specialKeyMap.next) { + this.specialKeyMap.next = [ 'tab' ]; + } + break; case 'autoScroll' : this.autoScroll = value; break; } From 35e761067008dae94405995c78ecaa09a91021d3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Oct 2016 21:39:29 -0600 Subject: [PATCH 06/86] Better code, more flexible ArchiveUtil --- core/archive_util.js | 102 +++++++++++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 1c543a12..fb378a84 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -10,6 +10,37 @@ const fs = require('fs'); const _ = require('lodash'); const pty = require('ptyw.js'); +let archiveUtil; + +class Archiver { + constructor(config) { + this.compress = config.compress; + this.decompress = config.decompress; + this.list = config.list; + this.extract = config.extract; + + this.sig = new Buffer(config.sig, 'hex'); + this.offset = config.offset || 0; + } + + ok() { + return this.canCompress() && this.canDecompress(); + } + + can(what) { + if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) { + return false; + } + + return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0; + } + + canCompress() { return this.can('compress'); } + canDecompress() { return this.can('decompress'); } + canList() { return this.can('list'); } + canExtract() { return this.can('extract'); } +} + module.exports = class ArchiveUtil { constructor() { @@ -17,34 +48,31 @@ module.exports = class ArchiveUtil { this.longestSignature = 0; } + // singleton access + static getInstance() { + if(!archiveUtil) { + archiveUtil = new ArchiveUtil(); + archiveUtil.init(); + return archiveUtil; + } + } + init() { // // Load configuration // if(_.has(Config, 'archivers')) { Object.keys(Config.archivers).forEach(archKey => { - const arch = Config.archivers[archKey]; - if(!_.isString(arch.sig) || - !_.isString(arch.compressCmd) || - !_.isString(arch.decompressCmd) || - !_.isArray(arch.compressArgs) || - !_.isArray(arch.decompressArgs)) - { - // :TODO: log warning - return; + + const archConfig = Config.archivers[archKey]; + const archiver = new Archiver(archConfig); + + if(!archiver.ok()) { + // :TODO: Log warning - bad archiver/config } - const archiver = { - compressCmd : arch.compressCmd, - compressArgs : arch.compressArgs, - decompressCmd : arch.decompressCmd, - decompressArgs : arch.decompressArgs, - sig : new Buffer(arch.sig, 'hex'), - offset : arch.offset || 0, - }; - this.archivers[archKey] = archiver; - + if(archiver.offset + archiver.sig.length > this.longestSignature) { this.longestSignature = archiver.offset + archiver.sig.length; } @@ -65,6 +93,10 @@ module.exports = class ArchiveUtil { return this.getArchiver(archType) ? true : false; } + detectTypeWithBuf(buf, cb) { + // :TODO: implement me! + } + detectType(path, cb) { fs.open(path, 'r', (err, fd) => { if(err) { @@ -123,15 +155,13 @@ module.exports = class ArchiveUtil { return cb(new Error(`Unknown archive type: ${archType}`)); } - let args = _.clone(archiver.compressArgs); // don't muck with orig - for(let i = 0; i < args.length; ++i) { - args[i] = stringFormat(args[i], { - archivePath : archivePath, - fileList : files.join(' '), - }); - } + const fmtObj = { + archivePath : archivePath, + fileList : files.join(' '), + }; - let comp = pty.spawn(archiver.compressCmd, args, this.getPtyOpts()); + const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); + const comp = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); return this.spawnHandler(comp, 'Compression', cb); } @@ -142,16 +172,14 @@ module.exports = class ArchiveUtil { if(!archiver) { return cb(new Error(`Unknown archive type: ${archType}`)); } - - let args = _.clone(archiver.decompressArgs); // don't muck with orig - for(let i = 0; i < args.length; ++i) { - args[i] = stringFormat(args[i], { - archivePath : archivePath, - extractPath : extractPath, - }); - } - - let comp = pty.spawn(archiver.decompressCmd, args, this.getPtyOpts()); + + const fmtObj = { + archivePath : archivePath, + extractPath : extractPath, + }; + + const args = archiver.decompress.args.map( arg => stringFormat(arg, fmtObj) ); + const comp = pty.spawn(archiver.decompress.cmd, args, this.getPtyOpts()); return this.spawnHandler(comp, 'Decompression', cb); } From ec716fdf2c35b77a9beb5a8f518f31e360ca5bb9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Oct 2016 21:40:37 -0600 Subject: [PATCH 07/86] * More file area utility methods/etc. * Start adding oputil file-area --scan stuff * New new ArchiveUtil.getInstance() --- core/config.js | 22 +++- core/file_area.js | 178 +++++++++++++++++++++++++++++++- core/file_entry.js | 2 +- core/scanner_tossers/ftn_bso.js | 6 +- mods/file_area_list.js | 6 +- oputil.js | 78 +++++++++++--- 6 files changed, 268 insertions(+), 24 deletions(-) diff --git a/core/config.js b/core/config.js index 78e55c42..211e779d 100644 --- a/core/config.js +++ b/core/config.js @@ -215,10 +215,24 @@ function getDefaultConfig() { zip : { sig : '504b0304', offset : 0, - compressCmd : '7z', - compressArgs : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], - decompressCmd : '7z', - decompressArgs : [ 'e', '-o{extractPath}', '{archivePath}' ] + compress : { + cmd : '7z', + args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + }, + decompress : { + cmd : '7z', + args : [ 'e', '-o{extractPath}', '{archivePath}' ] + }, + /* + list : { + cmd : '7z', + args : [ 'l', '{archivePath}' ], + match : '...someregex...' + },*/ + extract : { + cmd : '7z', + args : [ 'x', '-o{extractPath}', '{archivePath}', '{fileList}' ], + }, } }, diff --git a/core/file_area.js b/core/file_area.js index 08c600af..0a684442 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -5,16 +5,24 @@ const Config = require('./config.js').config; const Errors = require('./enig_error.js').Errors; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +const FileEntry = require('./file_entry.js'); +const FileDb = require('./database.js').dbs.file; +const ArchiveUtil = require('./archive_util.js'); // deps const _ = require('lodash'); const async = require('async'); +const fs = require('fs'); +const crypto = require('crypto'); +const paths = require('path'); exports.getAvailableFileAreas = getAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; exports.getDefaultFileArea = getDefaultFileArea; exports.getFileAreaByTag = getFileAreaByTag; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; +//exports.addOrUpdateFileEntry = addOrUpdateFileEntry; +exports.scanFileAreaForChanges = scanFileAreaForChanges; const WellKnownAreaTags = exports.WellKnownAreaTags = { Invalid : '', @@ -64,7 +72,11 @@ function getDefaultFileArea(client, disableAcsCheck) { } function getFileAreaByTag(areaTag) { - return Config.fileAreas.areas[areaTag]; + const areaInfo = Config.fileAreas.areas[areaTag]; + if(areaInfo) { + areaInfo.areaTag = areaTag; // convienence! + return areaInfo; + } } function changeFileAreaWithOptions(client, areaTag, options, cb) { @@ -101,3 +113,167 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) { } ); } + +function getAreaStorageDirectory(areaInfo) { + return paths.join(Config.fileBase.areaStoragePrefix, areaInfo.storageDir || ''); +} + +function getExistingFileEntriesBySha1(sha1, cb) { + const entries = []; + + FileDb.each( + `SELECT file_id, area_tag + FROM file + WHERE file_sha1=?;`, + [ sha1 ], + (err, fileRow) => { + if(fileRow) { + entries.push({ + fileId : fileRow.file_id, + areaTag : fileRow.area_tag, + }); + } + }, + err => { + return cb(err, entries); + } + ); +} + +function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { + async.series( + [ + function getArchiveFileList(callback) { + // :TODO: get list of files in archive + return callback(null); + } + ], + err => { + return cb(err); + } + ); +} + +function addNewFileEntry(fileEntry, filePath, cb) { + const archiveUtil = ArchiveUtil.getInstance(); + + // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data + archiveUtil.detectType(filePath, (err, archiveType) => { + if(archiveType) { + return addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb); + } else { + // :TODO:addNewNonArchiveFileEntry + } + }); +} + +function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { + + const fileEntry = new FileEntry({ + areaTag : areaInfo.areaTag, + meta : options.meta, + hashTags : options.hashTags, // Set() or Array + }); + + const filePath = paths.join(getAreaStorageDirectory(areaInfo), fileName); + + async.waterfall( + [ + function processPhysicalFile(callback) { + const stream = fs.createReadStream(filePath); + + let byteSize = 0; + const sha1 = crypto.createHash('sha1'); + const sha256 = crypto.createHash('sha256'); + const md5 = crypto.createHash('md5'); + + + // :TODO: crc32 + + stream.on('data', data => { + byteSize += data.length; + + sha1.update(data); + sha256.update(data); + md5.update(data); + }); + + stream.on('end', () => { + fileEntry.meta.byte_size = byteSize; + + // sha-1 is in basic file entry + fileEntry.fileSha1 = sha1.digest('hex'); + + // others are meta + fileEntry.meta.file_sha256 = sha256.digest('hex'); + fileEntry.meta.file_md5 = md5.digest('hex'); + + return callback(null); + }); + + stream.on('error', err => { + return callback(err); + }); + }, + function fetchExistingEntry(callback) { + getExistingFileEntriesBySha1(fileEntry.fileSha1, (err, existingEntries) => { + return callback(err, existingEntries); + }); + }, + function addOrUpdate(callback, existingEntries) { + if(existingEntries.length > 0) { + + } else { + return addNewFileEntry(fileEntry, filePath, callback); + } + }, + ], + err => { + return cb(err); + } + ); +} + +function scanFileAreaForChanges(areaInfo, cb) { + const areaPhysDir = getAreaStorageDirectory(areaInfo); + + async.series( + [ + function scanPhysFiles(callback) { + fs.readdir(areaPhysDir, (err, files) => { + if(err) { + return callback(err); + } + + async.each(files, (fileName, next) => { + const fullPath = paths.join(areaPhysDir, fileName); + + fs.stat(fullPath, (err, stats) => { + if(err) { + // :TODO: Log me! + return next(null); // always try next file + } + + if(!stats.isFile()) { + return next(null); + } + + addOrUpdateFileEntry(areaInfo, fileName, err => { + + }); + }); + }, err => { + return callback(err); + }); + }); + }, + function scanDbEntries(callback) { + // :TODO: Look @ db entries for area that were *not* processed above + return callback(null); + } + ], + err => { + return cb(err); + } + ); +} \ No newline at end of file diff --git a/core/file_entry.js b/core/file_entry.js index ffafbe85..e4c44d11 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -10,7 +10,7 @@ const _ = require('lodash'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha1', 'file_name', - 'desc', 'desc_long', 'upload_by_username', 'upload_timestamp' + 'desc', 'desc_long', 'upload_by_username', 'upload_timestamp' // :TODO: remove upload_by_username -- and from database.js, etc. ]; const FILE_WELL_KNOWN_META = { diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index b5e65227..d9b9e13b 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -37,6 +37,8 @@ exports.moduleInfo = { * Support NetMail * NetMail needs explicit isNetMail() check * NetMail filename / location / etc. is still unknown - need to post on groups & get real answers + * Validate packet passwords!!!! + => secure vs insecure landing areas */ @@ -49,9 +51,7 @@ function FTNMessageScanTossModule() { let self = this; - this.archUtil = new ArchiveUtil(); - this.archUtil.init(); - + this.archUtil = ArchiveUtil.getInstance(); if(_.has(Config, 'scannerTossers.ftn_bso')) { this.moduleConfig = Config.scannerTossers.ftn_bso; diff --git a/mods/file_area_list.js b/mods/file_area_list.js index a56f0a0c..ae1297d8 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -67,6 +67,8 @@ exports.getModule = class FileAreaList extends MenuModule { this.filterCriteria = this.filterCriteria || { // :TODO: set area tag - all in current area by default }; + + this.currentFileEntry = new FileEntry(); } enter() { @@ -139,9 +141,7 @@ exports.getModule = class FileAreaList extends MenuModule { function fetchEntryData(callback) { return self.loadFileIds(callback); }, - function loadCurrentFileInfo(callback) { - self.currentFileEntry = new FileEntry(); - + function loadCurrentFileInfo(callback) { self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { return callback(err); }); diff --git a/oputil.js b/oputil.js index 11f4fd8f..c78e7683 100755 --- a/oputil.js +++ b/oputil.js @@ -38,6 +38,7 @@ global args: commands: user : user utilities config : config file management + file-area : file area management `, User : @@ -56,11 +57,24 @@ valid args: valid args: --new : generate a new/initial configuration +`, + FileArea : +`usage: oputil.js file-area + +valid args: + --scan AREA_TAG : (re)scan area specified by AREA_TAG for new files ` }; -function printUsage(command) { - console.error(USAGE_HELP[command]); +function printUsageAndSetExitCode(command, exitCode) { + if(_.isUndefined(exitCode)) { + exitCode = ExitCodes.ERROR; + } + process.exitCode = exitCode; + const errMsg = USAGE_HELP[command]; + if(errMsg) { + console.error(errMsg); + } } function initConfig(cb) { @@ -144,8 +158,7 @@ function setAccountStatus(userName, active) { function handleUserCommand() { if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) { - process.exitCode = ExitCodes.ERROR; - return printUsage('User'); + return printUsageAndSetExitCode('User', ExitCodes.ERROR); } if(_.isString(argv.password)) { @@ -399,8 +412,7 @@ function askNewConfigQuestions(cb) { function handleConfigCommand() { if(true === argv.help) { - process.exitCode = ExitCodes.ERROR; - return printUsage('Config'); + return printUsageAndSetExitCode('Config', ExitCodes.ERROR); } if(argv.new) { @@ -419,11 +431,51 @@ function handleConfigCommand() { } }); } else { - process.exitCode = ExitCodes.ERROR; - return printUsage('Config'); + return printUsageAndSetExitCode('Config', ExitCodes.ERROR); } } +function fileAreaScan(areaTag) { + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases(callback); + }, + function getFileArea(callback) { + const fileAreaMod = require('./core/file_area.js'); + + const areaInfo = fileAreaMod.getFileAreaByTag(argv.scan); + if(!areaInfo) { + return callback(new Error('Invalid file area')); + } + + return callback(null, fileAreaMod, areaInfo); + }, + function performScan(fileAreaMod, areaInfo, callback) { + fileAreaMod.scanFileAreaForChanges(areaInfo, err => { + + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); +} + +function handleFileAreaCommand() { + if(true === argv.help) { + return printUsageAndSetExitCode('FileArea', ExitCodes.ERROR); + } + + if(argv.scan) { + return fileAreaScan(argv.scan); + } +} + function main() { process.exitCode = ExitCodes.SUCCESS; @@ -435,8 +487,7 @@ function main() { if(0 === argv._.length || 'help' === argv._[0]) { - printUsage('General'); - process.exit(ExitCodes.SUCCESS); + printUsageAndSetExitCode('General', ExitCodes.SUCCESS); } switch(argv._[0]) { @@ -448,9 +499,12 @@ function main() { handleConfigCommand(); break; + case 'file-area' : + handleFileAreaCommand(); + break; + default: - printUsage(''); - process.exitCode = ExitCodes.BAD_COMMAND; + printUsageAndSetExitCode('', ExitCodes.BAD_COMMAND); } } From 9593da5626eac46589fbf6955082fee2549c8be1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Oct 2016 22:21:37 -0600 Subject: [PATCH 08/86] Additional work with archivers --- core/archive_util.js | 62 +++++++++++++++++++++++++++++++++++++------- core/config.js | 9 +++---- core/file_area.js | 6 ++++- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index fb378a84..b84b8b98 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -37,7 +37,7 @@ class Archiver { canCompress() { return this.can('compress'); } canDecompress() { return this.can('decompress'); } - canList() { return this.can('list'); } + canList() { return this.can('list'); } // :TODO: validate entryMatch canExtract() { return this.can('extract'); } } @@ -53,8 +53,8 @@ module.exports = class ArchiveUtil { if(!archiveUtil) { archiveUtil = new ArchiveUtil(); archiveUtil.init(); - return archiveUtil; } + return archiveUtil; } init() { @@ -127,17 +127,17 @@ module.exports = class ArchiveUtil { }); } - spawnHandler(comp, action, cb) { + spawnHandler(proc, action, cb) { // pty.js doesn't currently give us a error when things fail, // so we have this horrible, horrible hack: let err; - comp.once('data', d => { + proc.once('data', d => { if(_.isString(d) && d.startsWith('execvp(3) failed.: No such file or directory')) { err = new Error(`${action} failed: ${d.trim()}`); } }); - comp.once('exit', exitCode => { + proc.once('exit', exitCode => { if(exitCode) { return cb(new Error(`${action} failed with exit code: ${exitCode}`)); } @@ -161,9 +161,9 @@ module.exports = class ArchiveUtil { }; const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); - const comp = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); + const proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); - return this.spawnHandler(comp, 'Compression', cb); + return this.spawnHandler(proc, 'Compression', cb); } extractTo(archivePath, extractPath, archType, cb) { @@ -179,9 +179,53 @@ module.exports = class ArchiveUtil { }; const args = archiver.decompress.args.map( arg => stringFormat(arg, fmtObj) ); - const comp = pty.spawn(archiver.decompress.cmd, args, this.getPtyOpts()); + const proc = pty.spawn(archiver.decompress.cmd, args, this.getPtyOpts()); - return this.spawnHandler(comp, 'Decompression', cb); + return this.spawnHandler(proc, 'Decompression', cb); + } + + listEntries(archivePath, archType, cb) { + const archiver = this.getArchiver(archType); + + if(!archiver) { + return cb(new Error(`Unknown archive type: ${archType}`)); + } + + const fmtObj = { + archivePath : archivePath, + }; + + const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); + const proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); + + let output = ''; + proc.on('data', data => { + // :TODO: hack for: execvp(3) failed.: No such file or directory + + output += data; + }); + + proc.once('exit', exitCode => { + if(exitCode) { + return cb(new Error(`List failed with exit code: ${exitCode}`)); + } + //if(err) { + // return cb(err); + // } + + const entries = []; + const entryMatchRe = new RegExp(archiver.list.entryMatch, 'g'); + let m; + while(null !== (m = entryMatchRe.exec(output))) { + // :TODO: allow alternate ordering!!! + entries.push({ + size : m[1], + fileName : m[2], + }); + } + + return cb(null, entries); + }); } getPtyOpts() { diff --git a/core/config.js b/core/config.js index 211e779d..8c2a89c5 100644 --- a/core/config.js +++ b/core/config.js @@ -223,12 +223,11 @@ function getDefaultConfig() { cmd : '7z', args : [ 'e', '-o{extractPath}', '{archivePath}' ] }, - /* list : { - cmd : '7z', - args : [ 'l', '{archivePath}' ], - match : '...someregex...' - },*/ + cmd : '7z', + args : [ 'l', '{archivePath}' ], + entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\n]+)$', + }, extract : { cmd : '7z', args : [ 'x', '-o{extractPath}', '{archivePath}', '{fileList}' ], diff --git a/core/file_area.js b/core/file_area.js index 0a684442..4f33d218 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -141,11 +141,15 @@ function getExistingFileEntriesBySha1(sha1, cb) { } function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { + const archiveUtil = ArchiveUtil.getInstance(); + async.series( [ function getArchiveFileList(callback) { // :TODO: get list of files in archive - return callback(null); + archiveUtil.listEntries(filePath, archiveType, (err, entries) => { + return callback(err); + }); } ], err => { From 61b065874353cdf13d7becd274426b0ca024d362 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Oct 2016 22:03:32 -0600 Subject: [PATCH 09/86] Work on new archivers layout, short/long desc file discovery --- core/archive_util.js | 4 ++-- core/config.js | 54 ++++++++++++++++++++++++++++++++++++++++++-- core/file_area.js | 22 ++++++++++++++---- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index b84b8b98..c21bf4ce 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -214,9 +214,9 @@ module.exports = class ArchiveUtil { // } const entries = []; - const entryMatchRe = new RegExp(archiver.list.entryMatch, 'g'); + const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm'); let m; - while(null !== (m = entryMatchRe.exec(output))) { + while((m = entryMatchRe.exec(output))) { // :TODO: allow alternate ordering!!! entries.push({ size : m[1], diff --git a/core/config.js b/core/config.js index 8c2a89c5..96427624 100644 --- a/core/config.js +++ b/core/config.js @@ -226,12 +226,62 @@ function getDefaultConfig() { list : { cmd : '7z', args : [ 'l', '{archivePath}' ], - entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\n]+)$', + entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', }, extract : { cmd : '7z', args : [ 'x', '-o{extractPath}', '{archivePath}', '{fileList}' ], - }, + }, + }, + }, + + archivers2 : { + tools : { + '7Zip' : { + compress : { + cmd : '7z', + args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + }, + decompress : { + cmd : '7z', + args : [ 'e', '-o{extractPath}', '{archivePath}' ] + }, + list : { + cmd : '7z', + args : [ 'l', '{archivePath}' ], + entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : '7z', + args : [ 'x', '-o{extractPath}', '{archivePath}', '{fileList}' ], + }, + } + }, + formats : { + zip : { + sig : '504b0304', + offset : 0, + exts : [ 'zip' ], + tool : '7Zip', + }, + '7z' : { + sig : '377abcaf271c', + offset : 0, + exts : [ '7z' ], + tool : '7Zip', + }, + arj : { + sig : '60ea', + offset : 0, + exts : [ 'arj' ], + tool : '7Zip', + }, + rar : { + sig : '526172211a0700', + offset : 0, + exts : [ 'rar' ], + tool : '7Zip', + } } }, diff --git a/core/file_area.js b/core/file_area.js index 4f33d218..c6ae2f0e 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -143,13 +143,25 @@ function getExistingFileEntriesBySha1(sha1, cb) { function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { const archiveUtil = ArchiveUtil.getInstance(); - async.series( + async.waterfall( [ - function getArchiveFileList(callback) { - // :TODO: get list of files in archive + function getArchiveFileList(callback) { archiveUtil.listEntries(filePath, archiveType, (err, entries) => { - return callback(err); - }); + return callback(null, entries || []); // ignore any errors here + }); + }, + function extractDescFiles(entries, callback) { + + // :TODO: would be nice if these RegExp's were cached + const shortDescFile = entries.find( e => { + return Config.fileBase.fileNamePatterns.shortDesc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + }); + + const longDescFile = entries.find( e => { + return Config.fileBase.fileNamePatterns.longDesc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + }); + + return callback(null); } ], err => { From 29947611f617374e5c9db06109c6e85155d21ef2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 5 Oct 2016 23:22:59 -0600 Subject: [PATCH 10/86] * New archives/archiver format - more flexible, more formats, etc. * Add scanning inside archives ability, extract file_id.diz, etc. * Initial year est (WIP) work --- core/archive_util.js | 95 +++++++++++++++++++++++++++--------- core/art.js | 2 +- core/config.js | 56 ++++++++------------- core/file_area.js | 113 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 205 insertions(+), 61 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index c21bf4ce..81daf9c1 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -4,6 +4,7 @@ // ENiGMA½ const Config = require('./config.js').config; const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; // base/modules const fs = require('fs'); @@ -19,8 +20,8 @@ class Archiver { this.list = config.list; this.extract = config.extract; - this.sig = new Buffer(config.sig, 'hex'); - this.offset = config.offset || 0; + /*this.sig = new Buffer(config.sig, 'hex'); + this.offset = config.offset || 0;*/ } ok() { @@ -61,10 +62,10 @@ module.exports = class ArchiveUtil { // // Load configuration // - if(_.has(Config, 'archivers')) { - Object.keys(Config.archivers).forEach(archKey => { + if(_.has(Config, 'archives.archivers')) { + Object.keys(Config.archives.archivers).forEach(archKey => { - const archConfig = Config.archivers[archKey]; + const archConfig = Config.archives.archivers[archKey]; const archiver = new Archiver(archConfig); if(!archiver.ok()) { @@ -72,21 +73,43 @@ module.exports = class ArchiveUtil { } this.archivers[archKey] = archiver; + }); + } - if(archiver.offset + archiver.sig.length > this.longestSignature) { - this.longestSignature = archiver.offset + archiver.sig.length; - } + if(_.has(Config, 'archives.formats')) { + Object.keys(Config.archives.formats).forEach(fmtKey => { + + Config.archives.formats[fmtKey].sig = new Buffer(Config.archives.formats[fmtKey].sig, 'hex'); + Config.archives.formats[fmtKey].offset = Config.archives.formats[fmtKey].offset || 0; + + const sigLen = Config.archives.formats[fmtKey].offset + Config.archives.formats[fmtKey].sig.length; + if(sigLen > this.longestSignature) { + this.longestSignature = sigLen; + } }); } } + /* getArchiver(archType) { - if(!archType) { + if(!archType || 0 === archType.length) { return; } archType = archType.toLowerCase(); return this.archivers[archType]; + }*/ + + getArchiver(archType) { + if(!archType || 0 === archType.length) { + return; + } + + if(_.has(Config, [ 'archives', 'formats', archType, 'handler' ] ) && + _.has(Config, [ 'archives', 'archivers', Config.archives.formats[archType].handler ] )) + { + return Config.archives.archivers[ Config.archives.formats[archType].handler ]; + } } haveArchiver(archType) { @@ -98,31 +121,33 @@ module.exports = class ArchiveUtil { } detectType(path, cb) { + if(!_.has(Config, 'archives.formats')) { + return cb(Errors.DoesNotExist('No formats configured')); + } + fs.open(path, 'r', (err, fd) => { if(err) { - cb(err); - return; + return cb(err); } - let buf = new Buffer(this.longestSignature); + const buf = new Buffer(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { if(err) { return cb(err); } - // return first match - const detected = _.findKey(this.archivers, arch => { - const lenNeeded = arch.offset + arch.sig.length; - + const archFormat = _.findKey(Config.archives.formats, archFormat => { + const lenNeeded = archFormat.offset + archFormat.sig.length; + if(bytesRead < lenNeeded) { return false; } - const comp = buf.slice(arch.offset, arch.offset + arch.sig.length); - return (arch.sig.equals(comp)); + const comp = buf.slice(archFormat.offset, archFormat.offset + archFormat.sig.length); + return (archFormat.sig.equals(comp)); }); - cb(detected ? null : new Error('Unknown type'), detected); + return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); }); }); } @@ -157,7 +182,7 @@ module.exports = class ArchiveUtil { const fmtObj = { archivePath : archivePath, - fileList : files.join(' '), + fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! }; const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); @@ -166,7 +191,17 @@ module.exports = class ArchiveUtil { return this.spawnHandler(proc, 'Compression', cb); } - extractTo(archivePath, extractPath, archType, cb) { + extractTo(archivePath, extractPath, archType, fileList, cb) { + let haveFileList; + + if(!cb && _.isFunction(fileList)) { + cb = fileList; + fileList = []; + haveFileList = false; + } else { + haveFileList = true; + } + const archiver = this.getArchiver(archType); if(!archiver) { @@ -178,10 +213,22 @@ module.exports = class ArchiveUtil { extractPath : extractPath, }; - const args = archiver.decompress.args.map( arg => stringFormat(arg, fmtObj) ); - const proc = pty.spawn(archiver.decompress.cmd, args, this.getPtyOpts()); + const action = haveFileList ? 'extract' : 'decompress'; - return this.spawnHandler(proc, 'Decompression', cb); + // we need to treat {fileList} special in that it should be broken up to 0:n args + const args = archiver[action].args.map( arg => { + return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); + }); + + const fileListPos = args.indexOf('{fileList}'); + if(fileListPos > -1) { + // replace {fileList} with 0:n sep file list arguments + args.splice.apply(args, [fileListPos, 1].concat(fileList)); + } + + const proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts()); + + return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); } listEntries(archivePath, archType, cb) { diff --git a/core/art.js b/core/art.js index 0e283218..4b870cde 100644 --- a/core/art.js +++ b/core/art.js @@ -7,7 +7,6 @@ const miscUtil = require('./misc_util.js'); const ansi = require('./ansi_term.js'); const aep = require('./ansi_escape_parser.js'); const sauce = require('./sauce.js'); -const farmhash = require('farmhash'); // deps const fs = require('fs'); @@ -15,6 +14,7 @@ const paths = require('path'); const assert = require('assert'); const iconv = require('iconv-lite'); const _ = require('lodash'); +const farmhash = require('farmhash'); exports.getArt = getArt; exports.getArtFromPath = getArtFromPath; diff --git a/core/config.js b/core/config.js index 96427624..c94470bb 100644 --- a/core/config.js +++ b/core/config.js @@ -211,49 +211,25 @@ function getDefaultConfig() { } }, - archivers : { - zip : { - sig : '504b0304', - offset : 0, - compress : { - cmd : '7z', - args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], - }, - decompress : { - cmd : '7z', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] - }, - list : { - cmd : '7z', - args : [ 'l', '{archivePath}' ], - entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', - }, - extract : { - cmd : '7z', - args : [ 'x', '-o{extractPath}', '{archivePath}', '{fileList}' ], - }, - }, - }, - - archivers2 : { - tools : { + archives : { + archivers : { '7Zip' : { compress : { - cmd : '7z', + cmd : '7za', args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], }, decompress : { - cmd : '7z', + cmd : '7za', args : [ 'e', '-o{extractPath}', '{archivePath}' ] }, list : { - cmd : '7z', + cmd : '7za', args : [ 'l', '{archivePath}' ], entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', }, extract : { - cmd : '7z', - args : [ 'x', '-o{extractPath}', '{archivePath}', '{fileList}' ], + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], }, } }, @@ -262,25 +238,25 @@ function getDefaultConfig() { sig : '504b0304', offset : 0, exts : [ 'zip' ], - tool : '7Zip', + handler : '7Zip', }, '7z' : { sig : '377abcaf271c', offset : 0, exts : [ '7z' ], - tool : '7Zip', + handler : '7Zip', }, arj : { sig : '60ea', offset : 0, exts : [ 'arj' ], - tool : '7Zip', + handler : '7Zip', }, rar : { sig : '526172211a0700', offset : 0, exts : [ 'rar' ], - tool : '7Zip', + handler : '7Zip', } } }, @@ -367,6 +343,16 @@ function getDefaultConfig() { longDesc : [ '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$' ], }, + yearEstPatterns: [ + // + // Patterns should produce the year in the first submatch + // The year may be YY or YYYY + // + '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. + "\\B('[1789][0-9])\\b", + // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. + ], + areas: { message_attachment : { name : 'Message attachments', diff --git a/core/file_area.js b/core/file_area.js index c6ae2f0e..af5c3075 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -15,6 +15,8 @@ const async = require('async'); const fs = require('fs'); const crypto = require('crypto'); const paths = require('path'); +const temp = require('temp').track(); // track() cleans up temp dir/files for us +const iconv = require('iconv-lite'); exports.getAvailableFileAreas = getAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; @@ -140,6 +142,41 @@ function getExistingFileEntriesBySha1(sha1, cb) { ); } +// :TODO: This is bascially sliceAtEOF() from art.js .... DRY! +function sliceAtSauceMarker(data) { + let eof = data.length; + const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + + for(let i = eof - 1; i > stopPos; i--) { + if(0x1a === data[i]) { + eof = i; + break; + } + } + return data.slice(0, eof); +} + +function getEstYear(input) { + // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time + const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); + + let match; + for(let i = 0; i < patterns.length; ++i) { + match = patterns[i].exec(input); + if(match) { + break; + } + } + + if(match) { + if(2 == match[1].length) { + return parseInt('19' + match[1]); + } else { + return parseInt(match[1]); + } + } +} + function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { const archiveUtil = ArchiveUtil.getInstance(); @@ -153,15 +190,78 @@ function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { function extractDescFiles(entries, callback) { // :TODO: would be nice if these RegExp's were cached + // :TODO: this is long winded... + + const extractList = []; + const shortDescFile = entries.find( e => { return Config.fileBase.fileNamePatterns.shortDesc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); }); + if(shortDescFile) { + extractList.push(shortDescFile.fileName); + } + const longDescFile = entries.find( e => { return Config.fileBase.fileNamePatterns.longDesc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); }); - return callback(null); + if(longDescFile) { + extractList.push(longDescFile.fileName); + } + + temp.mkdir('enigextract-', (err, tempDir) => { + if(err) { + return callback(err); + } + + archiveUtil.extractTo(filePath, tempDir, archiveType, extractList, err => { + if(err) { + return callback(err); + } + + const descFiles = { + desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, + descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, + }; + + return callback(null, descFiles); + }); + }); + }, + function readDescFiles(descFiles, callback) { + // :TODO: we shoudl probably omit files that are too large + async.each(Object.keys(descFiles), (descType, next) => { + const path = descFiles[descType]; + if(!path) { + return next(null); + } + + fs.readFile(path, (err, data) => { + if(err || !data) { + return next(null); + } + + // + // Assume FILE_ID.DIZ, NFO files, etc. are CP437. + // + // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... + fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); + return next(null); + }); + }, () => { + // cleanup, but don't wait... + temp.cleanup( err => { + // :TODO: Log me! + }); + return callback(null); + }); + }, + function attemptReleaseYearEstimation(callback) { + let estYear; + if(fileEntry.descLong) { + estYear = getEstYear(fileEntry.descLong); + } } ], err => { @@ -240,6 +340,17 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { if(existingEntries.length > 0) { } else { + // + // Some basics for new entries + // + fileEntry.meta.user_rating = 0; + if(options.uploadByUserName) { + fileEntry.meta.upload_by_username = options.uploadByUserName; + } + if(options.uploadByUserId) { + fileEntry.meta.upload_by_user_id = options.uploadByUserId; + } + return addNewFileEntry(fileEntry, filePath, callback); } }, From 67e2ff987f40c7360966d40c4fec4fcccf773b16 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 6 Oct 2016 21:03:04 -0600 Subject: [PATCH 11/86] * getISOTimestampString() * More file entry load/persist --- core/database.js | 8 +++- core/file_area.js | 96 +++++++++++++++++++++++++++++----------------- core/file_entry.js | 64 ++++++++++++++++++++++++++++--- core/message.js | 29 +++++++------- 4 files changed, 139 insertions(+), 58 deletions(-) diff --git a/core/database.js b/core/database.js index f4bfc752..608c263a 100644 --- a/core/database.js +++ b/core/database.js @@ -10,11 +10,13 @@ const paths = require('path'); const async = require('async'); const _ = require('lodash'); const assert = require('assert'); +const moment = require('moment'); // database handles let dbs = {}; exports.getModDatabasePath = getModDatabasePath; +exports.getISOTimestampString = getISOTimestampString; exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; @@ -46,6 +48,11 @@ function getModDatabasePath(moduleInfo, suffix) { return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); } +function getISOTimestampString(ts) { + ts = ts || moment(); + return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); +} + function initializeDatabases(cb) { async.each( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => { @@ -257,7 +264,6 @@ const DB_INIT_TABLE = { file_name, /* FTS @ file_fts */ desc, /* FTS @ file_fts */ desc_long, /* FTS @ file_fts */ - upload_by_username VARCHAR NOT NULL, upload_timestamp DATETIME NOT NULL );` ); diff --git a/core/file_area.js b/core/file_area.js index af5c3075..36d93078 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -156,28 +156,35 @@ function sliceAtSauceMarker(data) { return data.slice(0, eof); } -function getEstYear(input) { +function attemptSetEstimatedReleaseDate(fileEntry) { // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); - let match; - for(let i = 0; i < patterns.length; ++i) { - match = patterns[i].exec(input); - if(match) { - break; + function getMatch(input) { + if(input) { + let m; + for(let i = 0; i < patterns.length; ++i) { + m = patterns[i].exec(input); + if(m) { + return m; + } + } } } - if(match) { - if(2 == match[1].length) { - return parseInt('19' + match[1]); - } else { - return parseInt(match[1]); + // + // We attempt deteciton in short -> long order + // + const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); + if(match && match[1]) { + const year = (2 === match[1].length) ? parseInt('19' + match[1]) : parseInt(match[1]); + if(year) { + fileEntry.meta.est_release_year = year; } } } -function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { +function populateFileEntryWithArchive(fileEntry, filePath, archiveType, cb) { const archiveUtil = ArchiveUtil.getInstance(); async.waterfall( @@ -258,10 +265,8 @@ function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { }); }, function attemptReleaseYearEstimation(callback) { - let estYear; - if(fileEntry.descLong) { - estYear = getEstYear(fileEntry.descLong); - } + attemptSetEstimatedReleaseDate(fileEntry); + return callback(null); } ], err => { @@ -270,17 +275,47 @@ function addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb) { ); } +function populateFileEntry(fileEntry, filePath, archiveType, cb) { + // :TODO: implement me! + return cb(null); +} + function addNewFileEntry(fileEntry, filePath, cb) { const archiveUtil = ArchiveUtil.getInstance(); // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data - archiveUtil.detectType(filePath, (err, archiveType) => { - if(archiveType) { - return addNewArchiveFileEnty(fileEntry, filePath, archiveType, cb); - } else { - // :TODO:addNewNonArchiveFileEntry - } - }); + + async.series( + [ + function populateInfo(callback) { + archiveUtil.detectType(filePath, (err, archiveType) => { + if(archiveType) { + populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => { + if(err) { + populateFileEntry(fileEntry, filePath, err => { + // :TODO: log err + return callback(null); // ignore err + }); + } + return callback(null); + }); + } else { + populateFileEntry(fileEntry, filePath, err => { + // :TODO: log err + return callback(null); // ignore err + }); + } + }); + }, + function addNewDbRecord(callback) { + return fileEntry.persist(callback); + } + ] + ); +} + +function updateFileEntry(fileEntry, filePath, cb) { + } function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { @@ -289,6 +324,7 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { areaTag : areaInfo.areaTag, meta : options.meta, hashTags : options.hashTags, // Set() or Array + fileName : fileName, }); const filePath = paths.join(getAreaStorageDirectory(areaInfo), fileName); @@ -302,8 +338,7 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { const sha1 = crypto.createHash('sha1'); const sha256 = crypto.createHash('sha256'); const md5 = crypto.createHash('md5'); - - + // :TODO: crc32 stream.on('data', data => { @@ -340,17 +375,6 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { if(existingEntries.length > 0) { } else { - // - // Some basics for new entries - // - fileEntry.meta.user_rating = 0; - if(options.uploadByUserName) { - fileEntry.meta.upload_by_username = options.uploadByUserName; - } - if(options.uploadByUserId) { - fileEntry.meta.upload_by_user_id = options.uploadByUserId; - } - return addNewFileEntry(fileEntry, filePath, callback); } }, diff --git a/core/file_entry.js b/core/file_entry.js index e4c44d11..25e6e81c 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -1,16 +1,17 @@ /* jslint node: true */ 'use strict'; -const fileDb = require('./database.js').dbs.file; -const Errors = require('./enig_error.js').Errors; +const fileDb = require('./database.js').dbs.file; +const Errors = require('./enig_error.js').Errors; +const getISOTimestampString = require('./database.js').getISOTimestampString; // deps -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha1', 'file_name', - 'desc', 'desc_long', 'upload_by_username', 'upload_timestamp' // :TODO: remove upload_by_username -- and from database.js, etc. + 'desc', 'desc_long', 'upload_timestamp' ]; const FILE_WELL_KNOWN_META = { @@ -34,6 +35,7 @@ module.exports = class FileEntry { this.areaTag = options.areaTag || ''; this.meta = {}; this.hashTags = new Set(); + this.fileName = options.fileName; } load(fileId, cb) { @@ -79,6 +81,58 @@ module.exports = class FileEntry { ); } + persist(cb) { + const self = this; + + async.series( + [ + function startTrans(callback) { + return fileDb.run('BEGIN;', callback); + }, + function storeEntry(callback) { + fileDb.run( + `REPLACE INTO file (area_tag, file_sha1, file_name, desc, desc_long, upload_timestamp) + VALUES(?, ?, ?, ?, ?, ?);`, + [ self.areaTag, self.fileSha1, self.fileName, self.desc, self.descLong, getISOTimestampString() ], + function inserted(err) { // use non-arrow func for 'this' scope / lastID + if(!err) { + self.fileId = this.lastID; + } + return callback(err); + } + ); + }, + function storeMeta(callback) { + async.each(Object.keys(self.meta), (n, next) => { + const v = self.meta[n]; + return FileEntry.persistMetaValue(self.fileId, n, v, next); + }, + err => { + return callback(err); + }); + }, + function storeHashTags(callback) { + return callback(null); + } + ], + err => { + // :TODO: Log orig err + fileDb.run(err ? 'ROLLBACK;' : 'COMMIT;', err => { + return cb(err); + }); + } + ); + } + + static persistMetaValue(fileId, name, value, cb) { + fileDb.run( + `REPLACE INTO file_meta (file_id, meta_name, meta_value) + VALUES(?, ?, ?);`, + [ fileId, name, value ], + cb + ); + } + loadMeta(cb) { fileDb.each( `SELECT meta_name, meta_value diff --git a/core/message.js b/core/message.js index e1d3e4fd..ce6a7c8f 100644 --- a/core/message.js +++ b/core/message.js @@ -1,17 +1,19 @@ /* jslint node: true */ 'use strict'; -let msgDb = require('./database.js').dbs.message; -let wordWrapText = require('./word_wrap.js').wordWrapText; -let ftnUtil = require('./ftn_util.js'); -let createNamedUUID = require('./uuid_util.js').createNamedUUID; +const msgDb = require('./database.js').dbs.message; +const wordWrapText = require('./word_wrap.js').wordWrapText; +const ftnUtil = require('./ftn_util.js'); +const createNamedUUID = require('./uuid_util.js').createNamedUUID; +const getISOTimestampString = require('./database.js').getISOTimestampString; -let uuid = require('node-uuid'); -let async = require('async'); -let _ = require('lodash'); -let assert = require('assert'); -let moment = require('moment'); -const iconvEncode = require('iconv-lite').encode; +// deps +const uuid = require('node-uuid'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); +const iconvEncode = require('iconv-lite').encode; module.exports = Message; @@ -64,11 +66,6 @@ function Message(options) { this.isPrivate = function() { return Message.isPrivateAreaTag(this.areaTag); }; - - this.getMessageTimestampString = function(ts) { - ts = ts || moment(); - return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); - }; } Message.WellKnownAreaTags = { @@ -374,7 +371,7 @@ Message.prototype.persist = function(cb) { msgDb.run( `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(msgTimestamp) ], + [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], function inserted(err) { // use non-arrow function for 'this' scope if(!err) { self.messageId = this.lastID; From 806e6539f414cc8770b852578a5efa6deb596e80 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 12 Oct 2016 22:07:22 -0600 Subject: [PATCH 12/86] * Lots of improvements (WIP) to file browsing --- core/archive_util.js | 2 +- core/config.js | 12 +- core/file_area.js | 47 +++-- core/file_entry.js | 10 +- core/horizontal_menu_view.js | 49 +++-- core/view_controller.js | 6 +- mods/file_area_list.js | 384 +++++++++++++++++++++++++++++------ 7 files changed, 412 insertions(+), 98 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 81daf9c1..9e21f997 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -266,7 +266,7 @@ module.exports = class ArchiveUtil { while((m = entryMatchRe.exec(output))) { // :TODO: allow alternate ordering!!! entries.push({ - size : m[1], + byteSize : parseInt(m[1]), fileName : m[2], }); } diff --git a/core/config.js b/core/config.js index c94470bb..059051bc 100644 --- a/core/config.js +++ b/core/config.js @@ -239,24 +239,28 @@ function getDefaultConfig() { offset : 0, exts : [ 'zip' ], handler : '7Zip', + desc : 'ZIP Archive', }, '7z' : { sig : '377abcaf271c', offset : 0, exts : [ '7z' ], handler : '7Zip', + desc : '7-Zip Archive', }, arj : { sig : '60ea', offset : 0, exts : [ 'arj' ], handler : '7Zip', + desc : 'ARJ Archive', }, rar : { sig : '526172211a0700', offset : 0, exts : [ 'rar' ], handler : '7Zip', + desc : 'RAR Archive', } } }, @@ -339,7 +343,11 @@ function getDefaultConfig() { areaStoragePrefix : paths.join(__dirname, './../file_base/'), fileNamePatterns: { - shortDesc : [ '^FILE_ID\.DIZ$', '^DESC\.SDI$' ], + // These are NOT case sensitive + shortDesc : [ + '^FILE_ID\.DIZ$', '^DESC\.SDI$', '^DESCRIPT\.ION$', '^FILE\.DES$', '$FILE\.SDI$', '^DISK\.ID$' + ], + longDesc : [ '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$' ], }, @@ -349,7 +357,7 @@ function getDefaultConfig() { // The year may be YY or YYYY // '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. - "\\B('[1789][0-9])\\b", + "\\B('[1789][0-9])\\b", // eslint-disable-line quotes // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], diff --git a/core/file_area.js b/core/file_area.js index 36d93078..d3884f26 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -20,7 +20,7 @@ const iconv = require('iconv-lite'); exports.getAvailableFileAreas = getAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; -exports.getDefaultFileArea = getDefaultFileArea; +exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getFileAreaByTag = getFileAreaByTag; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; //exports.addOrUpdateFileEntry = addOrUpdateFileEntry; @@ -45,18 +45,20 @@ function getAvailableFileAreas(client, options) { } function getSortedAvailableFileAreas(client, options) { - const areas = _.map(getAvailableFileAreas(client, options), (v, k) => { - return { + const areas = _.map(getAvailableFileAreas(client, options), (v, k) => { + const areaInfo = { areaTag : k, area : v }; + + return areaInfo; }); sortAreasOrConfs(areas, 'area'); return areas; } -function getDefaultFileArea(client, disableAcsCheck) { +function getDefaultFileAreaTag(client, disableAcsCheck) { let defaultArea = _.findKey(Config.fileAreas, o => o.default); if(defaultArea) { const area = Config.fileAreas.areas[defaultArea]; @@ -76,7 +78,8 @@ function getDefaultFileArea(client, disableAcsCheck) { function getFileAreaByTag(areaTag) { const areaInfo = Config.fileAreas.areas[areaTag]; if(areaInfo) { - areaInfo.areaTag = areaTag; // convienence! + areaInfo.areaTag = areaTag; // convienence! + areaInfo.storageDirectory = getAreaStorageDirectory(areaInfo); return areaInfo; } } @@ -177,7 +180,20 @@ function attemptSetEstimatedReleaseDate(fileEntry) { // const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); if(match && match[1]) { - const year = (2 === match[1].length) ? parseInt('19' + match[1]) : parseInt(match[1]); + let year; + if(2 === match[1].length) { + year = parseInt(match[1]); + if(year) { + if(year > 70) { + year += 1900; + } else { + year += 2000; + } + } + } else { + year = parseInt(match[1]); + } + if(year) { fileEntry.meta.est_release_year = year; } @@ -290,14 +306,18 @@ function addNewFileEntry(fileEntry, filePath, cb) { function populateInfo(callback) { archiveUtil.detectType(filePath, (err, archiveType) => { if(archiveType) { + // save this off + fileEntry.meta.archive_type = archiveType; + populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => { if(err) { populateFileEntry(fileEntry, filePath, err => { // :TODO: log err return callback(null); // ignore err }); + } else { + return callback(null); } - return callback(null); }); } else { populateFileEntry(fileEntry, filePath, err => { @@ -310,7 +330,10 @@ function addNewFileEntry(fileEntry, filePath, cb) { function addNewDbRecord(callback) { return fileEntry.persist(callback); } - ] + ], + err => { + return cb(err); + } ); } @@ -371,7 +394,7 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { return callback(err, existingEntries); }); }, - function addOrUpdate(callback, existingEntries) { + function addOrUpdate(existingEntries, callback) { if(existingEntries.length > 0) { } else { @@ -396,7 +419,7 @@ function scanFileAreaForChanges(areaInfo, cb) { return callback(err); } - async.each(files, (fileName, next) => { + async.eachSeries(files, (fileName, next) => { const fullPath = paths.join(areaPhysDir, fileName); fs.stat(fullPath, (err, stats) => { @@ -409,8 +432,8 @@ function scanFileAreaForChanges(areaInfo, cb) { return next(null); } - addOrUpdateFileEntry(areaInfo, fileName, err => { - + addOrUpdateFileEntry(areaInfo, fileName, { areaTag : areaInfo.areaTag }, err => { + return next(err); }); }); }, err => { diff --git a/core/file_entry.js b/core/file_entry.js index 25e6e81c..dd656442 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -25,6 +25,7 @@ const FILE_WELL_KNOWN_META = { dl_count : (d) => parseInt(d) || 0, byte_size : (b) => parseInt(b) || 0, user_rating : (r) => Math.min(parseInt(r) || 0, 5), + archive_type : null, }; module.exports = class FileEntry { @@ -33,8 +34,13 @@ module.exports = class FileEntry { this.fileId = options.fileId || 0; this.areaTag = options.areaTag || ''; - this.meta = {}; - this.hashTags = new Set(); + this.meta = options.meta || { + // values we always want + user_rating : 0, + dl_count : 0, + }; + + this.hashTags = options.hashTags || new Set(); this.fileName = options.fileName; } diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 87c194ee..28f4c29d 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -113,30 +113,39 @@ HorizontalMenuView.prototype.setItems = function(items) { this.positionCacheExpired = true; }; +HorizontalMenuView.prototype.focusNext = function() { + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + } else { + this.focusedItemIndex++; + } + + // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes + this.redraw(); + + HorizontalMenuView.super_.prototype.focusNext.call(this); +}; + +HorizontalMenuView.prototype.focusPrevious = function() { + + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + } else { + this.focusedItemIndex--; + } + + // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes + this.redraw(); + + HorizontalMenuView.super_.prototype.focusPrevious.call(this); +}; + HorizontalMenuView.prototype.onKeyPress = function(ch, key) { if(key) { - var prevFocusedItemIndex = this.focusedItemIndex; - if(this.isKeyMapped('left', key.name)) { - if(0 === this.focusedItemIndex) { - this.focusedItemIndex = this.items.length - 1; - } else { - this.focusedItemIndex--; - } - + this.focusPrevious(); } else if(this.isKeyMapped('right', key.name)) { - if(this.items.length - 1 === this.focusedItemIndex) { - this.focusedItemIndex = 0; - } else { - this.focusedItemIndex++; - } - } - - if(prevFocusedItemIndex !== this.focusedItemIndex) { - // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes - // if this is changed to allow scrolling - this.redraw(); - return; + this.focusNext(); } } diff --git a/core/view_controller.js b/core/view_controller.js index f2fbb366..cf90de3e 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -143,8 +143,10 @@ function ViewController(options) { var mci = mciMap[name]; var view = self.mciViewFactory.createFromMCI(mci); - if(view && false === self.noInput) { - view.on('action', self.viewActionListener); + if(view) { + if(false === self.noInput) { + view.on('action', self.viewActionListener); + } self.addView(view); } diff --git a/mods/file_area_list.js b/mods/file_area_list.js index ae1297d8..ab013d85 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -9,11 +9,15 @@ const theme = require('../core/theme.js'); const FileEntry = require('../core/file_entry.js'); const stringFormat = require('../core/string_format.js'); const FileArea = require('../core/file_area.js'); +const Errors = require('../core/enig_error.js').Errors; +const ArchiveUtil = require('../core/archive_util.js'); +const Config = require('../core/config.js').config; // deps const async = require('async'); const _ = require('lodash'); const moment = require('moment'); +const paths = require('path'); /* Misc TODO @@ -41,15 +45,35 @@ exports.moduleInfo = { }; const FormIds = { - browse : 0, - details : 1, + browse : 0, + details : 1, + detailsGeneral : 2, + detailsNfo : 3, + detailsFileList : 4, }; const MciViewIds = { browse : { desc : 1, navMenu : 2, - // 10+: customs + // 10+ = customs + }, + details : { + navMenu : 1, + infoXyTop : 2, // %XY starting position for info area + infoXyBottom : 3, + // 10+ = customs + }, + detailsGeneral : { + // 10+ = customs + }, + detailsNfo : { + nfo : 1, + // 10+ = customs + }, + detailsFileList : { + fileList : 1, + // 10+ = customs }, }; @@ -69,6 +93,39 @@ exports.getModule = class FileAreaList extends MenuModule { }; this.currentFileEntry = new FileEntry(); + + this.menuMethods = { + nextFile : (formData, extraArgs, cb) => { + if(this.fileListPosition + 1 < this.fileList.length) { + this.fileListPosition += 1; + + delete this.currentFileEntry.archiveEntries; + + return this.displayBrowsePage(true, cb); // true=clerarScreen + } + + return cb(null); + }, + prevFile : (formData, extraArgs, cb) => { + if(this.fileListPosition > 0) { + --this.fileListPosition; + + delete this.currentFileEntry.archiveEntries; + + 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 + }, + }; } enter() { @@ -97,19 +154,87 @@ exports.getModule = class FileAreaList extends MenuModule { ); } - displayBrowsePage(clearScreen, cb) { + populateCurrentEntryInfo() { + 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 entryInfo = this.currentFileEntry.entryInfo = { + fileId : currEntry.fileId, + areaTag : currEntry.areaTag, + areaName : area.name || 'N/A', + areaDesc : area.desc || 'N/A', + fileSha1 : currEntry.fileSha1, + fileName : currEntry.fileName, + desc : currEntry.desc || '', + descLong : currEntry.descLong || '', + uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), + hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), + }; + + // + // 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] : ''; + 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 + + // create a rating string, e.g. "**---" + const userRatingTicked = config.userRatingTicked || '*'; + const userRatingUnticked = config.userRatingUnticked || ''; + entryInfo.userRating = 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); + } + } + + populateCustomLabels(category, startId) { + let textView; + let customMciId = startId; + const config = this.menuConfig.config; + + while( (textView = this.viewControllers[category].getView(customMciId)) ) { + const key = `${category}InfoFormat${customMciId}`; + const format = config[key]; + + if(format) { + textView.setText(stringFormat(format, this.currentFileEntry.entryInfo)); + } + + ++customMciId; + } + } + + displayArtAndPrepViewController(name, options, cb) { const self = this; const config = this.menuConfig.config; async.waterfall( [ - function clearAndDisplayArt(callback) { - if (clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); + function readyAndDisplayArt(callback) { + if(options.clearScreen) { + self.client.term.rawWrite(ansi.clearScreen()); } - + theme.displayThemedAsset( - config.art.browse, + config.art[name], self.client, { font : self.menuConfig.font, trailingLF : false }, (err, artData) => { @@ -118,31 +243,66 @@ exports.getModule = class FileAreaList extends MenuModule { ); }, function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.browse)) { - const vc = self.addViewController( - 'browse', - new ViewController( { client : self.client, formId : FormIds.browse } ) - ); + 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.browse, + formId : FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); } + + self.viewControllers[name].setFocus(true); + return callback(null); + + }, + ], + err => { + return cb(err); + } + ); + } - self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); + displayBrowsePage(clearScreen, cb) { + const self = this; - return callback(null); + async.series( + [ + function prepArtAndViewController(callback) { + return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); }, function fetchEntryData(callback) { + if(self.fileList) { + return callback(null); + } return self.loadFileIds(callback); }, function loadCurrentFileInfo(callback) { self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { + self.populateCurrentEntryInfo(); return callback(err); }); }, @@ -153,58 +313,164 @@ exports.getModule = class FileAreaList extends MenuModule { descView.setText(self.currentFileEntry.desc); } } - - const currEntry = self.currentFileEntry; - const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; - const area = FileArea.getFileAreaByTag(currEntry.areaTag); - const hashTagsSep = config.hashTagsSep || ', '; - - const entryInfo = { - fileId : currEntry.fileId, - areaTag : currEntry.areaTag, - areaName : area.name || 'N/A', - areaDesc : area.desc || 'N/A', - fileSha1 : currEntry.fileSha1, - fileName : currEntry.fileName, - desc : currEntry.desc, - descLong : currEntry.descLong, - uploadByUsername : currEntry.uploadByUsername, - uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), - hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), - }; - // - // 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 = currEntry.meta[name] || ''; - entryInfo[_.camelCase(name)] = value; + self.populateCustomLabels('browse', 10); + + 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', 10); + 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); + } }); - const userRatingChar = config.userRatingChar ? config.userRatingChar[0] : '*'; - if(_.isNumber(entryInfo.userRating)) { - entryInfo.userRatingString = new Array(entryInfo.userRating).join(userRatingChar); - } else { - entryInfo.userRatingString = ''; + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } + + 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 = paths.join(areaInfo.storageDirectory, this.currentFileEntry.fileName); + 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}'; + 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${_.capitalize(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)); } - // 10+ are custom textviews - let textView; - let customMciId = 10; + gotoTopPos(); - while( (textView = self.viewControllers.browse.getView(customMciId)) ) { - const key = `browseInfoFormat${customMciId}`; - const format = config[key]; + if(clearArea) { + self.client.term.rawWrite(ansi.reset()); - if(format) { - textView.setText(stringFormat(format, entryInfo)); + let pos = self.detailsInfoArea.top[0]; + const bottom = self.detailsInfoArea.bottom[0]; + + while(pos++ <= bottom) { + self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); } - ++customMciId; + 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, 10); return callback(null); } ], From 78607f8a484a223819bb79eecc53d96abf9b3bc1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Oct 2016 20:53:38 -0600 Subject: [PATCH 13/86] * Add CRC32 * Fix stale entries loaded from previous item in file listing --- core/crc.js | 22 ++++++++++++++++++++++ core/file_area.js | 6 +++++- mods/file_area_list.js | 15 ++++++--------- 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 core/crc.js diff --git a/core/crc.js b/core/crc.js new file mode 100644 index 00000000..5d9e9e02 --- /dev/null +++ b/core/crc.js @@ -0,0 +1,22 @@ +/* jslint node: true */ +'use strict'; + +const CRC32_TABLE = + '00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D'.split(' ').map(s => parseInt(s, 16)); + +exports.CRC32 = class CRC32 { + constructor() { + this.crc = -1; + } + + update(input) { + input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); + input.forEach(c => { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ c) & 0xff]; + }); + } + + finalize() { + return (this.crc ^ (-1)) >>> 0; + } +} \ No newline at end of file diff --git a/core/file_area.js b/core/file_area.js index d3884f26..46cb0016 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -8,6 +8,7 @@ const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const FileEntry = require('./file_entry.js'); const FileDb = require('./database.js').dbs.file; const ArchiveUtil = require('./archive_util.js'); +const CRC32 = require('./crc.js').CRC32; // deps const _ = require('lodash'); @@ -361,6 +362,7 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { const sha1 = crypto.createHash('sha1'); const sha256 = crypto.createHash('sha256'); const md5 = crypto.createHash('md5'); + const crc32 = new CRC32(); // :TODO: crc32 @@ -369,7 +371,8 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { sha1.update(data); sha256.update(data); - md5.update(data); + md5.update(data); + crc32.update(data); }); stream.on('end', () => { @@ -381,6 +384,7 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { // others are meta fileEntry.meta.file_sha256 = sha256.digest('hex'); fileEntry.meta.file_md5 = md5.digest('hex'); + fileEntry.meta.file_crc32 = crc32.finalize().toString(16); return callback(null); }); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index ab013d85..f1357f2f 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -92,15 +92,11 @@ exports.getModule = class FileAreaList extends MenuModule { // :TODO: set area tag - all in current area by default }; - this.currentFileEntry = new FileEntry(); - this.menuMethods = { nextFile : (formData, extraArgs, cb) => { if(this.fileListPosition + 1 < this.fileList.length) { this.fileListPosition += 1; - delete this.currentFileEntry.archiveEntries; - return this.displayBrowsePage(true, cb); // true=clerarScreen } @@ -110,8 +106,6 @@ exports.getModule = class FileAreaList extends MenuModule { if(this.fileListPosition > 0) { --this.fileListPosition; - delete this.currentFileEntry.archiveEntries; - return this.displayBrowsePage(true, cb); // true=clearScreen } @@ -181,7 +175,7 @@ exports.getModule = class FileAreaList extends MenuModule { // const metaValues = FileEntry.getWellKnownMetaValues(); metaValues.forEach(name => { - const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : ''; + const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A'; entryInfo[_.camelCase(name)] = value; }); @@ -193,7 +187,8 @@ exports.getModule = class FileAreaList extends MenuModule { entryInfo.archiveTypeDesc = 'N/A'; } - entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported + entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported + entryInfo.hashTags = entryInfo.hashTags || '(none)'; // create a rating string, e.g. "**---" const userRatingTicked = config.userRatingTicked || '*'; @@ -300,7 +295,9 @@ exports.getModule = class FileAreaList extends MenuModule { } return self.loadFileIds(callback); }, - function loadCurrentFileInfo(callback) { + function loadCurrentFileInfo(callback) { + self.currentFileEntry = new FileEntry(); + self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { self.populateCurrentEntryInfo(); return callback(err); From 1ef546d5692cda56c9b83723473a45bdbc62704b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Oct 2016 21:56:45 -0600 Subject: [PATCH 14/86] Download queue manager --- core/download_queue.js | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 core/download_queue.js diff --git a/core/download_queue.js b/core/download_queue.js new file mode 100644 index 00000000..6b909643 --- /dev/null +++ b/core/download_queue.js @@ -0,0 +1,47 @@ +/* jslint node: true */ +'use strict'; + +const FileEntry = require('./file_entry.js'); + +module.exports = class DownloadQueue { + constructor(user) { + this.user = user; + + this.user.downloadQueue = this.user.downloadQueue || []; + } + + toggle(fileEntry) { + if(this.isQueued(fileEntry)) { + this.user.downloadQueue = this.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); + } else { + this.add(fileEntry); + } + } + + add(fileEntry) { + this.user.downloadQueue.push({ + fileId : fileEntry.fileId, + areaTag : fileEntry.areaTag, + fileName : fileEntry.fileName, + byteSize : fileEntry.meta.byteSize || 0, + }); + } + + isQueued(entryOrId) { + if(entryOrId instanceof FileEntry) { + entryOrId = entryOrId.fileId; + } + + return this.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; + } + + toProperty() { return JSON.stringify(this.user.downloadQueue); } + + loadFromProperty(prop) { + try { + this.user.downloadQueue = JSON.parse(prop); + } catch(e) { + this.user.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); + } + } +}; From 712cf512f0a115bf5de341d75fb8a3972d0fbeb1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Oct 2016 21:57:02 -0600 Subject: [PATCH 15/86] * Add another year est regex * Add queue file support --- core/config.js | 1 + core/crc.js | 2 +- core/text_view.js | 2 +- mods/file_area_list.js | 47 +++++++++++++++++++++++++++--------------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/core/config.js b/core/config.js index 059051bc..27721b0c 100644 --- a/core/config.js +++ b/core/config.js @@ -358,6 +358,7 @@ function getDefaultConfig() { // '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. "\\B('[1789][0-9])\\b", // eslint-disable-line quotes + '[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], diff --git a/core/crc.js b/core/crc.js index 5d9e9e02..869d6693 100644 --- a/core/crc.js +++ b/core/crc.js @@ -19,4 +19,4 @@ exports.CRC32 = class CRC32 { finalize() { return (this.crc ^ (-1)) >>> 0; } -} \ No newline at end of file +}; diff --git a/core/text_view.js b/core/text_view.js index 8d8439a0..210b05b9 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -102,7 +102,7 @@ function TextView(options) { renderLength = renderStringLength(textToDraw); - if(renderLength > this.dimens.width) { + if(renderLength >= this.dimens.width) { if(this.hasFocus) { if(this.horizScroll) { textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index f1357f2f..eb83708a 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -12,6 +12,7 @@ const FileArea = require('../core/file_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'); // deps const async = require('async'); @@ -21,21 +22,7 @@ const paths = require('path'); /* Misc TODO - * Allow rating to be user defined colors & characters/etc. - * - - - Well known file entry meta values: - * upload_by_username - * upload_by_user_id - * file_md5 - * file_sha256 - * file_crc32 - * est_release_year - * dl_count - * byte_size - * user_rating - * + */ exports.moduleInfo = { @@ -56,6 +43,7 @@ const MciViewIds = { browse : { desc : 1, navMenu : 2, + queueToggle : 3, // active queue toggle indicator - others avail in customs as {isQueued} // 10+ = customs }, details : { @@ -82,12 +70,12 @@ exports.getModule = class FileAreaList extends MenuModule { constructor(options) { super(options); - const config = this.menuConfig.config; - if(options.extraArgs) { this.filterCriteria = options.extraArgs.filterCriteria; } + this.dlQueue = new DownloadQueue(this.client.user); + this.filterCriteria = this.filterCriteria || { // :TODO: set area tag - all in current area by default }; @@ -119,6 +107,11 @@ exports.getModule = class FileAreaList extends MenuModule { 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); + }, }; } @@ -155,6 +148,8 @@ exports.getModule = class FileAreaList extends MenuModule { 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 = this.currentFileEntry.entryInfo = { fileId : currEntry.fileId, @@ -167,6 +162,7 @@ exports.getModule = class FileAreaList extends MenuModule { descLong : currEntry.descLong || '', uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), + isQueued : this.dlQueue.isQueued(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator, }; // @@ -311,6 +307,7 @@ exports.getModule = class FileAreaList extends MenuModule { } } + self.updateQueueIndicator(); self.populateCustomLabels('browse', 10); return callback(null); @@ -364,6 +361,22 @@ exports.getModule = class FileAreaList extends MenuModule { ); } + updateQueueIndicator() { + const indicatorView = this.viewControllers.browse.getView(MciViewIds.browse.queueToggle); + + if(indicatorView) { + const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; + const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; + + indicatorView.setText(stringFormat( + this.dlQueue.isQueued(this.currentFileEntry) ? + isQueuedIndicator : + isNotQueuedIndicator + ) + ); + } + } + cacheArchiveEntries(cb) { // check cache if(this.currentFileEntry.archiveEntries) { From a7c0f2b7b05eb4b4dcd65a3b12582b6b322e79ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 24 Oct 2016 21:49:45 -0600 Subject: [PATCH 16/86] * Add FileBaseFilters * Add HTTP(S) file web server with temp URLs * Get temp web d/l from file list * Add File area filter editor (all file area stuff will be rename to file "base" later) * Concept of "listening servers" vs "login servers" * Ability to get servers by their package name * New MCI: %FN: File Base active filter name * Some ES6 updates * VC resetInitialFocus() to set focus to explicit/detected initial focus field * Limit what is dumped out when logging form data --- core/bbs.js | 136 +++------------- core/client_connections.js | 5 + core/config.js | 23 +++ core/config_cache.js | 4 +- core/database.js | 7 + core/download_queue.js | 20 +-- core/file_area.js | 20 +-- core/file_area_web.js | 230 +++++++++++++++++++++++++++ core/file_base_filter.js | 88 +++++++++++ core/file_entry.js | 15 +- core/listening_server.js | 65 ++++++++ core/login_server_module.js | 87 +++++++++++ core/module_util.js | 5 +- core/predefined_mci.js | 5 + core/servers/content/web.js | 123 +++++++++++++++ core/servers/login/ssh.js | 90 +++++------ core/servers/login/telnet.js | 52 ++++--- core/user_login.js | 119 +++++++------- core/view_controller.js | 43 ++++-- mods/file_area_filter_edit.js | 283 ++++++++++++++++++++++++++++++++++ mods/file_area_list.js | 95 +++++++++++- package.json | 4 +- 22 files changed, 1233 insertions(+), 286 deletions(-) create mode 100644 core/file_area_web.js create mode 100644 core/file_base_filter.js create mode 100644 core/listening_server.js create mode 100644 core/login_server_module.js create mode 100644 core/servers/content/web.js create mode 100644 mods/file_area_filter_edit.js diff --git a/core/bbs.js b/core/bbs.js index d1ea390f..b0f64838 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -76,9 +76,6 @@ function bbsMain() { return callback(err); }); }, - function listenConnections(callback) { - return startListening(callback); - } ], function complete(err) { // note this is escaped: @@ -113,6 +110,12 @@ function shutdownSystem() { } callback(null); }, + function stopListeningServers(callback) { + return require('./listening_server.js').shutdown( () => { + // :TODO: log err + return callback(null); // ignore err + }); + }, function stopEventScheduler(callback) { if(initServices.eventScheduler) { return initServices.eventScheduler.shutdown( () => { @@ -122,6 +125,12 @@ function shutdownSystem() { return callback(null); } }, + function stopFileAreaWeb(callback) { + require('./file_area_web.js').startup(err => { + // :TODO: Log me if err + return callback(null); + }); + }, function stopMsgNetwork(callback) { require('./msg_network.js').shutdown(callback); } @@ -222,6 +231,12 @@ function initialize(cb) { function readyMessageNetworkSupport(callback) { return require('./msg_network.js').startup(callback); }, + function listenConnections(callback) { + return require('./listening_server.js').startup(callback); + }, + function readyFileAreaWeb(callback) { + return require('./file_area_web.js').startup(callback); + }, function readyEventScheduler(callback) { const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; EventSchedulerModule.loadAndStart( (err, modInst) => { @@ -235,118 +250,3 @@ function initialize(cb) { } ); } - -function startListening(cb) { - if(!conf.config.loginServers) { - // :TODO: Log error ... output to stderr as well. We can do it all with the logger - return cb(new Error('No login servers configured')); - } - - const moduleUtil = require('./module_util.js'); // late load so we get Config - - moduleUtil.loadModulesForCategory('loginServers', (err, module) => { - if(err) { - if('EENIGMODDISABLED' === err.code) { - logger.log.debug(err.message); - } else { - logger.log.info( { err : err }, 'Failed loading module'); - } - return; - } - - const port = parseInt(module.runtime.config.port); - if(isNaN(port)) { - logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)'); - return; - } - - const moduleInst = new module.getModule(); - let server; - try { - server = moduleInst.createServer(); - } catch(e) { - logger.log.warn(e, 'Exception caught creating server!'); - return; - } - - // :TODO: handle maxConnections, e.g. conf.maxConnections - - server.on('client', function newClient(client, clientSock) { - // - // Start tracking the client. We'll assign it an ID which is - // just the index in our connections array. - // - if(_.isUndefined(client.session)) { - client.session = {}; - } - - client.session.serverName = module.moduleInfo.name; - client.session.isSecure = module.moduleInfo.isSecure || false; - - clientConns.addNewClient(client, clientSock); - - client.on('ready', function clientReady(readyOptions) { - - client.startIdleMonitor(); - - // Go to module -- use default error handler - prepareClient(client, function clientPrepared() { - require('./connect.js').connectEntry(client, readyOptions.firstMenu); - }); - }); - - client.on('end', function onClientEnd() { - clientConns.removeClient(client); - }); - - client.on('error', function onClientError(err) { - logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message); - }); - - client.on('close', function onClientClose(hadError) { - const logFunc = hadError ? logger.log.info : logger.log.debug; - logFunc( { clientId : client.session.id }, 'Connection closed'); - - clientConns.removeClient(client); - }); - - client.on('idle timeout', function idleTimeout() { - client.log.info('User idle timeout expired'); - - client.menuStack.goto('idleLogoff', function goMenuRes(err) { - if(err) { - // likely just doesn't exist - client.term.write('\nIdle timeout expired. Goodbye!\n'); - client.end(); - } - }); - }); - }); - - server.on('error', function serverErr(err) { - logger.log.info(err); // 'close' should be handled after - }); - - server.listen(port); - - logger.log.info( - { server : module.moduleInfo.name, port : port }, 'Listening for connections'); - }, err => { - cb(err); - }); -} - -function prepareClient(client, cb) { - const theme = require('./theme.js'); - - // :TODO: it feels like this should go somewhere else... and be a bit more elegant. - - if('*' === conf.config.preLoginTheme) { - client.user.properties.theme_id = theme.getRandomTheme() || ''; - } else { - client.user.properties.theme_id = conf.config.preLoginTheme; - } - - theme.setClientTheme(client, client.user.properties.theme_id); - return cb(null); // note: currently useless to use cb here - but this may change...again... -} \ No newline at end of file diff --git a/core/client_connections.js b/core/client_connections.js index e4a5c5ff..2b3c1714 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -12,6 +12,7 @@ exports.getActiveConnections = getActiveConnections; exports.getActiveNodeList = getActiveNodeList; exports.addNewClient = addNewClient; exports.removeClient = removeClient; +exports.getConnectionByUserId = getConnectionByUserId; const clientConnections = []; exports.clientConnections = clientConnections; @@ -93,3 +94,7 @@ function removeClient(client) { ); } } + +function getConnectionByUserId(userId) { + return getActiveConnections().find( ac => userId === ac.user.userId ); +} \ No newline at end of file diff --git a/core/config.js b/core/config.js index 27721b0c..929cd5a9 100644 --- a/core/config.js +++ b/core/config.js @@ -211,6 +211,23 @@ function getDefaultConfig() { } }, + contentServers : { + web : { + domain : 'another-fine-enigma-bbs.org', + + http : { + enabled : false, + port : 8080, + }, + https : { + enabled : false, + port : 8443, + certPem : paths.join(__dirname, './../misc/https_cert.pem'), + keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'), + } + } + }, + archives : { archivers : { '7Zip' : { @@ -362,6 +379,12 @@ function getDefaultConfig() { // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], + web : { + path : '/f/', + routePath : '/f/[a-zA-Z0-9]+$', + expireMinutes : 1440, // 1 day + }, + areas: { message_attachment : { name : 'Message attachments', diff --git a/core/config_cache.js b/core/config_cache.js index 41bdef16..fa80c19d 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -40,11 +40,11 @@ function ConfigCache() { this.gaze.on('changed', function fileChanged(filePath) { assert(filePath in self.cache); - Log.info( { filePath : filePath }, 'Configuration file changed; recaching'); + Log.info( { path : filePath }, 'Configuration file changed; re-caching'); self.reCacheConfigFromFile(filePath, function reCached(err) { if(err) { - Log.error( { error : err, filePath : filePath } , 'Error recaching HJSON config'); + Log.error( { error : err.message, path : filePath } , 'Failed re-caching configuration'); } else { self.emit('recached', filePath); } diff --git a/core/database.js b/core/database.js index 608c263a..e5496ce4 100644 --- a/core/database.js +++ b/core/database.js @@ -333,5 +333,12 @@ const DB_INIT_TABLE = { UNIQUE(hash_tag_id, file_id) );` ); + + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_web_serve ( + hash_id VARCHAR NOT NULL PRIMARY KEY, + expire_timestamp DATETIME NOT NULL + );` + ); } }; \ No newline at end of file diff --git a/core/download_queue.js b/core/download_queue.js index 6b909643..7554e885 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -4,22 +4,22 @@ const FileEntry = require('./file_entry.js'); module.exports = class DownloadQueue { - constructor(user) { - this.user = user; + constructor(client) { + this.client = client; - this.user.downloadQueue = this.user.downloadQueue || []; + this.loadFromProperty(client); } toggle(fileEntry) { if(this.isQueued(fileEntry)) { - this.user.downloadQueue = this.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); + this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); } else { this.add(fileEntry); } } add(fileEntry) { - this.user.downloadQueue.push({ + this.client.user.downloadQueue.push({ fileId : fileEntry.fileId, areaTag : fileEntry.areaTag, fileName : fileEntry.fileName, @@ -32,16 +32,18 @@ module.exports = class DownloadQueue { entryOrId = entryOrId.fileId; } - return this.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; + return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; } - toProperty() { return JSON.stringify(this.user.downloadQueue); } + toProperty() { return JSON.stringify(this.client.user.downloadQueue); } loadFromProperty(prop) { try { - this.user.downloadQueue = JSON.parse(prop); + this.client.user.downloadQueue = JSON.parse(prop); } catch(e) { - this.user.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); + this.client.user.downloadQueue = []; + + this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); } } }; diff --git a/core/file_area.js b/core/file_area.js index 46cb0016..bd5cc336 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -21,8 +21,9 @@ const iconv = require('iconv-lite'); exports.getAvailableFileAreas = getAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; -exports.getDefaultFileAreaTag = getDefaultFileAreaTag; +exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getFileAreaByTag = getFileAreaByTag; +exports.getFileEntryPath = getFileEntryPath; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; //exports.addOrUpdateFileEntry = addOrUpdateFileEntry; exports.scanFileAreaForChanges = scanFileAreaForChanges; @@ -46,15 +47,7 @@ function getAvailableFileAreas(client, options) { } function getSortedAvailableFileAreas(client, options) { - const areas = _.map(getAvailableFileAreas(client, options), (v, k) => { - const areaInfo = { - areaTag : k, - area : v - }; - - return areaInfo; - }); - + const areas = _.map(getAvailableFileAreas(client, options), v => v); sortAreasOrConfs(areas, 'area'); return areas; } @@ -124,6 +117,13 @@ function getAreaStorageDirectory(areaInfo) { return paths.join(Config.fileBase.areaStoragePrefix, areaInfo.storageDir || ''); } +function getFileEntryPath(fileEntry) { + const areaInfo = getFileAreaByTag(fileEntry.areaTag); + if(areaInfo) { + return paths.join(areaInfo.storageDirectory, fileEntry.fileName); + } +} + function getExistingFileEntriesBySha1(sha1, cb) { const entries = []; diff --git a/core/file_area_web.js b/core/file_area_web.js new file mode 100644 index 00000000..3975390b --- /dev/null +++ b/core/file_area_web.js @@ -0,0 +1,230 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Config = require('./config.js').config; +const FileDb = require('./database.js').dbs.file; +const getISOTimestampString = require('./database.js').getISOTimestampString; +const FileEntry = require('./file_entry.js'); +const getServer = require('./listening_server.js').getServer; +const Errors = require('./enig_error.js').Errors; + +// deps +const hashids = require('hashids'); +const moment = require('moment'); +const paths = require('path'); +const async = require('async'); +const fs = require('fs'); +const mimeTypes = require('mime-types'); + +const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server'; + + /* + :TODO: + * Load temp download URLs @ startup & set expire timers via scheduler. + * At creation, set expire timer via scheduler + * + */ + +class FileAreaWebAccess { + constructor() { + this.hashids = new hashids(Config.general.boardName); + } + + startup(cb) { + const self = this; + + async.series( + [ + function initFromDb(callback) { + // :TODO: Init from DB & register expiration timers + return callback(null); + }, + function addWebRoute(callback) { + const webServer = getServer(WEB_SERVER_PACKAGE_NAME); + if(!webServer) { + return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`)); + } + + const routeAdded = webServer.instance.addRoute({ + method : 'GET', + path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable + handler : self.routeWebRequest.bind(self), + }); + + return callback(routeAdded ? null : Errors.General('Failed adding route')); + } + ], + err => { + return cb(err); + } + ); + } + + shutdown(cb) { + return cb(null); + } + + load(cb) { + return cb(null); // :TODO: Load from db + } + + loadServedHashId(hashId, cb) { + FileDb.get( + `SELECT expire_timestamp FROM + file_web_serve + WHERE hash_id = ?`, + [ hashId ], + (err, result) => { + if(err) { + return cb(err); + } + + const decoded = this.hashids.decode(hashId); + if(!result || 2 !== decoded.length) { + return cb(Errors.Invalid('Invalid or unknown hash ID')); + } + + return cb( + null, + { + hashId : hashId, + userId : decoded[0], + fileId : decoded[1], + expireTimestamp : moment(result.expire_timestamp), + } + ); + } + ); + } + + getHashId(client, fileEntry) { + // + // Hashid is a unique combination of userId & fileId + // + return this.hashids.encode(client.user.userId, fileEntry.fileId); + } + + buildTempDownloadLink(client, fileEntry, hashId) { + hashId = hashId || this.getHashId(client, fileEntry); + + // + // Create a URL such as + // https://l33t.codes:44512/f/qFdxyZr + // + // :TODO: build from config + + // + // Prefer HTTPS over HTTP. Be explicit about the port + // only if required. + // + let schema; + let port; + if(Config.contentServers.web.https.enabled) { + schema = 'https://'; + port = (443 === Config.contentServers.web.https.port) ? + '' : + `:${Config.contentServers.web.https.port}`; + } else { + schema = 'http://'; + port = (80 === Config.contentServers.web.http.port) ? + '' : + `:${Config.contentServers.web.http.port}`; + } + + return `${schema}${Config.contentServers.web.domain}${port}${Config.fileBase.web.path}${hashId}`; + } + + getExistingTempDownloadServeItem(client, fileEntry, cb) { + const hashId = this.getHashId(client, fileEntry); + this.loadServedHashId(hashId, (err, servedItem) => { + if(err) { + return cb(err); + } + + servedItem.url = this.buildTempDownloadLink(client, fileEntry); + + return cb(null, servedItem); + }); + } + + createAndServeTempDownload(client, fileEntry, options, cb) { + const hashId = this.getHashId(client, fileEntry); + const url = this.buildTempDownloadLink(client, fileEntry, hashId); + options.expireTime = options.expireTime || moment().add(2, 'days'); + + // add/update rec with hash id and (latest) timestamp + FileDb.run( + `REPLACE INTO file_web_serve (hash_id, expire_timestamp) + VALUES (?, ?);`, + [ hashId, getISOTimestampString(options.expireTime) ], + err => { + if(err) { + return cb(err); + } + + // :TODO: setup tracking of expiration time so we can clean up the entry + + return cb(null, url); + } + ); + } + + fileNotFound(resp) { + resp.writeHead(404, { 'Content-Type' : 'text/html' } ); + + // :TODO: allow custom 404 + return resp.end('Not found'); + } + + routeWebRequest(req, resp) { + const hashId = paths.basename(req.url); + + this.loadServedHashId(hashId, (err, servedItem) => { + + if(err) { + return this.fileNotFound(resp); + } + + const fileEntry = new FileEntry(); + fileEntry.load(servedItem.fileId, err => { + if(err) { + return this.fileNotFound(resp); + } + + const filePath = fileEntry.filePath; + if(!filePath) { + return this.fileNotFound(resp); + } + + fs.stat(filePath, (err, stats) => { + if(err) { + return this.fileNotFound(resp); + } + + resp.on('close', () => { + // connection closed *before* the response was fully sent + // :TODO: Log and such + }); + + resp.on('finish', () => { + // transfer completed fully + // :TODO: we need to update the users stats - bytes xferred, credit stuff, etc. + }); + + const headers = { + 'Content-Type' : mimeTypes.contentType(paths.extname(filePath)) || mimeTypes.contentType('.bin'), + 'Content-Length' : stats.size, + 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, + }; + + const readStream = fs.createReadStream(filePath); + resp.writeHead(200, headers); + return readStream.pipe(resp); + }); + }); + }); + } +} + +module.exports = new FileAreaWebAccess(); \ No newline at end of file diff --git a/core/file_base_filter.js b/core/file_base_filter.js new file mode 100644 index 00000000..10628747 --- /dev/null +++ b/core/file_base_filter.js @@ -0,0 +1,88 @@ +/* jslint node: true */ +'use strict'; + +const _ = require('lodash'); +const uuids = require('node-uuid'); + +module.exports = class FileBaseFilters { + constructor(client) { + this.client = client; + + this.load(); + } + + static get OrderByValues() { + return [ 'ascending', 'descending' ]; + } + + static get SortByValues() { + return [ + 'upload_timestamp', + 'upload_by_username', + 'dl_count', + 'user_rating', + 'est_release_year', + 'byte_size', + ]; + } + + toArray() { + return _.map(this.filters, (filter, uuid) => { + return Object.assign( { uuid : uuid }, filter ); + }); + } + + get(filterUuid) { + return this.filters[filterUuid]; + } + + add(filterInfo) { + const filterUuid = uuids.v4(); + + filterInfo.tags = this.cleanTags(filterInfo.tags); + + this.filters[filterUuid] = filterInfo; + + return filterUuid; + } + + remove(filterUuid) { + delete this.filters[filterUuid]; + } + + load(prop) { + prop = prop || this.client.user.properties.file_base_filters; + + try { + this.filters = JSON.parse(prop); + } catch(e) { + this.filters = {}; + + this.client.log.error( { error : e.message, property : prop }, 'Failed parsing file base filters property' ); + } + } + + persist(cb) { + return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb); + } + + cleanTags(tags) { + return tags.toLowerCase().replace(/,?\s+|\,/g, ' ').trim(); + } + + setActive(filterUuid) { + const activeFilter = this.get(filterUuid); + + if(activeFilter) { + this.activeFilter = activeFilter; + this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); + return true; + } + + return false; + } + + static getActiveFilter(client) { + return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); + } +}; diff --git a/core/file_entry.js b/core/file_entry.js index dd656442..17d2995f 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -3,11 +3,13 @@ const fileDb = require('./database.js').dbs.file; const Errors = require('./enig_error.js').Errors; -const getISOTimestampString = require('./database.js').getISOTimestampString; +const getISOTimestampString = require('./database.js').getISOTimestampString; +const Config = require('./config.js').config; // deps const async = require('async'); const _ = require('lodash'); +const paths = require('path'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha1', 'file_name', @@ -130,6 +132,17 @@ module.exports = class FileEntry { ); } + get filePath() { + const areaInfo = Config.fileAreas.areas[this.areaTag]; + if(areaInfo) { + return paths.join( + Config.fileBase.areaStoragePrefix, + areaInfo.storageDir || '', + this.fileName + ); + } + } + static persistMetaValue(fileId, name, value, cb) { fileDb.run( `REPLACE INTO file_meta (file_id, meta_name, meta_value) diff --git a/core/listening_server.js b/core/listening_server.js new file mode 100644 index 00000000..e1009338 --- /dev/null +++ b/core/listening_server.js @@ -0,0 +1,65 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const logger = require('./logger.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); + +const listeningServers = {}; // packageName -> info + +exports.startup = startup; +exports.shutdown = shutdown; +exports.getServer = getServer; + +function startup(cb) { + return startListening(cb); +} + +function shutdown(cb) { + return cb(null); +} + +function getServer(packageName) { + return listeningServers[packageName]; +} + +function startListening(cb) { + const moduleUtil = require('./module_util.js'); // late load so we get Config + + async.each( [ 'login', 'content' ], (category, next) => { + moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { + // :TODO: use enig error here! + if(err) { + if('EENIGMODDISABLED' === err.code) { + logger.log.debug(err.message); + } else { + logger.log.info( { err : err }, 'Failed loading module'); + } + return; + } + + const moduleInst = new module.getModule(); + try { + moduleInst.createServer(); + if(!moduleInst.listen()) { + throw new Error('Failed listening'); + } + + listeningServers[module.moduleInfo.packageName] = { + instance : moduleInst, + info : module.moduleInfo, + }; + + } catch(e) { + logger.log.error(e, 'Exception caught creating server!'); + } + }, err => { + return next(err); + }); + }, err => { + return cb(err); + }); +} diff --git a/core/login_server_module.js b/core/login_server_module.js new file mode 100644 index 00000000..4f003982 --- /dev/null +++ b/core/login_server_module.js @@ -0,0 +1,87 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const conf = require('./config.js'); +const logger = require('./logger.js'); +const ServerModule = require('./server_module.js').ServerModule; +const clientConns = require('./client_connections.js'); + +// deps +const _ = require('lodash'); + +module.exports = class LoginServerModule extends ServerModule { + constructor() { + super(); + } + + // :TODO: we need to max connections -- e.g. from config 'maxConnections' + + prepareClient(client, cb) { + const theme = require('./theme.js'); + + // + // Choose initial theme before we have user context + // + if('*' === conf.config.preLoginTheme) { + client.user.properties.theme_id = theme.getRandomTheme() || ''; + } else { + client.user.properties.theme_id = conf.config.preLoginTheme; + } + + theme.setClientTheme(client, client.user.properties.theme_id); + return cb(null); // note: currently useless to use cb here - but this may change...again... + } + + handleNewClient(client, clientSock, modInfo) { + // + // Start tracking the client. We'll assign it an ID which is + // just the index in our connections array. + // + if(_.isUndefined(client.session)) { + client.session = {}; + } + + client.session.serverName = modInfo.name; + client.session.isSecure = modInfo.isSecure || false; + + clientConns.addNewClient(client, clientSock); + + client.on('ready', readyOptions => { + + client.startIdleMonitor(); + + // Go to module -- use default error handler + this.prepareClient(client, () => { + require('./connect.js').connectEntry(client, readyOptions.firstMenu); + }); + }); + + client.on('end', () => { + clientConns.removeClient(client); + }); + + client.on('error', err => { + logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message); + }); + + client.on('close', err => { + const logFunc = err ? logger.log.info : logger.log.debug; + logFunc( { clientId : client.session.id }, 'Connection closed'); + + clientConns.removeClient(client); + }); + + client.on('idle timeout', () => { + client.log.info('User idle timeout expired'); + + client.menuStack.goto('idleLogoff', err => { + if(err) { + // likely just doesn't exist + client.term.write('\nIdle timeout expired. Goodbye!\n'); + client.end(); + } + }); + }); + } +}; diff --git a/core/module_util.js b/core/module_util.js index 651a2a8a..9834fe5a 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -59,9 +59,6 @@ function loadModuleEx(options, cb) { return cb(new Error('Invalid or missing "getModule" method for module!')); } - // Ref configuration, if any, for convience to the module - mod.runtime = { config : modConfig }; - return cb(null, mod); } @@ -89,7 +86,7 @@ function loadModulesForCategory(category, iterator, complete) { }); async.each(jsModules, (file, next) => { - loadModule(paths.basename(file, '.js'), category, (err, mod) => { ++ loadModule(paths.basename(file, '.js'), category, (err, mod) => { iterator(err, mod); return next(); }); diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 985225bb..aadd824f 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -8,6 +8,7 @@ const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; const clientConnections = require('./client_connections.js'); const StatLog = require('./stat_log.js'); +const FileBaseFilters = require('./file_base_filter.js'); // deps const packageJson = require('../package.json'); @@ -80,6 +81,10 @@ function getPredefinedMCIValue(client, code) { ND : function connectedNode() { return client.node.toString(); }, IP : function clientIpAddress() { return client.address().address; }, ST : function serverName() { return client.session.serverName; }, + FN : function activeFileBaseFilterName() { + const activeFilter = FileBaseFilters.getActiveFilter(client); + return activeFilter ? activeFilter.name : ''; + }, MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, CS : function currentStatus() { return client.currentStatus; }, diff --git a/core/servers/content/web.js b/core/servers/content/web.js new file mode 100644 index 00000000..6a184284 --- /dev/null +++ b/core/servers/content/web.js @@ -0,0 +1,123 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('../../logger.js').log; +const ServerModule = require('../../server_module.js').ServerModule; +const Config = require('../../config.js').config; + +// deps +const http = require('http'); +const https = require('https'); +const _ = require('lodash'); +const fs = require('fs'); + +const ModuleInfo = exports.moduleInfo = { + name : 'Web', + desc : 'Web Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.web.server', +}; + +class Route { + constructor(route) { + Object.assign(this, route); + + if(this.method) { + this.method = this.method.toUpperCase(); + } + + try { + this.pathRegExp = new RegExp(this.path); + } catch(e) { + Log.debug( { route : route }, 'Invalid regular expression for route path' ); + } + } + + isValid() { + return ( + this.pathRegExp instanceof RegExp && + ( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) || + !_.isFunction(this.handler) + ); + } + + matchesRequest(req) { return req.method === this.method && this.pathRegExp.test(req.url); } + + getRouteKey() { return `${this.method}:${this.path}`; } +} + +exports.getModule = class WebServerModule extends ServerModule { + constructor() { + super(); + + this.enableHttp = Config.contentServers.web.http.enabled || true; + this.enableHttps = Config.contentServers.web.https.enabled || false; + + this.routes = {}; + } + + createServer() { + if(this.enableHttp) { + this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) ); + } + + if(this.enableHttps) { + const options = { + cert : fs.readFileSync(Config.contentServers.web.https.certPem), + key : fs.readFileSync(Config.contentServers.web.https.keyPem), + }; + + // additional options + Object.assign(options, Config.contentServers.web.https.options || {} ); + + this.httpsServer = https.createServer(options, this.routeRequest); + } + } + + listen() { + let ok = true; + + [ 'http', 'https' ].forEach(service => { + const name = `${service}Server`; + if(this[name]) { + const port = parseInt(Config.contentServers.web[service].port); + if(isNaN(port)) { + ok = false; + return Log.warn( { port : Config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); + } + return this[name].listen(port); + } + }); + + return ok; + } + + addRoute(route) { + route = new Route(route); + + if(!route.isValid()) { + Log( { route : route }, 'Cannot add route: missing or invalid required members' ); + return false; + } + + const routeKey = route.getRouteKey(); + if(routeKey in this.routes) { + Log( { route : route }, 'Cannot add route: duplicate method/path combination exists' ); + return false; + } + + this.routes[routeKey] = route; + return true; + } + + routeRequest(req, resp) { + const route = _.find(this.routes, r => r.matchesRequest(req) ); + return route ? route.handler(req, resp) : this.accessDenied(resp); + } + + accessDenied(resp) { + resp.writeHead(401, { 'Content-Type' : 'text/html' } ); + return resp.end('Access denied'); + } +} \ No newline at end of file diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 1abd1e84..51a2da2f 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -2,14 +2,14 @@ 'use strict'; // ENiGMA½ -const Config = require('../../config.js').config; -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const ServerModule = require('../../server_module.js').ServerModule; -const userLogin = require('../../user_login.js').userLogin; -const enigVersion = require('../../../package.json').version; -const theme = require('../../theme.js'); -const stringFormat = require('../../string_format.js'); +const Config = require('../../config.js').config; +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); +const userLogin = require('../../user_login.js').userLogin; +const enigVersion = require('../../../package.json').version; +const theme = require('../../theme.js'); +const stringFormat = require('../../string_format.js'); // deps const ssh2 = require('ssh2'); @@ -18,15 +18,14 @@ const util = require('util'); const _ = require('lodash'); const assert = require('assert'); -exports.moduleInfo = { +const ModuleInfo = exports.moduleInfo = { name : 'SSH', desc : 'SSH Server', author : 'NuSkooler', isSecure : true, + packageName : 'codes.l33t.enigma.ssh.server', }; -exports.getModule = SSHServerModule; - function SSHClient(clientConn) { baseClient.Client.apply(this, arguments); @@ -226,40 +225,45 @@ util.inherits(SSHClient, baseClient.Client); SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ]; -function SSHServerModule() { - ServerModule.call(this); -} +exports.getModule = class SSHServerModule extends LoginServerModule { + constructor() { + super(); + } -util.inherits(SSHServerModule, ServerModule); + createServer() { + const serverConf = { + hostKeys : [ + { + key : fs.readFileSync(Config.loginServers.ssh.privateKeyPem), + passphrase : Config.loginServers.ssh.privateKeyPass, + } + ], + ident : 'enigma-bbs-' + enigVersion + '-srv', + + // Note that sending 'banner' breaks at least EtherTerm! + debug : (sshDebugLine) => { + if(true === Config.loginServers.ssh.traceConnections) { + Log.trace(`SSH: ${sshDebugLine}`); + } + }, + }; -SSHServerModule.prototype.createServer = function() { - SSHServerModule.super_.prototype.createServer.call(this); + this.server = ssh2.Server(serverConf); + this.server.on('connection', (conn, info) => { + Log.info(info, 'New SSH connection'); + this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo); + }); + } - const serverConf = { - hostKeys : [ - { - key : fs.readFileSync(Config.loginServers.ssh.privateKeyPem), - passphrase : Config.loginServers.ssh.privateKeyPass, - } - ], - ident : 'enigma-bbs-' + enigVersion + '-srv', - - // Note that sending 'banner' breaks at least EtherTerm! - debug : (sshDebugLine) => { - if(true === Config.loginServers.ssh.traceConnections) { - Log.trace(`SSH: ${sshDebugLine}`); - } - }, - }; + listen() { + const port = parseInt(Config.loginServers.ssh.port); + if(isNaN(port)) { + Log.error( { server : ModuleInfo.name, port : Config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); + return false; + } - const server = ssh2.Server(serverConf); - server.on('connection', function onConnection(conn, info) { - Log.info(info, 'New SSH connection'); - - const client = new SSHClient(conn); - - this.emit('client', client, conn._sock); - }); - - return server; + this.server.listen(port); + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + return true; + } }; diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 7573e62e..406e28a5 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -2,10 +2,10 @@ 'use strict'; // ENiGMA½ -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const ServerModule = require('../../server_module.js').ServerModule; -const Config = require('../../config.js').config; +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); +const Config = require('../../config.js').config; // deps const net = require('net'); @@ -16,16 +16,14 @@ const util = require('util'); //var debug = require('debug')('telnet'); -exports.moduleInfo = { +const ModuleInfo = exports.moduleInfo = { name : 'Telnet', desc : 'Telnet Server', author : 'NuSkooler', isSecure : false, + packageName : 'codes.l33t.enigma.telnet.server', }; -exports.getModule = TelnetServerModule; - - // // Telnet Protocol Resources // * http://pcmicro.com/netfoss/telnet.html @@ -767,22 +765,34 @@ Object.keys(OPTIONS).forEach(function(name) { }); }); -function TelnetServerModule() { - ServerModule.call(this); -} +exports.getModule = class TelnetServerModule extends LoginServerModule { + constructor() { + super(); + } -util.inherits(TelnetServerModule, ServerModule); + createServer() { + this.server = net.createServer( sock => { + const client = new TelnetClient(sock, sock); -TelnetServerModule.prototype.createServer = function() { - TelnetServerModule.super_.prototype.createServer.call(this); + client.banner(); - const server = net.createServer( (sock) => { - const client = new TelnetClient(sock, sock); - - client.banner(); + this.handleNewClient(client, sock, ModuleInfo); + }); - server.emit('client', client, sock); - }); + this.server.on('error', err => { + Log.info( { error : err.message }, 'Telnet server error'); + }); + } - return server; + listen() { + const port = parseInt(Config.loginServers.telnet.port); + if(isNaN(port)) { + Log.error( { server : ModuleInfo.name, port : Config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); + return false; + } + + this.server.listen(port); + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + return true; + } }; diff --git a/core/user_login.js b/core/user_login.js index b122b372..84d20a9d 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -4,7 +4,6 @@ // ENiGMA½ const setClientTheme = require('./theme.js').setClientTheme; const clientConnections = require('./client_connections.js').clientConnections; -const userDb = require('./database.js').dbs.user; const StatLog = require('./stat_log.js'); const logger = require('./logger.js'); @@ -21,66 +20,64 @@ function userLogin(client, username, password, cb) { // :TODO: if username exists, record failed login attempt to properties // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true - cb(err); - } else { - const now = new Date(); - const user = client.user; - - // - // Ensure this user is not already logged in. - // Loop through active connections -- which includes the current -- - // and check for matching user ID. If the count is > 1, disallow. - // - var existingClientConnection; - clientConnections.forEach(function connEntry(cc) { - if(cc.user !== user && cc.user.userId === user.userId) { - existingClientConnection = cc; - } - }); - - if(existingClientConnection) { - client.log.info( { - existingClientId : existingClientConnection.session.id, - username : user.username, - userId : user.userId }, - 'Already logged in' - ); - - var existingConnError = new Error('Already logged in as supplied user'); - existingConnError.existingConn = true; - - return cb(existingClientConnection); - } - - - // update client logger with addition of username - client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username }); - client.log.info('Successful login'); - - async.parallel( - [ - function setTheme(callback) { - setClientTheme(client, user.properties.theme_id); - callback(null); - }, - function updateSystemLoginCount(callback) { - StatLog.incrementSystemStat('login_count', 1, callback); - }, - function recordLastLogin(callback) { - StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); - }, - function updateUserLoginCount(callback) { - StatLog.incrementUserStat(user, 'login_count', 1, callback); - }, - function recordLoginHistory(callback) { - const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers - StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); - } - ], - function complete(err) { - cb(err); - } - ); + return cb(err); } + const user = client.user; + + // + // Ensure this user is not already logged in. + // Loop through active connections -- which includes the current -- + // and check for matching user ID. If the count is > 1, disallow. + // + let existingClientConnection = + clientConnections.forEach(function connEntry(cc) { + if(cc.user !== user && cc.user.userId === user.userId) { + existingClientConnection = cc; + } + }); + + if(existingClientConnection) { + client.log.info( { + existingClientId : existingClientConnection.session.id, + username : user.username, + userId : user.userId }, + 'Already logged in' + ); + + var existingConnError = new Error('Already logged in as supplied user'); + existingConnError.existingConn = true; + + return cb(existingClientConnection); + } + + + // update client logger with addition of username + client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username }); + client.log.info('Successful login'); + + async.parallel( + [ + function setTheme(callback) { + setClientTheme(client, user.properties.theme_id); + callback(null); + }, + function updateSystemLoginCount(callback) { + StatLog.incrementSystemStat('login_count', 1, callback); + }, + function recordLastLogin(callback) { + StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); + }, + function updateUserLoginCount(callback) { + StatLog.incrementUserStat(user, 'login_count', 1, callback); + }, + function recordLoginHistory(callback) { + const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers + StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); + } + ], + function complete(err) { + cb(err); + } + ); }); } \ No newline at end of file diff --git a/core/view_controller.js b/core/view_controller.js index cf90de3e..37f41fad 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -6,7 +6,6 @@ var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; var menuUtil = require('./menu_util.js'); var asset = require('./asset.js'); var ansi = require('./ansi_term.js'); -const Log = require('./logger.js'); // deps var events = require('events'); @@ -449,6 +448,12 @@ ViewController.prototype.setFocus = function(focused) { this.setViewFocusWithEvents(this.focusedView, focused); }; +ViewController.prototype.resetInitialFocus = function() { + if(this.formInitialFocusId) { + return this.switchFocus(this.formInitialFocusId); + } +} + ViewController.prototype.switchFocus = function(id) { // // Perform focus switching validation now @@ -618,7 +623,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { var self = this; var formIdKey = options.formId ? options.formId.toString() : '0'; - var initialFocusId = 1; // default to first + this.formInitialFocusId = 1; // default to first var formConfig; // :TODO: honor options.withoutForm @@ -671,7 +676,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { function applyViewConfiguration(callback) { if(_.isObject(formConfig)) { self.applyViewConfig(formConfig, function configApplied(err, info) { - initialFocusId = info.initialFocusId; + self.formInitialFocusId = info.initialFocusId; callback(err); }); } else { @@ -746,12 +751,12 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { callback(null); }, function drawAllViews(callback) { - self.redrawAll(initialFocusId); + self.redrawAll(self.formInitialFocusId); callback(null); }, function setInitialViewFocus(callback) { - if(initialFocusId) { - self.switchFocus(initialFocusId); + if(self.formInitialFocusId) { + self.switchFocus(self.formInitialFocusId); } callback(null); } @@ -794,7 +799,7 @@ ViewController.prototype.getFormData = function(key) { } */ - var formData = { + const formData = { id : this.formId, submitId : this.focusedView.id, value : {}, @@ -804,6 +809,26 @@ ViewController.prototype.getFormData = function(key) { formData.key = key; } + let viewData; + _.each(this.views, view => { + try { + // don't fill forms with static, non user-editable data data + if(!view.acceptsInput) { + return; + } + + viewData = view.getData(); + if(_.isUndefined(viewData)) { + return; + } + + formData.value[ view.submitArgName ? view.submitArgName : view.id ] = viewData; + } catch(e) { + this.client.log.error( { error : e.message }, 'Exception caught gathering form data' ); + } + }); + /* + var viewData; var view; for(var id in this.views) { @@ -820,10 +845,10 @@ ViewController.prototype.getFormData = function(key) { } catch(e) { this.client.log.error(e); // :TODO: Log better ;) } - } + }*/ return formData; -} +}; /* ViewController.prototype.formatMenuArgs = function(args) { diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js new file mode 100644 index 00000000..7dfc3143 --- /dev/null +++ b/mods/file_area_filter_edit.js @@ -0,0 +1,283 @@ +/* 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_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('../core/file_base_filter.js'); +const stringFormat = require('../core/string_format.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); + +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, + + selectedFilterInfo : 10, // { ...filter object ... } + activeFilterInfo : 11, // { ...filter object ... } + } +}; + +exports.getModule = class FileAreaFilterEdit extends MenuModule { + constructor(options) { + super(options); + + this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them + this.currentFilterIndex = 0; // into |filtersArray| + + this.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(); + + // :TODO: Need to update %FN somehow + return cb(null); + }, + newFilter : (formData, extraArgs, cb) => { + this.currentFilterIndex = this.filtersArray.length; // next avail slot + this.clearForm(true); // true=reset focus + 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 + // :TODO: is this what we want? + this.client.user.properties.file_base_filter_active_uuid = 'none'; + } + } + + // update UI + if(this.filtersArray.length > 0) { + this.loadDataForFilter(this.currentFilterIndex); + } else { + this.clearForm(true); // true=reset focus + } + return cb(null); + }); + } + }; + } + + 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(setFocus) { + [ 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(setFocus) { + 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); + } 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(); + } + } +}; diff --git a/mods/file_area_list.js b/mods/file_area_list.js index eb83708a..193021da 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -13,6 +13,7 @@ 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'); // deps const async = require('async'); @@ -43,7 +44,6 @@ const MciViewIds = { browse : { desc : 1, navMenu : 2, - queueToggle : 3, // active queue toggle indicator - others avail in customs as {isQueued} // 10+ = customs }, details : { @@ -74,7 +74,7 @@ exports.getModule = class FileAreaList extends MenuModule { this.filterCriteria = options.extraArgs.filterCriteria; } - this.dlQueue = new DownloadQueue(this.client.user); + this.dlQueue = new DownloadQueue(this.client); this.filterCriteria = this.filterCriteria || { // :TODO: set area tag - all in current area by default @@ -112,6 +112,9 @@ exports.getModule = class FileAreaList extends MenuModule { this.updateQueueIndicator(); return cb(null); }, + showWebDownloadLink : (formData, extraArgs, cb) => { + return this.fetchAndDisplayWebDownloadLink(cb); + }, }; } @@ -141,7 +144,7 @@ exports.getModule = class FileAreaList extends MenuModule { ); } - populateCurrentEntryInfo() { + populateCurrentEntryInfo(cb) { const config = this.menuConfig.config; const currEntry = this.currentFileEntry; @@ -163,6 +166,8 @@ exports.getModule = class FileAreaList extends MenuModule { uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), isQueued : this.dlQueue.isQueued(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator, + webDlLink : '', // :TODO: fetch web any existing web d/l link + webDlExpire : '', // :TODO: fetch web d/l link expire time }; // @@ -194,9 +199,27 @@ exports.getModule = class FileAreaList extends MenuModule { 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.updateCustomLabelsWithFilter(category, startId); + } + + updateCustomLabelsWithFilter(category, startId, filter) { let textView; let customMciId = startId; const config = this.menuConfig.config; @@ -205,7 +228,7 @@ exports.getModule = class FileAreaList extends MenuModule { const key = `${category}InfoFormat${customMciId}`; const format = config[key]; - if(format) { + if(format && (!filter || filter.find(f => format.indexOf(f) > - 1))) { textView.setText(stringFormat(format, this.currentFileEntry.entryInfo)); } @@ -295,8 +318,11 @@ exports.getModule = class FileAreaList extends MenuModule { self.currentFileEntry = new FileEntry(); self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { - self.populateCurrentEntryInfo(); - return callback(err); + if(err) { + return callback(err); + } + + return self.populateCurrentEntryInfo(callback); }); }, function populateViews(callback) { @@ -360,8 +386,63 @@ exports.getModule = class FileAreaList extends MenuModule { } ); } + + 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.updateCustomLabelsWithFilter( 'browse', 10, [ '{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.updateCustomLabelsWithFilter( 'browse', 10, [ '{isQueued}' ] ); + /* const indicatorView = this.viewControllers.browse.getView(MciViewIds.browse.queueToggle); if(indicatorView) { @@ -374,7 +455,7 @@ exports.getModule = class FileAreaList extends MenuModule { isNotQueuedIndicator ) ); - } + }*/ } cacheArchiveEntries(cb) { diff --git a/package.json b/package.json index d35884eb..0c4c175e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "ptyw.js": "NuSkooler/ptyw.js", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", - "temp": "^0.8.3" + "temp": "^0.8.3", + "hashids" : "^1.1.1", + "mime-types" : "^2.1.12" }, "engines": { "node": ">=4.2.0" From 6da7d557f91c21c742dc1e30ccf90e9e271e3141 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 6 Dec 2016 18:58:56 -0700 Subject: [PATCH 17/86] * Improvements to ANSI parser * Introduction of storage tags for file bases / areas * Expiration for file web server items * WIP work on clean ANSI (on hold for a bit while other file base stuff is worked on) --- core/acs.js | 11 ++ core/ansi_escape_parser.js | 137 ++++++++++++++++----- core/conf_area_util.js | 4 +- core/config.js | 26 ++-- core/database.js | 1 + core/download_queue.js | 4 +- core/file_area.js | 115 ++++++++++++++--- core/file_area_web.js | 68 ++++++++-- core/file_entry.js | 53 ++++---- core/sauce.js | 9 +- core/scanner_tossers/ftn_bso.js | 2 + core/string_util.js | 211 +++++++++++++++++++++++--------- core/user.js | 16 +++ mods/file_area_filter_edit.js | 25 +++- mods/file_area_list.js | 51 ++++---- oputil.js | 4 +- 16 files changed, 557 insertions(+), 180 deletions(-) diff --git a/core/acs.js b/core/acs.js index 3d2fd678..484407da 100644 --- a/core/acs.js +++ b/core/acs.js @@ -25,6 +25,9 @@ class ACS { } } + // + // Message Conferences & Areas + // hasMessageConfRead(conf) { return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); } @@ -33,10 +36,17 @@ class ACS { return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead); } + // + // File Base / Areas + // hasFileAreaRead(area) { return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); } + hasFileAreaDownload(area) { + return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload); + } + getConditionalValue(condArray, memberName) { assert(_.isArray(condArray)); assert(_.isString(memberName)); @@ -65,6 +75,7 @@ ACS.Defaults = { MessageConfRead : 'GM[users]', FileAreaRead : 'GM[users]', + FileAreaDownload : 'GM[users]', }; module.exports = ACS; \ No newline at end of file diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index c3df3568..f551cd5b 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -48,8 +48,10 @@ function ANSIEscapeParser(options) { self.row = Math.max(self.row, 1); self.row = Math.min(self.row, self.termHeight); - self.emit('move cursor', self.column, self.row); - self.rowUpdated(); +// self.emit('move cursor', self.column, self.row); + + self.positionUpdated(); + //self.rowUpdated(); }; self.saveCursorPosition = function() { @@ -63,7 +65,9 @@ function ANSIEscapeParser(options) { self.row = self.savedPosition.row; self.column = self.savedPosition.column; delete self.savedPosition; - self.rowUpdated(); + + self.positionUpdated(); +// self.rowUpdated(); }; self.clearScreen = function() { @@ -71,11 +75,76 @@ function ANSIEscapeParser(options) { self.emit('clear screen'); }; +/* self.rowUpdated = function() { self.emit('row update', self.row + self.scrollBack); + };*/ + + self.positionUpdated = function() { + self.emit('position update', self.row, self.column); }; function literal(text) { + let charCode; + let pos; + let start = 0; + const len = text.length; + + function emitLiteral() { + self.emit('literal', text.slice(start, pos)); + start = pos; + } + + for(pos = 0; pos < len; ++pos) { + charCode = text.charCodeAt(pos) & 0xff; // ensure 8bit clean + + switch(charCode) { + case CR : + emitLiteral(); + + self.column = 1; + + self.positionUpdated(); + break; + + case LF : + emitLiteral(); + + self.row += 1; + + self.positionUpdated(); + break; + + default : + if(self.column > self.termWidth) { + // + // Emit data up to this point so it can be drawn before the postion update + // + emitLiteral(); + + self.column = 1; + self.row += 1; + + self.positionUpdated(); + + + } else { + self.column += 1; + } + break; + } + } + + self.emit('literal', text.slice(start)); + + if(self.column > self.termWidth) { + self.column = 1; + self.row += 1; + self.positionUpdated(); + } + } + + function literal2(text) { var charCode; var len = text.length; @@ -88,29 +157,31 @@ function ANSIEscapeParser(options) { case LF : self.row++; - self.rowUpdated(); + self.positionUpdated(); + //self.rowUpdated(); break; default : // wrap - if(self.column === self.termWidth) { + if(self.column > self.termWidth) { self.column = 1; self.row++; - self.rowUpdated(); + //self.rowUpdated(); + self.positionUpdated(); } else { - self.column++; + self.column += 1; } break; } - if(self.row === 26) { // :TODO: should be termHeight + 1 ? - self.scrollBack++; - self.row--; - self.rowUpdated(); + if(self.row === self.termHeight) { + self.scrollBack += 1; + self.row -= 1; + + self.positionUpdated(); } } - //self.emit('chunk', text); self.emit('literal', text); } @@ -188,10 +259,10 @@ function ANSIEscapeParser(options) { } } - self.reset = function(buffer) { + self.reset = function(input) { self.parseState = { // ignore anything past EOF marker, if any - buffer : buffer.split(String.fromCharCode(0x1a), 1)[0], + buffer : input.split(String.fromCharCode(0x1a), 1)[0], re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, stop : false, }; @@ -201,7 +272,11 @@ function ANSIEscapeParser(options) { self.parseState.stop = true; }; - self.parse = function() { + self.parse = function(input) { + if(input) { + self.reset(input); + } + // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. var pos; var match; @@ -308,40 +383,45 @@ function ANSIEscapeParser(options) { */ function escape(opCode, args) { - var arg; - var i; - var len; + let arg; switch(opCode) { // cursor up case 'A' : - arg = args[0] || 1; + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(0, -arg); break; // cursor down case 'B' : - arg = args[0] || 1; + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(0, arg); break; // cursor forward/right case 'C' : - arg = args[0] || 1; + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(arg, 0); break; // cursor back/left case 'D' : - arg = args[0] || 1; + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(-arg, 0); break; case 'f' : // horiz & vertical case 'H' : // cursor position - self.row = args[0] || 1; - self.column = args[1] || 1; - self.rowUpdated(); + //self.row = args[0] || 1; + //self.column = args[1] || 1; + self.row = isNaN(args[0]) ? 1 : args[0]; + self.column = isNaN(args[1]) ? 1 : args[1]; + //self.rowUpdated(); + self.positionUpdated(); break; // save position @@ -356,7 +436,7 @@ function ANSIEscapeParser(options) { // set graphic rendition case 'm' : - for(i = 0, len = args.length; i < len; ++i) { + for(let i = 0, len = args.length; i < len; ++i) { arg = args[i]; if(ANSIEscapeParser.foregroundColors[arg]) { @@ -410,12 +490,13 @@ function ANSIEscapeParser(options) { } } } + break; // m - break; + // :TODO: s, u, K // erase display/screen case 'J' : - // :TODO: Handle others + // :TODO: Handle other 'J' types! if(2 === args[0]) { self.clearScreen(); } diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 6009bb34..5d122cc5 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -16,8 +16,8 @@ function sortAreasOrConfs(areasOrConfs, type) { let entryB; areasOrConfs.sort((a, b) => { - entryA = a[type]; - entryB = b[type]; + entryA = type ? a[type] : a; + entryB = type ? b[type] : b; if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { return entryA.sort - entryB.sort; diff --git a/core/config.js b/core/config.js index 929cd5a9..975e1d55 100644 --- a/core/config.js +++ b/core/config.js @@ -361,17 +361,21 @@ function getDefaultConfig() { fileNamePatterns: { // These are NOT case sensitive + // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ shortDesc : [ '^FILE_ID\.DIZ$', '^DESC\.SDI$', '^DESCRIPT\.ION$', '^FILE\.DES$', '$FILE\.SDI$', '^DISK\.ID$' ], - longDesc : [ '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$' ], + // common README filename - https://en.wikipedia.org/wiki/README + longDesc : [ + '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$', '^READ\.ME$', '^README$', '^README\.md$' + ], }, yearEstPatterns: [ // - // Patterns should produce the year in the first submatch - // The year may be YY or YYYY + // Patterns should produce the year in the first submatch. + // The extracted year may be YY or YYYY // '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. "\\B('[1789][0-9])\\b", // eslint-disable-line quotes @@ -385,17 +389,25 @@ function getDefaultConfig() { expireMinutes : 1440, // 1 day }, + // + // File area storage location tag/value pairs. + // Non-absolute paths are relative to |areaStoragePrefix|. + // + storageTags : { + sys_msg_attach : 'msg_attach', + }, + areas: { - message_attachment : { - name : 'Message attachments', - desc : 'File attachments to messages', + systemm_message_attachment : { + name : 'Message attachments', + desc : 'File attachments to messages', + storageTags : 'sys_msg_attach', // may be string or array of strings } } }, eventScheduler : { - events : { trimMessageAreas : { // may optionally use [or ]@watch:/path/to/file diff --git a/core/database.js b/core/database.js index e5496ce4..24ef4941 100644 --- a/core/database.js +++ b/core/database.js @@ -262,6 +262,7 @@ const DB_INIT_TABLE = { area_tag VARCHAR NOT NULL, file_sha1 VARCHAR NOT NULL, file_name, /* FTS @ file_fts */ + storage_tag VARCHAR NOT NULL, desc, /* FTS @ file_fts */ desc_long, /* FTS @ file_fts */ upload_timestamp DATETIME NOT NULL diff --git a/core/download_queue.js b/core/download_queue.js index 7554e885..e1ecc6f8 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -7,7 +7,9 @@ module.exports = class DownloadQueue { constructor(client) { this.client = client; - this.loadFromProperty(client); + if(!Array.isArray(this.client.user.downloadQueue)) { + this.loadFromProperty(client); + } } toggle(fileEntry) { diff --git a/core/file_area.js b/core/file_area.js index bd5cc336..ac0ae0d1 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -37,7 +37,8 @@ function getAvailableFileAreas(client, options) { options = options || { includeSystemInternal : false }; // perform ACS check per conf & omit system_internal if desired - return _.omit(Config.fileAreas.areas, (area, areaTag) => { + const areasWithTags = _.map(Config.fileBase.areas, (area, areaTag) => Object.assign(area, { areaTag : areaTag } ) ); + return _.omit(Config.fileBase.areas, (area, areaTag) => { if(!options.includeSystemInternal && WellKnownAreaTags.MessageAreaAttach === areaTag) { return true; } @@ -48,21 +49,21 @@ function getAvailableFileAreas(client, options) { function getSortedAvailableFileAreas(client, options) { const areas = _.map(getAvailableFileAreas(client, options), v => v); - sortAreasOrConfs(areas, 'area'); + sortAreasOrConfs(areas); return areas; } function getDefaultFileAreaTag(client, disableAcsCheck) { - let defaultArea = _.findKey(Config.fileAreas, o => o.default); + let defaultArea = _.findKey(Config.fileBase, o => o.default); if(defaultArea) { - const area = Config.fileAreas.areas[defaultArea]; + const area = Config.fileBase.areas[defaultArea]; if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) { return defaultArea; } } // just use anything we can - defaultArea = _.findKey(Config.fileAreas.areas, (area, areaTag) => { + defaultArea = _.findKey(Config.fileBase.areas, (area, areaTag) => { return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); }); @@ -70,10 +71,10 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { } function getFileAreaByTag(areaTag) { - const areaInfo = Config.fileAreas.areas[areaTag]; + const areaInfo = Config.fileBase.areas[areaTag]; if(areaInfo) { - areaInfo.areaTag = areaTag; // convienence! - areaInfo.storageDirectory = getAreaStorageDirectory(areaInfo); + areaInfo.areaTag = areaTag; // convienence! + areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } } @@ -113,8 +114,38 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) { ); } -function getAreaStorageDirectory(areaInfo) { - return paths.join(Config.fileBase.areaStoragePrefix, areaInfo.storageDir || ''); +function getAreaStorageDirectoryByTag(storageTag) { + const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); + + return paths.resolve(Config.fileBase.areaStoragePrefix, storageLocation || ''); + + /* + // absolute paths as-is + if(storageLocation && '/' === storageLocation.charAt(0)) { + return storageLocation; + } + + // relative to |areaStoragePrefix| + return paths.join(Config.fileBase.areaStoragePrefix, storageLocation || ''); + */ +} + +function getAreaStorageLocations(areaInfo) { + + const storageTags = Array.isArray(areaInfo.storageTags) ? + areaInfo.storageTags : + [ areaInfo.storageTags || '' ]; + + const avail = Config.fileBase.storageTags; + + return _.compact(storageTags.map(storageTag => { + if(avail[storageTag]) { + return { + storageTag : storageTag, + dir : getAreaStorageDirectoryByTag(storageTag), + }; + } + })); } function getFileEntryPath(fileEntry) { @@ -342,29 +373,28 @@ function updateFileEntry(fileEntry, filePath, cb) { } -function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { +function addOrUpdateFileEntry(areaInfo, storageLocation, fileName, options, cb) { const fileEntry = new FileEntry({ areaTag : areaInfo.areaTag, meta : options.meta, hashTags : options.hashTags, // Set() or Array fileName : fileName, + storageTag : storageLocation.storageTag, }); - const filePath = paths.join(getAreaStorageDirectory(areaInfo), fileName); + const filePath = paths.join(storageLocation.dir, fileName); async.waterfall( [ function processPhysicalFile(callback) { - const stream = fs.createReadStream(filePath); - let byteSize = 0; const sha1 = crypto.createHash('sha1'); const sha256 = crypto.createHash('sha256'); const md5 = crypto.createHash('md5'); const crc32 = new CRC32(); - // :TODO: crc32 + const stream = fs.createReadStream(filePath); stream.on('data', data => { byteSize += data.length; @@ -413,6 +443,58 @@ function addOrUpdateFileEntry(areaInfo, fileName, options, cb) { } function scanFileAreaForChanges(areaInfo, cb) { + const storageLocations = getAreaStorageLocations(areaInfo); + + async.eachSeries(storageLocations, (storageLoc, nextLocation) => { + async.series( + [ + function scanPhysFiles(callback) { + const physDir = storageLoc.dir; + + fs.readdir(physDir, (err, files) => { + if(err) { + return callback(err); + } + + async.eachSeries(files, (fileName, nextFile) => { + const fullPath = paths.join(physDir, fileName); + + fs.stat(fullPath, (err, stats) => { + if(err) { + // :TODO: Log me! + return nextFile(null); // always try next file + } + + if(!stats.isFile()) { + return nextFile(null); + } + + addOrUpdateFileEntry(areaInfo, storageLoc, fileName, { }, err => { + return nextFile(err); + }); + }); + }, err => { + return callback(err); + }); + }); + }, + function scanDbEntries(callback) { + // :TODO: Look @ db entries for area that were *not* processed above + return callback(null); + } + ], + err => { + return nextLocation(err); + } + ); + }, + err => { + return cb(err); + }); +} + +/* +function scanFileAreaForChanges2(areaInfo, cb) { const areaPhysDir = getAreaStorageDirectory(areaInfo); async.series( @@ -454,4 +536,5 @@ function scanFileAreaForChanges(areaInfo, cb) { return cb(err); } ); -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/core/file_area_web.js b/core/file_area_web.js index 3975390b..cc5227a2 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -28,7 +28,8 @@ const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server'; class FileAreaWebAccess { constructor() { - this.hashids = new hashids(Config.general.boardName); + this.hashids = new hashids(Config.general.boardName); + this.expireTimers = {}; // hashId->timer } startup(cb) { @@ -37,8 +38,7 @@ class FileAreaWebAccess { async.series( [ function initFromDb(callback) { - // :TODO: Init from DB & register expiration timers - return callback(null); + return self.load(callback); }, function addWebRoute(callback) { const webServer = getServer(WEB_SERVER_PACKAGE_NAME); @@ -66,7 +66,56 @@ class FileAreaWebAccess { } load(cb) { - return cb(null); // :TODO: Load from db + // + // Load entries, register expiration timers + // + FileDb.each( + `SELECT hash_id, expire_timestamp + FROM file_web_serve;`, + (err, row) => { + if(row) { + this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); + } + }, + err => { + return cb(err); + } + ); + } + + removeEntry(hashId) { + // + // Delete record from DB, and our timer + // + FileDb.run( + `DELETE FROM file_web_serve + WHERE hash_id = ?;`, + [ hashId ] + ); + + delete this.expireTime[hashId]; + } + + scheduleExpire(hashId, expireTime) { + + // remove any previous entry for this hashId + const previous = this.expireTimers[hashId]; + if(previous) { + clearTimeout(previous); + delete this.expireTimers[hashId]; + } + + const timeoutMs = expireTime.diff(moment()); + + if(timeoutMs <= 0) { + setImmediate( () => { + this.removeEntry(hashId); + }); + } else { + this.expireTimers[hashId] = setTimeout( () => { + this.removeEntry(hashId); + }, timeoutMs); + } } loadServedHashId(hashId, cb) { @@ -111,12 +160,9 @@ class FileAreaWebAccess { // // Create a URL such as // https://l33t.codes:44512/f/qFdxyZr - // - // :TODO: build from config - // // Prefer HTTPS over HTTP. Be explicit about the port - // only if required. + // only if non-standard. // let schema; let port; @@ -163,8 +209,8 @@ class FileAreaWebAccess { return cb(err); } - // :TODO: setup tracking of expiration time so we can clean up the entry - + this.scheduleExpire(hashId, options.expireTime); + return cb(null, url); } ); @@ -173,7 +219,7 @@ class FileAreaWebAccess { fileNotFound(resp) { resp.writeHead(404, { 'Content-Type' : 'text/html' } ); - // :TODO: allow custom 404 + // :TODO: allow custom 404 - mods//file_area_web-404.html return resp.end('Not found'); } diff --git a/core/file_entry.js b/core/file_entry.js index 17d2995f..f4e65b51 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -12,7 +12,7 @@ const _ = require('lodash'); const paths = require('path'); const FILE_TABLE_MEMBERS = [ - 'file_id', 'area_tag', 'file_sha1', 'file_name', + 'file_id', 'area_tag', 'file_sha1', 'file_name', 'storage_tag', 'desc', 'desc_long', 'upload_timestamp' ]; @@ -44,6 +44,7 @@ module.exports = class FileEntry { this.hashTags = options.hashTags || new Set(); this.fileName = options.fileName; + this.storageTag = options.storageTag; } load(fileId, cb) { @@ -99,9 +100,9 @@ module.exports = class FileEntry { }, function storeEntry(callback) { fileDb.run( - `REPLACE INTO file (area_tag, file_sha1, file_name, desc, desc_long, upload_timestamp) - VALUES(?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.fileSha1, self.fileName, self.desc, self.descLong, getISOTimestampString() ], + `REPLACE INTO file (area_tag, file_sha1, file_name, storage_tag, desc, desc_long, upload_timestamp) + VALUES(?, ?, ?, ?, ?, ?, ?);`, + [ self.areaTag, self.fileSha1, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], function inserted(err) { // use non-arrow func for 'this' scope / lastID if(!err) { self.fileId = this.lastID; @@ -132,15 +133,21 @@ module.exports = class FileEntry { ); } - get filePath() { - const areaInfo = Config.fileAreas.areas[this.areaTag]; - if(areaInfo) { - return paths.join( - Config.fileBase.areaStoragePrefix, - areaInfo.storageDir || '', - this.fileName - ); + static getAreaStorageDirectoryByTag(storageTag) { + const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); + + // absolute paths as-is + if(storageLocation && '/' === storageLocation.charAt(0)) { + return storageLocation; } + + // relative to |areaStoragePrefix| + return paths.join(Config.fileBase.areaStoragePrefix, storageLocation || ''); + } + + get filePath() { + const storageDir = FileEntry.getAreaStorageDirectoryByTag(this.storageTag); + return paths.join(storageDir, this.fileName); } static persistMetaValue(fileId, name, value, cb) { @@ -193,7 +200,7 @@ module.exports = class FileEntry { static getWellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); } - static findFiles(criteria, cb) { + static findFiles(filter, cb) { // :TODO: build search here - return [ fileid1, fileid2, ... ] // free form // areaTag @@ -201,6 +208,8 @@ module.exports = class FileEntry { // order by // sort + filter = filter || {}; + let sql = `SELECT file_id FROM file`; @@ -216,21 +225,23 @@ module.exports = class FileEntry { sqlWhere += clause; } - if(criteria.areaTag) { - appendWhereClause(`area_tag="${criteria.areaTag}"`); + if(filter.areaTag) { + appendWhereClause(`area_tag="${filter.areaTag}"`); } - if(criteria.search) { + if(filter.terms) { appendWhereClause( `file_id IN ( SELECT rowid FROM file_fts - WHERE file_fts MATCH "${criteria.search.replace(/"/g,'""')}" + WHERE file_fts MATCH "${filter.terms.replace(/"/g,'""')}" )` ); } - if(Array.isArray(criteria.hashTags)) { + if(filter.tags) { + const tags = filter.tags.split(' '); // filter stores as sep separated values + appendWhereClause( `file_id IN ( SELECT file_id @@ -238,14 +249,14 @@ module.exports = class FileEntry { WHERE hash_tag_id IN ( SELECT hash_tag_id FROM hash_tag - WHERE hash_tag IN (${criteria.hashTags.join(',')}) + WHERE hash_tag IN (${tags.join(',')}) ) )` ); } - // :TODO: criteria.orderBy - // :TODO: criteria.sort + // :TODO: filter.orderBy + // :TODO: filter.sort sql += sqlWhere + ';'; const matchingFileIds = []; diff --git a/core/sauce.js b/core/sauce.js index 0dad1bc9..295a6069 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -50,20 +50,17 @@ function readSAUCE(data, cb) { .tap(function onVars(vars) { if(!SAUCE_ID.equals(vars.id)) { - cb(new Error('No SAUCE record present')); - return; + return cb(new Error('No SAUCE record present')); } var ver = iconv.decode(vars.version, 'cp437'); if('00' !== ver) { - cb(new Error('Unsupported SAUCE version: ' + ver)); - return; + return cb(new Error('Unsupported SAUCE version: ' + ver)); } if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) { - cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType)); - return; + return cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType)); } var sauce = { diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index d9b9e13b..8cd6e1a0 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1098,6 +1098,8 @@ function FTNMessageScanTossModule() { return nextFile(); // unknown archive type } + + Log.debug( { bundleFile : bundleFile }, 'Processing bundle' ); self.archUtil.extractTo( bundleFile.path, diff --git a/core/string_util.js b/core/string_util.js index 0ba8dec9..edb0e613 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -2,8 +2,12 @@ 'use strict'; // ENiGMA½ -const miscUtil = require('./misc_util.js'); -const iconv = require('iconv-lite'); +const miscUtil = require('./misc_util.js'); +const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; +const ANSI = require('./ansi_term.js'); + +// deps +const iconv = require('iconv-lite'); exports.stylizeString = stylizeString; exports.pad = pad; @@ -16,6 +20,7 @@ exports.renderStringLength = renderStringLength; exports.formatByteSizeAbbr = formatByteSizeAbbr; exports.formatByteSize = formatByteSize; exports.cleanControlCodes = cleanControlCodes; +exports.createCleanAnsi = createCleanAnsi; // :TODO: create Unicode verison of this const VOWELS = [ 'a', 'e', 'i', 'o', 'u' ]; @@ -310,15 +315,23 @@ function formatByteSize(byteSize, withAbbr, decimals) { //const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g; const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([\?=;0-9]*?)([A-ORZcf-npsu=><])/g; const ANSI_OPCODES_ALLOWED_CLEAN = [ - 'C', 'm' , - 'A', 'B', 'D' + 'A', 'B', // up, down + 'C', 'D', // right, left + 'm', // color ]; -function cleanControlCodes(input) { +const AnsiSpecialOpCodes = { + positioning : [ 'A', 'B', 'C', 'D' ], // up, down, right, left + style : [ 'm' ] // color +}; + +function cleanControlCodes(input, options) { let m; let pos; let cleaned = ''; + options = options || {}; + // // Loop through |input| adding only allowed ESC // sequences and literals to |cleaned| @@ -332,6 +345,10 @@ function cleanControlCodes(input) { cleaned += input.slice(pos, m.index); } + if(options.all) { + continue; + } + if(ANSI_OPCODES_ALLOWED_CLEAN.indexOf(m[2].charAt(0)) > -1) { cleaned += m[0]; } @@ -347,61 +364,141 @@ function cleanControlCodes(input) { return cleaned; } -function getCleanAnsi(input) { - // - // Process |input| and produce |cleaned|, an array - // of lines with "clean" ANSI. - // - // Clean ANSI: - // * Contains only color/SGR sequences - // * All movement (up/down/left/right) removed but positioning - // left intact via spaces/etc. - // - // Temporary processing will occur in a grid. Each cell - // containing a character (defaulting to space) possibly a SGR - // - - let m; - let pos; - let grid = []; - let gridPos = { row : 0, col : 0 }; - - function updateGrid(data, dataType) { - // - // Start at to grid[row][col] and populate val[0]...val[N] - // creating cells as necessary - // - if(!grid[gridPos.row]) { - grid[gridPos.row] = []; - } - - if('literal' === dataType) { - data.forEach(c => { - grid[gridPos.row][gridPos.col] = (grid[gridPos.row][gridPos.col] || '') + c; // append to existing SGR - gridPos.col++; - }); - } else if('sgr' === dataType) { - grid[gridPos.row][gridPos.col] = (grid[gridPos.row][gridPos.col] || '') + data; - } - } - - function literal(s) { - let charCode; - const len = s.length; - for(let i = 0; i < len; ++i) { - charCode = s.charCodeAt(i) & 0xff; +function createCleanAnsi(input, options, cb) { + options.width = options.width || 80; + options.height = options.height || 25; + + const canvas = new Array(options.height); + for(let i = 0; i < options.height; ++i) { + canvas[i] = new Array(options.width); + for(let j = 0; j < options.width; ++j) { + canvas[i][j] = {}; } } - - do { - pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; - m = REGEXP_ANSI_CONTROL_CODES.exec(input); - - if(null !== m) { - if(m.index > pos) { - updateGrid(input.slice(pos, m.index), 'literal'); + + const parserOpts = { + termHeight : options.height, + termWidth : options.width, + }; + + const parser = new ANSIEscapeParser(parserOpts); + + const canvasPos = { + col : 0, + row : 0, + }; + + let sgr; + + function ensureCell() { + // we've pre-allocated a matrix, but allow for > supplied dimens up front. They will be trimmed @ finalize + if(!canvas[canvasPos.row]) { + canvas[canvasPos.row] = new Array(options.width); + for(let j = 0; j < options.width; ++j) { + canvas[canvasPos.row][j] = {}; } } - } while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex); + canvas[canvasPos.row][canvasPos.col] = canvas[canvasPos.row][canvasPos.col] || {}; + //canvas[canvasPos.row][0].width = Math.max(canvas[canvasPos.row][0].width || 0, canvasPos.col); + } + + parser.on('literal', literal => { + // + // CR/LF are handled for 'position update'; we don't need the chars themselves + // + literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); + + for(let i = 0; i < literal.length; ++i) { + const c = literal.charAt(i); + + ensureCell(); + + canvas[canvasPos.row][canvasPos.col].char = c; + + if(sgr) { + canvas[canvasPos.row][canvasPos.col].sgr = sgr; + sgr = null; + } + + canvasPos.col += 1; + } + }); + + parser.on('control', (match, opCode) => { + if('m' !== opCode) { + return; // don't care' + } + sgr = match; + }); + + parser.on('position update', (row, col) => { + canvasPos.row = row - 1; + canvasPos.col = Math.min(col - 1, options.width); + }); + + parser.on('complete', () => { + for(let row = 0; row < options.height; ++row) { + let col = 0; + + //while(col <= canvas[row][0].width) { + while(col < options.width) { + if(!canvas[row][col].char) { + canvas[row][col].char = 'P'; + if(!canvas[row][col].sgr) { + // :TODO: fix duplicate SGR's in a row here - we just need one per sequence + canvas[row][col].sgr = ANSI.reset(); + } + } + + col += 1; + } + + // :TODO: end *all* with CRLF - general usage will be width : 79 - prob update defaults + + if(col <= options.width) { + canvas[row][col] = canvas[row][col] || {}; + + //canvas[row][col].char = '\r\n'; + canvas[row][col].sgr = ANSI.reset(); + + // :TODO: don't splice, just reset + fill with ' ' till end + for(let fillCol = col; fillCol <= options.width; ++fillCol) { + canvas[row][fillCol].char = 'X'; + } + + //canvas[row] = canvas[row].splice(0, col + 1); + //canvas[row][options.width - 1].char = '\r\n'; + + + } else { + canvas[row] = canvas[row].splice(0, options.width + 1); + } + + } + + let out = ''; + for(let row = 0; row < options.height; ++row) { + out += canvas[row].map( col => { + let c = col.sgr || ''; + c += col.char; + return c; + }).join(''); + + } + + // :TODO: finalize: @ any non-char cell, reset sgr & set to ' ' + // :TODO: finalize: after sgr established, omit anything > supplied dimens + return cb(out); + }); + + parser.parse(input); } + +const fs = require('fs'); +let data = fs.readFileSync('/home/nuskooler/Downloads/art3.ans'); +data = iconv.decode(data, 'cp437'); +createCleanAnsi(data, { width : 79, height : 25 }, (out) => { + out = iconv.encode(out, 'cp437'); + fs.writeFileSync('/home/nuskooler/Downloads/art4.ans', out); +}); diff --git a/core/user.js b/core/user.js index 0288d8dd..e7b14740 100644 --- a/core/user.js +++ b/core/user.js @@ -325,6 +325,22 @@ User.prototype.persistProperty = function(propName, propValue, cb) { ); }; +User.prototype.removeProperty = function(propName, cb) { + // update live + delete this.properties[propName]; + + userDb.run( + `DELETE FROM user_property + WHERE user_id = ? AND prop_name = ?;`, + [ this.userId, propName ], + err => { + if(cb) { + return cb(err); + } + } + ) +}; + User.prototype.persistProperties = function(properties, cb) { var self = this; diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index 7dfc3143..80d87fa3 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -30,6 +30,7 @@ const MciViewIds = { selectedFilterInfo : 10, // { ...filter object ... } activeFilterInfo : 11, // { ...filter object ... } + error : 12, // validation errors } }; @@ -67,7 +68,6 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { this.updateActiveLabel(); - // :TODO: Need to update %FN somehow return cb(null); }, newFilter : (formData, extraArgs, cb) => { @@ -92,9 +92,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { if(newActive) { filters.setActive(newActive.uuid); } else { - // nothing to set active to - // :TODO: is this what we want? - this.client.user.properties.file_base_filter_active_uuid = 'none'; + // nothing to set active to + this.client.user.removeProperty('file_base_filter_active_uuid'); } } @@ -106,7 +105,23 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } 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); + }, }; } diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 193021da..219e4d20 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -8,12 +8,16 @@ 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_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'); @@ -328,15 +332,27 @@ exports.getModule = class FileAreaList extends MenuModule { function populateViews(callback) { if(_.isString(self.currentFileEntry.desc)) { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); - if(descView) { - descView.setText(self.currentFileEntry.desc); + if(descView) { + createCleanAnsi( + self.currentFileEntry.desc, + { height : self.client.termHeight, width : descView.dimens.width }, + cleanDesc => { + descView.setText(cleanDesc); + + self.updateQueueIndicator(); + self.populateCustomLabels('browse', 10); + + return callback(null); + } + ); + descView.setText( self.currentFileEntry.desc ); } + } else { + self.updateQueueIndicator(); + self.populateCustomLabels('browse', 10); + + return callback(null); } - - self.updateQueueIndicator(); - self.populateCustomLabels('browse', 10); - - return callback(null); } ], err => { @@ -442,20 +458,6 @@ exports.getModule = class FileAreaList extends MenuModule { ); this.updateCustomLabelsWithFilter( 'browse', 10, [ '{isQueued}' ] ); - /* - const indicatorView = this.viewControllers.browse.getView(MciViewIds.browse.queueToggle); - - if(indicatorView) { - const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; - - indicatorView.setText(stringFormat( - this.dlQueue.isQueued(this.currentFileEntry) ? - isQueuedIndicator : - isNotQueuedIndicator - ) - ); - }*/ } cacheArchiveEntries(cb) { @@ -469,7 +471,7 @@ exports.getModule = class FileAreaList extends MenuModule { return cb(Errors.Invalid('Invalid area tag')); } - const filePath = paths.join(areaInfo.storageDirectory, this.currentFileEntry.fileName); + const filePath = this.currentFileEntry.filePath; const archiveUtil = ArchiveUtil.getInstance(); archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { @@ -574,9 +576,10 @@ exports.getModule = class FileAreaList extends MenuModule { } loadFileIds(cb) { - this.fileListPosition = 0; + this.fileListPosition = 0; + const activeFilter = FileBaseFilters.getActiveFilter(this.client); - FileEntry.findFiles(this.filterCriteria, (err, fileIds) => { + FileEntry.findFiles(activeFilter, (err, fileIds) => { this.fileList = fileIds; return cb(err); }); diff --git a/oputil.js b/oputil.js index c78e7683..b00d6428 100755 --- a/oputil.js +++ b/oputil.js @@ -435,7 +435,7 @@ function handleConfigCommand() { } } -function fileAreaScan(areaTag) { +function fileAreaScan() { async.waterfall( [ function init(callback) { @@ -453,7 +453,7 @@ function fileAreaScan(areaTag) { }, function performScan(fileAreaMod, areaInfo, callback) { fileAreaMod.scanFileAreaForChanges(areaInfo, err => { - + return callback(err); }); } ], From 0a92eec5e8350d278bb364722ceb3a128abf189d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 31 Dec 2016 14:50:29 -0700 Subject: [PATCH 18/86] * WIP on upload support - protocols, modules, etc. * Ability for KeyEntryView to only show specific/allowed keys * Start moving/adding common methods to MenuModule vs boilerplate code * menuFlags: String|StringArray: flags for menus, e.g. new 'noHistory' flag to prevent appending to history/stack * New download stats/MCI codes * Ability to redirect input stream to [protocols] temporairly --- core/acs.js | 5 + core/art.js | 4 +- core/config.js | 12 +- core/door_party.js | 2 +- core/download_queue.js | 19 +- core/file_area.js | 6 +- core/file_area_web.js | 2 +- core/file_entry.js | 15 + core/key_entry_view.js | 28 +- core/menu_module.js | 71 +++- core/menu_stack.js | 27 +- core/menu_util.js | 6 + core/predefined_mci.js | 14 + core/servers/login/telnet.js | 61 +++- core/stat_log.js | 4 + core/transfer_file.js | 460 +++++++++++++++++++++++--- main.js | 1 + mods/file_area_filter_edit.js | 1 - mods/file_area_list.js | 2 +- mods/file_base_download_manager.js | 195 +++++++++++ mods/file_transfer_protocol_select.js | 126 +++++++ 21 files changed, 985 insertions(+), 76 deletions(-) create mode 100644 mods/file_base_download_manager.js create mode 100644 mods/file_transfer_protocol_select.js diff --git a/core/acs.js b/core/acs.js index 484407da..f2e04b9f 100644 --- a/core/acs.js +++ b/core/acs.js @@ -43,6 +43,10 @@ class ACS { return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); } + hasFileAreaWrite(area) { + return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite); + } + hasFileAreaDownload(area) { return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload); } @@ -75,6 +79,7 @@ ACS.Defaults = { MessageConfRead : 'GM[users]', FileAreaRead : 'GM[users]', + FileAreaWrite : 'GM[sysops]', FileAreaDownload : 'GM[users]', }; diff --git a/core/art.js b/core/art.js index 4b870cde..d33c81da 100644 --- a/core/art.js +++ b/core/art.js @@ -266,7 +266,7 @@ function display(client, art, options, cb) { if(!options.disableMciCache && !mciMapFromCache) { // cache our MCI findings... client.mciCache[artHash] = mciMap; - client.log.trace( { artHash : artHash, mciMap : mciMap }, 'Added MCI map to cache'); + client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache'); } ansiParser.removeAllListeners(); // :TODO: Necessary??? @@ -290,7 +290,7 @@ function display(client, art, options, cb) { if(mciMap) { mciMapFromCache = true; - client.log.trace( { artHash : artHash, mciMap : mciMap }, 'Loaded MCI map from cache'); + client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache'); } else { // no cached MCI info mciMap = {}; diff --git a/core/config.js b/core/config.js index 975e1d55..0a7a2217 100644 --- a/core/config.js +++ b/core/config.js @@ -289,22 +289,28 @@ function getDefaultConfig() { external : { sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" sendArgs : [ - '--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePath}' + '--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePaths}' ], escapeTelnet : true, // set to true to escape Telnet codes such as IAC + supportsBatch : true, } }, zmodem8kSexyz : { - name : 'ZModem (SEXYZ)', + name : 'ZModem 8k (SEXYZ)', type : 'external', external : { // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems sendCmd : 'sexyz', sendArgs : [ - '-telnet', 'sz', '{filePath}' + '-telnet', '-8', 'sz', '@{fileListPath}' + ], + recvCmd : 'sexyz', + recvArgs : [ + '-telnet', '-8', 'rz', '{uploadDir}' ], escapeTelnet : false, // -telnet option does this for us + supportsBatch : true, } } diff --git a/core/door_party.js b/core/door_party.js index 9eb733f4..d782c07e 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -116,7 +116,7 @@ function DoorPartyModule(options) { ], err => { if(err) { - self.client.log.warn( { error : err.toString() }, 'DoorParty error'); + self.client.log.warn( { error : err.message }, 'DoorParty error'); } // if the client is stil here, go to previous diff --git a/core/download_queue.js b/core/download_queue.js index e1ecc6f8..7254b9c8 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -12,6 +12,14 @@ module.exports = class DownloadQueue { } } + get items() { + return this.client.user.downloadQueue; + } + + clear() { + this.client.user.downloadQueue = []; + } + toggle(fileEntry) { if(this.isQueued(fileEntry)) { this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); @@ -25,10 +33,19 @@ module.exports = class DownloadQueue { fileId : fileEntry.fileId, areaTag : fileEntry.areaTag, fileName : fileEntry.fileName, - byteSize : fileEntry.meta.byteSize || 0, + path : fileEntry.filePath, + byteSize : fileEntry.meta.byte_size || 0, }); } + removeItems(fileIds) { + if(!Array.isArray(fileIds)) { + fileIds = [ fileIds ]; + } + + this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => ( -1 === fileIds.indexOf(e.fileId) ) ); + } + isQueued(entryOrId) { if(entryOrId instanceof FileEntry) { entryOrId = entryOrId.fileId; diff --git a/core/file_area.js b/core/file_area.js index ac0ae0d1..4903f571 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -34,7 +34,7 @@ const WellKnownAreaTags = exports.WellKnownAreaTags = { }; function getAvailableFileAreas(client, options) { - options = options || { includeSystemInternal : false }; + options = options || { }; // perform ACS check per conf & omit system_internal if desired const areasWithTags = _.map(Config.fileBase.areas, (area, areaTag) => Object.assign(area, { areaTag : areaTag } ) ); @@ -43,6 +43,10 @@ function getAvailableFileAreas(client, options) { return true; } + if(options.writeAcs && !client.acs.FileAreaWrite(area)) { + return true; // omit + } + return !client.acs.hasFileAreaRead(area); }); } diff --git a/core/file_area_web.js b/core/file_area_web.js index cc5227a2..841ce904 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -93,7 +93,7 @@ class FileAreaWebAccess { [ hashId ] ); - delete this.expireTime[hashId]; + delete this.expireTimers[hashId]; } scheduleExpire(hashId, expireTime) { diff --git a/core/file_entry.js b/core/file_entry.js index f4e65b51..f816a62b 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -159,6 +159,21 @@ module.exports = class FileEntry { ); } + static incrementAndPersistMetaValue(fileId, name, incrementBy, cb) { + incrementBy = incrementBy || 1; + fileDb.run( + `UPDATE file_meta + SET meta_value = meta_value + ? + WHERE file_id = ? AND meta_name = ?;`, + [ incrementBy, fileId, name ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + loadMeta(cb) { fileDb.each( `SELECT meta_name, meta_value diff --git a/core/key_entry_view.js b/core/key_entry_view.js index fa48e877..cf1ba008 100644 --- a/core/key_entry_view.js +++ b/core/key_entry_view.js @@ -15,22 +15,30 @@ module.exports = class KeyEntryView extends View { super(options); - this.eatTabKey = options.eatTabKey || true; - this.caseInsensitive = options.caseInsensitive || true; + this.eatTabKey = options.eatTabKey || true; + this.caseInsensitive = options.caseInsensitive || true; - // :TODO: allow (by default) only supplied keys[] to even draw + if(Array.isArray(options.keys)) { + if(this.caseInsensitive) { + this.keys = options.keys.map( k => k.toUpperCase() ); + } else { + this.keys = options.keys; + } + } } onKeyPress(ch, key) { - if(ch && isPrintable(ch)) { - this.redraw(); // sets position - this.client.term.write(stylizeString(ch, this.textStyle)); - } + const drawKey = ch; if(ch && this.caseInsensitive) { ch = ch.toUpperCase(); } + if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { + this.redraw(); // sets position + this.client.term.write(stylizeString(ch, this.textStyle)); + } + this.keyEntered = ch || key.name; if(key && 'tab' === key.name && !this.eatTabKey) { @@ -54,6 +62,12 @@ module.exports = class KeyEntryView extends View { this.caseInsensitive = propValue; } break; + + case 'keys' : + if(Array.isArray(propValue)) { + this.keys = propValue; + } + break; } super.setPropertyValue(propName, propValue); diff --git a/core/menu_module.js b/core/menu_module.js index 5477e6d3..d740c478 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -3,14 +3,12 @@ var PluginModule = require('./plugin_module.js').PluginModule; var theme = require('./theme.js'); -var art = require('./art.js'); -var Log = require('./logger.js').log; var ansi = require('./ansi_term.js'); -var asset = require('./asset.js'); var ViewController = require('./view_controller.js').ViewController; var menuUtil = require('./menu_util.js'); var Config = require('./config.js').config; +// deps var async = require('async'); var assert = require('assert'); var _ = require('lodash'); @@ -236,6 +234,11 @@ MenuModule.prototype.gotoMenu = function(name, options, cb) { this.client.menuStack.goto(name, options, cb); }; +MenuModule.prototype.popAndGotoMenu = function(name, options, cb) { + this.client.menuStack.pop(); + this.client.menuStack.goto(name, options, cb); +}; + MenuModule.prototype.leave = function() { this.detachViewControllers(); }; @@ -322,3 +325,65 @@ MenuModule.prototype.finishedLoading = function() { MenuModule.prototype.getMenuResult = function() { // nothing in base }; + +MenuModule.prototype.displayAsset = function(name, options, cb) { + + if(_.isFunction(options)) { + cb = options; + options = {}; + } + + if(options.clearScreen) { + this.client.term.rawWrite(ansi.clearScreen()); + } + + return theme.displayThemedAsset( + name, + this.client, + Object.merge( { font : this.menuConfig.config }, options ), + (err, artData) => { + if(cb) { + return cb(err, artData); + } + } + ); + +}; + +MenuModule.prototype.prepViewController = function(name, formId, artData, cb) { + + if(_.isUndefined(this.viewControllers[name])) { + const vcOpts = { + client : this.client, + formId : formId, + }; + + const vc = this.addViewController(name, new ViewController(vcOpts)); + + const loadOpts = { + callingMenu : this, + mciMap : artData.mciMap, + formId : formId, + }; + + return vc.loadFromMenuConfig(loadOpts, cb); + } + + this.viewControllers[name].setFocus(true); + return cb(null); +}; + + +MenuModule.prototype.prepViewControllerWithArt = function(name, formId, options, cb) { + this.displayAsset( + name, + options, + (err, artData) => { + if(err) { + return cb(err); + } + + return this.prepViewController(name, formId, artData, cb); + } + ); +}; \ No newline at end of file diff --git a/core/menu_stack.js b/core/menu_stack.js index 84e6a6e0..d1fc15bb 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -135,11 +135,21 @@ module.exports = class MenuStack { currentModuleInfo.instance.leave(); } - self.push({ - name : name, - instance : modInst, - extraArgs : loadOpts.extraArgs, - }); + const noHistory = modInst.menuConfig.options.menuFlags.indexOf('noHistory') > -1; + + const stackToLog = _.map(self.stack, stackEntry => stackEntry.name); + + if(!noHistory) { + self.push({ + name : name, + instance : modInst, + extraArgs : loadOpts.extraArgs, + }); + + stackToLog.push(name); + } else { + stackToLog.push(`${name} (noHistory)`); + } // restore previous state if requested if(options && options.savedState) { @@ -149,8 +159,11 @@ module.exports = class MenuStack { modInst.enter(); self.client.log.trace( - { stack : _.map(self.stack, stackEntry => stackEntry.name) }, - 'Updated menu stack'); + { + stack : stackToLog + }, + 'Updated menu stack' + ); if(cb) { cb(null); diff --git a/core/menu_util.js b/core/menu_util.js index bc439a9b..f62dbd6c 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -64,6 +64,12 @@ function loadMenu(options, cb) { }, function loadMenuModule(menuConfig, callback) { + menuConfig.options = menuConfig.options || {}; + menuConfig.options.menuFlags = menuConfig.options.menuFlags || []; + if(!Array.isArray(menuConfig.options.menuFlags)) { + menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; + } + const modAsset = asset.getModuleAsset(menuConfig.module); const modSupplied = null !== modAsset; diff --git a/core/predefined_mci.js b/core/predefined_mci.js index aadd824f..9ad300e7 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -9,6 +9,7 @@ const getMessageConferenceByTag = require('./message_area.js').getMessageConfe const clientConnections = require('./client_connections.js'); const StatLog = require('./stat_log.js'); const FileBaseFilters = require('./file_base_filter.js'); +const formatByteSize = require('./string_util.js').formatByteSize; // deps const packageJson = require('../package.json'); @@ -85,6 +86,13 @@ function getPredefinedMCIValue(client, code) { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : ''; }, + DN : function userNumDownloads() { return StatLog.getUserStat(client.user, 'dl_total_count'); }, // Obv/2 + DK : function userByteDownload() { // Obv/2 + const byteSize = parseInt(StatLog.getUserStat(client.user, 'dl_total_bytes')) || 0; + return formatByteSize(byteSize, true); + }, + // :TODO: Up/down ratio (count) + // :TODO: Up/down ratio (bytes) MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, CS : function currentStatus() { return client.currentStatus; }, @@ -168,6 +176,12 @@ function getPredefinedMCIValue(client, code) { return StatLog.getSystemStat('random_rumor'); }, + // + // System File Base, Up/Download Info + // + // :TODO: DD - Today's # of downloads (iNiQUiTY) + // + // // Special handling for XY // diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 406e28a5..f23a1dc7 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -438,6 +438,65 @@ function TelnetClient(input, output) { newEnvironRequested : false, }; + this.setTemporaryDataHandler = function(handler) { + this.input.removeAllListeners(); + this.input.on('data', handler); + }; + + this.restoreDataHandler = function() { + this.input.removeAllListeners(); + this.input.on('data', this.dataHandler); + }; + + this.dataHandler = function(b) { + bufs.push(b); + + let i; + while((i = bufs.indexOf(IAC_BUF)) >= 0) { + + // + // Some clients will send even IAC separate from data + // + if(bufs.length <= (i + 1)) { + i = MORE_DATA_REQUIRED; + break; + } + + assert(bufs.length > (i + 1)); + + if(i > 0) { + self.emit('data', bufs.splice(0, i).toBuffer()); + } + + i = parseBufs(bufs); + + if(MORE_DATA_REQUIRED === i) { + break; + } else { + if(i.option) { + self.emit(i.option, i); // "transmit binary", "echo", ... + } + + self.handleTelnetEvent(i); + + if(i.data) { + self.emit('data', i.data); + } + } + } + + if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { + // + // Standard data payload. This can still be "non-user" data + // such as ANSI control, but we don't handle that here. + // + self.emit('data', bufs.splice(0).toBuffer()); + } + }; + + this.input.on('data', this.dataHandler); + + /* this.input.on('data', b => { bufs.push(b); @@ -482,8 +541,8 @@ function TelnetClient(input, output) { // self.emit('data', bufs.splice(0).toBuffer()); } - }); + */ this.input.on('end', () => { self.emit('end'); diff --git a/core/stat_log.js b/core/stat_log.js index 60e9e77f..4d53581a 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -118,6 +118,10 @@ class StatLog { return user.persistProperty(statName, statValue, cb); } + getUserStat(user, statName) { + return user.properties[statName]; + } + incrementUserStat(user, statName, incrementBy, cb) { incrementBy = incrementBy || 1; diff --git a/core/transfer_file.js b/core/transfer_file.js index 4faf5aa3..bed1a613 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -2,14 +2,22 @@ 'use strict'; // enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').config; -const stringFormat = require('./string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').config; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; +const DownloadQueue = require('./download_queue.js'); +const StatLog = require('./stat_log.js'); +const FileEntry = require('./file_entry.js'); // deps const async = require('async'); const _ = require('lodash'); const pty = require('ptyw.js'); +const temp = require('temp').track(); // track() cleans up temp dir/files for us +const paths = require('path'); +const fs = require('fs'); +const fse = require('fs-extra'); /* Resources @@ -30,67 +38,281 @@ exports.getModule = class TransferFileModule extends MenuModule { super(options); this.config = this.menuConfig.config || {}; - this.config.protocol = this.config.protocol || 'zmodem8kSz'; - this.config.direction = this.config.direction || 'send'; - this.protocolConfig = Config.fileTransferProtocols[this.config.protocol]; + // + // Most options can be set via extraArgs or config block + // + if(options.extraArgs) { + if(options.extraArgs.protocol) { + this.protocolConfig = Config.fileTransferProtocols[options.extraArgs.protocol]; + } - // :TODO: bring in extraArgs for path(s) to send when sending; Allow to hard code in config (e.g. for info pack/static downloads) + if(options.extraArgs.direction) { + this.direction = options.extraArgs.direction; + } + + if(options.extraArgs.sendQueue) { + this.sendQueue = options.extraArgs.sendQueue; + } + + if(options.extraArgs.recvFileName) { + this.recvFileName = options.extraArgs.recvFiles; + } + + if(options.extraArgs.recvDirectory) { + this.recvDirectory = options.extraArgs.recvDirectory; + } + } else { + if(this.config.protocol) { + this.protocolConfig = Config.fileTransferProtocols[this.config.protocol]; + } + + if(this.config.direction) { + this.direction = this.config.direction; + } + + if(this.config.sendQueue) { + this.sendQueue = this.config.sendQueue; + } + + if(this.config.recvFileName) { + this.recvFileName = this.config.recvFileName; + } + + if(this.config.recvDirectory) { + this.recvDirectory = this.config.recvDirectory; + } + } + + this.protocolConfig = this.protocolConfig || Config.fileTransferProtocols.zmodem8kSz; // try for *something* + this.direction = this.direction || 'send'; + this.sendQueue = this.sendQueue || []; + + // Ensure sendQueue is an array of objects that contain at least a 'path' member + this.sendQueue = this.sendQueue.map(item => { + if(_.isString(item)) { + return { path : item }; + } else { + return item; + } + }); + + this.sentFileIds = []; + } + + get isSending() { + return 'send' === this.direction; } restorePipeAfterExternalProc(pipe) { if(!this.pipeRestored) { this.pipeRestored = true; + + this.client.restoreDataHandler(); - this.client.term.output.unpipe(pipe); - this.client.term.output.resume(); + //this.client.term.output.unpipe(pipe); + //this.client.term.output.resume(); } } sendFiles(cb) { - async.eachSeries(this.sendQueue, (filePath, next) => { - // :TODO: built in protocols - // :TODO: use protocol passed in - this.executeExternalProtocolHandler(filePath, err => { - return next(err); + // :TODO: built in/native protocol support + + if(this.protocolConfig.external.supportsBatch) { + const allFiles = this.sendQueue.map(f => f.path); + this.executeExternalProtocolHandlerForSend(allFiles, err => { + if(err) { + this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); + } else { + const sentFiles = []; + this.sendQueue.forEach(f => { + f.sent = true; + sentFiles.push(f.path); + + }); + + this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); + } + return cb(err); }); - }, err => { - return cb(err); + } else { + // :TODO: we need to prompt between entries such that users can prepare their clients + async.eachSeries(this.sendQueue, (queueItem, next) => { + this.executeExternalProtocolHandlerForSend(queueItem.path, err => { + if(err) { + this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' ); + } else { + queueItem.sent = true; + + this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' ); + } + return next(err); + }); + }, err => { + return cb(err); + }); + } + } + + recvFiles(cb) { + this.executeExternalProtocolHandlerForRecv( (err, tempWorkingDir) => { + if(err) { + return cb(err); + } + + this.receivedFiles = []; + + if(this.recvFileName) { + // file name specified - we expect a single file in |tempWorkingDir| + return cb(null); + } else { + // + // blind recv (upload) - files in |tempWorkingDir| should be named appropriately already + // move files to |this.recvDirectory| + // + fs.readdir(tempWorkingDir, (err, files) => { + if(err) { + return cb(err); + } + + async.each(files, (file, nextFile) => { + fse.move( + paths.join(tempWorkingDir, file), + paths.join(this.recvDirectory, file), + err => { + if(err) { + // :TODO: IMPORTANT: Handle collisions - rename to FILE(1).EXT, etc. + this.client.log.warn( + { tempWorkingDir : tempWorkingDir, recvDirectory : this.recvDirectory, file : file, error : err.message }, + 'Failed to move upload file to destination directory' + ); + } else { + this.receivedFiles.push(file); + } + return nextFile(null); // don't pass along err; try next + } + ); + }, () => { + return cb(null); + }); + + }); + } }); } - executeExternalProtocolHandler(filePath, cb) { - const external = this.protocolConfig.external; - const cmd = external[`${this.config.direction}Cmd`]; - const args = external[`${this.config.direction}Args`].map(arg => { - return stringFormat(arg, { - filePath : filePath, - }); - }); + pathWithTerminatingSeparator(path) { + if(path && paths.sep !== path.charAt(path.length - 1)) { + path = path + paths.sep; + } + return path; + } - /*this.client.term.rawWrite(new Buffer( - [ - 255, 253, 0, // IAC DO TRANSMIT_BINARY - 255, 251, 0, // IAC WILL TRANSMIT_BINARY - ] - ));*/ + prepAndBuildSendArgs(filePaths, cb) { + const external = this.protocolConfig.external; + const externalArgs = external[`${this.direction}Args`]; + const self = this; + let tempWorkingDir; + + async.waterfall( + [ + function getTempFileListPath(callback) { + const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); + if(!hasFileList) { + temp.mkdir('enigdl-', (err, tempDir) => { + if(err) { + return callback(err); + } + + tempWorkingDir = self.pathWithTerminatingSeparator(tempDir); + return callback(null, null); + }); + } else { + temp.open( { prefix : 'enigdl-', suffix : '.txt' }, (err, tempFileInfo) => { + if(err) { + return callback(err); // failed to create it + } + + tempWorkingDir = self.pathWithTerminatingSeparator(paths.dirname(tempFileInfo.path)); + + fs.write(tempFileInfo.fd, filePaths.join('\n')); + fs.close(tempFileInfo.fd, err => { + return callback(err, tempFileInfo.path); + }); + }); + } + }, + function createArgs(tempFileListPath, callback) { + // initial args: ignore {filePaths} as we must break that into it's own sep array items + const args = externalArgs.map(arg => { + return '{filePaths}' === arg ? arg : stringFormat(arg, { + fileListPath : tempFileListPath || '', + }); + }); + + const filePathsPos = args.indexOf('{filePaths}'); + if(filePathsPos > -1) { + // replace {filePaths} with 0:n individual entries in |args| + args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); + } + + return callback(null, args); + } + ], + (err, args) => { + return cb(err, args, tempWorkingDir); + } + ); + } + + prepAndBuildRecvArgs(cb) { + const self = this; + + async.waterfall( + [ + function getTempRecvPath(callback) { + temp.mkdir('enigrcv-', (err, tempWorkingDir) => { + tempWorkingDir = self.pathWithTerminatingSeparator(tempWorkingDir); + return callback(err, tempWorkingDir); + }); + }, + function createArgs(tempWorkingDir, callback) { + const externalArgs = self.protocolConfig.external[`${self.direction}Args`]; + const args = externalArgs.map(arg => stringFormat(arg, { + uploadDir : tempWorkingDir, + fileName : self.recvFileName || '', + })); + + return callback(null, args, tempWorkingDir); + } + ], + (err, args, tempWorkingDir) => { + return cb(err, args, tempWorkingDir); + } + ); + } + + executeExternalProtocolHandler(args, tempWorkingDir, cb) { + const external = this.protocolConfig.external; + const cmd = external[`${this.direction}Cmd`]; + + this.client.log.debug( + { cmd : cmd, args : args, tempDir : tempWorkingDir, direction : this.direction }, + 'Executing external protocol' + ); const externalProc = pty.spawn(cmd, args, { - cols : this.client.term.termWidth, - rows : this.client.term.termHeight, - // :TODO: cwd - // :TODO: anything else?? - //env : self.exeInfo.env, + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + cwd : tempWorkingDir, }); - this.client.term.output.pipe(externalProc); - - /*this.client.term.output.on('data', data => { - // let tmp = data.toString('binary').replace(/\xff\xff/g, '\xff'); - // proc.write(new Buffer(tmp, 'binary')); - proc.write(data); + this.client.setTemporaryDataHandler(data => { + externalProc.write(data); }); - */ + + //this.client.term.output.pipe(externalProc); + externalProc.on('data', data => { // needed for things like sz/rz if(external.escapeTelnet) { @@ -105,7 +327,9 @@ exports.getModule = class TransferFileModule extends MenuModule { return this.restorePipeAfterExternalProc(externalProc); }); - externalProc.once('exit', exitCode => { + externalProc.once('exit', (exitCode) => { + this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); + this.restorePipeAfterExternalProc(externalProc); externalProc.removeAllListeners(); @@ -113,20 +337,162 @@ exports.getModule = class TransferFileModule extends MenuModule { }); } + executeExternalProtocolHandlerForSend(filePaths, cb) { + if(!Array.isArray(filePaths)) { + filePaths = [ filePaths ]; + } + + this.prepAndBuildSendArgs(filePaths, (err, args, tempWorkingDir) => { + if(err) { + return cb(err); + } + + this.executeExternalProtocolHandler(args, tempWorkingDir, err => { + return cb(err); + }); + }); + } + + executeExternalProtocolHandlerForRecv(cb) { + this.prepAndBuildRecvArgs( (err, args, tempWorkingDir) => { + if(err) { + return cb(err); + } + + this.executeExternalProtocolHandler(args, tempWorkingDir, err => { + return cb(err, tempWorkingDir); + }); + }); + } + + getMenuResult() { + return { sentFileIds : this.sentFileIds }; + } + + updateSendStats(cb) { + let downloadBytes = 0; + let downloadCount = 0; + let fileIds = []; + + async.each(this.sendQueue, (queueItem, next) => { + if(!queueItem.sent) { + return next(null); + } + + if(queueItem.fileId) { + fileIds.push(queueItem.fileId); + } + + downloadCount += 1; + + if(_.isNumber(queueItem.byteSize)) { + downloadBytes += queueItem.byteSize; + return next(null); + } + + // we just have a path - figure it out + fs.stat(queueItem.path, (err, stats) => { + if(err) { + this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); + } else { + downloadBytes += stats.size; + } + + return next(null); + }); + }, () => { + // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks + StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); + StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); + StatLog.incrementSystemStat('dl_total_count', downloadCount); + StatLog.incrementSystemStat('dl_total_bytes', downloadBytes); + + fileIds.forEach(fileId => { + FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); + }); + + return cb(null); + }); + } + + updateRecvStats(cb) { + // :TODO: update user & system upload stats + return cb(null); + } + initSequence() { const self = this; + // :TODO: break this up to send|recv + async.series( [ function validateConfig(callback) { - // :TODO: + if(self.isSending) { + if(!Array.isArray(self.sendQueue)) { + self.sendQueue = [ self.sendQueue ]; + } + } + return callback(null); }, function transferFiles(callback) { - self.sendQueue = [ '/home/nuskooler/Downloads/fdoor100.zip' ]; // :TODO: testing of course - return self.sendFiles(callback); + if(self.isSending) { + self.sendFiles( err => { + if(err) { + return callback(err); + } + + const sentFileIds = []; + self.sendQueue.forEach(queueItem => { + if(queueItem.sent && queueItem.fileId) { + sentFileIds.push(queueItem.fileId); + } + }); + + if(sentFileIds.length > 0) { + // remove items we sent from the D/L queue + const dlQueue = new DownloadQueue(self.client); + dlQueue.removeItems(sentFileIds); + + self.sentFileIds = sentFileIds; + } + + return callback(null); + }); + } else { + self.recvFiles( err => { + return callback(err); + }); + } + }, + function cleanupTempFiles(callback) { + temp.cleanup( err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Failed to clean up temporary file/directory(s)' ); + } + return callback(null); // ignore err + }); + }, + function updateUserAndSystemStats(callback) { + if(self.isSending) { + return self.updateSendStats(callback); + } else { + return self.updateRecvStats(callback); + } } - ] + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'File transfer error'); + } + + // Wait for a key press - attempt to avoid issues with some terminals after xfer + self.client.term.write('|00\nTransfer(s) complete. Press a key\n'); + self.client.waitForKeyPress( () => { + self.prevMenu(); + }); + } ); } }; diff --git a/main.js b/main.js index ef624294..ab8d2652 100755 --- a/main.js +++ b/main.js @@ -1,6 +1,7 @@ #!/usr/bin/env node /* jslint node: true */ + 'use strict'; /* diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index 80d87fa3..e7935f9a 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -10,7 +10,6 @@ const stringFormat = require('../core/string_format.js'); // deps const async = require('async'); -const _ = require('lodash'); exports.moduleInfo = { name : 'File Area Filter Editor', diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 219e4d20..58e72c35 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -496,7 +496,7 @@ exports.getModule = class FileAreaList extends MenuModule { } if('re-cached' === cacheStatus) { - const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; + 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) ) ); diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js new file mode 100644 index 00000000..017a3141 --- /dev/null +++ b/mods/file_base_download_manager.js @@ -0,0 +1,195 @@ +/* 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, + } + }; + + return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); + }, + viewItemInfo : (formData, extraArgs, cb) => { + }, + removeItem : (formData, extraArgs, cb) => { + const selectedItem = formData.value.queueItem; + this.dlQueue.removeItems(selectedItem); + return this.updateDownloadQueueView(cb); + }, + clearQueue : (formData, extraArgs, cb) => { + this.dlQueue.clear(); + return this.updateDownloadQueueView(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 + // :TODO: This technique can be applied in many areas of the code; probablly need a better name than 'popAndGotoMenu' though + // ...actually, the option to not append to the stack would be better here + return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); + //return this.popAndGotoMenu(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(); + } + ); + } + + 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.clearScreen()); + } + + 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); + } + ); + } +}; diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js new file mode 100644 index 00000000..b6f2a8ce --- /dev/null +++ b/mods/file_transfer_protocol_select.js @@ -0,0 +1,126 @@ +/* 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 || {}; + this.config.direction = this.config.direction || 'send'; + + this.loadAvailProtocols(); + + this.extraArgs = options.extraArgs; + + if(_.has(options, 'lastMenuResult.sentFileIds')) { + this.sentFileIds = options.lastMenuResult.sentFileIds; + } + + this.fallbackOnly = options.lastMenuResult ? true : false; + + 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 || 'downloadFiles', modOpts, cb); + } else { + return this.gotoMenu(this.config.uploadFilesMenu || 'uploadFiles', modOpts, cb); + } + }, + }; + } + + getMenuResult() { + if(this.sentFileIds) { + return { sentFileIds : this.sentFileIds }; + } + } + + initSequence() { + if(this.sentFileIds) { + // nothing to do here; move along + 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, + }; + }); + + this.protocols.sort( (a, b) => a.name.localeCompare(b.name) ); + } +}; From a45142f2fd36c932ce9c4e5df1f774d1d468cdc8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 1 Jan 2017 21:53:04 -0700 Subject: [PATCH 19/86] * Use more standard code paths & emit index events in ToggleMenuView * Fix fetching areas & internal message attach area name * Use proper config in new MenuModule methods * More good progress on uploading --- core/config.js | 2 +- core/file_area.js | 27 +++-- core/menu_module.js | 4 +- core/menu_view.js | 2 - core/toggle_menu_view.js | 54 ++++++---- mods/file_transfer_protocol_select.js | 13 ++- mods/upload.js | 148 ++++++++++++++++++++++++++ 7 files changed, 205 insertions(+), 45 deletions(-) create mode 100644 mods/upload.js diff --git a/core/config.js b/core/config.js index 0a7a2217..c7a717ca 100644 --- a/core/config.js +++ b/core/config.js @@ -404,7 +404,7 @@ function getDefaultConfig() { }, areas: { - systemm_message_attachment : { + system_message_attachment : { name : 'Message attachments', desc : 'File attachments to messages', storageTags : 'sys_msg_attach', // may be string or array of strings diff --git a/core/file_area.js b/core/file_area.js index 4903f571..e3aa2081 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -19,8 +19,10 @@ const paths = require('path'); const temp = require('temp').track(); // track() cleans up temp dir/files for us const iconv = require('iconv-lite'); +exports.isInternalArea = isInternalArea; exports.getAvailableFileAreas = getAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; +exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getFileAreaByTag = getFileAreaByTag; exports.getFileEntryPath = getFileEntryPath; @@ -30,20 +32,23 @@ exports.scanFileAreaForChanges = scanFileAreaForChanges; const WellKnownAreaTags = exports.WellKnownAreaTags = { Invalid : '', - MessageAreaAttach : 'message_area_attach', + MessageAreaAttach : 'system_message_attachment', }; +function isInternalArea(areaTag) { + return areaTag === WellKnownAreaTags.MessageAreaAttach; +} + function getAvailableFileAreas(client, options) { options = options || { }; - // perform ACS check per conf & omit system_internal if desired - const areasWithTags = _.map(Config.fileBase.areas, (area, areaTag) => Object.assign(area, { areaTag : areaTag } ) ); + // perform ACS check per conf & omit internal if desired return _.omit(Config.fileBase.areas, (area, areaTag) => { - if(!options.includeSystemInternal && WellKnownAreaTags.MessageAreaAttach === areaTag) { + if(!options.includeSystemInternal && isInternalArea(areaTag)) { return true; } - if(options.writeAcs && !client.acs.FileAreaWrite(area)) { + if(options.writeAcs && !client.acs.hasFileAreaWrite(area)) { return true; // omit } @@ -122,16 +127,10 @@ function getAreaStorageDirectoryByTag(storageTag) { const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); return paths.resolve(Config.fileBase.areaStoragePrefix, storageLocation || ''); - - /* - // absolute paths as-is - if(storageLocation && '/' === storageLocation.charAt(0)) { - return storageLocation; - } +} - // relative to |areaStoragePrefix| - return paths.join(Config.fileBase.areaStoragePrefix, storageLocation || ''); - */ +function getAreaDefaultStorageDirectory(areaInfo) { + return getAreaStorageDirectoryByTag(areaInfo.storageTags[0]); } function getAreaStorageLocations(areaInfo) { diff --git a/core/menu_module.js b/core/menu_module.js index d740c478..5f5d0318 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -340,7 +340,7 @@ MenuModule.prototype.displayAsset = function(name, options, cb) { return theme.displayThemedAsset( name, this.client, - Object.merge( { font : this.menuConfig.config }, options ), + Object.assign( { font : this.menuConfig.config.font }, options ), (err, artData) => { if(cb) { return cb(err, artData); @@ -376,7 +376,7 @@ MenuModule.prototype.prepViewController = function(name, formId, artData, cb) { MenuModule.prototype.prepViewControllerWithArt = function(name, formId, options, cb) { this.displayAsset( - name, + this.menuConfig.config.art[name], options, (err, artData) => { if(err) { diff --git a/core/menu_view.js b/core/menu_view.js index 0c11c51c..ec225907 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -92,12 +92,10 @@ MenuView.prototype.getItem = function(index) { }; MenuView.prototype.focusNext = function() { - // nothing @ base currently this.emit('index update', this.focusedItemIndex); }; MenuView.prototype.focusPrevious = function() { - // nothign @ base currently this.emit('index update', this.focusedItemIndex); }; diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index a740782c..35676193 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -25,9 +25,7 @@ function ToggleMenuView (options) { */ this.updateSelection = function() { - //assert(!self.positionCacheExpired); assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - self.redraw(); }; } @@ -74,28 +72,38 @@ ToggleMenuView.prototype.setFocus = function(focused) { this.redraw(); }; -ToggleMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - var needsUpdate; - if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) { - if(this.items.length - 1 === this.focusedItemIndex) { - this.focusedItemIndex = 0; - } else { - this.focusedItemIndex++; - } - needsUpdate = true; - } else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.name)) { - if(0 === this.focusedItemIndex) { - this.focusedItemIndex = this.items.length - 1; - } else { - this.focusedItemIndex--; - } - needsUpdate = true; - } +ToggleMenuView.prototype.focusNext = function() { + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + } else { + this.focusedItemIndex++; + } - if(needsUpdate) { - this.updateSelection(); - return; + this.updateSelection(); + + ToggleMenuView.super_.prototype.focusNext.call(this); +}; + +ToggleMenuView.prototype.focusPrevious = function() { + + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + } else { + this.focusedItemIndex--; + } + + this.updateSelection(); + + ToggleMenuView.super_.prototype.focusPrevious.call(this); +}; + +ToggleMenuView.prototype.onKeyPress = function(ch, key) { + + if(key) { + if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) { + this.focusNext(); + } else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.nam4e)) { + this.focusPrevious(); } } diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js index b6f2a8ce..42e78bb4 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/file_transfer_protocol_select.js @@ -27,11 +27,18 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { 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.loadAvailProtocols(); - this.extraArgs = options.extraArgs; + this.extraArgs = options.extraArgs; if(_.has(options, 'lastMenuResult.sentFileIds')) { this.sentFileIds = options.lastMenuResult.sentFileIds; @@ -50,9 +57,9 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { }; if('send' === this.config.direction) { - return this.gotoMenu(this.config.downloadFilesMenu || 'downloadFiles', modOpts, cb); + return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); } else { - return this.gotoMenu(this.config.uploadFilesMenu || 'uploadFiles', modOpts, cb); + return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); } }, }; diff --git a/mods/upload.js b/mods/upload.js new file mode 100644 index 00000000..47fbd9b4 --- /dev/null +++ b/mods/upload.js @@ -0,0 +1,148 @@ +/* jslint node: true */ +'use strict'; + +// enigma-bbs +const MenuModule = require('../core/menu_module.js').MenuModule; +const ViewController = require('../core/view_controller.js').ViewController; +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'); +const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; +const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory; + +// deps +const async = require('async'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Upload', + desc : 'Module for classic file uploads', + author : 'NuSkooler', +}; + +const FormIds = { + options : 0, + fileDetails : 1, + +}; + +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. + }, + + fileDetails : { + tags : 1, // tag(s) for item + desc : 2, // defaults to 'desc' (e.g. from FILE_ID.DIZ) + accept : 3, // accept fields & continue + } +}; + +exports.getModule = class UploadModule extends MenuModule { + + constructor(options) { + super(options); + + this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs : true } ); + + this.menuMethods = { + navContinue : (formData, extraArgs, cb) => { + if(this.isBlindUpload()) { + // jump to fileDetails form + // :TODO: support blind + } else { + // jump to protocol selection + const areaUploadDir = this.getSelectedAreaUploadDirectory(); + + const modOpts = { + extraArgs : { + recvDirectory : areaUploadDir, + direction : 'recv', + } + }; + + return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); + } + } + }; + } + + getSelectedAreaUploadDirectory() { + const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); + const selectedArea = this.availAreas[areaSelectView.getData()]; + + return getAreaDefaultStorageDirectory(selectedArea); + } + + isBlindUpload() { return 'blind' === this.uploadType; } + + initSequence() { + const self = this; + + async.series( + [ + function before(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayOptionsPage(false, callback); + } + ], + () => { + return self.finishedLoading(); + } + ); + } + + 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); + + // :TODO: when blind, fileNameView should not be focus/editable + } + }); + + uploadTypeView.setFocusItemIndex(0); // default to blind + fileNameView.setText(blindFileNameText); + areaSelectView.redraw(); + + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + +}; From 1218fe65f9865fa86a94756046e1d6e3e2eef6ba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Jan 2017 22:47:00 -0700 Subject: [PATCH 20/86] Add new file scan year est regexp --- core/config.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/config.js b/core/config.js index c7a717ca..b43e2e50 100644 --- a/core/config.js +++ b/core/config.js @@ -286,12 +286,18 @@ function getDefaultConfig() { zmodem8kSz : { name : 'ZModem 8k', type : 'external', - external : { + external : { sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" sendArgs : [ - '--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePaths}' + // :TODO: try -q + '--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}' ], - escapeTelnet : true, // set to true to escape Telnet codes such as IAC + recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + recvArgs : [ + '--zmodem', '--binary', '--restricted', // dumps to CWD which is set to {uploadDir} + ], + // :TODO: can we not just use --escape ? + escapeTelnet : true, // set to true to escape Telnet codes such as IAC supportsBatch : true, } }, @@ -385,7 +391,8 @@ function getDefaultConfig() { // '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. "\\B('[1789][0-9])\\b", // eslint-disable-line quotes - '[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', + '[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', + '(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)\\s((?:[0-9]{2})?[0-9]{2})', // November 29th, 1997 // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], From 4c1c05e4da2f1ed66bf290df0ec597fa6d4b024a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Jan 2017 22:48:04 -0700 Subject: [PATCH 21/86] Fix bug with already logged in/connected check logic @ login --- core/user_login.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/user_login.js b/core/user_login.js index 84d20a9d..76b01598 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -29,7 +29,7 @@ function userLogin(client, username, password, cb) { // Loop through active connections -- which includes the current -- // and check for matching user ID. If the count is > 1, disallow. // - let existingClientConnection = + let existingClientConnection; clientConnections.forEach(function connEntry(cc) { if(cc.user !== user && cc.user.userId === user.userId) { existingClientConnection = cc; @@ -47,7 +47,9 @@ function userLogin(client, username, password, cb) { var existingConnError = new Error('Already logged in as supplied user'); existingConnError.existingConn = true; - return cb(existingClientConnection); + // :TODO: We should use EnigError & pass existing connection as second param + + return cb(existingConnError); } From e265e3cc970174c3903a3331a2ff2f6d4b36c7f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Jan 2017 22:51:00 -0700 Subject: [PATCH 22/86] * WIP on upload scan/processing * WIP on user add/edit data to uploads * Add write access (upload) to area ACS * Add upload collision handling * Add upload stats --- core/enig_error.js | 1 + core/file_area.js | 208 +++++++++++++--------- core/file_entry.js | 10 ++ core/menu_util.js | 3 +- core/multi_line_edit_text_view.js | 8 +- core/string_util.js | 6 +- core/transfer_file.js | 118 ++++++++++--- mods/file_area_list.js | 3 + mods/file_base_download_manager.js | 2 +- mods/file_transfer_protocol_select.js | 12 +- mods/upload.js | 241 ++++++++++++++++++++++++-- 11 files changed, 479 insertions(+), 133 deletions(-) diff --git a/core/enig_error.js b/core/enig_error.js index 69c6fb3c..98e3cb50 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -27,4 +27,5 @@ exports.Errors = { DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), + ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), }; diff --git a/core/file_area.js b/core/file_area.js index e3aa2081..09072d55 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -27,7 +27,7 @@ exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getFileAreaByTag = getFileAreaByTag; exports.getFileEntryPath = getFileEntryPath; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; -//exports.addOrUpdateFileEntry = addOrUpdateFileEntry; +exports.scanFile = scanFile; exports.scanFileAreaForChanges = scanFileAreaForChanges; const WellKnownAreaTags = exports.WellKnownAreaTags = { @@ -43,16 +43,18 @@ function getAvailableFileAreas(client, options) { options = options || { }; // perform ACS check per conf & omit internal if desired - return _.omit(Config.fileBase.areas, (area, areaTag) => { - if(!options.includeSystemInternal && isInternalArea(areaTag)) { + const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); + + return _.omit(allAreas, areaInfo => { + if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { return true; } - if(options.writeAcs && !client.acs.hasFileAreaWrite(area)) { + if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) { return true; // omit } - return !client.acs.hasFileAreaRead(area); + return !client.acs.hasFileAreaRead(areaInfo); }); } @@ -326,42 +328,16 @@ function populateFileEntryWithArchive(fileEntry, filePath, archiveType, cb) { ); } -function populateFileEntry(fileEntry, filePath, archiveType, cb) { +function populateFileEntryNonArchive(fileEntry, filePath, archiveType, cb) { // :TODO: implement me! return cb(null); } function addNewFileEntry(fileEntry, filePath, cb) { - const archiveUtil = ArchiveUtil.getInstance(); - // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data async.series( [ - function populateInfo(callback) { - archiveUtil.detectType(filePath, (err, archiveType) => { - if(archiveType) { - // save this off - fileEntry.meta.archive_type = archiveType; - - populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => { - if(err) { - populateFileEntry(fileEntry, filePath, err => { - // :TODO: log err - return callback(null); // ignore err - }); - } else { - return callback(null); - } - }); - } else { - populateFileEntry(fileEntry, filePath, err => { - // :TODO: log err - return callback(null); // ignore err - }); - } - }); - }, function addNewDbRecord(callback) { return fileEntry.persist(callback); } @@ -376,6 +352,102 @@ function updateFileEntry(fileEntry, filePath, cb) { } +function scanFile(filePath, options, cb) { + + if(_.isFunction(options) && !cb) { + cb = options; + options = {}; + } + + const fileEntry = new FileEntry({ + areaTag : options.areaTag, + meta : options.meta, + hashTags : options.hashTags, // Set() or Array + fileName : paths.basename(filePath), + storageTag : options.storageTag, + }); + + async.waterfall( + [ + function processPhysicalFileGeneric(callback) { + let byteSize = 0; + const sha1 = crypto.createHash('sha1'); + const sha256 = crypto.createHash('sha256'); + const md5 = crypto.createHash('md5'); + const crc32 = new CRC32(); + + const stream = fs.createReadStream(filePath); + + stream.on('data', data => { + byteSize += data.length; + + sha1.update(data); + sha256.update(data); + md5.update(data); + crc32.update(data); + }); + + stream.on('end', () => { + fileEntry.meta.byte_size = byteSize; + + // sha-1 is in basic file entry + fileEntry.fileSha1 = sha1.digest('hex'); + + // others are meta + fileEntry.meta.file_sha256 = sha256.digest('hex'); + fileEntry.meta.file_md5 = md5.digest('hex'); + fileEntry.meta.file_crc32 = crc32.finalize().toString(16); + + return callback(null); + }); + + stream.on('error', err => { + return callback(err); + }); + }, + function processPhysicalFileByType(callback) { + const archiveUtil = ArchiveUtil.getInstance(); + + archiveUtil.detectType(filePath, (err, archiveType) => { + if(archiveType) { + // save this off + fileEntry.meta.archive_type = archiveType; + + populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => { + if(err) { + populateFileEntryNonArchive(fileEntry, filePath, err => { + // :TODO: log err + return callback(null); // ignore err + }); + } else { + return callback(null); + } + }); + } else { + populateFileEntryNonArchive(fileEntry, filePath, err => { + // :TODO: log err + return callback(null); // ignore err + }); + } + }); + }, + function fetchExistingEntry(callback) { + getExistingFileEntriesBySha1(fileEntry.fileSha1, (err, existingEntries) => { + return callback(err, existingEntries); + }); + } + ], + (err, existingEntries) => { + if(err) { + return cb(err); + } + + return cb(null, fileEntry, existingEntries); + } + ); +} + +/* function addOrUpdateFileEntry(areaInfo, storageLocation, fileName, options, cb) { const fileEntry = new FileEntry({ @@ -444,6 +516,7 @@ function addOrUpdateFileEntry(areaInfo, storageLocation, fileName, options, cb) } ); } +*/ function scanFileAreaForChanges(areaInfo, cb) { const storageLocations = getAreaStorageLocations(areaInfo); @@ -472,9 +545,28 @@ function scanFileAreaForChanges(areaInfo, cb) { return nextFile(null); } - addOrUpdateFileEntry(areaInfo, storageLoc, fileName, { }, err => { - return nextFile(err); - }); + scanFile( + fullPath, + { + areaTag : areaInfo.areaTag, + storageTag : storageLoc.storageTag + }, + (err, fileEntry, existingEntries) => { + if(err) { + // :TODO: Log me!!! + return nextFile(null); // try next anyway + } + + if(existingEntries.length > 0) { + // :TODO: Handle duplidates -- what to do here??? + } else { + addNewFileEntry(fileEntry, fullPath, err => { + // pass along error; we failed to insert a record in our DB or something else bad + return nextFile(err); + }); + } + } + ); }); }, err => { return callback(err); @@ -495,49 +587,3 @@ function scanFileAreaForChanges(areaInfo, cb) { return cb(err); }); } - -/* -function scanFileAreaForChanges2(areaInfo, cb) { - const areaPhysDir = getAreaStorageDirectory(areaInfo); - - async.series( - [ - function scanPhysFiles(callback) { - fs.readdir(areaPhysDir, (err, files) => { - if(err) { - return callback(err); - } - - async.eachSeries(files, (fileName, next) => { - const fullPath = paths.join(areaPhysDir, fileName); - - fs.stat(fullPath, (err, stats) => { - if(err) { - // :TODO: Log me! - return next(null); // always try next file - } - - if(!stats.isFile()) { - return next(null); - } - - addOrUpdateFileEntry(areaInfo, fileName, { areaTag : areaInfo.areaTag }, err => { - return next(err); - }); - }); - }, err => { - return callback(err); - }); - }); - }, - function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above - return callback(null); - } - ], - err => { - return cb(err); - } - ); -} -*/ \ No newline at end of file diff --git a/core/file_entry.js b/core/file_entry.js index f816a62b..42712541 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -213,6 +213,16 @@ module.exports = class FileEntry { ); } + setHashTags(hashTags) { + if(_.isString(hashTags)) { + this.hashTags = new Set(hashTags.split(/[\s,]+/)); + } else if(Array.isArray(hashTags)) { + this.hashTags = new Set(hashTags); + } else if(hashTags instanceof Set) { + this.hashTags = hashTags; + } + } + static getWellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); } static findFiles(filter, cb) { diff --git a/core/menu_util.js b/core/menu_util.js index f62dbd6c..68751f33 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -148,8 +148,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { // if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { Log.trace('Using generic configuration'); - cb(null, formForId); - return; + return cb(null, formForId); } cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 4ef55590..05ac3586 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -409,10 +409,10 @@ function MultiLineEditTextView(options) { this.insertCharactersInText = function(c, index, col) { self.textLines[index].text = [ - self.textLines[index].text.slice(0, col), - c, - self.textLines[index].text.slice(col) - ].join(''); + self.textLines[index].text.slice(0, col), + c, + self.textLines[index].text.slice(col) + ].join(''); //self.cursorPos.col++; self.cursorPos.col += c.length; diff --git a/core/string_util.js b/core/string_util.js index edb0e613..68778533 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -444,7 +444,7 @@ function createCleanAnsi(input, options, cb) { //while(col <= canvas[row][0].width) { while(col < options.width) { if(!canvas[row][col].char) { - canvas[row][col].char = 'P'; + canvas[row][col].char = ' '; if(!canvas[row][col].sgr) { // :TODO: fix duplicate SGR's in a row here - we just need one per sequence canvas[row][col].sgr = ANSI.reset(); @@ -459,12 +459,12 @@ function createCleanAnsi(input, options, cb) { if(col <= options.width) { canvas[row][col] = canvas[row][col] || {}; - //canvas[row][col].char = '\r\n'; + canvas[row][col].char = '\r\n'; canvas[row][col].sgr = ANSI.reset(); // :TODO: don't splice, just reset + fill with ' ' till end for(let fillCol = col; fillCol <= options.width; ++fillCol) { - canvas[row][fillCol].char = 'X'; + canvas[row][fillCol].char = ' '; } //canvas[row] = canvas[row].splice(0, col + 1); diff --git a/core/transfer_file.js b/core/transfer_file.js index bed1a613..1807f88d 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -100,18 +100,15 @@ exports.getModule = class TransferFileModule extends MenuModule { this.sentFileIds = []; } - get isSending() { - return 'send' === this.direction; + isSending() { + return ('send' === this.direction); } - restorePipeAfterExternalProc(pipe) { + restorePipeAfterExternalProc() { if(!this.pipeRestored) { this.pipeRestored = true; this.client.restoreDataHandler(); - - //this.client.term.output.unpipe(pipe); - //this.client.term.output.resume(); } } @@ -154,16 +151,62 @@ exports.getModule = class TransferFileModule extends MenuModule { } } + moveFileWithCollisionHandling(src, dst, cb) { + // + // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. + // in the case of collisions. + // + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); + const dstFileSuffix = paths.basename(dst, dstFileExt); + + let renameIndex = 0; + let movedOk = false; + let tryDstPath; + + async.until( + () => movedOk, // until moved OK + (cb) => { + if(0 === renameIndex) { + // try originally supplied path first + tryDstPath = dst; + } else { + tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); + } + + fse.move(src, tryDstPath, err => { + if(err) { + if('EEXIST' === err.code) { + renameIndex += 1; + return cb(null); // keep trying + } + + return cb(err); + } + + movedOk = true; + return cb(null, tryDstPath); + }); + }, + (err, finalPath) => { + return cb(err, finalPath); + } + ); + } + recvFiles(cb) { this.executeExternalProtocolHandlerForRecv( (err, tempWorkingDir) => { if(err) { return cb(err); } - this.receivedFiles = []; + this.recvFilePaths = []; if(this.recvFileName) { // file name specified - we expect a single file in |tempWorkingDir| + + // :TODO: support non-blind: Move file to dest path, add to recvFilePaths, etc. + return cb(null); } else { // @@ -176,19 +219,19 @@ exports.getModule = class TransferFileModule extends MenuModule { } async.each(files, (file, nextFile) => { - fse.move( + this.moveFileWithCollisionHandling( paths.join(tempWorkingDir, file), paths.join(this.recvDirectory, file), - err => { + (err, destPath) => { if(err) { - // :TODO: IMPORTANT: Handle collisions - rename to FILE(1).EXT, etc. this.client.log.warn( { tempWorkingDir : tempWorkingDir, recvDirectory : this.recvDirectory, file : file, error : err.message }, 'Failed to move upload file to destination directory' ); } else { - this.receivedFiles.push(file); + this.recvFilePaths.push(destPath); } + return nextFile(null); // don't pass along err; try next } ); @@ -324,16 +367,16 @@ exports.getModule = class TransferFileModule extends MenuModule { }); externalProc.once('close', () => { - return this.restorePipeAfterExternalProc(externalProc); + return this.restorePipeAfterExternalProc(); }); externalProc.once('exit', (exitCode) => { this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); - this.restorePipeAfterExternalProc(externalProc); + this.restorePipeAfterExternalProc(); externalProc.removeAllListeners(); - return cb(null); + return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); }); } @@ -366,7 +409,11 @@ exports.getModule = class TransferFileModule extends MenuModule { } getMenuResult() { - return { sentFileIds : this.sentFileIds }; + if(this.isSending()) { + return { sentFileIds : this.sentFileIds }; + } else { + return { recvFilePaths : this.recvFilePaths }; + } } updateSendStats(cb) { @@ -383,9 +430,8 @@ exports.getModule = class TransferFileModule extends MenuModule { fileIds.push(queueItem.fileId); } - downloadCount += 1; - if(_.isNumber(queueItem.byteSize)) { + downloadCount += 1; downloadBytes += queueItem.byteSize; return next(null); } @@ -395,6 +441,7 @@ exports.getModule = class TransferFileModule extends MenuModule { if(err) { this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); } else { + downloadCount += 1; downloadBytes += stats.size; } @@ -416,8 +463,30 @@ exports.getModule = class TransferFileModule extends MenuModule { } updateRecvStats(cb) { - // :TODO: update user & system upload stats - return cb(null); + let uploadBytes = 0; + let uploadCount = 0; + + async.each(this.recvFilePaths, (filePath, next) => { + // we just have a path - figure it out + fs.stat(filePath, (err, stats) => { + if(err) { + this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); + } else { + uploadCount += 1; + uploadBytes += stats.size; + } + + return next(null); + }); + }, () => { + StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount); + StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes); + StatLog.incrementSystemStat('ul_total_count', uploadCount); + StatLog.incrementSystemStat('ul_total_bytes', uploadBytes); + + + return cb(null); + }); } initSequence() { @@ -428,7 +497,7 @@ exports.getModule = class TransferFileModule extends MenuModule { async.series( [ function validateConfig(callback) { - if(self.isSending) { + if(self.isSending()) { if(!Array.isArray(self.sendQueue)) { self.sendQueue = [ self.sendQueue ]; } @@ -437,7 +506,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return callback(null); }, function transferFiles(callback) { - if(self.isSending) { + if(self.isSending()) { self.sendFiles( err => { if(err) { return callback(err); @@ -475,7 +544,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); }, function updateUserAndSystemStats(callback) { - if(self.isSending) { + if(self.isSending()) { return self.updateSendStats(callback); } else { return self.updateRecvStats(callback); @@ -488,9 +557,10 @@ exports.getModule = class TransferFileModule extends MenuModule { } // Wait for a key press - attempt to avoid issues with some terminals after xfer - self.client.term.write('|00\nTransfer(s) complete. Press a key\n'); + // :TODO: display ANSI if it exists else prompt -- look @ Obv/2 for filename + self.client.term.pipeWrite('|00|07\nTransfer(s) complete. Press a key\n'); self.client.waitForKeyPress( () => { - self.prevMenu(); + return self.prevMenu(); }); } ); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 58e72c35..33adc8ba 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -333,6 +333,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(_.isString(self.currentFileEntry.desc)) { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); if(descView) { + /* :TODO: finish createCleanAnsi() and use here!!! createCleanAnsi( self.currentFileEntry.desc, { height : self.client.termHeight, width : descView.dimens.width }, @@ -345,6 +346,8 @@ exports.getModule = class FileAreaList extends MenuModule { return callback(null); } ); + */ + descView.setText( self.currentFileEntry.desc ); } } else { diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index 017a3141..77b6609c 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -45,7 +45,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { if(_.has(options, 'lastMenuResult.sentFileIds')) { this.sentFileIds = options.lastMenuResult.sentFileIds; } - + this.fallbackOnly = options.lastMenuResult ? true : false; this.menuMethods = { diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js index 42e78bb4..1d250d2e 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/file_transfer_protocol_select.js @@ -44,6 +44,10 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { this.sentFileIds = options.lastMenuResult.sentFileIds; } + if(_.has(options, 'lastMenuResult.recvFilePaths')) { + this.recvFilePaths = options.lastMenuResult.recvFilePaths; + } + this.fallbackOnly = options.lastMenuResult ? true : false; this.menuMethods = { @@ -69,11 +73,15 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { if(this.sentFileIds) { return { sentFileIds : this.sentFileIds }; } + + if(this.recvFilePaths) { + return { recvFilePaths : this.recvFilePaths }; + } } initSequence() { - if(this.sentFileIds) { - // nothing to do here; move along + if(this.sentFileIds || this.recvFilePaths) { + // nothing to do here; move along (we're just falling through) this.prevMenu(); } else { super.initSequence(); diff --git a/mods/upload.js b/mods/upload.js index 47fbd9b4..63c99961 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -10,10 +10,13 @@ const Errors = require('../core/enig_error.js').Errors; const stringFormat = require('../core/string_format.js'); const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory; +const scanFile = require('../core/file_area.js').scanFile; +const getAreaStorageDirectoryByTag = require('../core/file_area.js').getAreaStorageDirectoryByTag; // deps -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); +const paths = require('path'); exports.moduleInfo = { name : 'Upload', @@ -23,7 +26,8 @@ exports.moduleInfo = { const FormIds = { options : 0, - fileDetails : 1, + processing : 1, + fileDetails : 2, }; @@ -35,10 +39,16 @@ const MciViewIds = { navMenu : 4, // next/cancel/etc. }, + processing : { + // 10+ = customs + }, + fileDetails : { - tags : 1, // tag(s) for item - desc : 2, // defaults to 'desc' (e.g. from FILE_ID.DIZ) - accept : 3, // accept fields & continue + desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ) + tags : 2, // tag(s) for item + estYear : 3, + accept : 4, // accept fields & continue + // 10+ = customs } }; @@ -47,14 +57,15 @@ 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 = { - navContinue : (formData, extraArgs, cb) => { + optionsNavContinue : (formData, extraArgs, cb) => { if(this.isBlindUpload()) { - // jump to fileDetails form - // :TODO: support blind - } else { // jump to protocol selection const areaUploadDir = this.getSelectedAreaUploadDirectory(); @@ -66,20 +77,54 @@ exports.getModule = class UploadModule extends MenuModule { }; return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); + } else { + // jump to fileDetails form + // :TODO: support non-blind: collect info/filename -> upload -> complete } + }, + + fileDetailsContinue : (formData, extraArgs, cb) => { + + + // see notes in displayFileDetailsPageForEntry() about this hackery: + cb(null); + return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any } }; } + getSaveState() { + const saveState = { + uploadType : this.uploadType, + + }; + + if(this.isBlindUpload()) { + saveState.areaInfo = this.getSelectedAreaInfo(); + } + + return saveState; + } + + restoreSavedState(savedState) { + if(savedState.areaInfo) { + this.areaInfo = savedState.areaInfo; + } + } + + getSelectedAreaInfo() { + const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); + return this.availAreas[areaSelectView.getData()]; + } + getSelectedAreaUploadDirectory() { - const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); - const selectedArea = this.availAreas[areaSelectView.getData()]; - - return getAreaDefaultStorageDirectory(selectedArea); + const areaInfo = this.getSelectedAreaInfo(); + return getAreaDefaultStorageDirectory(areaInfo); } isBlindUpload() { return 'blind' === this.uploadType; } - + isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); } + initSequence() { const self = this; @@ -89,7 +134,11 @@ exports.getModule = class UploadModule extends MenuModule { return self.beforeArt(callback); }, function display(callback) { - return self.displayOptionsPage(false, callback); + if(self.isFileTransferComplete()) { + return self.displayProcessingPage(callback); + } else { + return self.displayOptionsPage(callback); + } } ], () => { @@ -98,6 +147,110 @@ exports.getModule = class UploadModule extends MenuModule { ); } + finishedLoading() { + if(this.isFileTransferComplete()) { + return this.processUploadedFiles(); + } + + + } + + scanFiles(cb) { + const self = this; + + const results = { + newEntries : [], + dupes : [], + }; + + async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => { + // :TODO: virus scanning/etc. should occur around here + + // :TODO: update scanning status art or display line "scanning {fileName}..." type of thing + + self.client.term.pipeWrite(`|00|07\nScanning ${paths.basename(filePath)}...`); + + scanFile( + filePath, + { + areaTag : self.areaInfo.areaTag, + storageTag : self.areaInfo.storageTags[0], + }, + (err, fileEntry, existingEntries) => { + if(err) { + return nextFilePath(err); + } + + self.client.term.pipeWrite(' done\n'); + + // new or dupe? + if(existingEntries.length > 0) { + // 1:n dupes found + results.dupes = results.dupes.concat(existingEntries); + } else { + // new one + results.newEntries.push(fileEntry); + } + + return nextFilePath(null); + } + ); + }, err => { + return cb(err, results); + }); + } + + processUploadedFiles() { + // + // For each file uploaded, we need to process & gather information + // + const self = this; + + async.waterfall( + [ + function scan(callback) { + return self.scanFiles(callback); + }, + function displayDupes(scanResults, callback) { + if(0 === scanResults.dupes.length) { + return callback(null, scanResults); + } + + // :TODO: display dupe info + return callback(null, scanResults); + }, + function prepDetails(scanResults, callback) { + async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { + self.displayFileDetailsPageForEntry(newEntry, (err, newValues) => { + if(!err) { + // if the file entry did *not* have a desc, take the user desc + if(!self.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 self.fileDetailsCurrentEntrySubmitCallback; + return callback(err); + }); + } + ], + err => { + + } + ); + } + displayOptionsPage(cb) { const self = this; @@ -130,6 +283,7 @@ exports.getModule = class UploadModule extends MenuModule { } }); + self.uploadType = 'blind'; uploadTypeView.setFocusItemIndex(0); // default to blind fileNameView.setText(blindFileNameText); areaSelectView.redraw(); @@ -145,4 +299,59 @@ exports.getModule = class UploadModule extends MenuModule { ); } + displayProcessingPage(cb) { + // :TODO: If art is supplied, display & start processing + update status/etc.; if no art, we'll just write each status update on a new line + return cb(null); + } + + fileEntryHasDetectedDesc(fileEntry) { + return (fileEntry.desc && fileEntry.desc.length > 0); + } + + displayFileDetailsPageForEntry(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); + + if(self.fileEntryHasDetectedDesc(fileEntry)) { + descView.setText(fileEntry.desc); + descView.setPropertyValue('mode', 'preview'); + + // :TODO: it would be nice to take this out of the focus order + } + + const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags); + tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse + + const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear); + yearView.setText(fileEntry.meta.est_release_year || ''); + + 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 + } + ); + } }; From fb176d3ab395bdc300238534cacc5638c401bb01 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 18 Jan 2017 22:23:53 -0700 Subject: [PATCH 23/86] * Fix updating of existing file filter * Update findFiles() to properly apply filters, sort order / direction, etc. * Properly persist hash tags @ file entry persist * Lots of improvements / additions to MCI for upload/download, etc. stats * Persist processed entries @ upload (WIP, but close!) --- core/file_base_filter.js | 11 ++++ core/file_entry.js | 94 +++++++++++++++++++++++++++-------- core/predefined_mci.js | 63 +++++++++++++---------- core/stat_log.js | 4 ++ main.js | 1 - mods/file_area_filter_edit.js | 6 ++- mods/file_area_list.js | 10 ++-- mods/upload.js | 36 +++++++++----- 8 files changed, 159 insertions(+), 66 deletions(-) diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 10628747..b6598670 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -46,6 +46,17 @@ module.exports = class FileBaseFilters { return filterUuid; } + replace(filterUuid, filterInfo) { + const filter = this.get(filterUuid); + if(!filter) { + return false; + } + + filterInfo.tags = this.cleanTags(filterInfo.tags); + this.filters[filterUuid] = filterInfo; + return true; + } + remove(filterUuid) { delete this.filters[filterUuid]; } diff --git a/core/file_entry.js b/core/file_entry.js index 42712541..b93ea967 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -121,7 +121,13 @@ module.exports = class FileEntry { }); }, function storeHashTags(callback) { - return callback(null); + const hashTagsArray = Array.from(self.hashTags); + async.each(hashTagsArray, (hashTag, next) => { + return FileEntry.persistHashTag(self.fileId, hashTag, next); + }, + err => { + return callback(err); + }); } ], err => { @@ -192,6 +198,30 @@ module.exports = class FileEntry { ); } + static persistHashTag(fileId, hashTag, cb) { + fileDb.serialize( () => { + fileDb.run( + `INSERT OR IGNORE INTO hash_tag (hash_tag) + VALUES (?);`, + [ hashTag ] + ); + + fileDb.run( + `REPLACE INTO file_hash_tag (hash_tag_id, file_id) + VALUES ( + (SELECT hash_tag_id + FROM hash_tag + WHERE hash_tag = ?), + ? + );`, + [ hashTag, fileId ], + err => { + return cb(err); + } + ); + }); + } + loadHashTags(cb) { fileDb.each( `SELECT ht.hash_tag_id, ht.hash_tag @@ -226,20 +256,20 @@ module.exports = class FileEntry { static getWellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); } static findFiles(filter, cb) { - // :TODO: build search here - return [ fileid1, fileid2, ... ] - // free form - // areaTag - // tags - // order by - // sort - filter = filter || {}; - let sql = - `SELECT file_id - FROM file`; - + let sql; let sqlWhere = ''; + let sqlOrderBy; + const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; + + function getOrderByWithCast(ob) { + if( [ 'dl_count', 'user_rating', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { + return `ORDER BY CAST(${ob} AS INTEGER)`; + } + + return `ORDER BY ${ob}`; + } function appendWhereClause(clause) { if(sqlWhere) { @@ -250,13 +280,38 @@ module.exports = class FileEntry { sqlWhere += clause; } + if(filter.sort) { + if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? + sql = + `SELECT f.file_id + FROM file f, file_meta m`; + + appendWhereClause(`f.file_id = m.file_id AND m.meta_name="${filter.sort}"`); + + sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; + } else { + sql = + `SELECT f.file_id, f.${filter.sort} + FROM file f`; + + sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + sqlOrderDir; + } + } else { + sql = + `SELECT f.file_id + FROM file`; + + sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; + } + + if(filter.areaTag) { - appendWhereClause(`area_tag="${filter.areaTag}"`); + appendWhereClause(`f.area_tag="${filter.areaTag}"`); } if(filter.terms) { appendWhereClause( - `file_id IN ( + `f.file_id IN ( SELECT rowid FROM file_fts WHERE file_fts MATCH "${filter.terms.replace(/"/g,'""')}" @@ -265,25 +320,24 @@ module.exports = class FileEntry { } if(filter.tags) { - const tags = filter.tags.split(' '); // filter stores as sep separated values + // build list of quoted tags; filter.tags comes in as a space separated values + const tags = filter.tags.split(' ').map( tag => `"${tag}"` ).join(','); appendWhereClause( - `file_id IN ( + `f.file_id IN ( SELECT file_id FROM file_hash_tag WHERE hash_tag_id IN ( SELECT hash_tag_id FROM hash_tag - WHERE hash_tag IN (${tags.join(',')}) + WHERE hash_tag IN (${tags}) ) )` ); } - // :TODO: filter.orderBy - // :TODO: filter.sort + sql += `${sqlWhere} ${sqlOrderBy};`; - sql += sqlWhere + ';'; const matchingFileIds = []; fileDb.each(sql, (err, fileId) => { if(fileId) { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 9ad300e7..b0f4178b 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -37,6 +37,17 @@ function setNextRandomRumor(cb) { }); } +function getRatio(client, propA, propB) { + const a = StatLog.getUserStatNum(client.user, propA); + const b = StatLog.getUserStatNum(client.user, propB); + const ratio = ~~((a / b) * 100); + return `${ratio}%`; +} + +function userStatAsString(client, statName, defaultValue) { + return (StatLog.getUserStat(client.user, statName) || defaultValue).toString(); +} + function getPredefinedMCIValue(client, code) { if(!client || !code) { @@ -69,16 +80,16 @@ function getPredefinedMCIValue(client, code) { UN : function userName() { return client.user.username; }, UI : function userId() { return client.user.userId.toString(); }, UG : function groups() { return _.values(client.user.groups).join(', '); }, - UR : function realName() { return client.user.properties.real_name; }, - LO : function location() { return client.user.properties.location; }, + UR : function realName() { return userStatAsString(client, 'real_name', ''); }, + LO : function location() { return userStatAsString(client, 'location', ''); }, UA : function age() { return client.user.getAge().toString(); }, - UB : function birthdate() { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, - US : function sex() { return client.user.properties.sex; }, - UE : function emailAddres() { return client.user.properties.email_address; }, - UW : function webAddress() { return client.user.properties.web_address; }, - UF : function affils() { return client.user.properties.affiliation; }, - UT : function themeId() { return client.user.properties.theme_id; }, - UC : function loginCount() { return client.user.properties.login_count.toString(); }, + BD : function birthdate() { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY + US : function sex() { return userStatAsString(client, 'sex', ''); }, + UE : function emailAddres() { return userStatAsString(client, 'email_address', ''); }, + UW : function webAddress() { return userStatAsString(client, 'web_address', ''); }, + UF : function affils() { return userStatAsString(client, 'affiliation', ''); }, + UT : function themeId() { return userStatAsString(client, 'theme_id', ''); }, + UC : function loginCount() { return userStatAsString(client, 'login_count', 0); }, ND : function connectedNode() { return client.node.toString(); }, IP : function clientIpAddress() { return client.address().address; }, ST : function serverName() { return client.session.serverName; }, @@ -86,26 +97,27 @@ function getPredefinedMCIValue(client, code) { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : ''; }, - DN : function userNumDownloads() { return StatLog.getUserStat(client.user, 'dl_total_count'); }, // Obv/2 - DK : function userByteDownload() { // Obv/2 - const byteSize = parseInt(StatLog.getUserStat(client.user, 'dl_total_bytes')) || 0; - return formatByteSize(byteSize, true); + DN : function userNumDownloads() { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 + DK : function userByteDownload() { // Obv/2 uses DK=downloaded Kbytes + const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + UP : function userNumUploads() { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 + UK : function userByteUpload() { // Obv/2 uses UK=uploaded Kbytes + const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + NR : function userUpDownRatio() { // Obv/2 + return getRatio(client, 'ul_total_count', 'dl_total_count'); + }, + KR : function userUpDownByteRatio() { // Obv/2 uses KR=upload/download Kbyte ratio + return getRatio(client, 'ul_total_bytes', 'dl_total_bytes'); }, - // :TODO: Up/down ratio (count) - // :TODO: Up/down ratio (bytes) MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, CS : function currentStatus() { return client.currentStatus; }, - PS : function userPostCount() { - const postCount = client.user.properties.post_count || 0; - return postCount.toString(); - }, - PC : function userPostCallRatio() { - const postCount = client.user.properties.post_count || 0; - const callCount = client.user.properties.login_count; - const ratio = ~~((postCount / callCount) * 100); - return `${ratio}%`; - }, + PS : function userPostCount() { return userStatAsString(client, 'post_count', 0); }, + PC : function userPostCallRatio() { return getRatio(client, 'post_count', 'login_count'); }, MD : function currentMenuDescription() { return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; @@ -181,6 +193,7 @@ function getPredefinedMCIValue(client, code) { // // :TODO: DD - Today's # of downloads (iNiQUiTY) // + // :TODO: System stat log for total ul/dl, total ul/dl bytes // // Special handling for XY diff --git a/core/stat_log.js b/core/stat_log.js index 4d53581a..41c4ceb5 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -122,6 +122,10 @@ class StatLog { return user.properties[statName]; } + getUserStatNum(user, statName) { + return parseInt(this.getUserStat(user, statName)) || 0; + } + incrementUserStat(user, statName, incrementBy, cb) { incrementBy = incrementBy || 1; diff --git a/main.js b/main.js index ab8d2652..ef624294 100755 --- a/main.js +++ b/main.js @@ -1,7 +1,6 @@ #!/usr/bin/env node /* jslint node: true */ - 'use strict'; /* diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index e7935f9a..3b848e91 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -262,11 +262,13 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } saveCurrentFilter(formData, cb) { - const filters = new FileBaseFilters(this.client); - const selectedFilter = this.filtersArray[this.currentFilterIndex]; + const filters = new FileBaseFilters(this.client); + const selectedFilter = this.filtersArray[this.currentFilterIndex]; + if(selectedFilter) { // *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 = {}; diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 33adc8ba..d1b9c351 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -333,22 +333,20 @@ exports.getModule = class FileAreaList extends MenuModule { if(_.isString(self.currentFileEntry.desc)) { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); if(descView) { - /* :TODO: finish createCleanAnsi() and use here!!! createCleanAnsi( self.currentFileEntry.desc, { height : self.client.termHeight, width : descView.dimens.width }, cleanDesc => { - descView.setText(cleanDesc); + // :TODO: use cleanDesc -- need to finish createCleanAnsi() !! + //descView.setText(cleanDesc); + descView.setText( self.currentFileEntry.desc ); self.updateQueueIndicator(); self.populateCustomLabels('browse', 10); return callback(null); } - ); - */ - - descView.setText( self.currentFileEntry.desc ); + ); } } else { self.updateQueueIndicator(); diff --git a/mods/upload.js b/mods/upload.js index 63c99961..d01aa827 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -222,31 +222,43 @@ exports.getModule = class UploadModule extends MenuModule { function prepDetails(scanResults, callback) { async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { self.displayFileDetailsPageForEntry(newEntry, (err, newValues) => { - if(!err) { - // if the file entry did *not* have a desc, take the user desc - if(!self.fileEntryHasDetectedDesc(newEntry)) { - newEntry.desc = newValues.shortDesc.trim(); - } + if(err) { + return nextEntry(err); + } - if(newValues.estYear.length > 0) { - newEntry.meta.est_release_year = newValues.estYear; - } + // if the file entry did *not* have a desc, take the user desc + if(!self.fileEntryHasDetectedDesc(newEntry)) { + newEntry.desc = newValues.shortDesc.trim(); + } - if(newValues.tags.length > 0) { - newEntry.setHashTags(newValues.tags); - } + 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 self.fileDetailsCurrentEntrySubmitCallback; + return callback(err, scanResults); + }); + }, + function persistNewEntries(scanResults, callback) { + // loop over entries again & persist to DB + async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { + newEntry.persist(err => { + return nextEntry(err); + }); + }, err => { return callback(err); }); } ], err => { - + console.log('eh'); // :TODO: remove me :) } ); } From 8d51c7d47c2de5d7cce10c08f8b7726b947ff307 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jan 2017 22:09:29 -0700 Subject: [PATCH 24/86] * Additional helpers to MenuModule * Gzip signature (.gz) * Switch to sha-256 vs sha1 for internal file hashes * Nearly complete callback / scan update support for scanFile() * Fix data input issue after performing upload * Support 'sz' recv (uploads) --- core/config.js | 9 +- core/database.js | 2 +- core/file_area.js | 249 +++++++++++++++++++---------------- core/file_entry.js | 8 +- core/menu_module.js | 43 ++++-- core/predefined_mci.js | 11 ++ core/servers/login/telnet.js | 4 +- core/transfer_file.js | 15 ++- core/uuid_util.js | 11 +- mods/file_area_list.js | 51 ++++--- mods/upload.js | 141 +++++++++++++++----- 11 files changed, 349 insertions(+), 195 deletions(-) diff --git a/core/config.js b/core/config.js index b43e2e50..1b34a3e8 100644 --- a/core/config.js +++ b/core/config.js @@ -278,6 +278,13 @@ function getDefaultConfig() { exts : [ 'rar' ], handler : '7Zip', desc : 'RAR Archive', + }, + gzip : { + sig : '1f8b', + offset : 0, + exts : [ 'gz' ], + handler : '7Zip', + desc : 'Gzip Archive', } } }, @@ -294,7 +301,7 @@ function getDefaultConfig() { ], recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" recvArgs : [ - '--zmodem', '--binary', '--restricted', // dumps to CWD which is set to {uploadDir} + '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} ], // :TODO: can we not just use --escape ? escapeTelnet : true, // set to true to escape Telnet codes such as IAC diff --git a/core/database.js b/core/database.js index 24ef4941..76c4d72b 100644 --- a/core/database.js +++ b/core/database.js @@ -260,7 +260,7 @@ const DB_INIT_TABLE = { `CREATE TABLE IF NOT EXISTS file ( file_id INTEGER PRIMARY KEY, area_tag VARCHAR NOT NULL, - file_sha1 VARCHAR NOT NULL, + file_sha256 VARCHAR NOT NULL, file_name, /* FTS @ file_fts */ storage_tag VARCHAR NOT NULL, desc, /* FTS @ file_fts */ diff --git a/core/file_area.js b/core/file_area.js index 09072d55..bea5f8c7 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -160,14 +160,14 @@ function getFileEntryPath(fileEntry) { } } -function getExistingFileEntriesBySha1(sha1, cb) { +function getExistingFileEntriesBySha256(sha256, cb) { const entries = []; FileDb.each( `SELECT file_id, area_tag FROM file - WHERE file_sha1=?;`, - [ sha1 ], + WHERE file_sha256=?;`, + [ sha256 ], (err, fileRow) => { if(fileRow) { entries.push({ @@ -237,14 +237,38 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } } -function populateFileEntryWithArchive(fileEntry, filePath, archiveType, cb) { - const archiveUtil = ArchiveUtil.getInstance(); +function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) { + const archiveUtil = ArchiveUtil.getInstance(); + const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() async.waterfall( [ - function getArchiveFileList(callback) { - archiveUtil.listEntries(filePath, archiveType, (err, entries) => { - return callback(null, entries || []); // ignore any errors here + function getArchiveFileList(callback) { + stepInfo.step = 'archive_list_start'; + + iterator(err => { + if(err) { + return callback(err); + } + + archiveUtil.listEntries(filePath, archiveType, (err, entries) => { + if(err) { + stepInfo.step = 'archive_list_failed'; + } else { + stepInfo.step = 'archive_list_finish'; + stepInfo.archiveEntries = entries || []; + } + + iterator(iterErr => { + return callback( iterErr, entries || [] ); // ignore original |err| here + }); + }); + }); + }, + function processDescFilesStart(entries, callback) { + stepInfo.step = 'desc_files_start'; + iterator(err => { + return callback(err, entries); }); }, function extractDescFiles(entries, callback) { @@ -320,7 +344,11 @@ function populateFileEntryWithArchive(fileEntry, filePath, archiveType, cb) { function attemptReleaseYearEstimation(callback) { attemptSetEstimatedReleaseDate(fileEntry); return callback(null); - } + }, + function processDescFilesFinish(callback) { + stepInfo.step = 'desc_files_finish'; + return iterator(callback); + }, ], err => { return cb(err); @@ -328,7 +356,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, archiveType, cb) { ); } -function populateFileEntryNonArchive(fileEntry, filePath, archiveType, cb) { +function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) { // :TODO: implement me! return cb(null); } @@ -352,11 +380,17 @@ function updateFileEntry(fileEntry, filePath, cb) { } -function scanFile(filePath, options, cb) { - - if(_.isFunction(options) && !cb) { - cb = options; - options = {}; +const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ]; + +function scanFile(filePath, options, iterator, cb) { + + if(3 === arguments.length && _.isFunction(iterator)) { + cb = iterator; + iterator = null; + } else if(2 === arguments.length && _.isFunction(options)) { + cb = options; + iterator = null; + options = {}; } const fileEntry = new FileEntry({ @@ -367,42 +401,96 @@ function scanFile(filePath, options, cb) { storageTag : options.storageTag, }); + const stepInfo = { + filePath : filePath, + fileName : paths.basename(filePath), + }; + + function callIter(next) { + if(iterator) { + return iterator(stepInfo, next); + } else { + return next(null); + } + } + + function readErrorCallIter(origError, next) { + stepInfo.step = 'read_error'; + stepInfo.error = origError.message; + + callIter( () => { + return next(origError); + }); + } + async.waterfall( [ + function startScan(callback) { + fs.stat(filePath, (err, stats) => { + if(err) { + return readErrorCallIter(err, callback); + } + + stepInfo.step = 'start'; + stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; + + return callIter(callback); + }); + }, function processPhysicalFileGeneric(callback) { - let byteSize = 0; - const sha1 = crypto.createHash('sha1'); - const sha256 = crypto.createHash('sha256'); - const md5 = crypto.createHash('md5'); - const crc32 = new CRC32(); - + stepInfo.bytesProcessed = 0; + + const hashes = { + sha1 : crypto.createHash('sha1'), + sha256 : crypto.createHash('sha256'), + md5 : crypto.createHash('md5'), + crc32 : new CRC32(), + }; + const stream = fs.createReadStream(filePath); stream.on('data', data => { - byteSize += data.length; + stream.pause(); // until iterator compeltes - sha1.update(data); - sha256.update(data); - md5.update(data); - crc32.update(data); + stepInfo.bytesProcessed += data.length; + stepInfo.step = 'hash_update'; + + callIter(err => { + if(err) { + stream.destroy(); // cancel read + return callback(err); + } + + async.each( HASH_NAMES, (hashName, nextHash) => { + hashes[hashName].update(data); + return nextHash(null); + }, () => { + return stream.resume(); + }); + }); }); stream.on('end', () => { - fileEntry.meta.byte_size = byteSize; + fileEntry.meta.byte_size = stepInfo.bytesProcessed; - // sha-1 is in basic file entry - fileEntry.fileSha1 = sha1.digest('hex'); + async.each(HASH_NAMES, (hashName, nextHash) => { + if('sha256' === hashName) { + stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); + } else if('sha1' === hashName || 'md5' === hashName) { + stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex'); + } else if('crc32' === hashName) { + stepInfo.crc32 = fileEntry.meta.crc32 = hashes.crc32.finalize().toString(16); + } - // others are meta - fileEntry.meta.file_sha256 = sha256.digest('hex'); - fileEntry.meta.file_md5 = md5.digest('hex'); - fileEntry.meta.file_crc32 = crc32.finalize().toString(16); - - return callback(null); + return nextHash(null); + }, () => { + stepInfo.step = 'hash_finish'; + return callIter(callback); + }); }); stream.on('error', err => { - return callback(err); + return readErrorCallIter(err, callback); }); }, function processPhysicalFileByType(callback) { @@ -413,9 +501,9 @@ function scanFile(filePath, options, cb) { // save this off fileEntry.meta.archive_type = archiveType; - populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => { + populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { if(err) { - populateFileEntryNonArchive(fileEntry, filePath, err => { + populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { // :TODO: log err return callback(null); // ignore err }); @@ -424,7 +512,7 @@ function scanFile(filePath, options, cb) { } }); } else { - populateFileEntryNonArchive(fileEntry, filePath, err => { + populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { // :TODO: log err return callback(null); // ignore err }); @@ -432,92 +520,21 @@ function scanFile(filePath, options, cb) { }); }, function fetchExistingEntry(callback) { - getExistingFileEntriesBySha1(fileEntry.fileSha1, (err, existingEntries) => { - return callback(err, existingEntries); + getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { + return callback(err, dupeEntries); }); } ], - (err, existingEntries) => { + (err, dupeEntries) => { if(err) { return cb(err); } - return cb(null, fileEntry, existingEntries); + return cb(null, fileEntry, dupeEntries); } ); } -/* -function addOrUpdateFileEntry(areaInfo, storageLocation, fileName, options, cb) { - - const fileEntry = new FileEntry({ - areaTag : areaInfo.areaTag, - meta : options.meta, - hashTags : options.hashTags, // Set() or Array - fileName : fileName, - storageTag : storageLocation.storageTag, - }); - - const filePath = paths.join(storageLocation.dir, fileName); - - async.waterfall( - [ - function processPhysicalFile(callback) { - let byteSize = 0; - const sha1 = crypto.createHash('sha1'); - const sha256 = crypto.createHash('sha256'); - const md5 = crypto.createHash('md5'); - const crc32 = new CRC32(); - - const stream = fs.createReadStream(filePath); - - stream.on('data', data => { - byteSize += data.length; - - sha1.update(data); - sha256.update(data); - md5.update(data); - crc32.update(data); - }); - - stream.on('end', () => { - fileEntry.meta.byte_size = byteSize; - - // sha-1 is in basic file entry - fileEntry.fileSha1 = sha1.digest('hex'); - - // others are meta - fileEntry.meta.file_sha256 = sha256.digest('hex'); - fileEntry.meta.file_md5 = md5.digest('hex'); - fileEntry.meta.file_crc32 = crc32.finalize().toString(16); - - return callback(null); - }); - - stream.on('error', err => { - return callback(err); - }); - }, - function fetchExistingEntry(callback) { - getExistingFileEntriesBySha1(fileEntry.fileSha1, (err, existingEntries) => { - return callback(err, existingEntries); - }); - }, - function addOrUpdate(existingEntries, callback) { - if(existingEntries.length > 0) { - - } else { - return addNewFileEntry(fileEntry, filePath, callback); - } - }, - ], - err => { - return cb(err); - } - ); -} -*/ - function scanFileAreaForChanges(areaInfo, cb) { const storageLocations = getAreaStorageLocations(areaInfo); @@ -551,13 +568,13 @@ function scanFileAreaForChanges(areaInfo, cb) { areaTag : areaInfo.areaTag, storageTag : storageLoc.storageTag }, - (err, fileEntry, existingEntries) => { + (err, fileEntry, dupeEntries) => { if(err) { // :TODO: Log me!!! return nextFile(null); // try next anyway } - if(existingEntries.length > 0) { + if(dupeEntries.length > 0) { // :TODO: Handle duplidates -- what to do here??? } else { addNewFileEntry(fileEntry, fullPath, err => { diff --git a/core/file_entry.js b/core/file_entry.js index b93ea967..353a5118 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -12,7 +12,7 @@ const _ = require('lodash'); const paths = require('path'); const FILE_TABLE_MEMBERS = [ - 'file_id', 'area_tag', 'file_sha1', 'file_name', 'storage_tag', + 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', 'desc', 'desc_long', 'upload_timestamp' ]; @@ -21,7 +21,7 @@ const FILE_WELL_KNOWN_META = { upload_by_username : null, upload_by_user_id : null, file_md5 : null, - file_sha256 : null, + file_sha1 : null, file_crc32 : null, est_release_year : (y) => parseInt(y) || new Date().getFullYear(), dl_count : (d) => parseInt(d) || 0, @@ -100,9 +100,9 @@ module.exports = class FileEntry { }, function storeEntry(callback) { fileDb.run( - `REPLACE INTO file (area_tag, file_sha1, file_name, storage_tag, desc, desc_long, upload_timestamp) + `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) VALUES(?, ?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.fileSha1, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], + [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], function inserted(err) { // use non-arrow func for 'this' scope / lastID if(!err) { self.fileId = this.lastID; diff --git a/core/menu_module.js b/core/menu_module.js index 5f5d0318..4194b6c8 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -1,17 +1,18 @@ /* jslint node: true */ 'use strict'; -var PluginModule = require('./plugin_module.js').PluginModule; -var theme = require('./theme.js'); -var ansi = require('./ansi_term.js'); -var ViewController = require('./view_controller.js').ViewController; -var menuUtil = require('./menu_util.js'); -var Config = require('./config.js').config; +const PluginModule = require('./plugin_module.js').PluginModule; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const ViewController = require('./view_controller.js').ViewController; +const menuUtil = require('./menu_util.js'); +const Config = require('./config.js').config; +const stringFormat = require('../core/string_format.js'); // deps -var async = require('async'); -var assert = require('assert'); -var _ = require('lodash'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); exports.MenuModule = MenuModule; @@ -386,4 +387,28 @@ MenuModule.prototype.prepViewControllerWithArt = function(name, formId, options, return this.prepViewController(name, formId, artData, cb); } ); +}; + +MenuModule.prototype.setViewText = function(formName, mciId, text) { + const view = this.viewControllers[formName].getView(mciId); + if(view) { + view.setText(text); + } +}; + +MenuModule.prototype.updateCustomViewTextsWithFilter = function(formName, startId, fmtObj, filter) { + let textView; + let customMciId = startId; + const config = this.menuConfig.config; + + while( (textView = this.viewControllers[formName].getView(customMciId)) ) { + const key = `${formName}InfoFormat${customMciId}`; + const format = config[key]; + + if(format && (!filter || filter.find(f => format.indexOf(f) > - 1))) { + textView.setText(stringFormat(format, fmtObj)); + } + + ++customMciId; + } }; \ No newline at end of file diff --git a/core/predefined_mci.js b/core/predefined_mci.js index b0f4178b..5ed828cc 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -195,6 +195,17 @@ function getPredefinedMCIValue(client, code) { // // :TODO: System stat log for total ul/dl, total ul/dl bytes + // :TODO: PT - Messages posted *today* (Obv/2) + // :TODO: NT - New users today (Obv/2) + // :TODO: CT - Calls *today* (Obv/2) + // :TODO: TF - Total files on the system (Obv/2) + // :TODO: FT - Files uploaded/added *today* (Obv/2) + // :TODO: DD - Files downloaded *today* (iNiQUiTY) + // :TODO: TP - total message/posts on the system (Obv/2) + // :TODO: LC - name of last caller to system (Obv/2) + // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) + + // // Special handling for XY // diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index f23a1dc7..045c9280 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -439,12 +439,12 @@ function TelnetClient(input, output) { }; this.setTemporaryDataHandler = function(handler) { - this.input.removeAllListeners(); + this.input.removeAllListeners('data'); this.input.on('data', handler); }; this.restoreDataHandler = function() { - this.input.removeAllListeners(); + this.input.removeAllListeners('data'); this.input.on('data', this.dataHandler); }; diff --git a/core/transfer_file.js b/core/transfer_file.js index 1807f88d..b0ee78d7 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -351,7 +351,13 @@ exports.getModule = class TransferFileModule extends MenuModule { }); this.client.setTemporaryDataHandler(data => { - externalProc.write(data); + // needed for things like sz/rz + if(external.escapeTelnet) { + const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape + externalProc.write(new Buffer(tmp, 'binary')); + } else { + externalProc.write(data); + } }); //this.client.term.output.pipe(externalProc); @@ -359,7 +365,7 @@ exports.getModule = class TransferFileModule extends MenuModule { externalProc.on('data', data => { // needed for things like sz/rz if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); + const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape this.client.term.rawWrite(new Buffer(tmp, 'binary')); } else { this.client.term.rawWrite(data); @@ -484,7 +490,6 @@ exports.getModule = class TransferFileModule extends MenuModule { StatLog.incrementSystemStat('ul_total_count', uploadCount); StatLog.incrementSystemStat('ul_total_bytes', uploadBytes); - return cb(null); }); } @@ -556,12 +561,16 @@ exports.getModule = class TransferFileModule extends MenuModule { self.client.log.warn( { error : err.message }, 'File transfer error'); } + return self.prevMenu(); + /* + // Wait for a key press - attempt to avoid issues with some terminals after xfer // :TODO: display ANSI if it exists else prompt -- look @ Obv/2 for filename self.client.term.pipeWrite('|00|07\nTransfer(s) complete. Press a key\n'); self.client.waitForKeyPress( () => { return self.prevMenu(); }); + */ } ); } diff --git a/core/uuid_util.js b/core/uuid_util.js index 00e8840c..d8023f95 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -1,10 +1,7 @@ /* jslint node: true */ 'use strict'; -let uuid = require('node-uuid'); -let assert = require('assert'); -let _ = require('lodash'); -let createHash = require('crypto').createHash; +const createHash = require('crypto').createHash; exports.createNamedUUID = createNamedUUID; @@ -13,9 +10,9 @@ function createNamedUUID(namespaceUuid, key) { // v5 UUID generation code based on the work here: // https://github.com/download13/uuidv5/blob/master/uuid.js // - if(!Buffer.isBuffer(namespaceUuid)) { - namespaceUuid = new Buffer(namespaceUuid); - } + if(!Buffer.isBuffer(namespaceUuid)) { + namespaceUuid = new Buffer(namespaceUuid); + } if(!Buffer.isBuffer(key)) { key = new Buffer(key); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index d1b9c351..245941a6 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -46,26 +46,30 @@ const FormIds = { const MciViewIds = { browse : { - desc : 1, - navMenu : 2, - // 10+ = customs + desc : 1, + navMenu : 2, + + customRangeStart : 10, // 10+ = customs }, details : { - navMenu : 1, - infoXyTop : 2, // %XY starting position for info area - infoXyBottom : 3, - // 10+ = customs + navMenu : 1, + infoXyTop : 2, // %XY starting position for info area + infoXyBottom : 3, + + customRangeStart : 10, // 10+ = customs }, detailsGeneral : { - // 10+ = customs + customRangeStart : 10, // 10+ = customs }, detailsNfo : { nfo : 1, - // 10+ = customs + + customRangeStart : 10, // 10+ = customs }, detailsFileList : { fileList : 1, - // 10+ = customs + + customRangeStart : 10, // 10+ = customs }, }; @@ -163,7 +167,7 @@ exports.getModule = class FileAreaList extends MenuModule { areaTag : currEntry.areaTag, areaName : area.name || 'N/A', areaDesc : area.desc || 'N/A', - fileSha1 : currEntry.fileSha1, + fileSha256 : currEntry.fileSha256, fileName : currEntry.fileName, desc : currEntry.desc || '', descLong : currEntry.descLong || '', @@ -220,9 +224,10 @@ exports.getModule = class FileAreaList extends MenuModule { } populateCustomLabels(category, startId) { - return this.updateCustomLabelsWithFilter(category, startId); + return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); } +/* updateCustomLabelsWithFilter(category, startId, filter) { let textView; let customMciId = startId; @@ -239,6 +244,7 @@ exports.getModule = class FileAreaList extends MenuModule { ++customMciId; } } + */ displayArtAndPrepViewController(name, options, cb) { const self = this; @@ -342,7 +348,7 @@ exports.getModule = class FileAreaList extends MenuModule { descView.setText( self.currentFileEntry.desc ); self.updateQueueIndicator(); - self.populateCustomLabels('browse', 10); + self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); return callback(null); } @@ -350,7 +356,7 @@ exports.getModule = class FileAreaList extends MenuModule { } } else { self.updateQueueIndicator(); - self.populateCustomLabels('browse', 10); + self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); return callback(null); } @@ -373,7 +379,7 @@ exports.getModule = class FileAreaList extends MenuModule { return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback); }, function populateViews(callback) { - self.populateCustomLabels('details', 10); + self.populateCustomLabels('details', MciViewIds.details.customRangeStart); return callback(null); }, function prepSection(callback) { @@ -438,7 +444,11 @@ exports.getModule = class FileAreaList extends MenuModule { ); }, function updateActiveViews(callback) { - self.updateCustomLabelsWithFilter( 'browse', 10, [ '{webDlLink}', '{webDlExpire}' ] ); + self.updateCustomViewTextsWithFilter( + 'browse', + MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, + [ '{webDlLink}', '{webDlExpire}' ] + ); return callback(null); } ], @@ -458,7 +468,12 @@ exports.getModule = class FileAreaList extends MenuModule { isNotQueuedIndicator ); - this.updateCustomLabelsWithFilter( 'browse', 10, [ '{isQueued}' ] ); + this.updateCustomViewTextsWithFilter( + 'browse', + MciViewIds.browse.customRangeStart, + this.currentFileEntry.entryInfo, + [ '{isQueued}' ] + ); } cacheArchiveEntries(cb) { @@ -564,7 +579,7 @@ exports.getModule = class FileAreaList extends MenuModule { break; } - self.populateCustomLabels(name, 10); + self.populateCustomLabels(name, MciViewIds[name].customRangeStart); return callback(null); } ], diff --git a/mods/upload.js b/mods/upload.js index d01aa827..bb7f6bfd 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -40,15 +40,16 @@ const MciViewIds = { }, processing : { - // 10+ = customs + stepIndicator : 1, + 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 - // 10+ = customs + 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 } }; @@ -151,8 +152,66 @@ exports.getModule = class UploadModule extends MenuModule { if(this.isFileTransferComplete()) { return this.processUploadedFiles(); } + } + updateScanStepInfoViews(stepInfo) { + // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC + const fmtObj = Object.assign( {}, stepInfo); + let stepIndicatorFmt = ''; + + switch(stepInfo.step) { + case 'start' : + stepIndicatorFmt = this.menuConfig.config.scanningStartFormat || 'Scanning {fileName}'; + break; + + case 'hash_update' : + stepIndicatorFmt = this.menuConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; + + this.scanStatus.hashUpdateCount += 1; + fmtObj.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)).toString(); + + if(this.scanStatus.hashUpdateCount % 2) { + fmtObj.calcHashIndicator = this.menuConfig.config.hashUpdateIndicator1Fmt || '-'; + } else { + fmtObj.calcHashIndicator = this.menuConfig.config.hashUpdateIndicator2Fmt || '*'; + } + break; + + case 'hash_finish' : + stepIndicatorFmt = this.menuConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; + break; + + case 'archive_list_start' : + stepIndicatorFmt = this.menuConfig.extractArchiveListFormat || 'Extracting archive list'; + break; + + case 'archive_list_finish' : + fmtObj.archivedFileCount = stepInfo.archiveEntries.length; + stepIndicatorFmt = this.menuConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; + break; + + case 'archive_list_failed' : + stepIndicatorFmt = this.menuConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; + break; + + case 'desc_files_start' : + stepIndicatorFmt = this.menuConfig.processingDescFilesFormat || 'Processing description files'; + break; + + case 'desc_files_finish' : + stepIndicatorFmt = this.menuConfig.processingDescFilesFinishFormat || 'Finished processing description files'; + break; + } + + const stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); + + if(this.hasProcessingArt) { + this.setViewText('processing', MciViewIds.processing.stepIndicator, stepIndicatorText); + this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj); + } else { + this.client.term.pipeWrite(`${stepIndicatorText}\n`); + } } scanFiles(cb) { @@ -166,35 +225,36 @@ exports.getModule = class UploadModule extends MenuModule { async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => { // :TODO: virus scanning/etc. should occur around here - // :TODO: update scanning status art or display line "scanning {fileName}..." type of thing + self.scanStatus = { + hashUpdateCount : 0, + }; - self.client.term.pipeWrite(`|00|07\nScanning ${paths.basename(filePath)}...`); + const scanOpts = { + areaTag : self.areaInfo.areaTag, + storageTag : self.areaInfo.storageTags[0], + }; - scanFile( - filePath, - { - areaTag : self.areaInfo.areaTag, - storageTag : self.areaInfo.storageTags[0], - }, - (err, fileEntry, existingEntries) => { - if(err) { - return nextFilePath(err); - } + function handleScanStep(stepInfo, nextScanStep) { + self.updateScanStepInfoViews(stepInfo); + return nextScanStep(null); + } - self.client.term.pipeWrite(' done\n'); - - // new or dupe? - if(existingEntries.length > 0) { - // 1:n dupes found - results.dupes = results.dupes.concat(existingEntries); - } else { - // new one - results.newEntries.push(fileEntry); - } - - return nextFilePath(null); + scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { + if(err) { + return nextFilePath(err); } - ); + + // new or dupe? + if(dupeEntries.length > 0) { + // 1:n dupes found + results.dupes = results.dupes.concat(dupeEntries); + } else { + // new one + results.newEntries.push(fileEntry); + } + + return nextFilePath(null); + }); }, err => { return cb(err, results); }); @@ -258,7 +318,11 @@ exports.getModule = class UploadModule extends MenuModule { } ], err => { - console.log('eh'); // :TODO: remove me :) + if(err) { + self.client.log.warn('File upload error encountered', { error : err.message } ); + } + + return self.prevMenu(); } ); } @@ -312,8 +376,17 @@ exports.getModule = class UploadModule extends MenuModule { } displayProcessingPage(cb) { - // :TODO: If art is supplied, display & start processing + update status/etc.; if no art, we'll just write each status update on a new line - return cb(null); + 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) { From 99036592ae6ca26804318b4c294c27dd700d3760 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jan 2017 21:30:49 -0700 Subject: [PATCH 25/86] * Bump version to 0.0.2-alpha: File Base alpha here * LHA/LZH archive support via external lha command * Nearly complete upload processor * Set default file base filter if none is set * Additional MenuModule common method/helpers * MLTEV property: tabSwitchesView --- core/config.js | 43 ++++++++++++++++- core/file_area.js | 47 +++++++++++++----- core/file_base_filter.js | 41 ++++++++++++++-- core/file_entry.js | 10 ++-- core/menu_module.js | 58 ++++++++++++++++++----- core/multi_line_edit_text_view.js | 63 ++++++++++++++---------- core/string_util.js | 5 ++ core/text_view.js | 3 +- mods/abracadabra.js | 2 + mods/file_area_list.js | 23 +-------- mods/msg_area_list.js | 1 + mods/msg_conf_list.js | 1 + mods/upload.js | 79 ++++++++++++++++++++++--------- package.json | 2 +- 14 files changed, 269 insertions(+), 109 deletions(-) diff --git a/core/config.js b/core/config.js index 1b34a3e8..98d0f9dd 100644 --- a/core/config.js +++ b/core/config.js @@ -248,9 +248,36 @@ function getDefaultConfig() { cmd : '7za', args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], }, + }, + + Lha : { + // + // 'lha' command can be obtained from: + // * apt-get: lhasa + // + // (compress not currently supported) + // + decompress : { + cmd : 'lha', + args : [ '-ew={extractPath}', '{archivePath}' ], + }, + list : { + cmd : 'lha', + args : [ '-l', '{archivePath}' ], + entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : 'lha', + args : [ '-ew={extractPath}', '{archivePath}', '{fileList}' ] + } } }, + formats : { + // + // Resources + // * http://www.garykessler.net/library/file_sigs.html + // zip : { sig : '504b0304', offset : 0, @@ -285,6 +312,20 @@ function getDefaultConfig() { exts : [ 'gz' ], handler : '7Zip', desc : 'Gzip Archive', + }, + bzip : { + sig : '425a68', + offset : 0, + exts : [ 'bz2' ], + handler : '7Zip', + desc : 'BZip2 Archive', + }, + lzh : { + sig : '2d6c68', + offset : 2, + exts : [ 'lzh', 'ice' ], + handler : 'Lha', + desc : 'LHArc Archive', } } }, @@ -399,7 +440,7 @@ function getDefaultConfig() { '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. "\\B('[1789][0-9])\\b", // eslint-disable-line quotes '[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', - '(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)\\s((?:[0-9]{2})?[0-9]{2})', // November 29th, 1997 + '(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})', // November 29th, 1997 // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], diff --git a/core/file_area.js b/core/file_area.js index bea5f8c7..637847d6 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -294,6 +294,10 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c extractList.push(longDescFile.fileName); } + if(0 === extractList.length) { + return callback(null, [] ); + } + temp.mkdir('enigextract-', (err, tempDir) => { if(err) { return callback(err); @@ -423,6 +427,9 @@ function scanFile(filePath, options, iterator, cb) { }); } + + let lastCalcHashPercent; + async.waterfall( [ function startScan(callback) { @@ -449,25 +456,39 @@ function scanFile(filePath, options, iterator, cb) { const stream = fs.createReadStream(filePath); + function updateHashes(data) { + async.each( HASH_NAMES, (hashName, nextHash) => { + hashes[hashName].update(data); + return nextHash(null); + }, () => { + return stream.resume(); + }); + } + stream.on('data', data => { stream.pause(); // until iterator compeltes - stepInfo.bytesProcessed += data.length; - stepInfo.step = 'hash_update'; + stepInfo.bytesProcessed += data.length; + stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); - callIter(err => { - if(err) { - stream.destroy(); // cancel read - return callback(err); - } + // + // Only send 'hash_update' step update if we have a noticable percentage change in progress + // + if(stepInfo.calcHashPercent === lastCalcHashPercent) { + updateHashes(data); + } else { + lastCalcHashPercent = stepInfo.calcHashPercent; + stepInfo.step = 'hash_update'; - async.each( HASH_NAMES, (hashName, nextHash) => { - hashes[hashName].update(data); - return nextHash(null); - }, () => { - return stream.resume(); + callIter(err => { + if(err) { + stream.destroy(); // cancel read + return callback(err); + } + + updateHashes(data); }); - }); + } }); stream.on('end', () => { diff --git a/core/file_base_filter.js b/core/file_base_filter.js index b6598670..da6c4590 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -61,15 +61,29 @@ module.exports = class FileBaseFilters { delete this.filters[filterUuid]; } - load(prop) { - prop = prop || this.client.user.properties.file_base_filters; + load() { + let filtersProperty = this.client.user.properties.file_base_filters; + let defaulted; + if(!filtersProperty) { + filtersProperty = JSON.stringify(FileBaseFilters.getDefaultFilters()); + defaulted = true; + } try { - this.filters = JSON.parse(prop); + this.filters = JSON.parse(filtersProperty); } catch(e) { - this.filters = {}; + this.filters = FileBaseFilters.getDefaultFilters(); // something bad happened; reset everything back to defaults :( + defaulted = true; + this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); + } - this.client.log.error( { error : e.message, property : prop }, 'Failed parsing file base filters property' ); + if(defaulted) { + this.persist( err => { + if(!err) { + const defaultActiveUuid = this.toArray()[0].uuid; + this.setActive(defaultActiveUuid); + } + }); } } @@ -93,6 +107,23 @@ module.exports = class FileBaseFilters { return false; } + static getDefaultFilters() { + const filters = {}; + + const uuid = uuids.v4(); + filters[uuid] = { + name : 'Default', + areaTag : '', // all + terms : '', // * + tags : '', // * + order : 'ascending', + sort : 'upload_timestamp', + uuid : uuid, + }; + + return filters; + } + static getActiveFilter(client) { return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); } diff --git a/core/file_entry.js b/core/file_entry.js index 353a5118..cb208008 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -280,7 +280,7 @@ module.exports = class FileEntry { sqlWhere += clause; } - if(filter.sort) { + if(filter.sort && filter.sort.length > 0) { if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? sql = `SELECT f.file_id @@ -294,7 +294,7 @@ module.exports = class FileEntry { `SELECT f.file_id, f.${filter.sort} FROM file f`; - sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + sqlOrderDir; + sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; } } else { sql = @@ -305,11 +305,11 @@ module.exports = class FileEntry { } - if(filter.areaTag) { + if(filter.areaTag && filter.areaTag.length > 0) { appendWhereClause(`f.area_tag="${filter.areaTag}"`); } - if(filter.terms) { + if(filter.terms && filter.terms.length > 0) { appendWhereClause( `f.file_id IN ( SELECT rowid @@ -319,7 +319,7 @@ module.exports = class FileEntry { ); } - if(filter.tags) { + if(filter.tags && filter.tags.length > 0) { // build list of quoted tags; filter.tags comes in as a space separated values const tags = filter.tags.split(' ').map( tag => `"${tag}"` ).join(','); diff --git a/core/menu_module.js b/core/menu_module.js index 4194b6c8..213debda 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -1,13 +1,14 @@ /* jslint node: true */ 'use strict'; -const PluginModule = require('./plugin_module.js').PluginModule; -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const ViewController = require('./view_controller.js').ViewController; -const menuUtil = require('./menu_util.js'); -const Config = require('./config.js').config; -const stringFormat = require('../core/string_format.js'); +const PluginModule = require('./plugin_module.js').PluginModule; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const ViewController = require('./view_controller.js').ViewController; +const menuUtil = require('./menu_util.js'); +const Config = require('./config.js').config; +const stringFormat = require('../core/string_format.js'); +const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; // deps const async = require('async'); @@ -183,6 +184,7 @@ MenuModule.prototype.initSequence = function() { self.client.term.write(ansi.goto(self.afterArtPos[0], 1)); // :TODO: really need a client.term.pause() that uses the correct art/etc. + // :TODO: Use MenuModule.pausePrompt() theme.displayThemedPause( { client : self.client }, function keyPressed() { callback(null); }); @@ -389,24 +391,54 @@ MenuModule.prototype.prepViewControllerWithArt = function(name, formId, options, ); }; -MenuModule.prototype.setViewText = function(formName, mciId, text) { +MenuModule.prototype.pausePrompt = function(position, cb) { + if(!cb && _.isFunction(position)) { + cb = position; + position = null; + } + + if(position) { + position.x = position.row || position.x || 1; + position.y = position.col || position.y || 1; + + this.client.term.rawWrite(ansi.goto(position.x, position.y)); + } + + theme.displayThemedPause( { client : this.client }, cb); +}; + +MenuModule.prototype.setViewText = function(formName, mciId, text, appendMultiline) { const view = this.viewControllers[formName].getView(mciId); - if(view) { + if(!view) { + return; + } + + if(appendMultiline && (view instanceof MultiLineEditTextView)) { + view.addText(text); + } else { view.setText(text); } }; -MenuModule.prototype.updateCustomViewTextsWithFilter = function(formName, startId, fmtObj, filter) { +MenuModule.prototype.updateCustomViewTextsWithFilter = function(formName, startId, fmtObj, options) { + options = options || {}; + let textView; let customMciId = startId; const config = this.menuConfig.config; while( (textView = this.viewControllers[formName].getView(customMciId)) ) { - const key = `${formName}InfoFormat${customMciId}`; + const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" const format = config[key]; - if(format && (!filter || filter.find(f => format.indexOf(f) > - 1))) { - textView.setText(stringFormat(format, fmtObj)); + if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { + const text = stringFormat(format, fmtObj); + + if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { + textView.addText(text); + } else { + textView.setText(text); + } } ++customMciId; diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 05ac3586..532c8acf 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -115,8 +115,10 @@ function MultiLineEditTextView(options) { if ('preview' === this.mode) { this.autoScroll = options.autoScroll || true; + this.tabSwitchesView = true; } else { this.autoScroll = options.autoScroll || false; + this.tabSwitchesView = options.tabSwitchesView || false; } // // cursorPos represents zero-based row, col positions @@ -261,30 +263,30 @@ function MultiLineEditTextView(options) { return text; }; - this.getTextLines = function(startIndex, endIndex) { - var lines; + this.getTextLines = function(startIndex, endIndex) { + var lines; if(startIndex === endIndex) { lines = [ self.textLines[startIndex] ]; } else { lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end." } return lines; - }; + }; - this.getOutputText = function(startIndex, endIndex, eolMarker) { - let lines = self.getTextLines(startIndex, endIndex); - let text = ''; - var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); + this.getOutputText = function(startIndex, endIndex, eolMarker) { + let lines = self.getTextLines(startIndex, endIndex); + let text = ''; + var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); - lines.forEach(line => { - text += line.text.replace(re, '\t'); - if(eolMarker && line.eol) { - text += eolMarker; - } - }); + lines.forEach(line => { + text += line.text.replace(re, '\t'); + if(eolMarker && line.eol) { + text += eolMarker; + } + }); - return text; - } + return text; + }; this.getContiguousText = function(startIndex, endIndex, includeEol) { var lines = self.getTextLines(startIndex, endIndex); @@ -532,7 +534,7 @@ function MultiLineEditTextView(options) { // before and and after column // // :TODO: Need to clean this string (e.g. collapse tabs) - text = self.textLines + text = self.textLines; // :TODO: Remove original line @ index } @@ -544,18 +546,18 @@ function MultiLineEditTextView(options) { .replace(/\b/g, '') .split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); - var wrapped; + let wrapped; - for(var i = 0; i < text.length; ++i) { + for(let i = 0; i < text.length; ++i) { wrapped = self.wordWrapSingleLine( text[i], // input 'expand', // tabHandling self.dimens.width).wrapped; - for(var j = 0; j < wrapped.length - 1; ++j) { + for(let j = 0; j < wrapped.length - 1; ++j) { self.textLines.splice(index++, 0, { text : wrapped[j] } ); } - self.textLines.splice(index++, 0, { text : wrapped[wrapped.length - 1], eol : true }); + self.textLines.splice(index++, 0, { text : wrapped[wrapped.length - 1], eol : true } ); } }; @@ -1029,13 +1031,20 @@ MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { this.specialKeyMap.next = [ 'tab' ]; } break; + case 'autoScroll' : this.autoScroll = value; break; + + case 'tabSwitchesView' : + this.tabSwitchesView = value; + this.specialKeyMap.next = this.specialKeyMap.next || []; + this.specialKeyMap.next.push('tab'); + break; } MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); }; -var HANDLED_SPECIAL_KEYS = [ +const HANDLED_SPECIAL_KEYS = [ 'up', 'down', 'left', 'right', 'home', 'end', 'page up', 'page down', @@ -1046,13 +1055,13 @@ var HANDLED_SPECIAL_KEYS = [ 'delete line', ]; -var PREVIEW_MODE_KEYS = [ +const PREVIEW_MODE_KEYS = [ 'up', 'down', 'page up', 'page down' ]; MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { - var self = this; - var handled; + const self = this; + let handled; if(key) { HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { @@ -1062,8 +1071,10 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { return; } - self[_.camelCase('keyPress ' + specialKey)](); - handled = true; + if('tab' !== key.name || !self.tabSwitchesView) { + self[_.camelCase('keyPress ' + specialKey)](); + handled = true; + } } }); } diff --git a/core/string_util.js b/core/string_util.js index 68778533..e8370888 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -13,6 +13,7 @@ exports.stylizeString = stylizeString; exports.pad = pad; exports.replaceAt = replaceAt; exports.isPrintable = isPrintable; +exports.stripAllLineFeeds = stripAllLineFeeds; exports.debugEscapedString = debugEscapedString; exports.stringFromNullTermBuffer = stringFromNullTermBuffer; exports.renderSubstr = renderSubstr; @@ -189,6 +190,10 @@ function stringLength(s) { return s.length; } +function stripAllLineFeeds(s) { + return s.replace(/\r?\n|[\r\u2028\u2029]/g, ''); +} + function debugEscapedString(s) { return JSON.stringify(s).slice(1, -1); } diff --git a/core/text_view.js b/core/text_view.js index 210b05b9..e51309da 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -10,6 +10,7 @@ const stylizeString = require('./string_util.js').stylizeString; const renderSubstr = require('./string_util.js').renderSubstr; const renderStringLength = require('./string_util.js').renderStringLength; const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds; // deps const util = require('util'); @@ -183,7 +184,7 @@ TextView.prototype.setText = function(text, redraw) { text = text.toString(); } - text = pipeToAnsi(text, this.client); // expand MCI/etc. + text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. var widthDelta = 0; if(this.text && this.text !== text) { diff --git a/mods/abracadabra.js b/mods/abracadabra.js index 4596fb4a..08b3f2c2 100644 --- a/mods/abracadabra.js +++ b/mods/abracadabra.js @@ -99,6 +99,7 @@ function AbracadabraModule(options) { if(_.isString(self.config.tooManyArt)) { theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { + // :TODO: Use MenuModule.pausePrompt() theme.displayThemedPause( { client : self.client }, function keyPressed() { callback(new Error('Too many active instances')); }); @@ -106,6 +107,7 @@ function AbracadabraModule(options) { } else { self.client.term.write('\nToo many active instances. Try again later.\n'); + // :TODO: Use MenuModule.pausePrompt() theme.displayThemedPause( { client : self.client }, function keyPressed() { callback(new Error('Too many active instances')); }); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 245941a6..edcd9edb 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -227,25 +227,6 @@ exports.getModule = class FileAreaList extends MenuModule { return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); } -/* - updateCustomLabelsWithFilter(category, startId, filter) { - let textView; - let customMciId = startId; - const config = this.menuConfig.config; - - while( (textView = this.viewControllers[category].getView(customMciId)) ) { - const key = `${category}InfoFormat${customMciId}`; - const format = config[key]; - - if(format && (!filter || filter.find(f => format.indexOf(f) > - 1))) { - textView.setText(stringFormat(format, this.currentFileEntry.entryInfo)); - } - - ++customMciId; - } - } - */ - displayArtAndPrepViewController(name, options, cb) { const self = this; const config = this.menuConfig.config; @@ -447,7 +428,7 @@ exports.getModule = class FileAreaList extends MenuModule { self.updateCustomViewTextsWithFilter( 'browse', MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, - [ '{webDlLink}', '{webDlExpire}' ] + { filter : [ '{webDlLink}', '{webDlExpire}' ] } ); return callback(null); } @@ -472,7 +453,7 @@ exports.getModule = class FileAreaList extends MenuModule { 'browse', MciViewIds.browse.customRangeStart, this.currentFileEntry.entryInfo, - [ '{isQueued}' ] + { filter : [ '{isQueued}' ] } ); } diff --git a/mods/msg_area_list.js b/mods/msg_area_list.js index 85603ef4..c9ad5afd 100644 --- a/mods/msg_area_list.js +++ b/mods/msg_area_list.js @@ -84,6 +84,7 @@ function MessageAreaListModule(options) { if(_.has(area, 'options.pause') && false === area.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { + // :TODO: Use MenuModule.pausePrompt() displayThemedPause( { client : self.client }, () => { return self.prevMenu(cb); }); diff --git a/mods/msg_conf_list.js b/mods/msg_conf_list.js index 1a328430..e813a154 100644 --- a/mods/msg_conf_list.js +++ b/mods/msg_conf_list.js @@ -71,6 +71,7 @@ function MessageConfListModule(options) { if(_.has(conf, 'options.pause') && false === conf.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { + // :TODO: Use MenuModule.pausePrompt() displayThemedPause( { client : self.client }, () => { return self.prevMenu(cb); }); diff --git a/mods/upload.js b/mods/upload.js index bb7f6bfd..914856f1 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -3,20 +3,15 @@ // enigma-bbs const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -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'); const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory; const scanFile = require('../core/file_area.js').scanFile; -const getAreaStorageDirectoryByTag = require('../core/file_area.js').getAreaStorageDirectoryByTag; +const ansiGoto = require('../core/ansi_term.js').goto; // deps const async = require('async'); const _ = require('lodash'); -const paths = require('path'); exports.moduleInfo = { name : 'Upload', @@ -40,8 +35,11 @@ const MciViewIds = { }, processing : { - stepIndicator : 1, - customRangeStart : 10, // 10+ = customs + calcHashIndicator : 1, + archiveListIndicator : 2, + descFileIndicator : 3, + + customRangeStart : 10, // 10+ = customs }, fileDetails : { @@ -160,6 +158,26 @@ exports.getModule = class UploadModule extends MenuModule { const fmtObj = Object.assign( {}, stepInfo); let stepIndicatorFmt = ''; + const indicatorStates = this.menuConfig.config.indicatorStates || [ '|', '/', '-', '\\' ]; + const indicatorFinished = this.menuConfig.config.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' : stepIndicatorFmt = this.menuConfig.config.scanningStartFormat || 'Scanning {fileName}'; @@ -167,28 +185,23 @@ exports.getModule = class UploadModule extends MenuModule { case 'hash_update' : stepIndicatorFmt = this.menuConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; - - this.scanStatus.hashUpdateCount += 1; - fmtObj.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)).toString(); - - if(this.scanStatus.hashUpdateCount % 2) { - fmtObj.calcHashIndicator = this.menuConfig.config.hashUpdateIndicator1Fmt || '-'; - } else { - fmtObj.calcHashIndicator = this.menuConfig.config.hashUpdateIndicator2Fmt || '*'; - } + updateIndicator(MciViewIds.processing.calcHashIndicator); break; case 'hash_finish' : stepIndicatorFmt = this.menuConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; + updateIndicator(MciViewIds.processing.calcHashIndicator, true); break; case 'archive_list_start' : stepIndicatorFmt = this.menuConfig.extractArchiveListFormat || 'Extracting archive list'; + updateIndicator(MciViewIds.processing.archiveListIndicator); break; case 'archive_list_finish' : fmtObj.archivedFileCount = stepInfo.archiveEntries.length; stepIndicatorFmt = this.menuConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; + updateIndicator(MciViewIds.processing.archiveListIndicator, true); break; case 'archive_list_failed' : @@ -197,20 +210,25 @@ exports.getModule = class UploadModule extends MenuModule { case 'desc_files_start' : stepIndicatorFmt = this.menuConfig.processingDescFilesFormat || 'Processing description files'; + updateIndicator(MciViewIds.processing.descFileIndicator); break; case 'desc_files_finish' : stepIndicatorFmt = this.menuConfig.processingDescFilesFinishFormat || 'Finished processing description files'; + updateIndicator(MciViewIds.processing.descFileIndicator, true); break; } - const stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); - + fmtObj.stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); + if(this.hasProcessingArt) { - this.setViewText('processing', MciViewIds.processing.stepIndicator, stepIndicatorText); - this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj); + this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj, { appendMultiLine : true } ); + + if(indicator.mci && indicator.text) { + this.setViewText('processing', indicator.mci, indicator.text); + } } else { - this.client.term.pipeWrite(`${stepIndicatorText}\n`); + this.client.term.pipeWrite(fmtObj.stepIndicatorText); } } @@ -226,7 +244,7 @@ exports.getModule = class UploadModule extends MenuModule { // :TODO: virus scanning/etc. should occur around here self.scanStatus = { - hashUpdateCount : 0, + indicatorPos : 0, }; const scanOpts = { @@ -239,6 +257,8 @@ exports.getModule = class UploadModule extends MenuModule { return nextScanStep(null); } + self.client.log.debug('Scanning upload', { filePath : filePath } ); + scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { if(err) { return nextFilePath(err); @@ -247,6 +267,8 @@ exports.getModule = class UploadModule extends MenuModule { // new or dupe? if(dupeEntries.length > 0) { // 1:n dupes found + self.client.log.debug('Duplicate(s) of upload found', { dupeEntries : dupeEntries } ); + results.dupes = results.dupes.concat(dupeEntries); } else { // new one @@ -271,6 +293,17 @@ exports.getModule = class UploadModule extends MenuModule { 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); diff --git a/package.json b/package.json index 0c4c175e..8fc24eff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.1-alpha", + "version": "0.0.2-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", From 414095a9fd8c917c9aae193fb99274e7722541ed Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 23 Jan 2017 20:21:07 -0700 Subject: [PATCH 26/86] Initial MCI docs --- docs/mci.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/mci.md diff --git a/docs/mci.md b/docs/mci.md new file mode 100644 index 00000000..4da71837 --- /dev/null +++ b/docs/mci.md @@ -0,0 +1,93 @@ +# MCI Codes + +## Introduction +ENiGMA½ supports a variety of MCI codes. Some **predefined** codes produce information about the current user, system, or other statistics while others are used to instanciate a **View**. MCI codes are two characters in length and are prefixed with a percent (%) symbol. Some MCI codes have additional options that may be set directly from the code itself while others -- and more advanced options -- are controlled via the current theme. Standard (non-focus) and focus colors are set by placing duplicate codes back to back in art files. + +## Views +A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is a Vertical Menu (`%VM`): Oldschool BBSers may recognize this as a lightbar menu. + +### Available Views +* Text Label (`%TL`): Displays text +* Edit Text (`%ET`): Collect user input +* Masked Edit Text (`%ME`): Collect user input using a *mask* +* Multi Line Text Edit (`%MT`): Multi line edit control +* Button (`%BT`): A button +* Vertical Menu (`%VM`): A vertical menu aka a vertical lightbar +* Horizontal Menu (`%HM`): A horizontal menu aka a horizontal lightbar +* Spinner Menu (`%SM`): A spinner input control +* Toggle Menu (`%TM`): A toggle menu commonly used for Yes/No style input +* Key Entry (`%KE`): A *single* key input control + +(Peek at `core/mci_view_factory.js` to see additional information on these) + +## Predefined +There are many predefined MCI codes that can be used anywhere on the system (placed in any art file). More are added all the time so also check out `core/predefined_mci.js` for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc. + +* `BN`: Board Name +* `VL`: Version *label*, e.g. "ENiGMA½ v0.0.3-alpha" +* `VN`: Version *number*, eg.. "0.0.3-alpha" +* `SN`: SysOp username +* `SR`: SysOp real name +* `SL`: SysOp location +* `SA`: SysOp affiliations +* `SS`: SysOp sex +* `SE`: SysOp email address +* `UN`: Current user's username +* `UI`: Current user's user ID +* `UG`: Current user's group membership(s) +* `UR`: Current user's real name +* `LO`: Current user's location +* `UA`: Current user's age +* `BD`: Current user's birthdate (using theme date format) +* `US`: Current user's sex +* `UE`: Current user's email address +* `UW`: Current user's web address +* `UF`: Current user's affiliations +* `UT`: Current user's *theme ID* (e.g. "luciano_blocktronics") +* `UC`: Current user's login/call count +* `ND`: Current user's connected node number +* `IP`: Current user's IP address +* `ST`: Current user's connected server name (e.g. "Telnet" or "SSH") +* `FN`: Current user's active file base filter name +* `DN`: Current user's number of downloads +* `DK`: Current user's download amount (formatted to appropriate bytes/megs/etc.) +* `UP`: Current user's number of uploads +* `UK`: Current user's upload amount (formatted to appropriate bytes/megs/etc.) +* `NR`: Current user's upload/download ratio +* `KR`: Current user's upload/download *bytes* ratio +* `MS`: Current user's account creation date (using theme date format) +* `PS`: Current user's post count +* `PC`: Current user's post/call ratio +* `MD`: Current user's status/viewing menu/activity +* `MA`: Current user's active message area name +* `MC`: Current user's active message conference name +* `ML`: Current user's active message area description +* `CM`: Current user's active message conference description +* `SH`: Current user's term height +* `SW`: Current user's term width +* `DT`: Current date (using theme date format) +* `CT`: Current time (using theme time format) +* `OS`: System OS (Linux, Windows, etc.) +* `OA`: System architecture (x86, x86_64, arm, etc.) +* `SC`: System CPU model +* `NV`: System underlying Node.js version +* `AN`: Current active node count +* `TC`: Total login/calls to system +* `RR`: Displays a random rumor + +A special `XY` MCI code may also be utilized for placement identification when creating menus. + +## Properties & Theming +Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. + +### Common Properties +* `textStyle`: Sets the standard (non-focus) text style to `normal` (as-is), `upper` (UPPER), `lower` (lower), `title` (Title Case), `first lower` (fIRST lOWER), `small vowels` (SMaLL VoWeLS), `big vowels` (bIg vOwELS), `small i` (ENiGMA), `mixed` (mIxED CAsE), or `l33t` (l337 5p34k) +* `focusTextStyle`: Sets focus text style to `normal` (as-is), `upper` (UPPER), `lower` (lower), `title` (Title Case), `first lower` (fIRST lOWER), `small vowels` (SMaLL VoWeLS), `big vowels` (bIg vOwELS), `small i` (ENiGMA), `mixed` (mIxED CAsE), or `l33t` (l337 5p34k) +* `itemSpacing`: Used to separate items in menus such as Vertical Menu and Horizontal Menu Views. +* `height`: Sets the height of views such as menus that may be > 1 character in height +* `width`: Sets the width of a view +* `focus`: If set to `true`, establishes initial focus +* `text`: (initial) text of a view +* `submit`: If set to `true` any `accept` action upon this view will submit the encompassing **form** + +These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files! \ No newline at end of file From 1c03c3021a2ad3c327fcde7efebb039fb55b5374 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 23 Jan 2017 23:32:40 -0700 Subject: [PATCH 27/86] * Temporary fix for MAJOR global temp cleanup bug: comment out node-temp .cleanup() methods * Don't move temp uploads to area directory until user submit/completed * New file util module --- core/file_area.js | 6 +- core/file_util.js | 62 ++++++++++++++ core/menu_module.js | 6 -- core/predefined_mci.js | 1 - core/scanner_tossers/ftn_bso.js | 6 ++ core/servers/login/telnet.js | 2 +- core/transfer_file.js | 143 ++++++++++++-------------------- mods/upload.js | 138 +++++++++++++++++++++--------- 8 files changed, 226 insertions(+), 138 deletions(-) create mode 100644 core/file_util.js diff --git a/core/file_area.js b/core/file_area.js index 637847d6..99e4a80f 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -339,9 +339,13 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c }); }, () => { // cleanup, but don't wait... + /* + :TODO: fix global temp cleanup issue!!! + temp.cleanup( err => { // :TODO: Log me! - }); + });*/ + return callback(null); }); }, diff --git a/core/file_util.js b/core/file_util.js new file mode 100644 index 00000000..e2ea6e90 --- /dev/null +++ b/core/file_util.js @@ -0,0 +1,62 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ + +// deps +const fse = require('fs-extra'); +const paths = require('path'); +const async = require('async'); + +exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; +exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; + +// +// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. +// in the case of collisions. +// +function moveFileWithCollisionHandling(src, dst, cb) { + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); + const dstFileSuffix = paths.basename(dst, dstFileExt); + + let renameIndex = 0; + let movedOk = false; + let tryDstPath; + + async.until( + () => movedOk, // until moved OK + (cb) => { + if(0 === renameIndex) { + // try originally supplied path first + tryDstPath = dst; + } else { + tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); + } + + fse.move(src, tryDstPath, err => { + if(err) { + if('EEXIST' === err.code) { + renameIndex += 1; + return cb(null); // keep trying + } + + return cb(err); + } + + movedOk = true; + return cb(null, tryDstPath); + }); + }, + (err, finalPath) => { + return cb(err, finalPath); + } + ); +} + +function pathWithTerminatingSeparator(path) { + if(path && paths.sep !== path.charAt(path.length - 1)) { + path = path + paths.sep; + } + return path; +} diff --git a/core/menu_module.js b/core/menu_module.js index 213debda..6cc81e4e 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -95,12 +95,6 @@ require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype); MenuModule.prototype.enter = function() { - if(_.isString(this.menuConfig.desc)) { - this.client.currentStatus = this.menuConfig.desc; - } else { - this.client.currentStatus = 'Browsing menus'; - } - this.initSequence(); }; diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 5ed828cc..69463f4d 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -115,7 +115,6 @@ function getPredefinedMCIValue(client, code) { }, MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, - CS : function currentStatus() { return client.currentStatus; }, PS : function userPostCount() { return userStatAsString(client, 'post_count', 0); }, PC : function userPostCallRatio() { return getRatio(client, 'post_count', 'login_count'); }, diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 8cd6e1a0..85fad2f7 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1290,6 +1290,9 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { // // Clean up temp dir/files we created // + /* + :TODO: fix global temp cleanup issue!!! + temp.cleanup((err, stats) => { const fullStats = Object.assign(stats, { exportTemp : this.exportTempDir, importTemp : this.importTempDir } ); @@ -1301,6 +1304,9 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }); + */ + + FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }; FTNMessageScanTossModule.prototype.performImport = function(cb) { diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 045c9280..2e27a6f8 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -438,7 +438,7 @@ function TelnetClient(input, output) { newEnvironRequested : false, }; - this.setTemporaryDataHandler = function(handler) { + this.setTemporaryDirectDataHandler = function(handler) { this.input.removeAllListeners('data'); this.input.on('data', handler); }; diff --git a/core/transfer_file.js b/core/transfer_file.js index b0ee78d7..28022c06 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -19,6 +19,10 @@ const paths = require('path'); const fs = require('fs'); const fse = require('fs-extra'); +// some consts +const SYSTEM_EOL = require('os').EOL; +const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. + /* Resources @@ -195,7 +199,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } recvFiles(cb) { - this.executeExternalProtocolHandlerForRecv( (err, tempWorkingDir) => { + this.executeExternalProtocolHandlerForRecv(err => { if(err) { return cb(err); } @@ -203,42 +207,39 @@ exports.getModule = class TransferFileModule extends MenuModule { this.recvFilePaths = []; if(this.recvFileName) { - // file name specified - we expect a single file in |tempWorkingDir| + // file name specified - we expect a single file in |this.recvDirectory| // :TODO: support non-blind: Move file to dest path, add to recvFilePaths, etc. return cb(null); } else { // - // blind recv (upload) - files in |tempWorkingDir| should be named appropriately already - // move files to |this.recvDirectory| + // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already // - fs.readdir(tempWorkingDir, (err, files) => { + fs.readdir(this.recvDirectory, (err, files) => { if(err) { return cb(err); } - async.each(files, (file, nextFile) => { - this.moveFileWithCollisionHandling( - paths.join(tempWorkingDir, file), - paths.join(this.recvDirectory, file), - (err, destPath) => { - if(err) { - this.client.log.warn( - { tempWorkingDir : tempWorkingDir, recvDirectory : this.recvDirectory, file : file, error : err.message }, - 'Failed to move upload file to destination directory' - ); - } else { - this.recvFilePaths.push(destPath); - } + // stat each to grab files only + async.each(files, (fileName, nextFile) => { + const recvFullPath = paths.join(this.recvDirectory, fileName); - return nextFile(null); // don't pass along err; try next + fs.stat(recvFullPath, (err, stats) => { + if(err) { + this.client.log.warn('Failed to stat file', { path : recvFullPath } ); + return nextFile(null); // just try the next one } - ); + + if(stats.isFile()) { + this.recvFilePaths.push(recvFullPath); + } + + return nextFile(null); + }); }, () => { return cb(null); - }); - + }); }); } }); @@ -254,36 +255,25 @@ exports.getModule = class TransferFileModule extends MenuModule { prepAndBuildSendArgs(filePaths, cb) { const external = this.protocolConfig.external; const externalArgs = external[`${this.direction}Args`]; - const self = this; - let tempWorkingDir; async.waterfall( [ function getTempFileListPath(callback) { const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); if(!hasFileList) { - temp.mkdir('enigdl-', (err, tempDir) => { - if(err) { - return callback(err); - } - - tempWorkingDir = self.pathWithTerminatingSeparator(tempDir); - return callback(null, null); - }); - } else { - temp.open( { prefix : 'enigdl-', suffix : '.txt' }, (err, tempFileInfo) => { - if(err) { - return callback(err); // failed to create it - } - - tempWorkingDir = self.pathWithTerminatingSeparator(paths.dirname(tempFileInfo.path)); - - fs.write(tempFileInfo.fd, filePaths.join('\n')); - fs.close(tempFileInfo.fd, err => { - return callback(err, tempFileInfo.path); - }); - }); + return callback(null, null); } + + temp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { + if(err) { + return callback(err); // failed to create it + } + + fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); + fs.close(tempFileInfo.fd, err => { + return callback(err, tempFileInfo.path); + }); + }); }, function createArgs(tempFileListPath, callback) { // initial args: ignore {filePaths} as we must break that into it's own sep array items @@ -303,54 +293,37 @@ exports.getModule = class TransferFileModule extends MenuModule { } ], (err, args) => { - return cb(err, args, tempWorkingDir); + return cb(err, args); } ); } prepAndBuildRecvArgs(cb) { - const self = this; + const externalArgs = this.protocolConfig.external[`${this.direction}Args`]; + const args = externalArgs.map(arg => stringFormat(arg, { + uploadDir : this.recvDirectory, + fileName : this.recvFileName || '', + })); - async.waterfall( - [ - function getTempRecvPath(callback) { - temp.mkdir('enigrcv-', (err, tempWorkingDir) => { - tempWorkingDir = self.pathWithTerminatingSeparator(tempWorkingDir); - return callback(err, tempWorkingDir); - }); - }, - function createArgs(tempWorkingDir, callback) { - const externalArgs = self.protocolConfig.external[`${self.direction}Args`]; - const args = externalArgs.map(arg => stringFormat(arg, { - uploadDir : tempWorkingDir, - fileName : self.recvFileName || '', - })); - - return callback(null, args, tempWorkingDir); - } - ], - (err, args, tempWorkingDir) => { - return cb(err, args, tempWorkingDir); - } - ); + return cb(null, args); } - executeExternalProtocolHandler(args, tempWorkingDir, cb) { + executeExternalProtocolHandler(args, cb) { const external = this.protocolConfig.external; const cmd = external[`${this.direction}Cmd`]; this.client.log.debug( - { cmd : cmd, args : args, tempDir : tempWorkingDir, direction : this.direction }, + { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, 'Executing external protocol' ); const externalProc = pty.spawn(cmd, args, { cols : this.client.term.termWidth, rows : this.client.term.termHeight, - cwd : tempWorkingDir, + cwd : this.recvDirectory, }); - this.client.setTemporaryDataHandler(data => { + this.client.setTemporaryDirectDataHandler(data => { // needed for things like sz/rz if(external.escapeTelnet) { const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape @@ -360,8 +333,6 @@ exports.getModule = class TransferFileModule extends MenuModule { } }); - //this.client.term.output.pipe(externalProc); - externalProc.on('data', data => { // needed for things like sz/rz if(external.escapeTelnet) { @@ -391,25 +362,25 @@ exports.getModule = class TransferFileModule extends MenuModule { filePaths = [ filePaths ]; } - this.prepAndBuildSendArgs(filePaths, (err, args, tempWorkingDir) => { + this.prepAndBuildSendArgs(filePaths, (err, args) => { if(err) { return cb(err); } - this.executeExternalProtocolHandler(args, tempWorkingDir, err => { + this.executeExternalProtocolHandler(args, err => { return cb(err); }); }); } executeExternalProtocolHandlerForRecv(cb) { - this.prepAndBuildRecvArgs( (err, args, tempWorkingDir) => { + this.prepAndBuildRecvArgs( (err, args) => { if(err) { return cb(err); } - this.executeExternalProtocolHandler(args, tempWorkingDir, err => { - return cb(err, tempWorkingDir); + this.executeExternalProtocolHandler(args, err => { + return cb(err); }); }); } @@ -541,12 +512,15 @@ exports.getModule = class TransferFileModule extends MenuModule { } }, function cleanupTempFiles(callback) { + /* :TODO: figure out the global temp cleanup() issue!!@! temp.cleanup( err => { if(err) { self.client.log.warn( { error : err.message }, 'Failed to clean up temporary file/directory(s)' ); } return callback(null); // ignore err }); + */ + return callback(null); }, function updateUserAndSystemStats(callback) { if(self.isSending()) { @@ -562,15 +536,6 @@ exports.getModule = class TransferFileModule extends MenuModule { } return self.prevMenu(); - /* - - // Wait for a key press - attempt to avoid issues with some terminals after xfer - // :TODO: display ANSI if it exists else prompt -- look @ Obv/2 for filename - self.client.term.pipeWrite('|00|07\nTransfer(s) complete. Press a key\n'); - self.client.waitForKeyPress( () => { - return self.prevMenu(); - }); - */ } ); } diff --git a/mods/upload.js b/mods/upload.js index 914856f1..c7f491e1 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -8,10 +8,14 @@ const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAv const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory; const scanFile = require('../core/file_area.js').scanFile; const ansiGoto = require('../core/ansi_term.js').goto; +const moveFileWithCollisionHandling = require('../core/file_util.js').moveFileWithCollisionHandling; +const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTerminatingSeparator; // deps const async = require('async'); const _ = require('lodash'); +const temp = require('temp').track(); // track() cleans up temp dir/files for us +const paths = require('path'); exports.moduleInfo = { name : 'Upload', @@ -65,21 +69,12 @@ exports.getModule = class UploadModule extends MenuModule { this.menuMethods = { optionsNavContinue : (formData, extraArgs, cb) => { if(this.isBlindUpload()) { - // jump to protocol selection - const areaUploadDir = this.getSelectedAreaUploadDirectory(); - - const modOpts = { - extraArgs : { - recvDirectory : areaUploadDir, - direction : 'recv', - } - }; - - return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); - } else { - // jump to fileDetails form - // :TODO: support non-blind: collect info/filename -> upload -> complete + return this.performBlindUpload(cb); } + + // non-blind + // jump to fileDetails form + // :TODO: support non-blind: collect info/filename -> upload -> complete }, fileDetailsContinue : (formData, extraArgs, cb) => { @@ -94,12 +89,13 @@ exports.getModule = class UploadModule extends MenuModule { getSaveState() { const saveState = { - uploadType : this.uploadType, - + uploadType : this.uploadType, + tempRecvDirectory : this.tempRecvDirectory }; if(this.isBlindUpload()) { - saveState.areaInfo = this.getSelectedAreaInfo(); + const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); + saveState.areaInfo = this.availAreas[areaSelectView.getData()]; } return saveState; @@ -107,20 +103,11 @@ exports.getModule = class UploadModule extends MenuModule { restoreSavedState(savedState) { if(savedState.areaInfo) { - this.areaInfo = savedState.areaInfo; + this.areaInfo = savedState.areaInfo; + this.tempRecvDirectory = savedState.tempRecvDirectory; } } - getSelectedAreaInfo() { - const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); - return this.availAreas[areaSelectView.getData()]; - } - - getSelectedAreaUploadDirectory() { - const areaInfo = this.getSelectedAreaInfo(); - return getAreaDefaultStorageDirectory(areaInfo); - } - isBlindUpload() { return 'blind' === this.uploadType; } isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); } @@ -152,6 +139,44 @@ exports.getModule = class UploadModule extends MenuModule { } } + leave() { + // remove any temp files - only do this when + if(this.isFileTransferComplete()) { + // :TODO: fix global temp cleanup issue!!! + //temp.cleanup(); // remove any temp files + } + + super.leave(); + } + + performBlindUpload(cb) { + temp.mkdir('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', + } + }; + + // + // 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 + ); + }); + } + updateScanStepInfoViews(stepInfo) { // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC @@ -240,6 +265,8 @@ exports.getModule = class UploadModule extends MenuModule { dupes : [], }; + self.client.log.debug('Scanning upload(s)', { paths : this.recvFilePaths } ); + async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => { // :TODO: virus scanning/etc. should occur around here @@ -257,7 +284,7 @@ exports.getModule = class UploadModule extends MenuModule { return nextScanStep(null); } - self.client.log.debug('Scanning upload', { filePath : filePath } ); + self.client.log.debug('Scanning file', { filePath : filePath } ); scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { if(err) { @@ -267,7 +294,7 @@ exports.getModule = class UploadModule extends MenuModule { // new or dupe? if(dupeEntries.length > 0) { // 1:n dupes found - self.client.log.debug('Duplicate(s) of upload found', { dupeEntries : dupeEntries } ); + self.client.log.debug('Duplicate file(s) found', { dupeEntries : dupeEntries } ); results.dupes = results.dupes.concat(dupeEntries); } else { @@ -282,6 +309,38 @@ exports.getModule = class UploadModule extends MenuModule { }); } + 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 + }); + }); + }); + } + processUploadedFiles() { // // For each file uploaded, we need to process & gather information @@ -339,16 +398,15 @@ exports.getModule = class UploadModule extends MenuModule { return callback(err, scanResults); }); }, - function persistNewEntries(scanResults, callback) { - // loop over entries again & persist to DB - async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { - newEntry.persist(err => { - return nextEntry(err); - }); - }, err => { - return callback(err); - }); - } + 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) { From 99ab60bf771a8738e9ddd5dcd5c8fe458b24533d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 25 Jan 2017 22:18:05 -0700 Subject: [PATCH 28/86] * Convert MenuModule to ES6 style class * Convert modules that are MenuModule subclasses to ES6 style classes * Convert mixins to ES6 style * Various cleanup --- core/door_party.js | 31 +- core/enig_error.js | 1 + core/fse.js | 723 ++++++++++++++--------------- core/menu_module.js | 721 ++++++++++++++-------------- core/mod_mixins.js | 41 +- core/new_scan.js | 193 ++++---- core/standard_menu.js | 39 +- core/user_config.js | 361 +++++++------- mods/abracadabra.js | 101 ++-- mods/art_pool.js | 33 -- mods/bbs_link.js | 42 +- mods/bbs_list.js | 278 ++++++----- mods/erc_client.js | 5 +- mods/file_base_download_manager.js | 3 - mods/last_callers.js | 185 ++++---- mods/msg_area_list.js | 243 +++++----- mods/msg_area_post_fse.js | 105 ++--- mods/msg_area_view_fse.js | 191 ++++---- mods/msg_conf_list.js | 237 +++++----- mods/msg_list.js | 398 ++++++++-------- mods/nua.js | 209 +++++---- mods/onelinerz.js | 182 ++++---- mods/telnet_bridge.js | 27 +- mods/user_list.js | 165 ++++--- mods/whos_online.js | 113 ++--- package.json | 5 +- 26 files changed, 2214 insertions(+), 2418 deletions(-) delete mode 100644 mods/art_pool.js diff --git a/core/door_party.js b/core/door_party.js index d782c07e..1766f5ff 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -10,28 +10,26 @@ const async = require('async'); const _ = require('lodash'); const SSHClient = require('ssh2').Client; -exports.getModule = DoorPartyModule; - exports.moduleInfo = { name : 'DoorParty', desc : 'DoorParty Access Module', author : 'NuSkooler', }; +exports.getModule = class DoorPartyModule extends MenuModule { + constructor(options) { + super(options); -function DoorPartyModule(options) { - MenuModule.call(this, options); + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'dp.throwbackbbs.com'; + this.config.sshPort = this.config.sshPort || 2022; + this.config.rloginPort = this.config.rloginPort || 513; + } - const self = this; - - // establish defaults - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'dp.throwbackbbs.com'; - this.config.sshPort = this.config.sshPort || 2022; - this.config.rloginPort = this.config.rloginPort || 513; - - this.initSequence = function() { + initSequence() { let clientTerminated; + const self = this; async.series( [ @@ -125,8 +123,5 @@ function DoorPartyModule(options) { } } ); - }; - -} - -require('util').inherits(DoorPartyModule, MenuModule); \ No newline at end of file + } +}; diff --git a/core/enig_error.js b/core/enig_error.js index 98e3cb50..0378ee00 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -28,4 +28,5 @@ exports.Errors = { AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), + MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), }; diff --git a/core/fse.js b/core/fse.js index 414c10ef..d7da12ed 100644 --- a/core/fse.js +++ b/core/fse.js @@ -12,6 +12,7 @@ const getUserIdAndName = require('./user.js').getUserIdAndName; const cleanControlCodes = require('./string_util.js').cleanControlCodes; const StatLog = require('./stat_log.js'); const stringFormat = require('./string_format.js'); +const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; // deps const async = require('async'); @@ -19,12 +20,6 @@ const assert = require('assert'); const _ = require('lodash'); const moment = require('moment'); -exports.FullScreenEditorModule = FullScreenEditorModule; - -// :TODO: clean this up: - -exports.getModule = FullScreenEditorModule; - exports.moduleInfo = { name : 'Full Screen Editor (FSE)', desc : 'A full screen editor/viewer', @@ -65,7 +60,7 @@ exports.moduleInfo = { */ -var MCICodeIds = { +const MciCodeIds = { ViewModeHeader : { From : 1, To : 2, @@ -97,72 +92,192 @@ var MCICodeIds = { }, }; -function FullScreenEditorModule(options) { - MenuModule.call(this, options); +// :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives - var self = this; - var config = this.menuConfig.config; +exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) { - // - // menuConfig.config: - // editorType : email | area - // editorMode : view | edit | quote - // - // menuConfig.config or extraArgs - // messageAreaTag - // messageIndex / messageTotal - // toUserId - // - this.editorType = config.editorType; - this.editorMode = config.editorMode; - - if(config.messageAreaTag) { - this.messageAreaTag = config.messageAreaTag; - } - - this.messageIndex = config.messageIndex || 0; - this.messageTotal = config.messageTotal || 0; - this.toUserId = config.toUserId || 0; + constructor(options) { + super(options); - // extraArgs can override some config - if(_.isObject(options.extraArgs)) { - if(options.extraArgs.messageAreaTag) { - this.messageAreaTag = options.extraArgs.messageAreaTag; + const self = this; + const config = this.menuConfig.config; + + // + // menuConfig.config: + // editorType : email | area + // editorMode : view | edit | quote + // + // menuConfig.config or extraArgs + // messageAreaTag + // messageIndex / messageTotal + // toUserId + // + this.editorType = config.editorType; + this.editorMode = config.editorMode; + + if(config.messageAreaTag) { + this.messageAreaTag = config.messageAreaTag; } - if(options.extraArgs.messageIndex) { - this.messageIndex = options.extraArgs.messageIndex; + + this.messageIndex = config.messageIndex || 0; + this.messageTotal = config.messageTotal || 0; + this.toUserId = config.toUserId || 0; + + // extraArgs can override some config + if(_.isObject(options.extraArgs)) { + if(options.extraArgs.messageAreaTag) { + this.messageAreaTag = options.extraArgs.messageAreaTag; + } + if(options.extraArgs.messageIndex) { + this.messageIndex = options.extraArgs.messageIndex; + } + if(options.extraArgs.messageTotal) { + this.messageTotal = options.extraArgs.messageTotal; + } + if(options.extraArgs.toUserId) { + this.toUserId = options.extraArgs.toUserId; + } } - if(options.extraArgs.messageTotal) { - this.messageTotal = options.extraArgs.messageTotal; - } - if(options.extraArgs.toUserId) { - this.toUserId = options.extraArgs.toUserId; + + this.isReady = false; + + if(_.has(options, 'extraArgs.message')) { + this.setMessage(options.extraArgs.message); + } else if(_.has(options, 'extraArgs.replyToMessage')) { + this.replyToMessage = options.extraArgs.replyToMessage; } + + this.menuMethods = { + // + // Validation stuff + // + viewValidationListener : function(err, cb) { + var errMsgView = self.viewControllers.header.getView(MciCodeIds.ReplyEditModeHeader.ErrorMsg); + var newFocusViewId; + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); + + if(MciCodeIds.ViewModeHeader.Subject === err.view.getId()) { + // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) + } + } else { + errMsgView.clearText(); + } + } + cb(newFocusViewId); + }, + + headerSubmit : function(formData, extraArgs, cb) { + self.switchToBody(); + return cb(null); + }, + editModeEscPressed : function(formData, extraArgs, cb) { + self.footerMode = 'editor' === self.footerMode ? 'editorMenu' : 'editor'; + + self.switchFooter(function next(err) { + if(err) { + // :TODO:... what now? + console.log(err) + } else { + switch(self.footerMode) { + case 'editor' : + if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { + //self.viewControllers.footerEditorMenu.setFocus(false); + self.viewControllers.footerEditorMenu.detachClientEvents(); + } + self.viewControllers.body.switchFocus(1); + self.observeEditorEvents(); + break; + + case 'editorMenu' : + self.viewControllers.body.setFocus(false); + self.viewControllers.footerEditorMenu.switchFocus(1); + break; + + default : throw new Error('Unexpected mode'); + } + } + + return cb(null); + }); + }, + editModeMenuQuote : function(formData, extraArgs, cb) { + self.viewControllers.footerEditorMenu.setFocus(false); + self.displayQuoteBuilder(); + return cb(null); + }, + appendQuoteEntry: function(formData, extraArgs, cb) { + // :TODO: Dont' use magic # ID's here + var quoteMsgView = self.viewControllers.quoteBuilder.getView(1); + + if(self.newQuoteBlock) { + self.newQuoteBlock = false; + quoteMsgView.addText(self.getQuoteByHeader()); + } + + var quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote); + quoteMsgView.addText(quoteText); + + // + // If this is *not* the last item, advance. Otherwise, do nothing as we + // don't want to jump back to the top and repeat already quoted lines + // + var quoteListView = self.viewControllers.quoteBuilder.getView(3); + if(quoteListView.getData() !== quoteListView.getCount() - 1) { + quoteListView.focusNext(); + } else { + self.quoteBuilderFinalize(); + } + + return cb(null); + }, + quoteBuilderEscPressed : function(formData, extraArgs, cb) { + self.quoteBuilderFinalize(); + return cb(null); + }, + /* + replyDiscard : function(formData, extraArgs) { + // :TODO: need to prompt yes/no + // :TODO: @method for fallback would be better + self.prevMenu(); + }, + */ + editModeMenuHelp : function(formData, extraArgs, cb) { + self.viewControllers.footerEditorMenu.setFocus(false); + return self.displayHelp(cb); + }, + /////////////////////////////////////////////////////////////////////// + // View Mode + /////////////////////////////////////////////////////////////////////// + viewModeMenuHelp : function(formData, extraArgs, cb) { + self.viewControllers.footerView.setFocus(false); + return self.displayHelp(cb); + } + }; } - this.isReady = false; + isEditMode() { + return 'edit' === this.editorMode; + } - this.isEditMode = function() { - return 'edit' === self.editorMode; - }; - - this.isViewMode = function() { - return 'view' === self.editorMode; - }; + isViewMode() { + return 'view' === this.editorMode; + } - this.isLocalEmail = function() { - return Message.WellKnownAreaTags.Private === self.messageAreaTag; - }; + isLocalEmail() { + return Message.WellKnownAreaTags.Private === this.messageAreaTag; + } - this.isReply = function() { - return !_.isUndefined(self.replyToMessage); - }; + isReply() { + return !_.isUndefined(this.replyToMessage); + } - this.getFooterName = function() { - return 'footer' + _.capitalize(self.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... - }; + getFooterName() { + return 'footer' + _.capitalize(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... + } - this.getFormId = function(name) { + getFormId(name) { return { header : 0, body : 1, @@ -173,27 +288,13 @@ function FullScreenEditorModule(options) { help : 50, }[name]; - }; - - /*ViewModeHeader : { - From : 1, - To : 2, - Subject : 3, - - DateTime : 5, - MsgNum : 6, - MsgTotal : 7, - ViewCount : 8, - HashTags : 9, - MessageID : 10, - ReplyToMsgID : 11 - },*/ + } // :TODO: convert to something like this for all view acces: - this.getHeaderViews = function() { - var vc = self.viewControllers.header; + getHeaderViews() { + var vc = this.viewControllers.header; - if(self.isViewMode()) { + if(this.isViewMode()) { return { from : vc.getView(1), to : vc.getView(2), @@ -205,61 +306,55 @@ function FullScreenEditorModule(options) { }; } - }; + } - this.setInitialFooterMode = function() { - switch(self.editorMode) { - case 'edit' : self.footerMode = 'editor'; break; - case 'view' : self.footerMode = 'view'; break; + setInitialFooterMode() { + switch(this.editorMode) { + case 'edit' : this.footerMode = 'editor'; break; + case 'view' : this.footerMode = 'view'; break; } - }; + } - this.buildMessage = function() { - var headerValues = self.viewControllers.header.getFormData().value; + buildMessage() { + var headerValues = this.viewControllers.header.getFormData().value; var msgOpts = { - areaTag : self.messageAreaTag, + areaTag : this.messageAreaTag, toUserName : headerValues.to, fromUserName : headerValues.from, subject : headerValues.subject, - message : self.viewControllers.body.getFormData().value.message, + message : this.viewControllers.body.getFormData().value.message, }; - if(self.isReply()) { - msgOpts.replyToMsgId = self.replyToMessage.messageId; + if(this.isReply()) { + msgOpts.replyToMsgId = this.replyToMessage.messageId; } - self.message = new Message(msgOpts); - }; + this.message = new Message(msgOpts); + } - /* - this.setBodyMessageViewText = function() { - self.bodyMessageView.setText(cleanControlCodes(self.message.message)); - }; - */ - - this.setMessage = function(message) { - self.message = message; + setMessage(message) { + this.message = message; updateMessageAreaLastReadId( - self.client.user.userId, self.messageAreaTag, self.message.messageId, () => { + this.client.user.userId, this.messageAreaTag, this.message.messageId, () => { - if(self.isReady) { - self.initHeaderViewMode(); - self.initFooterViewMode(); + if(this.isReady) { + this.initHeaderViewMode(); + this.initFooterViewMode(); - var bodyMessageView = self.viewControllers.body.getView(1); - if(bodyMessageView && _.has(self, 'message.message')) { - //self.setBodyMessageViewText(); - bodyMessageView.setText(cleanControlCodes(self.message.message)); - //bodyMessageView.redraw(); + var bodyMessageView = this.viewControllers.body.getView(1); + if(bodyMessageView && _.has(this, 'message.message')) { + bodyMessageView.setText(cleanControlCodes(this.message.message)); } } } ); - }; + } + + getMessage(cb) { + const self = this; - this.getMessage = function(cb) { async.series( [ function buildIfNecessary(callback) { @@ -295,24 +390,22 @@ function FullScreenEditorModule(options) { cb(err, self.message); } ); - }; + } - this.updateUserStats = function(cb) { + updateUserStats(cb) { if(Message.isPrivateAreaTag(this.message.areaTag)) { if(cb) { - return cb(null); + cb(null); } + return; // don't inc stats for private messages } - StatLog.incrementUserStat( - self.client.user, - 'post_count', - 1, - cb - ); - }; + return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); + } + + redrawFooter(options, cb) { + const self = this; - this.redrawFooter = function(options, cb) { async.waterfall( [ function moveToFooterPosition(callback) { @@ -354,10 +447,11 @@ function FullScreenEditorModule(options) { cb(err, artData); } ); - }; + } - this.redrawScreen = function(cb) { + redrawScreen(cb) { var comps = [ 'header', 'body' ]; + const self = this; var art = self.menuConfig.config.art; self.client.term.rawWrite(ansi.resetScreen()); @@ -398,43 +492,44 @@ function FullScreenEditorModule(options) { cb(err); } ); - }; + } + switchFooter(cb) { + var footerName = this.getFooterName(); - this.switchFooter = function(cb) { - var footerName = self.getFooterName(); - - self.redrawFooter( { footerName : footerName, clear : true }, function artDisplayed(err, artData) { + this.redrawFooter( { footerName : footerName, clear : true }, (err, artData) => { if(err) { cb(err); return; } - var formId = self.getFormId(footerName); + var formId = this.getFormId(footerName); - if(_.isUndefined(self.viewControllers[footerName])) { + if(_.isUndefined(this.viewControllers[footerName])) { var menuLoadOpts = { - callingMenu : self, + callingMenu : this, formId : formId, mciMap : artData.mciMap }; - self.addViewController( + this.addViewController( footerName, - new ViewController( { client : self.client, formId : formId } ) - ).loadFromMenuConfig(menuLoadOpts, function footerReady(err) { + new ViewController( { client : this.client, formId : formId } ) + ).loadFromMenuConfig(menuLoadOpts, err => { cb(err); }); } else { - self.viewControllers[footerName].redrawAll(); + this.viewControllers[footerName].redrawAll(); cb(null); } }); - }; + } - this.initSequence = function() { + initSequence() { var mciData = { }; + const self = this; var art = self.menuConfig.config.art; + assert(_.isObject(art)); async.series( @@ -488,10 +583,10 @@ function FullScreenEditorModule(options) { } } ); - }; + } - this.createInitialViews = function(mciData, cb) { - + createInitialViews(mciData, cb) { + const self = this; var menuLoadOpts = { callingMenu : self }; async.series( @@ -596,11 +691,11 @@ function FullScreenEditorModule(options) { cb(err); } ); - }; + } - this.mciReadyHandler = function(mciData, cb) { + mciReadyHandler(mciData, cb) { - self.createInitialViews(mciData, function viewsCreated(err) { + this.createInitialViews(mciData, err => { // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in // place - if this is for existing usernames else validate spec @@ -620,103 +715,94 @@ function FullScreenEditorModule(options) { cb(err); }); - }; + } - this.updateEditModePosition = function(pos) { - if(self.isEditMode()) { - var posView = self.viewControllers.footerEditor.getView(1); + updateEditModePosition(pos) { + if(this.isEditMode()) { + var posView = this.viewControllers.footerEditor.getView(1); if(posView) { - self.client.term.rawWrite(ansi.savePos()); + this.client.term.rawWrite(ansi.savePos()); + // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat posView.setText(_.padLeft(String(pos.row + 1), 2, '0') + ',' + _.padLeft(String(pos.col + 1), 2, '0')); - self.client.term.rawWrite(ansi.restorePos()); + this.client.term.rawWrite(ansi.restorePos()); } } - }; + } - this.updateTextEditMode = function(mode) { - if(self.isEditMode()) { - var modeView = self.viewControllers.footerEditor.getView(2); + updateTextEditMode(mode) { + if(this.isEditMode()) { + var modeView = this.viewControllers.footerEditor.getView(2); if(modeView) { - self.client.term.rawWrite(ansi.savePos()); + this.client.term.rawWrite(ansi.savePos()); modeView.setText('insert' === mode ? 'INS' : 'OVR'); - self.client.term.rawWrite(ansi.restorePos()); + this.client.term.rawWrite(ansi.restorePos()); } } - }; + } - this.setHeaderText = function(id, text) { - var v = self.viewControllers.header.getView(id); - if(v) { - v.setText(text); - } - }; + setHeaderText(id, text) { + this.setViewText('header', id, text); + } - this.initHeaderViewMode = function() { - assert(_.isObject(self.message)); + initHeaderViewMode() { + assert(_.isObject(this.message)); - self.setHeaderText(MCICodeIds.ViewModeHeader.From, self.message.fromUserName); - self.setHeaderText(MCICodeIds.ViewModeHeader.To, self.message.toUserName); - self.setHeaderText(MCICodeIds.ViewModeHeader.Subject, self.message.subject); - self.setHeaderText(MCICodeIds.ViewModeHeader.DateTime, moment(self.message.modTimestamp).format(self.client.currentTheme.helpers.getDateTimeFormat())); - self.setHeaderText(MCICodeIds.ViewModeHeader.MsgNum, (self.messageIndex + 1).toString()); - self.setHeaderText(MCICodeIds.ViewModeHeader.MsgTotal, self.messageTotal.toString()); - self.setHeaderText(MCICodeIds.ViewModeHeader.ViewCount, self.message.viewCount); - self.setHeaderText(MCICodeIds.ViewModeHeader.HashTags, 'TODO hash tags'); - self.setHeaderText(MCICodeIds.ViewModeHeader.MessageID, self.message.messageId); - self.setHeaderText(MCICodeIds.ViewModeHeader.ReplyToMsgID, self.message.replyToMessageId); - }; + this.setHeaderText(MciCodeIds.ViewModeHeader.From, this.message.fromUserName); + this.setHeaderText(MciCodeIds.ViewModeHeader.To, this.message.toUserName); + this.setHeaderText(MciCodeIds.ViewModeHeader.Subject, this.message.subject); + this.setHeaderText(MciCodeIds.ViewModeHeader.DateTime, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); + this.setHeaderText(MciCodeIds.ViewModeHeader.MsgNum, (this.messageIndex + 1).toString()); + this.setHeaderText(MciCodeIds.ViewModeHeader.MsgTotal, this.messageTotal.toString()); + this.setHeaderText(MciCodeIds.ViewModeHeader.ViewCount, this.message.viewCount); + this.setHeaderText(MciCodeIds.ViewModeHeader.HashTags, 'TODO hash tags'); + this.setHeaderText(MciCodeIds.ViewModeHeader.MessageID, this.message.messageId); + this.setHeaderText(MciCodeIds.ViewModeHeader.ReplyToMsgID, this.message.replyToMessageId); + } - this.initHeaderReplyEditMode = function() { - assert(_.isObject(self.replyToMessage)); + initHeaderReplyEditMode() { + assert(_.isObject(this.replyToMessage)); - self.setHeaderText(MCICodeIds.ReplyEditModeHeader.To, self.replyToMessage.fromUserName); + this.setHeaderText(MciCodeIds.ReplyEditModeHeader.To, this.replyToMessage.fromUserName); // // We want to prefix the subject with "RE: " only if it's not already // that way -- avoid RE: RE: RE: RE: ... // - let newSubj = self.replyToMessage.subject; + let newSubj = this.replyToMessage.subject; if(false === /^RE:\s+/i.test(newSubj)) { newSubj = `RE: ${newSubj}`; } - self.setHeaderText(MCICodeIds.ReplyEditModeHeader.Subject, newSubj); - }; + this.setHeaderText(MciCodeIds.ReplyEditModeHeader.Subject, newSubj); + } - this.initFooterViewMode = function() { - - function setFooterText(id, text) { - var v = self.viewControllers.footerView.getView(id); - if(v) { - v.setText(text); - } - } + initFooterViewMode() { + this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgNum, (this.messageIndex + 1).toString() ); + this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgTotal, this.messageTotal.toString() ); + } - setFooterText(MCICodeIds.ViewModeFooter.MsgNum, (self.messageIndex + 1).toString()); - setFooterText(MCICodeIds.ViewModeFooter.MsgTotal, self.messageTotal.toString()); - }; - - this.displayHelp = function(cb) { - self.client.term.rawWrite(ansi.resetScreen()); + displayHelp(cb) { + this.client.term.rawWrite(ansi.resetScreen()); theme.displayThemeArt( - { name : self.menuConfig.config.art.help, client : self.client }, + { name : this.menuConfig.config.art.help, client : this.client }, () => { - self.client.waitForKeyPress( () => { - self.redrawScreen( () => { - self.viewControllers[self.getFooterName()].setFocus(true); + this.client.waitForKeyPress( () => { + this.redrawScreen( () => { + this.viewControllers[this.getFooterName()].setFocus(true); return cb(null); }); }); } ); - }; + } - this.displayQuoteBuilder = function() { + displayQuoteBuilder() { // // Clear body area // - self.newQuoteBlock = true; + this.newQuoteBlock = true; + const self = this; async.waterfall( [ @@ -772,19 +858,19 @@ function FullScreenEditorModule(options) { } } ); - }; + } - this.observeEditorEvents = function() { - var bodyView = self.viewControllers.body.getView(1); + observeEditorEvents() { + const bodyView = this.viewControllers.body.getView(1); - bodyView.on('edit position', function cursorPosUpdate(pos) { - self.updateEditModePosition(pos); + bodyView.on('edit position', pos => { + this.updateEditModePosition(pos); }); - bodyView.on('text edit mode', function textEditMode(mode) { - self.updateTextEditMode(mode); + bodyView.on('text edit mode', mode => { + this.updateTextEditMode(mode); }); - }; + } /* this.observeViewPosition = function() { @@ -794,43 +880,43 @@ function FullScreenEditorModule(options) { }; */ - this.switchToHeader = function() { - self.viewControllers.body.setFocus(false); - self.viewControllers.header.switchFocus(2); // to + switchToHeader() { + this.viewControllers.body.setFocus(false); + this.viewControllers.header.switchFocus(2); // to + } + + switchToBody() { + this.viewControllers.header.setFocus(false); + this.viewControllers.body.switchFocus(1); + + this.observeEditorEvents(); }; - this.switchToBody = function() { - self.viewControllers.header.setFocus(false); - self.viewControllers.body.switchFocus(1); + switchToFooter() { + this.viewControllers.header.setFocus(false); + this.viewControllers.body.setFocus(false); - self.observeEditorEvents(); - }; + this.viewControllers[this.getFooterName()].switchFocus(1); // HM1 + } - this.switchToFooter = function() { - self.viewControllers.header.setFocus(false); - self.viewControllers.body.setFocus(false); - - self.viewControllers[self.getFooterName()].switchFocus(1); // HM1 - }; - - this.switchFromQuoteBuilderToBody = function() { - self.viewControllers.quoteBuilder.setFocus(false); - var body = self.viewControllers.body.getView(1); + switchFromQuoteBuilderToBody() { + this.viewControllers.quoteBuilder.setFocus(false); + var body = this.viewControllers.body.getView(1); body.redraw(); - self.viewControllers.body.switchFocus(1); + this.viewControllers.body.switchFocus(1); // :TODO: create method (DRY) - self.updateTextEditMode(body.getTextEditMode()); - self.updateEditModePosition(body.getEditPosition()); + this.updateTextEditMode(body.getTextEditMode()); + this.updateEditModePosition(body.getEditPosition()); - self.observeEditorEvents(); - }; + this.observeEditorEvents(); + } - this.quoteBuilderFinalize = function() { + quoteBuilderFinalize() { // :TODO: fix magic #'s - var quoteMsgView = self.viewControllers.quoteBuilder.getView(1); - var msgView = self.viewControllers.body.getView(1); + var quoteMsgView = this.viewControllers.quoteBuilder.getView(1); + var msgView = this.viewControllers.body.getView(1); var quoteLines = quoteMsgView.getData(); @@ -841,164 +927,43 @@ function FullScreenEditorModule(options) { quoteMsgView.setText(''); - var footerName = self.getFooterName(); + this.footerMode = 'editor'; - self.footerMode = 'editor'; - - self.switchFooter(function switched(err) { - self.switchFromQuoteBuilderToBody(); + this.switchFooter( () => { + this.switchFromQuoteBuilderToBody(); }); - }; + } - this.getQuoteByHeader = function() { + getQuoteByHeader() { let quoteFormat = this.menuConfig.config.quoteFormats; + if(Array.isArray(quoteFormat)) { quoteFormat = quoteFormat[ Math.floor(Math.random() * quoteFormat.length) ]; } else if(!_.isString(quoteFormat)) { quoteFormat = 'On {dateTime} {userName} said...'; } - const dtFormat = this.menuConfig.config.quoteDateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); + const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); return stringFormat(quoteFormat, { - dateTime : moment(self.replyToMessage.modTimestamp).format(dtFormat), - userName : self.replyToMessage.fromUserName, + dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), + userName : this.replyToMessage.fromUserName, }); - }; + } - this.menuMethods = { - // - // Validation stuff - // - viewValidationListener : function(err, cb) { - var errMsgView = self.viewControllers.header.getView(MCICodeIds.ReplyEditModeHeader.ErrorMsg); - var newFocusViewId; - if(errMsgView) { - if(err) { - errMsgView.setText(err.message); - - if(MCICodeIds.ViewModeHeader.Subject === err.view.getId()) { - // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) - } - } else { - errMsgView.clearText(); - } - } - cb(newFocusViewId); - }, - - headerSubmit : function(formData, extraArgs, cb) { - self.switchToBody(); - return cb(null); - }, - editModeEscPressed : function(formData, extraArgs, cb) { - self.footerMode = 'editor' === self.footerMode ? 'editorMenu' : 'editor'; - - self.switchFooter(function next(err) { - if(err) { - // :TODO:... what now? - console.log(err) - } else { - switch(self.footerMode) { - case 'editor' : - if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { - //self.viewControllers.footerEditorMenu.setFocus(false); - self.viewControllers.footerEditorMenu.detachClientEvents(); - } - self.viewControllers.body.switchFocus(1); - self.observeEditorEvents(); - break; - - case 'editorMenu' : - self.viewControllers.body.setFocus(false); - self.viewControllers.footerEditorMenu.switchFocus(1); - break; - - default : throw new Error('Unexpected mode'); - } - } - - return cb(null); - }); - }, - editModeMenuQuote : function(formData, extraArgs, cb) { - self.viewControllers.footerEditorMenu.setFocus(false); - self.displayQuoteBuilder(); - return cb(null); - }, - appendQuoteEntry: function(formData, extraArgs, cb) { - // :TODO: Dont' use magic # ID's here - var quoteMsgView = self.viewControllers.quoteBuilder.getView(1); - - if(self.newQuoteBlock) { - self.newQuoteBlock = false; - quoteMsgView.addText(self.getQuoteByHeader()); - } - - var quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote); - quoteMsgView.addText(quoteText); - - // - // If this is *not* the last item, advance. Otherwise, do nothing as we - // don't want to jump back to the top and repeat already quoted lines - // - var quoteListView = self.viewControllers.quoteBuilder.getView(3); - if(quoteListView.getData() !== quoteListView.getCount() - 1) { - quoteListView.focusNext(); - } else { - self.quoteBuilderFinalize(); - } - - return cb(null); - }, - quoteBuilderEscPressed : function(formData, extraArgs, cb) { - self.quoteBuilderFinalize(); - return cb(null); - }, - /* - replyDiscard : function(formData, extraArgs) { - // :TODO: need to prompt yes/no - // :TODO: @method for fallback would be better - self.prevMenu(); - }, - */ - editModeMenuHelp : function(formData, extraArgs, cb) { - self.viewControllers.footerEditorMenu.setFocus(false); - return self.displayHelp(cb); - }, - /////////////////////////////////////////////////////////////////////// - // View Mode - /////////////////////////////////////////////////////////////////////// - viewModeMenuHelp : function(formData, extraArgs, cb) { - self.viewControllers.footerView.setFocus(false); - return self.displayHelp(cb); + enter() { + if(this.messageAreaTag) { + this.tempMessageConfAndAreaSwitch(this.messageAreaTag); } - }; - if(_.has(options, 'extraArgs.message')) { - this.setMessage(options.extraArgs.message); - } else if(_.has(options, 'extraArgs.replyToMessage')) { - this.replyToMessage = options.extraArgs.replyToMessage; - } -} - -require('util').inherits(FullScreenEditorModule, MenuModule); - -require('./mod_mixins.js').MessageAreaConfTempSwitcher.call(FullScreenEditorModule.prototype); - -FullScreenEditorModule.prototype.enter = function() { - - if(this.messageAreaTag) { - this.tempMessageConfAndAreaSwitch(this.messageAreaTag); + super.enter(); } - FullScreenEditorModule.super_.prototype.enter.call(this); -}; + leave() { + this.tempMessageConfAndAreaRestore(); + super.leave(); + } -FullScreenEditorModule.prototype.leave = function() { - this.tempMessageConfAndAreaRestore(); - FullScreenEditorModule.super_.prototype.leave.call(this); -}; - -FullScreenEditorModule.prototype.mciReady = function(mciData, cb) { - this.mciReadyHandler(mciData, cb); + mciReady(mciData, cb) { + return this.mciReadyHandler(mciData, cb); + } }; diff --git a/core/menu_module.js b/core/menu_module.js index 6cc81e4e..95356f90 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -9,432 +9,393 @@ const menuUtil = require('./menu_util.js'); const Config = require('./config.js').config; const stringFormat = require('../core/string_format.js'); const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; +const Errors = require('../core/enig_error.js').Errors; // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuModule = MenuModule; +exports.MenuModule = class MenuModule extends PluginModule { + + constructor(options) { + super(options); -// :TODO: some of this is a bit off... should pause after finishedLoading() + this.menuName = options.menuName; + this.menuConfig = options.menuConfig; + this.client = options.client; + this.menuConfig.options = options.menuConfig.options || {}; + this.menuMethods = {}; // methods called from @method's + this.menuConfig.config = this.menuConfig.config || {}; + + this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config.menus.cls; -function MenuModule(options) { - PluginModule.call(this, options); + this.viewControllers = {}; + } - var self = this; - this.menuName = options.menuName; - this.menuConfig = options.menuConfig; - this.client = options.client; - - // :TODO: this and the line below with .config creates empty ({}) objects in the theme -- - // ...which we really should not do. If they aren't there already, don't use 'em. - this.menuConfig.options = options.menuConfig.options || {}; - this.menuMethods = {}; // methods called from @method's + enter() { + this.initSequence(); + } - this.cls = _.isBoolean(this.menuConfig.options.cls) ? - this.menuConfig.options.cls : - Config.menus.cls; + leave() { + this.detachViewControllers(); + } - this.menuConfig.config = this.menuConfig.config || {}; + initSequence() { + const self = this; + const mciData = {}; + let pausePosition; - this.initViewControllers(); + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function displayMenuArt(callback) { + if(!_.isString(self.menuConfig.art)) { + return callback(null); + } - this.shouldPause = function() { - return 'end' === self.menuConfig.options.pause || true === self.menuConfig.options.pause; - }; + self.displayAsset( + self.menuConfig.art, + self.menuConfig.options, + (err, artData) => { + if(err) { + self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); + } else { + mciData.menu = artData.mciMap; + } - this.hasNextTimeout = function() { - return _.isNumber(self.menuConfig.options.nextTimeout); - }; + return callback(null); // any errors are non-fatal + } + ); + }, + function moveToPromptLocation(callback) { + if(self.menuConfig.prompt) { + // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements + } - this.autoNextMenu = function(cb) { - function goNext() { - if(_.isString(self.menuConfig.next) || _.isArray(self.menuConfig.next)) { + return callback(null); + }, + function displayPromptArt(callback) { + if(!_.isString(self.menuConfig.prompt)) { + return callback(null); + } + + if(!_.isObject(self.menuConfig.promptConfig)) { + return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); + } + + self.displayAsset( + self.menuConfig.promptConfig.art, + self.menuConfig.options, + (err, artData) => { + if(artData) { + mciData.prompt = artData.mciMap; + } + return callback(err); // pass err here; prompts *must* have art + } + ); + }, + function recordCursorPosition(callback) { + if(!self.shouldPause()) { + return callback(null); // cursor position not needed + } + + self.client.once('cursor position report', pos => { + pausePosition = { row : pos[0], col : 1 }; + self.client.log.trace('After art position recorded', { position : pausePosition } ); + return callback(null); + }); + + self.client.term.rawWrite(ansi.queryPos()); + }, + function afterArtDisplayed(callback) { + return self.mciReady(mciData, callback); + }, + function displayPauseIfRequested(callback) { + if(!self.shouldPause()) { + return callback(null); + } + + return self.pausePrompt(pausePosition, callback); + }, + function finishAndNext(callback) { + self.finishedLoading(); + return self.autoNextMenu(callback); + } + ], + err => { + if(err) { + self.client.log.warn('Error during init sequence', { error : err.message } ); + + return self.prevMenu( () => { /* dummy */ } ); + } + } + ); + } + + beforeArt(cb) { + if(_.isNumber(this.menuConfig.options.baudRate)) { + // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here + this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); + } + + if(this.cls) { + this.client.term.rawWrite(ansi.resetScreen()); + } + + return cb(null); + } + + mciReady(mciData, cb) { + // available for sub-classes + return cb(null); + } + + finishedLoading() { + // nothing in base + } + + getSaveState() { + // nothing in base + } + + restoreSavedState(/*savedState*/) { + // nothing in base + } + + getMenuResult() { + // nothing in base + } + + nextMenu(cb) { + if(!this.haveNext()) { + return this.prevMenu(cb); // no next, go to prev + } + + return this.client.menuStack.next(cb); + } + + prevMenu(cb) { + return this.client.menuStack.prev(cb); + } + + gotoMenu(name, options, cb) { + return this.client.menuStack.goto(name, options, cb); + } + + addViewController(name, vc) { + assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); + + this.viewControllers[name] = vc; + return vc; + } + + detachViewControllers() { + Object.keys(this.viewControllers).forEach( name => { + this.viewControllers[name].detachClientEvents(); + }); + } + + shouldPause() { + return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause); + } + + hasNextTimeout() { + return _.isNumber(this.menuConfig.options.nextTimeout); + } + + haveNext() { + return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); + } + + autoNextMenu(cb) { + const self = this; + + function gotoNextMenu() { + if(self.haveNext()) { return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); } else { return self.prevMenu(cb); } } - if(_.has(self.menuConfig, 'runtime.autoNext') && true === self.menuConfig.runtime.autoNext) { - /* - If 'next' is supplied, we'll use it. Otherwise, utlize fallback which - may be explicit (supplied) or non-explicit (previous menu) - - 'next' may be a simple asset, or a object with next.asset and - extrArgs - - next: assetSpec - - -or- - - next: { - asset: assetSpec - extraArgs: ... - } - */ - if(self.hasNextTimeout()) { + if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { + if(this.hasNextTimeout()) { setTimeout( () => { - return goNext(); + return gotoNextMenu(); }, this.menuConfig.options.nextTimeout); } else { - goNext(); + return gotoNextMenu(); } } - }; + } - this.haveNext = function() { - return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); - }; -} + standardMCIReadyHandler(mciData, cb) { + // + // A quick rundown: + // * We may have mciData.menu, mciData.prompt, or both. + // * Prompt form is favored over menu form if both are present. + // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) + // + const self = this; -require('util').inherits(MenuModule, PluginModule); + async.series( + [ + function addViewControllers(callback) { + _.forEach(mciData, (mciMap, name) => { + assert('menu' === name || 'prompt' === name); + self.addViewController(name, new ViewController( { client : self.client } ) ); + }); -require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype); - - -MenuModule.prototype.enter = function() { - this.initSequence(); -}; - -MenuModule.prototype.initSequence = function() { - var mciData = { }; - const self = this; - - async.series( - [ - function beforeDisplayArt(callback) { - self.beforeArt(callback); - }, - function displayMenuArt(callback) { - if(_.isString(self.menuConfig.art)) { - theme.displayThemedAsset( - self.menuConfig.art, - self.client, - self.menuConfig.options, // can include .font, .trailingLF, etc. - function displayed(err, artData) { - if(err) { - self.client.log.trace( { art : self.menuConfig.art, error : err.message }, 'Could not display art'); - } else { - mciData.menu = artData.mciMap; - } - callback(null); // non-fatal - } - ); - } else { - callback(null); - } - }, - function moveToPromptLocation(callback) { - if(self.menuConfig.prompt) { - // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements - } - - callback(null); - }, - function displayPromptArt(callback) { - if(_.isString(self.menuConfig.prompt)) { - // If a prompt is specified, we need the configuration - if(!_.isObject(self.menuConfig.promptConfig)) { - callback(new Error('Prompt specified but configuraiton not found!')); - return; + return callback(null); + }, + function createMenu(callback) { + if(!self.viewControllers.menu) { + return callback(null); } - // Prompts *must* have art. If it's missing it's an error - // :TODO: allow inline prompts in the future, e.g. @inline:memberName -> { "memberName" : { "text" : "stuff", ... } } - var promptConfig = self.menuConfig.promptConfig; - theme.displayThemedAsset( - promptConfig.art, - self.client, - self.menuConfig.options, // can include .font, .trailingLF, etc. - function displayed(err, artData) { - if(!err) { - mciData.prompt = artData.mciMap; - } - callback(err); - }); - } else { - callback(null); - } - }, - function recordCursorPosition(callback) { - if(self.shouldPause()) { - self.client.once('cursor position report', function cpr(pos) { - self.afterArtPos = pos; - self.client.log.trace( { position : pos }, 'After art position recorded'); - callback(null); - }); - self.client.term.write(ansi.queryPos()); - } else { - callback(null); - } - }, - function afterArtDisplayed(callback) { - self.mciReady(mciData, callback); - }, - function displayPauseIfRequested(callback) { - if(self.shouldPause()) { - self.client.term.write(ansi.goto(self.afterArtPos[0], 1)); - - // :TODO: really need a client.term.pause() that uses the correct art/etc. - // :TODO: Use MenuModule.pausePrompt() - theme.displayThemedPause( { client : self.client }, function keyPressed() { - callback(null); - }); - } else { - callback(null); - } - }, - function finishAndNext(callback) { - self.finishedLoading(); - - self.autoNextMenu(callback); - } - ], - function complete(err) { - if(err) { - console.log(err) - // :TODO: what to do exactly????? - return self.prevMenu( () => { - // dummy - }); - } - } - ); -}; - -MenuModule.prototype.getSaveState = function() { - // nothing in base -}; - -MenuModule.prototype.restoreSavedState = function(/*savedState*/) { - // nothing in base -}; - -MenuModule.prototype.nextMenu = function(cb) { - // - // If we don't actually have |next|, we'll go previous - // - if(!this.haveNext()) { - return this.prevMenu(cb); - } - - this.client.menuStack.next(cb); -}; - -MenuModule.prototype.prevMenu = function(cb) { - this.client.menuStack.prev(cb); -}; - -MenuModule.prototype.gotoMenu = function(name, options, cb) { - this.client.menuStack.goto(name, options, cb); -}; - -MenuModule.prototype.popAndGotoMenu = function(name, options, cb) { - this.client.menuStack.pop(); - this.client.menuStack.goto(name, options, cb); -}; - -MenuModule.prototype.leave = function() { - this.detachViewControllers(); -}; - -MenuModule.prototype.beforeArt = function(cb) { - // - // Set emulated baud rate - note that some terminals will display - // part of the ESC sequence here (generally a single 'r') if they - // do not support cterm style baud rates - // - if(_.isNumber(this.menuConfig.options.baudRate)) { - this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); - } - - if(this.cls) { - this.client.term.write(ansi.resetScreen()); - } - - return cb(null); -}; - -MenuModule.prototype.mciReady = function(mciData, cb) { - // Reserved for sub classes - cb(null); -}; - -MenuModule.prototype.standardMCIReadyHandler = function(mciData, cb) { - // - // A quick rundown: - // * We may have mciData.menu, mciData.prompt, or both. - // * Prompt form is favored over menu form if both are present. - // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) - // - var self = this; - - async.series( - [ - function addViewControllers(callback) { - _.forEach(mciData, function entry(mciMap, name) { - assert('menu' === name || 'prompt' === name); - self.addViewController(name, new ViewController( { client : self.client } )); - }); - callback(null); - }, - function createMenu(callback) { - if(self.viewControllers.menu) { - var menuLoadOpts = { + const menuLoadOpts = { mciMap : mciData.menu, callingMenu : self, withoutForm : _.isObject(mciData.prompt), }; - self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, function menuLoaded(err) { - callback(err); + self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { + return callback(err); }); - } else { - callback(null); - } - }, - function createPrompt(callback) { - if(self.viewControllers.prompt) { - var promptLoadOpts = { + }, + function createPrompt(callback) { + if(!self.viewControllers.prompt) { + return callback(null); + } + + const promptLoadOpts = { callingMenu : self, mciMap : mciData.prompt, }; - self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, function promptLoaded(err) { - callback(err); + self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { + return callback(err); }); - } else { - callback(null); } - } - ], - function complete(err) { - cb(err); - } - ); -}; - -MenuModule.prototype.finishedLoading = function() { -}; - -MenuModule.prototype.getMenuResult = function() { - // nothing in base -}; - -MenuModule.prototype.displayAsset = function(name, options, cb) { - - if(_.isFunction(options)) { - cb = options; - options = {}; - } - - if(options.clearScreen) { - this.client.term.rawWrite(ansi.clearScreen()); - } - - return theme.displayThemedAsset( - name, - this.client, - Object.assign( { font : this.menuConfig.config.font }, options ), - (err, artData) => { - if(cb) { - return cb(err, artData); - } - } - ); - -}; - -MenuModule.prototype.prepViewController = function(name, formId, artData, cb) { - - if(_.isUndefined(this.viewControllers[name])) { - const vcOpts = { - client : this.client, - formId : formId, - }; - - const vc = this.addViewController(name, new ViewController(vcOpts)); - - const loadOpts = { - callingMenu : this, - mciMap : artData.mciMap, - formId : formId, - }; - - return vc.loadFromMenuConfig(loadOpts, cb); - } - - this.viewControllers[name].setFocus(true); - return cb(null); -}; - - -MenuModule.prototype.prepViewControllerWithArt = function(name, formId, options, cb) { - this.displayAsset( - this.menuConfig.config.art[name], - options, - (err, artData) => { - if(err) { + ], + err => { return cb(err); } + ); + } - return this.prepViewController(name, formId, artData, cb); + displayAsset(name, options, cb) { + if(_.isFunction(options)) { + cb = options; + options = {}; } - ); -}; -MenuModule.prototype.pausePrompt = function(position, cb) { - if(!cb && _.isFunction(position)) { - cb = position; - position = null; - } - - if(position) { - position.x = position.row || position.x || 1; - position.y = position.col || position.y || 1; - - this.client.term.rawWrite(ansi.goto(position.x, position.y)); - } - - theme.displayThemedPause( { client : this.client }, cb); -}; - -MenuModule.prototype.setViewText = function(formName, mciId, text, appendMultiline) { - const view = this.viewControllers[formName].getView(mciId); - if(!view) { - return; - } - - if(appendMultiline && (view instanceof MultiLineEditTextView)) { - view.addText(text); - } else { - view.setText(text); - } -}; - -MenuModule.prototype.updateCustomViewTextsWithFilter = function(formName, startId, fmtObj, options) { - options = options || {}; - - let textView; - let customMciId = startId; - const config = this.menuConfig.config; - - while( (textView = this.viewControllers[formName].getView(customMciId)) ) { - const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" - const format = config[key]; - - if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { - const text = stringFormat(format, fmtObj); - - if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { - textView.addText(text); - } else { - textView.setText(text); + if(options.clearScreen) { + this.client.term.rawWrite(ansi.clearScreen()); + } + + return theme.displayThemedAsset( + name, + this.client, + Object.assign( { font : this.menuConfig.config.font }, options ), + (err, artData) => { + if(cb) { + return cb(err, artData); + } } + ); + } + + prepViewController(name, formId, artData, cb) { + if(_.isUndefined(this.viewControllers[name])) { + const vcOpts = { + client : this.client, + formId : formId, + }; + + const vc = this.addViewController(name, new ViewController(vcOpts)); + + const loadOpts = { + callingMenu : this, + mciMap : artData.mciMap, + formId : formId, + }; + + return vc.loadFromMenuConfig(loadOpts, cb); } - ++customMciId; + this.viewControllers[name].setFocus(true); + return cb(null); } -}; \ No newline at end of file + + prepViewControllerWithArt(name, formId, options, cb) { + this.displayAsset( + this.menuConfig.config.art[name], + options, + (err, artData) => { + if(err) { + return cb(err); + } + + return this.prepViewController(name, formId, artData, cb); + } + ); + } + + pausePrompt(position, cb) { + if(!cb && _.isFunction(position)) { + cb = position; + position = null; + } + + if(position) { + position.x = position.row || position.x || 1; + position.y = position.col || position.y || 1; + + this.client.term.rawWrite(ansi.goto(position.x, position.y)); + } + + return theme.displayThemedPause( { client : this.client }, cb); + } + + setViewText(formName, mciId, text, appendMultiLine) { + const view = this.viewControllers[formName].getView(mciId); + if(!view) { + return; + } + + if(appendMultiLine && (view instanceof MultiLineEditTextView)) { + view.addText(text); + } else { + view.setText(text); + } + } + + updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { + options = options || {}; + + let textView; + let customMciId = startId; + const config = this.menuConfig.config; + + while( (textView = this.viewControllers[formName].getView(customMciId)) ) { + const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" + const format = config[key]; + + if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { + const text = stringFormat(format, fmtObj); + + if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { + textView.addText(text); + } else { + textView.setText(text); + } + } + + ++customMciId; + } + } +}; diff --git a/core/mod_mixins.js b/core/mod_mixins.js index 00ec452b..291e0cc9 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -1,56 +1,31 @@ /* jslint node: true */ 'use strict'; -const messageArea = require('../core/message_area.js'); +const messageArea = require('../core/message_area.js'); -// deps -const assert = require('assert'); -// -// A simple mixin for View Controller management -// -exports.ViewControllerManagement = function() { - this.initViewControllers = function() { - this.viewControllers = {}; - }; - - this.detachViewControllers = function() { - var self = this; - Object.keys(this.viewControllers).forEach(function vc(name) { - self.viewControllers[name].detachClientEvents(); - }); - }; - - this.addViewController = function(name, vc) { - assert(this.viewControllers, 'initViewControllers() has not been called!'); - assert(!this.viewControllers[name], 'ViewController by the name of \'' + name + '\' already exists!'); - - this.viewControllers[name] = vc; - return vc; - }; -}; - -exports.MessageAreaConfTempSwitcher = function() { +exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { - this.tempMessageConfAndAreaSwitch = function(messageAreaTag) { + tempMessageConfAndAreaSwitch(messageAreaTag) { messageAreaTag = messageAreaTag || this.messageAreaTag; if(!messageAreaTag) { return; // nothing to do! } + this.prevMessageConfAndArea = { confTag : this.client.user.properties.message_conf_tag, areaTag : this.client.user.properties.message_area_tag, }; + if(!messageArea.tempChangeMessageConfAndArea(this.client, this.messageAreaTag)) { this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); } - }; + } - this.tempMessageConfAndAreaRestore = function() { + tempMessageConfAndAreaRestore() { if(this.prevMessageConfAndArea) { this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; } - }; - + } }; diff --git a/core/new_scan.js b/core/new_scan.js index f38196e4..abcc43b2 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -17,8 +17,6 @@ exports.moduleInfo = { author : 'NuSkooler', }; -exports.getModule = NewScanModule; - /* * :TODO: * * User configurable new scan: Area selection (avail from messages area) (sep module) @@ -27,48 +25,45 @@ exports.getModule = NewScanModule; */ -var MciCodeIds = { +const MciCodeIds = { ScanStatusLabel : 1, // TL1 ScanStatusList : 2, // VM2 (appends) }; -function NewScanModule(options) { - MenuModule.call(this, options); +exports.getModule = class NewScanModule extends MenuModule { + constructor(options) { + super(options); - var self = this; - var config = this.menuConfig.config; + this.newScanFullExit = _.has(options, 'lastMenuResult.fullExit') ? options.lastMenuResult.fullExit : false; - this.newScanFullExit = _.has(options, 'lastMenuResult.fullExit') ? options.lastMenuResult.fullExit : false; + this.currentStep = 'messageConferences'; + this.currentScanAux = {}; - this.currentStep = 'messageConferences'; - this.currentScanAux = {}; + // :TODO: Make this conf/area specific: + const config = this.menuConfig.config; + this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; + this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; + this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; + this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; + } - // :TODO: Make this conf/area specific: - this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; - this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; - this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; - this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; - - this.updateScanStatus = function(statusText) { - var vc = self.viewControllers.allViews; - - var view = vc.getView(MciCodeIds.ScanStatusLabel); - if(view) { - view.setText(statusText); - } + updateScanStatus(statusText) { + this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); + /* view = vc.getView(MciCodeIds.ScanStatusList); // :TODO: MenuView needs appendItem() if(view) { } - }; + */ + } - this.newScanMessageConference = function(cb) { + newScanMessageConference(cb) { // lazy init - if(!self.sortedMessageConfs) { + if(!this.sortedMessageConfs) { const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. - self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => { + this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => { return { confTag : k, conf : v, @@ -80,7 +75,7 @@ function NewScanModule(options) { // always come first such that we display private mails/etc. before // other conferences & areas // - self.sortedMessageConfs.sort((a, b) => { + this.sortedMessageConfs.sort((a, b) => { if('system_internal' === a.confTag) { return -1; } else { @@ -88,11 +83,12 @@ function NewScanModule(options) { } }); - self.currentScanAux.conf = self.currentScanAux.conf || 0; - self.currentScanAux.area = self.currentScanAux.area || 0; + this.currentScanAux.conf = this.currentScanAux.conf || 0; + this.currentScanAux.area = this.currentScanAux.area || 0; } - const currentConf = self.sortedMessageConfs[self.currentScanAux.conf]; + const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; + const self = this; async.series( [ @@ -113,19 +109,22 @@ function NewScanModule(options) { }); } ], - cb + err => { + return cb(err); + } ); - }; + } - this.newScanMessageArea = function(conf, cb) { + newScanMessageArea(conf, cb) { // :TODO: it would be nice to cache this - must be done by conf! - const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : self.client } ); - const currentArea = sortedAreas[self.currentScanAux.area]; + const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); + const currentArea = sortedAreas[this.currentScanAux.area]; // // Scan and update index until we find something. If results are found, // we'll goto the list module & show them. // + const self = this; async.waterfall( [ function checkAndUpdateIndex(callback) { @@ -165,73 +164,73 @@ function NewScanModule(options) { } }; - return self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts); + return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts); } ], - cb // no more areas + err => { + return cb(err); + } ); - }; - -} - -require('util').inherits(NewScanModule, MenuModule); - -NewScanModule.prototype.getSaveState = function() { - return { - currentStep : this.currentStep, - currentScanAux : this.currentScanAux, - }; -}; - -NewScanModule.prototype.restoreSavedState = function(savedState) { - this.currentStep = savedState.currentStep; - this.currentScanAux = savedState.currentScanAux; -}; - -NewScanModule.prototype.mciReady = function(mciData, cb) { - - if(this.newScanFullExit) { - // user has canceled the entire scan @ message list view - return cb(null); } + getSaveState() { + return { + currentStep : this.currentStep, + currentScanAux : this.currentScanAux, + }; + } - var self = this; - var vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + restoreSavedState(savedState) { + this.currentStep = savedState.currentStep; + this.currentScanAux = savedState.currentScanAux; + } - // :TODO: display scan step/etc. - - async.series( - [ - function callParentMciReady(callback) { - NewScanModule.super_.prototype.mciReady.call(self, mciData, callback); - }, - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; - - vc.loadFromMenuConfig(loadOpts, callback); - }, - function performCurrentStepScan(callback) { - switch(self.currentStep) { - case 'messageConferences' : - self.newScanMessageConference( () => { - callback(null); // finished - }); - break; - - default : return callback(null); - } - } - ], - function complete(err) { - if(err) { - self.client.log.error( { error : err.toString() }, 'Error during new scan'); - } - cb(err); + mciReady(mciData, cb) { + if(this.newScanFullExit) { + // user has canceled the entire scan @ message list view + return cb(null); } - ); + + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + + // :TODO: display scan step/etc. + + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + noInput : true, + }; + + vc.loadFromMenuConfig(loadOpts, callback); + }, + function performCurrentStepScan(callback) { + switch(self.currentStep) { + case 'messageConferences' : + self.newScanMessageConference( () => { + callback(null); // finished + }); + break; + + default : return callback(null); + } + } + ], + err => { + if(err) { + self.client.log.error( { error : err.toString() }, 'Error during new scan'); + } + return cb(err); + } + ); + }); + } }; diff --git a/core/standard_menu.js b/core/standard_menu.js index 4d4b8819..ddaebff3 100644 --- a/core/standard_menu.js +++ b/core/standard_menu.js @@ -1,9 +1,7 @@ /* jslint node: true */ 'use strict'; -var MenuModule = require('./menu_module.js').MenuModule; - -exports.getModule = StandardMenuModule; +const MenuModule = require('./menu_module.js').MenuModule; exports.moduleInfo = { name : 'Standard Menu Module', @@ -11,30 +9,19 @@ exports.moduleInfo = { author : 'NuSkooler', }; -function StandardMenuModule(menuConfig) { - MenuModule.call(this, menuConfig); -} +exports.getModule = class StandardMenuModule extends MenuModule { + constructor(options) { + super(options); + } -require('util').inherits(StandardMenuModule, MenuModule); + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - -StandardMenuModule.prototype.enter = function() { - StandardMenuModule.super_.prototype.enter.call(this); -}; - -StandardMenuModule.prototype.beforeArt = function(cb) { - StandardMenuModule.super_.prototype.beforeArt.call(this, cb); -}; - -StandardMenuModule.prototype.mciReady = function(mciData, cb) { - var self = this; - - StandardMenuModule.super_.prototype.mciReady.call(this, mciData, function mciReadyComplete(err) { - if(err) { - cb(err); - } else { // we do this so other modules can be both customized and still perform standard tasks - StandardMenuModule.super_.prototype.standardMCIReadyHandler.call(self, mciData, cb); - } - }); + return this.standardMCIReadyHandler(mciData, cb); + }); + } }; diff --git a/core/user_config.js b/core/user_config.js index 901ec80f..432cdade 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -1,17 +1,15 @@ /* jslint node: true */ 'use strict'; -var MenuModule = require('./menu_module.js').MenuModule; -var ViewController = require('./view_controller.js').ViewController; -var theme = require('./theme.js'); -var sysValidate = require('./system_view_validate.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const sysValidate = require('./system_view_validate.js'); -var async = require('async'); -var assert = require('assert'); -var _ = require('lodash'); -var moment = require('moment'); - -exports.getModule = UserConfigModule; +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { name : 'User Configuration', @@ -19,7 +17,7 @@ exports.moduleInfo = { author : 'NuSkooler', }; -var MciCodeIds = { +const MciCodeIds = { RealName : 1, BirthDate : 2, Sex : 3, @@ -37,192 +35,187 @@ var MciCodeIds = { SaveCancel : 25, }; -function UserConfigModule(options) { - MenuModule.call(this, options); +exports.getModule = class UserConfigModule extends MenuModule { + constructor(options) { + super(options); - var self = this; - - self.getView = function(viewId) { - return self.viewControllers.menu.getView(viewId); - }; + const self = this; - self.setViewText = function(viewId, text) { - var v = self.getView(viewId); - if(v) { - v.setText(text); - } - }; - - this.menuMethods = { - // - // Validation support - // - validateEmailAvail : function(data, cb) { + this.menuMethods = { // - // If nothing changed, we know it's OK + // Validation support // - if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { - return cb(null); - } - - // Otherwise we can use the standard system method - return sysValidate.validateEmailAvail(data, cb); - }, - - validatePassword : function(data, cb) { - // - // Blank is OK - this means we won't be changing it - // - if(!data || 0 === data.length) { - return cb(null); - } - - // Otherwise we can use the standard system method - return sysValidate.validatePasswordSpec(data, cb); - }, - - validatePassConfirmMatch : function(data, cb) { - var passwordView = self.getView(MciCodeIds.Password); - cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); - }, - - viewValidationListener : function(err, cb) { - var errMsgView = self.getView(MciCodeIds.ErrorMsg); - var newFocusId; - if(errMsgView) { - if(err) { - errMsgView.setText(err.message); - - if(err.view.getId() === MciCodeIds.PassConfirm) { - newFocusId = MciCodeIds.Password; - var passwordView = self.getView(MciCodeIds.Password); - passwordView.clearText(); - err.view.clearText(); - } - } else { - errMsgView.clearText(); - } - } - cb(newFocusId); - }, - - // - // Handlers - // - saveChanges : function(formData, extraArgs, cb) { - assert(formData.value.password === formData.value.passwordConfirm); - - const newProperties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - term_height : formData.value.termHeight.toString(), - theme_id : self.availThemeInfo[formData.value.theme].themeId, - }; - - // runtime set theme - theme.setClientTheme(self.client, newProperties.theme_id); - - // persist all changes - self.client.user.persistProperties(newProperties, err => { - if(err) { - self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); - // :TODO: warn end user! - return self.prevMenu(cb); - } + validateEmailAvail : function(data, cb) { // - // New password if it's not empty + // If nothing changed, we know it's OK // - self.client.log.info('User updated properties'); - - if(formData.value.password.length > 0) { - self.client.user.setNewAuthCredentials(formData.value.password, err => { - if(err) { - self.client.log.error( { err : err }, 'Failed storing new authentication credentials'); - } else { - self.client.log.info('User changed authentication credentials'); - } - return self.prevMenu(cb); - }); - } else { - return self.prevMenu(cb); + if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { + return cb(null); } - }); - }, - }; -} - -require('util').inherits(UserConfigModule, MenuModule); - -UserConfigModule.prototype.mciReady = function(mciData, cb) { - var self = this; - var vc = self.viewControllers.menu = new ViewController( { client : self.client} ); - - var currentThemeIdIndex = 0; - - async.series( - [ - function callParentMciReady(callback) { - UserConfigModule.super_.prototype.mciReady.call(self, mciData, callback); - }, - function loadFromConfig(callback) { - vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); - }, - function prepareAvailableThemes(callback) { - self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { - return { - themeId : themeId, - name : t.info.name, - author : t.info.author, - desc : _.isString(t.info.desc) ? t.info.desc : '', - group : _.isString(t.info.group) ? t.info.group : '', - }; - }), 'name'); - currentThemeIdIndex = _.findIndex(self.availThemeInfo, function cmp(ti) { - return ti.themeId === self.client.user.properties.theme_id; - }); - - callback(null); + // Otherwise we can use the standard system method + return sysValidate.validateEmailAvail(data, cb); }, - function populateViews(callback) { - var user = self.client.user; - - self.setViewText(MciCodeIds.RealName, user.properties.real_name); - self.setViewText(MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); - self.setViewText(MciCodeIds.Sex, user.properties.sex); - self.setViewText(MciCodeIds.Loc, user.properties.location); - self.setViewText(MciCodeIds.Affils, user.properties.affiliation); - self.setViewText(MciCodeIds.Email, user.properties.email_address); - self.setViewText(MciCodeIds.Web, user.properties.web_address); - self.setViewText(MciCodeIds.TermHeight, user.properties.term_height.toString()); + + validatePassword : function(data, cb) { + // + // Blank is OK - this means we won't be changing it + // + if(!data || 0 === data.length) { + return cb(null); + } + + // Otherwise we can use the standard system method + return sysValidate.validatePasswordSpec(data, cb); + }, + + validatePassConfirmMatch : function(data, cb) { + var passwordView = self.getView(MciCodeIds.Password); + cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); + }, + + viewValidationListener : function(err, cb) { + var errMsgView = self.getView(MciCodeIds.ErrorMsg); + var newFocusId; + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); - - var themeView = self.getView(MciCodeIds.Theme); - if(themeView) { - themeView.setItems(_.map(self.availThemeInfo, 'name')); - themeView.setFocusItemIndex(currentThemeIdIndex); + if(err.view.getId() === MciCodeIds.PassConfirm) { + newFocusId = MciCodeIds.Password; + var passwordView = self.getView(MciCodeIds.Password); + passwordView.clearText(); + err.view.clearText(); + } + } else { + errMsgView.clearText(); + } } + cb(newFocusId); + }, + + // + // Handlers + // + saveChanges : function(formData, extraArgs, cb) { + assert(formData.value.password === formData.value.passwordConfirm); - var realNameView = self.getView(MciCodeIds.RealName); - if(realNameView) { - realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! - } + const newProperties = { + real_name : formData.value.realName, + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + term_height : formData.value.termHeight.toString(), + theme_id : self.availThemeInfo[formData.value.theme].themeId, + }; - callback(null); - } - ], - function complete(err) { + // runtime set theme + theme.setClientTheme(self.client, newProperties.theme_id); + + // persist all changes + self.client.user.persistProperties(newProperties, err => { + if(err) { + self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); + // :TODO: warn end user! + return self.prevMenu(cb); + } + // + // New password if it's not empty + // + self.client.log.info('User updated properties'); + + if(formData.value.password.length > 0) { + self.client.user.setNewAuthCredentials(formData.value.password, err => { + if(err) { + self.client.log.error( { err : err }, 'Failed storing new authentication credentials'); + } else { + self.client.log.info('User changed authentication credentials'); + } + return self.prevMenu(cb); + }); + } else { + return self.prevMenu(cb); + } + }); + }, + }; + } + + getView(viewId) { + return this.viewControllers.menu.getView(viewId); + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { if(err) { - self.client.log.warn( { error : err.toString() }, 'User configuration failed to init'); - self.prevMenu(); - } else { - cb(null); + return cb(err); } - } - ); + + const self = this; + const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); + let currentThemeIdIndex = 0; + + async.series( + [ + function loadFromConfig(callback) { + vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + }, + function prepareAvailableThemes(callback) { + self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { + return { + themeId : themeId, + name : t.info.name, + author : t.info.author, + desc : _.isString(t.info.desc) ? t.info.desc : '', + group : _.isString(t.info.group) ? t.info.group : '', + }; + }), 'name'); + + currentThemeIdIndex = _.findIndex(self.availThemeInfo, function cmp(ti) { + return ti.themeId === self.client.user.properties.theme_id; + }); + + callback(null); + }, + function populateViews(callback) { + var user = self.client.user; + + self.setViewText('menu', MciCodeIds.RealName, user.properties.real_name); + self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); + self.setViewText('menu', MciCodeIds.Sex, user.properties.sex); + self.setViewText('menu', MciCodeIds.Loc, user.properties.location); + self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation); + self.setViewText('menu', MciCodeIds.Email, user.properties.email_address); + self.setViewText('menu', MciCodeIds.Web, user.properties.web_address); + self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString()); + + + var themeView = self.getView(MciCodeIds.Theme); + if(themeView) { + themeView.setItems(_.map(self.availThemeInfo, 'name')); + themeView.setFocusItemIndex(currentThemeIdIndex); + } + + var realNameView = self.getView(MciCodeIds.RealName); + if(realNameView) { + realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! + } + + callback(null); + } + ], + function complete(err) { + if(err) { + self.client.log.warn( { error : err.toString() }, 'User configuration failed to init'); + self.prevMenu(); + } else { + cb(null); + } + } + ); + }); + } }; diff --git a/mods/abracadabra.js b/mods/abracadabra.js index 08b3f2c2..4b9afdbf 100644 --- a/mods/abracadabra.js +++ b/mods/abracadabra.js @@ -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) { @@ -148,54 +148,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(); -}; \ No newline at end of file diff --git a/mods/art_pool.js b/mods/art_pool.js deleted file mode 100644 index 8b0020fd..00000000 --- a/mods/art_pool.js +++ /dev/null @@ -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); -}; diff --git a/mods/bbs_link.js b/mods/bbs_link.js index 92bab4e8..0cf0a5db 100644 --- a/mods/bbs_link.js +++ b/mods/bbs_link.js @@ -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); \ No newline at end of file + } +}; diff --git a/mods/bbs_list.js b/mods/bbs_list.js index 9b37b24e..43ff3135 100644 --- a/mods/bbs_list.js +++ b/mods/bbs_list.js @@ -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); + }); + } }; diff --git a/mods/erc_client.js b/mods/erc_client.js index dd0f9494..02b42ad5 100644 --- a/mods/erc_client.js +++ b/mods/erc_client.js @@ -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; diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index 77b6609c..2bd166bf 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -80,10 +80,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { } // Simply an empty D/L queue: Present a specialized "empty queue" page - // :TODO: This technique can be applied in many areas of the code; probablly need a better name than 'popAndGotoMenu' though - // ...actually, the option to not append to the stack would be better here return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); - //return this.popAndGotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); } const self = this; diff --git a/mods/last_callers.js b/mods/last_callers.js index 36a682a2..bf05fc35 100644 --- a/mods/last_callers.js +++ b/mods/last_callers.js @@ -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); + } + ); + }); + } }; diff --git a/mods/msg_area_list.js b/mods/msg_area_list.js index c9ad5afd..cb8d14bb 100644 --- a/mods/msg_area_list.js +++ b/mods/msg_area_list.js @@ -14,8 +14,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,153 +34,146 @@ 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 { - // :TODO: Use MenuModule.pausePrompt() - 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 { + // :TODO: Use MenuModule.pausePrompt() + displayThemedPause( { client : self.client }, () => { + 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); - } - ); -}; \ No newline at end of file + + 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); + } + ); + }); + } +}; diff --git a/mods/msg_area_post_fse.js b/mods/msg_area_post_fse.js index 8a515895..21b5d068 100644 --- a/mods/msg_area_post_fse.js +++ b/mods/msg_area_post_fse.js @@ -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(); + } +}; \ No newline at end of file diff --git a/mods/msg_area_view_fse.js b/mods/msg_area_view_fse.js index a82433b2..51794399 100644 --- a/mods/msg_area_view_fse.js +++ b/mods/msg_area_view_fse.js @@ -8,122 +8,117 @@ 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; + + 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; + } }; diff --git a/mods/msg_conf_list.js b/mods/msg_conf_list.js index e813a154..0d6b6202 100644 --- a/mods/msg_conf_list.js +++ b/mods/msg_conf_list.js @@ -14,15 +14,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,128 +28,123 @@ 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 { - // :TODO: Use MenuModule.pausePrompt() - 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 { + // :TODO: Use MenuModule.pausePrompt() + displayThemedPause( { client : self.client }, () => { + 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); - } - ); -}; \ No newline at end of file + ], + function complete(err) { + cb(err); + } + ); + }); + } +}; diff --git a/mods/msg_list.js b/mods/msg_list.js index dc82d437..498d8976 100644 --- a/mods/msg_list.js +++ b/mods/msg_list.js @@ -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.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; -}; diff --git a/mods/nua.js b/mods/nua.js index 14ef4803..31658eb3 100644 --- a/mods/nua.js +++ b/mods/nua.js @@ -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); -}; \ No newline at end of file + // 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); + } +}; diff --git a/mods/onelinerz.js b/mods/onelinerz.js index a00685f2..335c25ce 100644 --- a/mods/onelinerz.js +++ b/mods/onelinerz.js @@ -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); + }); + } }; diff --git a/mods/telnet_bridge.js b/mods/telnet_bridge.js index a8a2d9d3..b70b4589 100644 --- a/mods/telnet_bridge.js +++ b/mods/telnet_bridge.js @@ -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', @@ -123,18 +120,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 +192,5 @@ function TelnetBridgeModule(options) { } } ); - }; -} - -require('util').inherits(TelnetBridgeModule, MenuModule); + } +}; diff --git a/mods/user_list.js b/mods/user_list.js index 8401a182..07c27965 100644 --- a/mods/user_list.js +++ b/mods/user_list.js @@ -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); - } - ); -}; \ No newline at end of file + + 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); + } + ); + }); + } +}; diff --git a/mods/whos_online.js b/mods/whos_online.js index 770528f0..8d38646d 100644 --- a/mods/whos_online.js +++ b/mods/whos_online.js @@ -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 = _.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) { + if(err) { + self.client.log.error( { error : err.message }, 'Error loading who\'s online'); + } + return cb(err); + } + ); + }); + } }; diff --git a/package.json b/package.json index 8fc24eff..902c34e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.2-alpha", + "version": "0.0.3-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -14,7 +14,8 @@ }, "keywords": [ "bbs", - "telnet" + "telnet", + "retro" ], "dependencies": { "async": "^1.5.1", From 8889014f47cc802092c0eef8da39d022014edc8a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 25 Jan 2017 22:59:12 -0700 Subject: [PATCH 29/86] Update copyrights --- README.md | 2 +- core/bbs.js | 2 +- core/connect.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 353c6a54..719ac928 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/inst ## License Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license: -Copyright (c) 2015-2016, Bryan D. Ashby +Copyright (c) 2015-2017, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/core/bbs.js b/core/bbs.js index b0f64838..3ae00e37 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -80,7 +80,7 @@ function bbsMain() { function complete(err) { // note this is escaped: fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { - console.info('ENiGMA½ Copyright (c) 2014-2016 Bryan Ashby'); + console.info('ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby'); if(!err) { console.info(banner); } diff --git a/core/connect.js b/core/connect.js index bd2e40a2..ffd28a0c 100644 --- a/core/connect.js +++ b/core/connect.js @@ -123,7 +123,7 @@ function displayBanner(term) { // note: intentional formatting: term.pipeWrite(` |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN -|06Copyright (c) 2014-2016 Bryan Ashby |14- |12http://l33t.codes/ +|06Copyright (c) 2014-2017 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |00` ); From 1fc9fc1c90f8718246266e3f0912fdd962459cfd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jan 2017 12:32:38 -0700 Subject: [PATCH 30/86] Make some concepts a bit more clear --- docs/mci.md | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/mci.md b/docs/mci.md index 4da71837..28d424fd 100644 --- a/docs/mci.md +++ b/docs/mci.md @@ -7,16 +7,16 @@ ENiGMA½ supports a variety of MCI codes. Some **predefined** codes produce info A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is a Vertical Menu (`%VM`): Oldschool BBSers may recognize this as a lightbar menu. ### Available Views -* Text Label (`%TL`): Displays text -* Edit Text (`%ET`): Collect user input -* Masked Edit Text (`%ME`): Collect user input using a *mask* -* Multi Line Text Edit (`%MT`): Multi line edit control -* Button (`%BT`): A button -* Vertical Menu (`%VM`): A vertical menu aka a vertical lightbar -* Horizontal Menu (`%HM`): A horizontal menu aka a horizontal lightbar -* Spinner Menu (`%SM`): A spinner input control -* Toggle Menu (`%TM`): A toggle menu commonly used for Yes/No style input -* Key Entry (`%KE`): A *single* key input control +* Text Label (`TL`): Displays text +* Edit Text (`ET`): Collect user input +* Masked Edit Text (`ME`): Collect user input using a *mask* +* Multi Line Text Edit (`MT`): Multi line edit control +* Button (`BT`): A button +* Vertical Menu (`VM`): A vertical menu aka a vertical lightbar +* Horizontal Menu (`HM`): A horizontal menu aka a horizontal lightbar +* Spinner Menu (`SM`): A spinner input control +* Toggle Menu (`TM`): A toggle menu commonly used for Yes/No style input +* Key Entry (`KE`): A *single* key input control (Peek at `core/mci_view_factory.js` to see additional information on these) @@ -81,8 +81,8 @@ A special `XY` MCI code may also be utilized for placement identification when c Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. ### Common Properties -* `textStyle`: Sets the standard (non-focus) text style to `normal` (as-is), `upper` (UPPER), `lower` (lower), `title` (Title Case), `first lower` (fIRST lOWER), `small vowels` (SMaLL VoWeLS), `big vowels` (bIg vOwELS), `small i` (ENiGMA), `mixed` (mIxED CAsE), or `l33t` (l337 5p34k) -* `focusTextStyle`: Sets focus text style to `normal` (as-is), `upper` (UPPER), `lower` (lower), `title` (Title Case), `first lower` (fIRST lOWER), `small vowels` (SMaLL VoWeLS), `big vowels` (bIg vOwELS), `small i` (ENiGMA), `mixed` (mIxED CAsE), or `l33t` (l337 5p34k) +* `textStyle`: Sets the standard (non-focus) text style. See **Text Styles** below +* `focusTextStyle`: Sets focus text style. See **Text Styles** below. * `itemSpacing`: Used to separate items in menus such as Vertical Menu and Horizontal Menu Views. * `height`: Sets the height of views such as menus that may be > 1 character in height * `width`: Sets the width of a view @@ -90,4 +90,19 @@ Predefined MCI codes and other Views can have properties set via `menu.hjson` an * `text`: (initial) text of a view * `submit`: If set to `true` any `accept` action upon this view will submit the encompassing **form** -These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files! \ No newline at end of file +These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files! + + +#### Text Styles +Standard style types available for `textStyle` and `focusTextStyle`: + +* `normal`: Leaves text as-is. This is the default. +* `upper`: ENIGMA BULLETIN BOARD SOFTWARE +* `lower`: enigma bulletin board software +* `title`: Enigma Bulletin Board Software +* `first lower`: eNIGMA bULLETIN bOARD sOFTWARE +* `small vowels`: eNiGMa BuLLeTiN BoaRD SoFTWaRe +* `big vowels`: EniGMa bUllEtIn bOArd sOftwArE +* `small i`: ENiGMA BULLETiN BOARD SOFTWARE +* `mixed`: EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned) +* `l33t`: 3n1gm4 bull371n b04rd 50f7w4r3 \ No newline at end of file From 12d4c158c4985a3fcc17c3c10705f176498c7d58 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jan 2017 12:33:06 -0700 Subject: [PATCH 31/86] * ENiGMA now require Node.js 6.x LTS+ * Bump version to 0.0.4-alpha * Update package dependencies * Use modified node-buffers that does not use deprecated Array.get() * Update lodash dependency to 4.x & convert to new methods/etc. * Better 'noHistory' support for menu stack * Fix bug in download queue init * Misc code cleanup --- core/ansi_escape_parser.js | 2 +- core/config.js | 73 ++++--- core/download_queue.js | 6 +- core/enig_error.js | 7 + core/file_area.js | 2 +- core/fse.js | 8 +- core/menu_module.js | 2 +- core/menu_stack.js | 41 ++-- core/menu_util.js | 12 +- core/message_area.js | 4 +- core/module_util.js | 2 +- core/theme.js | 303 ++++++++++++++--------------- core/user.js | 4 +- core/view_controller.js | 40 +--- mods/file_area_list.js | 2 +- mods/file_base_download_manager.js | 12 +- mods/msg_area_view_fse.js | 5 +- mods/whos_online.js | 2 +- package.json | 27 +-- 19 files changed, 277 insertions(+), 277 deletions(-) diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index f551cd5b..fb7ea732 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -226,7 +226,7 @@ function ANSIEscapeParser(options) { self.lastMciCode = fullMciCode; - self.graphicRenditionForErase = _.clone(self.graphicRendition, true); + self.graphicRenditionForErase = _.clone(self.graphicRendition); } diff --git a/core/config.js b/core/config.js index 98d0f9dd..ff614dfc 100644 --- a/core/config.js +++ b/core/config.js @@ -1,17 +1,19 @@ /* jslint node: true */ 'use strict'; -var miscUtil = require('./misc_util.js'); +// ENiGMA½ +const miscUtil = require('./misc_util.js'); -var fs = require('fs'); -var paths = require('path'); -var async = require('async'); -var _ = require('lodash'); -var hjson = require('hjson'); -var assert = require('assert'); +// deps +const fs = require('fs'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const hjson = require('hjson'); +const assert = require('assert'); -exports.init = init; -exports.getDefaultPath = getDefaultPath; +exports.init = init; +exports.getDefaultPath = getDefaultPath; function hasMessageConferenceAndArea(config) { assert(_.isObject(config.messageConferences)); // we create one ourself! @@ -43,38 +45,45 @@ function init(configPath, cb) { async.waterfall( [ function loadUserConfig(callback) { - if(_.isString(configPath)) { - fs.readFile(configPath, { encoding : 'utf8' }, function configData(err, data) { - if(err) { - callback(err); - } else { - try { - var configJson = hjson.parse(data); - callback(null, configJson); - } catch(e) { - callback(e); - } - } - }); - } else { - callback(null, { } ); + if(!_.isString(configPath)) { + return callback(null, { } ); } + + fs.readFile(configPath, { encoding : 'utf8' }, (err, configData) => { + if(err) { + return callback(err); + } + + let configJson; + try { + configJson = hjson.parse(configData); + } catch(e) { + return callback(e); + } + + return callback(null, configJson); + }); }, function mergeWithDefaultConfig(configJson, callback) { - var mergedConfig = _.merge(getDefaultConfig(), configJson, function mergeCustomizer(conf1, conf2) { - // Arrays should always concat - if(_.isArray(conf1)) { - // :TODO: look for collisions & override dupes - return conf1.concat(conf2); + + const mergedConfig = _.mergeWith( + getDefaultConfig(), + configJson, (conf1, conf2) => { + // Arrays should always concat + if(_.isArray(conf1)) { + // :TODO: look for collisions & override dupes + return conf1.concat(conf2); + } } - }); + ); - callback(null, mergedConfig); + return callback(null, mergedConfig); }, function validate(mergedConfig, callback) { // // Various sections must now exist in config // + // :TODO: Logic is broken here: if(hasMessageConferenceAndArea(mergedConfig)) { var msgAreasErr = new Error('Please create at least one message conference and area!'); msgAreasErr.code = 'EBADCONFIG'; @@ -92,7 +101,7 @@ function init(configPath, cb) { } function getDefaultPath() { - var base = miscUtil.resolvePath('~/'); + const base = miscUtil.resolvePath('~/'); if(base) { // e.g. /home/users/joeuser/.config/enigma-bbs/config.hjson return paths.join(base, '.config', 'enigma-bbs', 'config.hjson'); diff --git a/core/download_queue.js b/core/download_queue.js index 7254b9c8..6bfbd47f 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -8,7 +8,11 @@ module.exports = class DownloadQueue { this.client = client; if(!Array.isArray(this.client.user.downloadQueue)) { - this.loadFromProperty(client); + if(this.client.user.properties.dl_queue) { + this.loadFromProperty(this.client.user.properties.dl_queue); + } else { + this.client.user.downloadQueue = []; + } } } diff --git a/core/enig_error.js b/core/enig_error.js index 0378ee00..103a98b2 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -30,3 +30,10 @@ exports.Errors = { ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), }; + +exports.ErrorReasons = { + AlreadyThere : 'ALREADYTHERE', + InvalidNextMenu : 'BADNEXT', + NoPreviousMenu : 'NOPREV', + NoConditionMatch : 'NOCONDMATCH', +}; \ No newline at end of file diff --git a/core/file_area.js b/core/file_area.js index 99e4a80f..ab847b05 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -45,7 +45,7 @@ function getAvailableFileAreas(client, options) { // perform ACS check per conf & omit internal if desired const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); - return _.omit(allAreas, areaInfo => { + return _.omitBy(allAreas, areaInfo => { if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { return true; } diff --git a/core/fse.js b/core/fse.js index d7da12ed..aae8f713 100644 --- a/core/fse.js +++ b/core/fse.js @@ -274,7 +274,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } getFooterName() { - return 'footer' + _.capitalize(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... + return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... } getFormId(name) { @@ -431,9 +431,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul callback(null); }, function displayFooterArt(callback) { - var footerArt = self.menuConfig.config.art[options.footerName]; + const footerArt = self.menuConfig.config.art[options.footerName]; - theme.displayThemedAsset( +or theme.displayThemedAsset( footerArt, self.client, { font : self.menuConfig.font }, @@ -723,7 +723,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(posView) { this.client.term.rawWrite(ansi.savePos()); // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat - posView.setText(_.padLeft(String(pos.row + 1), 2, '0') + ',' + _.padLeft(String(pos.col + 1), 2, '0')); + posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0')); this.client.term.rawWrite(ansi.restorePos()); } } diff --git a/core/menu_module.js b/core/menu_module.js index 95356f90..2553335b 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -104,7 +104,7 @@ exports.MenuModule = class MenuModule extends PluginModule { self.client.once('cursor position report', pos => { pausePosition = { row : pos[0], col : 1 }; - self.client.log.trace('After art position recorded', { position : pausePosition } ); + self.client.log.trace('After art position recorded', pausePosition ); return callback(null); }); diff --git a/core/menu_stack.js b/core/menu_stack.js index d1fc15bb..f524ce75 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -133,37 +133,34 @@ module.exports = class MenuStack { currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); currentModuleInfo.instance.leave(); + + if(modInst.menuConfig.options.menuFlags.includes('noHistory')) { + this.pop().instance.leave(); // leave & remove current + } } - const noHistory = modInst.menuConfig.options.menuFlags.indexOf('noHistory') > -1; - - const stackToLog = _.map(self.stack, stackEntry => stackEntry.name); - - if(!noHistory) { - self.push({ - name : name, - instance : modInst, - extraArgs : loadOpts.extraArgs, - }); - - stackToLog.push(name); - } else { - stackToLog.push(`${name} (noHistory)`); - } + self.push({ + name : name, + instance : modInst, + extraArgs : loadOpts.extraArgs, + }); // restore previous state if requested if(options && options.savedState) { modInst.restoreSavedState(options.savedState); } - modInst.enter(); + const stackEntries = self.stack.map(stackEntry => { + let name = stackEntry.name; + if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) { + name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`; + } + return name; + }); - self.client.log.trace( - { - stack : stackToLog - }, - 'Updated menu stack' - ); + self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' ); + + modInst.enter(); if(cb) { cb(null); diff --git a/core/menu_util.js b/core/menu_util.js index 68751f33..7e15d6da 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -94,18 +94,20 @@ function loadMenu(options, cb) { { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, 'Creating menu module instance'); + let moduleInstance; try { - const moduleInstance = new modData.mod.getModule({ + moduleInstance = new modData.mod.getModule({ menuName : options.name, menuConfig : modData.config, extraArgs : options.extraArgs, client : options.client, lastMenuResult : options.lastMenuResult, - }); - return callback(null, moduleInstance); + }); } catch(e) { return callback(e); } + + return callback(null, moduleInstance); } ], (err, modInst) => { @@ -127,8 +129,8 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { return; } - var formForId = menuConfig.form[formId]; - var mciReqKey = _.filter(_.pluck(_.sortBy(mciMap, 'code'), 'code'), function(mci) { + const formForId = menuConfig.form[formId]; + const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => { return MCIViewFactory.UserViewCodes.indexOf(mci) > -1; }).join(''); diff --git a/core/message_area.js b/core/message_area.js index 95cadf1c..8fe250a1 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -37,7 +37,7 @@ function getAvailableMessageConferences(client, options) { options = options || { includeSystemInternal : false }; // perform ACS check per conf & omit system_internal if desired - return _.omit(Config.messageConferences, (conf, confTag) => { + return _.omitBy(Config.messageConferences, (conf, confTag) => { if(!options.includeSystemInternal && 'system_internal' === confTag) { return true; } @@ -73,7 +73,7 @@ function getAvailableMessageAreasByConfTag(confTag, options) { return areas; } else { // perform ACS check per area - return _.omit(areas, area => { + return _.omitBy(areas, area => { return !options.client.acs.hasMessageAreaRead(area); }); } diff --git a/core/module_util.js b/core/module_util.js index 9834fe5a..3e51207c 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -86,7 +86,7 @@ function loadModulesForCategory(category, iterator, complete) { }); async.each(jsModules, (file, next) => { -+ loadModule(paths.basename(file, '.js'), category, (err, mod) => { + loadModule(paths.basename(file, '.js'), category, (err, mod) => { iterator(err, mod); return next(); }); diff --git a/core/theme.js b/core/theme.js index 90ff0cd4..ee67b14e 100644 --- a/core/theme.js +++ b/core/theme.js @@ -1,21 +1,20 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').config; -var art = require('./art.js'); -var ansi = require('./ansi_term.js'); -var miscUtil = require('./misc_util.js'); -var Log = require('./logger.js').log; -var configCache = require('./config_cache.js'); -var getFullConfig = require('./config_util.js').getFullConfig; -var asset = require('./asset.js'); -var ViewController = require('./view_controller.js').ViewController; +const Config = require('./config.js').config; +const art = require('./art.js'); +const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; +const configCache = require('./config_cache.js'); +const getFullConfig = require('./config_util.js').getFullConfig; +const asset = require('./asset.js'); +const ViewController = require('./view_controller.js').ViewController; -var fs = require('fs'); -var paths = require('path'); -var async = require('async'); -var _ = require('lodash'); -var assert = require('assert'); +const fs = require('fs'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); exports.getThemeArt = getThemeArt; exports.getAvailableThemes = getAvailableThemes; @@ -100,10 +99,10 @@ function loadTheme(themeID, cb) { }); } -var availableThemes = {}; +const availableThemes = {}; -var IMMUTABLE_MCI_PROPERTIES = [ - 'maxLength', 'argName', 'submit', 'validate' +const IMMUTABLE_MCI_PROPERTIES = [ + 'maxLength', 'argName', 'submit', 'validate' ]; function getMergedTheme(menuConfig, promptConfig, theme) { @@ -119,44 +118,44 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // var mergedTheme = _.cloneDeep(menuConfig); - if(_.isObject(promptConfig.prompts)) { - mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); - } - - // - // Add in data we won't be altering directly from the theme - // - mergedTheme.info = theme.info; - mergedTheme.helpers = theme.helpers; - - // - // merge customizer to disallow immutable MCI properties - // - var mciCustomizer = function(objVal, srcVal, key) { + if(_.isObject(promptConfig.prompts)) { + mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); + } + + // + // Add in data we won't be altering directly from the theme + // + mergedTheme.info = theme.info; + mergedTheme.helpers = theme.helpers; + + // + // merge customizer to disallow immutable MCI properties + // + var mciCustomizer = function(objVal, srcVal, key) { return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; }; - - function getFormKeys(fromObj) { - return _.remove(_.keys(fromObj), function pred(k) { - return !isNaN(k); // remove all non-numbers - }); - } - - function mergeMciProperties(dest, src) { - Object.keys(src).forEach(function mciEntry(mci) { - _.merge(dest[mci], src[mci], mciCustomizer); - }); - } - - function applyThemeMciBlock(dest, src, formKey) { - if(_.isObject(src.mci)) { - mergeMciProperties(dest, src.mci); - } else { - if(_.has(src, [ formKey, 'mci' ])) { - mergeMciProperties(dest, src[formKey].mci); - } - } - } + + function getFormKeys(fromObj) { + return _.remove(_.keys(fromObj), function pred(k) { + return !isNaN(k); // remove all non-numbers + }); + } + + function mergeMciProperties(dest, src) { + Object.keys(src).forEach(function mciEntry(mci) { + _.mergeWith(dest[mci], src[mci], mciCustomizer); + }); + } + + function applyThemeMciBlock(dest, src, formKey) { + if(_.isObject(src.mci)) { + mergeMciProperties(dest, src.mci); + } else { + if(_.has(src, [ formKey, 'mci' ])) { + mergeMciProperties(dest, src[formKey].mci); + } + } + } // // menu.hjson can have a couple different structures: @@ -180,103 +179,103 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming // there is a generic 'mci' block. // - function applyToForm(form, menuTheme, formKey) { - if(_.isObject(form.mci)) { - // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID - applyThemeMciBlock(form.mci, menuTheme, formKey); - - } else { - var menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { - return k === k.toUpperCase(); // remove anything not uppercase - }); - - menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - var applyFrom; - if(_.has(menuTheme, [ mciKey, 'mci' ])) { - applyFrom = menuTheme[mciKey]; - } else { - applyFrom = menuTheme; - } - - applyThemeMciBlock(form[mciKey].mci, applyFrom); - }); - } - } + function applyToForm(form, menuTheme, formKey) { + if(_.isObject(form.mci)) { + // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID + applyThemeMciBlock(form.mci, menuTheme, formKey); + + } else { + var menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { + return k === k.toUpperCase(); // remove anything not uppercase + }); + + menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { + var applyFrom; + if(_.has(menuTheme, [ mciKey, 'mci' ])) { + applyFrom = menuTheme[mciKey]; + } else { + applyFrom = menuTheme; + } + + applyThemeMciBlock(form[mciKey].mci, applyFrom); + }); + } + } - [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { - _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { - var createdFormSection = false; - var mergedThemeMenu = mergedTheme[sectionName][menuName]; - - if(_.has(theme, [ 'customization', sectionName, menuName ])) { - var menuTheme = theme.customization[sectionName][menuName]; - - // config block is direct assign/overwrite - // :TODO: should probably be _.merge() - if(menuTheme.config) { - mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); - } - - if('menus' === sectionName) { - if(_.isObject(mergedThemeMenu.form)) { - getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { - applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); - }); - } else { - if(_.isObject(menuTheme.mci)) { - // - // Not specified at menu level means we apply anything from the - // theme to form.0.mci{} - // - mergedThemeMenu.form = { 0 : { mci : { } } }; - mergeMciProperties(mergedThemeMenu.form[0], menuTheme); - createdFormSection = true; - } - } - } else if('prompts' === sectionName) { - // no 'form' or form keys for prompts -- direct to mci - applyToForm(mergedThemeMenu, menuTheme); - } - } - - // - // Finished merging for this menu/prompt - // - // If the following conditions are true, set runtime.autoNext to true: - // * This is a menu - // * There is/was no explicit 'form' section - // * There is no 'prompt' specified - // - if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && - (createdFormSection || !_.isObject(mergedThemeMenu.form))) - { - mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); - } - }); - }); + [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { + _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { + var createdFormSection = false; + var mergedThemeMenu = mergedTheme[sectionName][menuName]; + + if(_.has(theme, [ 'customization', sectionName, menuName ])) { + var menuTheme = theme.customization[sectionName][menuName]; + + // config block is direct assign/overwrite + // :TODO: should probably be _.merge() + if(menuTheme.config) { + mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); + } + + if('menus' === sectionName) { + if(_.isObject(mergedThemeMenu.form)) { + getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { + applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); + }); + } else { + if(_.isObject(menuTheme.mci)) { + // + // Not specified at menu level means we apply anything from the + // theme to form.0.mci{} + // + mergedThemeMenu.form = { 0 : { mci : { } } }; + mergeMciProperties(mergedThemeMenu.form[0], menuTheme); + createdFormSection = true; + } + } + } else if('prompts' === sectionName) { + // no 'form' or form keys for prompts -- direct to mci + applyToForm(mergedThemeMenu, menuTheme); + } + } + + // + // Finished merging for this menu/prompt + // + // If the following conditions are true, set runtime.autoNext to true: + // * This is a menu + // * There is/was no explicit 'form' section + // * There is no 'prompt' specified + // + if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && + (createdFormSection || !_.isObject(mergedThemeMenu.form))) + { + mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); + } + }); + }); return mergedTheme; } function initAvailableThemes(cb) { - var menuConfig; - var promptConfig; + var menuConfig; + var promptConfig; async.waterfall( [ - function loadMenuConfig(callback) { - getFullConfig(Config.general.menuFile, function gotConfig(err, mc) { - menuConfig = mc; - callback(err); - }); - }, - function loadPromptConfig(callback) { - getFullConfig(Config.general.promptFile, function gotConfig(err, pc) { - promptConfig = pc; - callback(err); - }); - }, + function loadMenuConfig(callback) { + getFullConfig(Config.general.menuFile, function gotConfig(err, mc) { + menuConfig = mc; + callback(err); + }); + }, + function loadPromptConfig(callback) { + getFullConfig(Config.general.promptFile, function gotConfig(err, pc) { + promptConfig = pc; + callback(err); + }); + }, function getDir(callback) { fs.readdir(Config.paths.themes, function dirRead(err, files) { callback(err, files); @@ -294,7 +293,7 @@ function initAvailableThemes(cb) { filtered.forEach(function themeEntry(themeId) { loadTheme(themeId, function themeLoaded(err, theme, themePath) { if(!err) { - availableThemes[themeId] = getMergedTheme(menuConfig, promptConfig, theme); + availableThemes[themeId] = getMergedTheme(menuConfig, promptConfig, theme); configCache.on('recached', function recached(path) { if(themePath === path) { @@ -339,17 +338,17 @@ function getRandomTheme() { } function setClientTheme(client, themeId) { - var desc; - - try { - client.currentTheme = getAvailableThemes()[themeId]; - desc = 'Set client theme'; - } catch(e) { - client.currentTheme = getAvailableThemes()[Config.defaults.theme]; - desc = 'Failed setting theme by supplied ID; Using default'; - } - - client.log.debug( { themeId : themeId, info : client.currentTheme.info }, desc); + var desc; + + try { + client.currentTheme = getAvailableThemes()[themeId]; + desc = 'Set client theme'; + } catch(e) { + client.currentTheme = getAvailableThemes()[Config.defaults.theme]; + desc = 'Failed setting theme by supplied ID; Using default'; + } + + client.log.debug( { themeId : themeId, info : client.currentTheme.info }, desc); } function getThemeArt(options, cb) { diff --git a/core/user.js b/core/user.js index e7b14740..53272fff 100644 --- a/core/user.js +++ b/core/user.js @@ -338,7 +338,7 @@ User.prototype.removeProperty = function(propName, cb) { return cb(err); } } - ) + ); }; User.prototype.persistProperties = function(properties, cb) { @@ -474,7 +474,7 @@ function generatePasswordDerivedKeySalt(cb) { function generatePasswordDerivedKey(password, salt, cb) { password = new Buffer(password).toString('hex'); - crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, function onDerivedKey(err, dk) { + crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', function onDerivedKey(err, dk) { if(err) { cb(err); } else { diff --git a/core/view_controller.js b/core/view_controller.js index 37f41fad..392471cc 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -73,8 +73,9 @@ function ViewController(options) { self.switchFocus(actionForKey.viewId); self.submitForm(key); } else if(_.isString(actionForKey.action)) { + const formData = self.getFocusedView() ? self.getFormData() : { }; self.handleActionWrapper( - { ch : ch, key : key }, // formData + Object.assign( { ch : ch, key : key }, formData ), // formData + key info actionForKey); // actionBlock } } else { @@ -115,6 +116,7 @@ function ViewController(options) { self.emit('submit', this.getFormData(key)); }; + // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them this.getLogFriendlyFormData = function(formData) { // :TODO: these fields should be part of menu.json sensitiveMembers[] var safeFormData = _.cloneDeep(formData); @@ -585,7 +587,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { for(var c = 0; c < menuSubmit.length; ++c) { var actionBlock = menuSubmit[c]; - if(_.isEqual(formData.value, actionBlock.value, self.actionBlockValueComparator)) { + if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { self.handleActionWrapper(formData, actionBlock); break; // there an only be one... } @@ -713,7 +715,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { for(var c = 0; c < confForFormId.length; ++c) { var actionBlock = confForFormId[c]; - if(_.isEqual(formData.value, actionBlock.value, self.actionBlockValueComparator)) { + if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { self.handleActionWrapper(formData, actionBlock); break; // there an only be one... } @@ -827,38 +829,6 @@ ViewController.prototype.getFormData = function(key) { this.client.log.error( { error : e.message }, 'Exception caught gathering form data' ); } }); - /* - - var viewData; - var view; - for(var id in this.views) { - try { - view = this.views[id]; - viewData = view.getData(); - if(!_.isUndefined(viewData)) { - if(_.isString(view.submitArgName)) { - formData.value[view.submitArgName] = viewData; - } else { - formData.value[id] = viewData; - } - } - } catch(e) { - this.client.log.error(e); // :TODO: Log better ;) - } - }*/ return formData; }; - -/* -ViewController.prototype.formatMenuArgs = function(args) { - var self = this; - - return _.mapValues(args, function val(value) { - if('string' === typeof value) { - return self.formatMCIString(value); - } - return value; - }); -}; -*/ \ No newline at end of file diff --git a/mods/file_area_list.js b/mods/file_area_list.js index edcd9edb..f973a70e 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -509,7 +509,7 @@ exports.getModule = class FileAreaList extends MenuModule { displayDetailsSection(sectionName, clearArea, cb) { const self = this; - const name = `details${_.capitalize(sectionName)}`; + const name = `details${_.upperFirst(sectionName)}`; async.series( [ diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index 2bd166bf..f6b0c1b0 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -61,12 +61,20 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { viewItemInfo : (formData, extraArgs, cb) => { }, removeItem : (formData, extraArgs, cb) => { - const selectedItem = formData.value.queueItem; - this.dlQueue.removeItems(selectedItem); + 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.updateDownloadQueueView(cb); }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); + + // :TODO: broken: does not redraw menu properly - needs fixed! return this.updateDownloadQueueView(cb); } }; diff --git a/mods/msg_area_view_fse.js b/mods/msg_area_view_fse.js index 51794399..129476ae 100644 --- a/mods/msg_area_view_fse.js +++ b/mods/msg_area_view_fse.js @@ -32,7 +32,8 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { const self = this; - this.menuMethods = { + // assign *additional* menuMethods + Object.assign(this.menuMethods, { nextMessage : (formData, extraArgs, cb) => { if(self.messageIndex + 1 < self.messageList.length) { self.messageIndex++; @@ -85,7 +86,7 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { self.client.log(extraArgs, 'Missing extraArgs.menu'); return cb(null); } - }; + }); } diff --git a/mods/whos_online.js b/mods/whos_online.js index 8d38646d..a0a87829 100644 --- a/mods/whos_online.js +++ b/mods/whos_online.js @@ -56,7 +56,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule { onlineListView.setItems(_.map(onlineList, oe => { if(oe.authenticated) { - oe.timeOn = _.capitalize(oe.timeOn.humanize()); + oe.timeOn = _.upperFirst(oe.timeOn.humanize()); } else { [ 'realName', 'location', 'affils', 'timeOn' ].forEach(m => { oe[m] = otherUnknown; diff --git a/package.json b/package.json index 902c34e5..2a07b7d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.3-alpha", + "version": "0.0.4-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -18,29 +18,32 @@ "retro" ], "dependencies": { - "async": "^1.5.1", + "async": "^2.1.4", "binary": "0.3.x", - "buffers": "0.1.x", + "buffers": "NuSkooler/node-buffers", "bunyan": "^1.7.1", "farmhash": "^1.2.1", - "fs-extra": "0.26.x", - "gaze": "^0.5.2", - "hjson": "1.7.x", + "fs-extra": "^2.0.0", + "gaze": "^1.1.2", + "hashids": "^1.1.1", + "hjson": "^2.4.1", "iconv-lite": "^0.4.13", - "inquirer": "^1.1.0", + "inquirer": "^3.0.1", "later": "1.2.0", - "lodash": "^3.10.1", + "lodash": "^4.17.4", + "mime-types": "^2.1.12", "minimist": "1.2.x", "moment": "^2.11.0", "node-uuid": "^1.4.7", "ptyw.js": "NuSkooler/ptyw.js", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", - "temp": "^0.8.3", - "hashids" : "^1.1.1", - "mime-types" : "^2.1.12" + "temp": "^0.8.3" + }, + "devDependencies": { + "lodash-migrate": "^0.3.16" }, "engines": { - "node": ">=4.2.0" + "node": ">=6.9.2" } } From 8aa42342a2184af135eeb8ef6b55b77d642dab88 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jan 2017 12:48:04 -0700 Subject: [PATCH 32/86] Fix setText('')/clearText() issue with text views --- core/text_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/text_view.js b/core/text_view.js index e51309da..f1b3ee7e 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -151,7 +151,7 @@ TextView.prototype.redraw = function() { // and there is no actual text (e.g. save SGR's and processing) // if(!this.hasDrawnOnce) { - if(!this.text) { + if(_.isUndefined(this.text)) { return; } } From fe24bbbe7a1dd2696f27bb96a9a205cb03a391a2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jan 2017 19:55:48 -0700 Subject: [PATCH 33/86] Update notes, Node version --- misc/install.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index 628cc51a..655649c8 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -2,7 +2,7 @@ { # this ensures the entire script is downloaded before execution -ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=4.4} +ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=4} ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs} ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` @@ -22,7 +22,7 @@ _____________________ _____ ____________________ __________\\_ / ENiGMA½ will be installed to ${ENIGMA_INSTALL_DIR}, from source ${ENIGMA_SOURCE}. -ENiGMA½ requires Node, v${ENIGMA_NODE_VERSION} will be installed via nvm. If you already have nvm installed, this install script will update it to the latest version. +ENiGMA½ requires Node.js. Version ${ENIGMA_NODE_VERSION}.x current will be installed via nvm. If you already have nvm installed, this install script will update it to the latest version. If this isn't what you were expecting, hit ctrl-c now. Installation will continue in ${WAIT_BEFORE_INSTALL} seconds... @@ -103,8 +103,20 @@ enigma_footer() { cat << EndOfMessage If this is the first time you've installed ENiGMA½, you now need to generate a minimal configuration. To do so, run the following commands: -cd ${ENIGMA_INSTALL_DIR} -./oputil.js config --new + cd ${ENIGMA_INSTALL_DIR} + ./oputil.js config --new + +Additionally, the following support binaires are recommended: + 7zip: Archive support + Debian/Ubuntu : apt-get install p7zip + CentOS : yum install p7zip + + Lha: Archive support + Debian/Ubuntu : apt-get install lhasa + + sz/rz: Various X/Y/Z modem support + Debian/Ubuntu : apt-get install lrzsz + CentOS : yum install lrzsz EndOfMessage echo -e "\e[39m" From 416894adfef5d6902fb399af0ac1076cb42abd99 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jan 2017 19:56:08 -0700 Subject: [PATCH 34/86] Update copyright --- LICENSE.TXT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.TXT b/LICENSE.TXT index 5bc50d13..8d256374 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,4 @@ -Copyright (c) 2015-2016, Bryan D. Ashby +Copyright (c) 2015-2017, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without From e10d085cabc509f76df54606ec5054c977bb4fda Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jan 2017 19:56:34 -0700 Subject: [PATCH 35/86] Remove accidental typo in code --- core/fse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/fse.js b/core/fse.js index aae8f713..b2a5e19d 100644 --- a/core/fse.js +++ b/core/fse.js @@ -433,7 +433,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul function displayFooterArt(callback) { const footerArt = self.menuConfig.config.art[options.footerName]; -or theme.displayThemedAsset( + theme.displayThemedAsset( footerArt, self.client, { font : self.menuConfig.font }, From 9525afddd3f510a7dbd81c719d53a89cdb4b9cf5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jan 2017 19:56:46 -0700 Subject: [PATCH 36/86] * Created new npm module: temptmp: This replaces node-temp usage & solves global temp file cleanup issue with concept of temp "sessions" --- core/file_area.js | 18 ++++++++---------- core/scanner_tossers/ftn_bso.js | 25 +++++++++++-------------- core/transfer_file.js | 17 +++++++---------- mods/upload.js | 10 ++++++---- package.json | 2 +- 5 files changed, 33 insertions(+), 39 deletions(-) diff --git a/core/file_area.js b/core/file_area.js index ab847b05..d0e94e10 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -9,6 +9,7 @@ const FileEntry = require('./file_entry.js'); const FileDb = require('./database.js').dbs.file; const ArchiveUtil = require('./archive_util.js'); const CRC32 = require('./crc.js').CRC32; +const Log = require('./logger.js').log; // deps const _ = require('lodash'); @@ -16,7 +17,7 @@ const async = require('async'); const fs = require('fs'); const crypto = require('crypto'); const paths = require('path'); -const temp = require('temp').track(); // track() cleans up temp dir/files for us +const temptmp = require('temptmp').createTrackedSession('file_area'); const iconv = require('iconv-lite'); exports.isInternalArea = isInternalArea; @@ -298,7 +299,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c return callback(null, [] ); } - temp.mkdir('enigextract-', (err, tempDir) => { + temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { if(err) { return callback(err); } @@ -338,14 +339,11 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c return next(null); }); }, () => { - // cleanup, but don't wait... - /* - :TODO: fix global temp cleanup issue!!! - - temp.cleanup( err => { - // :TODO: Log me! - });*/ - + // cleanup but don't wait + temptmp.cleanup( paths => { + // note: don't use client logger here - may not be avail + Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); + }); return callback(null); }); }, diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 85fad2f7..856722b5 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -18,7 +18,7 @@ const paths = require('path'); const async = require('async'); const fs = require('fs'); const later = require('later'); -const temp = require('temp').track(); // track() cleans up temp dir/files for us +const temptmp = require('temptmp').createTrackedSession('ftn_bso'); const assert = require('assert'); const gaze = require('gaze'); const fse = require('fs-extra'); @@ -1153,14 +1153,14 @@ function FTNMessageScanTossModule() { }; this.createTempDirectories = function(cb) { - temp.mkdir('enigftnexport-', (err, tempDir) => { + temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => { if(err) { return cb(err); } self.exportTempDir = tempDir; - temp.mkdir('enigftnimport-', (err, tempDir) => { + temptmp.mkdir( { prefix : 'enigftnimport-' }, (err, tempDir) => { self.importTempDir = tempDir; cb(err); @@ -1290,21 +1290,18 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { // // Clean up temp dir/files we created // - /* - :TODO: fix global temp cleanup issue!!! - - temp.cleanup((err, stats) => { - const fullStats = Object.assign(stats, { exportTemp : this.exportTempDir, importTemp : this.importTempDir } ); + temptmp.cleanup( paths => { + const fullStats = { + exportDir : this.exportTempDir, + importTemp : this.importTempDir, + paths : paths, + sessionId : temptmp.sessionId, + }; - if(err) { - Log.warn(fullStats, 'Failed cleaning up temporary directories!'); - } else { - Log.trace(fullStats, 'Temporary directories cleaned up'); - } + Log.trace(fullStats, 'Temporary directories cleaned up'); FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }); - */ FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }; diff --git a/core/transfer_file.js b/core/transfer_file.js index 28022c06..20cddeac 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -9,12 +9,13 @@ const Errors = require('./enig_error.js').Errors; const DownloadQueue = require('./download_queue.js'); const StatLog = require('./stat_log.js'); const FileEntry = require('./file_entry.js'); +const Log = require('./logger.js').log; // deps const async = require('async'); const _ = require('lodash'); const pty = require('ptyw.js'); -const temp = require('temp').track(); // track() cleans up temp dir/files for us +const temptmp = require('temptmp').createTrackedSession('transfer_file'); const paths = require('path'); const fs = require('fs'); const fse = require('fs-extra'); @@ -264,7 +265,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return callback(null, null); } - temp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { + temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { if(err) { return callback(err); // failed to create it } @@ -511,15 +512,11 @@ exports.getModule = class TransferFileModule extends MenuModule { }); } }, - function cleanupTempFiles(callback) { - /* :TODO: figure out the global temp cleanup() issue!!@! - temp.cleanup( err => { - if(err) { - self.client.log.warn( { error : err.message }, 'Failed to clean up temporary file/directory(s)' ); - } - return callback(null); // ignore err + function cleanupTempFiles(callback) { + temptmp.cleanup( paths => { + Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); }); - */ + return callback(null); }, function updateUserAndSystemStats(callback) { diff --git a/mods/upload.js b/mods/upload.js index c7f491e1..748ecf9c 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -10,11 +10,12 @@ const scanFile = require('../core/file_area.js').scanFile; 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; // deps const async = require('async'); const _ = require('lodash'); -const temp = require('temp').track(); // track() cleans up temp dir/files for us +const temptmp = require('temptmp').createTrackedSession('upload'); const paths = require('path'); exports.moduleInfo = { @@ -142,15 +143,16 @@ exports.getModule = class UploadModule extends MenuModule { leave() { // remove any temp files - only do this when if(this.isFileTransferComplete()) { - // :TODO: fix global temp cleanup issue!!! - //temp.cleanup(); // remove any temp files + temptmp.cleanup( paths => { + Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); + }); } super.leave(); } performBlindUpload(cb) { - temp.mkdir('enigul-', (err, tempRecvDirectory) => { + temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => { if(err) { return cb(err); } diff --git a/package.json b/package.json index 2a07b7d8..b17fc63b 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "ptyw.js": "NuSkooler/ptyw.js", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", - "temp": "^0.8.3" + "temptmp" : "^1.0.0" }, "devDependencies": { "lodash-migrate": "^0.3.16" From 6f1015305ba30fc2f6baa30b6d0e7deb78598a59 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jan 2017 22:30:48 -0700 Subject: [PATCH 37/86] * Allow alternate matching group order for archive short/long desc * max short/long desc file input byte size (ignore files larger than N) * Add Arj support via 'arj' --- core/archive_util.js | 10 ++++------ core/config.js | 35 +++++++++++++++++++++++++++++++---- core/file_area.js | 37 +++++++++++++++++++++++++------------ core/string_util.js | 4 ++++ main.js | 1 + package.json | 1 - 6 files changed, 65 insertions(+), 23 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 9e21f997..f1bf70df 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -256,18 +256,16 @@ module.exports = class ArchiveUtil { if(exitCode) { return cb(new Error(`List failed with exit code: ${exitCode}`)); } - //if(err) { - // return cb(err); - // } + + const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 }; const entries = []; const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm'); let m; while((m = entryMatchRe.exec(output))) { - // :TODO: allow alternate ordering!!! entries.push({ - byteSize : parseInt(m[1]), - fileName : m[2], + byteSize : parseInt(m[entryGroupOrder.byteSize]), + fileName : m[entryGroupOrder.fileName], }); } diff --git a/core/config.js b/core/config.js index ff614dfc..5567f52a 100644 --- a/core/config.js +++ b/core/config.js @@ -246,7 +246,7 @@ function getDefaultConfig() { }, decompress : { cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] + args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? }, list : { cmd : '7za', @@ -279,6 +279,30 @@ function getDefaultConfig() { cmd : 'lha', args : [ '-ew={extractPath}', '{archivePath}', '{fileList}' ] } + }, + + Arj : { + // + // 'arj' command can be obtained from: + // * apt-get: arj + // + decompress : { + cmd : 'arj', + args : [ 'x', '{archivePath}', '{extractPath}' ], + }, + list : { + cmd : 'arj', + args : [ 'l', '{archivePath}' ], + entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', + entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } + fileName : 1, + byteSize : 2, + } + }, + extract : { + cmd : 'arj', + args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], + } } }, @@ -305,7 +329,7 @@ function getDefaultConfig() { sig : '60ea', offset : 0, exts : [ 'arj' ], - handler : '7Zip', + handler : 'Arj', desc : 'ARJ Archive', }, rar : { @@ -428,15 +452,18 @@ function getDefaultConfig() { // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: areaStoragePrefix : paths.join(__dirname, './../file_base/'), + maxDescFileByteSize : 471859, // ~1/4 MB + maxDescLongFileByteSize : 524288, // 1/2 MB + fileNamePatterns: { // These are NOT case sensitive // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ - shortDesc : [ + desc : [ '^FILE_ID\.DIZ$', '^DESC\.SDI$', '^DESCRIPT\.ION$', '^FILE\.DES$', '$FILE\.SDI$', '^DISK\.ID$' ], // common README filename - https://en.wikipedia.org/wiki/README - longDesc : [ + descLong : [ '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$', '^READ\.ME$', '^README$', '^README\.md$' ], }, diff --git a/core/file_area.js b/core/file_area.js index d0e94e10..9a1a3a0a 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -280,7 +280,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c const extractList = []; const shortDescFile = entries.find( e => { - return Config.fileBase.fileNamePatterns.shortDesc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + return Config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); }); if(shortDescFile) { @@ -288,7 +288,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c } const longDescFile = entries.find( e => { - return Config.fileBase.fileNamePatterns.longDesc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + return Config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) ); }); if(longDescFile) { @@ -318,25 +318,38 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c }); }); }, - function readDescFiles(descFiles, callback) { - // :TODO: we shoudl probably omit files that are too large + function readDescFiles(descFiles, callback) { async.each(Object.keys(descFiles), (descType, next) => { const path = descFiles[descType]; if(!path) { return next(null); } - fs.readFile(path, (err, data) => { - if(err || !data) { + fs.stat(path, (err, stats) => { + if(err) { return next(null); } - // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437. - // - // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... - fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); - return next(null); + // skip entries that are too large + const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; + + if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) { + Log.debug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); + return next(null); + } + + fs.readFile(path, (err, data) => { + if(err || !data) { + return next(null); + } + + // + // Assume FILE_ID.DIZ, NFO files, etc. are CP437. + // + // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... + fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); + return next(null); + }); }); }, () => { // cleanup but don't wait diff --git a/core/string_util.js b/core/string_util.js index e8370888..5d953711 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -371,6 +371,10 @@ function cleanControlCodes(input, options) { function createCleanAnsi(input, options, cb) { + if(!input) { + return cb(''); + } + options.width = options.width || 80; options.height = options.height || 25; diff --git a/main.js b/main.js index ef624294..ab8d2652 100755 --- a/main.js +++ b/main.js @@ -1,6 +1,7 @@ #!/usr/bin/env node /* jslint node: true */ + 'use strict'; /* diff --git a/package.json b/package.json index b17fc63b..86a61a6c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "temptmp" : "^1.0.0" }, "devDependencies": { - "lodash-migrate": "^0.3.16" }, "engines": { "node": ">=6.9.2" From ee7645c235d1363b257d034c28ed7a0e289ba69f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 30 Jan 2017 21:53:37 -0700 Subject: [PATCH 38/86] Lots of updates for upcoming file base release --- README.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 719ac928..45b07420 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,28 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! ## Features Available Now - * Multi platform: Anywhere Node.js runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows) - * Multi node support + * Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows) + * Unlimited multi node support (for all those BBS "callers"!) * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods * MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles - * Telnet & **SSH** access built in. Additional servers are easy to implement & plug in + * Telnet & **SSH** access built in. Additional servers are easy to implement * [CP437](http://www.ascii-codes.com/) and UTF-8 output * [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior * [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support - * Pipe codes (ala Renegade) - * [SQLite](http://sqlite.org/) storage of users and message areas + * Renegade style pipe color codes + * [SQLite](http://sqlite.org/) storage of users, message areas, and so on * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption - * Door support including common dropfile formats and legacy DOS doors, [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/)! (See [Doors](docs/doors.md)) + * [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/) support! * [Bunyan](https://github.com/trentm/node-bunyan) logging - * FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export + * [Message networks](docs/msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export + * [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs. Legacy X/Y/Z modem also supported! + * Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more! ## In the Works * More ES6+ usage, and **documentation**! -* File areas * More ACS support coverage * SysOp dashboard (ye ol' WFC) -* Missing functionality such as searching, message area coloring, etc. +* Missing functionality such as message FTS, user coloring of messages in the FST, etc. * String localization * A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) @@ -37,11 +38,11 @@ See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more ## Support * Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) -* **Discussion on a ENiGMA BBS!** +* **Discussion on a ENiGMA BBS!** (see Boards below) * IRC: **#enigma-bbs** on **chat.freenode.net** * Email: bryan -at- l33t.codes -* Facebook ENiGMA½ group -* ENiGMA discussion on [HappyNet](http://andrew.homeunix.org/doku.php?id=happynet) +* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/) +* ENiGMA discussion on [fsxNet](http://bbs.geek.nz/#fsxNet) ## Terminal Clients ENiGMA has been tested with many terminals. However, the following are suggested for BBSing: @@ -66,10 +67,10 @@ curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/inst * [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!) * [M. Griffin](https://github.com/M-griffin), author of [Enthral BBS](https://github.com/M-griffin/Enthral), [Oblivion/2 XRM](https://github.com/M-griffin/Oblivion2-XRM) and [EtherTerm](https://github.com/M-griffin/EtherTerm)! * [Caphood](http://www.reddit.com/user/Caphood), supreme SysOp of [BLACK ƒlag](http://www.bbsnexus.com/directory/listing/blackflag.html) BBS -* Luciano Ayres of [Blocktronics](http://blocktronics.org/), creator of the "Mystery Skulls" default ENiGMA½ theme! +* [Luciano Ayres](http://www.lucianoayres.com.br/) of [Blocktronics](http://blocktronics.org/), creator of the "Mystery Skulls" default ENiGMA½ theme! * Sudndeath for Xibalba ANSI work! * Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!) -* Avon of [Agency BBS](http://bbs.geek.nz/) and fsxNet +* Avon of [Agency BBS](http://bbs.geek.nz/) and [fsxNet](http://bbs.geek.nz/#fsxNet) * Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)! * [Apam](https://github.com/apamment) of HappyLand BBS and [HappyNet](http://andrew.homeunix.org/doku.php?id=happynet)! From 5a62b7ecc0949bef1a0e594dc27896def42754ed Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 30 Jan 2017 21:54:00 -0700 Subject: [PATCH 39/86] Add arj info --- misc/install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/misc/install.sh b/misc/install.sh index 655649c8..66f92d68 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -114,6 +114,9 @@ Additionally, the following support binaires are recommended: Lha: Archive support Debian/Ubuntu : apt-get install lhasa + Arj: Archive support + Debian/Ubuntu : apt-get install arj + sz/rz: Various X/Y/Z modem support Debian/Ubuntu : apt-get install lrzsz CentOS : yum install lrzsz From c7640de07e11d3b119969cf2618046caf2469e5c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 30 Jan 2017 21:54:32 -0700 Subject: [PATCH 40/86] Add focusItemIndex property --- core/menu_view.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/menu_view.js b/core/menu_view.js index ec225907..fe4954f2 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -141,12 +141,13 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { MenuView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'itemSpacing' : this.setItemSpacing(value); break; - case 'items' : this.setItems(value); break; - case 'focusItems' : this.setFocusItems(value); break; - case 'hotKeys' : this.setHotKeys(value); break; - case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; + case 'itemSpacing' : this.setItemSpacing(value); break; + case 'items' : this.setItems(value); break; + case 'focusItems' : this.setFocusItems(value); break; + case 'hotKeys' : this.setHotKeys(value); break; + case 'hotKeySubmit' : this.hotKeySubmit = value; break; + case 'justify' : this.justify = value; break; + case 'focusItemIndex' : this.focusedItemIndex = value; break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); From 744d7368d4c7d756a908c8e900d8b3452f677a9f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 30 Jan 2017 21:55:00 -0700 Subject: [PATCH 41/86] Sort lexically + active filter first --- mods/file_area_filter_edit.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index 3b848e91..3d79e80f 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -27,6 +27,7 @@ const MciViewIds = { 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 @@ -40,6 +41,23 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { 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); + }); + this.menuMethods = { saveFilter : (formData, extraArgs, cb) => { return this.saveCurrentFilter(formData, cb); From 807ca9bded7ed3277f9cdf357d0d427eaf941055 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 00:16:28 -0700 Subject: [PATCH 42/86] Fix some year est regex's, add yyyy-mm-dd --- core/config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/config.js b/core/config.js index 5567f52a..24490237 100644 --- a/core/config.js +++ b/core/config.js @@ -473,10 +473,10 @@ function getDefaultConfig() { // Patterns should produce the year in the first submatch. // The extracted year may be YY or YYYY // - '[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', // m/d/yyyy, mm-dd-yyyy, etc. - "\\B('[1789][0-9])\\b", // eslint-disable-line quotes - '[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})', - '(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})', // November 29th, 1997 + '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. + "\\b('[1789][0-9])\\b", // eslint-disable-line quotes + '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', + '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], From 9c61b2b945df84534fceefdc3747af96ac340dc8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 00:16:43 -0700 Subject: [PATCH 43/86] Use natural sort --- core/conf_area_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 5d122cc5..5dabfb73 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -24,7 +24,7 @@ function sortAreasOrConfs(areasOrConfs, type) { } else { const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; - return keyA.localeCompare(keyB); + return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare } }); } \ No newline at end of file From 541489acb02a42303c83d6b6a494ccbb9469d97f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 00:17:14 -0700 Subject: [PATCH 44/86] Use natural sort for filters --- mods/file_area_filter_edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index 3d79e80f..1f7e846c 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -55,7 +55,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } } - return filterA.name.localeCompare(filterB.name); + return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } ); }); this.menuMethods = { From 690d148ad5760bd8969d51781f85eb77f9fd4b2f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 00:17:19 -0700 Subject: [PATCH 45/86] * Move to uuid vs node-uuid * Use uuid-parse for to/from string --- core/file_base_filter.js | 6 +++--- core/ftn_util.js | 4 ++-- core/message.js | 6 +++--- core/scanner_tossers/ftn_bso.js | 4 ++-- mods/telnet_bridge.js | 4 +++- package.json | 3 ++- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/core/file_base_filter.js b/core/file_base_filter.js index da6c4590..75938a26 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -2,7 +2,7 @@ 'use strict'; const _ = require('lodash'); -const uuids = require('node-uuid'); +const uuidV4 = require('uuid/v4'); module.exports = class FileBaseFilters { constructor(client) { @@ -37,7 +37,7 @@ module.exports = class FileBaseFilters { } add(filterInfo) { - const filterUuid = uuids.v4(); + const filterUuid = uuidV4(); filterInfo.tags = this.cleanTags(filterInfo.tags); @@ -110,7 +110,7 @@ module.exports = class FileBaseFilters { static getDefaultFilters() { const filters = {}; - const uuid = uuids.v4(); + const uuid = uuidV4(); filters[uuid] = { name : 'Default', areaTag : '', // all diff --git a/core/ftn_util.js b/core/ftn_util.js index 0860aeb4..c48cc222 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -8,7 +8,7 @@ let FNV1a = require('./fnv1a.js'); let _ = require('lodash'); let iconv = require('iconv-lite'); let moment = require('moment'); -let uuid = require('node-uuid'); +//let uuid = require('node-uuid'); let os = require('os'); let packageJson = require('../package.json'); @@ -39,7 +39,7 @@ exports.getQuotePrefix = getQuotePrefix; // Namespace for RFC-4122 name based UUIDs generated from // FTN kludges MSGID + AREA // -const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654'); +//const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654'); // See list here: https://github.com/Mithgol/node-fidonet-jam diff --git a/core/message.js b/core/message.js index ce6a7c8f..19d13e61 100644 --- a/core/message.js +++ b/core/message.js @@ -8,7 +8,7 @@ const createNamedUUID = require('./uuid_util.js').createNamedUUID; const getISOTimestampString = require('./database.js').getISOTimestampString; // deps -const uuid = require('node-uuid'); +const uuidParse = require('uuid-parse'); const async = require('async'); const _ = require('lodash'); const assert = require('assert'); @@ -17,7 +17,7 @@ const iconvEncode = require('iconv-lite').encode; module.exports = Message; -const ENIGMA_MESSAGE_UUID_NAMESPACE = uuid.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); +const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); function Message(options) { options = options || {}; @@ -135,7 +135,7 @@ Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); - return uuid.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); + return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); }; Message.getMessageIdByUuid = function(uuid, cb) { diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 856722b5..9288479d 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -23,7 +23,7 @@ const assert = require('assert'); const gaze = require('gaze'); const fse = require('fs-extra'); const iconv = require('iconv-lite'); -const uuid = require('node-uuid'); +const uuidV4 = require('uuid/v4'); exports.moduleInfo = { name : 'FTN BSO', @@ -834,7 +834,7 @@ function FTNMessageScanTossModule() { // if(Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) { // just generate a UUID & therefor always allow for dupes - message.uuid = uuid.v1(); + message.uuid = uuidV4(); } callback(null); diff --git a/mods/telnet_bridge.js b/mods/telnet_bridge.js index b70b4589..1dbb1ae9 100644 --- a/mods/telnet_bridge.js +++ b/mods/telnet_bridge.js @@ -49,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(); } } diff --git a/package.json b/package.json index 86a61a6c..dac2e394 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "mime-types": "^2.1.12", "minimist": "1.2.x", "moment": "^2.11.0", - "node-uuid": "^1.4.7", + "uuid": "^3.0.1", + "uuid-parse" : "^1.0.0", "ptyw.js": "NuSkooler/ptyw.js", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", From 572f4684f77e1c552c2c891d5a0a5854ba1e52ae Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 19:32:42 -0700 Subject: [PATCH 46/86] Add info on archivers/archive formats --- docs/archive.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/archive.md diff --git a/docs/archive.md b/docs/archive.md new file mode 100644 index 00000000..4b5a005e --- /dev/null +++ b/docs/archive.md @@ -0,0 +1,69 @@ +# File Archives & Archivers +ENiGMA½ can detect and process various archive formats such as zip and arj for a variety of tasks from file upload processing to EchoMail bundle compress/decompression. The `archives` section of `config.hjson` is used to override defaults, add new handlers, and so on. + +## Archivers +Archivers are manged via the `archives:archivers` configuration block of `config.hjson`. Each entry in this section defines an **external archiver** that can be referenced in other sections of `config.hjson` as and in code. Entries define how to `compress`, `decompress` (a full archive), `list`, and `extract` (specific files from an archive). + +### Predefined Archivers +The following archivers are pre-configured in ENiGMA½ as of this writing. Remember that you can override settings or add new handlers! + +#### ZZip +* Formats: .7z, .bzip2, .zip, .gzip/.gz, and more +* Key: `7Zip` +* Homepage/package: [7-zip.org](http://www.7-zip.org/). Generally obtained from a `p7zip` package in *nix environments. See http://p7zip.sourceforge.net/ for details. + +#### Lha +* Formats: LHA files such as .lzh. +* Key: `Lha` +* Homepage/package: `lhasa` on most *nix environments. See also https://fragglet.github.io/lhasa/ + +#### Arj +* Formats: .arj +* Key: `Arj` +* Homepage/package: `arj` on most *nix environments. + +### Archiver Configuration +Archiver entries in `config.hjson` are mostly self explanatory with the exception of `list` commands that require some additional information. The `args` member for an entry is an array of arguments to pass to `cmd`. Some variables are available to `args` that will be expanded by the system: + +* `{archivePath}` (all): Path to the archive +* `{fileList}` (compress, extract): List of file(s) to compress or extract +* `{extractPath}` (decompress, extract): Path to extract *to* + +For `list` commands, the `entryMatch` key must be provided. This key should provide a regular expression that matches two sub groups: One for uncompressed file byte sizes (sub group 1) and the other for file names (sub group 2). An optional `entryGroupOrder` can be supplied to change the default sub group order. + +#### Example Archiver Configuration +``` +7Zip: { + compress: { + cmd: 7za, + args: [ "a", "-tzip", "{archivePath}", "{fileList}" ] + } + decompress: { + cmd: 7za, + args: [ "e", "-o{extractPath}", "{archivePath}" ] + } + list: { + cmd: 7za, + args: [ "l", "{archivePath}" ] + entryMatch: "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$", + } + extract: { + cmd: 7za, + args [ "e", "-o{extractPath}", "{archivePath}", "{fileList}" ] + } +} +``` + +## Archive Formats +Archive formats can be defined such that ENiGMA½ can detect them by signature or extension, then utilize the correct *archiver* to process them. Formats are defined in the `archives:formats` key in `config.hjson`. Many differnet types come pre-configured (see `core/config.js`). + +### Example Archive Format Configuration +``` +zip: { + sig: "504b0304" /* byte signature in HEX */ + offset: 0 + exts: [ "zip" ] + handler: 7Zip /* points to a defined archiver */ + desc: "ZIP Archive" +} +``` \ No newline at end of file From f65ef7b79e7b1ae1bc33cfef696961aa2698f3db Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 19:53:55 -0700 Subject: [PATCH 47/86] Additional doc updates for upcoming file base release --- docs/config.md | 27 ++++++--------------------- docs/index.md | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/docs/config.md b/docs/config.md index 00ea751e..15496834 100644 --- a/docs/config.md +++ b/docs/config.md @@ -25,28 +25,13 @@ general: { ``` ### Specific Areas of Interest +* [Doors](doors.md) +* [MCI Codes](mci.md) +* [Menu System](menu_system.md) +* [Message Conferences](msg_conf_area.md) +* [Message Networks](msg_networks.md) +* [File Archives & Archivers](archives.md) -#### Archivers -External archivers can be configured for various tasks such as EchoMail bundle handling. - -TODO: Document further inc. Members & defaults - -**Example**: - -```hjson -archivers: {' - zip: { - // byte signature in HEX of ZIP archives - sig: "504b0304" - // offset of sig - offset: 0 - compressCmd: "7za" - compressArgs: [ "a", "-tzip", "{archivePath}", "{fileList}" ] - decompressCmd: "7za" - decompressArgs: [ "e", "-o{extractPath}", "{archivePath}" ] - } -} -``` ### A Sample Configuration Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked. diff --git a/docs/index.md b/docs/index.md index 52b2d089..4ceb6449 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,12 +11,14 @@ Under most Linux/UNIX like environments (Linux, BSD, OS X, ...) new users can s curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash ``` +For other environments such as Windows, see **The Manual Way** below. + ## The Manual Way For Windows environments or if you simply like to do things manually, read on... ### Prerequisites -* [Node.js](https://nodejs.org/) version **v4.2.x or higher** - * :information_source: It is suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs +* [Node.js](https://nodejs.org/) version **v6.x or higher** + * :information_source: It is **highly** suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs * [Python](https://www.python.org/downloads/) 2.7.x * A compiler such as Clang or GCC for Linux/UNIX systems or a recent copy of Visual Studio ([Visual Studio Express](https://www.visualstudio.com/en-us/products/visual-studio-express-vs.aspx) editions OK) for Windows users. Note that you **should only need the Visual C++ component**. @@ -25,13 +27,15 @@ For Windows environments or if you simply like to do things manually, read on... If you're new to Node.js and/or do not care about Node itself and just want to get ENiGMA½ running these steps should get you going on most \*nix type enviornments (Please consider the `install.sh` approach unless you really want to manually install!): ```bash -curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash -nvm install 4.4.0 -nvm use 4.4.0 +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +nvm install 6 +nvm use 6 ``` - If the above completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment. + +For Windows nvm-like systems exist ([nvm-windows](https://github.com/coreybutler/nvm-windows), ...) or [just download the installer](https://nodejs.org/en/download/). + ### Clone ```bash @@ -56,9 +60,11 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson` #### Via oputil.js `oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**: - ./oputil.js config --new +```bash +./oputil.js config --new +``` -You wil be asked a series of basic questions. +(You wil be asked a series of basic questions) #### Example Starting Configuration Below is an _example_ configuration. It is recommended that you at least **start with a generated configuration using oputil.js described above**. @@ -69,7 +75,7 @@ Below is an _example_ configuration. It is recommended that you at least **start boardName: Super Awesome BBS } - servers: { + loginServers: { ssh: { privateKeyPass: YOUR_PK_PASS enabled: true /* set to false to disable the SSH server */ From 8261881e3e1278fc5898c682bd491bfc7aef16b8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 Jan 2017 23:10:17 -0700 Subject: [PATCH 48/86] Initial support for non-blind aka non-batch uploads --- core/config.js | 6 +- core/enig_error.js | 1 + core/transfer_file.js | 49 +++++++++-- mods/file_transfer_protocol_select.js | 15 +++- mods/upload.js | 112 +++++++++++++++----------- 5 files changed, 123 insertions(+), 60 deletions(-) diff --git a/core/config.js b/core/config.js index 24490237..816149e1 100644 --- a/core/config.js +++ b/core/config.js @@ -379,7 +379,6 @@ function getDefaultConfig() { ], // :TODO: can we not just use --escape ? escapeTelnet : true, // set to true to escape Telnet codes such as IAC - supportsBatch : true, } }, @@ -396,8 +395,10 @@ function getDefaultConfig() { recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], + recvArgsNonBatch : [ + '-telnet', '-8', 'rz', '{fileName}' + ], escapeTelnet : false, // -telnet option does this for us - supportsBatch : true, } } @@ -478,6 +479,7 @@ function getDefaultConfig() { '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. + // :TODO: "Copyright YYYY someone" ], web : { diff --git a/core/enig_error.js b/core/enig_error.js index 103a98b2..27453227 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -29,6 +29,7 @@ exports.Errors = { Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), + UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), }; exports.ErrorReasons = { diff --git a/core/transfer_file.js b/core/transfer_file.js index 20cddeac..406acfaa 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -61,7 +61,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } if(options.extraArgs.recvFileName) { - this.recvFileName = options.extraArgs.recvFiles; + this.recvFileName = options.extraArgs.recvFileName; } if(options.extraArgs.recvDirectory) { @@ -117,6 +117,28 @@ exports.getModule = class TransferFileModule extends MenuModule { } } + sendFiles(cb) { + // assume *sending* can always batch + // :TODO: Look into this further + const allFiles = this.sendQueue.map(f => f.path); + this.executeExternalProtocolHandlerForSend(allFiles, err => { + if(err) { + this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); + } else { + const sentFiles = []; + this.sendQueue.forEach(f => { + f.sent = true; + sentFiles.push(f.path); + + }); + + this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); + } + return cb(err); + }); + } + + /* sendFiles(cb) { // :TODO: built in/native protocol support @@ -155,6 +177,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); } } + */ moveFileWithCollisionHandling(src, dst, cb) { // @@ -208,11 +231,23 @@ exports.getModule = class TransferFileModule extends MenuModule { this.recvFilePaths = []; if(this.recvFileName) { + // // file name specified - we expect a single file in |this.recvDirectory| - - // :TODO: support non-blind: Move file to dest path, add to recvFilePaths, etc. + // by the name of |this.recvFileName| + // + const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); + fs.stat(recvFullPath, (err, stats) => { + if(err) { + return cb(err); + } - return cb(null); + if(!stats.isFile()) { + return cb(Errors.Invalid('Expected file entry in recv directory')); + } + + this.recvFilePaths.push(recvFullPath); + return cb(null); + }); } else { // // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already @@ -254,8 +289,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } prepAndBuildSendArgs(filePaths, cb) { - const external = this.protocolConfig.external; - const externalArgs = external[`${this.direction}Args`]; + const externalArgs = this.protocolConfig.external['sendArgs']; async.waterfall( [ @@ -300,7 +334,8 @@ exports.getModule = class TransferFileModule extends MenuModule { } prepAndBuildRecvArgs(cb) { - const externalArgs = this.protocolConfig.external[`${this.direction}Args`]; + const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; + const externalArgs = this.protocolConfig.external[argsKey]; const args = externalArgs.map(arg => stringFormat(arg, { uploadDir : this.recvDirectory, fileName : this.recvFileName || '', diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js index 1d250d2e..a25b8709 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/file_transfer_protocol_select.js @@ -36,8 +36,6 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { this.config.direction = this.config.direction || 'send'; - this.loadAvailProtocols(); - this.extraArgs = options.extraArgs; if(_.has(options, 'lastMenuResult.sentFileIds')) { @@ -50,6 +48,8 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { this.fallbackOnly = options.lastMenuResult ? true : false; + this.loadAvailProtocols(); + this.menuMethods = { selectProtocol : (formData, extraArgs, cb) => { const protocol = this.protocols[formData.value.protocol]; @@ -130,12 +130,21 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { loadAvailProtocols() { this.protocols = _.map(Config.fileTransferProtocols, (protInfo, protocol) => { - return { + return { protocol : protocol, name : protInfo.name, + hasBatch : _.has(protInfo, 'external.recvArgs'), + hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), }; }); + // 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 ); + } + this.protocols.sort( (a, b) => a.name.localeCompare(b.name) ); } }; diff --git a/mods/upload.js b/mods/upload.js index 748ecf9c..4169f34b 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -11,6 +11,7 @@ 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; // deps const async = require('async'); @@ -69,41 +70,28 @@ exports.getModule = class UploadModule extends MenuModule { this.menuMethods = { optionsNavContinue : (formData, extraArgs, cb) => { - if(this.isBlindUpload()) { - return this.performBlindUpload(cb); - } - - // non-blind - // jump to fileDetails form - // :TODO: support non-blind: collect info/filename -> upload -> complete + return this.performUpload(cb); }, fileDetailsContinue : (formData, extraArgs, cb) => { - - - // see notes in displayFileDetailsPageForEntry() about this hackery: - cb(null); + // see displayFileDetailsPageForUploadEntry() for this hackery: + cb(null); return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any } }; } getSaveState() { - const saveState = { + return { uploadType : this.uploadType, - tempRecvDirectory : this.tempRecvDirectory + tempRecvDirectory : this.tempRecvDirectory, + areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], }; - - if(this.isBlindUpload()) { - const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); - saveState.areaInfo = this.availAreas[areaSelectView.getData()]; - } - - return saveState; } restoreSavedState(savedState) { if(savedState.areaInfo) { + this.uploadType = savedState.uploadType; this.areaInfo = savedState.areaInfo; this.tempRecvDirectory = savedState.tempRecvDirectory; } @@ -151,7 +139,7 @@ exports.getModule = class UploadModule extends MenuModule { super.leave(); } - performBlindUpload(cb) { + performUpload(cb) { temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => { if(err) { return cb(err); @@ -167,6 +155,10 @@ exports.getModule = class UploadModule extends MenuModule { } }; + if(!this.isBlindUpload()) { + 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 @@ -176,7 +168,11 @@ exports.getModule = class UploadModule extends MenuModule { modOpts, cb ); - }); + }); + } + + continueNonBlindUpload(cb) { + return cb(null); } updateScanStepInfoViews(stepInfo) { @@ -343,6 +339,34 @@ exports.getModule = class UploadModule extends MenuModule { }); } + prepDetailsForUpload(scanResults, cb) { + async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { + 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); + }); + } + processUploadedFiles() { // // For each file uploaded, we need to process & gather information @@ -351,6 +375,22 @@ exports.getModule = class UploadModule extends MenuModule { 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); }, @@ -374,31 +414,7 @@ exports.getModule = class UploadModule extends MenuModule { return callback(null, scanResults); }, function prepDetails(scanResults, callback) { - async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { - self.displayFileDetailsPageForEntry(newEntry, (err, newValues) => { - if(err) { - return nextEntry(err); - } - - // if the file entry did *not* have a desc, take the user desc - if(!self.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 self.fileDetailsCurrentEntrySubmitCallback; - return callback(err, scanResults); - }); + return self.prepDetailsForUpload(scanResults, callback); }, function startMovingAndPersistingToDatabase(scanResults, callback) { // @@ -486,7 +502,7 @@ exports.getModule = class UploadModule extends MenuModule { return (fileEntry.desc && fileEntry.desc.length > 0); } - displayFileDetailsPageForEntry(fileEntry, cb) { + displayFileDetailsPageForUploadEntry(fileEntry, cb) { const self = this; async.series( From ff64a7aed575fca83666b3667276c205dd7e6b5b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 1 Feb 2017 19:42:27 -0700 Subject: [PATCH 49/86] * SEXYZ: XModem & YModem * Explicit sort avail to protocols * MenuView.removeItem() * Natural sort for more things * Fix some issues with HorizontalMenuView redraw/update * Sanatize non-blind upload filename (security) * Validator on non-blind upload filename --- core/config.js | 60 +++++++++++++++++---------- core/menu_view.js | 14 +++++++ core/new_scan.js | 2 +- core/predefined_mci.js | 2 + core/vertical_menu_view.js | 26 ++++++++---- core/view_controller.js | 19 +++++---- mods/file_base_download_manager.js | 21 +++++++++- mods/file_transfer_protocol_select.js | 10 ++++- mods/upload.js | 49 ++++++++++++++++++++-- package.json | 3 +- 10 files changed, 161 insertions(+), 45 deletions(-) diff --git a/core/config.js b/core/config.js index 816149e1..32335803 100644 --- a/core/config.js +++ b/core/config.js @@ -364,9 +364,48 @@ function getDefaultConfig() { }, fileTransferProtocols : { + zmodem8kSexyz : { + name : 'ZModem 8k (SEXYZ)', + type : 'external', + sort : 1, + external : { + // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems + sendCmd : 'sexyz', + sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], + recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], + } + }, + + xmodemSexyz : { + name : 'XModem (SEXYZ)', + type : 'external', + sort : 3, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] + } + }, + + ymodemSexyz : { + name : 'YModem (SEXYZ)', + type : 'external', + sort : 4, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], + } + }, + zmodem8kSz : { name : 'ZModem 8k', type : 'external', + sort : 2, external : { sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" sendArgs : [ @@ -380,28 +419,7 @@ function getDefaultConfig() { // :TODO: can we not just use --escape ? escapeTelnet : true, // set to true to escape Telnet codes such as IAC } - }, - - zmodem8kSexyz : { - name : 'ZModem 8k (SEXYZ)', - type : 'external', - external : { - // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems - sendCmd : 'sexyz', - sendArgs : [ - '-telnet', '-8', 'sz', '@{fileListPath}' - ], - recvCmd : 'sexyz', - recvArgs : [ - '-telnet', '-8', 'rz', '{uploadDir}' - ], - recvArgsNonBatch : [ - '-telnet', '-8', 'rz', '{fileName}' - ], - escapeTelnet : false, // -telnet option does this for us - } } - }, messageAreaDefaults : { diff --git a/core/menu_view.js b/core/menu_view.js index fe4954f2..07d19e8a 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -77,6 +77,20 @@ MenuView.prototype.setItems = function(items) { } }; +MenuView.prototype.removeItem = function(index) { + this.items.splice(index, 1); + + if(this.focusItems) { + this.focusItems.splice(index, 1); + } + + if(this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } + + this.positionCacheExpired = true; +}; + MenuView.prototype.getCount = function() { return this.items.length; }; diff --git a/core/new_scan.js b/core/new_scan.js index abcc43b2..0483e8d1 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -79,7 +79,7 @@ exports.getModule = class NewScanModule extends MenuModule { if('system_internal' === a.confTag) { return -1; } else { - return a.conf.name.localeCompare(b.conf.name); + return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } ); } }); diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 69463f4d..58f56cf4 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -195,12 +195,14 @@ function getPredefinedMCIValue(client, code) { // :TODO: System stat log for total ul/dl, total ul/dl bytes // :TODO: PT - Messages posted *today* (Obv/2) + // -> Include FTN/etc. // :TODO: NT - New users today (Obv/2) // :TODO: CT - Calls *today* (Obv/2) // :TODO: TF - Total files on the system (Obv/2) // :TODO: FT - Files uploaded/added *today* (Obv/2) // :TODO: DD - Files downloaded *today* (iNiQUiTY) // :TODO: TP - total message/posts on the system (Obv/2) + // -> Include FTN/etc. // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index f72095e1..445f5f4a 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -44,7 +44,7 @@ function VerticalMenuView(options) { self.viewWindow = { top : self.focusedItemIndex, - bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1 + bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1, }; }; @@ -107,12 +107,14 @@ VerticalMenuView.prototype.redraw = function() { delete this.oldDimens; } - let row = this.position.row; - for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { - this.items[i].row = row; - row += this.itemSpacing + 1; - this.items[i].focused = this.focusedItemIndex === i; - this.drawItem(i); + if(this.items.length) { + let row = this.position.row; + for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { + this.items[i].row = row; + row += this.itemSpacing + 1; + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } } }; @@ -171,7 +173,7 @@ VerticalMenuView.prototype.getData = function() { VerticalMenuView.prototype.setItems = function(items) { // if we have items already, save off their drawing area so we don't leave fragments at redraw if(this.items && this.items.length) { - this.oldDimens = this.dimens; + this.oldDimens = Object.assign({}, this.dimens); } VerticalMenuView.super_.prototype.setItems.call(this, items); @@ -179,6 +181,14 @@ VerticalMenuView.prototype.setItems = function(items) { this.positionCacheExpired = true; }; +VerticalMenuView.prototype.removeItem = function(index) { + if(this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + VerticalMenuView.super_.prototype.removeItem.call(this, index); +}; + // :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! VerticalMenuView.prototype.focusNext = function() { diff --git a/core/view_controller.js b/core/view_controller.js index 392471cc..51c63b3a 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -454,7 +454,7 @@ ViewController.prototype.resetInitialFocus = function() { if(this.formInitialFocusId) { return this.switchFocus(this.formInitialFocusId); } -} +}; ViewController.prototype.switchFocus = function(id) { // @@ -480,15 +480,19 @@ ViewController.prototype.switchFocus = function(id) { }; ViewController.prototype.nextFocus = function() { - var nextId; + let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId]; - if(!this.focusedView) { - nextId = this.views[this.firstId].id; - } else { - nextId = this.views[this.focusedView.id].nextId; + // find the next view that accepts focus + while(nextFocusView && nextFocusView.nextId) { + nextFocusView = this.getView(nextFocusView.nextId); + if(!nextFocusView || nextFocusView.acceptsFocus) { + break; + } } - this.switchFocus(nextId); + if(nextFocusView && this.focusedView !== nextFocusView) { + this.switchFocus(nextFocusView.id); + } }; ViewController.prototype.setViewOrder = function(order) { @@ -507,7 +511,6 @@ ViewController.prototype.setViewOrder = function(order) { } if(viewIdOrder.length > 0) { - var view; var count = viewIdOrder.length - 1; for(var i = 0; i < count; ++i) { this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1]; diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index f6b0c1b0..8ddcb735 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -69,13 +69,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { this.dlQueue.removeItems(selectedItem.fileId); // :TODO: broken: does not redraw menu properly - needs fixed! - return this.updateDownloadQueueView(cb); + 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.updateDownloadQueueView(cb); + return this.removeItemsFromDownloadQueueView('all', cb); } }; } @@ -108,6 +108,23 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { ); } + 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) { diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js index a25b8709..6efa5a93 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/file_transfer_protocol_select.js @@ -135,6 +135,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { name : protInfo.name, hasBatch : _.has(protInfo, 'external.recvArgs'), hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), + sort : protInfo.sort, }; }); @@ -145,6 +146,13 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { this.protocols = this.protocols.filter( prot => prot.hasBatch ); } - this.protocols.sort( (a, b) => a.name.localeCompare(b.name) ); + // 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 } ); + } + }); } }; diff --git a/mods/upload.js b/mods/upload.js index 4169f34b..c260d1f3 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -18,6 +18,7 @@ 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', @@ -38,6 +39,7 @@ const MciViewIds = { 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 : { @@ -77,6 +79,37 @@ exports.getModule = class UploadModule extends MenuModule { // 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); } }; } @@ -156,6 +189,7 @@ exports.getModule = class UploadModule extends MenuModule { }; if(!this.isBlindUpload()) { + // data has been sanatized at this point modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData(); } @@ -463,10 +497,19 @@ exports.getModule = class UploadModule extends MenuModule { if(self.isBlindUpload()) { fileNameView.setText(blindFileNameText); - - // :TODO: when blind, fileNameView should not be focus/editable + 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 diff --git a/package.json b/package.json index dac2e394..11a3cbc8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "ptyw.js": "NuSkooler/ptyw.js", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", - "temptmp" : "^1.0.0" + "temptmp" : "^1.0.0", + "sanitize-filename" : "^1.6.1" }, "devDependencies": { }, From 92772eb1a9a9bb514ba714debaf12c43c10a6410 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 4 Feb 2017 09:20:36 -0700 Subject: [PATCH 50/86] * Added ability to serve static files from web server * Web server can have custom error pages, e.g. 404.html * "file_area" stuff -> "file_base" * Fix some rare bugs in theme/art loading * Adjust tab order dynamically for file upload details --- UPGRADE.TXT | 49 ++++++++++++++++ core/art.js | 10 +++- core/config.js | 5 ++ core/file_area_web.js | 17 +++--- core/{file_area.js => file_base_area.js} | 0 core/{transfer_file.js => file_transfer.js} | 8 +++ core/listening_server.js | 1 - core/servers/content/web.js | 64 +++++++++++++++++++-- core/theme.js | 22 ++++--- mods/file_area_filter_edit.js | 2 +- mods/file_area_list.js | 8 +-- mods/upload.js | 25 ++++---- oputil.js | 2 +- www/.gitkeep | 0 14 files changed, 165 insertions(+), 48 deletions(-) create mode 100644 UPGRADE.TXT rename core/{file_area.js => file_base_area.js} (100%) rename core/{transfer_file.js => file_transfer.js} (98%) create mode 100644 www/.gitkeep diff --git a/UPGRADE.TXT b/UPGRADE.TXT new file mode 100644 index 00000000..cd5ecb91 --- /dev/null +++ b/UPGRADE.TXT @@ -0,0 +1,49 @@ +INTRODUCTION +------------------------------------------------------------------------------- +This document covers basic upgrade notes for major ENiGMA½. + + +BEFORE UPGRADING +------------------------------------------------------------------------------- +* Always back ALL files in the 'db' directory +* Back up your menu.hjson (or renamed equivalent) + + +GENERAL NOTES +------------------------------------------------------------------------------- +Upgrades often come with changes to the default menu.hjson. It is wise to +use a *different* file name for your BBS's version of this file and point to +it via config.hjson. For example: + +general: { + menuFile: my_bbs.hjson +} + +After updating code, use a program such as DiffMerge to merge in updates to +my_bbs.hjson from the shipping menu.hjson. + + +FROM GITHUB +------------------------------------------------------------------------------- +Upgrading from GitHub is easy: + + cd /path/to/enigma-bbs + git pull + npm install + + +PROBLEMS +------------------------------------------------------------------------------- +Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or +file a issue on GitHub. + + +0.0.1-alpha to 0.0.4-alpha +------------------------------------------------------------------------------- +* Manual Database Upgrade + sqlite3 db/message.sqlite + INSERT INTO message_fts(message_fts) VALUES('rebuild'); + +* Archiver Changes + If you have overridden or made additions to archivers in your config.hjson + you will need to update them. See docs/archive.md and core/config.js diff --git a/core/art.js b/core/art.js index d33c81da..3d3febe1 100644 --- a/core/art.js +++ b/core/art.js @@ -78,7 +78,7 @@ function getArtFromPath(path, options, cb) { return iconv.decode(data, encoding); } else { const eofMarker = defaultEofFromExtension(ext); - return iconv.decode(sliceAtEOF(data, eofMarker), encoding); + return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding); } } @@ -213,11 +213,15 @@ function getArt(name, options, cb) { } function defaultEncodingFromExtension(ext) { - return SUPPORTED_ART_TYPES[ext.toLowerCase()].defaultEncoding; + const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; + return artType ? artType.defaultEncoding : 'utf8'; } function defaultEofFromExtension(ext) { - return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof; + const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; + if(artType) { + return artType.eof; + } } // :TODO: Implement the following diff --git a/core/config.js b/core/config.js index 32335803..373f6c49 100644 --- a/core/config.js +++ b/core/config.js @@ -223,6 +223,8 @@ function getDefaultConfig() { contentServers : { web : { domain : 'another-fine-enigma-bbs.org', + + staticRoot : paths.join(__dirname, './../www'), http : { enabled : false, @@ -364,6 +366,9 @@ function getDefaultConfig() { }, fileTransferProtocols : { + // + // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ + // zmodem8kSexyz : { name : 'ZModem 8k (SEXYZ)', type : 'external', diff --git a/core/file_area_web.js b/core/file_area_web.js index 841ce904..3f1b60ac 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -41,15 +41,15 @@ class FileAreaWebAccess { return self.load(callback); }, function addWebRoute(callback) { - const webServer = getServer(WEB_SERVER_PACKAGE_NAME); - if(!webServer) { + self.webServer = getServer(WEB_SERVER_PACKAGE_NAME); + if(!self.webServer) { return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`)); } - const routeAdded = webServer.instance.addRoute({ + const routeAdded = self.webServer.instance.addRoute({ method : 'GET', path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable - handler : self.routeWebRequest.bind(self), + handler : self.routeWebRequestForFile.bind(self), }); return callback(routeAdded ? null : Errors.General('Failed adding route')); @@ -217,13 +217,10 @@ class FileAreaWebAccess { } fileNotFound(resp) { - resp.writeHead(404, { 'Content-Type' : 'text/html' } ); - - // :TODO: allow custom 404 - mods//file_area_web-404.html - return resp.end('Not found'); + this.webServer.instance.respondWithError(resp, 404, 'File not found.', 'File Not Found'); } - routeWebRequest(req, resp) { + routeWebRequestForFile(req, resp) { const hashId = paths.basename(req.url); this.loadServedHashId(hashId, (err, servedItem) => { @@ -259,7 +256,7 @@ class FileAreaWebAccess { }); const headers = { - 'Content-Type' : mimeTypes.contentType(paths.extname(filePath)) || mimeTypes.contentType('.bin'), + 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), 'Content-Length' : stats.size, 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, }; diff --git a/core/file_area.js b/core/file_base_area.js similarity index 100% rename from core/file_area.js rename to core/file_base_area.js diff --git a/core/transfer_file.js b/core/file_transfer.js similarity index 98% rename from core/transfer_file.js rename to core/file_transfer.js index 406acfaa..9735abe7 100644 --- a/core/transfer_file.js +++ b/core/file_transfer.js @@ -25,11 +25,19 @@ const SYSTEM_EOL = require('os').EOL; const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. /* + Notes + ----------------------------------------------------------------------------- + + See core/config.js for external protocol configuration + + Resources + ----------------------------------------------------------------------------- ZModem * http://gallium.inria.fr/~doligez/zmodem/zmodem.txt * https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c + */ exports.moduleInfo = { diff --git a/core/listening_server.js b/core/listening_server.js index e1009338..94efd475 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -6,7 +6,6 @@ const logger = require('./logger.js'); // deps const async = require('async'); -const _ = require('lodash'); const listeningServers = {}; // packageName -> info diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 6a184284..5a6f36d1 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -11,6 +11,8 @@ const http = require('http'); const https = require('https'); const _ = require('lodash'); const fs = require('fs'); +const paths = require('path'); +const mimeTypes = require('mime-types'); const ModuleInfo = exports.moduleInfo = { name : 'Web', @@ -55,6 +57,14 @@ exports.getModule = class WebServerModule extends ServerModule { this.enableHttps = Config.contentServers.web.https.enabled || false; this.routes = {}; + + if(Config.contentServers.web.staticRoot) { + this.addRoute({ + method : 'GET', + path : '/static/.*$', + handler : this.routeStaticFile, + }); + } } createServer() { @@ -116,8 +126,54 @@ exports.getModule = class WebServerModule extends ServerModule { return route ? route.handler(req, resp) : this.accessDenied(resp); } - accessDenied(resp) { - resp.writeHead(401, { 'Content-Type' : 'text/html' } ); - return resp.end('Access denied'); + respondWithError(resp, code, bodyText, title) { + const customErrorPage = paths.join(Config.contentServers.web.staticRoot, `${code}.html`); + + fs.readFile(customErrorPage, 'utf8', (err, data) => { + resp.writeHead(code, { 'Content-Type' : 'text/html' } ); + + if(err) { + return resp.end(` + + + + ${title} + + + +
+

${bodyText}

+
+ + ` + ); + } + + return resp.end(data); + }); } -} \ No newline at end of file + + accessDenied(resp) { + return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied'); + } + + routeStaticFile(req, resp) { + const fileName = req.url.substr(req.url.indexOf('/', 1)); + const filePath = paths.join(Config.contentServers.web.staticRoot, fileName); + + fs.stat(filePath, (err, stats) => { + if(err) { + return this.respondWithError(resp, 404, 'File not found.', 'File Not Found'); + } + + const headers = { + 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), + 'Content-Length' : stats.size, + }; + + const readStream = fs.createReadStream(filePath); + resp.writeHead(200, headers); + return readStream.pipe(resp); + }); + } +}; diff --git a/core/theme.js b/core/theme.js index ee67b14e..19ed9ff4 100644 --- a/core/theme.js +++ b/core/theme.js @@ -354,16 +354,16 @@ function setClientTheme(client, themeId) { function getThemeArt(options, cb) { // // options - required: - // name - // client + // name // // options - optional - // themeId - // asAnsi - // readSauce - // random + // client - needed for user's theme/etc. + // themeId + // asAnsi + // readSauce + // random // - if(!options.themeId && _.has(options.client, 'user.properties.theme_id')) { + if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { options.themeId = options.client.user.properties.theme_id; } else { options.themeId = Config.defaults.theme; @@ -437,9 +437,13 @@ function getThemeArt(options, cb) { ], function complete(err, artInfo) { if(err) { - options.client.log.debug( { error : err }, 'Cannot find art'); + if(options.client) { + options.client.log.debug( { error : err.message }, 'Cannot find theme art' ); + } else { + Log.debug( { error : err.message }, 'Cannot find theme art' ); + } } - cb(err, artInfo); + return cb(err, artInfo); } ); } diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index 1f7e846c..5cb211ac 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -4,7 +4,7 @@ // ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; const ViewController = require('../core/view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; +const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas; const FileBaseFilters = require('../core/file_base_filter.js'); const stringFormat = require('../core/string_format.js'); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index f973a70e..b946497b 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -9,7 +9,7 @@ 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_area.js'); +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; @@ -23,12 +23,6 @@ const cleanControlCodes = require('../core/string_util.js').cleanControlCodes; const async = require('async'); const _ = require('lodash'); const moment = require('moment'); -const paths = require('path'); - -/* - Misc TODO - -*/ exports.moduleInfo = { name : 'File Area List', diff --git a/mods/upload.js b/mods/upload.js index c260d1f3..d06b1fa6 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -4,9 +4,9 @@ // enigma-bbs const MenuModule = require('../core/menu_module.js').MenuModule; const stringFormat = require('../core/string_format.js'); -const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; -const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory; -const scanFile = require('../core/file_area.js').scanFile; +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 ansiGoto = require('../core/ansi_term.js').goto; const moveFileWithCollisionHandling = require('../core/file_util.js').moveFileWithCollisionHandling; const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTerminatingSeparator; @@ -560,20 +560,21 @@ exports.getModule = class UploadModule extends MenuModule { }, 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); + + 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.setText(fileEntry.desc); descView.setPropertyValue('mode', 'preview'); - - // :TODO: it would be nice to take this out of the focus order + self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.tags); + descView.acceptsFocus = false; + } else { + self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.desc); } - const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags); - tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse - - const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear); - yearView.setText(fileEntry.meta.est_release_year || ''); - return callback(null); } ], diff --git a/oputil.js b/oputil.js index b00d6428..20ae59da 100755 --- a/oputil.js +++ b/oputil.js @@ -442,7 +442,7 @@ function fileAreaScan() { return initConfigAndDatabases(callback); }, function getFileArea(callback) { - const fileAreaMod = require('./core/file_area.js'); + const fileAreaMod = require('./core/file_base_area.js'); const areaInfo = fileAreaMod.getFileAreaByTag(argv.scan); if(!areaInfo) { diff --git a/www/.gitkeep b/www/.gitkeep new file mode 100644 index 00000000..e69de29b From 725dc685c827a002df6cebfbedbe9b43e339ef94 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 20:14:33 -0700 Subject: [PATCH 51/86] Add Node.js note --- UPGRADE.TXT | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/UPGRADE.TXT b/UPGRADE.TXT index cd5ecb91..be9e0e8e 100644 --- a/UPGRADE.TXT +++ b/UPGRADE.TXT @@ -40,6 +40,13 @@ file a issue on GitHub. 0.0.1-alpha to 0.0.4-alpha ------------------------------------------------------------------------------- +* Node.js 6.x+ LTS is now required + You will need to upgrade Node.js to 6.x+. If using nvm (you should be!) + the process will go something like this: + + nvm install 6 + nvm alias default 6 + * Manual Database Upgrade sqlite3 db/message.sqlite INSERT INTO message_fts(message_fts) VALUES('rebuild'); From 5f929b3d63926288299968aed9f12143527852dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 20:14:56 -0700 Subject: [PATCH 52/86] comment --- core/button_view.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/button_view.js b/core/button_view.js index dec1cd10..007ca29f 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -1,10 +1,9 @@ /* jslint node: true */ 'use strict'; -var TextView = require('./text_view.js').TextView; -var miscUtil = require('./misc_util.js'); -var util = require('util'); -var assert = require('assert'); +const TextView = require('./text_view.js').TextView; +const miscUtil = require('./misc_util.js'); +const util = require('util'); exports.ButtonView = ButtonView; @@ -20,7 +19,8 @@ function ButtonView(options) { util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(ch, key) { - if(' ' === ch) { + // allow space = submit + if(' ' === ch) { this.emit('action', 'accept'); } From f0db0e3c948f6ca9fe04b241befc468ce788b3b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 20:20:10 -0700 Subject: [PATCH 53/86] * file.db: file_user_rating: Table for tracking average user rating of a file * Default filter order to descending * File rating support including in search/filter * Default to passing submitted form data (if any) @ prevMenu() * Fix issues with byte/size formatting for 0 * Allow action keys for prompts * use MenuModule.pausePrompt() in various places * Add quick search to file area * Display dupes, if any @ upload --- core/database.js | 11 ++ core/file_base_filter.js | 4 +- core/file_entry.js | 115 ++++++++++----- core/menu_module.js | 39 +++-- core/string_util.js | 6 +- core/system_menu_method.js | 10 +- core/theme.js | 271 ++++++++++++++++++++++------------ core/view_controller.js | 103 ++++++++----- mods/abracadabra.js | 5 +- mods/file_area_filter_edit.js | 12 +- mods/file_area_list.js | 75 ++++++++-- mods/file_base_search.js | 119 +++++++++++++++ mods/msg_area_list.js | 4 +- mods/msg_conf_list.js | 4 +- mods/prompt.hjson | 49 +++++- mods/upload.js | 117 ++++++++++++--- 16 files changed, 714 insertions(+), 230 deletions(-) create mode 100644 mods/file_base_search.js diff --git a/core/database.js b/core/database.js index 76c4d72b..6dca0101 100644 --- a/core/database.js +++ b/core/database.js @@ -214,6 +214,7 @@ const DB_INIT_TABLE = { );` ); + // :TODO: need SQL to ensure cleaned up if delete from message? /* dbs.message.run( @@ -335,6 +336,16 @@ const DB_INIT_TABLE = { );` ); + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_user_rating ( + file_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + rating INTEGER NOT NULL, + + UNIQUE(file_id, user_id) + );` + ); + dbs.file.run( `CREATE TABLE IF NOT EXISTS file_web_serve ( hash_id VARCHAR NOT NULL PRIMARY KEY, diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 75938a26..801ae47e 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -12,7 +12,7 @@ module.exports = class FileBaseFilters { } static get OrderByValues() { - return [ 'ascending', 'descending' ]; + return [ 'descending', 'ascending' ]; } static get SortByValues() { @@ -116,7 +116,7 @@ module.exports = class FileBaseFilters { areaTag : '', // all terms : '', // * tags : '', // * - order : 'ascending', + order : 'descending', sort : 'upload_timestamp', uuid : uuid, }; diff --git a/core/file_entry.js b/core/file_entry.js index cb208008..57276807 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -26,7 +26,6 @@ const FILE_WELL_KNOWN_META = { est_release_year : (y) => parseInt(y) || new Date().getFullYear(), dl_count : (d) => parseInt(d) || 0, byte_size : (b) => parseInt(b) || 0, - user_rating : (r) => Math.min(parseInt(r) || 0, 5), archive_type : null, }; @@ -38,50 +37,61 @@ module.exports = class FileEntry { this.areaTag = options.areaTag || ''; this.meta = options.meta || { // values we always want - user_rating : 0, dl_count : 0, }; - + this.hashTags = options.hashTags || new Set(); this.fileName = options.fileName; this.storageTag = options.storageTag; } + static loadBasicEntry(fileId, dest, cb) { + if(!cb && _.isFunction(dest)) { + cb = dest; + dest = this; + } + + fileDb.get( + `SELECT ${FILE_TABLE_MEMBERS.join(', ')} + FROM file + WHERE file_id=? + LIMIT 1;`, + [ fileId ], + (err, file) => { + if(err) { + return cb(err); + } + + if(!file) { + return cb(Errors.DoesNotExist('No file is available by that ID')); + } + + // assign props from |file| + FILE_TABLE_MEMBERS.forEach(prop => { + dest[_.camelCase(prop)] = file[prop]; + }); + + return cb(null); + } + ); + } + load(fileId, cb) { const self = this; async.series( [ function loadBasicEntry(callback) { - fileDb.get( - `SELECT ${FILE_TABLE_MEMBERS.join(', ')} - FROM file - WHERE file_id=? - LIMIT 1;`, - [ fileId ], - (err, file) => { - if(err) { - return callback(err); - } - - if(!file) { - return callback(Errors.DoesNotExist('No file is available by that ID')); - } - - // assign props from |file| - FILE_TABLE_MEMBERS.forEach(prop => { - self[_.camelCase(prop)] = file[prop]; - }); - - return callback(null); - } - ); + FileEntry.loadBasicEntry(fileId, self, callback); }, function loadMeta(callback) { return self.loadMeta(callback); }, function loadHashTags(callback) { return self.loadHashTags(callback); + }, + function loadUserRating(callback) { + return self.loadRating(callback); } ], err => { @@ -156,10 +166,19 @@ module.exports = class FileEntry { return paths.join(storageDir, this.fileName); } + static persistUserRating(fileId, userId, rating, cb) { + return fileDb.run( + `REPLACE INTO file_user_rating (file_id, user_id, rating) + VALUES (?, ?, ?);`, + [ fileId, userId, rating ], + cb + ); + } + static persistMetaValue(fileId, name, value, cb) { - fileDb.run( + return fileDb.run( `REPLACE INTO file_meta (file_id, meta_name, meta_value) - VALUES(?, ?, ?);`, + VALUES (?, ?, ?);`, [ fileId, name, value ], cb ); @@ -243,6 +262,23 @@ module.exports = class FileEntry { ); } + loadRating(cb) { + fileDb.get( + `SELECT AVG(fur.rating) AS avg_rating + FROM file_user_rating fur + INNER JOIN file f + ON f.file_id = fur.file_id + AND f.file_id = ?`, + [ this.fileId ], + (err, result) => { + if(result) { + this.userRating = result.avg_rating; + } + return cb(err); + } + ); + } + setHashTags(hashTags) { if(_.isString(hashTags)) { this.hashTags = new Set(hashTags.split(/[\s,]+/)); @@ -264,7 +300,7 @@ module.exports = class FileEntry { const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; function getOrderByWithCast(ob) { - if( [ 'dl_count', 'user_rating', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { + if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { return `ORDER BY CAST(${ob} AS INTEGER)`; } @@ -290,11 +326,24 @@ module.exports = class FileEntry { sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; } else { - sql = - `SELECT f.file_id, f.${filter.sort} - FROM file f`; + // additional special treatment for user ratings: we need to average them + if('user_rating' === filter.sort) { + sql = + `SELECT f.file_id, + (SELECT IFNULL(AVG(rating), 0) rating + FROM file_user_rating + WHERE file_id = f.file_id) + AS avg_rating + FROM file f`; + + sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; + } else { + sql = + `SELECT f.file_id, f.${filter.sort} + FROM file f`; - sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; + sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; + } } } else { sql = diff --git a/core/menu_module.js b/core/menu_module.js index 2553335b..dfcf0589 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -166,7 +166,8 @@ exports.MenuModule = class MenuModule extends PluginModule { } getMenuResult() { - // nothing in base + // default to the formData that was provided @ a submit, if any + return this.submitFormData; } nextMenu(cb) { @@ -345,22 +346,42 @@ exports.MenuModule = class MenuModule extends PluginModule { ); } - pausePrompt(position, cb) { - if(!cb && _.isFunction(position)) { - cb = position; - position = null; - } - + optionalMoveToPosition(position) { if(position) { position.x = position.row || position.x || 1; position.y = position.col || position.y || 1; this.client.term.rawWrite(ansi.goto(position.x, position.y)); } - - return theme.displayThemedPause( { client : this.client }, cb); } + pausePrompt(position, cb) { + if(!cb && _.isFunction(position)) { + cb = position; + position = null; + } + + this.optionalMoveToPosition(position); + + return theme.displayThemedPause(this.client, cb); + } + + /* + :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) + promptForInput(formName, name, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + + options.viewController = this.viewControllers[formName]; + + this.optionalMoveToPosition(options.position); + + return theme.displayThemedPrompt(name, this.client, options, cb); + } + */ + setViewText(formName, mciId, text, appendMultiLine) { const view = this.viewControllers[formName].getView(mciId); if(!view) { diff --git a/core/string_util.js b/core/string_util.js index 5d953711..860b78d4 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -301,13 +301,17 @@ function renderStringLength(s) { const SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) function formatByteSizeAbbr(byteSize) { + if(0 === byteSize) { + return SIZE_ABBRS[0]; // B + } + return SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } function formatByteSize(byteSize, withAbbr, decimals) { withAbbr = withAbbr || false; decimals = decimals || 3; - const i = Math.floor(Math.log(byteSize) / Math.log(1024)); + const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); if(withAbbr) { result += ` ${SIZE_ABBRS[i]}`; diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 95a47a96..2439d7df 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -63,9 +63,15 @@ function logoff(callingMenu, formData, extraArgs, cb) { } function prevMenu(callingMenu, formData, extraArgs, cb) { + + // :TODO: this is a pretty big hack -- need the whole key map concep there like other places + if(formData.key && 'return' === formData.key.name) { + callingMenu.submitFormData = formData; + } + callingMenu.prevMenu( err => { if(err) { - callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to fallback!'); + callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); } return cb(err); }); @@ -74,7 +80,7 @@ function prevMenu(callingMenu, formData, extraArgs, cb) { function nextMenu(callingMenu, formData, extraArgs, cb) { callingMenu.nextMenu( err => { if(err) { - callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to go to next menu!'); + callingMenu.client.log.error( { error : err.message}, 'Error attempting to go to next menu!'); } return cb(err); }); diff --git a/core/theme.js b/core/theme.js index 19ed9ff4..7dd9aa0a 100644 --- a/core/theme.js +++ b/core/theme.js @@ -9,6 +9,7 @@ const configCache = require('./config_cache.js'); const getFullConfig = require('./config_util.js').getFullConfig; const asset = require('./asset.js'); const ViewController = require('./view_controller.js').ViewController; +const Errors = require('./enig_error.js').Errors; const fs = require('fs'); const paths = require('path'); @@ -23,6 +24,7 @@ exports.setClientTheme = setClientTheme; exports.initAvailableThemes = initAvailableThemes; exports.displayThemeArt = displayThemeArt; exports.displayThemedPause = displayThemedPause; +exports.displayThemedPrompt = displayThemedPrompt; exports.displayThemedAsset = displayThemedAsset; function refreshThemeHelpers(theme) { @@ -484,110 +486,187 @@ function displayThemeArt(options, cb) { }); } +/* +function displayThemedPrompt(name, client, options, cb) { + + async.waterfall( + [ + function loadConfig(callback) { + configCache.getModConfig('prompt.hjson', (err, promptJson) => { + if(err) { + return callback(err); + } + + if(_.has(promptJson, [ 'prompts', name ] )) { + return callback(Errors.DoesNotExist(`Prompt "${name}" does not exist`)); + } + + const promptConfig = promptJson.prompts[name]; + if(!_.isObject(promptConfig)) { + return callback(Errors.Invalid(`Prompt "${name} is invalid`)); + } + + return callback(null, promptConfig); + }); + }, + function display(promptConfig, callback) { + if(options.clearScreen) { + client.term.rawWrite(ansi.clearScreen()); + } + + // + // If we did not clear the screen, don't let the font change + // + const dispOptions = Object.assign( {}, promptConfig.options ); + if(!options.clearScreen) { + dispOptions.font = 'not_really_a_font!'; + } + + displayThemedAsset( + promptConfig.art, + client, + dispOptions, + (err, artData) => { + if(err) { + return callback(err); + } + + return callback(null, promptConfig, artData.mciMap); + } + ); + }, + function prepViews(promptConfig, mciMap, callback) { + vc = new ViewController( { client : client } ); + + const loadOpts = { + promptName : name, + mciMap : mciMap, + config : promptConfig, + }; + + vc.loadFromPromptConfig(loadOpts, err => { + callback(null); + }); + } + ] + ); +} +*/ + +function displayThemedPrompt(name, client, options, cb) { + + const useTempViewController = _.isUndefined(options.viewController); + + async.waterfall( + [ + function display(callback) { + const promptConfig = client.currentTheme.prompts[name]; + if(!promptConfig) { + return callback(Errors.DoesNotExist(`Missing "${name}" prompt configuration!`)); + } + + if(options.clearScreen) { + client.term.rawWrite(ansi.clearScreen()); + } + + // + // If we did *not* clear the screen, don't let the font change + // as it will mess with the output of the existing art displayed in a terminal + // + const dispOptions = Object.assign( {}, promptConfig.options ); + if(!options.clearScreen) { + dispOptions.font = 'not_really_a_font!'; // kludge :) + } + + displayThemedAsset( + promptConfig.art, + client, + dispOptions, + (err, artInfo) => { + return callback(err, promptConfig, artInfo); + } + ); + }, + function discoverCursorPosition(promptConfig, artInfo, callback) { + if(!options.clearPrompt) { + // no need to query cursor - we're not gonna use it + return callback(null, promptConfig, artInfo); + } + + client.once('cursor position report', pos => { + artInfo.startRow = pos[0] - artInfo.height; + return callback(null, promptConfig, artInfo); + }); + + client.term.rawWrite(ansi.queryPos()); + }, + function createMCIViews(promptConfig, artInfo, callback) { + const tempViewController = useTempViewController ? new ViewController( { client : client } ) : options.viewController; + + const loadOpts = { + promptName : name, + mciMap : artInfo.mciMap, + config : promptConfig, + }; + + tempViewController.loadFromPromptConfig(loadOpts, () => { + return callback(null, artInfo, tempViewController); + }); + }, + function pauseForUserInput(artInfo, tempViewController, callback) { + if(!options.pause) { + return callback(null, artInfo, tempViewController); + } + + client.waitForKeyPress( () => { + return callback(null, artInfo, tempViewController); + }); + }, + function clearPauseArt(artInfo, tempViewController, callback) { + if(options.clearPrompt) { + if(artInfo.startRow && artInfo.height) { + client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); + + // Note: Does not work properly in NetRunner < 2.0b17: + client.term.rawWrite(ansi.deleteLine(artInfo.height)); + } else { + client.term.rawWrite(ansi.eraseLine(1)); + } + } + + return callback(null, tempViewController); + } + ], + (err, tempViewController) => { + if(err) { + client.log.warn( { error : err.message }, `Failed displaying "${name}" prompt` ); + } + + if(tempViewController && useTempViewController) { + tempViewController.detachClientEvents(); + } + + return cb(null); + } + ); +} + // // Pause prompts are a special prompt by the name 'pause'. // -function displayThemedPause(options, cb) { - // - // options.client - // options clearPrompt - // - assert(_.isObject(options.client)); +function displayThemedPause(client, options, cb) { + + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } if(!_.isBoolean(options.clearPrompt)) { options.clearPrompt = true; } - // :TODO: Support animated pause prompts. Probably via MCI with AnimatedView - - var artInfo; - var vc; - var promptConfig; - - async.series( - [ - function loadPromptJSON(callback) { - configCache.getModConfig('prompt.hjson', function loaded(err, promptJson) { - if(err) { - callback(err); - } else { - if(_.has(promptJson, [ 'prompts', 'pause' ] )) { - promptConfig = promptJson.prompts.pause; - callback(_.isObject(promptConfig) ? null : new Error('Invalid prompt config block!')); - } else { - callback(new Error('Missing standard \'pause\' prompt')); - } - } - }); - }, - function displayPausePrompt(callback) { - // - // Override .font so it doesn't change from current setting - // - var dispOptions = promptConfig.options; - dispOptions.font = 'not_really_a_font!'; - - displayThemedAsset( - promptConfig.art, - options.client, - dispOptions, - function displayed(err, artData) { - artInfo = artData; - callback(err); - } - ); - }, - function discoverCursorPosition(callback) { - options.client.once('cursor position report', function cpr(pos) { - artInfo.startRow = pos[0] - artInfo.height; - callback(null); - }); - options.client.term.rawWrite(ansi.queryPos()); - }, - function createMCIViews(callback) { - vc = new ViewController( { client : options.client, noInput : true } ); - vc.loadFromPromptConfig( { promptName : 'pause', mciMap : artInfo.mciMap, config : promptConfig }, function loaded(err) { - callback(null); - }); - }, - function pauseForUserInput(callback) { - options.client.waitForKeyPress(function keyPressed() { - callback(null); - }); - }, - function clearPauseArt(callback) { - if(options.clearPrompt) { - if(artInfo.startRow && artInfo.height) { - options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); - - // Note: Does not work properly in NetRunner < 2.0b17: - options.client.term.rawWrite(ansi.deleteLine(artInfo.height)); - } else { - options.client.term.rawWrite(ansi.eraseLine(1)) - } - } - callback(null); - } - /* - , function debugPause(callback) { - setTimeout(function to() { - callback(null); - }, 4000); - } - */ - ], - function complete(err) { - if(err) { - Log.error(err); - } - - if(vc) { - vc.detachClientEvents(); - } - - cb(); - } - ); + const promptOptions = Object.assign( {}, options, { pause : true } ); + return displayThemedPrompt('pause', client, promptOptions, cb); } function displayThemedAsset(assetSpec, client, options, cb) { diff --git a/core/view_controller.js b/core/view_controller.js index 51c63b3a..ecf36be5 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -184,52 +184,52 @@ function ViewController(options) { propAsset = asset.getViewPropertyAsset(conf[propName]); if(propAsset) { switch(propAsset.type) { - case 'config' : - propValue = asset.resolveConfigAsset(conf[propName]); - break; - - case 'sysStat' : - propValue = asset.resolveSystemStatAsset(conf[propName]); - break; + case 'config' : + propValue = asset.resolveConfigAsset(conf[propName]); + break; + + case 'sysStat' : + propValue = asset.resolveSystemStatAsset(conf[propName]); + break; - // :TODO: handle @art (e.g. text : @art ...) + // :TODO: handle @art (e.g. text : @art ...) - case 'method' : - case 'systemMethod' : - if('validate' === propName) { - // :TODO: handle propAsset.location for @method script specification - if('systemMethod' === propAsset.type) { - // :TODO: implementation validation @systemMethod handling! - var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); - if(_.isFunction(methodModule[propAsset.asset])) { - propValue = methodModule[propAsset.asset]; - } - } else { - if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) { - propValue = self.client.currentMenuModule.menuMethods[propAsset.asset]; - } - } - } else { - if(_.isString(propAsset.location)) { - - } else { + case 'method' : + case 'systemMethod' : + if('validate' === propName) { + // :TODO: handle propAsset.location for @method script specification if('systemMethod' === propAsset.type) { - // :TODO: + // :TODO: implementation validation @systemMethod handling! + var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); + if(_.isFunction(methodModule[propAsset.asset])) { + propValue = methodModule[propAsset.asset]; + } } else { - // local to current module - var currentModule = self.client.currentMenuModule; - if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { - // :TODO: Fix formData & extraArgs... this all needs general processing - propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) { + propValue = self.client.currentMenuModule.menuMethods[propAsset.asset]; + } + } + } else { + if(_.isString(propAsset.location)) { + + } else { + if('systemMethod' === propAsset.type) { + // :TODO: + } else { + // local to current module + var currentModule = self.client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { + // :TODO: Fix formData & extraArgs... this all needs general processing + propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + } } } } - } - break; + break; - default : - propValue = propValue = conf[propName]; - break; + default : + propValue = propValue = conf[propName]; + break; } } else { propValue = conf[propName]; @@ -601,6 +601,33 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { callback(null); }, + function loadActionKeys(callback) { + if(!_.isObject(promptConfig) || !_.isArray(promptConfig.actionKeys)) { + return callback(null); + } + + promptConfig.actionKeys.forEach(ak => { + // + // * 'keys' must be present and be an array of key names + // * If 'viewId' is present, key(s) will focus & submit on behalf + // of the specified view. + // * If 'action' is present, that action will be procesed when + // triggered by key(s) + // + // Ultimately, create a map of key -> { action block } + // + if(!_.isArray(ak.keys)) { + return; + } + + ak.keys.forEach(kn => { + self.actionKeyMap[kn] = ak; + }); + + }); + + return callback(null); + }, function drawAllViews(callback) { self.redrawAll(initialFocusId); callback(null); diff --git a/mods/abracadabra.js b/mods/abracadabra.js index 4b9afdbf..a84d2c63 100644 --- a/mods/abracadabra.js +++ b/mods/abracadabra.js @@ -99,8 +99,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { if(_.isString(self.config.tooManyArt)) { theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { - // :TODO: Use MenuModule.pausePrompt() - theme.displayThemedPause( { client : self.client }, function keyPressed() { + self.pausePrompt( () => { callback(new Error('Too many active instances')); }); }); @@ -108,7 +107,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { self.client.term.write('\nToo many active instances. Try again later.\n'); // :TODO: Use MenuModule.pausePrompt() - theme.displayThemedPause( { client : self.client }, function keyPressed() { + self.pausePrompt( () => { callback(new Error('Too many active instances')); }); } diff --git a/mods/file_area_filter_edit.js b/mods/file_area_filter_edit.js index 5cb211ac..00d13a2e 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/file_area_filter_edit.js @@ -89,7 +89,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { }, newFilter : (formData, extraArgs, cb) => { this.currentFilterIndex = this.filtersArray.length; // next avail slot - this.clearForm(true); // true=reset focus + this.clearForm(MciViewIds.editor.searchTerms); return cb(null); }, deleteFilter : (formData, extraArgs, cb) => { @@ -115,10 +115,12 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } // update UI + this.updateActiveLabel(); + if(this.filtersArray.length > 0) { this.loadDataForFilter(this.currentFilterIndex); } else { - this.clearForm(true); // true=reset focus + this.clearForm(); } return cb(null); }); @@ -203,7 +205,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } } - clearForm(setFocus) { + clearForm(newFocusId) { [ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => { this.setText(mciId, ''); }); @@ -212,7 +214,9 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { this.setFocusItemIndex(mciId, 0); }); - if(setFocus) { + if(newFocusId) { + this.viewControllers.editor.switchFocus(newFocusId); + } else { this.viewControllers.editor.resetInitialFocus(); } } diff --git a/mods/file_area_list.js b/mods/file_area_list.js index b946497b..2f590a1f 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -78,9 +78,17 @@ exports.getModule = class FileAreaList extends MenuModule { this.dlQueue = new DownloadQueue(this.client); - this.filterCriteria = this.filterCriteria || { - // :TODO: set area tag - all in current area by default - }; + 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) => { @@ -116,7 +124,7 @@ exports.getModule = class FileAreaList extends MenuModule { }, showWebDownloadLink : (formData, extraArgs, cb) => { return this.fetchAndDisplayWebDownloadLink(cb); - }, + } }; } @@ -128,11 +136,46 @@ exports.getModule = class FileAreaList extends MenuModule { 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); }, @@ -165,11 +208,12 @@ exports.getModule = class FileAreaList extends MenuModule { 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(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator, webDlLink : '', // :TODO: fetch web any existing web d/l link - webDlExpire : '', // :TODO: fetch web d/l link expire time + webDlExpire : '', // :TODO: fetch web d/l link expire time }; // @@ -196,7 +240,7 @@ exports.getModule = class FileAreaList extends MenuModule { // create a rating string, e.g. "**---" const userRatingTicked = config.userRatingTicked || '*'; const userRatingUnticked = config.userRatingUnticked || ''; - entryInfo.userRating = entryInfo.userRating || 0; // be safe! + 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); @@ -297,7 +341,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(self.fileList) { return callback(null); } - return self.loadFileIds(callback); + return self.loadFileIds(false, callback); // false=do not force }, function loadCurrentFileInfo(callback) { self.currentFileEntry = new FileEntry(); @@ -566,14 +610,13 @@ exports.getModule = class FileAreaList extends MenuModule { ); } - loadFileIds(cb) { - this.fileListPosition = 0; - const activeFilter = FileBaseFilters.getActiveFilter(this.client); - - FileEntry.findFiles(activeFilter, (err, fileIds) => { - this.fileList = fileIds; - 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); + }); + } } - }; diff --git a/mods/file_base_search.js b/mods/file_base_search.js new file mode 100644 index 00000000..87c127cd --- /dev/null +++ b/mods/file_base_search.js @@ -0,0 +1,119 @@ +/* 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); + if(areasView) { + areasView.setItems( self.availAreas.map( a => a.name ) ); + } + + 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, + } + }; + + return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); + } +}; diff --git a/mods/msg_area_list.js b/mods/msg_area_list.js index cb8d14bb..a6a0df4c 100644 --- a/mods/msg_area_list.js +++ b/mods/msg_area_list.js @@ -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'); @@ -76,8 +75,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { if(_.has(area, 'options.pause') && false === area.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { - // :TODO: Use MenuModule.pausePrompt() - displayThemedPause( { client : self.client }, () => { + self.pausePrompt( () => { return self.prevMenu(cb); }); } diff --git a/mods/msg_conf_list.js b/mods/msg_conf_list.js index 0d6b6202..91c24de4 100644 --- a/mods/msg_conf_list.js +++ b/mods/msg_conf_list.js @@ -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'); @@ -63,8 +62,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { if(_.has(conf, 'options.pause') && false === conf.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { - // :TODO: Use MenuModule.pausePrompt() - displayThemedPause( { client : self.client }, () => { + self.pausePrompt( () => { return self.prevMenu(cb); }); } diff --git a/mods/prompt.hjson b/mods/prompt.hjson index 3dac7326..9083f5d4 100644 --- a/mods/prompt.hjson +++ b/mods/prompt.hjson @@ -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,6 +132,25 @@ } } }, + + // File Base Related + fileBaseRateEntryPrompt: { + art: RATEFILE + mci: { + SM1: { + argName: rating + items: [ "-----", "*----", "**---", "***--", "****-", "*****" ] + } + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + /////////////////////////////////////////////////////////////////////// // Standard / Required /////////////////////////////////////////////////////////////////////// diff --git a/mods/upload.js b/mods/upload.js index d06b1fa6..f2bbe888 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -7,11 +7,14 @@ 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'); @@ -30,7 +33,7 @@ const FormIds = { options : 0, processing : 1, fileDetails : 2, - + dupes : 3, }; const MciViewIds = { @@ -56,6 +59,10 @@ const MciViewIds = { estYear : 3, accept : 4, // accept fields & continue customRangeStart : 10, // 10+ = customs + }, + + dupes : { + dupeList : 1, } }; @@ -161,17 +168,6 @@ exports.getModule = class UploadModule extends MenuModule { } } - leave() { - // remove any temp files - only do this when - if(this.isFileTransferComplete()) { - temptmp.cleanup( paths => { - Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); - }); - } - - super.leave(); - } - performUpload(cb) { temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => { if(err) { @@ -341,6 +337,12 @@ exports.getModule = class UploadModule extends MenuModule { }); } + cleanupTempFiles() { + temptmp.cleanup( paths => { + Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); + }); + } + moveAndPersistUploadsToDatabase(newEntries) { const areaStorageDir = getAreaDefaultStorageDirectory(this.areaInfo); @@ -370,6 +372,11 @@ exports.getModule = class UploadModule extends MenuModule { return nextEntry(null); // still try next file }); }); + }, () => { + // + // Finally, we can remove any temp files that we may have created + // + self.cleanupTempFiles(); }); } @@ -401,6 +408,75 @@ exports.getModule = class UploadModule extends MenuModule { }); } + 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}'; + + dupes.forEach(dupe => { + const formatted = stringFormat(dupeInfoFormat, dupe); + if(dupeListView) { + // dupesInfoFormatX + dupeListView.addText(enigmaToAnsi(formatted)); + } else { + self.client.term.pipeWrite(`${formatted}\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 @@ -437,15 +513,16 @@ exports.getModule = class UploadModule extends MenuModule { self.pausePrompt( () => { return callback(null, scanResults); - }); + }); }, function displayDupes(scanResults, callback) { if(0 === scanResults.dupes.length) { return callback(null, scanResults); } - // :TODO: display dupe info - return callback(null, scanResults); + return self.displayDupesPage(scanResults.dupes, () => { + return callback(null, scanResults); + }); }, function prepDetails(scanResults, callback) { return self.prepDetailsForUpload(scanResults, callback); @@ -463,6 +540,7 @@ exports.getModule = class UploadModule extends MenuModule { 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(); @@ -565,13 +643,16 @@ exports.getModule = class UploadModule extends MenuModule { 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.setText(fileEntry.desc); descView.setPropertyValue('mode', 'preview'); - self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.tags); + 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); } From 92e804b64bef12638560ffa521e5c4cfa326ca2a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 22:04:37 -0700 Subject: [PATCH 54/86] Use markdown for UPGRADE --- UPGRADE.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 UPGRADE.md diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 00000000..22815e9c --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,58 @@ +# Introduction +This document covers basic upgrade notes for major ENiGMA½. + + +# Before Upgrading +* Always back ALL files in the 'db' directory +* Back up your menu.hjson (or renamed equivalent) + + +# General Notes +Upgrades often come with changes to the default `menu.hjson`. It is wise to +use a *different* file name for your BBS's version of this file and point to +it via `config.hjson`. For example: + +```hjson +general: { + menuFile: my_bbs.hjson +} +``` + +After updating code, use a program such as DiffMerge to merge in updates to +`my_bbs.hjson` from the shipping `menu.hjson`. + + +# Pulling Latest From GitHub +Upgrading from GitHub is easy: + +```bash +cd /path/to/enigma-bbs +git pull +npm install +``` + + +# Problems +Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or +[file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). + + +# 0.0.1-alpha to 0.0.4-alpha +## Node.js 6.x+ LTS is now **required** +You will need to upgrade Node.js to 6.x+. If using nvm (you should be!) the process will go something like this: +```bash +nvm install 6 +nvm alias default 6 +``` + +## Manual Database Upgrade +A few upgrades need to be made to your SQLite databases: + +```bash +rm db/file.sqltie3 # safe to delete this time as it was not used previously +sqlite3 db/message.sqlite +sqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild'); +``` + +## Archiver Changes +If you have overridden or made additions to archivers in your `config.hjson` you will need to update them. See [docs/archive.md](Archive Configuration) and `core/config.js` From a18e4a1527309c5fa10f6821294ce61372e3cd09 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 22:05:50 -0700 Subject: [PATCH 55/86] Remove old text version --- UPGRADE.TXT | 56 ----------------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 UPGRADE.TXT diff --git a/UPGRADE.TXT b/UPGRADE.TXT deleted file mode 100644 index be9e0e8e..00000000 --- a/UPGRADE.TXT +++ /dev/null @@ -1,56 +0,0 @@ -INTRODUCTION -------------------------------------------------------------------------------- -This document covers basic upgrade notes for major ENiGMA½. - - -BEFORE UPGRADING -------------------------------------------------------------------------------- -* Always back ALL files in the 'db' directory -* Back up your menu.hjson (or renamed equivalent) - - -GENERAL NOTES -------------------------------------------------------------------------------- -Upgrades often come with changes to the default menu.hjson. It is wise to -use a *different* file name for your BBS's version of this file and point to -it via config.hjson. For example: - -general: { - menuFile: my_bbs.hjson -} - -After updating code, use a program such as DiffMerge to merge in updates to -my_bbs.hjson from the shipping menu.hjson. - - -FROM GITHUB -------------------------------------------------------------------------------- -Upgrading from GitHub is easy: - - cd /path/to/enigma-bbs - git pull - npm install - - -PROBLEMS -------------------------------------------------------------------------------- -Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or -file a issue on GitHub. - - -0.0.1-alpha to 0.0.4-alpha -------------------------------------------------------------------------------- -* Node.js 6.x+ LTS is now required - You will need to upgrade Node.js to 6.x+. If using nvm (you should be!) - the process will go something like this: - - nvm install 6 - nvm alias default 6 - -* Manual Database Upgrade - sqlite3 db/message.sqlite - INSERT INTO message_fts(message_fts) VALUES('rebuild'); - -* Archiver Changes - If you have overridden or made additions to archivers in your config.hjson - you will need to update them. See docs/archive.md and core/config.js From 7478d647d894f6918119bf3f3923ec2b5e17fdc0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 22:09:44 -0700 Subject: [PATCH 56/86] Minor updates --- UPGRADE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 22815e9c..94bd4950 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,8 +3,8 @@ This document covers basic upgrade notes for major ENiGMA½. # Before Upgrading -* Always back ALL files in the 'db' directory -* Back up your menu.hjson (or renamed equivalent) +* Alwasy back up your system! +* At least back up the `db` directory and your `menu.hjson` (or renamed equivalent) # General Notes @@ -39,7 +39,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or # 0.0.1-alpha to 0.0.4-alpha ## Node.js 6.x+ LTS is now **required** -You will need to upgrade Node.js to 6.x+. If using nvm (you should be!) the process will go something like this: +You will need to upgrade Node.js to [6.x+](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V6.md). If using nvm (you should be!) the process will go something like this: ```bash nvm install 6 nvm alias default 6 @@ -55,4 +55,4 @@ sqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild'); ``` ## Archiver Changes -If you have overridden or made additions to archivers in your `config.hjson` you will need to update them. See [docs/archive.md](Archive Configuration) and `core/config.js` +If you have overridden or made additions to archivers in your `config.hjson` you will need to update them. See [Archive Configuration](docs/archive.md) and `core/config.js` From b49f2be0edf8678f82497633334c74e010141e1f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 22:14:14 -0700 Subject: [PATCH 57/86] Additional updates & typo fixes --- UPGRADE.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 94bd4950..76f9e532 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,9 +1,9 @@ # Introduction -This document covers basic upgrade notes for major ENiGMA½. +This document covers basic upgrade notes for major ENiGMA½ version updates. # Before Upgrading -* Alwasy back up your system! +* Always back up your system! * At least back up the `db` directory and your `menu.hjson` (or renamed equivalent) @@ -22,7 +22,7 @@ After updating code, use a program such as DiffMerge to merge in updates to `my_bbs.hjson` from the shipping `menu.hjson`. -# Pulling Latest From GitHub +# Upgrading the Code Upgrading from GitHub is easy: ```bash @@ -39,7 +39,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or # 0.0.1-alpha to 0.0.4-alpha ## Node.js 6.x+ LTS is now **required** -You will need to upgrade Node.js to [6.x+](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V6.md). If using nvm (you should be!) the process will go something like this: +You will need to upgrade Node.js to [6.x+](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V6.md). If using [nvm](https://github.com/creationix/nvm) (you should be!) the process will go something like this: ```bash nvm install 6 nvm alias default 6 @@ -56,3 +56,6 @@ sqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild'); ## Archiver Changes If you have overridden or made additions to archivers in your `config.hjson` you will need to update them. See [Archive Configuration](docs/archive.md) and `core/config.js` + +## File Base Configuration +As 0.0.4-alpha contains file bases, you'll want to create a suitable configuration if you wish to use the feature. See [File Base Configuration](docs/file_base.md). From f4042e655624d7280216ca2caaee6b91fdcf1503 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 22:15:20 -0700 Subject: [PATCH 58/86] Placeholder --- docs/file_base.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/file_base.md diff --git a/docs/file_base.md b/docs/file_base.md new file mode 100644 index 00000000..8fe1ffb1 --- /dev/null +++ b/docs/file_base.md @@ -0,0 +1,2 @@ +# File Bases +TODO: Document file base configuration \ No newline at end of file From 5549ff5512f6a922615832299882716826a0de95 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Feb 2017 22:15:34 -0700 Subject: [PATCH 59/86] * Help pages * No results for criteria page * noHistory can be passed to gotoMenu() --- core/menu_stack.js | 5 ++++- mods/file_area_list.js | 38 ++++++++++++++++++++++++++++++++------ mods/file_base_search.js | 11 ++++++----- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/core/menu_stack.js b/core/menu_stack.js index f524ce75..f4b29460 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -99,6 +99,7 @@ module.exports = class MenuStack { if(!cb && _.isFunction(options)) { cb = options; + options = {}; } const self = this; @@ -134,7 +135,9 @@ module.exports = class MenuStack { currentModuleInfo.instance.leave(); - if(modInst.menuConfig.options.menuFlags.includes('noHistory')) { + const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags; + + if(menuFlags.includes('noHistory')) { this.pop().instance.leave(); // leave & remove current } } diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 2f590a1f..7a36f21b 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -124,6 +124,9 @@ exports.getModule = class FileAreaList extends MenuModule { }, showWebDownloadLink : (formData, extraArgs, cb) => { return this.fetchAndDisplayWebDownloadLink(cb); + }, + displayHelp : (formData, extraArgs, cb) => { + return this.displayHelpPage(cb); } }; } @@ -180,7 +183,12 @@ exports.getModule = class FileAreaList extends MenuModule { return self.beforeArt(callback); }, function display(callback) { - return self.displayBrowsePage(false, callback); + return self.displayBrowsePage(false, err => { + if(err && 'NORESULTS' === err.reasonCode) { + self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); + } + return callback(err); + }); } ], () => { @@ -334,16 +342,22 @@ exports.getModule = class FileAreaList extends MenuModule { async.series( [ - function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); - }, function fetchEntryData(callback) { if(self.fileList) { return callback(null); } return self.loadFileIds(false, callback); // false=do not force }, - function loadCurrentFileInfo(callback) { + 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 => { @@ -381,7 +395,7 @@ exports.getModule = class FileAreaList extends MenuModule { } } ], - err => { + err => { if(cb) { return cb(err); } @@ -428,6 +442,18 @@ exports.getModule = class FileAreaList extends MenuModule { } ); } + + displayHelpPage(cb) { + this.displayAsset( + this.menuConfig.config.art.help, + { clearScreen : true }, + () => { + this.client.waitForKeyPress( () => { + return this.displayBrowsePage(true, cb); + }); + } + ); + } fetchAndDisplayWebDownloadLink(cb) { const self = this; diff --git a/mods/file_base_search.js b/mods/file_base_search.js index 87c127cd..e984e1a4 100644 --- a/mods/file_base_search.js +++ b/mods/file_base_search.js @@ -58,9 +58,9 @@ exports.getModule = class FileBaseSearch extends MenuModule { self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); const areasView = vc.getView(MciViewIds.search.area); - if(areasView) { - areasView.setItems( self.availAreas.map( a => a.name ) ); - } + areasView.setItems( self.availAreas.map( a => a.name ) ); + areasView.redraw(); + vc.switchFocus(MciViewIds.search.searchTerms); return callback(null); } @@ -110,8 +110,9 @@ exports.getModule = class FileBaseSearch extends MenuModule { const menuOpts = { extraArgs : { - filterCriteria : filterCriteria, - } + filterCriteria : filterCriteria, + }, + menuFlags : [ 'noHistory' ], }; return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); From 1f879c8bfbd2d65f25b6283d57c25c04bbe41be8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 8 Feb 2017 20:57:31 -0700 Subject: [PATCH 60/86] Initial version of file base docs --- docs/file_base.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/file_base.md b/docs/file_base.md index 8fe1ffb1..9da9ff53 100644 --- a/docs/file_base.md +++ b/docs/file_base.md @@ -1,2 +1,75 @@ # File Bases -TODO: Document file base configuration \ No newline at end of file +Starting with version 0.0.4-alpha, ENiGMA½ has support for File Bases! Documentation below covers setup of file area(s), but first some information on what to expect: + +## A Different Appoach +ENiGMA½ has strayed away from the old familure setup here and instead takes a more modern approach: +* [Gazelle](https://whatcd.github.io/Gazelle/) inspired system for searching & browsing files +* No File Conferences +* File Areas are still around but should generally be used less. Instead, files can have one or more tags. Think things like `dos.retro`, `pc.warez`, `game`, etc. +* Temporary web (http:// or https://) download links in additional to standard X/Y/Z protocol support +* Users can star rate files & search/filter by ratings + +## Other bells and whistles +* A given area can span one to many physical storage locations +* Upload processor can extract and use `FILE_ID.DIZ`/`DESC.SDI`, for standard descriptions as well as `README.TXT`, `*.NFO`, and so on for longer descriptions +* Upload processor also attempts release year estimation by scanning prementioned description file(s) +* Fast indexed Full Text Search (FTS) +* Duplicates validated by SHA-256 + +## Configuration +Like many things in ENiGMA½, configuration of file base(s) is handled via `config.hjson` entries in the `fileBase` section. + +```hjson +fileBase: { + areaStoragePrefix: /path/to/somewhere/ + + storageTags: { + /* ... */ + } + + areas: { + /* ... */ + } +} +``` + +### Storage tags +**Storage Tags** define paths to a physical (file) storage location that can later be referenced in a file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths are relative to the value set by the `areaStoragePrefix` key. Below is an example defining a both a relative and fully qualified path each attached to a storage tag: + +```hjson +storageTags: { + retro_pc: "retro_pc" // relative + retro_pc_bbs: "retro_pc/bbs" // still relative! + bbs_stuff: "/path/to/bbs_stuff_storage" // fully qualified +} +``` + +### Areas +File base **Areas** are configured using the `fileBase::areas` configuration block in `config.hjson`. Each entry within `areas` must contain a `name`, `desc`, and `storageTags`. Remember that in ENiGMA½ while areas are important, they should generally be used less than in tradditional BBS software. It is recommended to favor the use of more **tags** over more areas. + +Example areas section: +```hjson +areas: { + retro_pc: { + name: Retro PC + desc: Oldschool PC/DOS + storageTags: [ "retro_pc", "retro_pc_bbs" ] + acs: { + write: GM[users] /* optional, see Uploads below */ + } + } +} +``` + +#### ACS +If no `acs` block is supplied, the following defaults apply to an area: +* `read` (list, download, etc.): `GM[users]` +* `write` (upload): `GM[sysops]` + +To override read and/or write ACS, supply a valid `acs` member. + +#### Uploads +Note that `storageTags` may contain *1:n* storage tag references. **Uploads in a particular area are stored in the first storage tag path**. + +## oputil +The `oputil.js` +op utilty `file-base` command has tools for managing file bases. See `oputil.js file-base --help`. \ No newline at end of file From a257a9ba4b7a1de7f53f751e8d8d0e93642b477c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 8 Feb 2017 22:53:48 -0700 Subject: [PATCH 61/86] * Fix up and improve oputil file-base stuff * Specialize if user does not have upload ACS --- .github/ISSUE_TEMPLATE.md | 2 +- core/file_base_area.js | 19 ++++++++++++++++--- docs/file_base.md | 2 +- main.js | 1 - mods/upload.js | 20 +++++++++++++++----- oputil.js | 24 ++++++++++++++++-------- 6 files changed, 49 insertions(+), 19 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a34168f3..5d3f54be 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ For :bug: bug reports, please fill out the information below plus any additional **Short problem description** **Environment** -- [ ] I am using Node.js v4.x or higher +- [ ] I am using Node.js v6.x or higher - [ ] `npm install` reports success - Actual Node.js version (`node --version`): - Operating system (`uname -a` on *nix systems): diff --git a/core/file_base_area.js b/core/file_base_area.js index 9a1a3a0a..cff77071 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -238,6 +238,13 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } } +// a simple log proxy for when we call from oputil.js +function logDebug(obj, msg) { + if(Log) { + Log.debug(obj, msg); + } +} + function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) { const archiveUtil = ArchiveUtil.getInstance(); const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() @@ -334,7 +341,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) { - Log.debug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); + logDebug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); return next(null); } @@ -355,7 +362,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c // cleanup but don't wait temptmp.cleanup( paths => { // note: don't use client logger here - may not be avail - Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); + logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); }); return callback(null); }); @@ -571,7 +578,12 @@ function scanFile(filePath, options, iterator, cb) { ); } -function scanFileAreaForChanges(areaInfo, cb) { +function scanFileAreaForChanges(areaInfo, iterator, cb) { + if(!cb && _.isFunction(iterator)) { + cb = iterator; + iterator = null; + } + const storageLocations = getAreaStorageLocations(areaInfo); async.eachSeries(storageLocations, (storageLoc, nextLocation) => { @@ -604,6 +616,7 @@ function scanFileAreaForChanges(areaInfo, cb) { areaTag : areaInfo.areaTag, storageTag : storageLoc.storageTag }, + iterator, (err, fileEntry, dupeEntries) => { if(err) { // :TODO: Log me!!! diff --git a/docs/file_base.md b/docs/file_base.md index 9da9ff53..3a10ed97 100644 --- a/docs/file_base.md +++ b/docs/file_base.md @@ -55,7 +55,7 @@ areas: { desc: Oldschool PC/DOS storageTags: [ "retro_pc", "retro_pc_bbs" ] acs: { - write: GM[users] /* optional, see Uploads below */ + write: GM[users] /* optional, see ACS below */ } } } diff --git a/main.js b/main.js index ab8d2652..ef624294 100755 --- a/main.js +++ b/main.js @@ -1,7 +1,6 @@ #!/usr/bin/env node /* jslint node: true */ - 'use strict'; /* diff --git a/mods/upload.js b/mods/upload.js index f2bbe888..321f6f49 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -122,11 +122,14 @@ exports.getModule = class UploadModule extends MenuModule { } getSaveState() { - return { - uploadType : this.uploadType, - tempRecvDirectory : this.tempRecvDirectory, - areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], - }; + // 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) { @@ -143,6 +146,11 @@ exports.getModule = class UploadModule extends MenuModule { initSequence() { const self = this; + if(0 === this.availAreas.length) { + // + return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail'); + } + async.series( [ function before(callback) { @@ -641,6 +649,8 @@ exports.getModule = class UploadModule extends MenuModule { 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 || ''); diff --git a/oputil.js b/oputil.js index 20ae59da..27e7f759 100755 --- a/oputil.js +++ b/oputil.js @@ -38,7 +38,7 @@ global args: commands: user : user utilities config : config file management - file-area : file area management + file-base : file base management `, User : @@ -58,8 +58,8 @@ valid args: valid args: --new : generate a new/initial configuration `, - FileArea : -`usage: oputil.js file-area + FileBase : +`usage: oputil.js file-base valid args: --scan AREA_TAG : (re)scan area specified by AREA_TAG for new files @@ -436,6 +436,14 @@ function handleConfigCommand() { } function fileAreaScan() { + function scanFileIterator(stepInfo, nextScanStep) { + if('start' === stepInfo.step) { + console.info(`Scanning ${stepInfo.filePath}...`); + } + return nextScanStep(null); + // :TODO: add 'finished' step when avail + } + async.waterfall( [ function init(callback) { @@ -452,7 +460,7 @@ function fileAreaScan() { return callback(null, fileAreaMod, areaInfo); }, function performScan(fileAreaMod, areaInfo, callback) { - fileAreaMod.scanFileAreaForChanges(areaInfo, err => { + fileAreaMod.scanFileAreaForChanges(areaInfo, scanFileIterator, err => { return callback(err); }); } @@ -466,9 +474,9 @@ function fileAreaScan() { ); } -function handleFileAreaCommand() { +function handleFileBaseCommand() { if(true === argv.help) { - return printUsageAndSetExitCode('FileArea', ExitCodes.ERROR); + return printUsageAndSetExitCode('FileBase', ExitCodes.ERROR); } if(argv.scan) { @@ -499,8 +507,8 @@ function main() { handleConfigCommand(); break; - case 'file-area' : - handleFileAreaCommand(); + case 'file-base' : + handleFileBaseCommand(); break; default: From 4ab08287a256c14f0200552bd2c56d5a9a1c85c7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 Feb 2017 20:33:49 -0700 Subject: [PATCH 62/86] Additional doc/updates --- docs/file_base.md | 22 ++++++++++++++++++---- docs/web_server.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 docs/web_server.md diff --git a/docs/file_base.md b/docs/file_base.md index 3a10ed97..c35c0280 100644 --- a/docs/file_base.md +++ b/docs/file_base.md @@ -4,10 +4,11 @@ Starting with version 0.0.4-alpha, ENiGMA½ has support for File Bases! Document ## A Different Appoach ENiGMA½ has strayed away from the old familure setup here and instead takes a more modern approach: * [Gazelle](https://whatcd.github.io/Gazelle/) inspired system for searching & browsing files -* No File Conferences -* File Areas are still around but should generally be used less. Instead, files can have one or more tags. Think things like `dos.retro`, `pc.warez`, `game`, etc. +* No File Conferences (just areas!) +* File Areas are still around but should generally be used less. Instead, files can have one or more tags. Think things like `dos.retro`, `pc.warez`, `games`, etc. * Temporary web (http:// or https://) download links in additional to standard X/Y/Z protocol support * Users can star rate files & search/filter by ratings +* Concept of user defined filters ## Other bells and whistles * A given area can span one to many physical storage locations @@ -17,7 +18,7 @@ ENiGMA½ has strayed away from the old familure setup here and instead takes a m * Duplicates validated by SHA-256 ## Configuration -Like many things in ENiGMA½, configuration of file base(s) is handled via `config.hjson` entries in the `fileBase` section. +Like many things in ENiGMA½, configuration of file base(s) is handled via `config.hjson` -- specifically in the `fileBase` section. ```hjson fileBase: { @@ -33,6 +34,8 @@ fileBase: { } ``` +(Take a look at `core/config.js` for additional keys that may be overridden) + ### Storage tags **Storage Tags** define paths to a physical (file) storage location that can later be referenced in a file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths are relative to the value set by the `areaStoragePrefix` key. Below is an example defining a both a relative and fully qualified path each attached to a storage tag: @@ -71,5 +74,16 @@ To override read and/or write ACS, supply a valid `acs` member. #### Uploads Note that `storageTags` may contain *1:n* storage tag references. **Uploads in a particular area are stored in the first storage tag path**. +## Web Access +Temporary web HTTP(S) URLs can be used to download files using the built in web server. Temporary links expire after `fileBase::web::expireMinutes`. The full URL given to users is built using `contentServers::web::domain` and will default to HTTPS (http://) if enabled with a fallback to HTTP. The end result is users are given a temporary web link that may look something like this: `https://xibalba.l33t.codes:44512/f/h7JK` + +See [Web Server](web_server.md) for more information. + ## oputil -The `oputil.js` +op utilty `file-base` command has tools for managing file bases. See `oputil.js file-base --help`. \ No newline at end of file +The `oputil.js` +op utilty `file-base` command has tools for managing file bases. For example, to import existing files found within **all** storage locations tied to an area: + +```bash +oputil.js file-base --scan some_area +``` + +See `oputil.js file-base --help` for additional information. \ No newline at end of file diff --git a/docs/web_server.md b/docs/web_server.md new file mode 100644 index 00000000..5e28ea95 --- /dev/null +++ b/docs/web_server.md @@ -0,0 +1,39 @@ +# Web Server +ENiGMA½ comes with a built in *content server* for supporting both HTTP and HTTPS. Currently the [File Bases](file_base.md) registeres routes for file downloads, and static files can also be served for your BBS. Other features will likely come in the future or you can easily write your own! + +## Configuration +By default the web server is not enabled. To enable it, you will need to at a minimum configure two keys in the `contentServers::web` section of `config.hjson`: + +```hjson +contentServers: { + web: { + domain: bbs.yourdomain.com + + http: { + enabled: true + } + } +} +``` + +This will configure HTTP for port 8080 (override with `port`). To additionally enable HTTPS, you will need a PEM encoded SSL certificate and private key. Once obtained, simply enable the HTTPS server: +```hjson +contentServers: { + web: { + domain: bbs.yourdomain.com + + https: { + enabled: true + port: 8443 + certPem: /path/to/your/cert.pem + keyPem: /path/to/your/cert_private_key.pem + } + } +} +``` + +### Static Routes +Static files live relative to the `contentServers::web::staticRoot` path which defaults to `enigma-bbs/www`. + +### Custom Error Pages +Customized error pages can be created for [HTTP error codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error) by providing a `.html` file in the *static routes* area. For example: `404.html`. From 2d9cd39ae87c977353edb6c70b1612b8a769adfe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 Feb 2017 20:34:29 -0700 Subject: [PATCH 63/86] Use config route path for temp files --- core/file_area_web.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_area_web.js b/core/file_area_web.js index 3f1b60ac..fc2ae61b 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -48,7 +48,7 @@ class FileAreaWebAccess { const routeAdded = self.webServer.instance.addRoute({ method : 'GET', - path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable + path : Config.fileBase.web.routePath, handler : self.routeWebRequestForFile.bind(self), }); From 2e7862043d74f53d908c019e639311e71b8b56b2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 Feb 2017 20:48:44 -0700 Subject: [PATCH 64/86] More document updates --- README.md | 4 ++-- docs/about.md | 29 ++++++++++++++++------------- docs/config.md | 15 ++++++++++----- docs/menu_system.md | 4 +++- docs/web_server.md | 2 +- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 45b07420..2dd16223 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/) support! * [Bunyan](https://github.com/trentm/node-bunyan) logging * [Message networks](docs/msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export - * [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs. Legacy X/Y/Z modem also supported! + * [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/web_server.md). Legacy X/Y/Z modem also supported! * Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more! - + ## In the Works * More ES6+ usage, and **documentation**! * More ACS support coverage diff --git a/docs/about.md b/docs/about.md index 55d0a45f..50656011 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,16 +1,19 @@ # About ENiGMA½ ## High Level Feature Overview -* Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X) -* Multi node support -* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JS based mods -* MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles -* Telnet & SSH access built in. Additional servers are easy to implement & plug in -* [CP437](http://www.ascii-codes.com/) and UTF-8 output -* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior. -* [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support -* Renegade style pipe codes -* [SQLite](http://sqlite.org/) storage of users and message areas -* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password storage -* Door support including common dropfile formats and [DOSEMU](http://www.dosemu.org/) -* [Bunyan](https://github.com/trentm/node-bunyan) logging \ No newline at end of file + * Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows) + * Unlimited multi node support (for all those BBS "callers"!) + * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods + * MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles + * Telnet & **SSH** access built in. Additional servers are easy to implement + * [CP437](http://www.ascii-codes.com/) and UTF-8 output + * [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior + * [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support + * Renegade style pipe color codes + * [SQLite](http://sqlite.org/) storage of users, message areas, and so on + * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption + * [Door support](doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/) support! + * [Bunyan](https://github.com/trentm/node-bunyan) logging + * [Message networks](msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export + * [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](web_server.md). Legacy X/Y/Z modem also supported! + * Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more! \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index 15496834..ecd2ed89 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,10 +1,10 @@ # Configuration Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.org/) files. HJSON is just like JSON but simplified and much more resilient to human error. -## System Configuraiton +## System Configuration The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `~/.config/enigma-bbs/config.hjson` though you can override this with the `--config` parameter when invoking `main.js`. Values found in core/config.js may be overridden by simply providing the object members you wish replace. -**Windows note**: **~** resolves to *C:\Users\YOURLOGINNAME\* on modern installations, e.g. *C:\Users\NuSkooler\\.config\enigma-bbs\config.hjson* +**Windows note**: **~** resolves to *C:\Users\YOURLOGINNAME\* on modern Windows installations, e.g. `C:\Users\NuSkooler\\.config\enigma-bbs\config.hjson` ### oputil.js Please see `oputil.js config` for configuration generation options. @@ -24,13 +24,18 @@ general: { } ``` +(Note the very slightly different syntax. **You can use standard JSON if you wish**) + ### Specific Areas of Interest -* [Doors](doors.md) -* [MCI Codes](mci.md) * [Menu System](menu_system.md) * [Message Conferences](msg_conf_area.md) * [Message Networks](msg_networks.md) +* [File Base](file_base.md) * [File Archives & Archivers](archives.md) +* [Doors](doors.md) +* [MCI Codes](mci.md) +* [Web Server](web_server.md) +...and other stuff [in the /docs directory](./) ### A Sample Configuration @@ -121,4 +126,4 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all ``` ## Menus -TODO: Documentation on menu.hjson, etc. \ No newline at end of file +See [the menu system docs](menu_system.md) \ No newline at end of file diff --git a/docs/menu_system.md b/docs/menu_system.md index b8b02fc8..1dea637c 100644 --- a/docs/menu_system.md +++ b/docs/menu_system.md @@ -1,13 +1,14 @@ # Menu System ENiGMA½'s menu system is highly flexible and moddable. The possibilities are almost endless! By modifying your `menu.hjson` you will be able to create a custom look and feel unique to your board. -The default `menu.hjson` file lives within the `mods` directory. To specify another file, set the `menuFile` property in your `config.hjson` file: +The default `menu.hjson` file lives within the `mods` directory. It is **highly recommended** to specify another file by setting the `menuFile` property in your `config.hjson` file: ```hjson general: { /* Can also specify a full path */ menuFile: mybbs.hjson } ``` +(You can start by copying the default `menu.hjson` to `mybbs.hjson`) ## The Basics Like all configuration within ENiGMA½, menu configuration is done via a HJSON file. This file is located in the `mods` directory: `mods/menu.hjson`. @@ -37,6 +38,7 @@ Now let's look at `matrix`, the `next` entry from `telnetConnected`: ```hjson matrix: { art: matrix + desc: Login Matrix form: { 0: { VM: { diff --git a/docs/web_server.md b/docs/web_server.md index 5e28ea95..740515e9 100644 --- a/docs/web_server.md +++ b/docs/web_server.md @@ -1,5 +1,5 @@ # Web Server -ENiGMA½ comes with a built in *content server* for supporting both HTTP and HTTPS. Currently the [File Bases](file_base.md) registeres routes for file downloads, and static files can also be served for your BBS. Other features will likely come in the future or you can easily write your own! +ENiGMA½ comes with a built in *content server* for supporting both HTTP and HTTPS. Currently the [File Bases](file_base.md) registers routes for file downloads, and static files can also be served for your BBS. Other features will likely come in the future or you can easily write your own! ## Configuration By default the web server is not enabled. To enable it, you will need to at a minimum configure two keys in the `contentServers::web` section of `config.hjson`: From f9e91987ac56bf6c987e59650962abd46e9ce6a2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 Feb 2017 21:08:23 -0700 Subject: [PATCH 65/86] Better arg parsing for main --- core/bbs.js | 41 ++++++++++++++++++++++--------------- core/servers/content/web.js | 4 ++-- docs/config.md | 2 +- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 3ae00e37..b877f918 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -10,6 +10,7 @@ const conf = require('./config.js'); const logger = require('./logger.js'); const database = require('./database.js'); const clientConns = require('./client_connections.js'); +const resolvePath = require('./misc_util.js').resolvePath; // deps const async = require('async'); @@ -25,30 +26,38 @@ exports.bbsMain = bbsMain; // object with various services we want to de-init/shutdown cleanly if possible const initServices = {}; +const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby'; +const HELP = +`${ENIGMA_COPYRIGHT} +usage: main.js + +valid args: + --version : display version + --help : displays this help + --config PATH : override default config.hjson path +`; + +function printHelpAndExit() { + console.info(HELP); + process.exit(); +} + function bbsMain() { async.waterfall( [ function processArgs(callback) { - const args = process.argv.slice(2); + const argv = require('minimist')(process.argv.slice(2)); - var configPath; - - if(args.indexOf('--help') > 0) { - // :TODO: display help - } else { - let argCount = args.length; - for(let i = 0; i < argCount; ++i) { - const arg = args[i]; - if('--config' === arg) { - configPath = args[i + 1]; - } - } + if(argv.help) { + printHelpAndExit(); } - callback(null, configPath || conf.getDefaultPath(), _.isString(configPath)); + const configOverridePath = argv.config; + + return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); }, function initConfig(configPath, configPathSupplied, callback) { - conf.init(configPath, function configInit(err) { + conf.init(resolvePath(configPath), function configInit(err) { // // If the user supplied a path and we can't read/parse it @@ -80,7 +89,7 @@ function bbsMain() { function complete(err) { // note this is escaped: fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { - console.info('ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby'); + console.info(ENIGMA_COPYRIGHT); if(!err) { console.info(banner); } diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 5a6f36d1..70f2eb12 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -107,13 +107,13 @@ exports.getModule = class WebServerModule extends ServerModule { route = new Route(route); if(!route.isValid()) { - Log( { route : route }, 'Cannot add route: missing or invalid required members' ); + Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' ); return false; } const routeKey = route.getRouteKey(); if(routeKey in this.routes) { - Log( { route : route }, 'Cannot add route: duplicate method/path combination exists' ); + Log.warn( { route : route }, 'Cannot add route: duplicate method/path combination exists' ); return false; } diff --git a/docs/config.md b/docs/config.md index ecd2ed89..ca760676 100644 --- a/docs/config.md +++ b/docs/config.md @@ -4,7 +4,7 @@ Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.or ## System Configuration The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `~/.config/enigma-bbs/config.hjson` though you can override this with the `--config` parameter when invoking `main.js`. Values found in core/config.js may be overridden by simply providing the object members you wish replace. -**Windows note**: **~** resolves to *C:\Users\YOURLOGINNAME\* on modern Windows installations, e.g. `C:\Users\NuSkooler\\.config\enigma-bbs\config.hjson` +**Windows note**: **~** resolves to *C:\Users\YOURLOGINNAME\* on modern Windows installations, e.g. `C:\Users\NuSkooler\.config\enigma-bbs\config.hjson` ### oputil.js Please see `oputil.js config` for configuration generation options. From 2e10fdfdf546376ccd418fd3afa8e0b7915d1083 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 Feb 2017 22:03:21 -0700 Subject: [PATCH 66/86] Updates to upload check, docs --- UPGRADE.md | 3 +++ core/bbs.js | 8 +++----- core/file_base_area.js | 6 ++++++ core/menu_module.js | 3 ++- mods/upload.js | 39 +++++++++++++++++++++++++-------------- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 76f9e532..3726fb78 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -45,6 +45,9 @@ nvm install 6 nvm alias default 6 ``` +### ES6 +Newly written code will use ES6 and a lot of code has started the migration process. Of note is the `MenuModule` class. If you have created a mod that inherits from `MenuModule`, you will need to upgrade your class to ES6. + ## Manual Database Upgrade A few upgrades need to be made to your SQLite databases: diff --git a/core/bbs.js b/core/bbs.js index b877f918..2995dabc 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -121,23 +121,21 @@ function shutdownSystem() { }, function stopListeningServers(callback) { return require('./listening_server.js').shutdown( () => { - // :TODO: log err return callback(null); // ignore err }); }, function stopEventScheduler(callback) { if(initServices.eventScheduler) { return initServices.eventScheduler.shutdown( () => { - callback(null); // ignore err + return callback(null); // ignore err }); } else { return callback(null); } }, function stopFileAreaWeb(callback) { - require('./file_area_web.js').startup(err => { - // :TODO: Log me if err - return callback(null); + require('./file_area_web.js').startup( () => { + return callback(null); // ignore err }); }, function stopMsgNetwork(callback) { diff --git a/core/file_base_area.js b/core/file_base_area.js index cff77071..65365293 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -566,6 +566,12 @@ function scanFile(filePath, options, iterator, cb) { getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { return callback(err, dupeEntries); }); + }, + function finished(dupeEntries, callback) { + stepInfo.step = 'finished'; + callIter( () => { + return callback(null, dupeEntries); + }); } ], (err, dupeEntries) => { diff --git a/core/menu_module.js b/core/menu_module.js index dfcf0589..12fd0cd0 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -401,8 +401,9 @@ exports.MenuModule = class MenuModule extends PluginModule { let textView; let customMciId = startId; const config = this.menuConfig.config; + const endId = options.endId || 99; // we'll fail to get a view before 99 - while( (textView = this.viewControllers[formName].getView(customMciId)) ) { + while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" const format = config[key]; diff --git a/mods/upload.js b/mods/upload.js index 321f6f49..7c9e0bbc 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -49,7 +49,7 @@ const MciViewIds = { calcHashIndicator : 1, archiveListIndicator : 2, descFileIndicator : 3, - + logStep : 4, customRangeStart : 10, // 10+ = customs }, @@ -218,13 +218,16 @@ exports.getModule = class UploadModule extends MenuModule { const fmtObj = Object.assign( {}, stepInfo); let stepIndicatorFmt = ''; + let logStepFmt; - const indicatorStates = this.menuConfig.config.indicatorStates || [ '|', '/', '-', '\\' ]; - const indicatorFinished = this.menuConfig.config.indicatorFinished || '√'; + 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; @@ -237,47 +240,51 @@ exports.getModule = class UploadModule extends MenuModule { } indicator.text = indicatorStates[self.scanStatus.indicatorPos]; } - } + } switch(stepInfo.step) { case 'start' : - stepIndicatorFmt = this.menuConfig.config.scanningStartFormat || 'Scanning {fileName}'; + logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Scanning {fileName}'; break; case 'hash_update' : - stepIndicatorFmt = this.menuConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; + stepIndicatorFmt = fmtConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; updateIndicator(MciViewIds.processing.calcHashIndicator); break; case 'hash_finish' : - stepIndicatorFmt = this.menuConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; + stepIndicatorFmt = fmtConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; updateIndicator(MciViewIds.processing.calcHashIndicator, true); break; case 'archive_list_start' : - stepIndicatorFmt = this.menuConfig.extractArchiveListFormat || 'Extracting archive list'; + stepIndicatorFmt = fmtConfig.extractArchiveListFormat || 'Extracting archive list'; updateIndicator(MciViewIds.processing.archiveListIndicator); break; case 'archive_list_finish' : fmtObj.archivedFileCount = stepInfo.archiveEntries.length; - stepIndicatorFmt = this.menuConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; + stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; updateIndicator(MciViewIds.processing.archiveListIndicator, true); break; case 'archive_list_failed' : - stepIndicatorFmt = this.menuConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; + stepIndicatorFmt = fmtConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; break; case 'desc_files_start' : - stepIndicatorFmt = this.menuConfig.processingDescFilesFormat || 'Processing description files'; + stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files'; updateIndicator(MciViewIds.processing.descFileIndicator); break; case 'desc_files_finish' : - stepIndicatorFmt = this.menuConfig.processingDescFilesFinishFormat || 'Finished processing description files'; + 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); @@ -288,6 +295,10 @@ exports.getModule = class UploadModule extends MenuModule { 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); } @@ -649,7 +660,7 @@ exports.getModule = class UploadModule extends MenuModule { 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); + 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 || ''); From 849ab68de2a5e31908b8ac538ee729ccd5e43fe7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 00:22:36 -0700 Subject: [PATCH 67/86] Add README.NOW support for long desc scan --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 373f6c49..5008e732 100644 --- a/core/config.js +++ b/core/config.js @@ -488,7 +488,7 @@ function getDefaultConfig() { // common README filename - https://en.wikipedia.org/wiki/README descLong : [ - '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$', '^READ\.ME$', '^README$', '^README\.md$' + '^.*\.NFO$', '^README\.1ST$', '^README\.NOW$', '^README\.TXT$', '^READ\.ME$', '^README$', '^README\.md$' ], }, From 6dccbd124fde7f41b73533bc684e3043eebf04b5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 00:22:53 -0700 Subject: [PATCH 68/86] Fix upload_by_user_id parse --- core/file_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_entry.js b/core/file_entry.js index 57276807..f7836555 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -19,7 +19,7 @@ const FILE_TABLE_MEMBERS = [ const FILE_WELL_KNOWN_META = { // name -> *read* converter, if any upload_by_username : null, - upload_by_user_id : null, + upload_by_user_id : (u) => parseInt(u) || 0, file_md5 : null, file_sha1 : null, file_crc32 : null, From 50a5b8d78e13c4736ccdce3f60a64d4ad513669a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 00:23:39 -0700 Subject: [PATCH 69/86] Use proper resetScreen() vs clearScreen() --- core/menu_module.js | 2 +- core/theme.js | 2 +- mods/file_base_download_manager.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 12fd0cd0..1f350564 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -295,7 +295,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } if(options.clearScreen) { - this.client.term.rawWrite(ansi.clearScreen()); + this.client.term.rawWrite(ansi.resetScreen()); } return theme.displayThemedAsset( diff --git a/core/theme.js b/core/theme.js index 7dd9aa0a..d9a51ca0 100644 --- a/core/theme.js +++ b/core/theme.js @@ -566,7 +566,7 @@ function displayThemedPrompt(name, client, options, cb) { } if(options.clearScreen) { - client.term.rawWrite(ansi.clearScreen()); + client.term.rawWrite(ansi.resetScreen()); } // diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index 8ddcb735..e7705c41 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -170,7 +170,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { [ function readyAndDisplayArt(callback) { if(options.clearScreen) { - self.client.term.rawWrite(ansi.clearScreen()); + self.client.term.rawWrite(ansi.resetScreen()); } theme.displayThemedAsset( From edb9d32acc2db6cbb4fa72d13f2df50225cf27a7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 00:24:24 -0700 Subject: [PATCH 70/86] * Fix CRC32 meta * Properly store upload user info in meta @ upload --- core/file_base_area.js | 2 +- mods/upload.js | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 65365293..69e4d4cf 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -522,7 +522,7 @@ function scanFile(filePath, options, iterator, cb) { } else if('sha1' === hashName || 'md5' === hashName) { stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex'); } else if('crc32' === hashName) { - stepInfo.crc32 = fileEntry.meta.crc32 = hashes.crc32.finalize().toString(16); + stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16); } return nextHash(null); diff --git a/mods/upload.js b/mods/upload.js index 7c9e0bbc..5faf0d76 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -314,9 +314,13 @@ exports.getModule = class UploadModule extends MenuModule { 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, }; @@ -327,11 +331,14 @@ exports.getModule = class UploadModule extends MenuModule { }; 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 } ); + self.client.log.debug('Scanning file', { filePath : filePath } ); scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { if(err) { @@ -401,6 +408,9 @@ exports.getModule = class UploadModule extends MenuModule { 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); @@ -474,15 +484,14 @@ exports.getModule = class UploadModule extends MenuModule { function populateDupeInfo(dupeListView, callback) { const dupeInfoFormat = self.menuConfig.config.dupeInfoFormat || '{fileName} @ {areaName}'; - dupes.forEach(dupe => { - const formatted = stringFormat(dupeInfoFormat, dupe); - if(dupeListView) { - // dupesInfoFormatX - dupeListView.addText(enigmaToAnsi(formatted)); - } else { - self.client.term.pipeWrite(`${formatted}\n`); - } - }); + 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); }, From 4324c410c07ee2be3107fbe3ee804d3f7138f040 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 00:25:25 -0700 Subject: [PATCH 71/86] Minor update; use resetScreen() vs clearScreen() --- mods/file_area_list.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 7a36f21b..aa661a5e 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -207,7 +207,7 @@ exports.getModule = class FileAreaList extends MenuModule { const isQueuedIndicator = config.isQueuedIndicator || 'Y'; const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; - const entryInfo = this.currentFileEntry.entryInfo = { + const entryInfo = currEntry.entryInfo = { fileId : currEntry.fileId, areaTag : currEntry.areaTag, areaName : area.name || 'N/A', @@ -219,7 +219,7 @@ exports.getModule = class FileAreaList extends MenuModule { userRating : currEntry.userRating, uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), - isQueued : this.dlQueue.isQueued(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator, + 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 }; @@ -281,7 +281,7 @@ exports.getModule = class FileAreaList extends MenuModule { [ function readyAndDisplayArt(callback) { if(options.clearScreen) { - self.client.term.rawWrite(ansi.clearScreen()); + self.client.term.rawWrite(ansi.resetScreen()); } theme.displayThemedAsset( From 60ae03ab3f115b374a6bde391ff7c39a16e91e97 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 00:26:08 -0700 Subject: [PATCH 72/86] WIP on luciano_blocktronics for new file base --- mods/prompt.hjson | 20 +++ mods/themes/luciano_blocktronics/FBHELP.ANS | Bin 0 -> 664 bytes mods/themes/luciano_blocktronics/FBNORES.ANS | Bin 0 -> 226 bytes mods/themes/luciano_blocktronics/FBRWSE.ANS | Bin 0 -> 923 bytes mods/themes/luciano_blocktronics/FDETAIL.ANS | Bin 0 -> 291 bytes mods/themes/luciano_blocktronics/FDETGEN.ANS | Bin 0 -> 865 bytes mods/themes/luciano_blocktronics/FDETLST.ANS | Bin 0 -> 183 bytes mods/themes/luciano_blocktronics/FDETNFO.ANS | Bin 0 -> 144 bytes mods/themes/luciano_blocktronics/FEMPTYQ.ANS | Bin 0 -> 226 bytes mods/themes/luciano_blocktronics/FMENU.ANS | Bin 0 -> 3472 bytes mods/themes/luciano_blocktronics/FSEARCH.ANS | Bin 0 -> 1033 bytes mods/themes/luciano_blocktronics/RATEFILE.ANS | Bin 0 -> 331 bytes mods/themes/luciano_blocktronics/ULCHECK.ANS | Bin 0 -> 2047 bytes mods/themes/luciano_blocktronics/ULNOAREA.ANS | Bin 0 -> 239 bytes mods/themes/luciano_blocktronics/theme.hjson | 170 ++++++++++++++++++ 15 files changed, 190 insertions(+) create mode 100644 mods/themes/luciano_blocktronics/FBHELP.ANS create mode 100644 mods/themes/luciano_blocktronics/FBNORES.ANS create mode 100644 mods/themes/luciano_blocktronics/FBRWSE.ANS create mode 100644 mods/themes/luciano_blocktronics/FDETAIL.ANS create mode 100644 mods/themes/luciano_blocktronics/FDETGEN.ANS create mode 100644 mods/themes/luciano_blocktronics/FDETLST.ANS create mode 100644 mods/themes/luciano_blocktronics/FDETNFO.ANS create mode 100644 mods/themes/luciano_blocktronics/FEMPTYQ.ANS create mode 100644 mods/themes/luciano_blocktronics/FMENU.ANS create mode 100644 mods/themes/luciano_blocktronics/FSEARCH.ANS create mode 100644 mods/themes/luciano_blocktronics/RATEFILE.ANS create mode 100644 mods/themes/luciano_blocktronics/ULCHECK.ANS create mode 100644 mods/themes/luciano_blocktronics/ULNOAREA.ANS diff --git a/mods/prompt.hjson b/mods/prompt.hjson index 9083f5d4..cfeb8fbc 100644 --- a/mods/prompt.hjson +++ b/mods/prompt.hjson @@ -133,7 +133,23 @@ } }, + /////////////////////////////////////////////////////////////////////// // File Base Related + /////////////////////////////////////////////////////////////////////// + fileMenuCommand: { + art: FILPMPT + mci: { + TL1: {} + ET2: { + argName: menuOption + width: 20 + maxLength: 20 + textStyle: upper + focus: true + } + } + } + fileBaseRateEntryPrompt: { art: RATEFILE mci: { @@ -153,6 +169,10 @@ /////////////////////////////////////////////////////////////////////// // Standard / Required + // + // Prompts in this section are considered "standard" and are required + // to be present + // /////////////////////////////////////////////////////////////////////// pause: { // diff --git a/mods/themes/luciano_blocktronics/FBHELP.ANS b/mods/themes/luciano_blocktronics/FBHELP.ANS new file mode 100644 index 0000000000000000000000000000000000000000..5dc95322fdf79ea5b521cea023e5e6c61c138728 GIT binary patch literal 664 zcmb`EF;BxV5QS65NS)a5;(<9_T8NQ4R3$=)0SO7g;>m4IYSnH5JM`~!xi}HD%Ek?j z?#}Pi^9z$OyN20RxU?%Ki;~IQaUPhQ;ulYFS(f!(;c$bZ!tJZdYP^uU&p4@d-41*l z1Qlw}8;EdB-bOUS)v^a3<5y9;QM;<8y~l~scJ0#$yXjzGgCyLLQ5B9jR-@}20l#c9 zDa!dEY752|oOW{f_Nn{{U{C}Vwk@v?X=?*aJ#)dX?@6A+hYNirHeC*y?b{PFII LzdbD4u0MYO@~)Yn literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FBNORES.ANS b/mods/themes/luciano_blocktronics/FBNORES.ANS new file mode 100644 index 0000000000000000000000000000000000000000..81464593221b56740a0e43328c712d3df40868ab GIT binary patch literal 226 zcmb1+Hn27^ur@Z&xk$%KvoPnHjb@L6hq>ZY6Xv?9o64RVnOui?qWoJ`nGj>6 zY$n1yy0i~U7s#ZZ>#{&wfS_68s~X=ZjwqxF64iokV8BoTLWKMWkok};C5WwqD&`xc z8z?+dPdP;3kofHq%VK?$IR2gZjy7Ch;H&h)6>YoJnOe z=G%|u-1k5B))bJMxC`Kn@0mpx;L8E*%3TRj##GpA292xq1F1Y+r$zNZxv3odF7+o3 zO7Fd`Z{`XdGx^nixmXn~>NUbxf;je=O}jeKmSEvpV_DDKp_j3$ZiqS!eRQ8m5)>)g zV0)wfW`~SUV3~EYe<$6NkXN3BwSsuteZ!_V!}I6K%k-9Wa_XOs4fa1nrm1Z!N($S`t&LHEAjdERsfh>?AK++&rA4D1D1|kVr0fm6>G0IKPS4c`s x&gPN|b_{iPH84=19xyU6G&3|Xu-L`Gz`)2Dz#svnfk4>P$yXuF-4VirlK{50GPM8z literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FDETGEN.ANS b/mods/themes/luciano_blocktronics/FDETGEN.ANS new file mode 100644 index 0000000000000000000000000000000000000000..c9162e7a01646af6f92069d9c7638beb7c8a5426 GIT binary patch literal 865 zcmb`FJx{|h5QbC6NK9;G@xq8W+l^b4sUU=qK&r&YlF788t<V$`UzR$hqd!3a~455%CWGpLrAr-!5^o{PTb+yr0Wu+9@{sT0Io=AN5bk8AmLQCR% zhkeF~p+y*2iW5b}TW0SVy&$@0(_=)))Q+k2?hqqGW==}L3+udg{6WWINRy8ph~BR@ zi(j68XL;&Js2#DJEd#y6Zol<5no(Co^tL+lWuyq|G%HBWO5a^=kd!B{~%eM1ADE>xgUN&FDE}SMWL`XwKP>BvsfWDx1gjFr~;;eODfng)Y;X*0AwY!A5DnKzoL1t%T)c#;xo4|tu~#kH%DdUBUbKx5v(;YXBY!I2;eDK@7_8e=+vNMUV)-v$(r zcIgyIWSRrxsn%5X7yB6T4>yLJ>wRw#;m#%_w>8Uuxd=~f=U=~lcj#Und^jWv&dD$v z=)pM`W5BlqSvn9g42D1vk$Qt^1b~I6g+#yWjG!$$vo$3gcfwcgZ!w^E3ZK@d)Ttib zDw%koo665tFPHFu-+B)NB^f_0CoP8|0=-<>4uuqtqE`w%$jn$4YO>SymIh?Tm47L2t&N>FIq`J>D2x2p|?4Y2>2*=RsE%oAN98~CSx!M~S}$C2G9 zx%+9E$jbztGHg1I;9X7NU2WEv#~%~Cx+9t&V2zzZR{?~gqXIPsluSbBYy#kVl5u9 zrAVg*fe7(V|Ji54gC8^$B+c?6AR|?BiPm`$zdtVYe^AM2a~HlCpAfV69!BdNQO&Wr^%>__#SdZQIx6=RdW zI5tkF6Hc3x^K19Q9e-S3oNe6c`RD7+C8`cP0i;`6=P9x6D5mp@IOsCGEGoa7D66-O z85*VsXK_TfWuLQXt$Ib$@YYl;XTLPf2k{C9H5RAhDaD@nkHs|&z7jK`b^hxO1UOnB ze{$DbB=-CQv&@1jK`H>3o&9`ekvOi&e6vA-p{ubCI4cGngVIGT@YxXf?D};3dkR+1 zvkHyf0Y3^Wq7-EktKhtwt-Ads8=~EV%n0c7-ck>+RykYE=|6Yhhqg3sh@|7IgeubB zIfBz8qk|QF!D0+Wj|kK)5EQmrV`VJhi#5z%V!q^7^MRcdp7TmMew-l=!b=krVUsZ^4R7Suj1=k8yH~!)$pCG`KY6QCcIxsO4 zOE}2F$tw#iP2tDxYx8ADrfkjRS5lVFm$;{%q8CjY!>_StrQ|f@Rw*gs+aW);n#3n= zqT?j+-Qym947z1#sFS2gt_y4`MUrUC-O%LBfx-+)9*dvxC@VQFfS;1IXi<+36AhmhrtS(I*$q7+C8pJ=AthNPN|`b3DwLV0>Pb~K%>UfmQ$bEVC8{l z$)rL`GE6KiYLW(Rlc$^=5A!LZ&;)73v|d->h#BY0HLf-cHNee_NgGGXCIehPJUh?^ zwT+_?`ndc2!@=Ln!>XeB|MY39dR{ek{m0XtohMJ;?>yVl=eM`7{&N4kIk2bl{T=v( BYbO8z literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FSEARCH.ANS b/mods/themes/luciano_blocktronics/FSEARCH.ANS new file mode 100644 index 0000000000000000000000000000000000000000..857592cdb7f120274fac47d8c5b2548f08a7d91b GIT binary patch literal 1033 zcmb`FzfQw25XM`vWo1D`f-IeJ{?q|7wNZddR03L9GF7Ew;iwdNAl@q$d6SCoE_Oh) zh=o+D?7<1{z5Uxrzdk}-anC^85u!jaC~gDh6mu#`+=8mJ+v zttdJb5fp}>AP8^_ArL@jafCQR;7dhdY*cu>RU}D{;JC_haVp@ZM(r9E*c~<>4ZntL z&eU~dYV0B0mCu<-YzbKL3%V3e-AY1`aEz*edlnF$Ifb}{y$Ase zVI%Ffel=Z8u8Uw+jPCL)*71}!9y2gvzlT2=Ha7}xN*;0MJ#L6Ilz5F zscA5tM>2NJ7v1g@New&MFZ|-^7kX^zv28EzQWw@)wZa?h|1dV2CXy;Ee@jE23#*_# zI(Fmc4-8)A{zFzUa#N3=^gas P*`JK3!NcXK-F5$;;*=(n literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/RATEFILE.ANS b/mods/themes/luciano_blocktronics/RATEFILE.ANS new file mode 100644 index 0000000000000000000000000000000000000000..394e6fe5f42ad6544ee6542766c739313514b120 GIT binary patch literal 331 zcmb`Au?~Vj5JZp8(As&;HP-GeL=vw;B*p?IA!tEG2qFnfLHts`$UQ*5z%6z%b35;3 z0mcX81?2L`0x^<>Hr}3|lAl=Hu2vk;F#Gb|_Ki)92XvT2_KetOiMFR!Eewc({sctI zQ+oxpq@Pz!(>Uj_AN4bAoVM5xvZ}7} z2M@fHiaviYz!^eXgkp$~?#KXI1!-YvbYR<_1prZw-lQl=7ubhaPXmGTKg)guyK;wMVYqEjIWp7EB3b)--?xq9g+-|NlEsz^A55m=hX%m{d08}Bt7P}#((Pu zdP?X4VOVmhMWe>s+EXw&%w9!!m(jOd^Z*EBOe{On_O&j~ZY*|1{l6{sSH=}cyR(zFY*=haL!^6F-_nlCr> zs=kDytLu4;`pM^oB9AXl)0B;k;PhGfdx3SxwwZ2juofSueD>O9O&*Xn!k%8wgn1FF zOxKG!s$ib>ptrC`2qg1>g40l7HCw3Q_qhKyo3Gbb%datSOQ$TJ9jJxzQ=>6gQK8v% zsnnlC>iOEZwffN#aI0HCQ~?=$6~bVL8P_GkJ;DXxCwiEbSmDslefR0dlgo0PBpkZ` i^khj|Bw3pNc+}|}9i4Zcbo4xa`|`|vdVONM?fW-npskJo literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/ULNOAREA.ANS b/mods/themes/luciano_blocktronics/ULNOAREA.ANS new file mode 100644 index 0000000000000000000000000000000000000000..2e47643a87788251a81770344d6d3e9203b1fe37 GIT binary patch literal 239 zcmb1+Hn27^ur@Z&baq@xXTAqw*H6-of5;JoWlX6lOO7ayd^Gk~q5|fkjOY=&A%3->>q=Fqoom~wKK&Dd( k7#SFv8yFcHmM}0dFfs-(umNcx5cYKPRS0u;gz(@b09%ADcK`qY literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index 1397060c..da031d6e 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -478,6 +478,176 @@ } } + //////////////////// 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: 42 + } + ET2: { + width: 42 + } + SM3: { + width: 14 + justify: right + } + SM4: { + width: 14 + justify: right + } + SM5: { + width: 14 + justify: right + } + ET6: { + width: 42 + } + HM7: { + focusTextStyle: first lower + } + } + } + ercClient: { config: { //chatEntryFormat: "|00|08[|03{bbsTag}|08] |10{userName}|08: |02{message}" From 8e39f3ec3d5c834ddc487c3aa8b4b9d4d262f02c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 12:20:30 -0700 Subject: [PATCH 73/86] More luciano_blocktronics theme for file base --- mods/themes/luciano_blocktronics/FBRWSE.ANS | Bin 923 -> 899 bytes mods/themes/luciano_blocktronics/FDETGEN.ANS | Bin 865 -> 703 bytes mods/themes/luciano_blocktronics/FDLMGR.ANS | Bin 0 -> 2337 bytes mods/themes/luciano_blocktronics/FFILEDT.ANS | Bin 0 -> 2138 bytes mods/themes/luciano_blocktronics/FILPMPT.ANS | Bin 0 -> 283 bytes mods/themes/luciano_blocktronics/FPROSEL.ANS | Bin 0 -> 261 bytes mods/themes/luciano_blocktronics/FSEARCH.ANS | Bin 1033 -> 967 bytes mods/themes/luciano_blocktronics/RATEFILE.ANS | Bin 331 -> 311 bytes mods/themes/luciano_blocktronics/ULCHECK.ANS | Bin 2047 -> 2114 bytes mods/themes/luciano_blocktronics/ULDETAIL.ANS | Bin 0 -> 1941 bytes mods/themes/luciano_blocktronics/ULOPTS.ANS | Bin 0 -> 1921 bytes mods/themes/luciano_blocktronics/theme.hjson | 140 +++++++++++++++++- 12 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 mods/themes/luciano_blocktronics/FDLMGR.ANS create mode 100644 mods/themes/luciano_blocktronics/FFILEDT.ANS create mode 100644 mods/themes/luciano_blocktronics/FILPMPT.ANS create mode 100644 mods/themes/luciano_blocktronics/FPROSEL.ANS create mode 100644 mods/themes/luciano_blocktronics/ULDETAIL.ANS create mode 100644 mods/themes/luciano_blocktronics/ULOPTS.ANS diff --git a/mods/themes/luciano_blocktronics/FBRWSE.ANS b/mods/themes/luciano_blocktronics/FBRWSE.ANS index fc3314042cea97c776cfd7037af1fae636cff9d8..52db95e07a55be29e57fd74a563d3ea2f5e5f145 100644 GIT binary patch delta 318 zcmbQu-poEB(#_Z?_n&mMv3ah7bhM#0kmnfW>L?v;U~Oy$5&}v9#Z8^Tk_Nf=?{ER- zfI_AwAfa5<5FbNBpvc5Lv59LW>M;xpadZzx=m4oU;sWx)x-k?-x;h4-DmKQg*ahe| zgIs+eX_V^|3~>~bq1j|hMptKf__F?I4UMk`*R hf|AtS0)_I_B!!g82~1^7MkbSAGj%aaO)g_r1pp(YSJwal delta 328 zcmZo>pUplYQq<7e*eo|uI@$n47o{dDNJkr+Pn<6{aRMKgp@}n4B3D5{Vd7Gada%k8 zsLI6jVlI%%T44>7-bl?Y0D9Okw;bl$Brq4K1sooeqnXM?!Jz;)0q8$)fB@;qmzla4jV8x2 Gs{#Orby#%( diff --git a/mods/themes/luciano_blocktronics/FDETGEN.ANS b/mods/themes/luciano_blocktronics/FDETGEN.ANS index c9162e7a01646af6f92069d9c7638beb7c8a5426..ffd94744382a286f3522fbd8b28a0141e5186d8c 100644 GIT binary patch delta 189 zcmaFJwx3l1ac1V}o3MkSRdDIbby=Ff~P!br@BJf#Qip$r(U{jB+7XluRyTRAx+` zyp&O6@(o56#^T8wOvV$`UzR$hqd!3a~455%CWGpLrAr-!5^o{PTb+yr0Wu+9@{sT0Io=AN5bk8AmLQCR% zhkeF~p+y*2iW5b}TW0SVy&$@0(_=)))Q+k2?hqqGW==}L3+udg{6WWINRy8ph~BR@ zi(j68XL;&Js2#DJEd#y6Zol<5no(Co^tL+lWuyq|G%HBWO5a^=kd!B{~%eM1ADE>xgUN_7WmiC_@ z-kX_S`;yQgTsYp@nc4T=JUcg86|-qEtBT+zt1@(Lf5k3#lT{HAFAs2I5@LMVUl*GXNqSCv4XAKyDs_Iy#EPL*E*agG0hnsAdfgLY-qJ@ z(s&FBfFUMPfCT!To|&+hIt&``i78T>F8my3kZ`qaqKwnnBRIxk=ErsR&Sz zNtQQ_l8aE3EyYIa4waJK4FO4P%g_%a;Yot3G)@c(3p63BKk&J7ukUlim+zO~avZM( zPuem?MghU)gDskEy~&)?P>$>Cq}?6rX#+~+W&?GSV9-=3(`z&XzisIqPaYby8gvjv znIbZ3KWU}!B%6t|OjCepLSMw6KeO_%qLCx%%T2?S zl>de34~8heMB@1I`TWhnq9{=Lzb;>tP2o%b>%nNWv-4{7Xe8Iem(Q2(?a^FU`Thsn Cv*c_5 literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FFILEDT.ANS b/mods/themes/luciano_blocktronics/FFILEDT.ANS new file mode 100644 index 0000000000000000000000000000000000000000..19245e6fb6f409f3e81a860df483b1cf8bfb7aa4 GIT binary patch literal 2138 zcmb_dO>fgc6iiP@xpF~Rg1z*)HW&B2}~Ye$339_x2?z>||so+1L-0V#iLCBC|dz(iePkzOFGlt+9q3af9QA zd|Efg82rl+cZ~JiFF9baZ<6Ky)#1@{c5G%Z50`I`F$q5BbxTsXT#pE5qkOe#BA{v# zN;myRHBDVLfX_r+IvL@iswyGGxAj8<3b68fEZ^N{B4rYt1O#qK%ox!K&!Vh02C-oq zkhx3-Fk&t9VBiD|t3iu(5GF+%!v=H@w=qDD=LIT4f53bBpm zsW^ZWVMDMY_px(-yIT7jWw-t=eD;X%w`a=~kYQw9IH?flE6F-tL3PxytHT_L0nv#o zkP|lg1@7|v59+&?+XZfn}Hr8raemWg3)@ zn%%FO@K&x@rNH1;L~o+N#XJ*%GWG;Md)^m_ZD}knmE}Y~}s`RT3E8CLGMdxgZv8NMSIzD!FavSm0typxq*ebdt06xLjunnU$9nC+3 zzRqUn38?}_yaRXyiFhVCgyNW4D_${_ zz$oEjH%_y1p%z_Wd6b5$=rWz;3H%bou=K{EIC;(P;WaumfH3)80ImN~p%V~7dc0IM zI!uZN|CiHZ1Pz1Mu-xWXe!(86@$ zAT`@>=3@e?efrjQ%lX*?s_$V*OT?4%^z-oPV*mYgW-TlCKYg4{^VFuhKOYSSTU+l2 TPX_XNe0VT7A71b4Tl@VR{bG(p literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FILPMPT.ANS b/mods/themes/luciano_blocktronics/FILPMPT.ANS new file mode 100644 index 0000000000000000000000000000000000000000..40415248b99fd6a74b2e1ba6eebbc5263603be2e GIT binary patch literal 283 zcmb1+Hn27^ur@ZxRgjK0v^Fu%y(0}0GPgE1%Dr<3A_$Z>&%G}l4HcD+hH`O1_nmP00t%?4FtlTPQD6Z?v4;1oCE-} CoI3&l literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FPROSEL.ANS b/mods/themes/luciano_blocktronics/FPROSEL.ANS new file mode 100644 index 0000000000000000000000000000000000000000..bf41f1bb5040da6ac47c6f3485200e09021ab355 GIT binary patch literal 261 zcmb`AJr2Vl428+ig|%B*Sh^dCU|>oSsv;#NYJL{Z;DhwiRc#^V1oiN&=lAqkRfs)A z7f?!7tx2ky=m52`BtP#UAg+ rY?#q|e%KtKysg8YG5{6-H5N<&YwSk|(RGWs(5AVLk69njp}A9kfGsdb literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/FSEARCH.ANS b/mods/themes/luciano_blocktronics/FSEARCH.ANS index 857592cdb7f120274fac47d8c5b2548f08a7d91b..efb19617fd11db0273af49e0a95a2676a9b42547 100644 GIT binary patch delta 80 zcmeC=IL^LdDI4c8Hq)yi9iXl$*D~GjQNun eFgb#xizmNhN?;HF>dk`W>o+?pc)MT delta 128 zcmX@k-pR3HDWia)wXs=liFC99h)ylaoy^E2KKU@C1uu%k~ uhPnALfhs0Ph>~KM$bO~-Mx)6J%win+3ewTWM!A8LwVBHp7ffEntO@`cKqE>3 diff --git a/mods/themes/luciano_blocktronics/RATEFILE.ANS b/mods/themes/luciano_blocktronics/RATEFILE.ANS index 394e6fe5f42ad6544ee6542766c739313514b120..bbea2bd093202dafb32ce0c42942b1664ab53278 100644 GIT binary patch delta 25 hcmX@jw4G^!D5tSmZdzteszT93qtA@{CaW>30swG_2$28) delta 45 tcmdnabed^`sH~y2u~}}KbhH78&dfY5GA>Mn4lP}B-e~ZK(X1}UOeF}3|1ndMY^kwGZMiIA<<3{ti&#CBl;t( z?bLsvyRfx?!A56xFEP=BE9T9-dGp?WFTd3)Zp1>Lg*+HnZdO_9My)+JYNo1+4Xn1P z3davmOH>7>;$A9_bFlK;=PcDqOAufAHF2MNHN$36h%FK2)y52+r4l3nDf2BPx)BdD zDXhhT({>OLch36k9u$3cuCC1}ttP4l&>1^}ltSzus)0~FnI%(1pP@GCQ1qZA`cPGn z()tg4YZ-5rgf3Xv^Tlmp#~$Nt&yUy%Zj(b9GK%(i79Pn|K`T6n8jZt_k1@|RC8lG$ z7&)74*Tl;H?1~^0Vbk{t<`yZmXo8=*5OJ}-h({%t!(q370jJ`z*e)P25}(EW3U%W1 z1bX7A)S)UKCOsI4m(p&{yw)g*`*6}7CCJo3@l9Waj&4{>;3tey^0B*lh)F%XeqxyUma|u^&-Uabm1O zmvSx;ntj%1Zx43hQYVfGJF`#@6ZUAm=R(46bD|m%jyOW9myf9wO3v={Z8p#Eq%r`% z^4HrTkrE;4#32y^A`?rbVhMRnB%3eQHll!VUO}8I6#Bws?|@l_!y6C0!dG+u7x1-L zOkut*`4N$#f+RRsmT2Ro6M&!zeT+B&A3$*MoFY(GWB!zt-nb*(YF>^5(_9kr2Uf=Z>zXXRgTQ>)gg6l`XHsP#cldu5gg5Oq!kUwdkG*^8k)1f-QLIrYg=ObIwN5T_TNLhBY&l!iDt^k0Si9SQcI zl)g8!D=D#$)j|6;^Jd+8$am&KVmTdo)9t1Fnn{g{KTC+k%b z(=V!QTzW6eXC5=EvKh9O0ugaG4e=Oo4*c(%-M9Pg-NW|xZHW7cLDUgeAs4ZJxcRvY zTQEadrcbtvGr3qNIQ63hOsD%m^<=rWVke{6C7Lvq%?`Q1{G_K?Xjs_h`|xD-XLEDE zmuIiXkQTCCrkXX!K{osLU@W0g)};KP654wAYkOz5Kg{Fp?(u#T!hVdZ8CnGDpv+U9 zd7Yvv&w4T^r9%ghxKxYjPfphvak6}#7JjAOh+Re1wi%n2U4$mXOarmWih}VlQXK-e zEkY}6i-nOT)|su@h8ZmIMyDtzS!#GOmor!h7OTy4(HJ-sSdGzO4&-pc-iMwAp`||| zE%g|91dhN#hyuCb(O^uD8?tiDi%WmT`~$N?7%)>t!Nmo0Ab?lF)G<{)IP3O&0znT- zsND@7;0Hdc4V48SR-w~uh%HQ*y@Zexsx@FCen=Bl^UQM3kJm;=79n8-08v9>v5=2i zwsfIpz|wf++j9!`TBbvSd7ULEG6WF%Oeku}I$$RcIlsmagr1*6yqEc@*nM3!3L8USNff&el{=J_v?G z1)iRQ1OuCprltTw#O@FRwD?QXYyr~dRg9Zh-7$3j17A^tCL)0*DIh+-;N|sTe=_}^ z-Qe?gRLvnW?jcNN(ze{xxuZs6ke4{@Zd(0yIr;m!@%yXAb-9dVy8Wle#z_&|#2&vH YjgF2kNAE`Rygfg?Fke3}bk)880h4Hf$^ZZW literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/ULOPTS.ANS b/mods/themes/luciano_blocktronics/ULOPTS.ANS new file mode 100644 index 0000000000000000000000000000000000000000..f49d712ba898e8608df5c70848271803f8be6cfb GIT binary patch literal 1921 zcmb_d!EVz)5KT`=z0wnwz?Ym^JFZR1saO?MA`(&q7oTzn5>gi-1PA`A>F=zC|05^f zd$a2}P&na8@y_ndn|W_%*IDP5m0K>HKh3&D-QutDeaO0ESz*v;UFCewwR5ah4Lt{Q z8u|f#SPob&8-~J_PoC%W^@s*KVoJ*v2=3)FC zI4oF5AHvWF*l@H6`oh&>@WwA6o#+g=;-bZfK7t+Klyyyu7m+XrM8MFKoKbO2TB;9e z#YHAbmOfAfctjE@Ky2%gI#rBI$ejlmh9y|b5ikaow32m9&+T9- zu_k0mCz^2$(9}?L9>Rc|(MTafat8w2fn+cg0@ud5SU?g%YHGt!JV=2AmPn#VB&LX} zp9}=76^d+BRJ$}naq1iKsUxfW$W06YkVHwkpn`)e>P=x;zGEhq?_V-StyL3w4X4~T zYyL2>hY}MQd^mBC$`T|-_xCB`ae=0>;?zTtdCZ-xX)MMj5Kq#$Nx$blNF2BGSbUrc zi{K%(ox_etGuL}EM19Mt~qE=lnRle7RTymX*X;+d!;Uc#{# zNZ5HEh_t(t_(`m7e3HVD1agedgT|a}PdnGV)xa*Y+-2R1)T!J*+ziakp$2^~8Elf0 z$)RpB!8qMbyNCv?#a8>`0oy`GmM2REJm`OT^Trnfg$zy+ox zg@5`CxTqawys9h;(&gWu-{#+beA&W5ld5XJC%Dh&>s2XGy&nbztsK=Cj#9+yx%MAF z-EKc`{9jf-YKk3MwpYL$O@eQDPC{|h3liW>j` literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index da031d6e..4d279e65 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -622,10 +622,10 @@ fileAreaFilterEditor: { mci: { ET1: { - width: 42 + width: 26 } ET2: { - width: 42 + width: 26 } SM3: { width: 14 @@ -640,7 +640,7 @@ justify: right } ET6: { - width: 42 + width: 26 } HM7: { focusTextStyle: first lower @@ -648,6 +648,132 @@ } } + 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}" @@ -663,6 +789,14 @@ } } } + + fileMenuCommand: { + mci: { + TL1: { + text: "|00|15|MD|08: |03active filter|08: |10|FN" + } + } + } } } } \ No newline at end of file From 29d572c04c02645252163def8665cefd739ad5df Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 14:03:29 -0700 Subject: [PATCH 74/86] * Add terminal warning pre-nua preamble * Merge in menu chagnes for file area/etc. to menu.hjson * Missing luciano art --- mods/art/NEWUSER1.ANS | Bin 0 -> 653 bytes mods/menu.hjson | 694 ++++++++++++++++++- mods/themes/luciano_blocktronics/ULDUPES.ANS | Bin 0 -> 344 bytes mods/themes/luciano_blocktronics/theme.hjson | 2 +- 4 files changed, 689 insertions(+), 7 deletions(-) create mode 100644 mods/art/NEWUSER1.ANS create mode 100644 mods/themes/luciano_blocktronics/ULDUPES.ANS diff --git a/mods/art/NEWUSER1.ANS b/mods/art/NEWUSER1.ANS new file mode 100644 index 0000000000000000000000000000000000000000..56d1fe27ff8591da82172653b8420460f8acee93 GIT binary patch literal 653 zcmb`Ey>G%W5XD2s3|+dxa-AwwO5!xFkjO+3su-XZfvOTyJ`9P9?Z`Go{P#UOAzeDw zgQc_I{rvoHv?K8|iK8)dU3av5nT>W~9EB{Qdm$+_QWTVyH9w4Y5n;aX8x>(jnNY~i z8H#k3Y0y-%nrBDVmZbnLwCQiCJYGPZnnp+s(^b0gk|YHb;8P4mJ@72#7YL4kT0t%{ zSsKMX>ImB^YX)Ipy05QU!LOxMFbbm4wcyHJO?oy~aTKvf{KlBUit6==b1Dtm_=xdy zo-Hb^n@JF`i_&$G=gKbx3rcb3N0F7vuQWR%w9Cl?TLMcjyx7dwOB)n;)^!I{cx4>> z$Sdi$anhRk(ExqP85{Bd9jpGak@6N4b*WDsn!pG~ht@`~3 DOF6T* literal 0 HcmV?d00001 diff --git a/mods/menu.hjson b/mods/menu.hjson index 372c11ab..0783707f 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -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: { // @@ -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: { @@ -708,6 +752,10 @@ value: { command: "D" } action: @menu:doorMenu } + { + value: { command: "F" } + action: @menu:fileArea + } { 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 + //////////////////////////////////////////////////////////////////////// + + fileArea: { + desc: File Area + 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 //////////////////////////////////////////////////////////////////////// diff --git a/mods/themes/luciano_blocktronics/ULDUPES.ANS b/mods/themes/luciano_blocktronics/ULDUPES.ANS new file mode 100644 index 0000000000000000000000000000000000000000..7a4cb20b90ec08c1ebcadec5245db8f1fdd761fa GIT binary patch literal 344 zcmb`BJ!`{242GSlaBH{RqNUJo^4Y{Go?IscLqh}kSZ+FEwHPcrIPTx=PcfB0l3x%F zI_c$}_m#F7hZqMDE6_GD-1msJHOq9^I}!XiSn%q&`^?hhum%~vlGR>Y9DW<_l!6=Q zzY8Y-@~cSVyx_Vbs2kwAA;^C(WpQ>bfef|FuaHwq=a%w`f;XiG=RTLei;^nG)GOMC zX1u8nlZZ$hi=U!$CAfy%guR@$zMAioW4cC!$CW`pLWAL-=XtZ)&U=zq>N)$^!fBI? H+r9q-w)RXn literal 0 HcmV?d00001 diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index 4d279e65..3dfe67e6 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -793,7 +793,7 @@ fileMenuCommand: { mci: { TL1: { - text: "|00|15|MD|08: |03active filter|08: |10|FN" + text: "|00|15|MD|08 >> |03active filter|08: |10|FN" } } } From 5ab47232fc7694277a1c97bdc50a08b1679e5b13 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 19:42:18 -0700 Subject: [PATCH 75/86] Some minor menu.hjson updates --- mods/menu.hjson | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mods/menu.hjson b/mods/menu.hjson index 0783707f..0521c5e1 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -52,7 +52,7 @@ // sshConnectedNewUser: { art: CONNECT - next: newUserApplicationSsh + next: newUserApplicationPreSsh options: { nextTimeout: 1500 } } @@ -78,7 +78,7 @@ } { value: { 1: 1 }, - action: @menu:newUserApplication + action: @menu:newUserApplicationPre } { value: { 1: 2 }, @@ -394,7 +394,7 @@ } { value: { "submission" : 1 } - action: @systemMethod:prevMenu + action: @systemMethod:logoff } ] } @@ -402,7 +402,7 @@ actionKeys: [ { keys: [ "escape" ] - action: @systemMethod:prevMenu + action: @systemMethod:logoff } ] } @@ -754,7 +754,7 @@ } { value: { command: "F" } - action: @menu:fileArea + action: @menu:fileBase } { value: { command: "U" } From e95aba0582058fa61129ba554cdb542d7f84ca3c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 19:49:56 -0700 Subject: [PATCH 76/86] Fix some DB startup issues for new installs --- core/database.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/core/database.js b/core/database.js index 6dca0101..2c327f4c 100644 --- a/core/database.js +++ b/core/database.js @@ -54,16 +54,16 @@ function getISOTimestampString(ts) { } function initializeDatabases(cb) { - async.each( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { + async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => { if(err) { return cb(err); } dbs[dbName].serialize( () => { - DB_INIT_TABLE[dbName](); - - return next(null); + DB_INIT_TABLE[dbName]( () => { + return next(null); + }); }); }); }, err => { @@ -72,7 +72,7 @@ function initializeDatabases(cb) { } const DB_INIT_TABLE = { - system : () => { + system : (cb) => { dbs.system.run('PRAGMA foreign_keys = ON;'); // Various stat/event logging - see stat_log.js @@ -105,9 +105,11 @@ const DB_INIT_TABLE = { UNIQUE(timestamp, user_id, log_name) );` ); + + return cb(null); }, - user : () => { + user : (cb) => { dbs.user.run('PRAGMA foreign_keys = ON;'); dbs.user.run( @@ -145,9 +147,11 @@ const DB_INIT_TABLE = { timestamp DATETIME NOT NULL );` ); + + return cb(null); }, - message : () => { + message : (cb) => { dbs.message.run('PRAGMA foreign_keys = ON;'); dbs.message.run( @@ -251,9 +255,11 @@ const DB_INIT_TABLE = { UNIQUE(scan_toss, area_tag) );` ); + + return cb(null); }, - file : () => { + file : (cb) => { dbs.file.run('PRAGMA foreign_keys = ON;'); dbs.file.run( @@ -352,5 +358,7 @@ const DB_INIT_TABLE = { expire_timestamp DATETIME NOT NULL );` ); + + return cb(null); } }; \ No newline at end of file From 098a187f2cb1f4ccedc54ea4b0054f5e1f0d59b2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 19:59:00 -0700 Subject: [PATCH 77/86] * Fix fileArea -> fileBase * Fix new config creation (missing conf name/desc) --- mods/menu.hjson | 4 ++-- oputil.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mods/menu.hjson b/mods/menu.hjson index 0521c5e1..c98b0122 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -2279,8 +2279,8 @@ // File Area //////////////////////////////////////////////////////////////////////// - fileArea: { - desc: File Area + fileBase: { + desc: File Base art: FMENU prompt: fileMenuCommand submit: [ diff --git a/oputil.js b/oputil.js index 27e7f759..c93ccc0c 100755 --- a/oputil.js +++ b/oputil.js @@ -373,6 +373,9 @@ function askNewConfigQuestions(cb) { }; config.messageConferences.another_sample_conf = { + name : 'Another Sample Conference', + desc : 'Another conf sample. Change me!', + areas : { another_sample_area : { name : 'Another Sample Area', From 2cc1c5170ca2b43e5beb14dc8ea0c71377ef388a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 20:27:40 -0700 Subject: [PATCH 78/86] Fix upload then download bug --- mods/file_base_download_manager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index e7705c41..812a2422 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -52,7 +52,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { downloadAll : (formData, extraArgs, cb) => { const modOpts = { extraArgs : { - sendQueue : this.dlQueue.items, + sendQueue : this.dlQueue.items, + direction : 'send', } }; From 4aba90e024b1e3568073b4eb65d45c689939b935 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 20:41:15 -0700 Subject: [PATCH 79/86] Update luciano STATUS art --- mods/themes/luciano_blocktronics/STATUS.ANS | Bin 4097 -> 4517 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/mods/themes/luciano_blocktronics/STATUS.ANS b/mods/themes/luciano_blocktronics/STATUS.ANS index f119dfb9776c43e83ecaa00c9bcee70ecbacd970..b90ed2d9ffede5f3db62c67ff44131899d870bca 100644 GIT binary patch delta 895 zcmZovSgJhXql}@cvvjnfwXs>Qf^@WjwXqS1Ha5=%3Ytv(>}!uCnX3SjGRrO1&jTw_ z00OYIL9P`-uVJogsGlK_4>H1VvKM2Ku_4e3puWVkG%Wg{HktvoxtW060MchL`6Xkm zun|OGdQkyf-{b-&NiKI&xbc&jnzg}(XXR(+rKUjC!5jr~uBvaaF}4{$a# zfNC_DypMewFcE+gk{Kl6QuK3*kpd2+5@HT8>RtQ{4Y{PFCs%Ux!gN7{8>ma6xC*<4 zF5ZSlAYI&?6ZD{#l;W`@G{6v+5GK#zoQ0$XrwyUrh9K{QlH_D_u38bW`@vZt1s)cl p#KPkjWC%-?29tMi%@+Z=2OO=C=mr^KxjB-%iG|5VaPnP#6#xdb=%fGu delta 461 zcmZ3g+^8_&Ba^Y=WL`#JuKUu_#^$-w(MD#IXEGLX894*_X1S&Md6UmENpkraN=F+2 zWerUx+cDL08AD`?pt3=Hgi=|!D5@KO`Q2|1^u@OiyP>X7)yD3~X$O>Q9 z5`LiU9R;W@B^kM3Uxb*!l`*n4s7XhgLX1t$&x08YH8D8E0PUFc7*=MK&b%JFJ@{3C_bbxFF1-hZ>wMLRmsw r6BvyrU*MVrCPgQ&<<4LM#*q1BFP`~KMh2Vz@-(q9&ETKhD5wGewQ_;B From ca695d779bb0a4d82f96d28f41326f263affae6d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 20:48:28 -0700 Subject: [PATCH 80/86] Bump install.sh node version to 6 --- misc/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/install.sh b/misc/install.sh index 66f92d68..915d3355 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -2,7 +2,7 @@ { # this ensures the entire script is downloaded before execution -ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=4} +ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=6} ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs} ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` From f85388e747b219486977eabab165335f95dd330e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 21:52:56 -0700 Subject: [PATCH 81/86] Remove bad code :) --- core/string_util.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/string_util.js b/core/string_util.js index 860b78d4..0c296da8 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -508,6 +508,7 @@ function createCleanAnsi(input, options, cb) { parser.parse(input); } +/* const fs = require('fs'); let data = fs.readFileSync('/home/nuskooler/Downloads/art3.ans'); data = iconv.decode(data, 'cp437'); @@ -515,3 +516,4 @@ createCleanAnsi(data, { width : 79, height : 25 }, (out) => { out = iconv.encode(out, 'cp437'); fs.writeFileSync('/home/nuskooler/Downloads/art4.ans', out); }); +*/ \ No newline at end of file From aa40d998b2d2683d7a59e568ba22e898a7979832 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Feb 2017 22:13:03 -0700 Subject: [PATCH 82/86] Fix from username in message post --- core/fse.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/fse.js b/core/fse.js index b2a5e19d..eabf1e54 100644 --- a/core/fse.js +++ b/core/fse.js @@ -316,12 +316,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } buildMessage() { - var headerValues = this.viewControllers.header.getFormData().value; + const headerValues = this.viewControllers.header.getFormData().value; var msgOpts = { areaTag : this.messageAreaTag, toUserName : headerValues.to, - fromUserName : headerValues.from, + fromUserName : this.client.user.username, subject : headerValues.subject, message : this.viewControllers.body.getFormData().value.message, }; From 9b0f9569347d9d0ef523732dc3988fd61d59e6c3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 13 Feb 2017 22:51:20 -0700 Subject: [PATCH 83/86] * Start work on new oputil format: oputil * Add auto tagging for oputil scan --- core/file_base_area.js | 17 ++++++--- oputil.js | 78 ++++++++++++++++++++++++++++++------------ 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 69e4d4cf..0e05b497 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -584,10 +584,14 @@ function scanFile(filePath, options, iterator, cb) { ); } -function scanFileAreaForChanges(areaInfo, iterator, cb) { - if(!cb && _.isFunction(iterator)) { - cb = iterator; - iterator = null; +function scanFileAreaForChanges(areaInfo, options, iterator, cb) { + if(3 === arguments.length && _.isFunction(iterator)) { + cb = iterator; + iterator = null; + } else if(2 === arguments.length && _.isFunction(options)) { + cb = options; + iterator = null; + options = {}; } const storageLocations = getAreaStorageLocations(areaInfo); @@ -632,6 +636,11 @@ function scanFileAreaForChanges(areaInfo, iterator, cb) { if(dupeEntries.length > 0) { // :TODO: Handle duplidates -- what to do here??? } else { + if(Array.isArray(options.tags)) { + options.tags.forEach(tag => { + fileEntry.hashTags.add(tag); + }); + } addNewFileEntry(fileEntry, fullPath, err => { // pass along error; we failed to insert a record in our DB or something else bad return nextFile(err); diff --git a/oputil.js b/oputil.js index c93ccc0c..c14f04fb 100755 --- a/oputil.js +++ b/oputil.js @@ -35,10 +35,11 @@ const USAGE_HELP = { global args: --config PATH : specify config path (${getDefaultConfigPath()}) -commands: +where is one of: user : user utilities config : config file management - file-base : file base management + file-base + fb : file base management `, User : @@ -59,10 +60,14 @@ valid args: --new : generate a new/initial configuration `, FileBase : -`usage: oputil.js file-base +`usage: oputil.js file-base [] [] -valid args: - --scan AREA_TAG : (re)scan area specified by AREA_TAG for new files +where is one of: + scan AREA_TAG : (re)scan area specified by AREA_TAG for new files + multiple area tags can be specified in form of AREA_TAG1 AREA_TAG2 ... + +scan args: + --tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries ` }; @@ -375,7 +380,7 @@ function askNewConfigQuestions(cb) { config.messageConferences.another_sample_conf = { name : 'Another Sample Conference', desc : 'Another conf sample. Change me!', - + areas : { another_sample_area : { name : 'Another Sample Area', @@ -438,7 +443,41 @@ function handleConfigCommand() { } } +function scanFileBaseArea(areaTag, options, iterator, cb) { + async.waterfall( + [ + function getFileArea(callback) { + const fileAreaMod = require('./core/file_base_area.js'); + + const areaInfo = fileAreaMod.getFileAreaByTag(areaTag); + if(!areaInfo) { + return callback(new Error(`Invalid file base area tag: ${areaTag}`)); + } + + return callback(null, fileAreaMod, areaInfo); + }, + function performScan(fileAreaMod, areaInfo, callback) { + fileAreaMod.scanFileAreaForChanges(areaInfo, options, iterator, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); +} + function fileAreaScan() { + const options = {}; + + const tags = argv.tags; + if(tags) { + options.tags = tags.split(','); + } + + const areaTags = argv._.slice(2); + function scanFileIterator(stepInfo, nextScanStep) { if('start' === stepInfo.step) { console.info(`Scanning ${stepInfo.filePath}...`); @@ -447,23 +486,17 @@ function fileAreaScan() { // :TODO: add 'finished' step when avail } - async.waterfall( + async.series( [ function init(callback) { return initConfigAndDatabases(callback); }, - function getFileArea(callback) { - const fileAreaMod = require('./core/file_base_area.js'); - - const areaInfo = fileAreaMod.getFileAreaByTag(argv.scan); - if(!areaInfo) { - return callback(new Error('Invalid file area')); - } - - return callback(null, fileAreaMod, areaInfo); - }, - function performScan(fileAreaMod, areaInfo, callback) { - fileAreaMod.scanFileAreaForChanges(areaInfo, scanFileIterator, err => { + function scanAreas(callback) { + async.eachSeries(areaTags, (areaTag, nextAreaTag) => { + scanFileBaseArea(areaTag, options, scanFileIterator, err => { + return nextAreaTag(err); + }); + }, err => { return callback(err); }); } @@ -482,8 +515,10 @@ function handleFileBaseCommand() { return printUsageAndSetExitCode('FileBase', ExitCodes.ERROR); } - if(argv.scan) { - return fileAreaScan(argv.scan); + const action = argv._[1]; + + switch(action) { + case 'scan' : return fileAreaScan(); } } @@ -511,6 +546,7 @@ function main() { break; case 'file-base' : + case 'fb' : handleFileBaseCommand(); break; From d41fbf6911e51c2019f841daf3097651c8e58cc4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 13 Feb 2017 22:54:56 -0700 Subject: [PATCH 84/86] Add file base menu item to luciano art --- mods/themes/luciano_blocktronics/MMENU.ANS | Bin 3398 -> 3429 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/mods/themes/luciano_blocktronics/MMENU.ANS b/mods/themes/luciano_blocktronics/MMENU.ANS index b23050131b90c856e7de67d328d37942e126a103..995a5db59590a759b09502a341a97ae8ed2fb8f8 100644 GIT binary patch delta 64 zcmX>m^;Bwu6c3~6WN#itM$5@EJbFrohR)K_hPi3d(FWGWX1NM!nK`KnNr}a&($U7| Uxzf?5Mw9b+HZlI1tjMbh06`uSR{#J2 delta 33 pcmaDVbxdl56c3Y;*<@QDJx0sP(maYxrpA+dc{VZLp6tM@3IL;j31|QS From d0511d5d745b11aedf0cfc8ac6b1c88b45baebbf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 15 Feb 2017 20:27:16 -0700 Subject: [PATCH 85/86] * Split out oputil stuff into modules based on * oputil: better syntax * oputil: allow areaTag@storageTag for scan, e.g. oputil fb scan retro_pc@some_specific_storage --- UPGRADE.md | 1 + core/file_base_area.js | 1 + core/oputil/oputil_common.js | 76 +++++ core/oputil/oputil_config.js | 258 +++++++++++++++ core/oputil/oputil_file_base.js | 175 ++++++++++ core/oputil/oputil_help.js | 55 ++++ core/oputil/oputil_main.js | 45 +++ core/oputil/oputil_user.js | 112 +++++++ docs/archive.md | 2 +- oputil.js | 553 +------------------------------- 10 files changed, 725 insertions(+), 553 deletions(-) create mode 100644 core/oputil/oputil_common.js create mode 100644 core/oputil/oputil_config.js create mode 100644 core/oputil/oputil_file_base.js create mode 100644 core/oputil/oputil_help.js create mode 100644 core/oputil/oputil_main.js create mode 100644 core/oputil/oputil_user.js diff --git a/UPGRADE.md b/UPGRADE.md index 3726fb78..802a8092 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -28,6 +28,7 @@ Upgrading from GitHub is easy: ```bash cd /path/to/enigma-bbs git pull +rm -rf npm_modules # do this any time you update Node.js itself npm install ``` diff --git a/core/file_base_area.js b/core/file_base_area.js index 0e05b497..bce81fac 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -24,6 +24,7 @@ exports.isInternalArea = isInternalArea; exports.getAvailableFileAreas = getAvailableFileAreas; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; +exports.getAreaStorageLocations = getAreaStorageLocations; exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getFileAreaByTag = getFileAreaByTag; exports.getFileEntryPath = getFileEntryPath; diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js new file mode 100644 index 00000000..3b99bf11 --- /dev/null +++ b/core/oputil/oputil_common.js @@ -0,0 +1,76 @@ +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const resolvePath = require('../misc_util.js').resolvePath; + +const config = require('../../core/config.js'); +const db = require('../../core/database.js'); + +const _ = require('lodash'); +const async = require('async'); + +exports.printUsageAndSetExitCode = printUsageAndSetExitCode; +exports.getDefaultConfigPath = getDefaultConfigPath; +exports.initConfigAndDatabases = initConfigAndDatabases; +exports.getAreaAndStorage = getAreaAndStorage; + +const exitCodes = exports.ExitCodes = { + SUCCESS : 0, + ERROR : -1, + BAD_COMMAND : -2, + BAD_ARGS : -3, +}; + +const argv = exports.argv = require('minimist')(process.argv.slice(2)); + +function printUsageAndSetExitCode(errMsg, exitCode) { + if(_.isUndefined(exitCode)) { + exitCode = exitCodes.ERROR; + } + + process.exitCode = exitCode; + + if(errMsg) { + console.error(errMsg); + } +} + +function getDefaultConfigPath() { + return resolvePath('~/.config/enigma-bbs/config.hjson'); +} + +function initConfig(cb) { + const configPath = argv.config ? argv.config : config.getDefaultPath(); + + config.init(configPath, cb); +} + +function initConfigAndDatabases(cb) { + async.series( + [ + function init(callback) { + initConfig(callback); + }, + function initDb(callback) { + db.initializeDatabases(callback); + }, + ], + err => { + return cb(err); + } + ); +} + +function getAreaAndStorage(tags) { + return tags.map(tag => { + const parts = tag.split('@'); + const entry = { + areaTag : parts[0], + }; + if(parts[1]) { + entry.storageTag = parts[1]; + } + return entry; + }); +} \ No newline at end of file diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js new file mode 100644 index 00000000..7071f459 --- /dev/null +++ b/core/oputil/oputil_config.js @@ -0,0 +1,258 @@ +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +// ENiGMA½ +const resolvePath = require('../../core/misc_util.js').resolvePath; +const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; +const ExitCodes = require('./oputil_common.js').ExitCodes; +const argv = require('./oputil_common.js').argv; +const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath; +const getHelpFor = require('./oputil_help.js').getHelpFor; + +// deps +const async = require('async'); +const inq = require('inquirer'); +const mkdirsSync = require('fs-extra').mkdirsSync; +const fs = require('fs'); +const hjson = require('hjson'); +const paths = require('path'); + +exports.handleConfigCommand = handleConfigCommand; + + +function getAnswers(questions, cb) { + inq.prompt(questions).then( answers => { + return cb(answers); + }); +} + +const QUESTIONS = { + Intro : [ + { + name : 'createNewConfig', + message : 'Create a new configuration?', + type : 'confirm', + default : false, + }, + { + name : 'configPath', + message : 'Configuration path:', + default : argv.config ? argv.config : getDefaultConfigPath(), + when : answers => answers.createNewConfig + }, + ], + + OverwriteConfig : [ + { + name : 'overwriteConfig', + message : 'Config file exists. Overwrite?', + type : 'confirm', + default : false, + } + ], + + Basic : [ + { + name : 'boardName', + message : 'BBS name:', + default : 'New ENiGMA½ BBS', + }, + ], + + Misc : [ + { + name : 'loggingLevel', + message : 'Logging level:', + type : 'list', + choices : [ 'Error', 'Warn', 'Info', 'Debug', 'Trace' ], + default : 2, + filter : s => s.toLowerCase(), + }, + { + name : 'sevenZipExe', + message : '7-Zip executable:', + type : 'list', + choices : [ '7z', '7za', 'None' ] + } + ], + + MessageConfAndArea : [ + { + name : 'msgConfName', + message : 'First message conference:', + default : 'Local', + }, + { + name : 'msgConfDesc', + message : 'Conference description:', + default : 'Local Areas', + }, + { + name : 'msgAreaName', + message : 'First area in message conference:', + default : 'General', + }, + { + name : 'msgAreaDesc', + message : 'Area description:', + default : 'General chit-chat', + } + ] +}; + +function makeMsgConfAreaName(s) { + return s.toLowerCase().replace(/\s+/g, '_'); +} + +function askNewConfigQuestions(cb) { + + const ui = new inq.ui.BottomBar(); + + let configPath; + let config; + + async.waterfall( + [ + function intro(callback) { + getAnswers(QUESTIONS.Intro, answers => { + if(!answers.createNewConfig) { + return callback('exit'); + } + + // adjust for ~ and the like + configPath = resolvePath(answers.configPath); + + const configDir = paths.dirname(configPath); + mkdirsSync(configDir); + + // + // Check if the file exists and can be written to + // + fs.access(configPath, fs.F_OK | fs.W_OK, err => { + if(err) { + if('EACCES' === err.code) { + ui.log.write(`${configPath} cannot be written to`); + callback('exit'); + } else if('ENOENT' === err.code) { + callback(null, false); + } + } else { + callback(null, true); // exists + writable + } + }); + }); + }, + function promptOverwrite(needPrompt, callback) { + if(needPrompt) { + getAnswers(QUESTIONS.OverwriteConfig, answers => { + callback(answers.overwriteConfig ? null : 'exit'); + }); + } else { + callback(null); + } + }, + function basic(callback) { + getAnswers(QUESTIONS.Basic, answers => { + config = { + general : { + boardName : answers.boardName, + }, + }; + + callback(null); + }); + }, + function msgConfAndArea(callback) { + getAnswers(QUESTIONS.MessageConfAndArea, answers => { + config.messageConferences = {}; + + const confName = makeMsgConfAreaName(answers.msgConfName); + const areaName = makeMsgConfAreaName(answers.msgAreaName); + + config.messageConferences[confName] = { + name : answers.msgConfName, + desc : answers.msgConfDesc, + sort : 1, + default : true, + }; + + config.messageConferences.another_sample_conf = { + name : 'Another Sample Conference', + desc : 'Another conference example. Change me!', + sort : 2, + }; + + config.messageConferences[confName].areas = {}; + config.messageConferences[confName].areas[areaName] = { + name : answers.msgAreaName, + desc : answers.msgAreaDesc, + sort : 1, + default : true, + }; + + config.messageConferences.another_sample_conf = { + name : 'Another Sample Conference', + desc : 'Another conf sample. Change me!', + + areas : { + another_sample_area : { + name : 'Another Sample Area', + desc : 'Another area example. Change me!', + sort : 2 + } + } + }; + + callback(null); + }); + }, + function misc(callback) { + getAnswers(QUESTIONS.Misc, answers => { + if('None' !== answers.sevenZipExe) { + config.archivers = { + zip : { + compressCmd : answers.sevenZipExe, + decompressCmd : answers.sevenZipExe, + } + }; + } + + config.logging = { + level : answers.loggingLevel, + }; + + callback(null); + }); + } + ], + err => { + cb(err, configPath, config); + } + ); +} + +function handleConfigCommand() { + if(true === argv.help) { + return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); + } + + if(argv.new) { + askNewConfigQuestions( (err, configPath, config) => { + if(err) { + return; + } + + config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t' } ); + + try { + fs.writeFileSync(configPath, config, 'utf8'); + console.info('Configuration generated'); + } catch(e) { + console.error('Exception attempting to create config: ' + e.toString()); + } + }); + } else { + return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); + } +} diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js new file mode 100644 index 00000000..fe9ebb54 --- /dev/null +++ b/core/oputil/oputil_file_base.js @@ -0,0 +1,175 @@ +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; +const ExitCodes = require('./oputil_common.js').ExitCodes; +const argv = require('./oputil_common.js').argv; +const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; +const getHelpFor = require('./oputil_help.js').getHelpFor; +const getAreaAndStorage = require('./oputil_common.js').getAreaAndStorage; + + +const async = require('async'); +const fs = require('fs'); +const paths = require('path'); + +exports.handleFileBaseCommand = handleFileBaseCommand; + +/* + :TODO: + + Global options: + --yes: assume yes + --no-prompt: try to avoid user input + + Prompt for import and description before scan + * Only after finding duplicate-by-path + * Default to filename -> desc if auto import + +*/ + +let fileArea; // required during init + +function scanFileAreaForChanges(areaInfo, options, cb) { + + const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => { + return options.areaAndStorageInfo.find(asi => { + return !asi.storageTag || sl.storageTag === asi.storageTag; + }); + }); + + async.eachSeries(storageLocations, (storageLoc, nextLocation) => { + async.series( + [ + function scanPhysFiles(callback) { + const physDir = storageLoc.dir; + + fs.readdir(physDir, (err, files) => { + if(err) { + return callback(err); + } + + async.eachSeries(files, (fileName, nextFile) => { + const fullPath = paths.join(physDir, fileName); + + fs.stat(fullPath, (err, stats) => { + if(err) { + // :TODO: Log me! + return nextFile(null); // always try next file + } + + if(!stats.isFile()) { + return nextFile(null); + } + + process.stdout.write(`* Scanning ${fullPath}... `); + + fileArea.scanFile( + fullPath, + { + areaTag : areaInfo.areaTag, + storageTag : storageLoc.storageTag + }, + (err, fileEntry, dupeEntries) => { + if(err) { + // :TODO: Log me!!! + console.info(`Error: ${err.message}`); + return nextFile(null); // try next anyway + } + + + + if(dupeEntries.length > 0) { + // :TODO: Handle duplidates -- what to do here??? + console.info('Dupe'); + return nextFile(null); + } else { + console.info('Done!'); + if(Array.isArray(options.tags)) { + options.tags.forEach(tag => { + fileEntry.hashTags.add(tag); + }); + } + + fileEntry.persist( err => { + return nextFile(err); + }); + } + } + ); + }); + }, err => { + return callback(err); + }); + }); + }, + function scanDbEntries(callback) { + // :TODO: Look @ db entries for area that were *not* processed above + return callback(null); + } + ], + err => { + return nextLocation(err); + } + ); + }, + err => { + return cb(err); + }); +} + +function scanFileAreas() { + const options = {}; + + const tags = argv.tags; + if(tags) { + options.tags = tags.split(','); + } + + options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); + + async.series( + [ + function init(callback) { + return initConfigAndDatabases(callback); + }, + function scanAreas(callback) { + fileArea = require('../../core/file_base_area.js'); + + async.eachSeries(options.areaAndStorageInfo, (areaAndStorage, nextAreaTag) => { + const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); + if(!areaInfo) { + return nextAreaTag(new Error(`Invalid file base area tag: ${areaAndStorage.areaTag}`)); + } + + console.info(`Processing area "${areaInfo.name}":`); + + scanFileAreaForChanges(areaInfo, options, err => { + return callback(err); + }); + }, err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); +} + +function handleFileBaseCommand() { + if(true === argv.help) { + return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); + } + + const action = argv._[1]; + + switch(action) { + case 'scan' : return scanFileAreas(); + } +} \ No newline at end of file diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js new file mode 100644 index 00000000..25237680 --- /dev/null +++ b/core/oputil/oputil_help.js @@ -0,0 +1,55 @@ +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath; + +exports.getHelpFor = getHelpFor; + +const usageHelp = exports.USAGE_HELP = { + General : +`usage: optutil.js [--version] [--help] + [] + +global args: + --config PATH : specify config path (${getDefaultConfigPath()}) + +where is one of: + user : user utilities + config : config file management + file-base + fb : file base management + +`, + User : +`usage: optutil.js user --user USERNAME + +valid args: + --user USERNAME : specify username for further actions + --password PASS : set new password + --delete : delete user + --activate : activate user + --deactivate : deactivate user +`, + + Config : +`usage: optutil.js config + +valid args: + --new : generate a new/initial configuration +`, + FileBase : +`usage: oputil.js file-base [] [] + +where is one of: + scan AREA_TAG : (re)scan area specified by AREA_TAG for new files + multiple area tags can be specified in form of AREA_TAG1 AREA_TAG2 ... + +valid scan : + --tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries +` +}; + +function getHelpFor(command) { + return usageHelp[command]; +} \ No newline at end of file diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js new file mode 100644 index 00000000..78dae8d2 --- /dev/null +++ b/core/oputil/oputil_main.js @@ -0,0 +1,45 @@ +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const ExitCodes = require('./oputil_common.js').ExitCodes; +const argv = require('./oputil_common.js').argv; +const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; +const handleUserCommand = require('./oputil_user.js').handleUserCommand; +const handleFileBaseCommand = require('./oputil_file_base.js').handleFileBaseCommand; +const handleConfigCommand = require('./oputil_config.js').handleConfigCommand; +const getHelpFor = require('./oputil_help.js').getHelpFor; + + +module.exports = function() { + + process.exitCode = ExitCodes.SUCCESS; + + if(true === argv.version) { + return console.info(require('../package.json').version); + } + + if(0 === argv._.length || + 'help' === argv._[0]) + { + printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS); + } + + switch(argv._[0]) { + case 'user' : + handleUserCommand(); + break; + + case 'config' : + handleConfigCommand(); + break; + + case 'file-base' : + case 'fb' : + handleFileBaseCommand(); + break; + + default: + return printUsageAndSetExitCode('', ExitCodes.BAD_COMMAND); + } +}; diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js new file mode 100644 index 00000000..feb712f6 --- /dev/null +++ b/core/oputil/oputil_user.js @@ -0,0 +1,112 @@ +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; +const ExitCodes = require('./oputil_common.js').ExitCodes; +const argv = require('./oputil_common.js').argv; +const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; + + +const async = require('async'); + +exports.handleUserCommand = handleUserCommand; + +function handleUserCommand() { + if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) { + return printUsageAndSetExitCode('User', ExitCodes.ERROR); + } + + if(_.isString(argv.password)) { + if(0 === argv.password.length) { + process.exitCode = ExitCodes.BAD_ARGS; + return console.error('Invalid password'); + } + + async.waterfall( + [ + function init(callback) { + initAndGetUser(argv.user, callback); + }, + function setNewPass(user, callback) { + user.setNewAuthCredentials(argv.password, function credsSet(err) { + if(err) { + process.exitCode = ExitCodes.ERROR; + callback(new Error('Failed setting password')); + } else { + callback(null); + } + }); + } + ], + function complete(err) { + if(err) { + console.error(err.message); + } else { + console.info('Password set'); + } + } + ); + } else if(argv.activate) { + setAccountStatus(argv.user, true); + } else if(argv.deactivate) { + setAccountStatus(argv.user, false); + } +} + +function getUser(userName, cb) { + const user = require('./core/user.js'); + user.getUserIdAndName(argv.user, function userNameAndId(err, userId) { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + return cb(new Error('Failed to retrieve user')); + } else { + let u = new user.User(); + u.userId = userId; + return cb(null, u); + } + }); +} + +function initAndGetUser(userName, cb) { + async.waterfall( + [ + function init(callback) { + initConfigAndDatabases(callback); + }, + function getUserObject(callback) { + getUser(argv.user, (err, user) => { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + return callback(err); + } + return callback(null, user); + }); + } + ], + (err, user) => { + return cb(err, user); + } + ); +} + +function setAccountStatus(userName, active) { + async.waterfall( + [ + function init(callback) { + initAndGetUser(argv.user, callback); + }, + function activateUser(user, callback) { + const AccountStatus = require('./core/user.js').User.AccountStatus; + user.persistProperty('account_status', active ? AccountStatus.active : AccountStatus.inactive, callback); + } + ], + err => { + if(err) { + console.error(err.message); + } else { + console.info('User ' + ((true === active) ? 'activated' : 'deactivated')); + } + } + ); +} \ No newline at end of file diff --git a/docs/archive.md b/docs/archive.md index 4b5a005e..8303858c 100644 --- a/docs/archive.md +++ b/docs/archive.md @@ -15,7 +15,7 @@ The following archivers are pre-configured in ENiGMA½ as of this writing. Remem #### Lha * Formats: LHA files such as .lzh. * Key: `Lha` -* Homepage/package: `lhasa` on most *nix environments. See also https://fragglet.github.io/lhasa/ +* Homepage/package: `lhasa` on most *nix environments. See also https://fragglet.github.io/lhasa/ and http://www2m.biglobe.ne.jp/~dolphin/lha/lha-unix.htm #### Arj * Formats: .arj diff --git a/oputil.js b/oputil.js index c14f04fb..365b0d06 100755 --- a/oputil.js +++ b/oputil.js @@ -4,555 +4,4 @@ /* eslint-disable no-console */ 'use strict'; -// ENiGMA½ -const config = require('./core/config.js'); -const db = require('./core/database.js'); -const resolvePath = require('./core/misc_util.js').resolvePath; - -// deps -const _ = require('lodash'); -const async = require('async'); -const inq = require('inquirer'); -const mkdirsSync = require('fs-extra').mkdirsSync; -const fs = require('fs'); -const hjson = require('hjson'); -const paths = require('path'); - -const argv = require('minimist')(process.argv.slice(2)); - -const ExitCodes = { - SUCCESS : 0, - ERROR : -1, - BAD_COMMAND : -2, - BAD_ARGS : -3, -}; - -const USAGE_HELP = { - General : -`usage: optutil.js [--version] [--help] - [] - -global args: - --config PATH : specify config path (${getDefaultConfigPath()}) - -where is one of: - user : user utilities - config : config file management - file-base - fb : file base management - -`, - User : -`usage: optutil.js user --user USERNAME - -valid args: - --user USERNAME : specify username for further actions - --password PASS : set new password - --delete : delete user - --activate : activate user - --deactivate : deactivate user -`, - - Config : -`usage: optutil.js config - -valid args: - --new : generate a new/initial configuration -`, - FileBase : -`usage: oputil.js file-base [] [] - -where is one of: - scan AREA_TAG : (re)scan area specified by AREA_TAG for new files - multiple area tags can be specified in form of AREA_TAG1 AREA_TAG2 ... - -scan args: - --tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries -` -}; - -function printUsageAndSetExitCode(command, exitCode) { - if(_.isUndefined(exitCode)) { - exitCode = ExitCodes.ERROR; - } - process.exitCode = exitCode; - const errMsg = USAGE_HELP[command]; - if(errMsg) { - console.error(errMsg); - } -} - -function initConfig(cb) { - const configPath = argv.config ? argv.config : config.getDefaultPath(); - - config.init(configPath, cb); -} - -function initConfigAndDatabases(cb) { - async.series( - [ - function init(callback) { - initConfig(callback); - }, - function initDb(callback) { - db.initializeDatabases(callback); - }, - ], - err => { - return cb(err); - } - ); -} - -function getUser(userName, cb) { - const user = require('./core/user.js'); - user.getUserIdAndName(argv.user, function userNameAndId(err, userId) { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - return cb(new Error('Failed to retrieve user')); - } else { - let u = new user.User(); - u.userId = userId; - return cb(null, u); - } - }); -} - -function initAndGetUser(userName, cb) { - async.waterfall( - [ - function init(callback) { - initConfigAndDatabases(callback); - }, - function getUserObject(callback) { - getUser(argv.user, (err, user) => { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - return callback(err); - } - return callback(null, user); - }); - } - ], - (err, user) => { - return cb(err, user); - } - ); -} - -function setAccountStatus(userName, active) { - async.waterfall( - [ - function init(callback) { - initAndGetUser(argv.user, callback); - }, - function activateUser(user, callback) { - const AccountStatus = require('./core/user.js').User.AccountStatus; - user.persistProperty('account_status', active ? AccountStatus.active : AccountStatus.inactive, callback); - } - ], - err => { - if(err) { - console.error(err.message); - } else { - console.info('User ' + ((true === active) ? 'activated' : 'deactivated')); - } - } - ); -} - -function handleUserCommand() { - if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) { - return printUsageAndSetExitCode('User', ExitCodes.ERROR); - } - - if(_.isString(argv.password)) { - if(0 === argv.password.length) { - process.exitCode = ExitCodes.BAD_ARGS; - return console.error('Invalid password'); - } - - async.waterfall( - [ - function init(callback) { - initAndGetUser(argv.user, callback); - }, - function setNewPass(user, callback) { - user.setNewAuthCredentials(argv.password, function credsSet(err) { - if(err) { - process.exitCode = ExitCodes.ERROR; - callback(new Error('Failed setting password')); - } else { - callback(null); - } - }); - } - ], - function complete(err) { - if(err) { - console.error(err.message); - } else { - console.info('Password set'); - } - } - ); - } else if(argv.activate) { - setAccountStatus(argv.user, true); - } else if(argv.deactivate) { - setAccountStatus(argv.user, false); - } -} - -function getAnswers(questions, cb) { - inq.prompt(questions).then( answers => { - return cb(answers); - }); -} - -function getDefaultConfigPath() { - return resolvePath('~/.config/enigma-bbs/config.hjson'); -} - -const QUESTIONS = { - Intro : [ - { - name : 'createNewConfig', - message : 'Create a new configuration?', - type : 'confirm', - default : false, - }, - { - name : 'configPath', - message : 'Configuration path:', - default : argv.config ? argv.config : getDefaultConfigPath(), - when : answers => answers.createNewConfig - }, - ], - - OverwriteConfig : [ - { - name : 'overwriteConfig', - message : 'Config file exists. Overwrite?', - type : 'confirm', - default : false, - } - ], - - Basic : [ - { - name : 'boardName', - message : 'BBS name:', - default : 'New ENiGMA½ BBS', - }, - ], - - Misc : [ - { - name : 'loggingLevel', - message : 'Logging level:', - type : 'list', - choices : [ 'Error', 'Warn', 'Info', 'Debug', 'Trace' ], - default : 2, - filter : s => s.toLowerCase(), - }, - { - name : 'sevenZipExe', - message : '7-Zip executable:', - type : 'list', - choices : [ '7z', '7za', 'None' ] - } - ], - - MessageConfAndArea : [ - { - name : 'msgConfName', - message : 'First message conference:', - default : 'Local', - }, - { - name : 'msgConfDesc', - message : 'Conference description:', - default : 'Local Areas', - }, - { - name : 'msgAreaName', - message : 'First area in message conference:', - default : 'General', - }, - { - name : 'msgAreaDesc', - message : 'Area description:', - default : 'General chit-chat', - } - ] -}; - -function makeMsgConfAreaName(s) { - return s.toLowerCase().replace(/\s+/g, '_'); -} - -function askNewConfigQuestions(cb) { - - const ui = new inq.ui.BottomBar(); - - let configPath; - let config; - - async.waterfall( - [ - function intro(callback) { - getAnswers(QUESTIONS.Intro, answers => { - if(!answers.createNewConfig) { - return callback('exit'); - } - - // adjust for ~ and the like - configPath = resolvePath(answers.configPath); - - const configDir = paths.dirname(configPath); - mkdirsSync(configDir); - - // - // Check if the file exists and can be written to - // - fs.access(configPath, fs.F_OK | fs.W_OK, err => { - if(err) { - if('EACCES' === err.code) { - ui.log.write(`${configPath} cannot be written to`); - callback('exit'); - } else if('ENOENT' === err.code) { - callback(null, false); - } - } else { - callback(null, true); // exists + writable - } - }); - }); - }, - function promptOverwrite(needPrompt, callback) { - if(needPrompt) { - getAnswers(QUESTIONS.OverwriteConfig, answers => { - callback(answers.overwriteConfig ? null : 'exit'); - }); - } else { - callback(null); - } - }, - function basic(callback) { - getAnswers(QUESTIONS.Basic, answers => { - config = { - general : { - boardName : answers.boardName, - }, - }; - - callback(null); - }); - }, - function msgConfAndArea(callback) { - getAnswers(QUESTIONS.MessageConfAndArea, answers => { - config.messageConferences = {}; - - const confName = makeMsgConfAreaName(answers.msgConfName); - const areaName = makeMsgConfAreaName(answers.msgAreaName); - - config.messageConferences[confName] = { - name : answers.msgConfName, - desc : answers.msgConfDesc, - sort : 1, - default : true, - }; - - config.messageConferences.another_sample_conf = { - name : 'Another Sample Conference', - desc : 'Another conference example. Change me!', - sort : 2, - }; - - config.messageConferences[confName].areas = {}; - config.messageConferences[confName].areas[areaName] = { - name : answers.msgAreaName, - desc : answers.msgAreaDesc, - sort : 1, - default : true, - }; - - config.messageConferences.another_sample_conf = { - name : 'Another Sample Conference', - desc : 'Another conf sample. Change me!', - - areas : { - another_sample_area : { - name : 'Another Sample Area', - desc : 'Another area example. Change me!', - sort : 2 - } - } - }; - - callback(null); - }); - }, - function misc(callback) { - getAnswers(QUESTIONS.Misc, answers => { - if('None' !== answers.sevenZipExe) { - config.archivers = { - zip : { - compressCmd : answers.sevenZipExe, - decompressCmd : answers.sevenZipExe, - } - }; - } - - config.logging = { - level : answers.loggingLevel, - }; - - callback(null); - }); - } - ], - err => { - cb(err, configPath, config); - } - ); -} - -function handleConfigCommand() { - if(true === argv.help) { - return printUsageAndSetExitCode('Config', ExitCodes.ERROR); - } - - if(argv.new) { - askNewConfigQuestions( (err, configPath, config) => { - if(err) { - return; - } - - config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t' } ); - - try { - fs.writeFileSync(configPath, config, 'utf8'); - console.info('Configuration generated'); - } catch(e) { - console.error('Exception attempting to create config: ' + e.toString()); - } - }); - } else { - return printUsageAndSetExitCode('Config', ExitCodes.ERROR); - } -} - -function scanFileBaseArea(areaTag, options, iterator, cb) { - async.waterfall( - [ - function getFileArea(callback) { - const fileAreaMod = require('./core/file_base_area.js'); - - const areaInfo = fileAreaMod.getFileAreaByTag(areaTag); - if(!areaInfo) { - return callback(new Error(`Invalid file base area tag: ${areaTag}`)); - } - - return callback(null, fileAreaMod, areaInfo); - }, - function performScan(fileAreaMod, areaInfo, callback) { - fileAreaMod.scanFileAreaForChanges(areaInfo, options, iterator, err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); -} - -function fileAreaScan() { - const options = {}; - - const tags = argv.tags; - if(tags) { - options.tags = tags.split(','); - } - - const areaTags = argv._.slice(2); - - function scanFileIterator(stepInfo, nextScanStep) { - if('start' === stepInfo.step) { - console.info(`Scanning ${stepInfo.filePath}...`); - } - return nextScanStep(null); - // :TODO: add 'finished' step when avail - } - - async.series( - [ - function init(callback) { - return initConfigAndDatabases(callback); - }, - function scanAreas(callback) { - async.eachSeries(areaTags, (areaTag, nextAreaTag) => { - scanFileBaseArea(areaTag, options, scanFileIterator, err => { - return nextAreaTag(err); - }); - }, err => { - return callback(err); - }); - } - ], - err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } - } - ); -} - -function handleFileBaseCommand() { - if(true === argv.help) { - return printUsageAndSetExitCode('FileBase', ExitCodes.ERROR); - } - - const action = argv._[1]; - - switch(action) { - case 'scan' : return fileAreaScan(); - } -} - -function main() { - - process.exitCode = ExitCodes.SUCCESS; - - if(true === argv.version) { - return console.info(require('./package.json').version); - } - - if(0 === argv._.length || - 'help' === argv._[0]) - { - printUsageAndSetExitCode('General', ExitCodes.SUCCESS); - } - - switch(argv._[0]) { - case 'user' : - handleUserCommand(); - break; - - case 'config' : - handleConfigCommand(); - break; - - case 'file-base' : - case 'fb' : - handleFileBaseCommand(); - break; - - default: - printUsageAndSetExitCode('', ExitCodes.BAD_COMMAND); - } -} - -main(); \ No newline at end of file +require('./core/oputil/oputil_main.js')(); From bced59da09180d207db975f4e8640426e98becf2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 15 Feb 2017 21:40:22 -0700 Subject: [PATCH 86/86] MUCH faster crc32 impl --- core/crc.js | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/core/crc.js b/core/crc.js index 869d6693..886dad1d 100644 --- a/core/crc.js +++ b/core/crc.js @@ -1,19 +1,51 @@ /* jslint node: true */ 'use strict'; -const CRC32_TABLE = - '00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D'.split(' ').map(s => parseInt(s, 16)); +const CRC32_TABLE = new Int32Array( + '00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D'.split(' ').map(s => parseInt(s, 16))); exports.CRC32 = class CRC32 { constructor() { this.crc = -1; } - + update(input) { - input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); - input.forEach(c => { - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ c) & 0xff]; - }); + input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); + return input.length > 10240 ? this.update_8(input) : this.update_4(input); + } + + update_4(input) { + const len = input.length - 3; + let i = 0; + + for(i = 0; i < len;) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + } + while(i < len + 3) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; + } + } + + update_8(input) { + const len = input.length - 7; + let i = 0; + + for(i = 0; i < len;) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + } + while(i < len + 7) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; + } } finalize() {