diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index ba08c290..a073465a 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -176,7 +176,7 @@ function ANSIEscapeParser(options) { id : id ? parseInt(id, 10) : null, args : args, SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) - }); + }); if(self.mciReplaceChar.length > 0) { self.emit('chunk', ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase)); diff --git a/core/fse.js b/core/fse.js index f6aa7e7c..e55a7e99 100644 --- a/core/fse.js +++ b/core/fse.js @@ -665,13 +665,12 @@ function FullScreenEditorModule(options) { // We want to prefix the subject with "RE: " only if it's not already // that way -- avoid RE: RE: RE: RE: ... // - var newSubj = self.replyToMessage.subject; - if(!_.startsWith(self.replyToMessage.subject, 'RE:')) { - newSubj = 'RE: ' + newSubj; + let newSubj = self.replyToMessage.subject; + if(false === /^RE:\s+/i.test(newSubj)) { + newSubj = `RE: ${newSubj}`; } self.setHeaderText(MCICodeIds.ReplyEditModeHeader.Subject, newSubj); - }; this.initFooterViewMode = function() { diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index c602593e..0ef88a9e 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -473,6 +473,7 @@ function Packet(options) { try { decoded = iconv.decode(messageBodyBuffer, encoding); } catch(e) { + // :TODO: add log warning here including failure reason decoded = iconv.decode(messageBodyBuffer, 'ascii'); } //const messageLines = iconv.decode(messageBodyBuffer, encoding).replace(/\xec/g, '').split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); @@ -603,7 +604,15 @@ function Packet(options) { if(self.options.keepTearAndOrigin) { msg.message += `${messageBodyData.originLine}\r\n`; } - } + } + + // + // If we have a UTC offset kludge (e.g. TZUTC) then update + // modDateTime with it + // + if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { + msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); + } const nextBuf = packetBuffer.slice(read); if(nextBuf.length > 0) { diff --git a/core/menu_util.js b/core/menu_util.js index a00edb0b..77ea42f7 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -97,7 +97,7 @@ function loadMenu(options, cb) { menuName : options.name, menuConfig : modData.config, extraArgs : options.extraArgs, - client : options.client, + client : options.client, }); callback(null, moduleInstance); } catch(e) { diff --git a/core/message.js b/core/message.js index a9f74b0f..9323b790 100644 --- a/core/message.js +++ b/core/message.js @@ -68,13 +68,6 @@ Message.WellKnownAreaTags = { Bulletin : 'local_bulletin', }; -// :TODO: FTN stuff really doesn't belong here - move it elsewhere and/or just use the names directly when needed -Message.MetaCategories = { - System : 1, // ENiGMA1/2 stuff - FtnProperty : 2, // Various FTN network properties, ftn_cost, ftn_origin, ... - FtnKludge : 3, // FTN kludges -- PATH, MSGID, ... -}; - Message.SystemMetaNames = { LocalToUserID : 'local_to_user_id', LocalFromUserID : 'local_from_user_id', diff --git a/core/message_area.js b/core/message_area.js index e2e73f48..87d4db3d 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -32,23 +32,23 @@ const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]'; const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]'; const AREA_ACS_DEFAULT = { - read : CONF_AREA_RW_ACS_DEFAULT, - write : CONF_AREA_RW_ACS_DEFAULT, - manage : AREA_MANAGE_ACS_DEFAULT, + read : CONF_AREA_RW_ACS_DEFAULT, + write : CONF_AREA_RW_ACS_DEFAULT, + manage : AREA_MANAGE_ACS_DEFAULT, }; function getAvailableMessageConferences(client, options) { - options = options || { includeSystemInternal : false }; + options = options || { includeSystemInternal : false }; // perform ACS check per conf & omit system_internal if desired - return _.omit(Config.messageConferences, (v, k) => { - if(!options.includeSystemInternal && 'system_internal' === k) { - return true; - } - - const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT; - return !checkAcs(client, readAcs); - }); + return _.omit(Config.messageConferences, (v, k) => { + if(!options.includeSystemInternal && 'system_internal' === k) { + return true; + } + + const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT; + return !checkAcs(client, readAcs); + }); } function getSortedAvailMessageConferences(client, options) { @@ -75,19 +75,19 @@ function getAvailableMessageAreasByConfTag(confTag, options) { // :TODO: confTag === "" then find default if(_.has(Config.messageConferences, [ confTag, 'areas' ])) { - const areas = Config.messageConferences[confTag].areas; + const areas = Config.messageConferences[confTag].areas; - if(!options.client || true === options.noAcsCheck) { - // everything - no ACS checks - return areas; - } else { - // perform ACS check per area - return _.omit(areas, (v, k) => { - const readAcs = _.has(v, 'acs.read') ? v.acs.read : CONF_AREA_RW_ACS_DEFAULT; - return !checkAcs(options.client, readAcs); - }); - } - } + if(!options.client || true === options.noAcsCheck) { + // everything - no ACS checks + return areas; + } else { + // perform ACS check per area + return _.omit(areas, v => { + const readAcs = _.has(v, 'acs.read') ? v.acs.read : CONF_AREA_RW_ACS_DEFAULT; + return !checkAcs(options.client, readAcs); + }); + } + } } function getSortedAvailMessageAreasByConfTag(confTag, options) { @@ -95,7 +95,7 @@ function getSortedAvailMessageAreasByConfTag(confTag, options) { return { areaTag : k, area : v, - } + }; }); areas.sort((a, b) => { @@ -322,16 +322,16 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { }); }, function getMessages(lastMessageId, callback) { - var sql = - 'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' + - 'FROM message ' + - 'WHERE area_tag ="' + areaTag + '" AND message_id > ' + lastMessageId; + let sql = + `SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count + FROM message + WHERE area_tag = "${areaTag}" AND message_id > ${lastMessageId}`; if(Message.WellKnownAreaTags.Private === areaTag) { sql += - ' AND message_id in (' + - 'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System + - ' AND meta_name="' + Message.SystemMetaNames.LocalToUserID + '" and meta_value=' + userId + ')'; + ` AND message_id in ( + SELECT message_id from message_meta where meta_category ="System" + AND meta_name ="${Message.SystemMetaNames.LocalToUserID}" AND meta_value =${userId})`; } sql += ' ORDER BY message_id;'; diff --git a/core/module_util.js b/core/module_util.js index 931f8194..6fa23598 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -2,15 +2,14 @@ 'use strict'; // ENiGMA½ -let Config = require('./config.js').config; -let miscUtil = require('./misc_util.js'); +const Config = require('./config.js').config; -// standard/deps -let fs = require('fs'); -let paths = require('path'); -let _ = require('lodash'); -let assert = require('assert'); -let async = require('async'); +// deps +const fs = require('fs'); +const paths = require('path'); +const _ = require('lodash'); +const assert = require('assert'); +const async = require('async'); // exports exports.loadModuleEx = loadModuleEx; @@ -25,25 +24,22 @@ function loadModuleEx(options, cb) { const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; if(_.isObject(modConfig) && false === modConfig.enabled) { - cb(new Error('Module "' + options.name + '" is disabled')); - return; + return cb(new Error('Module "' + options.name + '" is disabled')); } - var mod; - try { + let mod; + try { mod = require(paths.join(options.path, options.name + '.js')); } catch(e) { - cb(e); + return cb(e); } if(!_.isObject(mod.moduleInfo)) { - cb(new Error('Module is missing "moduleInfo" section')); - return; + return cb(new Error('Module is missing "moduleInfo" section')); } if(!_.isFunction(mod.getModule)) { - cb(new Error('Invalid or missing "getModule" method for module!')); - return; + return cb(new Error('Invalid or missing "getModule" method for module!')); } // Ref configuration, if any, for convience to the module @@ -53,11 +49,10 @@ function loadModuleEx(options, cb) { } function loadModule(name, category, cb) { - var path = Config.paths[category]; + const path = Config.paths[category]; if(!_.isString(path)) { - cb(new Error('Not sure where to look for "' + name + '" of category "' + category + '"')); - return; + return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`)); } loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { diff --git a/core/new_scan.js b/core/new_scan.js index 2b1f8a1f..b4879316 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -2,13 +2,13 @@ 'use strict'; // ENiGMA½ -var msgArea = require('./message_area.js'); -var Message = require('./message.js'); -var MenuModule = require('./menu_module.js').MenuModule; -var ViewController = require('../core/view_controller.js').ViewController; +const msgArea = require('./message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('../core/view_controller.js').ViewController; -var _ = require('lodash'); -var async = require('async'); +// deps +const _ = require('lodash'); +const async = require('async'); exports.moduleInfo = { name : 'New Scan', @@ -60,65 +60,74 @@ function NewScanModule(options) { } }; - this.newScanMessageConference = function(cb) { + this.newScanMessageConference = function(cb) { // lazy init - if(!self.sortedMessageConfs) { - const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. + if(!self.sortedMessageConfs) { + const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => { - return { - confTag : k, - conf : v, - }; - }); + return { + confTag : k, + conf : v, + }; + }); // // Sort conferences by name, other than 'system_internal' which should // always come first such that we display private mails/etc. before // other conferences & areas // - self.sortedMessageConfs.sort((a, b) => { + self.sortedMessageConfs.sort((a, b) => { if('system_internal' === a.confTag) { - return -1; - } else { - return a.conf.name.localeCompare(b.conf.name); - } - }); + return -1; + } else { + return a.conf.name.localeCompare(b.conf.name); + } + }); - self.currentScanAux.conf = self.currentScanAux.conf || 0; - self.currentScanAux.area = self.currentScanAux.area || 0; - } + self.currentScanAux.conf = self.currentScanAux.conf || 0; + self.currentScanAux.area = self.currentScanAux.area || 0; + } - const currentConf = self.sortedMessageConfs[self.currentScanAux.conf]; + const currentConf = self.sortedMessageConfs[self.currentScanAux.conf]; - async.series( - [ - function scanArea(callback) { - //self.currentScanAux.area = self.currentScanAux.area || 0; - - self.newScanMessageArea(currentConf, function areaScanComplete(err) { - if(self.sortedMessageConfs.length > self.currentScanAux.conf + 1) { - self.currentScanAux.conf += 1; - self.currentScanAux.area = 0; - - self.newScanMessageConference(cb); // recursive to next conf - //callback(null); - } else { - self.updateScanStatus(self.scanCompleteMsg); - callback(new Error('No more conferences')); - } - }); - } - ], - cb + async.series( + [ + function scanArea(callback) { + //self.currentScanAux.area = self.currentScanAux.area || 0; + + self.newScanMessageArea(currentConf, () => { + if(self.sortedMessageConfs.length > self.currentScanAux.conf + 1) { + self.currentScanAux.conf += 1; + self.currentScanAux.area = 0; + + self.newScanMessageConference(cb); // recursive to next conf + //callback(null); + } else { + self.updateScanStatus(self.scanCompleteMsg); + callback(new Error('No more conferences')); + } + }); + } + ], + cb ); - }; - + }; + this.newScanMessageArea = function(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 sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : self.client } ); const currentArea = sortedAreas[self.currentScanAux.area]; + function getFormatObj() { + return { + confName : conf.conf.name, + confDesc : conf.conf.desc, + areaName : currentArea.area.name, + areaDesc : currentArea.area.desc + }; + } + // // Scan and update index until we find something. If results are found, // we'll goto the list module & show them. @@ -136,12 +145,7 @@ function NewScanModule(options) { } }, function updateStatusScanStarted(callback) { - self.updateScanStatus(self.scanStartFmt.format({ - confName : conf.conf.name, - confDesc : conf.conf.desc, - areaName : currentArea.area.name, - areaDesc : currentArea.area.desc, - })); + self.updateScanStatus(self.scanStartFmt.format(getFormatObj())); callback(null); }, function newScanAreaAndGetMessages(callback) { @@ -149,26 +153,17 @@ function NewScanModule(options) { self.client.user.userId, currentArea.areaTag, function msgs(err, msgList) { if(!err) { if(0 === msgList.length) { - self.updateScanStatus(self.scanFinishNoneFmt.format({ - confName : conf.conf.name, - confDesc : conf.conf.desc, - areaName : currentArea.area.name, - areaDesc : currentArea.area.desc, - })); + self.updateScanStatus(self.scanFinishNoneFmt.format(getFormatObj())); } else { - self.updateScanStatus(self.scanFinishNewFmt.format({ - confName : conf.conf.name, - confDesc : conf.conf.desc, - areaName : currentArea.area.name, - count : msgList.length, - })); + const formatObj = Object.assign(getFormatObj(), { count : msgList.length } ); + self.updateScanStatus(self.scanFinishNewFmt.format(formatObj)); } } callback(err, msgList); } ); }, - function displayMessageList(msgList, callback) { + function displayMessageList(msgList) { if(msgList && msgList.length > 0) { var nextModuleOpts = { extraArgs: { @@ -183,7 +178,7 @@ function NewScanModule(options) { } } ], - cb + cb // no more areas ); }; @@ -216,7 +211,7 @@ NewScanModule.prototype.mciReady = function(mciData, cb) { NewScanModule.super_.prototype.mciReady.call(self, mciData, callback); }, function loadFromConfig(callback) { - var loadOpts = { + const loadOpts = { callingMenu : self, mciMap : mciData.menu, noInput : true, @@ -227,13 +222,12 @@ NewScanModule.prototype.mciReady = function(mciData, cb) { function performCurrentStepScan(callback) { switch(self.currentStep) { case 'messageConferences' : - self.newScanMessageConference(function scanComplete(err) { - callback(null); // finished - }); - break; + self.newScanMessageConference( () => { + callback(null); // finished + }); + break; - default : - callback(null); + default : return callback(null); } } ], diff --git a/oputil.js b/oputil.js index 5087b9b1..d1af5e9a 100755 --- a/oputil.js +++ b/oputil.js @@ -4,12 +4,19 @@ 'use strict'; // ENiGMA½ -var config = require('./core/config.js'); -var db = require('./core/database.js'); +const config = require('./core/config.js'); +const db = require('./core/database.js'); +const resolvePath = require('./core/misc_util.js').resolvePath; -var _ = require('lodash'); -var async = require('async'); -var assert = require('assert'); +// deps +const _ = require('lodash'); +const async = require('async'); +const assert = require('assert'); +const inq = require('inquirer'); +const mkdirsSync = require('fs-extra').mkdirsSync; +const fs = require('fs'); +const hjson = require('hjson'); +const paths = require('path'); var argv = require('minimist')(process.argv.slice(2)); @@ -116,6 +123,234 @@ function handleUserCommand() { } } +function getAnswers(questions, cb) { + inq.prompt(questions, cb); +} + +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 : 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 askQuestions(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 = { + 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() { + askQuestions( (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()); + } + }); + +} + function main() { process.exitCode = ExitCodes.SUCCESS; @@ -135,6 +370,10 @@ function main() { case 'user' : handleUserCommand(); break; + + case 'config' : + handleConfigCommand(); + break; default: printUsage('');