From 3f942871ae7ec9533093e2d4a1c14b213af608c4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 7 Sep 2017 21:21:24 -0600 Subject: [PATCH 0001/1013] * Update packages * Remove application/x-arj from mimeUtils hack - is included in mime-db now. --- core/mime_util.js | 1 - package.json | 32 ++++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/core/mime_util.js b/core/mime_util.js index bdb88a53..1e2bd32c 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -14,7 +14,6 @@ function startup(cb) { // Add in types (not yet) supported by mime-db -- and therefor, mime-types // const ADDITIONAL_EXT_MIMETYPES = { - arj : 'application/x-arj', ans : 'text/x-ansi', gz : 'application/gzip', // not in mime-types 2.1.15 :( }; diff --git a/package.json b/package.json index ec508841..219a4290 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.7-alpha", + "version": "0.0.8-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -19,34 +19,34 @@ "retro" ], "dependencies": { - "async": "^2.4.0", + "async": "^2.5.0", "binary": "0.3.x", "buffers": "NuSkooler/node-buffers", - "bunyan": "^1.8.10", - "farmhash": "^1.2.1", - "fs-extra": "^3.0.1", + "bunyan": "^1.8.12", + "exiftool": "^0.0.3", + "farmhash": "^2.0.1", + "fs-extra": "^4.0.1", "gaze": "^1.1.2", + "graceful-fs": "^4.1.11", "hashids": "^1.1.1", - "hjson": "^2.4.2", - "iconv-lite": "^0.4.17", - "inquirer": "^3.0.6", + "hjson": "^3.1.0", + "iconv-lite": "^0.4.18", + "inquirer": "^3.2.3", "later": "1.2.0", "lodash": "^4.17.4", - "mime-types": "^2.1.15", + "mime-types": "^2.1.17", "minimist": "1.2.x", "moment": "^2.18.1", - "nodemailer": "^4.0.1", + "node-glob": "^1.2.0", + "nodemailer": "^4.1.0", "ptyw.js": "NuSkooler/ptyw.js", "sanitize-filename": "^1.6.1", - "sqlite3": "^3.1.1", + "sqlite3": "^3.1.9", "ssh2": "^0.5.5", "temptmp": "^1.0.0", - "uuid": "^3.0.1", + "uuid": "^3.1.0", "uuid-parse": "^1.0.0", - "ws" : "^3.0.0", - "graceful-fs" : "^4.1.11", - "exiftool" : "^0.0.3", - "node-glob" : "^1.2.0" + "ws": "^3.1.0" }, "devDependencies": {}, "engines": { From 79e410315c83afbe6dff3199d9c4d630886e0b6f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Sep 2017 23:07:11 -0600 Subject: [PATCH 0002/1013] Remove a extra line when quoting --- core/fse.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/fse.js b/core/fse.js index ed9d3b13..2ccc6951 100644 --- a/core/fse.js +++ b/core/fse.js @@ -995,14 +995,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const quoteMsgView = this.viewControllers.quoteBuilder.getView(1); const msgView = this.viewControllers.body.getView(1); - let quoteLines = quoteMsgView.getData(); + let quoteLines = quoteMsgView.getData().trim(); - if(quoteLines.trim().length > 0) { + if(quoteLines.length > 0) { if(this.replyIsAnsi) { const bodyMessageView = this.viewControllers.body.getView(1); quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`; } - msgView.addText(`${quoteLines}\n`); + msgView.addText(`${quoteLines}\n\n`); } quoteMsgView.setText(''); From 18461e594adf4bd1de80d538daa41df4c9947eb1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Sep 2017 23:11:01 -0600 Subject: [PATCH 0003/1013] Add --update option to fb scan --- core/oputil/oputil_file_base.js | 85 +++++++++++++++++++++++++-------- core/oputil/oputil_help.js | 1 + 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 5cbe10e3..3f791267 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -34,7 +34,7 @@ exports.handleFileBaseCommand = handleFileBaseCommand; let fileArea; // required during init -function finalizeEntryAndPersist(fileEntry, descHandler, cb) { +function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { async.series( [ function getDescFromHandlerIfNeeded(callback) { @@ -53,18 +53,24 @@ function finalizeEntryAndPersist(fileEntry, descHandler, cb) { return callback(null); }, function getDescFromUserIfNeeded(callback) { - if(false === argv.prompt || ( fileEntry.desc && fileEntry.desc.length > 0 ) ) { + if(fileEntry.desc && fileEntry.desc.length > 0 ) { return callback(null); } - const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; + const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; + const descFromFile = getDescFromFileName(fileEntry.fileName); + + if(false === argv.prompt) { + fileEntry.desc = descFromFile; + return callback(null); + } const questions = [ { name : 'desc', message : `Description for ${fileEntry.fileName}:`, type : 'input', - default : getDescFromFileName(fileEntry.fileName), + default : descFromFile, } ]; @@ -74,7 +80,7 @@ function finalizeEntryAndPersist(fileEntry, descHandler, cb) { }); }, function persist(callback) { - fileEntry.persist( err => { + fileEntry.persist(isUpdate, err => { return callback(err); }); } @@ -104,6 +110,12 @@ function scanFileAreaForChanges(areaInfo, options, cb) { return !asi.storageTag || sl.storageTag === asi.storageTag; }); }); + + function updateTags(fe) { + if(Array.isArray(options.tags)) { + fe.hashTags = new Set(options.tags); + } + } async.eachSeries(storageLocations, (storageLoc, nextLocation) => { async.waterfall( @@ -153,27 +165,58 @@ function scanFileAreaForChanges(areaInfo, options, cb) { }, (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??? + // + // We'll update the entry if the following conditions are met: + // * We have a single duplicate, and: + // * --update-desc was passed or the existing entry's desc or + // longDesc are blank/empty + // + if(argv['update'] && 1 === dupeEntries.length) { + const FileEntry = require('../../core/file_entry.js'); + const existingEntry = new FileEntry(); + + return existingEntry.load(dupeEntries[0].fileId, err => { + if(err) { + console.info('Dupe (cannot update)'); + return nextFile(null); + } + + // + // Update only if tags or desc changed + // + const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; + const tagsEq = _.isEqual(optTags, existingEntry.hashTags); + + if( tagsEq && + fileEntry.desc === existingEntry.desc && + fileEntry.descLong == existingEntry.descLong) + { + console.info('Dupe'); + return nextFile(null); + } + + console.info('Dupe (updating)'); + updateTags(existingEntry); + + finalizeEntryAndPersist(true, existingEntry, descHandler, err => { + return nextFile(err); + }); + }); + } else if(dupeEntries.length > 0) { console.info('Dupe'); return nextFile(null); - } else { - console.info('Done!'); - if(Array.isArray(options.tags)) { - options.tags.forEach(tag => { - fileEntry.hashTags.add(tag); - }); - } - - finalizeEntryAndPersist(fileEntry, descHandler, err => { - return nextFile(err); - }); } + + console.info('Done!'); + updateTags(fileEntry); + + finalizeEntryAndPersist(false, fileEntry, descHandler, err => { + return nextFile(err); + }); } ); }); @@ -518,7 +561,7 @@ function moveFiles() { function removeFiles() { // - // REMOVE SHA|FILE_ID [SHA|FILE_ID ...] + // REMOVE FILENAME_WC|SHA|FILE_ID [SHA|FILE_ID ...] } function handleFileBaseCommand() { diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 5c20e3a5..c49200b0 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -65,6 +65,7 @@ scan args: other sources such as FILE_ID.DIZ. if PATH is specified, use DESCRIPT.ION at PATH instead of looking in specific storage locations + --update attempt to update information for existing entries info args: --show-desc display short description, if any From 1e2729186904620f9a65430c2db1bcb3e300492f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Sep 2017 23:27:28 -0600 Subject: [PATCH 0004/1013] Fix typo --- 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 bac85de7..aa60974f 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -284,7 +284,7 @@ class FileAreaWebAccess { resp.on('finish', () => { // transfer completed fully - this.updateDownloadStatsForUserIdAndSystemAndSystem(servedItem.userId, stats.size); + this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size); }); const headers = { From 42d61908024a1708360b3c56f13afbd8ab2fdcb4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Sep 2017 23:36:26 -0600 Subject: [PATCH 0005/1013] Additional logging --- core/servers/content/web.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 31c617e2..55fd37f2 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -148,7 +148,7 @@ exports.getModule = class WebServerModule extends ServerModule { const routeKey = route.getRouteKey(); if(routeKey in this.routes) { - Log.warn( { route : route }, 'Cannot add route: duplicate method/path combination exists' ); + Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' ); return false; } From 1c2926fbe91eeca49e40b7aedb794b8199bcff88 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Sep 2017 11:48:27 -0600 Subject: [PATCH 0006/1013] Roll farmhash version back - has bugs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 219a4290..f8393142 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "farmhash": "^2.0.1", + "farmhash": "1.2.1", "fs-extra": "^4.0.1", "gaze": "^1.1.2", "graceful-fs": "^4.1.11", From 3980c8acae3e1bbb6f0ee08460357fa0dda7e691 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Sep 2017 11:48:43 -0600 Subject: [PATCH 0007/1013] Add new file base system stats and MCI codes --- core/bbs.js | 11 +++++++ core/config.js | 5 ++++ core/file_base_area.js | 65 ++++++++++++++++++++++++++++++++++++++++++ core/predefined_mci.js | 10 ++++++- core/string_util.js | 4 +-- 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index c43d63a3..02058bce 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -234,6 +234,17 @@ function initialize(cb) { } ); }, + function initFileAreaStats(callback) { + const getAreaStats = require('./file_base_area.js').getAreaStats; + getAreaStats( (err, stats) => { + if(!err) { + const StatLog = require('./stat_log.js'); + StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + } + + return callback(null); + }); + }, function initMCI(callback) { return require('./predefined_mci.js').init(callback); }, diff --git a/core/config.js b/core/config.js index 449a5c98..85632000 100644 --- a/core/config.js +++ b/core/config.js @@ -713,6 +713,11 @@ function getDefaultConfig() { action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', }, + updateFileAreaStats : { + schedule : 'every 1 hours', + action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + }, + forgotPasswordMaintenance : { schedule : 'every 24 hours', action : '@method:core/web_password_reset.js:performMaintenanceTask', diff --git a/core/file_base_area.js b/core/file_base_area.js index 79b91ce8..93587aa7 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -13,6 +13,7 @@ const Log = require('./logger.js').log; const resolveMimeType = require('./mime_util.js').resolveMimeType; const stringFormat = require('./string_format.js'); const wordWrapText = require('./word_wrap.js').wordWrapText; +const StatLog = require('./stat_log.js'); // deps const _ = require('lodash'); @@ -39,6 +40,10 @@ exports.changeFileAreaWithOptions = changeFileAreaWithOptions; exports.scanFile = scanFile; exports.scanFileAreaForChanges = scanFileAreaForChanges; exports.getDescFromFileName = getDescFromFileName; +exports.getAreaStats = getAreaStats; + +// for scheduler: +exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; const WellKnownAreaTags = exports.WellKnownAreaTags = { Invalid : '', @@ -856,4 +861,64 @@ function getDescFromFileName(fileName) { const name = paths.basename(fileName, ext); return _.upperFirst(name.replace(/[\-_.+]/g, ' ').replace(/\s+/g, ' ')); +} + +// +// Return an object of stats about an area(s) +// +// { +// +// totalFiles : , +// totalBytes : , +// areas : { +// : { +// files : , +// bytes : +// } +// } +// } +// +function getAreaStats(cb) { + FileDb.all( + `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size + FROM file f, file_meta m + WHERE f.file_id = m.file_id AND m.meta_name='byte_size' + GROUP BY f.area_tag;`, + (err, statRows) => { + if(err) { + return cb(err); + } + + if(!statRows || 0 === statRows.length) { + return cb(Errors.DoesNotExist('No file areas to acquire stats from')); + } + + return cb( + null, + statRows.reduce( (stats, v) => { + stats.totalFiles = (stats.totalFiles || 0) + v.total_files; + stats.totalBytes = (stats.totalBytes || 0) + v.total_byte_size; + + stats.areas = stats.areas || {}; + + stats.areas[v.area_tag] = { + files : v.total_files, + bytes : v.total_byte_size, + }; + return stats; + }, {}) + ); + } + ); +} + +// method exposed for event scheduler +function updateAreaStatsScheduledEvent(args, cb) { + getAreaStats( (err, stats) => { + if(!err) { + StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + } + + return cb(err); + }); } \ No newline at end of file diff --git a/core/predefined_mci.js b/core/predefined_mci.js index afebd24c..ac8cffe7 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -201,12 +201,20 @@ const PREDEFINED_MCI_GENERATORS = { const byteSize = StatLog.getSystemStatNum('ul_total_bytes'); return formatByteSize(byteSize, true); // true=withAbbr }, + TF : function totalFilesOnSystem() { + const areaStats = StatLog.getSystemStat('file_base_area_stats'); + return _.get(areaStats, 'totalFiles', 0).toString(); + }, + TB : function totalBytesOnSystem() { + const areaStats = StatLog.getSystemStat('file_base_area_stats'); + const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); + return formatByteSize(totalBytes, true); // true=withAbbr + }, // :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) diff --git a/core/string_util.js b/core/string_util.js index ed6df231..d6f5e08a 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -326,9 +326,7 @@ 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; +function formatByteSize(byteSize, withAbbr = false, decimals = 2) { 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) { From 9cc14b570894f4a0aa333b6e3ca27780eb5835f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Sep 2017 12:11:55 -0600 Subject: [PATCH 0008/1013] Use nicely formatted number values for MCI stats --- core/predefined_mci.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/predefined_mci.js b/core/predefined_mci.js index ac8cffe7..7fe921b3 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -45,11 +45,11 @@ function getUserRatio(client, propA, propB) { } function userStatAsString(client, statName, defaultValue) { - return (StatLog.getUserStat(client.user, statName) || defaultValue).toString(); + return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString(); } function sysStatAsString(statName, defaultValue) { - return (StatLog.getSystemStat(statName) || defaultValue).toString(); + return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString(); } const PREDEFINED_MCI_GENERATORS = { @@ -177,7 +177,7 @@ const PREDEFINED_MCI_GENERATORS = { AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, - TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); }, + TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, RR : function randomRumor() { // start the process of picking another random one @@ -203,7 +203,7 @@ const PREDEFINED_MCI_GENERATORS = { }, TF : function totalFilesOnSystem() { const areaStats = StatLog.getSystemStat('file_base_area_stats'); - return _.get(areaStats, 'totalFiles', 0).toString(); + return _.get(areaStats, 'totalFiles', 0).toLocaleString(); }, TB : function totalBytesOnSystem() { const areaStats = StatLog.getSystemStat('file_base_area_stats'); From d545c86616135142a33b7fd93fb01bc809cd079b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Sep 2017 12:34:31 -0600 Subject: [PATCH 0009/1013] Add {totalFiles} and {totalBytes} stats to area selection --- mods/file_base_area_select.js | 62 +++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/mods/file_base_area_select.js b/mods/file_base_area_select.js index 5eef583b..ca182d6c 100644 --- a/mods/file_base_area_select.js +++ b/mods/file_base_area_select.js @@ -3,14 +3,12 @@ // 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; const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas; +const StatLog = require('../core/stat_log.js'); // deps -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); exports.moduleInfo = { name : 'File Area Selector', @@ -60,25 +58,47 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { return cb(err); } - this.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => { - if(err) { + const self = this; + + async.series( + [ + function mergeAreaStats(callback) { + const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; + + self.availAreas.forEach(area => { + const stats = areaStats.areas[area.areaTag]; + area.totalFiles = stats ? stats.files : 0; + area.totalBytes = stats ? stats.bytes : 0; + }); + + return callback(null); + }, + function prepView(callback) { + self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => { + if(err) { + return callback(err); + } + + const areaListView = vc.getView(MciViewIds.areaList); + + const areaListFormat = self.config.areaListFormat || '{name}'; + + areaListView.setItems(self.availAreas.map(a => stringFormat(areaListFormat, a) ) ); + + if(self.config.areaListFocusFormat) { + areaListView.setFocusItems(self.availAreas.map(a => stringFormat(self.config.areaListFocusFormat, a) ) ); + } + + areaListView.redraw(); + + return callback(null); + }); + } + ], + err => { return cb(err); } - - const areaListView = vc.getView(MciViewIds.areaList); - - const areaListFormat = this.config.areaListFormat || '{name}'; - - areaListView.setItems(this.availAreas.map(a => stringFormat(areaListFormat, a) ) ); - - if(this.config.areaListFocusFormat) { - areaListView.setFocusItems(this.availAreas.map(a => stringFormat(this.config.areaListFocusFormat, a) ) ); - } - - areaListView.redraw(); - - return cb(null); - }); + ); }); } }; From 9d093905619c7cd6ec10255b943409399eb1b5d5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Sep 2017 13:59:23 -0600 Subject: [PATCH 0010/1013] Add countWithAbbr and countAbbr format specifiers --- core/string_format.js | 18 ++++++++++++------ core/string_util.js | 29 +++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/core/string_format.js b/core/string_format.js index dd1ece78..297d92b2 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -2,12 +2,15 @@ 'use strict'; const EnigError = require('./enig_error.js').EnigError; -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; + +const { + pad, + stylizeString, + renderStringLength, + renderSubstr, + formatByteSize, formatByteSizeAbbr, + formatCount, formatCountAbbr, +} = require('./string_util.js'); // deps const _ = require('lodash'); @@ -273,6 +276,9 @@ const transformers = { sizeWithAbbr : (n) => formatByteSize(n, true, 2), sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), sizeAbbr : (n) => formatByteSizeAbbr(n), + countWithAbbr : (n) => formatCount(n, true, 0), + countWithoutAbbr : (n) => formatCount(n, false, 0), + countAbbr : (n) => formatCountAbbr(n), }; function transformValue(transformerName, value) { diff --git a/core/string_util.js b/core/string_util.js index d6f5e08a..08987058 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -23,6 +23,8 @@ exports.renderSubstr = renderSubstr; exports.renderStringLength = renderStringLength; exports.formatByteSizeAbbr = formatByteSizeAbbr; exports.formatByteSize = formatByteSize; +exports.formatCountAbbr = formatCountAbbr; +exports.formatCount = formatCount; exports.cleanControlCodes = cleanControlCodes; exports.isAnsi = isAnsi; exports.isAnsiLine = isAnsiLine; @@ -316,21 +318,40 @@ function renderStringLength(s) { return len; } -const SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) +const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) function formatByteSizeAbbr(byteSize) { if(0 === byteSize) { - return SIZE_ABBRS[0]; // B + return BYTE_SIZE_ABBRS[0]; // B } - return SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; + return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } function formatByteSize(byteSize, withAbbr = false, decimals = 2) { 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]}`; + result += ` ${BYTE_SIZE_ABBRS[i]}`; + } + return result; +} + +const COUNT_ABBRS = [ '', 'K', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y' ]; + +function formatCountAbbr(count) { + if(count < 1000) { + return ''; + } + + return COUNT_ABBRS[Math.floor(Math.log(count) / Math.log(1000))]; +} + +function formatCount(count, withAbbr = false, decimals = 2) { + const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); + let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); + if(withAbbr) { + result += `${COUNT_ABBRS[i]}`; } return result; } From a91ae779be65ad30bea76edfd61aac63a22ac939 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 10 Sep 2017 20:51:30 -0600 Subject: [PATCH 0011/1013] Add skipAcsCheck option to getAvailableFileAReas() --- core/file_base_area.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 93587aa7..ed34607d 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -65,6 +65,10 @@ function getAvailableFileAreas(client, options) { return true; } + if(options.skipAcsCheck) { + return false; // no ACS checks (below) + } + if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) { return true; // omit } @@ -900,7 +904,7 @@ function getAreaStats(cb) { stats.totalBytes = (stats.totalBytes || 0) + v.total_byte_size; stats.areas = stats.areas || {}; - + stats.areas[v.area_tag] = { files : v.total_files, bytes : v.total_byte_size, From 861055d93502ebdd1429627bba7230ee25f9aad3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 10 Sep 2017 20:51:43 -0600 Subject: [PATCH 0012/1013] Add some new ASCII output options to AnsiPrep --- core/ansi_prep.js | 16 ++++++++++++---- core/file_entry.js | 4 ++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/core/ansi_prep.js b/core/ansi_prep.js index 29bd5ee4..45b93d32 100644 --- a/core/ansi_prep.js +++ b/core/ansi_prep.js @@ -23,6 +23,8 @@ module.exports = function ansiPrep(input, options, cb) { options.rows = options.rows || options.termHeight || 'auto'; options.startCol = options.startCol || 1; options.exportMode = options.exportMode || false; + options.fillLines = _.get(options, 'fillLines', true); + options.indent = options.indent || 0; // in auto we start out at 25 rows, but can always expand for more const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); @@ -111,15 +113,18 @@ module.exports = function ansiPrep(input, options, cb) { const lastCol = getLastPopulatedColumn(row) + 1; let i; - line = ''; + line = options.indent ? + output.length > 0 ? ' '.repeat(options.indent) : '' : + ''; + for(i = 0; i < lastCol; ++i) { const col = row[i]; - sgr = 0 === i ? + sgr = !options.asciiMode && 0 === i ? col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : ''; - if(col.sgr) { + if(!options.asciiMode && col.sgr) { sgr += ANSI.getSGRFromGraphicRendition(col.sgr); } @@ -129,7 +134,10 @@ module.exports = function ansiPrep(input, options, cb) { output += line; if(i < row.length) { - output += `${ANSI.blackBG()}${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; + output += `${options.asciiMode ? '' : ANSI.blackBG()}`; + if(options.fillLines) { + output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; + } } if(options.startCol + i < options.termWidth || options.forceLineTerm) { diff --git a/core/file_entry.js b/core/file_entry.js index b88d41a0..02912c35 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -520,6 +520,10 @@ module.exports = class FileEntry { sql += `${sqlWhere} ${sqlOrderBy};`; + if(_.isNumber(filter.limit)) { + sql += `LIMIT ${filter.limit}`; + } + const matchingFileIds = []; fileDb.each(sql, (err, fileId) => { if(fileId) { From 50bac9585723f461856b320056c141a2b870b8ea Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 11 Sep 2017 21:01:35 -0600 Subject: [PATCH 0013/1013] * Fix ANSI description display during upload * Major improvements to upload: Allow user to properly edit descriptions even if provided by .diz/system/etc. --- core/multi_line_edit_text_view.js | 28 +++++++++++++----- core/string_util.js | 4 +++ mods/upload.js | 48 ++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 9d46e8a1..55e84080 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -1068,9 +1068,9 @@ MultiLineEditTextView.prototype.setFocus = function(focused) { MultiLineEditTextView.super_.prototype.setFocus.call(this, focused); }; -MultiLineEditTextView.prototype.setText = function(text) { +MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode : 'default' } ) { this.textLines = [ ]; - this.addText(text); + this.addText(text, options); /*this.insertRawText(text); if(this.isEditMode()) { @@ -1085,13 +1085,27 @@ MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : f return this.setAnsiWithOptions(ansi, options, cb); }; -MultiLineEditTextView.prototype.addText = function(text) { +MultiLineEditTextView.prototype.addText = function(text, options = { scrollMode : 'default' }) { this.insertRawText(text); - if(this.isEditMode() || this.autoScroll) { - this.cursorEndOfDocument(); - } else { - this.cursorStartOfDocument(); + switch(options.scrollMode) { + case 'default' : + if(this.isEditMode() || this.autoScroll) { + this.cursorEndOfDocument(); + } else { + this.cursorStartOfDocument(); + } + break; + + case 'top' : + case 'start' : + this.cursorStartOfDocument(); + break; + + case 'end' : + case 'bottom' : + this.cursorEndOfDocument(); + break; } }; diff --git a/core/string_util.js b/core/string_util.js index 08987058..238aeeee 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -623,6 +623,10 @@ function isFormattedLine(line) { } function isAnsi(input) { + if(!input || 0 === input.length) { + return false; + } + // // * ANSI found - limited, just colors // * Full ANSI art diff --git a/mods/upload.js b/mods/upload.js index 5c5fd5b2..30c84c48 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -15,6 +15,7 @@ const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTe const Log = require('../core/logger.js').log; const Errors = require('../core/enig_error.js').Errors; const FileEntry = require('../core/file_entry.js'); +const isAnsi = require('../core/string_util.js').isAnsi; // deps const async = require('async'); @@ -421,9 +422,8 @@ exports.getModule = class UploadModule extends MenuModule { 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(!newEntry.descIsAnsi) { + newEntry.desc = _.trimEnd(newValues.shortDesc); } if(newValues.estYear.length > 0) { @@ -659,14 +659,16 @@ exports.getModule = class UploadModule extends MenuModule { displayFileDetailsPageForUploadEntry(fileEntry, cb) { const self = this; - async.series( + async.waterfall( [ function prepArtAndViewController(callback) { return self.prepViewControllerWithArt( 'fileDetails', FormIds.fileDetails, { clearScreen : true, trailingLF : false }, - callback + err => { + return callback(err); + } ); }, function populateViews(callback) { @@ -679,18 +681,32 @@ 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.setPropertyValue('mode', 'preview'); - descView.setText(fileEntry.desc); - descView.acceptsFocus = false; - self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.tags); - } else { - descView.setPropertyValue('mode', 'edit'); - descView.setText(getDescFromFileName(fileEntry.fileName)); // try to come up with something good as a default - descView.acceptsFocus = true; - self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.desc); - } + if(isAnsi(fileEntry.desc)) { + fileEntry.descIsAnsi = true; + return descView.setAnsi( + fileEntry.desc, + { + prepped : false, + forceLineTerm : true, + }, + () => { + return callback(null, descView, 'preview', MciViewIds.fileDetails.tags); + } + ); + } else { + const hasDesc = self.fileEntryHasDetectedDesc(fileEntry); + descView.setText( + hasDesc ? fileEntry.desc : getDescFromFileName(fileEntry.fileName), + { scrollMode : 'top' } // override scroll mode; we want to be @ top + ); + return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc); + } + }, + function finalizeViews(descView, descViewMode, focusId, callback) { + descView.setPropertyValue('mode', descViewMode); + descView.acceptsFocus = 'preview' === descViewMode ? false : true; + self.viewControllers.fileDetails.switchFocus(focusId); return callback(null); } ], From 68247d87e81a23efd800b728f4d1c5329fb3dfb3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 14 Sep 2017 20:54:35 -0600 Subject: [PATCH 0014/1013] Add filename order by option for search --- core/file_base_filter.js | 1 + mods/menu.hjson | 1 + 2 files changed, 2 insertions(+) diff --git a/core/file_base_filter.js b/core/file_base_filter.js index fadd41fd..a7185316 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -24,6 +24,7 @@ module.exports = class FileBaseFilters { 'user_rating', 'est_release_year', 'byte_size', + 'file_name', ]; } diff --git a/mods/menu.hjson b/mods/menu.hjson index d1822cfc..dbe99d85 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -2762,6 +2762,7 @@ "rating", "estimated year", "size", + "filename", ] argName: sortByIndex } From 1e250f06d9391c60a6ab7d06946c535acb5fedcb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Sep 2017 17:13:11 -0600 Subject: [PATCH 0015/1013] * Fix major issue with SQLite transactions + aync code causing collisions --- core/database.js | 12 +++++-- core/file_entry.js | 55 +++++++++++++++++-------------- core/message.js | 57 +++++++++++++++----------------- core/user.js | 82 ++++++++++++++++------------------------------ core/user_group.js | 13 +++++--- mods/bbs_list.js | 9 +++-- mods/onelinerz.js | 9 +++-- package.json | 1 + 8 files changed, 116 insertions(+), 122 deletions(-) diff --git a/core/database.js b/core/database.js index d4fb4795..a6286279 100644 --- a/core/database.js +++ b/core/database.js @@ -6,6 +6,7 @@ const conf = require('./config.js'); // deps const sqlite3 = require('sqlite3'); +const sqlite3Trans = require('sqlite3-transactions'); const paths = require('path'); const async = require('async'); const _ = require('lodash'); @@ -13,14 +14,19 @@ const assert = require('assert'); const moment = require('moment'); // database handles -let dbs = {}; +const dbs = {}; +exports.getTransactionDatabase = getTransactionDatabase; exports.getModDatabasePath = getModDatabasePath; exports.getISOTimestampString = getISOTimestampString; exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; +function getTransactionDatabase(db) { + return new sqlite3Trans.TransactionDatabase(db); +} + function getDatabasePath(name) { return paths.join(conf.config.paths.db, `${name}.sqlite3`); } @@ -55,7 +61,7 @@ function getISOTimestampString(ts) { function initializeDatabases(cb) { async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { - dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => { + dbs[dbName] = new sqlite3Trans.TransactionDatabase(new sqlite3.Database(getDatabasePath(dbName), err => { if(err) { return cb(err); } @@ -65,7 +71,7 @@ function initializeDatabases(cb) { return next(null); }); }); - }); + })); }, err => { return cb(err); }); diff --git a/core/file_entry.js b/core/file_entry.js index 02912c35..3a5b3df2 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -110,9 +110,8 @@ module.exports = class FileEntry { } const self = this; - let inTransaction = false; - async.series( + async.waterfall( [ function check(callback) { if(isUpdate && !self.fileId) { @@ -121,22 +120,20 @@ module.exports = class FileEntry { return callback(null); }, function startTrans(callback) { - return fileDb.run('BEGIN;', callback); + return fileDb.beginTransaction(callback); }, - function storeEntry(callback) { - inTransaction = true; - + function storeEntry(trans, callback) { if(isUpdate) { - fileDb.run( + trans.run( `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, [ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], err => { - return callback(err); + return callback(err, trans); } ); } else { - fileDb.run( + trans.run( `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) VALUES(?, ?, ?, ?, ?, ?, ?);`, [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], @@ -144,34 +141,34 @@ module.exports = class FileEntry { if(!err) { self.fileId = this.lastID; } - return callback(err); + return callback(err, trans); } ); } }, - function storeMeta(callback) { + function storeMeta(trans, callback) { async.each(Object.keys(self.meta), (n, next) => { const v = self.meta[n]; - return FileEntry.persistMetaValue(self.fileId, n, v, next); + return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); }, err => { - return callback(err); + return callback(err, trans); }); }, - function storeHashTags(callback) { + function storeHashTags(trans, callback) { const hashTagsArray = Array.from(self.hashTags); async.each(hashTagsArray, (hashTag, next) => { - return FileEntry.persistHashTag(self.fileId, hashTag, next); + return FileEntry.persistHashTag(self.fileId, hashTag, trans, next); }, err => { - return callback(err); + return callback(err, trans); }); } ], - err => { + (err, trans) => { // :TODO: Log orig err - if(inTransaction) { - fileDb.run(err ? 'ROLLBACK;' : 'COMMIT;', err => { + if(trans) { + trans[err ? 'rollback' : 'commit'](err => { return cb(err); }); } else { @@ -207,8 +204,13 @@ module.exports = class FileEntry { ); } - static persistMetaValue(fileId, name, value, cb) { - return fileDb.run( + static persistMetaValue(fileId, name, value, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = fileDb; + } + + return transOrDb.run( `REPLACE INTO file_meta (file_id, meta_name, meta_value) VALUES (?, ?, ?);`, [ fileId, name, value ], @@ -249,15 +251,20 @@ module.exports = class FileEntry { ); } - static persistHashTag(fileId, hashTag, cb) { - fileDb.serialize( () => { + static persistHashTag(fileId, hashTag, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = fileDb; + } + + transOrDb.serialize( () => { fileDb.run( `INSERT OR IGNORE INTO hash_tag (hash_tag) VALUES (?);`, [ hashTag ] ); - fileDb.run( + transOrDb.run( `REPLACE INTO file_hash_tag (hash_tag_id, file_id) VALUES ( (SELECT hash_tag_id diff --git a/core/message.js b/core/message.js index 710444ce..a251ef50 100644 --- a/core/message.js +++ b/core/message.js @@ -321,8 +321,13 @@ Message.prototype.load = function(options, cb) { ); }; -Message.prototype.persistMetaValue = function(category, name, value, cb) { - const metaStmt = msgDb.prepare( +Message.prototype.persistMetaValue = function(category, name, value, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = msgDb; + } + + const metaStmt = transOrDb.prepare( `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) VALUES (?, ?, ?, ?);`); @@ -341,18 +346,6 @@ Message.prototype.persistMetaValue = function(category, name, value, cb) { }); }; -Message.startTransaction = function(cb) { - msgDb.run('BEGIN;', err => { - cb(err); - }); -}; - -Message.endTransaction = function(hadError, cb) { - msgDb.run(hadError ? 'ROLLBACK;' : 'COMMIT;', err => { - cb(err); - }); -}; - Message.prototype.persist = function(cb) { if(!this.isValid()) { @@ -361,14 +354,12 @@ Message.prototype.persist = function(cb) { const self = this; - async.series( + async.waterfall( [ function beginTransaction(callback) { - Message.startTransaction(err => { - return callback(err); - }); + return msgDb.beginTransaction(callback); }, - function storeMessage(callback) { + function storeMessage(trans, callback) { // generate a UUID for this message if required (general case) const msgTimestamp = moment(); if(!self.uuid) { @@ -379,7 +370,7 @@ Message.prototype.persist = function(cb) { self.message); } - msgDb.run( + trans.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, getISOTimestampString(msgTimestamp) ], @@ -388,13 +379,13 @@ Message.prototype.persist = function(cb) { self.messageId = this.lastID; } - return callback(err); + return callback(err, trans); } ); }, - function storeMeta(callback) { + function storeMeta(trans, callback) { if(!self.meta) { - return callback(null); + return callback(null, trans); } /* Example of self.meta: @@ -410,7 +401,7 @@ Message.prototype.persist = function(cb) { */ async.each(Object.keys(self.meta), (category, nextCat) => { async.each(Object.keys(self.meta[category]), (name, nextName) => { - self.persistMetaValue(category, name, self.meta[category][name], err => { + self.persistMetaValue(category, name, self.meta[category][name], trans, err => { nextName(err); }); }, err => { @@ -418,18 +409,22 @@ Message.prototype.persist = function(cb) { }); }, err => { - callback(err); + callback(err, trans); }); }, - function storeHashTags(callback) { + function storeHashTags(trans, callback) { // :TODO: hash tag support - return callback(null); + return callback(null, trans); } ], - err => { - Message.endTransaction(err, transErr => { - return cb(err ? err : transErr, self.messageId); - }); + (err, trans) => { + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr, self.messageId); + }); + } else { + return cb(err); + } } ); }; diff --git a/core/user.js b/core/user.js index 15e5a844..fb125f6b 100644 --- a/core/user.js +++ b/core/user.js @@ -189,15 +189,13 @@ module.exports = class User { // :TODO: set various defaults, e.g. default activation status, etc. self.properties.account_status = Config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; - async.series( + async.waterfall( [ function beginTransaction(callback) { - userDb.run('BEGIN;', err => { - return callback(err); - }); + return userDb.beginTransaction(callback); }, - function createUserRec(callback) { - userDb.run( + function createUserRec(trans, callback) { + trans.run( `INSERT INTO user (user_name) VALUES (?);`, [ self.username ], @@ -213,11 +211,11 @@ module.exports = class User { self.properties.account_status = User.AccountStatus.active; } - return callback(null); + return callback(null, trans); } ); }, - function genAuthCredentials(callback) { + function genAuthCredentials(trans, callback) { User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { if(err) { return callback(err); @@ -225,85 +223,56 @@ module.exports = class User { self.properties.pw_pbkdf2_salt = info.salt; self.properties.pw_pbkdf2_dk = info.dk; - return callback(null); + return callback(null, trans); }); }, - function setInitialGroupMembership(callback) { + function setInitialGroupMembership(trans, callback) { self.groups = Config.users.defaultGroups; if(User.RootUserID === self.userId) { // root/SysOp? self.groups.push('sysops'); } - return callback(null); + return callback(null, trans); }, - function saveAll(callback) { - self.persist(false, err => { - return callback(err); + function saveAll(trans, callback) { + self.persistWithTransaction(trans, err => { + return callback(err, trans); }); } ], - err => { - if(err) { - const originalError = err; - userDb.run('ROLLBACK;', err => { - assert(!err); - return cb(originalError); + (err, trans) => { + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr); }); } else { - userDb.run('COMMIT;', err => { - return cb(err); - }); + return cb(err); } } ); } - persist(useTransaction, cb) { + persistWithTransaction(trans, cb) { assert(this.userId > 0); const self = this; async.series( [ - function beginTransaction(callback) { - if(useTransaction) { - userDb.run('BEGIN;', err => { - return callback(err); - }); - } else { - return callback(null); - } - }, function saveProps(callback) { - self.persistProperties(self.properties, err => { + self.persistProperties(self.properties, trans, err => { return callback(err); }); }, function saveGroups(callback) { - userGroup.addUserToGroups(self.userId, self.groups, err => { + userGroup.addUserToGroups(self.userId, self.groups, trans, err => { return callback(err); }); } ], err => { - if(err) { - if(useTransaction) { - userDb.run('ROLLBACK;', err => { - return cb(err); - }); - } else { - return cb(err); - } - } else { - if(useTransaction) { - userDb.run('COMMIT;', err => { - return cb(err); - }); - } else { - return cb(null); - } - } + return cb(err); } ); } @@ -340,13 +309,18 @@ module.exports = class User { ); } - persistProperties(properties, cb) { + persistProperties(properties, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = userDb; + } + const self = this; // update live props _.merge(this.properties, properties); - const stmt = userDb.prepare( + const stmt = transOrDb.prepare( `REPLACE INTO user_property (user_id, prop_name, prop_value) VALUES (?, ?, ?);` ); diff --git a/core/user_group.js b/core/user_group.js index 2fcaacf3..3903f2c3 100644 --- a/core/user_group.js +++ b/core/user_group.js @@ -33,8 +33,13 @@ function getGroupsForUser(userId, cb) { }); } -function addUserToGroup(userId, groupName, cb) { - userDb.run( +function addUserToGroup(userId, groupName, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = userDb; + } + + transOrDb.run( 'REPLACE INTO user_group_member (group_name, user_id) ' + 'VALUES(?, ?);', [ groupName, userId ], @@ -44,10 +49,10 @@ function addUserToGroup(userId, groupName, cb) { ); } -function addUserToGroups(userId, groups, cb) { +function addUserToGroups(userId, groups, transOrDb, cb) { async.each(groups, function item(groupName, next) { - addUserToGroup(userId, groupName, next); + addUserToGroup(userId, groupName, transOrDb, next); }, function complete(err) { cb(err); }); diff --git a/mods/bbs_list.js b/mods/bbs_list.js index e24beba6..07af017c 100644 --- a/mods/bbs_list.js +++ b/mods/bbs_list.js @@ -3,7 +3,10 @@ // ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; -const getModDatabasePath = require('../core/database.js').getModDatabasePath; +const { + getModDatabasePath, + getTransactionDatabase +} = require('../core/database.js').getModDatabasePath; const ViewController = require('../core/view_controller.js').ViewController; const ansi = require('../core/ansi_term.js'); const theme = require('../core/theme.js'); @@ -392,10 +395,10 @@ exports.getModule = class BBSListModule extends MenuModule { async.series( [ function openDatabase(callback) { - self.database = new sqlite3.Database( + self.database = getTransactionDatabase(new sqlite3.Database( getModDatabasePath(moduleInfo), callback - ); + )); }, function createTables(callback) { self.database.serialize( () => { diff --git a/mods/onelinerz.js b/mods/onelinerz.js index 335c25ce..49ecd3db 100644 --- a/mods/onelinerz.js +++ b/mods/onelinerz.js @@ -3,7 +3,10 @@ // ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; -const getModDatabasePath = require('../core/database.js').getModDatabasePath; +const { + getModDatabasePath, + getTransactionDatabase +} = require('../core/database.js').getModDatabasePath; const ViewController = require('../core/view_controller.js').ViewController; const theme = require('../core/theme.js'); const ansi = require('../core/ansi_term.js'); @@ -263,12 +266,12 @@ exports.getModule = class OnelinerzModule extends MenuModule { async.series( [ function openDatabase(callback) { - self.db = new sqlite3.Database( + self.db = getTransactionDatabase(new sqlite3.Database( getModDatabasePath(exports.moduleInfo), err => { return callback(err); } - ); + )); }, function createTables(callback) { self.db.run( diff --git a/package.json b/package.json index f8393142..cdd1ab3a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "ptyw.js": "NuSkooler/ptyw.js", "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.9", + "sqlite3-transactions": "^0.0.5", "ssh2": "^0.5.5", "temptmp": "^1.0.0", "uuid": "^3.1.0", From 59826930e4026bdc679bdb0c4a025cc9c089145c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Sep 2017 17:24:26 -0600 Subject: [PATCH 0016/1013] Fix requires --- mods/bbs_list.js | 4 +++- mods/onelinerz.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mods/bbs_list.js b/mods/bbs_list.js index 07af017c..ec964a36 100644 --- a/mods/bbs_list.js +++ b/mods/bbs_list.js @@ -3,10 +3,12 @@ // ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; + const { getModDatabasePath, getTransactionDatabase -} = require('../core/database.js').getModDatabasePath; +} = require('../core/database.js'); + const ViewController = require('../core/view_controller.js').ViewController; const ansi = require('../core/ansi_term.js'); const theme = require('../core/theme.js'); diff --git a/mods/onelinerz.js b/mods/onelinerz.js index 49ecd3db..065c0a30 100644 --- a/mods/onelinerz.js +++ b/mods/onelinerz.js @@ -3,10 +3,12 @@ // ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; -const { + +const { getModDatabasePath, getTransactionDatabase -} = require('../core/database.js').getModDatabasePath; +} = require('../core/database.js'); + const ViewController = require('../core/view_controller.js').ViewController; const theme = require('../core/theme.js'); const ansi = require('../core/ansi_term.js'); From 985a14629a76169a3223c3676d46bec5c18cc4d7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 Sep 2017 20:16:42 -0600 Subject: [PATCH 0017/1013] Fix document output to better work with reality --- util/exiftool2desc.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/util/exiftool2desc.js b/util/exiftool2desc.js index 210c800d..4b3b350f 100755 --- a/util/exiftool2desc.js +++ b/util/exiftool2desc.js @@ -43,12 +43,12 @@ function documentFile(metadata) { return; } - let desc = `${metadata.author||'Unknown Author'} - ${metadata.title||'Unknown'}`; - const created = moment(metadata.createdate); - if(created.isValid()) { - desc += ` (${created.format('YYYY')})`; + let result = metadata.author || ''; + if(result) { + result += ' - '; } - return desc; + result += metadata.title || 'Unknown Title'; + return result; } function imageFile(metadata) { From 5f9b3eb90d43f4b190cf5d9eea16bc8561450ba3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 Sep 2017 21:05:38 -0600 Subject: [PATCH 0018/1013] Switch to sqltie-trans, a more updated transaction handling module --- README.md | 2 +- core/database.js | 6 +++--- docs/mods.md | 9 +++++++++ package.json | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 docs/mods.md diff --git a/README.md b/README.md index 39ee0561..3795dd30 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! ## Features Available Now * 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 + * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based [mods](docs/mods.md) * [MCI support](docs/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles * Telnet, **SSH**, and both secure and non-secure [WebSocket](https://en.wikipedia.org/wiki/WebSocket) access built in! Additional servers are easy to implement * [CP437](http://www.ascii-codes.com/) and UTF-8 output diff --git a/core/database.js b/core/database.js index a6286279..41aa7a41 100644 --- a/core/database.js +++ b/core/database.js @@ -6,7 +6,7 @@ const conf = require('./config.js'); // deps const sqlite3 = require('sqlite3'); -const sqlite3Trans = require('sqlite3-transactions'); +const sqlite3Trans = require('sqlite3-trans'); const paths = require('path'); const async = require('async'); const _ = require('lodash'); @@ -24,7 +24,7 @@ exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; function getTransactionDatabase(db) { - return new sqlite3Trans.TransactionDatabase(db); + return sqlite3Trans.wrap(db); } function getDatabasePath(name) { @@ -61,7 +61,7 @@ function getISOTimestampString(ts) { function initializeDatabases(cb) { async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { - dbs[dbName] = new sqlite3Trans.TransactionDatabase(new sqlite3.Database(getDatabasePath(dbName), err => { + dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { if(err) { return cb(err); } diff --git a/docs/mods.md b/docs/mods.md new file mode 100644 index 00000000..3abc7e2f --- /dev/null +++ b/docs/mods.md @@ -0,0 +1,9 @@ +# Mods + + +## Existing Mods +* **Married Bob Fetch Event**: An event for fetching the latest Married Bob ANSI's for display on you board. ACiDic release [ACD-MB4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MB4E.ZIP). Can also be [found on GitHub](https://github.com/NuSkooler/enigma-bbs-married_bob_evt) +* **Latest Files Announcement**: An event for posting the latest file arrivals of your board to message areas such as FTN style networks. ACiDic release [ACD-LFA1.ZIP](https://l33t.codes/outgoing/ACD/ACD-LFA1.ZIP) Also [found on GitHub](https://github.com/NuSkooler/enigma-bbs-latest_files_announce_evt) +* **Message Post Event**: An event for posting messages/ads to networks. ACiDic release [ACD-MP4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MP4E.ZIP) + +See also [ACiDic BBS Mods by Myself](https://l33t.codes/acidic-mods-by-myself/) \ No newline at end of file diff --git a/package.json b/package.json index cdd1ab3a..249a56c2 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "ptyw.js": "NuSkooler/ptyw.js", "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.9", - "sqlite3-transactions": "^0.0.5", + "sqlite3-trans" : "^1.1.0", "ssh2": "^0.5.5", "temptmp": "^1.0.0", "uuid": "^3.1.0", From 7837a2a7bdd4db82a9c38e15a1c1c23aa760d860 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 Sep 2017 21:11:17 -0600 Subject: [PATCH 0019/1013] Update sqlite3-trans version again --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 249a56c2..ef9da900 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "ptyw.js": "NuSkooler/ptyw.js", "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.9", - "sqlite3-trans" : "^1.1.0", + "sqlite3-trans" : "^1.2.0", "ssh2": "^0.5.5", "temptmp": "^1.0.0", "uuid": "^3.1.0", From b0260049ba39b55050562017722e455d80d150dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 Sep 2017 21:23:30 -0600 Subject: [PATCH 0020/1013] Add VTX hyperlink support for URLs --- core/ansi_term.js | 13 ++++++++++++- core/client.js | 7 ++++++- core/config.js | 5 ++++- core/string_format.js | 2 ++ mods/file_area_list.js | 4 ++-- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/core/ansi_term.js b/core/ansi_term.js index 8b4094f1..504a1709 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setCursorStyle = setCursorStyle; exports.setEmulatedBaudRate = setEmulatedBaudRate; - +exports.getVtxHyperlink = getVtxHyperlink; // // See also @@ -485,3 +485,14 @@ function setEmulatedBaudRate(rate) { }[rate] || 0; return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); } + +function getVtxHyperlink(client, url, text) { + if(!client.terminalSupports('vtx_hyperlink')) { + return ''; + } + + text = text || url; + + url = url.split('').map(c => c.charCodeAt(0)).join(';'); + return `${ESC_CSI}1;${text.length};1;1;${url}\\`; +} \ No newline at end of file diff --git a/core/client.js b/core/client.js index 4501396d..71d7a9ad 100644 --- a/core/client.js +++ b/core/client.js @@ -493,10 +493,15 @@ Client.prototype.defaultHandlerMissingMod = function(err) { }; Client.prototype.terminalSupports = function(query) { + const termClient = this.term.termClient; + switch(query) { case 'vtx_audio' : // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt - return this.termClient === 'vtx'; + return 'vtx' === termClient; + + case 'vtx_hyperlink' : + return termClient === 'vtx'; default : return false; diff --git a/core/config.js b/core/config.js index 85632000..75be7795 100644 --- a/core/config.js +++ b/core/config.js @@ -158,7 +158,10 @@ function getDefaultConfig() { newUserNames : [ 'new', 'apply' ], // Names reserved for applying // :TODO: Mystic uses TRASHCAN.DAT for this -- is there a reason to support something like that? - badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all' ], + badUserNames : [ + 'sysop', 'admin', 'administrator', 'root', 'all', + 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' + ], }, // :TODO: better name for "defaults"... which is redundant here! diff --git a/core/string_format.js b/core/string_format.js index 297d92b2..7fb7109a 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -271,8 +271,10 @@ const transformers = { styleMixed : (s) => stylizeString(s, 'mixed'), styleL33t : (s) => stylizeString(s, 'l33t'), + // :TODO: // toMegs(), toKilobytes(), ... // toList(), toCommaList(), + sizeWithAbbr : (n) => formatByteSize(n, true, 2), sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), sizeAbbr : (n) => formatByteSizeAbbr(n), diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 076d2a98..21b5557d 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -274,7 +274,7 @@ exports.getModule = class FileAreaList extends MenuModule { } else { const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - entryInfo.webDlLink = serveItem.url; + entryInfo.webDlLink = ansi.getVtxHyperlink(this.client, serveItem.url) + serveItem.url; entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); } @@ -497,7 +497,7 @@ exports.getModule = class FileAreaList extends MenuModule { const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - self.currentFileEntry.entryInfo.webDlLink = url; + self.currentFileEntry.entryInfo.webDlLink = ansi.getVtxHyperlink(self.client, url) + url; self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); return callback(null); From 1ad5b125f5bd54607757c183135de8c2aad21b37 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Sep 2017 23:03:21 -0600 Subject: [PATCH 0021/1013] oputil fb rm|remove|del|delete functionality --- core/file_entry.js | 36 +++++- core/oputil/oputil_file_base.js | 190 +++++++++++++++++++++++--------- core/oputil/oputil_help.js | 14 ++- 3 files changed, 178 insertions(+), 62 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 3a5b3df2..f5286dd3 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -11,6 +11,7 @@ const async = require('async'); const _ = require('lodash'); const paths = require('path'); const fse = require('fs-extra'); +const { unlink } = require('graceful-fs'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', @@ -541,6 +542,40 @@ module.exports = class FileEntry { }); } + static removeEntry(srcFileEntry, options, cb) { + if(!_.isFunction(cb) && _.isFunction(options)) { + cb = options; + options = {}; + } + + async.series( + [ + function removeFromDatabase(callback) { + fileDb.run( + `DELETE FROM file + WHERE file_id = ?;`, + [ srcFileEntry.fileId ], + err => { + return callback(err); + } + ); + }, + function optionallyRemovePhysicalFile(callback) { + if(true !== options.removePhysFile) { + return callback(null); + } + + unlink(srcFileEntry.filePath, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } + static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) { if(!cb && _.isFunction(destFileName)) { cb = destFileName; @@ -550,7 +585,6 @@ module.exports = class FileEntry { const srcPath = srcFileEntry.filePath; const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); - if(!dstDir) { return cb(Errors.Invalid('Invalid storage tag')); } diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 3f791267..d9ad9366 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -438,6 +438,62 @@ function scanFileAreas() { ); } +function expandFileTargets(targets, cb) { + let entries = []; + + // Each entry may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + const FileEntry = require('../../core/file_entry.js'); + + async.eachSeries(targets, (areaAndStorage, next) => { + const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); + + if(areaInfo) { + // AREA_TAG[@STORAGE_TAG] - all files in area@tag + const findFilter = { + areaTag : areaAndStorage.areaTag, + }; + + if(areaAndStorage.storageTag) { + findFilter.storageTag = areaAndStorage.storageTag; + } + + FileEntry.findFiles(findFilter, (err, fileIds) => { + if(err) { + return next(err); + } + + async.each(fileIds, (fileId, nextFileId) => { + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + if(!err) { + entries.push(fileEntry); + } + return nextFileId(err); + }); + }, + err => { + return next(err); + }); + }); + + } else { + // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA + // :TODO: FULL_PATH -> entries + getFileEntries(areaAndStorage.pattern, (err, fileEntries) => { + if(err) { + return next(err); + } + + entries = entries.concat(fileEntries); + return next(null); + }); + } + }, + err => { + return cb(err, entries); + }); +} + function moveFiles() { // // oputil fb move SRC [SRC2 ...] DST @@ -450,8 +506,9 @@ function moveFiles() { } const moveArgs = argv._.slice(2); - let src = getAreaAndStorage(moveArgs.slice(0, -1)); - let dst = getAreaAndStorage(moveArgs.slice(-1))[0]; + const src = getAreaAndStorage(moveArgs.slice(0, -1)); + const dst = getAreaAndStorage(moveArgs.slice(-1))[0]; + let FileEntry; async.waterfall( @@ -465,8 +522,6 @@ function moveFiles() { }); }, function validateAndExpandSourceAndDest(callback) { - let srcEntries = []; - const areaInfo = fileArea.getFileAreaByTag(dst.areaTag); if(areaInfo) { dst.areaInfo = areaInfo; @@ -474,57 +529,9 @@ function moveFiles() { return callback(Errors.DoesNotExist('Invalid or unknown destination area')); } - // Each SRC may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] FileEntry = require('../../core/file_entry.js'); - async.eachSeries(src, (areaAndStorage, next) => { - const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); - - if(areaInfo) { - // AREA_TAG[@STORAGE_TAG] - all files in area@tag - src.areaInfo = areaInfo; - - const findFilter = { - areaTag : areaAndStorage.areaTag, - }; - - if(areaAndStorage.storageTag) { - findFilter.storageTag = areaAndStorage.storageTag; - } - - FileEntry.findFiles(findFilter, (err, fileIds) => { - if(err) { - return next(err); - } - - async.each(fileIds, (fileId, nextFileId) => { - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - if(!err) { - srcEntries.push(fileEntry); - } - return nextFileId(err); - }); - }, - err => { - return next(err); - }); - }); - - } else { - // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA - // :TODO: FULL_PATH -> entries - getFileEntries(areaAndStorage.pattern, (err, entries) => { - if(err) { - return next(err); - } - - srcEntries = srcEntries.concat(entries); - return next(null); - }); - } - }, - err => { + expandFileTargets(src, (err, srcEntries) => { return callback(err, srcEntries); }); }, @@ -555,13 +562,80 @@ function moveFiles() { return callback(err); }); } - ] + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } ); } function removeFiles() { // - // REMOVE FILENAME_WC|SHA|FILE_ID [SHA|FILE_ID ...] + // oputil fb rm|remove|del|delete SRC [SRC2 ...] + // + // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + // + // AREA_TAG[@STORAGE_TAG] remove all entries matching + // supplied area/storage tags + // + // --phys-file removes backing physical file(s) + // + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); + } + + const removePhysFile = argv['phys-file']; + + const src = getAreaAndStorage(argv._.slice(2)); + + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases( err => { + if(!err) { + fileArea = require('../../core/file_base_area.js'); + } + return callback(err); + }); + }, + function expandSources(callback) { + expandFileTargets(src, (err, srcEntries) => { + return callback(err, srcEntries); + }); + }, + function removeEntries(srcEntries, callback) { + const FileEntry = require('../../core/file_entry.js'); + + const extraOutput = removePhysFile ? ' (including physical file)' : ''; + + async.eachSeries(srcEntries, (entry, nextEntry) => { + + process.stdout.write(`Removing ${entry.filePath}${extraOutput}... `); + + FileEntry.removeEntry(entry, { removePhysFile }, err => { + if(err) { + console.info(`Failed: ${err.message}`); + } else { + console.info('Done'); + } + + return nextEntry(err); + }); + }, err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); } function handleFileBaseCommand() { @@ -582,7 +656,13 @@ function handleFileBaseCommand() { return ({ info : displayFileAreaInfo, scan : scanFileAreas, + + mv : moveFiles, move : moveFiles, + + rm : removeFiles, remove : removeFiles, + del : removeFiles, + delete : removeFiles, }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index c49200b0..1a2b42bf 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -45,7 +45,7 @@ import-areas args: --type TYPE specifies area import type. valid options are "bbs" and "na" `, FileBase : -`usage: oputil.js fb [] [] +`usage: oputil.js fb [] actions: scan AREA_TAG[@STORAGE_TAG] scan specified area @@ -53,14 +53,16 @@ actions: info AREA_TAG|SHA|FILE_ID display information about areas and/or files SHA may be a full or partial SHA-256 - move SRC [SRC...]] DST move entry(s) from SRC to DST - * SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] - * DST: AREA_TAG[@STORAGE_TAG] + mv SRC [SRC...] DST move entry(s) from SRC to DST + SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + DST: AREA_TAG[@STORAGE_TAG] - remove SHA|FILE_ID removes a entry from the system + rm SRC [SRC...] remove entry(s) from the system matching SRC + SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] scan args: --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries + --desc-file [PATH] prefer file descriptions from DESCRIPT.ION file over other sources such as FILE_ID.DIZ. if PATH is specified, use DESCRIPT.ION at PATH instead @@ -71,7 +73,7 @@ info args: --show-desc display short description, if any remove args: - --delete also remove underlying physical file + --phys-file also remove underlying physical file `, FileOpsInfo : ` From 48c6edc5b39b478c0c2366c9eb2625f6ac3bc99e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Sep 2017 23:17:16 -0600 Subject: [PATCH 0022/1013] Rename VTX Hyperlink stuff --- core/ansi_term.js | 8 ++++---- core/client.js | 2 +- mods/file_area_list.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/ansi_term.js b/core/ansi_term.js index 504a1709..7eb10ec2 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setCursorStyle = setCursorStyle; exports.setEmulatedBaudRate = setEmulatedBaudRate; -exports.getVtxHyperlink = getVtxHyperlink; +exports.vtxHyperlink = vtxHyperlink; // // See also @@ -486,13 +486,13 @@ function setEmulatedBaudRate(rate) { return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); } -function getVtxHyperlink(client, url, text) { +function vtxHyperlink(client, url, len) { if(!client.terminalSupports('vtx_hyperlink')) { return ''; } - text = text || url; + len = len || url.length; url = url.split('').map(c => c.charCodeAt(0)).join(';'); - return `${ESC_CSI}1;${text.length};1;1;${url}\\`; + return `${ESC_CSI}1;${len};1;1;${url}\\`; } \ No newline at end of file diff --git a/core/client.js b/core/client.js index 71d7a9ad..08776bf4 100644 --- a/core/client.js +++ b/core/client.js @@ -501,7 +501,7 @@ Client.prototype.terminalSupports = function(query) { return 'vtx' === termClient; case 'vtx_hyperlink' : - return termClient === 'vtx'; + return 'vtx' === termClient; default : return false; diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 21b5557d..64a663cb 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -274,7 +274,7 @@ exports.getModule = class FileAreaList extends MenuModule { } else { const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - entryInfo.webDlLink = ansi.getVtxHyperlink(this.client, serveItem.url) + serveItem.url; + entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); } @@ -497,7 +497,7 @@ exports.getModule = class FileAreaList extends MenuModule { const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - self.currentFileEntry.entryInfo.webDlLink = ansi.getVtxHyperlink(self.client, url) + url; + self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); return callback(null); From 68da131b1b13749b5fc6caf90e19b4c6b21f8754 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 24 Sep 2017 09:58:43 -0600 Subject: [PATCH 0023/1013] Fix waterfall error --- core/theme.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/theme.js b/core/theme.js index c5b90233..0796b8ab 100644 --- a/core/theme.js +++ b/core/theme.js @@ -580,7 +580,11 @@ function displayThemedPrompt(name, client, options, cb) { client, dispOptions, (err, artInfo) => { - return callback(err, promptConfig, artInfo); + if(err) { + return callback(err); + } + + return callback(null, promptConfig, artInfo); } ); }, From d5334270c497090a5561e5c152017e6e8cbf903f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 24 Sep 2017 09:58:57 -0600 Subject: [PATCH 0024/1013] Update default menu.hjson - fix parse error --- mods/menu.hjson | 82 +++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 51 deletions(-) diff --git a/mods/menu.hjson b/mods/menu.hjson index d1822cfc..15a59cb8 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -598,7 +598,17 @@ fullLoginSequenceOnelinerz: { desc: Viewing Onelinerz module: onelinerz - next: fullLoginSequenceNewScanConfirm + next: [ + { + // calls >= 2 + acs: NC2 + next: fullLoginSequenceNewScanConfirm + } + { + // new users - skip new scan + next: fullLoginSequenceUserStats + } + ] options: { cls: true } @@ -1006,28 +1016,33 @@ } ] } + mainMenuLastCallers: { desc: Last Callers module: last_callers art: LASTCALL options: { pause: true } } + mainMenuWhosOnline: { desc: Who's Online module: whos_online art: WHOSON options: { pause: true } } + mainMenuUserStats: { desc: User Stats art: STATUS options: { pause: true } } + mainMenuSystemStats: { desc: System Stats art: SYSSTAT options: { pause: true } } + mainMenuUserList: { desc: User Listing module: user_list @@ -1615,15 +1630,11 @@ action: @systemMethod:prevMenu } { - value: { command: "1" } + value: { command: "PW" } action: @menu:doorPimpWars } { - value: { command: "2" } - action: @menu:doorLORD - } - { - value: { command: "4" } + value: { command: "TW" } action: @menu:doorTradeWars2002BBSLink } { @@ -1635,19 +1646,22 @@ action: @menu:doorParty } { - value: { command: "HL" } - action: @menu:telnetBridgeHappyLand + value: { command: "AGENT" } + action: @menu:telnetBridgeAgency } ] } + // + // Example using the abracadabra module for a retro DOS door + // doorPimpWars: { desc: Playing PimpWars module: abracadabra config: { name: PimpWars dropFileType: DORINFO - cmd: /home/nuskooler/DOS/scripts/pimpwars.sh + cmd: /home/enigma/DOS/scripts/pimpwars.sh args: [ "{node}", "{dropFile}", @@ -1659,37 +1673,6 @@ } } - doorDarkLands: { - desc: Playing Dark Lands - module: abracadabra - config: { - name: DARKLANDS - dropFileType: DOOR - cmd: /home/nuskooler/dev/enigma-bbs/doors/darklands/start.sh - args: [ - "{node}", - "{dropFile}", - "{srvPort}", - ], - nodeMax: 1 - tooManyArt: DOORMANY - io: socket - } - } - - doorLORD: { - desc: Playing L.O.R.D. - module: abracadabra - config: { - name: LORD - dropFileType: DOOR - cmd: /usr/bin/dosemu - args: [ - "-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\LORD\\START.BAT {node}" - ] - } - } - // // TradeWars 2000 example via BBSLink // @@ -1706,6 +1689,7 @@ } } + // DoorParty! support. You'll need to registger to obtain credentials doorParty: { desc: Using DoorParty! module: @systemModule:door_party @@ -1716,14 +1700,11 @@ } } - telnetBridgeHappyLand: { + telnetBridgeAgency: { desc: Connected to HappyLand BBS module: telnet_bridge config: { - host: andrew.homeunix.org - port: 2023 - //host: agency.bbs.geek.nz - //port: 23 + host: agency.bbs.geek.nz } } @@ -2069,12 +2050,11 @@ } 3: { - HM: { - mci: { - HM1: { - items: [ "save", "discard", "quote", "help" ] - } + mci: { + HM1: { + items: [ "save", "discard", "quote", "help" ] } + } submit: { *: [ From 47551b18034c872f349b281756c9715c71a37e75 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 24 Sep 2017 11:15:26 -0600 Subject: [PATCH 0025/1013] Add isNixTerm(), use includes vs indexOf on array search --- core/client_term.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/client_term.js b/core/client_term.js index 9992f9f3..b313841e 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -110,6 +110,17 @@ ClientTerminal.prototype.disconnect = function() { this.output = null; }; +ClientTerminal.prototype.isNixTerm = function() { + // + // Standard *nix type terminals + // + if(this.termType.startsWith('xterm')) { + return true; + } + + return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType); +}; + ClientTerminal.prototype.isANSI = function() { // // ANSI terminals should be encoded to CP437 @@ -142,7 +153,7 @@ ClientTerminal.prototype.isANSI = function() { // linux: // * JuiceSSH (note: TERM=linux also) // - return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1; + return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType); }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) From e37409e9b5e350901564bfa74ca6980b09691f24 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 24 Sep 2017 11:15:38 -0600 Subject: [PATCH 0026/1013] * Separate out DEL vs backspace when possible for ANSI-BBS terminals. *nix terminals don't send us what we need, but deal with it. * Handle delete in MultiLineTextEditView. More to come soon! --- core/client.js | 16 +++++++++++- core/multi_line_edit_text_view.js | 41 ++++++++++++++++++------------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/core/client.js b/core/client.js index 08776bf4..424748a6 100644 --- a/core/client.js +++ b/core/client.js @@ -306,7 +306,21 @@ function Client(input, output) { key.name = 'line feed'; } else if('\t' === s) { key.name = 'tab'; - } else if ('\b' === s || '\x7f' === s || '\x1b\x7f' === s || '\x1b\b' === s) { + } else if('\x7f' === s) { + // + // Backspace vs delete is a crazy thing, especially in *nix. + // - ANSI-BBS uses 0x7f for DEL + // - xterm et. al clients send 0x7f for backspace... ugg. + // + // See http://www.hypexr.org/linux_ruboff.php + // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html + // + if(self.term.isNixTerm()) { + key.name = 'backspace'; + } else { + key.name = 'delete'; + } + } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { // backspace, CTRL-H key.name = 'backspace'; key.meta = ('\x1b' === s.charAt(0)); diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 55e84080..68d8b3d6 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -67,7 +67,7 @@ const SPECIAL_KEY_MAP_DEFAULT = { 'line feed' : [ 'return' ], exit : [ 'esc' ], backspace : [ 'backspace' ], - delete : [ 'del' ], + delete : [ 'delete' ], tab : [ 'tab' ], up : [ 'up arrow' ], down : [ 'down arrow' ], @@ -354,21 +354,14 @@ function MultiLineEditTextView(options) { }; this.removeCharactersFromText = function(index, col, operation, count) { - if('right' === operation) { + if('delete' === operation) { self.textLines[index].text = - self.textLines[index].text.slice(col, count) + + self.textLines[index].text.slice(0, col) + self.textLines[index].text.slice(col + count); - self.cursorPos.col -= count; - self.updateTextWordWrap(index); self.redrawRows(self.cursorPos.row, self.dimens.height); - - if(0 === self.textLines[index].text) { - - } else { - self.redrawRows(self.cursorPos.row, self.dimens.height); - } + self.moveClientCursorToCursorPos(); } else if ('backspace' === operation) { // :TODO: method for splicing text self.textLines[index].text = @@ -868,11 +861,25 @@ function MultiLineEditTextView(options) { }; this.keyPressDelete = function() { - self.removeCharactersFromText( - self.getTextLinesIndex(), - self.cursorPos.col, - 'right', - 1); + const lineIndex = self.getTextLinesIndex(); + + if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) { + // + // Start of line and nothing left. Just delete the line + // + self.removeCharactersFromText( + lineIndex, + 0, + 'delete line' + ); + } else { + self.removeCharactersFromText( + lineIndex, + self.cursorPos.col, + 'delete', + 1 + ); + } self.emitEditPosition(); }; @@ -1143,7 +1150,7 @@ const HANDLED_SPECIAL_KEYS = [ 'line feed', 'insert', 'tab', - 'backspace', 'del', + 'backspace', 'delete', 'delete line', ]; From 88049a3c7ac47217b38b6bfa9889c049eadf7f9a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 24 Sep 2017 11:35:12 -0600 Subject: [PATCH 0027/1013] Prefer FILE_ID.ANS > FILE_ID.DIZ --- core/config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 75be7795..da210a75 100644 --- a/core/config.js +++ b/core/config.js @@ -653,8 +653,9 @@ function getDefaultConfig() { fileNamePatterns: { // These are NOT case sensitive // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ + // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. desc : [ - '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' + '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' ], // common README filename - https://en.wikipedia.org/wiki/README From 37c78209a8a78b6b11bb66b6adcd0a3be46b550e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:39:07 -0600 Subject: [PATCH 0028/1013] Fix up system internal file areas --- core/config.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/config.js b/core/config.js index da210a75..549b44a2 100644 --- a/core/config.js +++ b/core/config.js @@ -689,14 +689,21 @@ function getDefaultConfig() { // Non-absolute paths are relative to |areaStoragePrefix|. // storageTags : { - sys_msg_attach : 'msg_attach', + sys_msg_attach : 'sys_msg_attach', + sys_temp_download : 'sys_temp_download', }, areas: { system_message_attachment : { - name : 'Message attachments', + name : 'System Message Attachments', desc : 'File attachments to messages', - storageTags : 'sys_msg_attach', // may be string or array of strings + storageTags : [ 'sys_msg_attach' ], + }, + + system_temporary_download : { + name : 'System Temporary Downloads', + desc : 'Temporary downloadables', + storageTags : [ 'sys_temp_download' ], } } }, From f105c25e17622360db297876f7f91b687cd2113c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:39:23 -0600 Subject: [PATCH 0029/1013] Add file_web_serve_batch table --- core/database.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/database.js b/core/database.js index 41aa7a41..14b3bf95 100644 --- a/core/database.js +++ b/core/database.js @@ -374,6 +374,15 @@ const DB_INIT_TABLE = { );` ); + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_web_serve_batch ( + hash_id VARCHAR NOT NULL, + file_id INTEGER NOT NULL, + + UNIQUE(hash_id, file_id) + );` + ); + return cb(null); } }; \ No newline at end of file From 59da1a2461cdc4a2a17692799fc00d5db5b103b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:40:30 -0600 Subject: [PATCH 0030/1013] * Add getAvailableFileAreaTags() * Properly check area tags for system internal --- core/file_base_area.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index ed34607d..1d1062a9 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -28,6 +28,7 @@ const moment = require('moment'); exports.isInternalArea = isInternalArea; exports.getAvailableFileAreas = getAvailableFileAreas; +exports.getAvailableFileAreaTags = getAvailableFileAreaTags; exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; exports.isValidStorageTag = isValidStorageTag; exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag; @@ -48,10 +49,11 @@ exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; const WellKnownAreaTags = exports.WellKnownAreaTags = { Invalid : '', MessageAreaAttach : 'system_message_attachment', + TempDownloads : 'system_temporary_download', }; function isInternalArea(areaTag) { - return areaTag === WellKnownAreaTags.MessageAreaAttach; + return [ WellKnownAreaTags.MessageAreaAttach, WellKnownAreaTags.TempDownloads ].includes(areaTag); } function getAvailableFileAreas(client, options) { @@ -77,6 +79,10 @@ function getAvailableFileAreas(client, options) { }); } +function getAvailableFileAreaTags(client, options) { + return _.map(getAvailableFileAreas(client, options), area => area.areaTag); +} + function getSortedAvailableFileAreas(client, options) { const areas = _.map(getAvailableFileAreas(client, options), v => v); sortAreasOrConfs(areas); From 0f9e545b7bf62e516e32fc78e4ca8d069517b44e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:41:41 -0600 Subject: [PATCH 0031/1013] Allow filter on 1:n area tags in findFiles(). Add ability to calc sha256 if not already set (use sparingly!) --- core/file_entry.js | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index f5286dd3..84c6559f 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -11,7 +11,8 @@ const async = require('async'); const _ = require('lodash'); const paths = require('path'); const fse = require('fs-extra'); -const { unlink } = require('graceful-fs'); +const { unlink, readFile } = require('graceful-fs'); +const crypto = require('crypto'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', @@ -120,6 +121,26 @@ module.exports = class FileEntry { } return callback(null); }, + function calcSha256IfNeeded(callback) { + if(self.fileSha256) { + return callback(null); + } + + if(isUpdate) { + return callback(Errors.MissingParam('fileSha256 property must be set for updates!')); + } + + readFile(self.filePath, (err, data) => { + if(err) { + return callback(err); + } + + const sha256 = crypto.createHash('sha256'); + sha256.update(data); + self.fileSha256 = sha256.digest('hex'); + return callback(null); + }); + }, function startTrans(callback) { return fileDb.beginTransaction(callback); }, @@ -169,8 +190,8 @@ module.exports = class FileEntry { (err, trans) => { // :TODO: Log orig err if(trans) { - trans[err ? 'rollback' : 'commit'](err => { - return cb(err); + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(transErr ? transErr : err); }); } else { return cb(err); @@ -459,7 +480,12 @@ module.exports = class FileEntry { } if(filter.areaTag && filter.areaTag.length > 0) { - appendWhereClause(`f.area_tag = "${filter.areaTag}"`); + if(Array.isArray(filter.areaTag)) { + const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); + appendWhereClause(`f.area_tag IN(${areaList})`); + } else { + appendWhereClause(`f.area_tag = "${filter.areaTag}"`); + } } if(filter.metaPairs && filter.metaPairs.length > 0) { From 8479091d330aa21b934cec37ad1ce4766b491842 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:42:53 -0600 Subject: [PATCH 0032/1013] Filter out system areas --- mods/file_area_list.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 64a663cb..4937eb49 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -675,7 +675,13 @@ exports.getModule = class FileAreaList extends MenuModule { loadFileIds(force, cb) { if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { this.fileListPosition = 0; - FileEntry.findFiles(this.filterCriteria, (err, fileIds) => { + + const filterCriteria = Object.assign({}, this.filterCriteria); + if(!filterCriteria.areaTag) { + filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client); + } + + FileEntry.findFiles(filterCriteria, (err, fileIds) => { this.fileList = fileIds; return cb(err); }); From e555a28160ca2e0cb87c8ee754963e06c6e698c3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:43:22 -0600 Subject: [PATCH 0033/1013] Filter out system areas --- core/new_scan.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/core/new_scan.js b/core/new_scan.js index 3c3f1371..a75a5b2d 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -2,13 +2,14 @@ 'use strict'; // ENiGMA½ -const msgArea = require('./message_area.js'); -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const stringFormat = require('./string_format.js'); -const FileEntry = require('./file_entry.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const Errors = require('./enig_error.js').Errors; +const msgArea = require('./message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const stringFormat = require('./string_format.js'); +const FileEntry = require('./file_entry.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const Errors = require('./enig_error.js').Errors; +const { getAvailableFileAreaTags } = require('./file_base_area.js'); // deps const _ = require('lodash'); @@ -166,8 +167,13 @@ exports.getModule = class NewScanModule extends MenuModule { newScanFileBase(cb) { // :TODO: add in steps + const filterCriteria = { + newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), + areaTag : getAvailableFileAreaTags(this.client), + }; + FileEntry.findFiles( - { newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user) }, + filterCriteria, (err, fileIds) => { if(err || 0 === fileIds.length) { return cb(err ? err : Errors.DoesNotExist('No more new files')); From dc2b3031fd95df19ea9a44090afad11ed7d3c395 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 Sep 2017 10:44:15 -0600 Subject: [PATCH 0034/1013] * Change how hashids are generated for web file area: include a 'type' * Add support for web *batch* downloads via streaming zip file creation * Add new web download manager and batch mode display * Add extra info to 'standard' downloads mod/menu --- core/file_area_web.js | 336 ++++++++++++++++++------- mods/file_base_download_manager.js | 38 ++- mods/file_base_web_download_manager.js | 287 +++++++++++++++++++++ package.json | 3 +- 4 files changed, 560 insertions(+), 104 deletions(-) create mode 100644 mods/file_base_web_download_manager.js diff --git a/core/file_area_web.js b/core/file_area_web.js index aa60974f..a8d095d6 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -22,14 +22,7 @@ const paths = require('path'); const async = require('async'); const fs = require('graceful-fs'); const mimeTypes = require('mime-types'); -const _ = require('lodash'); - - /* - :TODO: - * Load temp download URLs @ startup & set expire timers via scheduler. - * At creation, set expire timer via scheduler - * - */ +const yazl = require('yazl'); function notEnabledError() { return Errors.General('Web server is not enabled', ErrNotEnabled); @@ -59,7 +52,7 @@ class FileAreaWebAccess { const routeAdded = self.webServer.instance.addRoute({ method : 'GET', path : Config.fileBase.web.routePath, - handler : self.routeWebRequestForFile.bind(self), + handler : self.routeWebRequest.bind(self), }); return callback(routeAdded ? null : Errors.General('Failed adding route')); } else { @@ -81,6 +74,13 @@ class FileAreaWebAccess { return this.webServer.instance.isEnabled(); } + static getHashIdTypes() { + return { + SingleFile : 0, + BatchArchive : 1, + }; + } + load(cb) { // // Load entries, register expiration timers @@ -141,12 +141,14 @@ class FileAreaWebAccess { WHERE hash_id = ?`, [ hashId ], (err, result) => { - if(err) { - return cb(err); + if(err || !result) { + return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID')); } const decoded = this.hashids.decode(hashId); - if(!result || 2 !== decoded.length) { + + // decode() should provide an array of [ userId, hashIdType, id, ... ] + if(!Array.isArray(decoded) || decoded.length < 3) { return cb(Errors.Invalid('Invalid or unknown hash ID')); } @@ -155,7 +157,8 @@ class FileAreaWebAccess { { hashId : hashId, userId : decoded[0], - fileId : decoded[1], + hashIdType : decoded[1], + fileIds : decoded.slice(2), expireTimestamp : moment(result.expire_timestamp), } ); @@ -163,46 +166,26 @@ class FileAreaWebAccess { ); } - getHashId(client, fileEntry) { - // - // Hashid is a unique combination of userId & fileId - // - return this.hashids.encode(client.user.userId, fileEntry.fileId); + getSingleFileHashId(client, fileEntry) { + return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] ); } - buildTempDownloadLink(client, fileEntry, hashId) { - hashId = hashId || this.getHashId(client, fileEntry); + getBatchArchiveHashId(client, batchId) { + return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId); + } + + getHashId(client, hashIdType, identifier) { + return this.hashids.encode(client.user.userId, hashIdType, identifier); + } + + buildSingleFileTempDownloadLink(client, fileEntry, hashId) { + hashId = hashId || this.getSingleFileHashId(client, fileEntry); return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`); - /* - - // - // Create a URL such as - // https://l33t.codes:44512/f/qFdxyZr - // - // Prefer HTTPS over HTTP. Be explicit about the port - // only if non-standard. - // - let schema; - let port; - if(_.isString(Config.contentServers.web.overrideUrlPrefix)) { - return `${Config.contentServers.web.overrideUrlPrefix}${Config.fileBase.web.path}${hashId}`; - } else { - 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}`; - } - */ + } + + buildBatchArchiveTempDownloadLink(client, hashId) { + return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`); } getExistingTempDownloadServeItem(client, fileEntry, cb) { @@ -210,49 +193,95 @@ class FileAreaWebAccess { return cb(notEnabledError()); } - const hashId = this.getHashId(client, fileEntry); + const hashId = this.getSingleFileHashId(client, fileEntry); this.loadServedHashId(hashId, (err, servedItem) => { if(err) { return cb(err); } - servedItem.url = this.buildTempDownloadLink(client, fileEntry); + servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); return cb(null, servedItem); }); } + _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { + // add/update rec with hash id and (latest) timestamp + dbOrTrans.run( + `REPLACE INTO file_web_serve (hash_id, expire_timestamp) + VALUES (?, ?);`, + [ hashId, getISOTimestampString(expireTime) ], + err => { + if(err) { + return cb(err); + } + + this.scheduleExpire(hashId, expireTime); + + return cb(null); + } + ); + } + createAndServeTempDownload(client, fileEntry, options, cb) { if(!this.isEnabled()) { return cb(notEnabledError()); } - const hashId = this.getHashId(client, fileEntry); - const url = this.buildTempDownloadLink(client, fileEntry, hashId); + const hashId = this.getSingleFileHashId(client, fileEntry); + const url = this.buildSingleFileTempDownloadLink(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 => { + this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { + return cb(err, url); + }); + } + + createAndServeTempBatchDownload(client, fileEntries, options, cb) { + if(!this.isEnabled()) { + return cb(notEnabledError()); + } + + const batchId = moment().utc().unix(); + const hashId = this.getBatchArchiveHashId(client, batchId); + const url = this.buildBatchArchiveTempDownloadLink(client, hashId); + options.expireTime = options.expireTime || moment().add(2, 'days'); + + FileDb.beginTransaction( (err, trans) => { + if(err) { + return cb(err); + } + + this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => { if(err) { - return cb(err); + return trans.rollback( () => { + return cb(err); + }); } - this.scheduleExpire(hashId, options.expireTime); - - return cb(null, url); - } - ); + async.eachSeries(fileEntries, (entry, nextEntry) => { + trans.run( + `INSERT INTO file_web_serve_batch (hash_id, file_id) + VALUES (?, ?);`, + [ hashId, entry.fileId ], + err => { + return nextEntry(err); + } + ); + }, err => { + trans[err ? 'rollback' : 'commit']( () => { + return cb(err, url); + }); + }); + }); + }); } fileNotFound(resp) { return this.webServer.instance.fileNotFound(resp); } - routeWebRequestForFile(req, resp) { + routeWebRequest(req, resp) { const hashId = paths.basename(req.url); this.loadServedHashId(hashId, (err, servedItem) => { @@ -261,44 +290,157 @@ class FileAreaWebAccess { return this.fileNotFound(resp); } - const fileEntry = new FileEntry(); - fileEntry.load(servedItem.fileId, err => { + const hashIdTypes = FileAreaWebAccess.getHashIdTypes(); + switch(servedItem.hashIdType) { + case hashIdTypes.SingleFile : + return this.routeWebRequestForSingleFile(servedItem, req, resp); + + case hashIdTypes.BatchArchive : + return this.routeWebRequestForBatchArchive(servedItem, req, resp); + + default : + return this.fileNotFound(resp); + } + }); + } + + routeWebRequestForSingleFile(servedItem, req, resp) { + const fileEntry = new FileEntry(); + + servedItem.fileId = servedItem.fileIds[0]; + + 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); } - const filePath = fileEntry.filePath; - if(!filePath) { + resp.on('close', () => { + // connection closed *before* the response was fully sent + // :TODO: Log and such + }); + + resp.on('finish', () => { + // transfer completed fully + this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size); + }); + + const headers = { + 'Content-Type' : mimeTypes.contentType(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); + }); + }); + } + + routeWebRequestForBatchArchive(servedItem, req, resp) { + // + // We are going to build an on-the-fly zip file stream of 1:n + // files in the batch. + // + // First, collect all file IDs + // + const self = this; + + async.waterfall( + [ + function fetchFileIds(callback) { + FileDb.all( + `SELECT file_id + FROM file_web_serve_batch + WHERE hash_id = ?;`, + [ servedItem.hashId ], + (err, fileIdRows) => { + if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { + return callback(Errors.DoesNotExist('Could not get file IDs for batch')); + } + + return callback(null, fileIdRows.map(r => r.file_id)); + } + ); + }, + function loadFileEntries(fileIds, callback) { + const filePaths = []; + async.eachSeries(fileIds, (fileId, nextFileId) => { + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + if(!err) { + filePaths.push(fileEntry.filePath); + } + return nextFileId(err); + }); + }, err => { + if(err) { + return callback(Errors.DoesNotExist('Coudl not load file IDs for batch')); + } + + return callback(null, filePaths); + }); + }, + function createAndServeStream(filePaths, callback) { + const zipFile = new yazl.ZipFile(); + + filePaths.forEach(fp => { + zipFile.addFile( + fp, // path to physical file + paths.basename(fp), // filename/path *stored in archive* + { + compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. + } + ); + }); + + zipFile.end( finalZipSize => { + if(-1 === finalZipSize) { + return callback(Errors.UnexpectedState('Unable to acquire final zip size')); + } + + resp.on('close', () => { + // connection closed *before* the response was fully sent + // :TODO: Log and such + }); + + resp.on('finish', () => { + // transfer completed fully + self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize); + }); + + const batchFileName = `batch_${servedItem.hashId}.zip`; + + const headers = { + 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), + 'Content-Length' : finalZipSize, + 'Content-Disposition' : `attachment; filename="${batchFileName}"`, + }; + + resp.writeHead(200, headers); + return zipFile.outputStream.pipe(resp); + }); + } + ], + err => { + if(err) { + // :TODO: Log me! 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 - this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size); - }); - - const headers = { - 'Content-Type' : mimeTypes.contentType(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); - }); - }); - }); + // ...otherwise, we would have called resp() already. + } + ); } updateDownloadStatsForUserIdAndSystem(userId, dlBytes, cb) { diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index 812a2422..382a7305 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -9,10 +9,12 @@ 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 FileAreaWeb = require('../core/file_area_web.js'); // deps const async = require('async'); const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { name : 'File Base Download Queue Manager', @@ -22,17 +24,15 @@ exports.moduleInfo = { const FormIds = { queueManager : 0, - details : 1, }; const MciViewIds = { queueManager : { - queue : 1, - navMenu : 2, - }, - details : { + queue : 1, + navMenu : 2, - } + customRangeStart : 10, + }, }; exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { @@ -126,6 +126,26 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return cb(null); } + displayWebDownloadLinkForFileEntry(fileEntry) { + FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => { + if(serveItem && serveItem.url) { + const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + + fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; + fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + } else { + fileEntry.webDlLink = ''; + fileEntry.webDlExpire = ''; + } + + this.updateCustomViewTextsWithFilter( + 'queueManager', + MciViewIds.queueManager.customRangeStart, fileEntry, + { filter : [ '{webDlLink}', '{webDlExpire}' ] } + ); + }); + } + updateDownloadQueueView(cb) { const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); if(!queueView) { @@ -138,7 +158,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); + queueView.on('index update', idx => { + const fileEntry = this.dlQueue.items[idx]; + this.displayWebDownloadLinkForFileEntry(fileEntry); + }); + queueView.redraw(); + this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]); return cb(null); } diff --git a/mods/file_base_web_download_manager.js b/mods/file_base_web_download_manager.js new file mode 100644 index 00000000..d171cfdb --- /dev/null +++ b/mods/file_base_web_download_manager.js @@ -0,0 +1,287 @@ +/* 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'); +const FileAreaWeb = require('../core/file_area_web.js'); +const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; +const Config = require('../core/config.js').config; + +// deps +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); + +exports.moduleInfo = { + name : 'File Base Download Web Queue Manager', + desc : 'Module for interacting with web backed download queue/batch', + author : 'NuSkooler', +}; + +const FormIds = { + queueManager : 0 +}; + +const MciViewIds = { + queueManager : { + queue : 1, + navMenu : 2, + + customRangeStart : 10, + } +}; + +exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { + + constructor(options) { + super(options); + + this.dlQueue = new DownloadQueue(this.client); + + this.menuMethods = { + removeItem : (formData, extraArgs, cb) => { + const selectedItem = this.dlQueue.items[formData.value.queueItem]; + if(!selectedItem) { + return cb(null); + } + + this.dlQueue.removeItems(selectedItem.fileId); + + // :TODO: broken: does not redraw menu properly - needs fixed! + return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); + }, + clearQueue : (formData, extraArgs, cb) => { + this.dlQueue.clear(); + + // :TODO: broken: does not redraw menu properly - needs fixed! + return this.removeItemsFromDownloadQueueView('all', cb); + }, + getBatchLink : (formData, extraArgs, cb) => { + return this.generateAndDisplayBatchLink(cb); + } + }; + } + + initSequence() { + if(0 === this.dlQueue.items.length) { + return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); + } + + const self = this; + + async.series( + [ + function beforeArt(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayQueueManagerPage(false, callback); + } + ], + () => { + return self.finishedLoading(); + } + ); + } + + removeItemsFromDownloadQueueView(itemIndex, cb) { + const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); + if(!queueView) { + return cb(Errors.DoesNotExist('Queue view does not exist')); + } + + if('all' === itemIndex) { + queueView.setItems([]); + queueView.setFocusItems([]); + } else { + queueView.removeItem(itemIndex); + } + + queueView.redraw(); + return cb(null); + } + + displayFileInfoForFileEntry(fileEntry) { + this.updateCustomViewTextsWithFilter( + 'queueManager', + MciViewIds.queueManager.customRangeStart, fileEntry, + { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... + ); + } + + updateDownloadQueueView(cb) { + const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); + if(!queueView) { + return cb(Errors.DoesNotExist('Queue view does not exist')); + } + + const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; + const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; + + queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); + queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); + + queueView.on('index update', idx => { + const fileEntry = this.dlQueue.items[idx]; + this.displayFileInfoForFileEntry(fileEntry); + }); + + queueView.redraw(); + this.displayFileInfoForFileEntry(this.dlQueue.items[0]); + + return cb(null); + } + + generateAndDisplayBatchLink(cb) { + const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); + + FileAreaWeb.createAndServeTempBatchDownload( + this.client, + this.dlQueue.items, + { + expireTime : expireTime + }, + (err, webBatchDlLink) => { + // :TODO: handle not enabled -> display such + if(err) { + return cb(err); + } + + const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + + const formatObj = { + webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, + webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), + }; + + this.updateCustomViewTextsWithFilter( + 'queueManager', + MciViewIds.queueManager.customRangeStart, + formatObj, + { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } + ); + + return cb(null); + } + ); + } + + displayQueueManagerPage(clearScreen, cb) { + const self = this; + + async.series( + [ + function prepArtAndViewController(callback) { + return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + }, + function prepareQueueDownloadLinks(callback) { + const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + + async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => { + FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { + if(err) { + if(ErrNotEnabled === err.reasonCode) { + return nextFileEntry(err); // we should have caught this prior + } + + const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); + + FileAreaWeb.createAndServeTempDownload( + self.client, + fileEntry, + { expireTime : expireTime }, + (err, url) => { + if(err) { + return nextFileEntry(err); + } + + fileEntry.webDlLinkRaw = url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); + + return nextFileEntry(null); + } + ); + } else { + fileEntry.webDlLinkRaw = serveItem.url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; + fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + return nextFileEntry(null); + } + }); + }, err => { + return callback(err); + }); + }, + function populateViews(callback) { + return self.updateDownloadQueueView(callback); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + + displayArtAndPrepViewController(name, options, cb) { + const self = this; + const config = this.menuConfig.config; + + async.waterfall( + [ + function readyAndDisplayArt(callback) { + if(options.clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } + + theme.displayThemedAsset( + config.art[name], + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function prepeareViewController(artData, callback) { + if(_.isUndefined(self.viewControllers[name])) { + const vcOpts = { + client : self.client, + formId : FormIds[name], + }; + + if(!_.isUndefined(options.noInput)) { + vcOpts.noInput = options.noInput; + } + + const vc = self.addViewController(name, new ViewController(vcOpts)); + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } + + self.viewControllers[name].setFocus(true); + return callback(null); + + }, + ], + err => { + return cb(err); + } + ); + } +}; + \ No newline at end of file diff --git a/package.json b/package.json index ef9da900..224fda84 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "temptmp": "^1.0.0", "uuid": "^3.1.0", "uuid-parse": "^1.0.0", - "ws": "^3.1.0" + "ws": "^3.1.0", + "yazl" : "^2.4.2" }, "devDependencies": {}, "engines": { From 38b9bf2c30687ac239c18efd914c567a1791189f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 28 Sep 2017 21:34:46 -0600 Subject: [PATCH 0035/1013] Fix typo in persistHashTag() --- 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 84c6559f..32903315 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -280,7 +280,7 @@ module.exports = class FileEntry { } transOrDb.serialize( () => { - fileDb.run( + transOrDb.run( `INSERT OR IGNORE INTO hash_tag (hash_tag) VALUES (?);`, [ hashTag ] From e84920e508ab9c382b005cf5c8a7c06555b8c04d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 28 Sep 2017 21:34:58 -0600 Subject: [PATCH 0036/1013] Add web d/l manager to menu.hjson --- mods/menu.hjson | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/mods/menu.hjson b/mods/menu.hjson index 08202ca6..0dffdcbb 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -2934,6 +2934,64 @@ } } + fileBaseWebDownloadManager: { + desc: Web D/L Manager + module: file_base_web_download_manager + config: { + art: { + queueManager: FWDLMGR + batchList: BATDLINF + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "get batch link", "quit", "help" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:getBatchLink + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "b", "shift + b" ] + action: @method:getBatchLink + } + { + 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 From efee0eafdee6950aaf0ea6a501671ec11d821ba2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Sep 2017 16:30:05 -0600 Subject: [PATCH 0037/1013] Minor document updates --- docs/menu_system.md | 32 +++++++++++++++++++++----------- docs/modding.md | 12 ++++++++++++ docs/msg_networks.md | 17 ++++++++++++----- 3 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 docs/modding.md diff --git a/docs/menu_system.md b/docs/menu_system.md index 1dea637c..026ef648 100644 --- a/docs/menu_system.md +++ b/docs/menu_system.md @@ -1,17 +1,26 @@ # 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. +ENiGMA½'s menu system is highly flexible and moddable. The possibilities are almost endless! + +This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` (or whatever you reference in `config.hjson` using the `menuFile` property — see below). By modifying your `menu.hjson` you will be able to create a custom experience unique to your board. 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 + menuFile: yourboardname.hjson } ``` -(You can start by copying the default `menu.hjson` to `mybbs.hjson`) + +You can start by copying the default `mods/menu.hjson` to `yourboardname.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`. +Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. + +Entries in `menu.hjson` are objects defining a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: +* Classical Main, Messages, and File menus +* Art file display +* Module driven menus such as door launchers + Each entry in `menu.hjson` defines an object that represents a menu. These objects live within the `menus` parent object. Each object's *key* is a menu name you can reference within other menus in the system. @@ -26,9 +35,9 @@ telnetConnected: { } ``` -The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the server's config). +The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). -An art pattern of `CONNECT` is set telling the system to look for `CONNECT` in the current theme location, then in the common `mods/art` directory where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. You can be explicit here if desired, by specifying a file extension. +An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`. @@ -47,20 +56,21 @@ matrix: { submit: true focus: true items: [ "login", "apply", "log off" ] + argName: matrixSubmit } } submit: { *: [ { - value: { 1: 0 } + value: { matrixSubmit: 0 } action: @menu:login } { - value: { 1: 1 }, + value: { matrixSubmit: 1 }, action: @menu:newUserApplication } { - value: { 1: 2 }, + value: { matrixSubmit: 2 }, action: @menu:logoff } ] @@ -71,6 +81,6 @@ matrix: { } ``` -In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined. +In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` for this action as `matrixSubmit`. -The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ 1: 0 }` or view ID 1, value 0 will match causing `action` of `@menu:login` to be executed (go to `login` menu). \ No newline at end of file +The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go to `login` menu). diff --git a/docs/modding.md b/docs/modding.md new file mode 100644 index 00000000..5dcf5a29 --- /dev/null +++ b/docs/modding.md @@ -0,0 +1,12 @@ +# Modding + +## General Configuraiton +See [Configuration](config.md) + +## Menus +See [Menu System](menu_system.md) + +## Theming +Take a look at how the default `luciano_blocktronics` theme found under `mods/themes` works! + +TODO document me! \ No newline at end of file diff --git a/docs/msg_networks.md b/docs/msg_networks.md index ca0afe17..1bb46c8c 100644 --- a/docs/msg_networks.md +++ b/docs/msg_networks.md @@ -6,7 +6,7 @@ Message networks are configured in `messageNetworks` section of `config.hjson`. * `originLine` (optional): Overrwrite the default origin line for networks that support it. For example: `originLine: Xibalba - xibalba.l33t.codes:44510` ## FidoNet Technology Network (FTN) -FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`. +FTN networks are configured under the `messageNetworks.ftn` section of `config.hjson`. ### Networks The `networks` section contains a sub section for network(s) you wish you join your board with. Each entry's key name can be referenced elsewhere in `config.hjson` for FTN oriented configurations. @@ -30,7 +30,7 @@ The `networks` section contains a sub section for network(s) you wish you join y ``` ### Areas -The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. +The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages (In fact you can import AREAS.BBS using `oputil.js`!) When importing, messages will be placed in the local area that matches key under `areas`. @@ -57,11 +57,11 @@ When importing, messages will be placed in the local area that matches key under ``` ### BSO Import / Export -The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers::ftn_bso`. +The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. **Members**: * `defaultZone` (required): Sets the default BSO outbound zone - * `defaultNetwork` (optional): Sets the default network name from `messageNetworks::ftn::networks`. **Required if more than one network is defined**. + * `defaultNetwork` (optional): Sets the default network name from `messageNetworks.ftn.networks`. **Required if more than one network is defined**. * `paths` (optional): Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`. * `packetTargetByteSize` (optional): Overrides the system *target* packet (.pkt) size of 512000 bytes (512k) * `bundleTargetByteSize` (optional): Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M) @@ -85,7 +85,7 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config. scannerTossers: { ftn_bso: { nodes: { - "46:*: { + "46:*": { packetType: 2+ packetPassword: mypass encoding: cp437 @@ -97,6 +97,13 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config. } ``` +#### TIC Support +ENiGMA½ supports TIC files. This is handled by mapping TIC areas to local file areas. + +Under a given node (described above) TIC configuration may be supplied. + +TODO + #### Scheduling Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers. From b6e628014a5fbdc53a7dfe31e36a8f57cc139900 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Sep 2017 16:45:59 -0600 Subject: [PATCH 0038/1013] More doc updates --- docs/config.md | 26 +++++++++++++++----------- docs/modding.md | 5 ++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/config.md b/docs/config.md index ca760676..ff653e37 100644 --- a/docs/config.md +++ b/docs/config.md @@ -6,11 +6,16 @@ The main system configuration file, `config.hjson` both overrides defaults and p **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. +### Creating a Configuration +Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: +``` +./oputil.js config new +``` -### Example: System Name -`core/config.js` provides the default system name as follows: +You will be asked a series of questions to create an initial configuration. + +### Overriding Defaults +The file `core/config.js` provides various defaults to the system that you can override via `config.hjson`. For example, the default system name is defined as follows: ```javascript general : { boardName : 'Another Fine ENiGMA½ System' @@ -26,17 +31,14 @@ general: { (Note the very slightly different syntax. **You can use standard JSON if you wish**) +While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)! + ### Specific Areas of Interest -* [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 Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked. @@ -125,5 +127,7 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all } ``` -## Menus -See [the menu system docs](menu_system.md) \ No newline at end of file +## See Also +* [Modding](modding.md) +* [Doors](doors.md) +* [MCI Codes](mci.md) diff --git a/docs/modding.md b/docs/modding.md index 5dcf5a29..9449a91e 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -9,4 +9,7 @@ See [Menu System](menu_system.md) ## Theming Take a look at how the default `luciano_blocktronics` theme found under `mods/themes` works! -TODO document me! \ No newline at end of file +TODO document me! + +## Add-On Modules +See [Mods](mods.md) \ No newline at end of file From 8b7cf1f2100f1c297a09f67b16767e494df66beb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Sep 2017 19:43:22 -0600 Subject: [PATCH 0039/1013] Add extra logging around TIC processing --- core/scanner_tossers/ftn_bso.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 039caac7..65009333 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1289,6 +1289,7 @@ function FTNMessageScanTossModule() { return ticFileInfo.validate(config, (err, localInfo) => { if(err) { + Log.trace( { reason : err.message }, 'Validation failure'); return callback(err); } @@ -1349,10 +1350,17 @@ function FTNMessageScanTossModule() { localInfo.existingFileId = fileIds[0]; // fetch old filename - we may need to remove it if replacing with a new name - FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (cb, info) => { - localInfo.oldFileName = info.fileName; - localInfo.oldStorageTag = info.storageTag; - return callback(null, localInfo); + FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (err, info) => { + if(info) { + Log.trace( + { fileId : localInfo.existingFileId, oldFileName : info.fileName, oldStorageTag : info.storageTag }, + 'Existing TIC file target to be replaced' + ); + + localInfo.oldFileName = info.fileName; + localInfo.oldStorageTag = info.storageTag; + } + return callback(null, localInfo); // continue even if we couldn't find an old match }); } else if(fileIds.legnth > 1) { return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`)); @@ -1397,6 +1405,10 @@ function FTNMessageScanTossModule() { ticFileInfo.filePath, scanOpts, (err, fileEntry) => { + if(err) { + Log.trace( { reason : err.message }, 'Scanning failed'); + } + localInfo.fileEntry = fileEntry; return callback(err, localInfo); } @@ -1441,6 +1453,7 @@ function FTNMessageScanTossModule() { self.copyTicAttachment(ticFileInfo.filePath, dst, isUpdate, (err, finalPath) => { if(err) { + Log.info( { reason : err.message }, 'Failed to copy TIC attachment'); return callback(err); } @@ -1474,7 +1487,7 @@ function FTNMessageScanTossModule() { ], (err, localInfo) => { if(err) { - Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.path }, 'Failed import/update TIC record' ); + Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed import/update TIC record' ); } else { Log.debug( { tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag }, From 5cbbd76411ccfd4d7cef20a194f8595eaaedb8a0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 30 Sep 2017 12:34:10 -0600 Subject: [PATCH 0040/1013] Updates to oputil when --update with desc/descLong --- core/oputil/oputil_file_base.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index d9ad9366..0c4c6065 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -172,10 +172,10 @@ function scanFileAreaForChanges(areaInfo, options, cb) { // // We'll update the entry if the following conditions are met: // * We have a single duplicate, and: - // * --update-desc was passed or the existing entry's desc or + // * --update was passed or the existing entry's desc or // longDesc are blank/empty // - if(argv['update'] && 1 === dupeEntries.length) { + if(argv.update && 1 === dupeEntries.length) { const FileEntry = require('../../core/file_entry.js'); const existingEntry = new FileEntry(); @@ -200,6 +200,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { } console.info('Dupe (updating)'); + existingEntry.desc = fileEntry.desc; + existingEntry.descLong = fileEntry.descLong; updateTags(existingEntry); finalizeEntryAndPersist(true, existingEntry, descHandler, err => { From 1ad7ce848a962d4b6cdd897dbd6b7d4e36f94a42 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 30 Sep 2017 15:37:46 -0600 Subject: [PATCH 0041/1013] Upgrade farmhash to v2.0.4+ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 224fda84..f23ece19 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "farmhash": "1.2.1", + "farmhash": "^2.0.4", "fs-extra": "^4.0.1", "gaze": "^1.1.2", "graceful-fs": "^4.1.11", From af52ed6153327dd9a82a4bd639e066aa75fb2442 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 1 Oct 2017 11:07:49 -0600 Subject: [PATCH 0042/1013] Better handling of TIC import descriptions * Add descPriority config option (default='diz') * Really prefer diz/ldesc over *generated* descriptions e.g. from filename or info extractors --- core/config.js | 1 + core/file_base_area.js | 12 ++++++++---- core/scanner_tossers/ftn_bso.js | 26 +++++++++++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/config.js b/core/config.js index 549b44a2..e33648b9 100644 --- a/core/config.js +++ b/core/config.js @@ -639,6 +639,7 @@ function getDefaultConfig() { secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) allowReplace : false, // use "Replaces" TIC field + descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc } } }, diff --git a/core/file_base_area.js b/core/file_base_area.js index 1d1062a9..f3845cc1 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -356,7 +356,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { // 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'); + fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); + fileEntry[`${descType}Src`] = 'descFile'; return next(null); }); }); @@ -404,7 +405,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries function processSingleExtractedFile(extractedFile, callback) { populateFileEntryInfoFromFile(fileEntry, extractedFile, err => { if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; } return callback(err); }); @@ -529,7 +531,8 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n'); } - fileEntry[key] = stdout; + fileEntry[key] = stdout; + fileEntry[`${key}Src`] = 'infoTool'; } } @@ -551,7 +554,8 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb function getDescriptions(callback) { populateFileEntryInfoFromFile(fileEntry, filePath, err => { if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; } return callback(err); }); diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 65009333..7e6e924b 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1321,7 +1321,7 @@ function FTNMessageScanTossModule() { // Lastly, we will only replace if the item is in the same/specified area // and that come from the same origin as a previous entry. // - const allowReplace = _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ] ) || Config.scannerTossers.ftn_bso.tic.allowReplace; + const allowReplace = _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config.scannerTossers.ftn_bso.tic.allowReplace); const replaces = ticFileInfo.getAsString('Replaces'); if(!allowReplace || !replaces) { @@ -1377,7 +1377,7 @@ function FTNMessageScanTossModule() { short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name tic_origin : ticFileInfo.getAsString('Origin'), tic_desc : ticFileInfo.getAsString('Desc'), - upload_by_username : _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ]) || Config.scannerTossers.ftn_bso.tic.uploadBy, + upload_by_username : _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config.scannerTossers.ftn_bso.tic.uploadBy), } }; @@ -1432,9 +1432,25 @@ function FTNMessageScanTossModule() { localInfo.fileEntry.areaTag = localInfo.areaTag; localInfo.fileEntry.fileName = ticFileInfo.longFileName; - // we default to .DIZ/etc. desc, but use from TIC if needed - if(!localInfo.fileEntry.desc || 0 === localInfo.fileEntry.desc.length) { - localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || ticFileInfo.getAsString('Desc') || getDescFromFileName(ticFileInfo.filePath); + // + // We may now have two descriptions: from .DIZ/etc. or the TIC itself. + // Determine which one to use using |descPriority| and availability. + // + // We will still fallback as needed from -> -> + // + const descPriority = _.get( + Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], + Config.scannerTossers.ftn_bso.tic.descPriority + ); + + if('tic' === descPriority) { + const origDesc = localInfo.fileEntry.desc; + localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || origDesc || getDescFromFileName(ticFileInfo.filePath); + } else { + // see if we got desc from .DIZ/etc. + const fromDescFile = 'descFile' === localInfo.fileEntry.descSrc; + localInfo.fileEntry.desc = fromDescFile ? localInfo.fileEntry.desc : ticFileInfo.getAsString('Ldesc'); + localInfo.fileEntry.desc = localInfo.fileEntry.desc || getDescFromFileName(ticFileInfo.filePath); } const areaStorageDir = getAreaStorageDirectoryByTag(storageTag); From 067bb9e884db00d2462ef4b22e52edbf61422441 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 2 Oct 2017 21:06:53 -0600 Subject: [PATCH 0043/1013] * Switch to sane over gaze for file watching: Gaze was not triggering on file additions * Remove watching of config files for now -- doesn't work anyway. Will revisit later. --- core/config_cache.js | 12 ++++++------ core/event_scheduler.js | 28 +++++++++++++++++++--------- core/scanner_tossers/ftn_bso.js | 18 +++++++++++++----- package.json | 9 +++++---- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/core/config_cache.js b/core/config_cache.js index 9df08316..8b57e125 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -6,7 +6,6 @@ var Log = require('./logger.js').log; var paths = require('path'); var fs = require('graceful-fs'); -var Gaze = require('gaze').Gaze; var events = require('events'); var util = require('util'); var assert = require('assert'); @@ -18,7 +17,7 @@ function ConfigCache() { var self = this; this.cache = {}; // filePath -> HJSON - this.gaze = new Gaze(); + //this.gaze = new Gaze(); this.reCacheConfigFromFile = function(filePath, cb) { fs.readFile(filePath, { encoding : 'utf-8' }, function fileRead(err, data) { @@ -32,7 +31,7 @@ function ConfigCache() { }); }; - +/* this.gaze.on('error', function gazeErr(err) { }); @@ -50,6 +49,7 @@ function ConfigCache() { } }); }); + */ } @@ -58,13 +58,13 @@ util.inherits(ConfigCache, events.EventEmitter); ConfigCache.prototype.getConfigWithOptions = function(options, cb) { assert(_.isString(options.filePath)); - var self = this; +// var self = this; var isCached = (options.filePath in this.cache); if(options.forceReCache || !isCached) { this.reCacheConfigFromFile(options.filePath, function fileCached(err, config) { if(!err && !isCached) { - self.gaze.add(options.filePath); + //self.gaze.add(options.filePath); } cb(err, config, true); }); @@ -82,4 +82,4 @@ ConfigCache.prototype.getModConfig = function(fileName, cb) { this.getConfig(paths.join(Config.paths.mods, fileName), cb); }; -module.exports = exports = new ConfigCache(); \ No newline at end of file +module.exports = exports = new ConfigCache(); diff --git a/core/event_scheduler.js b/core/event_scheduler.js index a1c2f6ef..27c68b37 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -10,8 +10,9 @@ const _ = require('lodash'); const later = require('later'); const path = require('path'); const pty = require('ptyw.js'); -const gaze = require('gaze'); +const sane = require('sane'); const moment = require('moment'); +const paths = require('path'); exports.getModule = EventSchedulerModule; exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart @@ -209,13 +210,13 @@ EventSchedulerModule.prototype.startup = function(cb) { Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); return; } - + Log.debug( { eventName : schedEvent.name, schedule : this.moduleConfig.events[schedEvent.name].schedule, action : schedEvent.action, - next : moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') + next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', }, 'Scheduled event loaded' ); @@ -226,12 +227,21 @@ EventSchedulerModule.prototype.startup = function(cb) { }, schedEvent.schedule.sched)); } - if(schedEvent.schedule.watchFile) { - gaze(schedEvent.schedule.watchFile, (err, watcher) => { - // :TODO: should track watched files & stop watching @ shutdown - watcher.on('all', (watchEvent, watchedPath) => { - if(schedEvent.schedule.watchFile === watchedPath) { - self.performAction(schedEvent, `Watch file: ${watchedPath}`); + if(schedEvent.schedule.watchFile) { + const watcher = sane( + paths.dirname(schedEvent.schedule.watchFile), + { + glob : `**/${paths.basename(schedEvent.schedule.watchFile)}` + } + ); + + // :TODO: should track watched files & stop watching @ shutdown? + + [ 'change', 'add', 'delete' ].forEach(event => { + watcher.on(event, (fileName, fileRoot) => { + const eventPath = paths.join(fileRoot, fileName); + if(schedEvent.schedule.watchFile === eventPath) { + self.performAction(schedEvent, `Watch file: ${eventPath}`); } }); }); diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 039caac7..598667e8 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -30,7 +30,7 @@ const fs = require('graceful-fs'); const later = require('later'); const temptmp = require('temptmp').createTrackedSession('ftn_bso'); const assert = require('assert'); -const gaze = require('gaze'); +const sane = require('sane'); const fse = require('fs-extra'); const iconv = require('iconv-lite'); const uuidV4 = require('uuid/v4'); @@ -1654,10 +1654,18 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { } if(_.isString(importSchedule.watchFile)) { - gaze(importSchedule.watchFile, (err, watcher) => { - watcher.on('all', (event, watchedPath) => { - if(importSchedule.watchFile === watchedPath) { - tryImportNow(`Performing import/toss due to @watch: ${watchedPath} (${event})`); + const watcher = sane( + paths.dirname(importSchedule.watchFile), + { + glob : `**/${paths.basename(importSchedule.watchFile)}` + } + ); + + [ 'change', 'add', 'delete' ].forEach(event => { + watcher.on(event, (fileName, fileRoot) => { + const eventPath = paths.join(fileRoot, fileName); + if(paths.join(fileRoot, fileName) === importSchedule.watchFile) { + tryImportNow(`Performing import/toss due to @watch: ${eventPath} (${event})`); } }); }); diff --git a/package.json b/package.json index ec508841..5017536e 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "binary": "0.3.x", "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.10", + "exiftool": "^0.0.3", "farmhash": "^1.2.1", "fs-extra": "^3.0.1", "gaze": "^1.1.2", + "graceful-fs": "^4.1.11", "hashids": "^1.1.1", "hjson": "^2.4.2", "iconv-lite": "^0.4.17", @@ -35,18 +37,17 @@ "mime-types": "^2.1.15", "minimist": "1.2.x", "moment": "^2.18.1", + "node-glob": "^1.2.0", "nodemailer": "^4.0.1", "ptyw.js": "NuSkooler/ptyw.js", + "sane": "^2.2.0", "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.1", "ssh2": "^0.5.5", "temptmp": "^1.0.0", "uuid": "^3.0.1", "uuid-parse": "^1.0.0", - "ws" : "^3.0.0", - "graceful-fs" : "^4.1.11", - "exiftool" : "^0.0.3", - "node-glob" : "^1.2.0" + "ws": "^3.0.0" }, "devDependencies": {}, "engines": { From 8ead65c1ff1b6fae353d69f0f06b7333c86d48ac Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 2 Oct 2017 21:10:52 -0600 Subject: [PATCH 0044/1013] Remvoe gaze package --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 5017536e..e00d9211 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "exiftool": "^0.0.3", "farmhash": "^1.2.1", "fs-extra": "^3.0.1", - "gaze": "^1.1.2", "graceful-fs": "^4.1.11", "hashids": "^1.1.1", "hjson": "^2.4.2", From 7150631fb76667acead1776a2ebcd2b380f18527 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 2 Oct 2017 21:11:22 -0600 Subject: [PATCH 0045/1013] Remvoe gaze package --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 5017536e..e00d9211 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "exiftool": "^0.0.3", "farmhash": "^1.2.1", "fs-extra": "^3.0.1", - "gaze": "^1.1.2", "graceful-fs": "^4.1.11", "hashids": "^1.1.1", "hjson": "^2.4.2", From 0bef268276a15fe8be12dfbdd577e5cd10bd1cb4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 2 Oct 2017 21:28:32 -0600 Subject: [PATCH 0046/1013] Issue #122: If watch file exists at startup, kick off task/schedule --- core/event_scheduler.js | 7 +++++++ core/scanner_tossers/ftn_bso.js | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 27c68b37..8b3d3239 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -13,6 +13,7 @@ const pty = require('ptyw.js'); const sane = require('sane'); const moment = require('moment'); const paths = require('path'); +const fse = require('fs-extra'); exports.getModule = EventSchedulerModule; exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart @@ -245,6 +246,12 @@ EventSchedulerModule.prototype.startup = function(cb) { } }); }); + + fse.exists(schedEvent.schedule.watchFile, exists => { + if(exists) { + self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`); + } + }); } }); } diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 598667e8..57ff0525 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1669,6 +1669,16 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { } }); }); + + // + // If the watch file already exists, kick off now + // https://github.com/NuSkooler/enigma-bbs/issues/122 + // + fse.exists(importSchedule.watchFile, exists => { + if(exists) { + tryImportNow(`Performing import/toss due to @watch: ${importSchedule.watchFile} (initial exists)`); + } + }); } } } From efd1ea28df5ef0f11428b3d2e3b9591d188c7e44 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Fri, 27 Oct 2017 16:54:02 +0200 Subject: [PATCH 0047/1013] Support npm start Fill-in the missing pieces in the package.json file. From now on `npm start` just works like all other nodejs apps. --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index e00d9211..e1589314 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,10 @@ "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", + "main": "./core/bbs", + "scripts": { + "start": "node main" + }, "repository": { "type": "git", "url": "https://github.com/NuSkooler/enigma-bbs.git" From 4092ecd2b23dc4180a48feebb66a6c4461d10111 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Fri, 27 Oct 2017 17:12:45 +0200 Subject: [PATCH 0048/1013] Show it's possible to change the telnet default port In my case the default port (8888) was already in use. However, it was very hard to know (more guessing) the port change in possible via telnet: { port: xxxx }. --- docs/index.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1147c578..9fbb0cee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -77,9 +77,12 @@ Below is an _example_ configuration. It is recommended that you at least **start loginServers: { ssh: { - privateKeyPass: YOUR_PK_PASS - enabled: true /* set to false to disable the SSH server */ - } + privateKeyPass: YOUR_PK_PASS + enabled: true /* set to false to disable the SSH server */ + } + telnet: { + port: 8888 + } } messageConferences: { @@ -87,15 +90,14 @@ Below is an _example_ configuration. It is recommended that you at least **start name: Local desc: Local Discussions default: true - - areas: { - local_music: { + areas: { + local_music: { name: Music Discussion desc: Music, bands, etc. default: true + } } - } - } + } } } ``` From 0f9e9c75ed9747100f7219631a700dadf1efd33f Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Fri, 27 Oct 2017 17:59:45 +0200 Subject: [PATCH 0049/1013] Just add a link to the webchat For people who don't have a IRC client installed. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39ee0561..d7053333 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ 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!** (see Boards below) -* IRC: **#enigma-bbs** on **chat.freenode.net** +* IRC: **#enigma-bbs** on **chat.freenode.net** ([webchat](https://webchat.freenode.net/?channels=enigma-bbs)) * Discussion on [fsxNet](http://bbs.geek.nz/#fsxNet) available on many boards * Email: bryan -at- l33t.codes * [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/) From 051ef274281f169f2620eafb4a3d6e60cdce4a20 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Fri, 27 Oct 2017 21:43:10 +0200 Subject: [PATCH 0050/1013] Update package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index e1589314..fb04a0f4 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,8 @@ "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", - "main": "./core/bbs", "scripts": { - "start": "node main" + "start": "node main.js" }, "repository": { "type": "git", From 5672fa144b43e24ffdd0834797a1629232490551 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Sun, 29 Oct 2017 02:15:53 +0100 Subject: [PATCH 0051/1013] Add some extra begginers info I found this by digging into the code itself, since again oputil doesn't provide anything useful yet. --- docs/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.md b/docs/index.md index 9fbb0cee..ae30690c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,6 +107,11 @@ Below is an _example_ configuration. It is recommended that you at least **start ./main.js ``` + +*Note #1:* The first user who register/apply (user ID = 1) will be automatically be added to the sysop group. And thus becomes SysOp. + +*Note #2:* All data is stored by default in Sqlite3 database files within the `db` sub folder, including user data, messages, system logs and file meta data. + ## Monitoring Logs Logs are produced by Bunyan which outputs each entry as a JSON object. To tail logs in a colorized and pretty pretty format, issue the following command: From acc14483930e9561176ad3138c9b629d1e3d1fc5 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Sun, 29 Oct 2017 02:17:53 +0100 Subject: [PATCH 0052/1013] Update index.md Typo --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index ae30690c..73b45739 100644 --- a/docs/index.md +++ b/docs/index.md @@ -108,7 +108,7 @@ Below is an _example_ configuration. It is recommended that you at least **start ``` -*Note #1:* The first user who register/apply (user ID = 1) will be automatically be added to the sysop group. And thus becomes SysOp. +*Note #1:* The first user who register/apply (user ID = 1) will be automatically be added to the `sysops` group. And thus becomes SysOp. *Note #2:* All data is stored by default in Sqlite3 database files within the `db` sub folder, including user data, messages, system logs and file meta data. From ecc6562b7975b0eda60a9736883ff4583a717880 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:47:23 +0000 Subject: [PATCH 0053/1013] Add VTXClient docs --- docs/images/vtxclient.png | Bin 0 -> 130515 bytes docs/vtx_web_client.md | 87 ++++++++++++++++++++++++++++++++++++++ misc/vtx/vtx.html | 27 ++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 docs/images/vtxclient.png create mode 100644 docs/vtx_web_client.md create mode 100644 misc/vtx/vtx.html diff --git a/docs/images/vtxclient.png b/docs/images/vtxclient.png new file mode 100644 index 0000000000000000000000000000000000000000..99261ced646d1a1caf710aaca694d42ca47ce815 GIT binary patch literal 130515 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

7h<34FTU<$wruVgQ$ci`;FTlE>QiwiTBzZu8 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 + + + + + + + + + +

+
+ +
+
+ + \ No newline at end of file From 0ea36e4574dada7e46c2e6ad1a9984a11426f760 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:48:17 +0000 Subject: [PATCH 0054/1013] Updated Raspberry Pi docs --- docs/rpi.md | 49 +++++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/docs/rpi.md b/docs/rpi.md index 4794bbcf..f474e959 100644 --- a/docs/rpi.md +++ b/docs/rpi.md @@ -3,39 +3,32 @@ ENiGMA½ can run under your Linux / RPi installation! The following instructions should help get you started. ## Tested RPi Models -###Model A -Works, but fairly slow (Node itself is not the fastest on this device). May work better overlocked, etc. +### Model A +Works, but fairly slow when browsing message areas (Node itself is not the fastest on this device). May work better overlocked, etc. -###v2 Model B -Works well with default rasbian, follow the normal quickstart install procedure, except for installing nodejs. To install nodejs do the following: - - curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - - sudo apt-get install -y nodejs +### v2 Model B +Works well with Raspbian! Keep in mind, compiling the dependencies with `npm install` will take some time and appear to hang. Just be patient. -##Example Configuration: RPi Model A + Minibian +## Example Configuration: RPi Model A + Raspbian Stretch Lite ### Basic Instructions -1. Download and `dd` the Minibian .img file from https://minibianpi.wordpress.com/ to a SDCARD. Cards >= 16GB recommended. -2. After booting Minibian, expand your file system. See http://elinux.org/RPi_Resize_Flash_Partitions#Manually_resizing_the_SD_card_on_Raspberry_Pi for information. -3. Update & upgrade: `apt-get update && apt-get upgrade` -4. It is recommended that you install `sudo` and create an admin user: `apt-get install sudo`, `adduser `, `adduser sudo` (reboot & login as the user your just created) -5. We want to build dependencies with a updated version of GCC. The following works to install GCC 4.9 on Minibian "wheezy": -a. Update */etc/apt/sources.list* replacing all "wheezy" with "jessie" -b. `sudo apt-get update` -c. `sudo apt-get install gcc-4.9 g++-4.9` -d. Update */etc/apt/sources.list* reverting all "jessie" back to "wheezy" -e. `sudo apt-get update` -f. Update alternatives: `sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 60 --slave /usr/bin/g++ g++ /usr/bin/g++-4.9` -6. Install dependencies: `sudo apt-get install make python libicu-dev libssl-dev git` -7. Install the latest Node.js from here: http://node-arm.herokuapp.com/ (**only download the .dep and dpkg install it!**) -8. The RPi A has very low memory, we'll need a swap file: -a. `sudo dd if=/dev/zero of=tmpswap bs=1024 count=1M` -b. `sudo mkswap tmpswap` -c. `sudo swapon tmpswap` -9. Clone enigma-bbs.git -10. Install dependencies. Here we will force GCC 4.9 for compilation: `CC=gcc-4.9 npm install` -11. Follow generic setup for creating a config.hjson, etc. and you should be ready to go! +1. Download [Raspbian Stretch Lite](https://www.raspberrypi.org/downloads/raspbian/). Follow the instructions +on the [Raspbian site](https://www.raspberrypi.org/documentation/installation/installing-images/README.md) regarding how +to get it written to an SD card. + +2. Run `sudo raspi-config`, then: + 1. Set your timezone (option 4, option I2) + 2. Enable SSH (option 5, option P2) + 3. Expand the filesystem to use the entire SD card (option 7, option A1) + +3. Update & upgrade all packages: `apt-get update && apt-get upgrade` + +4. Install required packages: `sudo apt install lrzsz p7zip-full` + +5. Follow the [Quickstart](docs/index.md) instructions to install ENiGMA½. + +6. Profit! From 6c4745ee22ddd3a0b78e31a4db433021c9bb2290 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:49:08 +0000 Subject: [PATCH 0055/1013] Typo fixes --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1147c578..48fdc090 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ For Windows environments or if you simply like to do things manually, read on... ### New to Node -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!): +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 environments (Please consider the `install.sh` approach unless you really want to manually install!): ```bash curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash @@ -64,7 +64,7 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson` ./oputil.js config new ``` -(You wil be asked a series of basic questions) +(You will 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**. From 7f18f3d6140b8a6253cec45cd59469329c937527 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:50:25 +0000 Subject: [PATCH 0056/1013] Add note about LetsEncrypt to webserver docs --- docs/web_server.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/web_server.md b/docs/web_server.md index 8c341191..1f87cd80 100644 --- a/docs/web_server.md +++ b/docs/web_server.md @@ -1,8 +1,10 @@ # Web Server -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! +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`: +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: { @@ -16,12 +18,17 @@ contentServers: { } ``` -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: +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. [LetsEncrypt](https://letsencrypt.org/) supply free trusted +certificates that work perfectly with ENiGMA½. + +Once obtained, simply enable the HTTPS server: + ```hjson contentServers: { web: { domain: bbs.yourdomain.com - // set 'overrideUrlPrefix' if for example, you use a transparent proxy in front of ENiGMA and need to be explicit about URLs the system hands out + // set 'overrideUrlPrefix' if for example, you use a transparent proxy in front of ENiGMA and need to be explicit about URLs the system hands out overrideUrlPrefix: https://bbs.yourdomain.com https: { enabled: true @@ -37,4 +44,5 @@ contentServers: { 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`. +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 d26117e428bbf810f654831d827f0e72886e7619 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:51:04 +0000 Subject: [PATCH 0057/1013] Typo fix in file base docs --- docs/file_base.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/file_base.md b/docs/file_base.md index bfd802b9..893a93de 100644 --- a/docs/file_base.md +++ b/docs/file_base.md @@ -1,7 +1,7 @@ # File Bases 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 +## A Different Approach 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 (just areas!) From 40ba4f5075c326d1978c502d96735671e3f0b2e4 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:51:56 +0000 Subject: [PATCH 0058/1013] Add TIC packet info to msg_networks docs --- docs/msg_networks.md | 69 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/docs/msg_networks.md b/docs/msg_networks.md index 1bb46c8c..11fa9202 100644 --- a/docs/msg_networks.md +++ b/docs/msg_networks.md @@ -100,9 +100,74 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config. #### TIC Support ENiGMA½ supports TIC files. This is handled by mapping TIC areas to local file areas. -Under a given node (described above) TIC configuration may be supplied. +Under a given node (like the one configured above), TIC configuration may be supplied: + +```hjson +{ + scannerTossers: { + ftn_bso: { + nodes: { + "46:*": { + packetType: 2+ + packetPassword: mypass + encoding: cp437 + archiveType: zip + tic: { + password: TESTY-TEST + uploadBy: Agoranet TIC + allowReplace: true + } + } + } + } + } +} +``` + +You then need to configure the mapping between TIC areas you want to carry, and the file +base area for them to be tossed to. Start by creating a storage tag and file base, if you haven't +already: + +````hjson +fileBase: { + areaStoragePrefix: /home/bbs/file_areas/ + + storageTags: { + msg_network: "msg_network" + } + + areas: { + msgNetworks: { + name: Message Networks + desc: Message networks news & info + storageTags: [ + "msg_network" + ] + } + } +} + +```` +and then create the mapping between the TIC area and the file area created: + +````hjson +ticAreas: { + agn_node: { + areaTag: msgNetworks + hashTags: agoranet,nodelist + storageTag: msg_network + } + + agn_info: { + areaTag: msgNetworks + hashTags: agoranet,infopack + storageTag: msg_network + } +} + +```` +Multiple TIC areas can be mapped to a single file base area. -TODO #### Scheduling Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers. From 40e269fa8dea5d06f8e04e0b97c729eb8080f667 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Oct 2017 12:52:31 +0000 Subject: [PATCH 0059/1013] Fix alt text on ENiGMA logo image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3795dd30..0773f3fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ENiGMA½ BBS Software -![alt text](docs/images/enigma-bbs.png "ENiGMA½ BBS") +![ENiGMA½ BBS](docs/images/enigma-bbs.png "ENiGMA½ BBS") ENiGMA½ is a modern BBS software with a nostalgic flair! From c38acebafbf1030a10391eeda548a484114b5cb8 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Sun, 29 Oct 2017 20:18:40 +0100 Subject: [PATCH 0060/1013] Update index.md Moved the section + improve text. --- docs/index.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/index.md b/docs/index.md index 73b45739..548d08ee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,10 +107,14 @@ Below is an _example_ configuration. It is recommended that you at least **start ./main.js ``` +Read the Points of Interest below for more info. -*Note #1:* The first user who register/apply (user ID = 1) will be automatically be added to the `sysops` group. And thus becomes SysOp. - -*Note #2:* All data is stored by default in Sqlite3 database files within the `db` sub folder, including user data, messages, system logs and file meta data. +## Points of Interest +* **The first user you create via register/applying (user ID = 1) will be automatically be added to the `sysops` group. And thus becomes SysOp.** (aka root) +* Default port for Telnet is 8888 and for SSH 8889 + * Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired. +* All data is stored by default in Sqlite3 database files, within the `db` sub folder. Including user data, messages, system logs and file meta data. +* You may want to tail the logfile with Bunyan. See Monitoring Logs below. ## Monitoring Logs Logs are produced by Bunyan which outputs each entry as a JSON object. To tail logs in a colorized and pretty pretty format, issue the following command: @@ -119,12 +123,6 @@ Logs are produced by Bunyan which outputs each entry as a JSON object. To tail l ENiGMA½ does not produce much to standard out. See below for tailing the log file to see what's going on. -## Points of Interest -* Default ports are 8888 (Telnet) and 8889 (SSH) - * Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired. -* **The first user you create via applying is the SysOp** (aka root) -* You may want to tail the logfile with Bunyan. See Monitoring Logs above. - # Advanced Installation If you've become convinced you would like a "production" BBS running ENiGMA½ a more advanced installation may be in order. From 7b167b20778fbfc5cc4b51ba5d4e64f3e4735a7b Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Sun, 29 Oct 2017 20:39:12 +0100 Subject: [PATCH 0061/1013] Add missing menuFile hjson spec Add missing config hjson file item --- docs/config.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index ca760676..b0ce2e6e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -48,6 +48,7 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all { general: { boardName: A Sample BBS + menuFile: your_bbs.hjson # copy of menu.hjson file (and adapt to your needs) } defaults: { @@ -126,4 +127,4 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all ``` ## Menus -See [the menu system docs](menu_system.md) \ No newline at end of file +See [the menu system docs](menu_system.md) From 64a8d9fc4106ba0108a262c6ded5d2c430e29712 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Sun, 29 Oct 2017 20:43:31 +0100 Subject: [PATCH 0062/1013] Update index.md Add link to docs --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 548d08ee..b57fac84 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,7 +107,7 @@ Below is an _example_ configuration. It is recommended that you at least **start ./main.js ``` -Read the Points of Interest below for more info. +Read the Points of Interest below for more info. Also check-out all the other documentation files in the [docs](.) directory. ## Points of Interest * **The first user you create via register/applying (user ID = 1) will be automatically be added to the `sysops` group. And thus becomes SysOp.** (aka root) From e55b4aa50b99627ebb9f373014f56df2615e36a3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Oct 2017 20:03:33 -0600 Subject: [PATCH 0063/1013] Return event in unknownOption() --- core/servers/login/telnet.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 4377f029..06961800 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -88,6 +88,7 @@ const SB_COMMANDS = { // // Resources // * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html +// * http://www.networksorcery.com/enp/protocol/telnet.htm // const OPTIONS = { TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 @@ -186,6 +187,8 @@ const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { function unknownOption(bufs, i, event) { Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option'); + event.buf = bufs.splice(0, i).toBuffer(); + return event; } const OPTION_IMPLS = {}; From 2efc522d680feaf95fba0327d0d9857b54f438c5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Oct 2017 20:03:49 -0600 Subject: [PATCH 0064/1013] Mask out passwordConfirm --- core/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/logger.js b/core/logger.js index 3b0b47e2..f90aec41 100644 --- a/core/logger.js +++ b/core/logger.js @@ -62,7 +62,7 @@ module.exports = class Log { // Use a regexp -- we don't know how nested fields we want to seek and destroy may be // return JSON.parse( - JSON.stringify(obj).replace(/"(password|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { + JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { return `"${valueName}":"********"`; }) ); From 4b0ef8543291df80712e7e0c7d66ebbc57fa1224 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Oct 2017 20:04:10 -0600 Subject: [PATCH 0065/1013] Allow index.html in root --- core/servers/content/web.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 55fd37f2..92aa05c3 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -158,6 +158,11 @@ exports.getModule = class WebServerModule extends ServerModule { routeRequest(req, resp) { const route = _.find(this.routes, r => r.matchesRequest(req) ); + + if(!route && '/' === req.url) { + return this.routeIndex(req, resp); + } + return route ? route.handler(req, resp) : this.accessDenied(resp); } @@ -196,9 +201,20 @@ exports.getModule = class WebServerModule extends ServerModule { return this.respondWithError(resp, 404, 'File not found.', 'File Not Found'); } + routeIndex(req, resp) { + const filePath = paths.join(Config.contentServers.web.staticRoot, 'index.html'); + + return this.returnStaticPage(filePath, resp); + } + routeStaticFile(req, resp) { const fileName = req.url.substr(req.url.indexOf('/', 1)); const filePath = paths.join(Config.contentServers.web.staticRoot, fileName); + + return this.returnStaticPage(filePath, resp); + } + + returnStaticPage(filePath, resp) { const self = this; fs.stat(filePath, (err, stats) => { From a5f72a345ceeae3496206ea3aa3f467806fca363 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Oct 2017 21:02:36 -0600 Subject: [PATCH 0066/1013] Fix Content-Type --- core/servers/content/web.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 92aa05c3..8a9903b4 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -223,7 +223,7 @@ exports.getModule = class WebServerModule extends ServerModule { } const headers = { - 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), + 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), 'Content-Length' : stats.size, }; From 4e9b72366c80660d7682eb15722988be46927515 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Mon, 30 Oct 2017 22:31:39 +0100 Subject: [PATCH 0067/1013] Update config.md --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index b0ce2e6e..1839978e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -48,7 +48,7 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all { general: { boardName: A Sample BBS - menuFile: your_bbs.hjson # copy of menu.hjson file (and adapt to your needs) + menuFile: your_bbs.hjson //copy of menu.hjson file (and adapt to your needs) } defaults: { From 8ff3d013bd98a02489866da8d104444545e62dd4 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Mon, 30 Oct 2017 23:26:55 +0100 Subject: [PATCH 0068/1013] I made a shame of myself I made a shame of myself, lets improve it even further. --- docs/config.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/config.md b/docs/config.md index 1839978e..fba48119 100644 --- a/docs/config.md +++ b/docs/config.md @@ -48,14 +48,14 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all { general: { boardName: A Sample BBS - menuFile: your_bbs.hjson //copy of menu.hjson file (and adapt to your needs) + menuFile: "your_bbs.hjson" // copy of menu.hjson file (and adapt to your needs) } defaults: { - theme: super-fancy-theme + theme: "super-fancy-theme" // default-assigned theme (for new users) } - preLoginTheme: luciano_blocktronics + preLoginTheme: "luciano_blocktronics" // theme used before a user logs in (matrix, NUA, etc.) messageConferences: { local_general: { From d5059525102cffa1783e827c561fc24a1c520b41 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Thu, 2 Nov 2017 00:41:20 +0000 Subject: [PATCH 0069/1013] CombatNet support! --- README.md | 2 +- core/combatnet.js | 115 ++++++++++++++++++++++++++++++++++++++++++++++ docs/doors.md | 16 +++++++ mods/menu.hjson | 16 ++++++- package.json | 1 + 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 core/combatnet.js diff --git a/README.md b/README.md index 83fb50ea..93752343 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * 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](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), and [Exodus](https://oddnetwork.org/exodus/) support! + * [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) 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 using a built in [web server](docs/web_server.md). Legacy X/Y/Z modem also supported! diff --git a/core/combatnet.js b/core/combatnet.js new file mode 100644 index 00000000..6cde9c7b --- /dev/null +++ b/core/combatnet.js @@ -0,0 +1,115 @@ +/* jslint node: true */ +'use strict'; + +// enigma-bbs +const MenuModule = require('../core/menu_module.js').MenuModule; +const resetScreen = require('../core/ansi_term.js').resetScreen; + +// deps +const async = require('async'); +const _ = require('lodash'); +const RLogin = require('rlogin'); + +exports.moduleInfo = { + name : 'CombatNet', + desc : 'CombatNet Access Module', + author : 'Dave Stephens', +}; + +exports.getModule = class CombatNetModule extends MenuModule { + constructor(options) { + super(options); + + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'bbs.combatnet.us'; + this.config.rloginPort = this.config.rloginPort || 4513; + } + + initSequence() { + const self = this; + + async.series( + [ + function validateConfig(callback) { + if(!_.isString(self.config.password)) { + return callback(new Error('Config requires "password"!')); + } + if(!_.isString(self.config.bbsTag)) { + return callback(new Error('Config requires "bbsTag"!')); + } + return callback(null); + }, + function establishRloginConnection(callback) { + self.client.term.write(resetScreen()); + self.client.term.write('Connecting to CombatNet, please wait...\n'); + + const restorePipeToNormal = function() { + self.client.term.output.removeListener('data', sendToRloginBuffer); + }; + + const rlogin = new RLogin( + { 'clientUsername' : self.config.password, + 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, + 'host' : self.config.host, + 'port' : self.config.rloginPort, + 'terminalType' : self.client.term.termClient, + 'terminalSpeed' : 57600 + } + ); + + // If there was an error ... + rlogin.on('error', err => { + self.client.log.info(`CombatNet rlogin client error: ${err.message}`); + restorePipeToNormal(); + callback(err); + }); + + // If we've been disconnected ... + rlogin.on('disconnect', () => { + self.client.log.info(`Disconnected from CombatNet`); + restorePipeToNormal(); + callback(null); + }); + + function sendToRloginBuffer(buffer) { + rlogin.send(buffer); + }; + + rlogin.on("connect", + /* The 'connect' event handler will be supplied with one argument, + a boolean indicating whether or not the connection was established. */ + + function(state) { + if(state) { + self.client.log.info('Connected to CombatNet'); + self.client.term.output.on('data', sendToRloginBuffer); + + } else { + return callback(new Error('Failed to establish establish CombatNet connection')); + } + } + ); + + // If data (a Buffer) has been received from the server ... + rlogin.on("data", (data) => { + self.client.term.rawWrite(data); + }); + + // connect... + rlogin.connect(); + + // note: no explicit callback() until we're finished! + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'CombatNet error'); + } + + // if the client is still here, go to previous + self.prevMenu(); + } + ); + } +}; diff --git a/docs/doors.md b/docs/doors.md index 702d75d9..0ca55c23 100644 --- a/docs/doors.md +++ b/docs/doors.md @@ -194,6 +194,22 @@ doorParty: { Fill in `username`, `password`, and `bbsTag` with credentials provided to you and you should be in business! +## The CombatNet Module +The `combatnet` module provides native support for [CombatNet](http://combatnet.us/). Add the following to your menu config: + +````hjson +combatNet: { + desc: Using CombatNet + module: @systemModule:combatnet + config: { + bbsTag: CBNxxx + password: XXXXXXXXX + } +} +```` +Update `bbsTag` (in the format CBNxxx) and `password` with the details provided when you register, then +you should be ready to rock! + # Resources ### DOSBox diff --git a/mods/menu.hjson b/mods/menu.hjson index 0dffdcbb..b445ec85 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -1646,6 +1646,10 @@ action: @menu:doorParty } { + value: { command: "CN" } + action: @menu:combatNet + } + { value: { command: "AGENT" } action: @menu:telnetBridgeAgency } @@ -1689,7 +1693,7 @@ } } - // DoorParty! support. You'll need to registger to obtain credentials + // DoorParty! support. You'll need to register to obtain credentials doorParty: { desc: Using DoorParty! module: @systemModule:door_party @@ -1700,6 +1704,16 @@ } } + // CombatNet support. You'll need to register at http://combatnet.us/ to obtain credentials + combatNet: { + desc: Using CombatNet + module: @systemModule:combatnet + config: { + bbsTag: CBNxxx + password: XXXXXXXXX + } + } + telnetBridgeAgency: { desc: Connected to HappyLand BBS module: telnet_bridge diff --git a/package.json b/package.json index 232e1dca..29bd16da 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "node-glob": "^1.2.0", "nodemailer": "^4.1.0", "ptyw.js": "NuSkooler/ptyw.js", + "rlogin": "^1.0.0", "sane": "^2.2.0", "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.9", From 6d31589c8b8871a96f9c1026e3a232a23175a9fa Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Nov 2017 18:55:57 -0700 Subject: [PATCH 0070/1013] Add PCB/WildCat!, WWIV, Renegade, etc. color code support to file descriptions --- core/color_codes.js | 158 +++++++++++++++++++++++++++++++++++++++++ mods/file_area_list.js | 47 +++++++----- 2 files changed, 187 insertions(+), 18 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index 4dc8da99..7e71bd1c 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -11,11 +11,13 @@ exports.enigmaToAnsi = enigmaToAnsi; exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; +exports.controlCodesToAnsi = controlCodesToAnsi; // :TODO: Not really happy with the module name of "color_codes". Would like something better + // Also add: // * fromCelerity(): | // * fromPCBoard(): (@X) @@ -148,3 +150,159 @@ function renegadeToAnsi(s, client) { return (0 === result.length ? s : result + s.substr(lastIndex)); } + +// +// Converts various control codes popular in BBS packages +// to ANSI escape sequences. Additionaly supports ENiGMA style +// MCI codes. +// +// Supported control code formats: +// * Renegade : |## +// * PCBoard : @X## where the first number/char is FG color, and second is BG +// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix +// * WWIV : ^# +// +// TODO: Add Synchronet and Celerity format support +// +// Resources: +// * http://wiki.synchro.net/custom:colors +// +function controlCodesToAnsi(s, client) { + const RE = /(\|([A-Z0-9]{2})|\|)|(\@X([0-9A-F]{2}))|(\@([0-9A-F]{2})\@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + + let m; + let result = ''; + let lastIndex = 0; + let v; + let fg; + let bg; + + while((m = RE.exec(s))) { + switch(m[0].charAt(0)) { + case '|' : + // Renegade or ENiGMA MCI + v = parseInt(m[2], 10); + + if(isNaN(v)) { + v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal + } + + if(_.isString(v)) { + result += s.substr(lastIndex, m.index - lastIndex) + v; + } else { + v = ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], + + 8 : [ 'bold', 'black' ], + 9 : [ 'bold', 'blue' ], + 10 : [ 'bold', 'green' ], + 11 : [ 'bold', 'cyan' ], + 12 : [ 'bold', 'red' ], + 13 : [ 'bold', 'magenta' ], + 14 : [ 'bold', 'yellow' ], + 15 : [ 'bold', 'white' ], + + 16 : [ 'blackBG' ], + 17 : [ 'blueBG' ], + 18 : [ 'greenBG' ], + 19 : [ 'cyanBG' ], + 20 : [ 'redBG' ], + 21 : [ 'magentaBG' ], + 22 : [ 'yellowBG' ], + 23 : [ 'whiteBG' ], + }[v] || 'normal'); + + result += s.substr(lastIndex, m.index - lastIndex) + v; + } + break; + + case '@' : + // PCBoard @X## or Wildcat! @##@ + if('@' === m[0].substr(-1)) { + // Wildcat! + v = m[6]; + } else { + v = m[4]; + } + + fg = { + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], + + 8 : [ 'blink', 'black' ], + 9 : [ 'blink', 'blue' ], + A : [ 'blink', 'green' ], + B : [ 'blink', 'cyan' ], + C : [ 'blink', 'red' ], + D : [ 'blink', 'magenta' ], + E : [ 'blink', 'yellow' ], + F : [ 'blink', 'white' ], + }[v.charAt(0)] || ['normal']; + + bg = { + 0 : [ 'blackBG' ], + 1 : [ 'blueBG' ], + 2 : [ 'greenBG' ], + 3 : [ 'cyanBG' ], + 4 : [ 'redBG' ], + 5 : [ 'magentaBG' ], + 6 : [ 'yellowBG' ], + 7 : [ 'whiteBG' ], + + 8 : [ 'bold', 'blackBG' ], + 9 : [ 'bold', 'blueBG' ], + A : [ 'bold', 'greenBG' ], + B : [ 'bold', 'cyanBG' ], + C : [ 'bold', 'redBG' ], + D : [ 'bold', 'magentaBG' ], + E : [ 'bold', 'yellowBG' ], + F : [ 'bold', 'whiteBG' ], + }[v.charAt(1)] || [ 'normal' ]; + + v = ansi.sgr(fg.concat(bg)); + result += s.substr(lastIndex, m.index - lastIndex) + v; + break; + + case '\x03' : + v = parseInt(m[8], 10); + + if(isNaN(v)) { + v += m[0]; + } else { + v = ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'bold', 'cyan' ], + 2 : [ 'bold', 'yellow' ], + 3 : [ 'reset', 'magenta' ], + 4 : [ 'bold', 'white', 'blueBG' ], + 5 : [ 'reset', 'green' ], + 6 : [ 'bold', 'blink', 'red' ], + 7 : [ 'bold', 'blue' ], + 8 : [ 'reset', 'blue' ], + 9 : [ 'reset', 'cyan' ], + }[v] || 'normal'); + } + + result += s.substr(lastIndex, m.index - lastIndex) + v; + + break; + } + + lastIndex = RE.lastIndex; + } + + return (0 === result.length ? s : result + s.substr(lastIndex)); +} \ No newline at end of file diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 4937eb49..0c24f9f8 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -2,22 +2,23 @@ '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_base_area.js'); -const Errors = require('../core/enig_error.js').Errors; -const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; -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 resolveMimeType = require('../core/mime_util.js').resolveMimeType; -const isAnsi = require('../core/string_util.js').isAnsi; +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_base_area.js'); +const Errors = require('../core/enig_error.js').Errors; +const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; +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 resolveMimeType = require('../core/mime_util.js').resolveMimeType; +const isAnsi = require('../core/string_util.js').isAnsi; +const controlCodesToAnsi = require('../core/color_codes.js').controlCodesToAnsi; // deps const async = require('async'); @@ -385,9 +386,19 @@ exports.getModule = class FileAreaList extends MenuModule { if(_.isString(self.currentFileEntry.desc)) { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); if(descView) { - if(isAnsi(self.currentFileEntry.desc)) { + // + // For descriptions we want to support as many color code systems + // as we can for coverage of what is found in the while (e.g. Renegade + // pipes, PCB @X##, etc.) + // + // MLTEV doesn't support all of this, so convert. If we produced ANSI + // esc sequences, we'll proceed with specialization, else just treat + // it as text. + // + const desc = controlCodesToAnsi(self.currentFileEntry.desc); + if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { descView.setAnsi( - self.currentFileEntry.desc, + desc, { prepped : false, forceLineTerm : true From f0b9cd102d2d20af1ee2610bead548688d91e34f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 18 Nov 2017 14:09:17 -0700 Subject: [PATCH 0071/1013] Fix some year est issues & add ability for oputil fb scan --update to pick up years --- core/config.js | 11 ++++++++--- core/oputil/oputil_file_base.js | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/core/config.js b/core/config.js index e33648b9..367143b6 100644 --- a/core/config.js +++ b/core/config.js @@ -670,12 +670,17 @@ function getDefaultConfig() { // Patterns should produce the year in the first submatch. // The extracted year may be YY or YYYY // - '\\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((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... + '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... + //'\\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 - '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- do this before 19xx 20xx such that this has priority + '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries + '\\b\'([17-9][0-9])\\b', // '95, '17, ... // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 0c4c6065..835db5b9 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -172,8 +172,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { // // We'll update the entry if the following conditions are met: // * We have a single duplicate, and: - // * --update was passed or the existing entry's desc or - // longDesc are blank/empty + // * --update was passed or the existing entry's desc, + // longDesc, or yearEst are blank/empty // if(argv.update && 1 === dupeEntries.length) { const FileEntry = require('../../core/file_entry.js'); @@ -193,15 +193,19 @@ function scanFileAreaForChanges(areaInfo, options, cb) { if( tagsEq && fileEntry.desc === existingEntry.desc && - fileEntry.descLong == existingEntry.descLong) + fileEntry.descLong == existingEntry.descLong && + fileEntry.meta.est_release_year == existingEntry.meta.est_release_year) { console.info('Dupe'); return nextFile(null); } console.info('Dupe (updating)'); - existingEntry.desc = fileEntry.desc; - existingEntry.descLong = fileEntry.descLong; + + // don't allow overwrite of values if new version is blank + existingEntry.desc = fileEntry.desc || existingEntry.desc; + existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; + existingEntry.meta.est_release_year = fileEntry.meta.est_release_year || existingEntry.meta.est_release_year; updateTags(existingEntry); finalizeEntryAndPersist(true, existingEntry, descHandler, err => { From 02cd8c26c72c72f69f1e94954eeb205df6b38929 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 18 Nov 2017 14:14:19 -0700 Subject: [PATCH 0072/1013] Minor fix --- core/oputil/oputil_file_base.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 835db5b9..a00f0889 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -173,7 +173,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) { // We'll update the entry if the following conditions are met: // * We have a single duplicate, and: // * --update was passed or the existing entry's desc, - // longDesc, or yearEst are blank/empty + // longDesc, or est_release_year meta are blank/empty // if(argv.update && 1 === dupeEntries.length) { const FileEntry = require('../../core/file_entry.js'); @@ -205,7 +205,11 @@ function scanFileAreaForChanges(areaInfo, options, cb) { // don't allow overwrite of values if new version is blank existingEntry.desc = fileEntry.desc || existingEntry.desc; existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; - existingEntry.meta.est_release_year = fileEntry.meta.est_release_year || existingEntry.meta.est_release_year; + + if(fileEntry.meta.est_release_year) { + existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; + } + updateTags(existingEntry); finalizeEntryAndPersist(true, existingEntry, descHandler, err => { From 617f0ef07ecadbd47b4c3b71f2526132a369774a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 18 Nov 2017 16:15:50 -0700 Subject: [PATCH 0073/1013] Add extended pipe color codes (24-31) ala Mystic et. al. --- core/color_codes.js | 100 +++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 58 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index 7e71bd1c..2e368aa3 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -87,6 +87,46 @@ function enigmaStrLen(s) { return stripEnigmaCodes(s).length; } +function ansiSgrFromRenegadeColorCode(cc) { + return ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], + + 8 : [ 'bold', 'black' ], + 9 : [ 'bold', 'blue' ], + 10 : [ 'bold', 'green' ], + 11 : [ 'bold', 'cyan' ], + 12 : [ 'bold', 'red' ], + 13 : [ 'bold', 'magenta' ], + 14 : [ 'bold', 'yellow' ], + 15 : [ 'bold', 'white' ], + + 16 : [ 'blackBG' ], + 17 : [ 'blueBG' ], + 18 : [ 'greenBG' ], + 19 : [ 'cyanBG' ], + 20 : [ 'redBG' ], + 21 : [ 'magentaBG' ], + 22 : [ 'yellowBG' ], + 23 : [ 'whiteBG' ], + + 24 : [ 'bold', 'blackBG' ], + 25 : [ 'bold', 'blueBG' ], + 26 : [ 'bold', 'greenBG' ], + 27 : [ 'bold', 'cyanBG' ], + 28 : [ 'bold', 'redBG' ], + 29 : [ 'bold', 'magentaBG' ], + 30 : [ 'bold', 'yellowBG' ], + 31 : [ 'bold', 'whiteBG' ], + }[cc] || 'normal'); +} + function renegadeToAnsi(s, client) { if(-1 == s.indexOf('|')) { return s; // no pipe codes present @@ -113,35 +153,7 @@ function renegadeToAnsi(s, client) { if(_.isString(val)) { result += s.substr(lastIndex, m.index - lastIndex) + val; } else { - var attr = ansi.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], - - 8 : [ 'bold', 'black' ], - 9 : [ 'bold', 'blue' ], - 10 : [ 'bold', 'green' ], - 11 : [ 'bold', 'cyan' ], - 12 : [ 'bold', 'red' ], - 13 : [ 'bold', 'magenta' ], - 14 : [ 'bold', 'yellow' ], - 15 : [ 'bold', 'white' ], - - 16 : [ 'blackBG' ], - 17 : [ 'blueBG' ], - 18 : [ 'greenBG' ], - 19 : [ 'cyanBG' ], - 20 : [ 'redBG' ], - 21 : [ 'magentaBG' ], - 22 : [ 'yellowBG' ], - 23 : [ 'whiteBG' ], - }[val] || 'normal'); - + const attr = ansiSgrFromRenegadeColorCode(val); result += s.substr(lastIndex, m.index - lastIndex) + attr; } @@ -190,35 +202,7 @@ function controlCodesToAnsi(s, client) { if(_.isString(v)) { result += s.substr(lastIndex, m.index - lastIndex) + v; } else { - v = ansi.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], - - 8 : [ 'bold', 'black' ], - 9 : [ 'bold', 'blue' ], - 10 : [ 'bold', 'green' ], - 11 : [ 'bold', 'cyan' ], - 12 : [ 'bold', 'red' ], - 13 : [ 'bold', 'magenta' ], - 14 : [ 'bold', 'yellow' ], - 15 : [ 'bold', 'white' ], - - 16 : [ 'blackBG' ], - 17 : [ 'blueBG' ], - 18 : [ 'greenBG' ], - 19 : [ 'cyanBG' ], - 20 : [ 'redBG' ], - 21 : [ 'magentaBG' ], - 22 : [ 'yellowBG' ], - 23 : [ 'whiteBG' ], - }[v] || 'normal'); - + v = ansiSgrFromRenegadeColorCode(v); result += s.substr(lastIndex, m.index - lastIndex) + v; } break; From 57d46dd57ea3f5d5675eac85f548b8358b811328 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Wed, 22 Nov 2017 23:27:33 +0000 Subject: [PATCH 0074/1013] Mega rejig! --- {mods/art => art/general}/CONNECT1.ANS | Bin {mods/art => art/general}/DOORMANY.ANS | Bin {mods/art => art/general}/GNSPMPT.ANS | Bin {mods/art => art/general}/LOGPMPT.ANS | Bin {mods/art => art/general}/NEWSCAN.ANS | Bin {mods/art => art/general}/NEWUSER1.ANS | Bin {mods/art => art/general}/ONEADD.ANS | Bin {mods/art => art/general}/ONELINER.ANS | Bin {mods/art => art/general}/PRELOGAD.ANS | Bin {mods/art => art/general}/WELCOME1.ANS | 0 {mods/art => art/general}/WELCOME2.ANS | Bin .../general}/demo_edit_text_view.ans | Bin .../general}/demo_edit_text_view1.ans | Bin .../general}/demo_fse_local_user.ans | Bin .../general}/demo_fse_netmail_body.ans | Bin .../general}/demo_fse_netmail_footer_edit.ans | Bin .../demo_fse_netmail_footer_edit_menu.ans | Bin .../general}/demo_fse_netmail_header.ans | Bin .../general}/demo_fse_netmail_help.ans | Bin .../general}/demo_horizontal_menu_view1.ans | Bin .../general}/demo_mask_edit_text_view1.ans | Bin .../demo_multi_line_edit_text_view1.ans | Bin .../art => art/general}/demo_selection_vm.ans | Bin .../general}/demo_spin_and_toggle.ans | Bin .../general}/demo_vertical_menu_view1.ans | Bin {mods/art => art/general}/erc.ans | Bin {mods/art => art/general}/menu_prompt.ans | Bin .../general}/msg_area_footer_view.ans | Bin {mods/art => art/general}/msg_area_list.ans | Bin .../general}/msg_area_post_header.ans | Bin .../general}/msg_area_view_header.ans | Bin {mods/art => art/general}/test.ans | 0 .../themes/luciano_blocktronics/BBSADD.ANS | Bin .../themes/luciano_blocktronics/BBSLIST.ANS | Bin .../themes/luciano_blocktronics/CCHANGE.ANS | Bin .../themes/luciano_blocktronics/CHANGE.ANS | Bin .../themes/luciano_blocktronics/CONFSCR.ANS | Bin .../themes/luciano_blocktronics/DONE.ANS | Bin .../themes/luciano_blocktronics/DOORMNU.ANS | Bin .../themes/luciano_blocktronics/FAREASEL.ANS | Bin .../themes/luciano_blocktronics/FBHELP.ANS | Bin .../themes/luciano_blocktronics/FBNORES.ANS | Bin .../themes/luciano_blocktronics/FBRWSE.ANS | Bin .../themes/luciano_blocktronics/FDETAIL.ANS | Bin .../themes/luciano_blocktronics/FDETGEN.ANS | Bin .../themes/luciano_blocktronics/FDETLST.ANS | Bin .../themes/luciano_blocktronics/FDETNFO.ANS | Bin .../themes/luciano_blocktronics/FDLMGR.ANS | Bin .../themes/luciano_blocktronics/FEMPTYQ.ANS | Bin .../themes/luciano_blocktronics/FFILEDT.ANS | Bin .../themes/luciano_blocktronics/FILPMPT.ANS | Bin .../themes/luciano_blocktronics/FMENU.ANS | Bin .../themes/luciano_blocktronics/FNEWBRWSE.ANS | Bin .../themes/luciano_blocktronics/FORGOTPW.ANS | Bin .../luciano_blocktronics/FORGOTPWSENT.ANS | Bin .../themes/luciano_blocktronics/FPROSEL.ANS | Bin .../themes/luciano_blocktronics/FSEARCH.ANS | Bin .../themes/luciano_blocktronics/IDLELOG.ANS | Bin .../themes/luciano_blocktronics/LASTCALL.ANS | Bin .../themes/luciano_blocktronics/LETTER.ANS | Bin .../themes/luciano_blocktronics/MAILMNU.ANS | Bin .../themes/luciano_blocktronics/MATRIX.ANS | Bin .../themes/luciano_blocktronics/MMENU.ANS | Bin .../themes/luciano_blocktronics/MNUPRMT.ANS | Bin .../themes/luciano_blocktronics/MSGBODY.ANS | Bin .../themes/luciano_blocktronics/MSGEFTR.ANS | Bin .../themes/luciano_blocktronics/MSGEHDR.ANS | Bin .../themes/luciano_blocktronics/MSGEHLP.ANS | Bin .../themes/luciano_blocktronics/MSGEMFT.ANS | Bin .../themes/luciano_blocktronics/MSGLIST.ANS | Bin .../themes/luciano_blocktronics/MSGMNU.ANS | Bin .../themes/luciano_blocktronics/MSGPMPT.ANS | Bin .../themes/luciano_blocktronics/MSGQUOT.ANS | Bin .../themes/luciano_blocktronics/MSGVFTR.ANS | Bin .../themes/luciano_blocktronics/MSGVHDR.ANS | Bin .../themes/luciano_blocktronics/MSGVHLP.ANS | Bin .../themes/luciano_blocktronics/NEWMSGS.ANS | Bin .../themes/luciano_blocktronics/NUA.ANS | Bin .../themes/luciano_blocktronics/ONEADD.ANS | Bin .../themes/luciano_blocktronics/ONELINER.ANS | Bin .../themes/luciano_blocktronics/PAUSE.ANS | Bin .../themes/luciano_blocktronics/RATEFILE.ANS | Bin .../themes/luciano_blocktronics/RUMORADD.ANS | Bin .../themes/luciano_blocktronics/RUMORS.ANS | Bin .../themes/luciano_blocktronics/STATUS.ANS | Bin .../themes/luciano_blocktronics/SYSSTAT.ANS | Bin .../themes/luciano_blocktronics/TBRIDGE.ANS | Bin .../themes/luciano_blocktronics/TOONODE.ANS | Bin .../themes/luciano_blocktronics/ULCHECK.ANS | Bin .../themes/luciano_blocktronics/ULDETAIL.ANS | Bin .../themes/luciano_blocktronics/ULDUPES.ANS | Bin .../themes/luciano_blocktronics/ULNOAREA.ANS | Bin .../themes/luciano_blocktronics/ULOPTS.ANS | Bin .../themes/luciano_blocktronics/USERLOG.ANS | Bin .../themes/luciano_blocktronics/USERLST.ANS | Bin .../themes/luciano_blocktronics/WHOSON.ANS | Bin .../themes/luciano_blocktronics/theme.hjson | 0 {mods => config}/menu.hjson | 0 {mods => config}/prompt.hjson | 0 core/config.js | 15 ++++---- core/config_util.js | 9 +++-- core/module_util.js | 1 + core/oputil/oputil_common.js | 2 +- mods/{ => system}/abracadabra.js | 10 +++--- mods/{ => system}/bbs_link.js | 6 ++-- mods/{ => system}/bbs_list.js | 14 ++++---- mods/{ => system}/erc_client.js | 4 +-- mods/{ => system}/file_area_filter_edit.js | 10 +++--- mods/{ => system}/file_area_list.js | 34 +++++++++--------- mods/{ => system}/file_base_area_select.js | 8 ++--- .../file_base_download_manager.js | 16 ++++----- mods/{ => system}/file_base_search.js | 8 ++--- .../file_base_web_download_manager.js | 20 +++++------ .../file_transfer_protocol_select.js | 8 ++--- mods/{ => system}/last_callers.js | 10 +++--- mods/{ => system}/msg_area_list.js | 12 +++---- mods/{ => system}/msg_area_post_fse.js | 4 +-- mods/{ => system}/msg_area_reply_fse.js | 2 +- mods/{ => system}/msg_area_view_fse.js | 4 +-- mods/{ => system}/msg_conf_list.js | 12 +++---- mods/{ => system}/msg_list.js | 10 +++--- mods/{ => system}/nua.js | 12 +++---- mods/{ => system}/onelinerz.js | 12 +++---- mods/{ => system}/rumorz.js | 14 ++++---- mods/{ => system}/telnet_bridge.js | 6 ++-- mods/{ => system}/upload.js | 28 +++++++-------- mods/{ => system}/user_list.js | 8 ++--- mods/{ => system}/whos_online.js | 8 ++--- mods/user/.keep | 0 129 files changed, 153 insertions(+), 154 deletions(-) rename {mods/art => art/general}/CONNECT1.ANS (100%) rename {mods/art => art/general}/DOORMANY.ANS (100%) rename {mods/art => art/general}/GNSPMPT.ANS (100%) rename {mods/art => art/general}/LOGPMPT.ANS (100%) rename {mods/art => art/general}/NEWSCAN.ANS (100%) rename {mods/art => art/general}/NEWUSER1.ANS (100%) rename {mods/art => art/general}/ONEADD.ANS (100%) rename {mods/art => art/general}/ONELINER.ANS (100%) rename {mods/art => art/general}/PRELOGAD.ANS (100%) rename {mods/art => art/general}/WELCOME1.ANS (100%) rename {mods/art => art/general}/WELCOME2.ANS (100%) rename {mods/art => art/general}/demo_edit_text_view.ans (100%) rename {mods/art => art/general}/demo_edit_text_view1.ans (100%) rename {mods/art => art/general}/demo_fse_local_user.ans (100%) rename {mods/art => art/general}/demo_fse_netmail_body.ans (100%) rename {mods/art => art/general}/demo_fse_netmail_footer_edit.ans (100%) rename {mods/art => art/general}/demo_fse_netmail_footer_edit_menu.ans (100%) rename {mods/art => art/general}/demo_fse_netmail_header.ans (100%) rename {mods/art => art/general}/demo_fse_netmail_help.ans (100%) rename {mods/art => art/general}/demo_horizontal_menu_view1.ans (100%) rename {mods/art => art/general}/demo_mask_edit_text_view1.ans (100%) rename {mods/art => art/general}/demo_multi_line_edit_text_view1.ans (100%) rename {mods/art => art/general}/demo_selection_vm.ans (100%) rename {mods/art => art/general}/demo_spin_and_toggle.ans (100%) rename {mods/art => art/general}/demo_vertical_menu_view1.ans (100%) rename {mods/art => art/general}/erc.ans (100%) rename {mods/art => art/general}/menu_prompt.ans (100%) rename {mods/art => art/general}/msg_area_footer_view.ans (100%) rename {mods/art => art/general}/msg_area_list.ans (100%) rename {mods/art => art/general}/msg_area_post_header.ans (100%) rename {mods/art => art/general}/msg_area_view_header.ans (100%) rename {mods/art => art/general}/test.ans (100%) rename {mods => art}/themes/luciano_blocktronics/BBSADD.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/BBSLIST.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/CCHANGE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/CHANGE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/CONFSCR.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/DONE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/DOORMNU.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FAREASEL.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FBHELP.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FBNORES.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FBRWSE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FDETAIL.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FDETGEN.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FDETLST.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FDETNFO.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FDLMGR.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FEMPTYQ.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FFILEDT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FILPMPT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FMENU.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FNEWBRWSE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FORGOTPW.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FORGOTPWSENT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FPROSEL.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/FSEARCH.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/IDLELOG.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/LASTCALL.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/LETTER.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MAILMNU.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MATRIX.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MMENU.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MNUPRMT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGBODY.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGEFTR.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGEHDR.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGEHLP.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGEMFT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGLIST.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGMNU.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGPMPT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGQUOT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGVFTR.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGVHDR.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/MSGVHLP.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/NEWMSGS.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/NUA.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ONEADD.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ONELINER.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/PAUSE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/RATEFILE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/RUMORADD.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/RUMORS.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/STATUS.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/SYSSTAT.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/TBRIDGE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/TOONODE.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ULCHECK.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ULDETAIL.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ULDUPES.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ULNOAREA.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/ULOPTS.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/USERLOG.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/USERLST.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/WHOSON.ANS (100%) rename {mods => art}/themes/luciano_blocktronics/theme.hjson (100%) rename {mods => config}/menu.hjson (100%) rename {mods => config}/prompt.hjson (100%) rename mods/{ => system}/abracadabra.js (94%) rename mods/{ => system}/bbs_link.js (96%) rename mods/{ => system}/bbs_list.js (96%) rename mods/{ => system}/erc_client.js (97%) rename mods/{ => system}/file_area_filter_edit.js (95%) rename mods/{ => system}/file_area_list.js (94%) rename mods/{ => system}/file_base_area_select.js (88%) rename mods/{ => system}/file_base_download_manager.js (92%) rename mods/{ => system}/file_base_search.js (89%) rename mods/{ => system}/file_base_web_download_manager.js (91%) rename mods/{ => system}/file_transfer_protocol_select.js (93%) rename mods/{ => system}/last_callers.js (92%) rename mods/{ => system}/msg_area_list.js (91%) rename mods/{ => system}/msg_area_post_fse.js (90%) rename mods/{ => system}/msg_area_reply_fse.js (81%) rename mods/{ => system}/msg_area_view_fse.js (95%) rename mods/{ => system}/msg_conf_list.js (89%) rename mods/{ => system}/msg_list.js (95%) rename mods/{ => system}/nua.js (91%) rename mods/{ => system}/onelinerz.js (95%) rename mods/{ => system}/rumorz.js (92%) rename mods/{ => system}/telnet_bridge.js (94%) rename mods/{ => system}/upload.js (94%) rename mods/{ => system}/user_list.js (91%) rename mods/{ => system}/whos_online.js (87%) create mode 100644 mods/user/.keep diff --git a/mods/art/CONNECT1.ANS b/art/general/CONNECT1.ANS similarity index 100% rename from mods/art/CONNECT1.ANS rename to art/general/CONNECT1.ANS diff --git a/mods/art/DOORMANY.ANS b/art/general/DOORMANY.ANS similarity index 100% rename from mods/art/DOORMANY.ANS rename to art/general/DOORMANY.ANS diff --git a/mods/art/GNSPMPT.ANS b/art/general/GNSPMPT.ANS similarity index 100% rename from mods/art/GNSPMPT.ANS rename to art/general/GNSPMPT.ANS diff --git a/mods/art/LOGPMPT.ANS b/art/general/LOGPMPT.ANS similarity index 100% rename from mods/art/LOGPMPT.ANS rename to art/general/LOGPMPT.ANS diff --git a/mods/art/NEWSCAN.ANS b/art/general/NEWSCAN.ANS similarity index 100% rename from mods/art/NEWSCAN.ANS rename to art/general/NEWSCAN.ANS diff --git a/mods/art/NEWUSER1.ANS b/art/general/NEWUSER1.ANS similarity index 100% rename from mods/art/NEWUSER1.ANS rename to art/general/NEWUSER1.ANS diff --git a/mods/art/ONEADD.ANS b/art/general/ONEADD.ANS similarity index 100% rename from mods/art/ONEADD.ANS rename to art/general/ONEADD.ANS diff --git a/mods/art/ONELINER.ANS b/art/general/ONELINER.ANS similarity index 100% rename from mods/art/ONELINER.ANS rename to art/general/ONELINER.ANS diff --git a/mods/art/PRELOGAD.ANS b/art/general/PRELOGAD.ANS similarity index 100% rename from mods/art/PRELOGAD.ANS rename to art/general/PRELOGAD.ANS diff --git a/mods/art/WELCOME1.ANS b/art/general/WELCOME1.ANS similarity index 100% rename from mods/art/WELCOME1.ANS rename to art/general/WELCOME1.ANS diff --git a/mods/art/WELCOME2.ANS b/art/general/WELCOME2.ANS similarity index 100% rename from mods/art/WELCOME2.ANS rename to art/general/WELCOME2.ANS diff --git a/mods/art/demo_edit_text_view.ans b/art/general/demo_edit_text_view.ans similarity index 100% rename from mods/art/demo_edit_text_view.ans rename to art/general/demo_edit_text_view.ans diff --git a/mods/art/demo_edit_text_view1.ans b/art/general/demo_edit_text_view1.ans similarity index 100% rename from mods/art/demo_edit_text_view1.ans rename to art/general/demo_edit_text_view1.ans diff --git a/mods/art/demo_fse_local_user.ans b/art/general/demo_fse_local_user.ans similarity index 100% rename from mods/art/demo_fse_local_user.ans rename to art/general/demo_fse_local_user.ans diff --git a/mods/art/demo_fse_netmail_body.ans b/art/general/demo_fse_netmail_body.ans similarity index 100% rename from mods/art/demo_fse_netmail_body.ans rename to art/general/demo_fse_netmail_body.ans diff --git a/mods/art/demo_fse_netmail_footer_edit.ans b/art/general/demo_fse_netmail_footer_edit.ans similarity index 100% rename from mods/art/demo_fse_netmail_footer_edit.ans rename to art/general/demo_fse_netmail_footer_edit.ans diff --git a/mods/art/demo_fse_netmail_footer_edit_menu.ans b/art/general/demo_fse_netmail_footer_edit_menu.ans similarity index 100% rename from mods/art/demo_fse_netmail_footer_edit_menu.ans rename to art/general/demo_fse_netmail_footer_edit_menu.ans diff --git a/mods/art/demo_fse_netmail_header.ans b/art/general/demo_fse_netmail_header.ans similarity index 100% rename from mods/art/demo_fse_netmail_header.ans rename to art/general/demo_fse_netmail_header.ans diff --git a/mods/art/demo_fse_netmail_help.ans b/art/general/demo_fse_netmail_help.ans similarity index 100% rename from mods/art/demo_fse_netmail_help.ans rename to art/general/demo_fse_netmail_help.ans diff --git a/mods/art/demo_horizontal_menu_view1.ans b/art/general/demo_horizontal_menu_view1.ans similarity index 100% rename from mods/art/demo_horizontal_menu_view1.ans rename to art/general/demo_horizontal_menu_view1.ans diff --git a/mods/art/demo_mask_edit_text_view1.ans b/art/general/demo_mask_edit_text_view1.ans similarity index 100% rename from mods/art/demo_mask_edit_text_view1.ans rename to art/general/demo_mask_edit_text_view1.ans diff --git a/mods/art/demo_multi_line_edit_text_view1.ans b/art/general/demo_multi_line_edit_text_view1.ans similarity index 100% rename from mods/art/demo_multi_line_edit_text_view1.ans rename to art/general/demo_multi_line_edit_text_view1.ans diff --git a/mods/art/demo_selection_vm.ans b/art/general/demo_selection_vm.ans similarity index 100% rename from mods/art/demo_selection_vm.ans rename to art/general/demo_selection_vm.ans diff --git a/mods/art/demo_spin_and_toggle.ans b/art/general/demo_spin_and_toggle.ans similarity index 100% rename from mods/art/demo_spin_and_toggle.ans rename to art/general/demo_spin_and_toggle.ans diff --git a/mods/art/demo_vertical_menu_view1.ans b/art/general/demo_vertical_menu_view1.ans similarity index 100% rename from mods/art/demo_vertical_menu_view1.ans rename to art/general/demo_vertical_menu_view1.ans diff --git a/mods/art/erc.ans b/art/general/erc.ans similarity index 100% rename from mods/art/erc.ans rename to art/general/erc.ans diff --git a/mods/art/menu_prompt.ans b/art/general/menu_prompt.ans similarity index 100% rename from mods/art/menu_prompt.ans rename to art/general/menu_prompt.ans diff --git a/mods/art/msg_area_footer_view.ans b/art/general/msg_area_footer_view.ans similarity index 100% rename from mods/art/msg_area_footer_view.ans rename to art/general/msg_area_footer_view.ans diff --git a/mods/art/msg_area_list.ans b/art/general/msg_area_list.ans similarity index 100% rename from mods/art/msg_area_list.ans rename to art/general/msg_area_list.ans diff --git a/mods/art/msg_area_post_header.ans b/art/general/msg_area_post_header.ans similarity index 100% rename from mods/art/msg_area_post_header.ans rename to art/general/msg_area_post_header.ans diff --git a/mods/art/msg_area_view_header.ans b/art/general/msg_area_view_header.ans similarity index 100% rename from mods/art/msg_area_view_header.ans rename to art/general/msg_area_view_header.ans diff --git a/mods/art/test.ans b/art/general/test.ans similarity index 100% rename from mods/art/test.ans rename to art/general/test.ans diff --git a/mods/themes/luciano_blocktronics/BBSADD.ANS b/art/themes/luciano_blocktronics/BBSADD.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/BBSADD.ANS rename to art/themes/luciano_blocktronics/BBSADD.ANS diff --git a/mods/themes/luciano_blocktronics/BBSLIST.ANS b/art/themes/luciano_blocktronics/BBSLIST.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/BBSLIST.ANS rename to art/themes/luciano_blocktronics/BBSLIST.ANS diff --git a/mods/themes/luciano_blocktronics/CCHANGE.ANS b/art/themes/luciano_blocktronics/CCHANGE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/CCHANGE.ANS rename to art/themes/luciano_blocktronics/CCHANGE.ANS diff --git a/mods/themes/luciano_blocktronics/CHANGE.ANS b/art/themes/luciano_blocktronics/CHANGE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/CHANGE.ANS rename to art/themes/luciano_blocktronics/CHANGE.ANS diff --git a/mods/themes/luciano_blocktronics/CONFSCR.ANS b/art/themes/luciano_blocktronics/CONFSCR.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/CONFSCR.ANS rename to art/themes/luciano_blocktronics/CONFSCR.ANS diff --git a/mods/themes/luciano_blocktronics/DONE.ANS b/art/themes/luciano_blocktronics/DONE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/DONE.ANS rename to art/themes/luciano_blocktronics/DONE.ANS diff --git a/mods/themes/luciano_blocktronics/DOORMNU.ANS b/art/themes/luciano_blocktronics/DOORMNU.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/DOORMNU.ANS rename to art/themes/luciano_blocktronics/DOORMNU.ANS diff --git a/mods/themes/luciano_blocktronics/FAREASEL.ANS b/art/themes/luciano_blocktronics/FAREASEL.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FAREASEL.ANS rename to art/themes/luciano_blocktronics/FAREASEL.ANS diff --git a/mods/themes/luciano_blocktronics/FBHELP.ANS b/art/themes/luciano_blocktronics/FBHELP.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FBHELP.ANS rename to art/themes/luciano_blocktronics/FBHELP.ANS diff --git a/mods/themes/luciano_blocktronics/FBNORES.ANS b/art/themes/luciano_blocktronics/FBNORES.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FBNORES.ANS rename to art/themes/luciano_blocktronics/FBNORES.ANS diff --git a/mods/themes/luciano_blocktronics/FBRWSE.ANS b/art/themes/luciano_blocktronics/FBRWSE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FBRWSE.ANS rename to art/themes/luciano_blocktronics/FBRWSE.ANS diff --git a/mods/themes/luciano_blocktronics/FDETAIL.ANS b/art/themes/luciano_blocktronics/FDETAIL.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FDETAIL.ANS rename to art/themes/luciano_blocktronics/FDETAIL.ANS diff --git a/mods/themes/luciano_blocktronics/FDETGEN.ANS b/art/themes/luciano_blocktronics/FDETGEN.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FDETGEN.ANS rename to art/themes/luciano_blocktronics/FDETGEN.ANS diff --git a/mods/themes/luciano_blocktronics/FDETLST.ANS b/art/themes/luciano_blocktronics/FDETLST.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FDETLST.ANS rename to art/themes/luciano_blocktronics/FDETLST.ANS diff --git a/mods/themes/luciano_blocktronics/FDETNFO.ANS b/art/themes/luciano_blocktronics/FDETNFO.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FDETNFO.ANS rename to art/themes/luciano_blocktronics/FDETNFO.ANS diff --git a/mods/themes/luciano_blocktronics/FDLMGR.ANS b/art/themes/luciano_blocktronics/FDLMGR.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FDLMGR.ANS rename to art/themes/luciano_blocktronics/FDLMGR.ANS diff --git a/mods/themes/luciano_blocktronics/FEMPTYQ.ANS b/art/themes/luciano_blocktronics/FEMPTYQ.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FEMPTYQ.ANS rename to art/themes/luciano_blocktronics/FEMPTYQ.ANS diff --git a/mods/themes/luciano_blocktronics/FFILEDT.ANS b/art/themes/luciano_blocktronics/FFILEDT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FFILEDT.ANS rename to art/themes/luciano_blocktronics/FFILEDT.ANS diff --git a/mods/themes/luciano_blocktronics/FILPMPT.ANS b/art/themes/luciano_blocktronics/FILPMPT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FILPMPT.ANS rename to art/themes/luciano_blocktronics/FILPMPT.ANS diff --git a/mods/themes/luciano_blocktronics/FMENU.ANS b/art/themes/luciano_blocktronics/FMENU.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FMENU.ANS rename to art/themes/luciano_blocktronics/FMENU.ANS diff --git a/mods/themes/luciano_blocktronics/FNEWBRWSE.ANS b/art/themes/luciano_blocktronics/FNEWBRWSE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FNEWBRWSE.ANS rename to art/themes/luciano_blocktronics/FNEWBRWSE.ANS diff --git a/mods/themes/luciano_blocktronics/FORGOTPW.ANS b/art/themes/luciano_blocktronics/FORGOTPW.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FORGOTPW.ANS rename to art/themes/luciano_blocktronics/FORGOTPW.ANS diff --git a/mods/themes/luciano_blocktronics/FORGOTPWSENT.ANS b/art/themes/luciano_blocktronics/FORGOTPWSENT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FORGOTPWSENT.ANS rename to art/themes/luciano_blocktronics/FORGOTPWSENT.ANS diff --git a/mods/themes/luciano_blocktronics/FPROSEL.ANS b/art/themes/luciano_blocktronics/FPROSEL.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FPROSEL.ANS rename to art/themes/luciano_blocktronics/FPROSEL.ANS diff --git a/mods/themes/luciano_blocktronics/FSEARCH.ANS b/art/themes/luciano_blocktronics/FSEARCH.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/FSEARCH.ANS rename to art/themes/luciano_blocktronics/FSEARCH.ANS diff --git a/mods/themes/luciano_blocktronics/IDLELOG.ANS b/art/themes/luciano_blocktronics/IDLELOG.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/IDLELOG.ANS rename to art/themes/luciano_blocktronics/IDLELOG.ANS diff --git a/mods/themes/luciano_blocktronics/LASTCALL.ANS b/art/themes/luciano_blocktronics/LASTCALL.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/LASTCALL.ANS rename to art/themes/luciano_blocktronics/LASTCALL.ANS diff --git a/mods/themes/luciano_blocktronics/LETTER.ANS b/art/themes/luciano_blocktronics/LETTER.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/LETTER.ANS rename to art/themes/luciano_blocktronics/LETTER.ANS diff --git a/mods/themes/luciano_blocktronics/MAILMNU.ANS b/art/themes/luciano_blocktronics/MAILMNU.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MAILMNU.ANS rename to art/themes/luciano_blocktronics/MAILMNU.ANS diff --git a/mods/themes/luciano_blocktronics/MATRIX.ANS b/art/themes/luciano_blocktronics/MATRIX.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MATRIX.ANS rename to art/themes/luciano_blocktronics/MATRIX.ANS diff --git a/mods/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MMENU.ANS rename to art/themes/luciano_blocktronics/MMENU.ANS diff --git a/mods/themes/luciano_blocktronics/MNUPRMT.ANS b/art/themes/luciano_blocktronics/MNUPRMT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MNUPRMT.ANS rename to art/themes/luciano_blocktronics/MNUPRMT.ANS diff --git a/mods/themes/luciano_blocktronics/MSGBODY.ANS b/art/themes/luciano_blocktronics/MSGBODY.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGBODY.ANS rename to art/themes/luciano_blocktronics/MSGBODY.ANS diff --git a/mods/themes/luciano_blocktronics/MSGEFTR.ANS b/art/themes/luciano_blocktronics/MSGEFTR.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGEFTR.ANS rename to art/themes/luciano_blocktronics/MSGEFTR.ANS diff --git a/mods/themes/luciano_blocktronics/MSGEHDR.ANS b/art/themes/luciano_blocktronics/MSGEHDR.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGEHDR.ANS rename to art/themes/luciano_blocktronics/MSGEHDR.ANS diff --git a/mods/themes/luciano_blocktronics/MSGEHLP.ANS b/art/themes/luciano_blocktronics/MSGEHLP.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGEHLP.ANS rename to art/themes/luciano_blocktronics/MSGEHLP.ANS diff --git a/mods/themes/luciano_blocktronics/MSGEMFT.ANS b/art/themes/luciano_blocktronics/MSGEMFT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGEMFT.ANS rename to art/themes/luciano_blocktronics/MSGEMFT.ANS diff --git a/mods/themes/luciano_blocktronics/MSGLIST.ANS b/art/themes/luciano_blocktronics/MSGLIST.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGLIST.ANS rename to art/themes/luciano_blocktronics/MSGLIST.ANS diff --git a/mods/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGMNU.ANS rename to art/themes/luciano_blocktronics/MSGMNU.ANS diff --git a/mods/themes/luciano_blocktronics/MSGPMPT.ANS b/art/themes/luciano_blocktronics/MSGPMPT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGPMPT.ANS rename to art/themes/luciano_blocktronics/MSGPMPT.ANS diff --git a/mods/themes/luciano_blocktronics/MSGQUOT.ANS b/art/themes/luciano_blocktronics/MSGQUOT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGQUOT.ANS rename to art/themes/luciano_blocktronics/MSGQUOT.ANS diff --git a/mods/themes/luciano_blocktronics/MSGVFTR.ANS b/art/themes/luciano_blocktronics/MSGVFTR.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGVFTR.ANS rename to art/themes/luciano_blocktronics/MSGVFTR.ANS diff --git a/mods/themes/luciano_blocktronics/MSGVHDR.ANS b/art/themes/luciano_blocktronics/MSGVHDR.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGVHDR.ANS rename to art/themes/luciano_blocktronics/MSGVHDR.ANS diff --git a/mods/themes/luciano_blocktronics/MSGVHLP.ANS b/art/themes/luciano_blocktronics/MSGVHLP.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/MSGVHLP.ANS rename to art/themes/luciano_blocktronics/MSGVHLP.ANS diff --git a/mods/themes/luciano_blocktronics/NEWMSGS.ANS b/art/themes/luciano_blocktronics/NEWMSGS.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/NEWMSGS.ANS rename to art/themes/luciano_blocktronics/NEWMSGS.ANS diff --git a/mods/themes/luciano_blocktronics/NUA.ANS b/art/themes/luciano_blocktronics/NUA.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/NUA.ANS rename to art/themes/luciano_blocktronics/NUA.ANS diff --git a/mods/themes/luciano_blocktronics/ONEADD.ANS b/art/themes/luciano_blocktronics/ONEADD.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ONEADD.ANS rename to art/themes/luciano_blocktronics/ONEADD.ANS diff --git a/mods/themes/luciano_blocktronics/ONELINER.ANS b/art/themes/luciano_blocktronics/ONELINER.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ONELINER.ANS rename to art/themes/luciano_blocktronics/ONELINER.ANS diff --git a/mods/themes/luciano_blocktronics/PAUSE.ANS b/art/themes/luciano_blocktronics/PAUSE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/PAUSE.ANS rename to art/themes/luciano_blocktronics/PAUSE.ANS diff --git a/mods/themes/luciano_blocktronics/RATEFILE.ANS b/art/themes/luciano_blocktronics/RATEFILE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/RATEFILE.ANS rename to art/themes/luciano_blocktronics/RATEFILE.ANS diff --git a/mods/themes/luciano_blocktronics/RUMORADD.ANS b/art/themes/luciano_blocktronics/RUMORADD.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/RUMORADD.ANS rename to art/themes/luciano_blocktronics/RUMORADD.ANS diff --git a/mods/themes/luciano_blocktronics/RUMORS.ANS b/art/themes/luciano_blocktronics/RUMORS.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/RUMORS.ANS rename to art/themes/luciano_blocktronics/RUMORS.ANS diff --git a/mods/themes/luciano_blocktronics/STATUS.ANS b/art/themes/luciano_blocktronics/STATUS.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/STATUS.ANS rename to art/themes/luciano_blocktronics/STATUS.ANS diff --git a/mods/themes/luciano_blocktronics/SYSSTAT.ANS b/art/themes/luciano_blocktronics/SYSSTAT.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/SYSSTAT.ANS rename to art/themes/luciano_blocktronics/SYSSTAT.ANS diff --git a/mods/themes/luciano_blocktronics/TBRIDGE.ANS b/art/themes/luciano_blocktronics/TBRIDGE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/TBRIDGE.ANS rename to art/themes/luciano_blocktronics/TBRIDGE.ANS diff --git a/mods/themes/luciano_blocktronics/TOONODE.ANS b/art/themes/luciano_blocktronics/TOONODE.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/TOONODE.ANS rename to art/themes/luciano_blocktronics/TOONODE.ANS diff --git a/mods/themes/luciano_blocktronics/ULCHECK.ANS b/art/themes/luciano_blocktronics/ULCHECK.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ULCHECK.ANS rename to art/themes/luciano_blocktronics/ULCHECK.ANS diff --git a/mods/themes/luciano_blocktronics/ULDETAIL.ANS b/art/themes/luciano_blocktronics/ULDETAIL.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ULDETAIL.ANS rename to art/themes/luciano_blocktronics/ULDETAIL.ANS diff --git a/mods/themes/luciano_blocktronics/ULDUPES.ANS b/art/themes/luciano_blocktronics/ULDUPES.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ULDUPES.ANS rename to art/themes/luciano_blocktronics/ULDUPES.ANS diff --git a/mods/themes/luciano_blocktronics/ULNOAREA.ANS b/art/themes/luciano_blocktronics/ULNOAREA.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ULNOAREA.ANS rename to art/themes/luciano_blocktronics/ULNOAREA.ANS diff --git a/mods/themes/luciano_blocktronics/ULOPTS.ANS b/art/themes/luciano_blocktronics/ULOPTS.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/ULOPTS.ANS rename to art/themes/luciano_blocktronics/ULOPTS.ANS diff --git a/mods/themes/luciano_blocktronics/USERLOG.ANS b/art/themes/luciano_blocktronics/USERLOG.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/USERLOG.ANS rename to art/themes/luciano_blocktronics/USERLOG.ANS diff --git a/mods/themes/luciano_blocktronics/USERLST.ANS b/art/themes/luciano_blocktronics/USERLST.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/USERLST.ANS rename to art/themes/luciano_blocktronics/USERLST.ANS diff --git a/mods/themes/luciano_blocktronics/WHOSON.ANS b/art/themes/luciano_blocktronics/WHOSON.ANS similarity index 100% rename from mods/themes/luciano_blocktronics/WHOSON.ANS rename to art/themes/luciano_blocktronics/WHOSON.ANS diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson similarity index 100% rename from mods/themes/luciano_blocktronics/theme.hjson rename to art/themes/luciano_blocktronics/theme.hjson diff --git a/mods/menu.hjson b/config/menu.hjson similarity index 100% rename from mods/menu.hjson rename to config/menu.hjson diff --git a/mods/prompt.hjson b/config/prompt.hjson similarity index 100% rename from mods/prompt.hjson rename to config/prompt.hjson diff --git a/core/config.js b/core/config.js index 367143b6..fa7d346c 100644 --- a/core/config.js +++ b/core/config.js @@ -111,11 +111,8 @@ function init(configPath, options, cb) { } function getDefaultPath() { - const base = miscUtil.resolvePath('~/'); - if(base) { - // e.g. /home/users/joeuser/.config/enigma-bbs/config.hjson - return paths.join(base, '.config', 'enigma-bbs', 'config.hjson'); - } + // e.g. /enigma-bbs-install-path/config/config.hjson + return './config/config.hjson'; } function getDefaultConfig() { @@ -193,15 +190,17 @@ function getDefaultConfig() { }, paths : { - mods : paths.join(__dirname, './../mods/'), + config : paths.join(__dirname, './../config/'), + mods : paths.join(__dirname, './../mods/system'), + userMods : paths.join(__dirname, './../mods/user'), loginServers : paths.join(__dirname, './servers/login/'), contentServers : paths.join(__dirname, './servers/content/'), scannerTossers : paths.join(__dirname, './scanner_tossers/'), mailers : paths.join(__dirname, './mailers/') , - art : paths.join(__dirname, './../mods/art/'), - themes : paths.join(__dirname, './../mods/themes/'), + art : paths.join(__dirname, './../art/general/'), + themes : paths.join(__dirname, './../art/themes/'), logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such db : paths.join(__dirname, './../db/'), modsDb : paths.join(__dirname, './../db/mods/'), diff --git a/core/config_util.js b/core/config_util.js index f078f758..3ed7b98c 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -1,16 +1,15 @@ /* jslint node: true */ 'use strict'; - -var configCache = require('./config_cache.js'); - -var paths = require('path'); +const Config = require('./config.js').config; +const configCache = require('./config_cache.js'); +const paths = require('path'); exports.getFullConfig = getFullConfig; function getFullConfig(filePath, cb) { // |filePath| is assumed to be in 'mods' if it's only a file name if('.' === paths.dirname(filePath)) { - filePath = paths.join(__dirname, '../mods', filePath); + filePath = paths.join(Config.paths.config, filePath); } configCache.getConfig(filePath, function loaded(err, configJson) { diff --git a/core/module_util.js b/core/module_util.js index 67e87306..b730d0ca 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -102,6 +102,7 @@ function loadModulesForCategory(category, iterator, complete) { function getModulePaths() { return [ Config.paths.mods, + Config.paths.userMods, Config.paths.loginServers, Config.paths.contentServers, Config.paths.scannerTossers, diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index 21e1a6d0..14edd518 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -45,7 +45,7 @@ function printUsageAndSetExitCode(errMsg, exitCode) { } function getDefaultConfigPath() { - return resolvePath('~/.config/enigma-bbs/config.hjson'); + return './config/config.hjson'; } function getConfigPath() { diff --git a/mods/abracadabra.js b/mods/system/abracadabra.js similarity index 94% rename from mods/abracadabra.js rename to mods/system/abracadabra.js index a84d2c63..595f84d4 100644 --- a/mods/abracadabra.js +++ b/mods/system/abracadabra.js @@ -1,11 +1,11 @@ /* jslint node: true */ 'use strict'; -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'); +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'); const async = require('async'); const assert = require('assert'); diff --git a/mods/bbs_link.js b/mods/system/bbs_link.js similarity index 96% rename from mods/bbs_link.js rename to mods/system/bbs_link.js index 0cf0a5db..1d5492df 100644 --- a/mods/bbs_link.js +++ b/mods/system/bbs_link.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; +const MenuModule = require('../../core/menu_module.js').MenuModule; +const resetScreen = require('../../core/ansi_term.js').resetScreen; const async = require('async'); const _ = require('lodash'); @@ -10,7 +10,7 @@ const http = require('http'); const net = require('net'); const crypto = require('crypto'); -const packageJson = require('../package.json'); +const packageJson = require('../../package.json'); /* Expected configuration block: diff --git a/mods/bbs_list.js b/mods/system/bbs_list.js similarity index 96% rename from mods/bbs_list.js rename to mods/system/bbs_list.js index ec964a36..8ae94c6c 100644 --- a/mods/bbs_list.js +++ b/mods/system/bbs_list.js @@ -2,18 +2,18 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; +const MenuModule = require('../../core/menu_module.js').MenuModule; const { getModDatabasePath, getTransactionDatabase -} = require('../core/database.js'); +} = require('../../core/database.js'); -const ViewController = require('../core/view_controller.js').ViewController; -const ansi = require('../core/ansi_term.js'); -const theme = require('../core/theme.js'); -const User = require('../core/user.js'); -const stringFormat = require('../core/string_format.js'); +const ViewController = require('../../core/view_controller.js').ViewController; +const ansi = require('../../core/ansi_term.js'); +const theme = require('../../core/theme.js'); +const User = require('../../core/user.js'); +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/erc_client.js b/mods/system/erc_client.js similarity index 97% rename from mods/erc_client.js rename to mods/system/erc_client.js index 02b42ad5..cdc71521 100644 --- a/mods/erc_client.js +++ b/mods/system/erc_client.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('../core/menu_module.js').MenuModule; -const stringFormat = require('../core/string_format.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/file_area_filter_edit.js b/mods/system/file_area_filter_edit.js similarity index 95% rename from mods/file_area_filter_edit.js rename to mods/system/file_area_filter_edit.js index cb3322f9..0ff8b37d 100644 --- a/mods/file_area_filter_edit.js +++ b/mods/system/file_area_filter_edit.js @@ -2,11 +2,11 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('../core/file_base_filter.js'); -const stringFormat = require('../core/string_format.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const ViewController = require('../../core/view_controller.js').ViewController; +const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('../../core/file_base_filter.js'); +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/file_area_list.js b/mods/system/file_area_list.js similarity index 94% rename from mods/file_area_list.js rename to mods/system/file_area_list.js index 0c24f9f8..5359e48e 100644 --- a/mods/file_area_list.js +++ b/mods/system/file_area_list.js @@ -2,23 +2,23 @@ '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_base_area.js'); -const Errors = require('../core/enig_error.js').Errors; -const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; -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 resolveMimeType = require('../core/mime_util.js').resolveMimeType; -const isAnsi = require('../core/string_util.js').isAnsi; -const controlCodesToAnsi = require('../core/color_codes.js').controlCodesToAnsi; +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_base_area.js'); +const Errors = require('../../core/enig_error.js').Errors; +const ErrNotEnabled = require('../../core/enig_error.js').ErrorReasons.NotEnabled; +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 resolveMimeType = require('../../core/mime_util.js').resolveMimeType; +const isAnsi = require('../../core/string_util.js').isAnsi; +const controlCodesToAnsi = require('../../core/color_codes.js').controlCodesToAnsi; // deps const async = require('async'); diff --git a/mods/file_base_area_select.js b/mods/system/file_base_area_select.js similarity index 88% rename from mods/file_base_area_select.js rename to mods/system/file_base_area_select.js index ca182d6c..38b7eba7 100644 --- a/mods/file_base_area_select.js +++ b/mods/system/file_base_area_select.js @@ -2,10 +2,10 @@ 'use strict'; // enigma-bbs -const MenuModule = require('../core/menu_module.js').MenuModule; -const stringFormat = require('../core/string_format.js'); -const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas; -const StatLog = require('../core/stat_log.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const stringFormat = require('../../core/string_format.js'); +const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; +const StatLog = require('../../core/stat_log.js'); // deps const async = require('async'); diff --git a/mods/file_base_download_manager.js b/mods/system/file_base_download_manager.js similarity index 92% rename from mods/file_base_download_manager.js rename to mods/system/file_base_download_manager.js index 382a7305..15a892f1 100644 --- a/mods/file_base_download_manager.js +++ b/mods/system/file_base_download_manager.js @@ -2,14 +2,14 @@ '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'); -const FileAreaWeb = require('../core/file_area_web.js'); +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'); +const FileAreaWeb = require('../../core/file_area_web.js'); // deps const async = require('async'); diff --git a/mods/file_base_search.js b/mods/system/file_base_search.js similarity index 89% rename from mods/file_base_search.js rename to mods/system/file_base_search.js index e984e1a4..d3ebd1db 100644 --- a/mods/file_base_search.js +++ b/mods/system/file_base_search.js @@ -2,10 +2,10 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('../core/file_base_filter.js'); +const 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'); diff --git a/mods/file_base_web_download_manager.js b/mods/system/file_base_web_download_manager.js similarity index 91% rename from mods/file_base_web_download_manager.js rename to mods/system/file_base_web_download_manager.js index d171cfdb..9acad951 100644 --- a/mods/file_base_web_download_manager.js +++ b/mods/system/file_base_web_download_manager.js @@ -2,16 +2,16 @@ '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'); -const FileAreaWeb = require('../core/file_area_web.js'); -const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; -const Config = require('../core/config.js').config; +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'); +const FileAreaWeb = require('../../core/file_area_web.js'); +const ErrNotEnabled = require('../../core/enig_error.js').ErrorReasons.NotEnabled; +const Config = require('../../core/config.js').config; // deps const async = require('async'); diff --git a/mods/file_transfer_protocol_select.js b/mods/system/file_transfer_protocol_select.js similarity index 93% rename from mods/file_transfer_protocol_select.js rename to mods/system/file_transfer_protocol_select.js index 6efa5a93..c731dff2 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/system/file_transfer_protocol_select.js @@ -2,10 +2,10 @@ '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; +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'); diff --git a/mods/last_callers.js b/mods/system/last_callers.js similarity index 92% rename from mods/last_callers.js rename to mods/system/last_callers.js index afb429d8..85d4bef0 100644 --- a/mods/last_callers.js +++ b/mods/system/last_callers.js @@ -2,11 +2,11 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -const StatLog = require('../core/stat_log.js'); -const User = require('../core/user.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 StatLog = require('../../core/stat_log.js'); +const User = require('../../core/user.js'); +const stringFormat = require('../../core/string_format.js'); // deps const moment = require('moment'); diff --git a/mods/msg_area_list.js b/mods/system/msg_area_list.js similarity index 91% rename from mods/msg_area_list.js rename to mods/system/msg_area_list.js index a6a0df4c..51b18953 100644 --- a/mods/msg_area_list.js +++ b/mods/system/msg_area_list.js @@ -2,12 +2,12 @@ '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 displayThemeArt = require('../core/theme.js').displayThemeArt; -const resetScreen = require('../core/ansi_term.js').resetScreen; -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 displayThemeArt = require('../../core/theme.js').displayThemeArt; +const resetScreen = require('../../core/ansi_term.js').resetScreen; +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/msg_area_post_fse.js b/mods/system/msg_area_post_fse.js similarity index 90% rename from mods/msg_area_post_fse.js rename to mods/system/msg_area_post_fse.js index 21b5d068..a0671f85 100644 --- a/mods/msg_area_post_fse.js +++ b/mods/system/msg_area_post_fse.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule; -const persistMessage = require('../core/message_area.js').persistMessage; +const FullScreenEditorModule = require('../../core/fse.js').FullScreenEditorModule; +const persistMessage = require('../../core/message_area.js').persistMessage; const _ = require('lodash'); const async = require('async'); diff --git a/mods/msg_area_reply_fse.js b/mods/system/msg_area_reply_fse.js similarity index 81% rename from mods/msg_area_reply_fse.js rename to mods/system/msg_area_reply_fse.js index d1cb5faa..497c8de7 100644 --- a/mods/msg_area_reply_fse.js +++ b/mods/system/msg_area_reply_fse.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -var FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule; +var FullScreenEditorModule = require('../../core/fse.js').FullScreenEditorModule; exports.getModule = AreaReplyFSEModule; diff --git a/mods/msg_area_view_fse.js b/mods/system/msg_area_view_fse.js similarity index 95% rename from mods/msg_area_view_fse.js rename to mods/system/msg_area_view_fse.js index de4657f1..7cb5a1b8 100644 --- a/mods/msg_area_view_fse.js +++ b/mods/system/msg_area_view_fse.js @@ -2,8 +2,8 @@ 'use strict'; // ENiGMA½ -const FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule; -const Message = require('../core/message.js'); +const FullScreenEditorModule = require('../../core/fse.js').FullScreenEditorModule; +const Message = require('../../core/message.js'); // deps const _ = require('lodash'); diff --git a/mods/msg_conf_list.js b/mods/system/msg_conf_list.js similarity index 89% rename from mods/msg_conf_list.js rename to mods/system/msg_conf_list.js index 91c24de4..06e9d59b 100644 --- a/mods/msg_conf_list.js +++ b/mods/system/msg_conf_list.js @@ -2,12 +2,12 @@ '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 displayThemeArt = require('../core/theme.js').displayThemeArt; -const resetScreen = require('../core/ansi_term.js').resetScreen; -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 displayThemeArt = require('../../core/theme.js').displayThemeArt; +const resetScreen = require('../../core/ansi_term.js').resetScreen; +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/msg_list.js b/mods/system/msg_list.js similarity index 95% rename from mods/msg_list.js rename to mods/system/msg_list.js index bc80e27b..28d1b609 100644 --- a/mods/msg_list.js +++ b/mods/system/msg_list.js @@ -2,11 +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 MessageAreaConfTempSwitcher = require('../core/mod_mixins.js').MessageAreaConfTempSwitcher; +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'); diff --git a/mods/nua.js b/mods/system/nua.js similarity index 91% rename from mods/nua.js rename to mods/system/nua.js index 878e0581..7b4611d6 100644 --- a/mods/nua.js +++ b/mods/system/nua.js @@ -2,12 +2,12 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const User = require('../core/user.js'); -const theme = require('../core/theme.js'); -const login = require('../core/system_menu_method.js').login; -const Config = require('../core/config.js').config; -const messageArea = require('../core/message_area.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const User = require('../../core/user.js'); +const theme = require('../../core/theme.js'); +const login = require('../../core/system_menu_method.js').login; +const Config = require('../../core/config.js').config; +const messageArea = require('../../core/message_area.js'); exports.moduleInfo = { name : 'NUA', diff --git a/mods/onelinerz.js b/mods/system/onelinerz.js similarity index 95% rename from mods/onelinerz.js rename to mods/system/onelinerz.js index 065c0a30..416124c6 100644 --- a/mods/onelinerz.js +++ b/mods/system/onelinerz.js @@ -2,17 +2,17 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; +const MenuModule = require('../../core/menu_module.js').MenuModule; const { getModDatabasePath, getTransactionDatabase -} = require('../core/database.js'); +} = require('../../core/database.js'); -const ViewController = require('../core/view_controller.js').ViewController; -const theme = require('../core/theme.js'); -const ansi = require('../core/ansi_term.js'); -const stringFormat = require('../core/string_format.js'); +const ViewController = require('../../core/view_controller.js').ViewController; +const theme = require('../../core/theme.js'); +const ansi = require('../../core/ansi_term.js'); +const stringFormat = require('../../core/string_format.js'); // deps const sqlite3 = require('sqlite3'); diff --git a/mods/rumorz.js b/mods/system/rumorz.js similarity index 92% rename from mods/rumorz.js rename to mods/system/rumorz.js index 20aace03..e85271dc 100644 --- a/mods/rumorz.js +++ b/mods/system/rumorz.js @@ -2,13 +2,13 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -const theme = require('../core/theme.js'); -const resetScreen = require('../core/ansi_term.js').resetScreen; -const StatLog = require('../core/stat_log.js'); -const renderStringLength = require('../core/string_util.js').renderStringLength; -const stringFormat = require('../core/string_format.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const ViewController = require('../../core/view_controller.js').ViewController; +const theme = require('../../core/theme.js'); +const resetScreen = require('../../core/ansi_term.js').resetScreen; +const StatLog = require('../../core/stat_log.js'); +const renderStringLength = require('../../core/string_util.js').renderStringLength; +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/telnet_bridge.js b/mods/system/telnet_bridge.js similarity index 94% rename from mods/telnet_bridge.js rename to mods/system/telnet_bridge.js index 1dbb1ae9..42c73217 100644 --- a/mods/telnet_bridge.js +++ b/mods/system/telnet_bridge.js @@ -2,9 +2,9 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; -const setSyncTermFontWithAlias = require('../core/ansi_term.js').setSyncTermFontWithAlias; +const MenuModule = require('../../core/menu_module.js').MenuModule; +const resetScreen = require('../../core/ansi_term.js').resetScreen; +const setSyncTermFontWithAlias = require('../../core/ansi_term.js').setSyncTermFontWithAlias; // deps const async = require('async'); diff --git a/mods/upload.js b/mods/system/upload.js similarity index 94% rename from mods/upload.js rename to mods/system/upload.js index 30c84c48..8e545452 100644 --- a/mods/upload.js +++ b/mods/system/upload.js @@ -2,20 +2,20 @@ 'use strict'; // enigma-bbs -const MenuModule = require('../core/menu_module.js').MenuModule; -const stringFormat = require('../core/string_format.js'); -const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas; -const getAreaDefaultStorageDirectory = require('../core/file_base_area.js').getAreaDefaultStorageDirectory; -const scanFile = require('../core/file_base_area.js').scanFile; -const getFileAreaByTag = require('../core/file_base_area.js').getFileAreaByTag; -const getDescFromFileName = require('../core/file_base_area.js').getDescFromFileName; -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 isAnsi = require('../core/string_util.js').isAnsi; +const MenuModule = require('../../core/menu_module.js').MenuModule; +const stringFormat = require('../../core/string_format.js'); +const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; +const getAreaDefaultStorageDirectory = require('../../core/file_base_area.js').getAreaDefaultStorageDirectory; +const scanFile = require('../../core/file_base_area.js').scanFile; +const getFileAreaByTag = require('../../core/file_base_area.js').getFileAreaByTag; +const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; +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 isAnsi = require('../../core/string_util.js').isAnsi; // deps const async = require('async'); diff --git a/mods/user_list.js b/mods/system/user_list.js similarity index 91% rename from mods/user_list.js rename to mods/system/user_list.js index b2a88e79..7b85b331 100644 --- a/mods/user_list.js +++ b/mods/system/user_list.js @@ -1,10 +1,10 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('../core/menu_module.js').MenuModule; -const User = require('../core/user.js'); -const ViewController = require('../core/view_controller.js').ViewController; -const stringFormat = require('../core/string_format.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const User = require('../../core/user.js'); +const ViewController = require('../../core/view_controller.js').ViewController; +const stringFormat = require('../../core/string_format.js'); const moment = require('moment'); const async = require('async'); diff --git a/mods/whos_online.js b/mods/system/whos_online.js similarity index 87% rename from mods/whos_online.js rename to mods/system/whos_online.js index a0a87829..cec3bb4b 100644 --- a/mods/whos_online.js +++ b/mods/system/whos_online.js @@ -2,10 +2,10 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -const getActiveNodeList = require('../core/client_connections.js').getActiveNodeList; -const stringFormat = require('../core/string_format.js'); +const MenuModule = require('../../core/menu_module.js').MenuModule; +const ViewController = require('../../core/view_controller.js').ViewController; +const getActiveNodeList = require('../../core/client_connections.js').getActiveNodeList; +const stringFormat = require('../../core/string_format.js'); // deps const async = require('async'); diff --git a/mods/user/.keep b/mods/user/.keep new file mode 100644 index 00000000..e69de29b From 618ecc07142ad580a865148fd5b0a4364872eae1 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 24 Nov 2017 23:23:15 +0000 Subject: [PATCH 0075/1013] Move modules in mods into /core --- config/menu.hjson | 70 +++++++++---------- {mods/system => core}/abracadabra.js | 10 +-- {mods/system => core}/bbs_link.js | 6 +- {mods/system => core}/bbs_list.js | 14 ++-- {mods/system => core}/erc_client.js | 4 +- .../system => core}/file_area_filter_edit.js | 10 +-- {mods/system => core}/file_area_list.js | 34 ++++----- .../system => core}/file_base_area_select.js | 8 +-- .../file_base_download_manager.js | 16 ++--- {mods/system => core}/file_base_search.js | 8 +-- .../file_base_web_download_manager.js | 20 +++--- .../file_transfer_protocol_select.js | 8 +-- {mods/system => core}/last_callers.js | 10 +-- core/module_util.js | 1 - {mods/system => core}/msg_area_list.js | 12 ++-- {mods/system => core}/msg_area_post_fse.js | 4 +- {mods/system => core}/msg_area_reply_fse.js | 2 +- {mods/system => core}/msg_area_view_fse.js | 4 +- {mods/system => core}/msg_conf_list.js | 12 ++-- {mods/system => core}/msg_list.js | 10 +-- {mods/system => core}/nua.js | 12 ++-- {mods/system => core}/onelinerz.js | 12 ++-- {mods/system => core}/rumorz.js | 14 ++-- {mods/system => core}/telnet_bridge.js | 6 +- {mods/system => core}/upload.js | 28 ++++---- {mods/system => core}/user_list.js | 8 +-- {mods/system => core}/whos_online.js | 8 +-- mods/{user => }/.keep | 0 28 files changed, 175 insertions(+), 176 deletions(-) rename {mods/system => core}/abracadabra.js (94%) rename {mods/system => core}/bbs_link.js (96%) rename {mods/system => core}/bbs_list.js (96%) rename {mods/system => core}/erc_client.js (97%) rename {mods/system => core}/file_area_filter_edit.js (95%) rename {mods/system => core}/file_area_list.js (94%) rename {mods/system => core}/file_base_area_select.js (88%) rename {mods/system => core}/file_base_download_manager.js (92%) rename {mods/system => core}/file_base_search.js (89%) rename {mods/system => core}/file_base_web_download_manager.js (91%) rename {mods/system => core}/file_transfer_protocol_select.js (93%) rename {mods/system => core}/last_callers.js (92%) rename {mods/system => core}/msg_area_list.js (91%) rename {mods/system => core}/msg_area_post_fse.js (90%) rename {mods/system => core}/msg_area_reply_fse.js (81%) rename {mods/system => core}/msg_area_view_fse.js (95%) rename {mods/system => core}/msg_conf_list.js (89%) rename {mods/system => core}/msg_list.js (95%) rename {mods/system => core}/nua.js (91%) rename {mods/system => core}/onelinerz.js (95%) rename {mods/system => core}/rumorz.js (92%) rename {mods/system => core}/telnet_bridge.js (94%) rename {mods/system => core}/upload.js (94%) rename {mods/system => core}/user_list.js (91%) rename {mods/system => core}/whos_online.js (87%) rename mods/{user => }/.keep (100%) diff --git a/config/menu.hjson b/config/menu.hjson index b445ec85..ce5e8a38 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -220,7 +220,7 @@ } newUserApplication: { - module: nua + module: @systemModule:nua art: NUA options: { menuFlags: [ "noHistory" ] @@ -341,7 +341,7 @@ // Canceling this form logs off vs falling back to matrix // newUserApplicationSsh: { - module: nua + module: @systemModule:nua art: NUA fallback: logoff options: { @@ -445,7 +445,7 @@ newUserFeedbackToSysOp: { desc: Feedback to SysOp - module: msg_area_post_fse + module: @systemModule:msg_area_post_fse next: [ { acs: AS2 @@ -579,7 +579,7 @@ fullLoginSequenceLastCallers: { desc: Last Callers - module: last_callers + module: @systemModule:last_callers art: LASTCALL options: { pause: true @@ -589,7 +589,7 @@ } fullLoginSequenceWhosOnline: { desc: Who's Online - module: whos_online + module: @systemModule:whos_online art: WHOSON options: { pause: true } next: fullLoginSequenceOnelinerz @@ -597,7 +597,7 @@ fullLoginSequenceOnelinerz: { desc: Viewing Onelinerz - module: onelinerz + module: @systemModule:onelinerz next: [ { // calls >= 2 @@ -732,7 +732,7 @@ newScanMessageList: { desc: New Messages - module: msg_list + module: @systemModule:msg_list art: NEWMSGS config: { menuViewPost: messageAreaViewPost @@ -772,7 +772,7 @@ } newScanFileBaseList: { - module: file_area_list + module: @systemModule:file_area_list desc: New Files config: { art: { @@ -1019,14 +1019,14 @@ mainMenuLastCallers: { desc: Last Callers - module: last_callers + module: @systemModule:last_callers art: LASTCALL options: { pause: true } } mainMenuWhosOnline: { desc: Who's Online - module: whos_online + module: @systemModule:whos_online art: WHOSON options: { pause: true } } @@ -1045,7 +1045,7 @@ mainMenuUserList: { desc: User Listing - module: user_list + module: @systemModule:user_list art: USERLST form: { 0: { @@ -1166,7 +1166,7 @@ mainMenuFeedbackToSysOp: { desc: Feedback to SysOp - module: msg_area_post_fse + module: @systemModule:msg_area_post_fse config: { art: { header: MSGEHDR @@ -1286,7 +1286,7 @@ mainMenuOnelinerz: { desc: Viewing Onelinerz - module: onelinerz + module: @systemModule:onelinerz options: { cls: true } @@ -1372,7 +1372,7 @@ mainMenuRumorz: { desc: Rumorz - module: rumorz + module: @systemModule:rumorz options: { cls: true } @@ -1458,7 +1458,7 @@ ercClient: { art: erc - module: erc_client + module: @systemModule:erc_client config: { host: localhost port: 5001 @@ -1510,7 +1510,7 @@ bbsList: { desc: Viewing BBS List - module: bbs_list + module: @systemModule:bbs_list options: { cls: true } @@ -1661,7 +1661,7 @@ // doorPimpWars: { desc: Playing PimpWars - module: abracadabra + module: @systemModule:abracadabra config: { name: PimpWars dropFileType: DORINFO @@ -1684,7 +1684,7 @@ // doorTradeWars2002BBSLink: { desc: Playing TW 2002 (BBSLink) - module: bbs_link + module: @systemModule:bbs_link config: { sysCode: XXXXXXXX authCode: XXXXXXXX @@ -1716,7 +1716,7 @@ telnetBridgeAgency: { desc: Connected to HappyLand BBS - module: telnet_bridge + module: @systemModule:telnet_bridge config: { host: agency.bbs.geek.nz } @@ -1779,7 +1779,7 @@ messageAreaChangeCurrentConference: { art: CCHANGE - module: msg_conf_list + module: @systemModule:msg_conf_list form: { 0: { mci: { @@ -1810,7 +1810,7 @@ messageAreaChangeCurrentArea: { // :TODO: rename this art to ACHANGE art: CHANGE - module: msg_area_list + module: @systemModule:msg_area_list form: { 0: { mci: { @@ -1839,7 +1839,7 @@ } messageAreaMessageList: { - module: msg_list + module: @systemModule:msg_list art: MSGLIST config: { menuViewPost: messageAreaViewPost @@ -1875,7 +1875,7 @@ } messageAreaViewPost: { - module: msg_area_view_fse + module: @systemModule:msg_area_view_fse config: { art: { header: MSGVHDR @@ -1991,7 +1991,7 @@ } messageAreaReplyPost: { - module: msg_area_post_fse + module: @systemModule:msg_area_post_fse config: { art: { header: MSGEHDR @@ -2150,7 +2150,7 @@ // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu messageAreaNewPost: { desc: Posting message, - module: msg_area_post_fse + module: @systemModule:msg_area_post_fse config: { art: { header: MSGEHDR @@ -2306,7 +2306,7 @@ mailMenuCreateMessage: { desc: Mailing Someone - module: msg_area_post_fse + module: @systemModule:msg_area_post_fse config: { art: { header: MSGEHDR @@ -2423,7 +2423,7 @@ } mailMenuInbox: { - module: msg_list + module: @systemModule:msg_list art: MSGLIST config: { menuViewPost: messageAreaViewPost @@ -2501,7 +2501,7 @@ } fileBaseListEntries: { - module: file_area_list + module: @systemModule:file_area_list desc: Browsing Files config: { art: { @@ -2669,7 +2669,7 @@ fileBaseBrowseByAreaSelect: { desc: Browsing File Areas - module: file_base_area_select + module: @systemModule:file_base_area_select art: FAREASEL form: { 0: { @@ -2725,7 +2725,7 @@ } fileBaseSearch: { - module: file_base_search + module: @systemModule:file_base_search desc: Searching Files art: FSEARCH form: { @@ -2799,7 +2799,7 @@ fileAreaFilterEditor: { desc: File Filter Editor - module: file_area_filter_edit + module: @systemModule:file_area_filter_edit art: FFILEDT form: { 0: { @@ -2889,7 +2889,7 @@ fileBaseDownloadManager: { desc: Download Manager - module: file_base_download_manager + module: @systemModule:file_base_download_manager config: { art: { queueManager: FDLMGR @@ -2950,7 +2950,7 @@ fileBaseWebDownloadManager: { desc: Web D/L Manager - module: file_base_web_download_manager + module: @systemModule:file_base_web_download_manager config: { art: { queueManager: FWDLMGR @@ -3017,7 +3017,7 @@ fileTransferProtocolSelection: { desc: Protocol selection - module: file_transfer_protocol_select + module: @systemModule:file_transfer_protocol_select art: FPROSEL form: { 0: { @@ -3049,7 +3049,7 @@ fileBaseUploadFiles: { desc: Uploading - module: upload + module: @systemModule:upload config: { art: { options: ULOPTS diff --git a/mods/system/abracadabra.js b/core/abracadabra.js similarity index 94% rename from mods/system/abracadabra.js rename to core/abracadabra.js index 595f84d4..85d1e205 100644 --- a/mods/system/abracadabra.js +++ b/core/abracadabra.js @@ -1,11 +1,11 @@ /* jslint node: true */ 'use strict'; -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'); +const MenuModule = require('./menu_module.js').MenuModule; +const DropFile = require('./dropfile.js').DropFile; +const door = require('./door.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); const async = require('async'); const assert = require('assert'); diff --git a/mods/system/bbs_link.js b/core/bbs_link.js similarity index 96% rename from mods/system/bbs_link.js rename to core/bbs_link.js index 1d5492df..be341115 100644 --- a/mods/system/bbs_link.js +++ b/core/bbs_link.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('../../core/menu_module.js').MenuModule; -const resetScreen = require('../../core/ansi_term.js').resetScreen; +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; const async = require('async'); const _ = require('lodash'); @@ -10,7 +10,7 @@ const http = require('http'); const net = require('net'); const crypto = require('crypto'); -const packageJson = require('../../package.json'); +const packageJson = require('../package.json'); /* Expected configuration block: diff --git a/mods/system/bbs_list.js b/core/bbs_list.js similarity index 96% rename from mods/system/bbs_list.js rename to core/bbs_list.js index 8ae94c6c..33a7ff59 100644 --- a/mods/system/bbs_list.js +++ b/core/bbs_list.js @@ -2,18 +2,18 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; const { getModDatabasePath, getTransactionDatabase -} = require('../../core/database.js'); +} = require('./database.js'); -const ViewController = require('../../core/view_controller.js').ViewController; -const ansi = require('../../core/ansi_term.js'); -const theme = require('../../core/theme.js'); -const User = require('../../core/user.js'); -const stringFormat = require('../../core/string_format.js'); +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const User = require('./user.js'); +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/system/erc_client.js b/core/erc_client.js similarity index 97% rename from mods/system/erc_client.js rename to core/erc_client.js index cdc71521..4fb549f6 100644 --- a/mods/system/erc_client.js +++ b/core/erc_client.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('../../core/menu_module.js').MenuModule; -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/system/file_area_filter_edit.js b/core/file_area_filter_edit.js similarity index 95% rename from mods/system/file_area_filter_edit.js rename to core/file_area_filter_edit.js index 0ff8b37d..4a53096c 100644 --- a/mods/system/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -2,11 +2,11 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const ViewController = require('../../core/view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('../../core/file_base_filter.js'); -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('./file_base_filter.js'); +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/system/file_area_list.js b/core/file_area_list.js similarity index 94% rename from mods/system/file_area_list.js rename to core/file_area_list.js index 5359e48e..3bcfd7c2 100644 --- a/mods/system/file_area_list.js +++ b/core/file_area_list.js @@ -2,23 +2,23 @@ '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_base_area.js'); -const Errors = require('../../core/enig_error.js').Errors; -const ErrNotEnabled = require('../../core/enig_error.js').ErrorReasons.NotEnabled; -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 resolveMimeType = require('../../core/mime_util.js').resolveMimeType; -const isAnsi = require('../../core/string_util.js').isAnsi; -const controlCodesToAnsi = require('../../core/color_codes.js').controlCodesToAnsi; +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const FileEntry = require('./file_entry.js'); +const stringFormat = require('./string_format.js'); +const FileArea = require('./file_base_area.js'); +const Errors = require('./enig_error.js').Errors; +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const ArchiveUtil = require('./archive_util.js'); +const Config = require('./config.js').config; +const DownloadQueue = require('./download_queue.js'); +const FileAreaWeb = require('./file_area_web.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const isAnsi = require('./string_util.js').isAnsi; +const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; // deps const async = require('async'); diff --git a/mods/system/file_base_area_select.js b/core/file_base_area_select.js similarity index 88% rename from mods/system/file_base_area_select.js rename to core/file_base_area_select.js index 38b7eba7..8abb668e 100644 --- a/mods/system/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -2,10 +2,10 @@ 'use strict'; // enigma-bbs -const MenuModule = require('../../core/menu_module.js').MenuModule; -const stringFormat = require('../../core/string_format.js'); -const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; -const StatLog = require('../../core/stat_log.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const stringFormat = require('./string_format.js'); +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const StatLog = require('./stat_log.js'); // deps const async = require('async'); diff --git a/mods/system/file_base_download_manager.js b/core/file_base_download_manager.js similarity index 92% rename from mods/system/file_base_download_manager.js rename to core/file_base_download_manager.js index 15a892f1..7444af56 100644 --- a/mods/system/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -2,14 +2,14 @@ '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'); -const FileAreaWeb = require('../../core/file_area_web.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const DownloadQueue = require('./download_queue.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const Errors = require('./enig_error.js').Errors; +const stringFormat = require('./string_format.js'); +const FileAreaWeb = require('./file_area_web.js'); // deps const async = require('async'); diff --git a/mods/system/file_base_search.js b/core/file_base_search.js similarity index 89% rename from mods/system/file_base_search.js rename to core/file_base_search.js index d3ebd1db..adb618d0 100644 --- a/mods/system/file_base_search.js +++ b/core/file_base_search.js @@ -2,10 +2,10 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const ViewController = require('../../core/view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('../../core/file_base_filter.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('./file_base_filter.js'); // deps const async = require('async'); diff --git a/mods/system/file_base_web_download_manager.js b/core/file_base_web_download_manager.js similarity index 91% rename from mods/system/file_base_web_download_manager.js rename to core/file_base_web_download_manager.js index 9acad951..dea7c5a8 100644 --- a/mods/system/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -2,16 +2,16 @@ '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'); -const FileAreaWeb = require('../../core/file_area_web.js'); -const ErrNotEnabled = require('../../core/enig_error.js').ErrorReasons.NotEnabled; -const Config = require('../../core/config.js').config; +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const DownloadQueue = require('./download_queue.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const Errors = require('./enig_error.js').Errors; +const stringFormat = require('./string_format.js'); +const FileAreaWeb = require('./file_area_web.js'); +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const Config = require('./config.js').config; // deps const async = require('async'); diff --git a/mods/system/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js similarity index 93% rename from mods/system/file_transfer_protocol_select.js rename to core/file_transfer_protocol_select.js index c731dff2..f1b3dbed 100644 --- a/mods/system/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -2,10 +2,10 @@ '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; +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').config; +const stringFormat = require('./string_format.js'); +const ViewController = require('./view_controller.js').ViewController; // deps const async = require('async'); diff --git a/mods/system/last_callers.js b/core/last_callers.js similarity index 92% rename from mods/system/last_callers.js rename to core/last_callers.js index 85d4bef0..3a889468 100644 --- a/mods/system/last_callers.js +++ b/core/last_callers.js @@ -2,11 +2,11 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const ViewController = require('../../core/view_controller.js').ViewController; -const StatLog = require('../../core/stat_log.js'); -const User = require('../../core/user.js'); -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const stringFormat = require('./string_format.js'); // deps const moment = require('moment'); diff --git a/core/module_util.js b/core/module_util.js index b730d0ca..67e87306 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -102,7 +102,6 @@ function loadModulesForCategory(category, iterator, complete) { function getModulePaths() { return [ Config.paths.mods, - Config.paths.userMods, Config.paths.loginServers, Config.paths.contentServers, Config.paths.scannerTossers, diff --git a/mods/system/msg_area_list.js b/core/msg_area_list.js similarity index 91% rename from mods/system/msg_area_list.js rename to core/msg_area_list.js index 51b18953..eaedbef8 100644 --- a/mods/system/msg_area_list.js +++ b/core/msg_area_list.js @@ -2,12 +2,12 @@ '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 displayThemeArt = require('../../core/theme.js').displayThemeArt; -const resetScreen = require('../../core/ansi_term.js').resetScreen; -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const displayThemeArt = require('./theme.js').displayThemeArt; +const resetScreen = require('./ansi_term.js').resetScreen; +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/system/msg_area_post_fse.js b/core/msg_area_post_fse.js similarity index 90% rename from mods/system/msg_area_post_fse.js rename to core/msg_area_post_fse.js index a0671f85..c13f39a6 100644 --- a/mods/system/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const FullScreenEditorModule = require('../../core/fse.js').FullScreenEditorModule; -const persistMessage = require('../../core/message_area.js').persistMessage; +const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +const persistMessage = require('./message_area.js').persistMessage; const _ = require('lodash'); const async = require('async'); diff --git a/mods/system/msg_area_reply_fse.js b/core/msg_area_reply_fse.js similarity index 81% rename from mods/system/msg_area_reply_fse.js rename to core/msg_area_reply_fse.js index 497c8de7..24ee5377 100644 --- a/mods/system/msg_area_reply_fse.js +++ b/core/msg_area_reply_fse.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -var FullScreenEditorModule = require('../../core/fse.js').FullScreenEditorModule; +var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; exports.getModule = AreaReplyFSEModule; diff --git a/mods/system/msg_area_view_fse.js b/core/msg_area_view_fse.js similarity index 95% rename from mods/system/msg_area_view_fse.js rename to core/msg_area_view_fse.js index 7cb5a1b8..02915f79 100644 --- a/mods/system/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -2,8 +2,8 @@ 'use strict'; // ENiGMA½ -const FullScreenEditorModule = require('../../core/fse.js').FullScreenEditorModule; -const Message = require('../../core/message.js'); +const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +const Message = require('./message.js'); // deps const _ = require('lodash'); diff --git a/mods/system/msg_conf_list.js b/core/msg_conf_list.js similarity index 89% rename from mods/system/msg_conf_list.js rename to core/msg_conf_list.js index 06e9d59b..6f42cf36 100644 --- a/mods/system/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -2,12 +2,12 @@ '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 displayThemeArt = require('../../core/theme.js').displayThemeArt; -const resetScreen = require('../../core/ansi_term.js').resetScreen; -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const displayThemeArt = require('./theme.js').displayThemeArt; +const resetScreen = require('./ansi_term.js').resetScreen; +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/system/msg_list.js b/core/msg_list.js similarity index 95% rename from mods/system/msg_list.js rename to core/msg_list.js index 28d1b609..e5a69e80 100644 --- a/mods/system/msg_list.js +++ b/core/msg_list.js @@ -2,11 +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 MessageAreaConfTempSwitcher = require('../../core/mod_mixins.js').MessageAreaConfTempSwitcher; +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const stringFormat = require('./string_format.js'); +const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; // deps const async = require('async'); diff --git a/mods/system/nua.js b/core/nua.js similarity index 91% rename from mods/system/nua.js rename to core/nua.js index 7b4611d6..7939e739 100644 --- a/mods/system/nua.js +++ b/core/nua.js @@ -2,12 +2,12 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const User = require('../../core/user.js'); -const theme = require('../../core/theme.js'); -const login = require('../../core/system_menu_method.js').login; -const Config = require('../../core/config.js').config; -const messageArea = require('../../core/message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const User = require('./user.js'); +const theme = require('./theme.js'); +const login = require('./system_menu_method.js').login; +const Config = require('./config.js').config; +const messageArea = require('./message_area.js'); exports.moduleInfo = { name : 'NUA', diff --git a/mods/system/onelinerz.js b/core/onelinerz.js similarity index 95% rename from mods/system/onelinerz.js rename to core/onelinerz.js index 416124c6..9e89addf 100644 --- a/mods/system/onelinerz.js +++ b/core/onelinerz.js @@ -2,17 +2,17 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; const { getModDatabasePath, getTransactionDatabase -} = require('../../core/database.js'); +} = require('./database.js'); -const ViewController = require('../../core/view_controller.js').ViewController; -const theme = require('../../core/theme.js'); -const ansi = require('../../core/ansi_term.js'); -const stringFormat = require('../../core/string_format.js'); +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const stringFormat = require('./string_format.js'); // deps const sqlite3 = require('sqlite3'); diff --git a/mods/system/rumorz.js b/core/rumorz.js similarity index 92% rename from mods/system/rumorz.js rename to core/rumorz.js index e85271dc..b83853f0 100644 --- a/mods/system/rumorz.js +++ b/core/rumorz.js @@ -2,13 +2,13 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const ViewController = require('../../core/view_controller.js').ViewController; -const theme = require('../../core/theme.js'); -const resetScreen = require('../../core/ansi_term.js').resetScreen; -const StatLog = require('../../core/stat_log.js'); -const renderStringLength = require('../../core/string_util.js').renderStringLength; -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const resetScreen = require('./ansi_term.js').resetScreen; +const StatLog = require('./stat_log.js'); +const renderStringLength = require('./string_util.js').renderStringLength; +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/system/telnet_bridge.js b/core/telnet_bridge.js similarity index 94% rename from mods/system/telnet_bridge.js rename to core/telnet_bridge.js index 42c73217..3232228a 100644 --- a/mods/system/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -2,9 +2,9 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const resetScreen = require('../../core/ansi_term.js').resetScreen; -const setSyncTermFontWithAlias = require('../../core/ansi_term.js').setSyncTermFontWithAlias; +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; +const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias; // deps const async = require('async'); diff --git a/mods/system/upload.js b/core/upload.js similarity index 94% rename from mods/system/upload.js rename to core/upload.js index 8e545452..5a49a0ca 100644 --- a/mods/system/upload.js +++ b/core/upload.js @@ -2,20 +2,20 @@ 'use strict'; // enigma-bbs -const MenuModule = require('../../core/menu_module.js').MenuModule; -const stringFormat = require('../../core/string_format.js'); -const getSortedAvailableFileAreas = require('../../core/file_base_area.js').getSortedAvailableFileAreas; -const getAreaDefaultStorageDirectory = require('../../core/file_base_area.js').getAreaDefaultStorageDirectory; -const scanFile = require('../../core/file_base_area.js').scanFile; -const getFileAreaByTag = require('../../core/file_base_area.js').getFileAreaByTag; -const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; -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 isAnsi = require('../../core/string_util.js').isAnsi; +const MenuModule = require('./menu_module.js').MenuModule; +const stringFormat = require('./string_format.js'); +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const getAreaDefaultStorageDirectory = require('./file_base_area.js').getAreaDefaultStorageDirectory; +const scanFile = require('./file_base_area.js').scanFile; +const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag; +const getDescFromFileName = require('./file_base_area.js').getDescFromFileName; +const ansiGoto = require('./ansi_term.js').goto; +const moveFileWithCollisionHandling = require('./file_util.js').moveFileWithCollisionHandling; +const pathWithTerminatingSeparator = require('./file_util.js').pathWithTerminatingSeparator; +const Log = require('./logger.js').log; +const Errors = require('./enig_error.js').Errors; +const FileEntry = require('./file_entry.js'); +const isAnsi = require('./string_util.js').isAnsi; // deps const async = require('async'); diff --git a/mods/system/user_list.js b/core/user_list.js similarity index 91% rename from mods/system/user_list.js rename to core/user_list.js index 7b85b331..be85c586 100644 --- a/mods/system/user_list.js +++ b/core/user_list.js @@ -1,10 +1,10 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('../../core/menu_module.js').MenuModule; -const User = require('../../core/user.js'); -const ViewController = require('../../core/view_controller.js').ViewController; -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const User = require('./user.js'); +const ViewController = require('./view_controller.js').ViewController; +const stringFormat = require('./string_format.js'); const moment = require('moment'); const async = require('async'); diff --git a/mods/system/whos_online.js b/core/whos_online.js similarity index 87% rename from mods/system/whos_online.js rename to core/whos_online.js index cec3bb4b..6abd76ef 100644 --- a/mods/system/whos_online.js +++ b/core/whos_online.js @@ -2,10 +2,10 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../../core/menu_module.js').MenuModule; -const ViewController = require('../../core/view_controller.js').ViewController; -const getActiveNodeList = require('../../core/client_connections.js').getActiveNodeList; -const stringFormat = require('../../core/string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getActiveNodeList = require('./client_connections.js').getActiveNodeList; +const stringFormat = require('./string_format.js'); // deps const async = require('async'); diff --git a/mods/user/.keep b/mods/.keep similarity index 100% rename from mods/user/.keep rename to mods/.keep From 32557975d9a589b9365bb0e8c51d6c132acd32cb Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 24 Nov 2017 23:33:45 +0000 Subject: [PATCH 0076/1013] Update mod paths in config.js --- core/config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index fa7d346c..506b5c28 100644 --- a/core/config.js +++ b/core/config.js @@ -191,8 +191,7 @@ function getDefaultConfig() { paths : { config : paths.join(__dirname, './../config/'), - mods : paths.join(__dirname, './../mods/system'), - userMods : paths.join(__dirname, './../mods/user'), + mods : paths.join(__dirname, './../mods/'), loginServers : paths.join(__dirname, './servers/login/'), contentServers : paths.join(__dirname, './servers/content/'), From 521e38d7e9ad10669e095767e2a4bb97ebaca60d Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 25 Nov 2017 22:45:19 +0000 Subject: [PATCH 0077/1013] Supply config path to main.js and oputil.js, rather than specific config file --- core/bbs.js | 8 +++++--- core/config.js | 4 ++-- core/config_util.js | 2 +- core/oputil/oputil_common.js | 5 +++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 02058bce..43bf7cf3 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -29,11 +29,12 @@ const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby'; const HELP = `${ENIGMA_COPYRIGHT} usage: main.js +eg : main.js --config /enigma_install_path/config/ valid args: --version : display version --help : displays this help - --config PATH : override default config.hjson path + --config PATH : override default config path `; function printHelpAndExit() { @@ -56,7 +57,8 @@ function main() { return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); }, function initConfig(configPath, configPathSupplied, callback) { - conf.init(resolvePath(configPath), function configInit(err) { + const configFile = configPath + 'config.hjson'; + conf.init(resolvePath(configFile), function configInit(err) { // // If the user supplied a path and we can't read/parse it @@ -65,7 +67,7 @@ function main() { if(err) { if('ENOENT' === err.code) { if(configPathSupplied) { - console.error('Configuration file does not exist: ' + configPath); + console.error('Configuration file does not exist: ' + configFile); } else { configPathSupplied = null; // make non-fatal; we'll go with defaults } diff --git a/core/config.js b/core/config.js index 506b5c28..a33efb99 100644 --- a/core/config.js +++ b/core/config.js @@ -111,8 +111,8 @@ function init(configPath, options, cb) { } function getDefaultPath() { - // e.g. /enigma-bbs-install-path/config/config.hjson - return './config/config.hjson'; + // e.g. /enigma-bbs-install-path/config/ + return './config/'; } function getDefaultConfig() { diff --git a/core/config_util.js b/core/config_util.js index 3ed7b98c..40723d9a 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -7,7 +7,7 @@ const paths = require('path'); exports.getFullConfig = getFullConfig; function getFullConfig(filePath, cb) { - // |filePath| is assumed to be in 'mods' if it's only a file name + // |filePath| is assumed to be in the config path if it's only a file name if('.' === paths.dirname(filePath)) { filePath = paths.join(Config.paths.config, filePath); } diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index 14edd518..e175a166 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -45,11 +45,12 @@ function printUsageAndSetExitCode(errMsg, exitCode) { } function getDefaultConfigPath() { - return './config/config.hjson'; + return './config/'; } function getConfigPath() { - return argv.config ? argv.config : config.getDefaultPath(); + const baseConfigPath = argv.config ? argv.config : config.getDefaultPath(); + return baseConfigPath + 'config.hjson'; } function initConfig(cb) { From 3e268f4b27cb4e6f938931d1dc5a9bbfd1b43531 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 25 Nov 2017 23:08:38 +0000 Subject: [PATCH 0078/1013] Update docs to reflect config changes --- docs/config.md | 4 +--- docs/index.md | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/config.md b/docs/config.md index 4e510b35..e2122f9f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,9 +2,7 @@ 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 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` +The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config.hjson` though you can override the `config.hjson` location 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. ### Creating a Configuration Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: diff --git a/docs/index.md b/docs/index.md index 909ff836..99f83e32 100644 --- a/docs/index.md +++ b/docs/index.md @@ -55,7 +55,7 @@ openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048 ``` ### Create a Minimal Config -The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information. +The main system configuration is handled via `/enigma-bbs-install-path/config/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information. #### Via oputil.js `oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**: @@ -110,7 +110,7 @@ Below is an _example_ configuration. It is recommended that you at least **start Read the Points of Interest below for more info. Also check-out all the other documentation files in the [docs](.) directory. ## Points of Interest -* **The first user you create via register/applying (user ID = 1) will be automatically be added to the `sysops` group. And thus becomes SysOp.** (aka root) +* **The first user you create via register/applying (user ID = 1) will be automatically be added to the `sysops` group, and thus becomes SysOp.** (aka root) * Default port for Telnet is 8888 and for SSH 8889 * Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired. * All data is stored by default in Sqlite3 database files, within the `db` sub folder. Including user data, messages, system logs and file meta data. From b25b96d9dec5a46da8fa1ba2daf4b357f7e1b992 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 Nov 2017 09:09:11 +0000 Subject: [PATCH 0079/1013] * Move default cert path into config * Update docs to reflect changes * More doc tweaks for new structure --- core/config.js | 18 +++++++++--------- docs/config.md | 2 +- docs/index.md | 2 +- docs/menu_system.md | 2 +- docs/modding.md | 2 +- docs/mods.md | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/config.js b/core/config.js index a33efb99..73f0fa28 100644 --- a/core/config.js +++ b/core/config.js @@ -124,8 +124,8 @@ function getDefaultConfig() { loginAttempts : 3, - menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./mods) - promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./mods) + menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) + promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) }, // :TODO: see notes below about 'theme' section - move this! @@ -215,18 +215,18 @@ function getDefaultConfig() { }, ssh : { port : 8889, - enabled : false, // defualt to false as PK/pass in config.hjson are required + enabled : false, // default to false as PK/pass in config.hjson are required // // Private key in PEM format // // Generating your PK: - // > openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048 + // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 // // Then, set servers.ssh.privateKeyPass to the password you use above // in your config.hjson // - privateKeyPem : paths.join(__dirname, './../misc/ssh_private_key.pem'), + privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), firstMenu : 'sshConnected', firstMenuNewUser : 'sshConnectedNewUser', }, @@ -234,8 +234,8 @@ function getDefaultConfig() { port : 8810, // ws:// enabled : false, securePort : 8811, // wss:// - must provide certPem and keyPem - certPem : paths.join(__dirname, './../misc/https_cert.pem'), - keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'), + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), }, }, @@ -271,8 +271,8 @@ function getDefaultConfig() { https : { enabled : false, port : 8443, - certPem : paths.join(__dirname, './../misc/https_cert.pem'), - keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'), + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), } } }, diff --git a/docs/config.md b/docs/config.md index e2122f9f..98a6730f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,7 +2,7 @@ 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 Configuration -The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config.hjson` though you can override the `config.hjson` location 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. +The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location 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. ### Creating a Configuration Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: diff --git a/docs/index.md b/docs/index.md index 99f83e32..2320829c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,7 +51,7 @@ npm install ## Generate a SSH Private Key To utilize the SSH server, a SSH Private Key will need generated. This step can be skipped if you do not wish to enable SSH access. ```bash -openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048 +openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 ``` ### Create a Minimal Config diff --git a/docs/menu_system.md b/docs/menu_system.md index 026ef648..aef51199 100644 --- a/docs/menu_system.md +++ b/docs/menu_system.md @@ -3,7 +3,7 @@ ENiGMA½'s menu system is highly flexible and moddable. The possibilities are al This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` (or whatever you reference in `config.hjson` using the `menuFile` property — see below). By modifying your `menu.hjson` you will be able to create a custom experience unique to your board. -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: +The default `menu.hjson` file lives within the `config` 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 */ diff --git a/docs/modding.md b/docs/modding.md index 9449a91e..609729b4 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -7,7 +7,7 @@ See [Configuration](config.md) See [Menu System](menu_system.md) ## Theming -Take a look at how the default `luciano_blocktronics` theme found under `mods/themes` works! +Take a look at how the default `luciano_blocktronics` theme found under `art/themes` works! TODO document me! diff --git a/docs/mods.md b/docs/mods.md index 3abc7e2f..f73a3fc6 100644 --- a/docs/mods.md +++ b/docs/mods.md @@ -1,5 +1,5 @@ # Mods - +Custom mods should be added to `/enigma-install-path/mods`. ## Existing Mods * **Married Bob Fetch Event**: An event for fetching the latest Married Bob ANSI's for display on you board. ACiDic release [ACD-MB4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MB4E.ZIP). Can also be [found on GitHub](https://github.com/NuSkooler/enigma-bbs-married_bob_evt) From 10044b6749fc08fed83d89653a1acacbf792fa56 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 Nov 2017 18:26:56 +0000 Subject: [PATCH 0080/1013] Switch to xxhash to save farmhash jiggery-pokery when initialising Docker image --- core/art.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/art.js b/core/art.js index 28657fc0..19e0bafe 100644 --- a/core/art.js +++ b/core/art.js @@ -14,7 +14,7 @@ const paths = require('path'); const assert = require('assert'); const iconv = require('iconv-lite'); const _ = require('lodash'); -const farmhash = require('farmhash'); +const xxhash = require('xxhash'); exports.getArt = getArt; exports.getArtFromPath = getArtFromPath; @@ -288,7 +288,7 @@ function display(client, art, options, cb) { } if(!options.disableMciCache) { - artHash = farmhash.hash32(art); + artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE); // see if we have a mciMap cached for this art if(client.mciCache) { diff --git a/package.json b/package.json index 29bd16da..566a454d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "farmhash": "^2.0.4", "fs-extra": "^4.0.1", "graceful-fs": "^4.1.11", "hashids": "^1.1.1", @@ -52,6 +51,7 @@ "uuid": "^3.1.0", "uuid-parse": "^1.0.0", "ws": "^3.1.0", + "xxhash": "^0.2.4", "yazl" : "^2.4.2" }, "devDependencies": {}, From 7af30ea112989531b7b1ea585fa4f4e710cc81da Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 28 Nov 2017 20:17:44 -0700 Subject: [PATCH 0081/1013] Proceed telnet login even if term type is not received [right away]; Allows older DOS terms to function --- core/servers/login/telnet.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 06961800..a6fa0deb 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -541,6 +541,13 @@ function TelnetClient(input, output) { const logger = self.log || Log; return logger.warn(info, `Telnet: ${msg}`); }; + + this.readyNow = () => { + if(!this.didReady) { + this.didReady = true; + this.emit('ready', { firstMenu : Config.loginServers.telnet.firstMenu } ); + } + }; } util.inherits(TelnetClient, baseClient.Client); @@ -633,10 +640,7 @@ TelnetClient.prototype.handleSbCommand = function(evt) { self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout - if(!self.didReady) { - self.didReady = true; - self.emit('ready', { firstMenu : Config.loginServers.telnet.firstMenu } ); - } + self.readyNow(); } else if('new environment' === evt.option) { // // Handling is as follows: @@ -832,6 +836,18 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { client.banner(); this.handleNewClient(client, sock, ModuleInfo); + + // + // Set a timeout and attempt to proceed even if we don't know + // the term type yet, which is the preferred trigger + // for moving along + // + setTimeout( () => { + if(!client.didReady) { + Log.info('Proceeding after 3s without knowing term type'); + client.readyNow(); + } + }, 3000); }); this.server.on('error', err => { From 99898c83524f7684b108e083052022603eac0387 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 29 Nov 2017 12:13:03 -0700 Subject: [PATCH 0082/1013] Add VTXClient note --- art/general/NEWUSER1.ANS | Bin 751 -> 884 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/general/NEWUSER1.ANS b/art/general/NEWUSER1.ANS index 70edd11e6df33bb840d6263ae879e8aa7e16f474..267c331c52c418b8f044ea9709276641b2494dcc 100644 GIT binary patch delta 178 zcmaFQ`h{)7Q^tDFJcW{sRE4CX{PN<|A{~X2qDqDQ(h{(sbhM$hv2kuI3!0Lo^4c7L}zIO}@|gN{yA7fq{`RfI)}>41_(Md=`OlGRFV_ delta 25 hcmeyu_MUaaQ^v_zOiws#nHU%t83P#jCx Date: Wed, 29 Nov 2017 12:24:21 -0700 Subject: [PATCH 0083/1013] Initial 0.0.7-alpha to 0.0.8-alpha upgrade instructions --- UPGRADE.md | 151 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 86 insertions(+), 65 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 802a8092..e1a6f42f 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,65 +1,86 @@ -# Introduction -This document covers basic upgrade notes for major ENiGMA½ version updates. - - -# Before Upgrading -* Always back up your system! -* At least back up the `db` directory and 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`. - - -# Upgrading the Code -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 -``` - - -# 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+](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 -``` - -### 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: - -```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 [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). +# Introduction +This document covers basic upgrade notes for major ENiGMA½ version updates. + + +# Before Upgrading +* Always back up your system! +* At least back up the `db` directory and 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`. + + +# Upgrading the Code +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 +``` + + +# 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.7-alpha to 0.0.8-alpha +ENiGMA 0.0.8-alpha comes with some structure changes: +* Configuration files are defaulted to `./config`. Related, the `--config` option now points to a configuration **directory** +* `./mods/art` has been moved to `./art/general` +* `./mods` is now reserved for actual user addon modules +* Themes have been moved from `./mods/themes` to `./art/themes` + +With the above changes, you'll need to to at least: +* Move your `~/.config/enigma-bbs/config.hjson` to `./config/config.hjson` or utlize the `--config` option. +* Move your `prompt.hjson` and `menu.hjson` (e.g. `myboardname.hjson`) to `./config` +* Move any non-theme art files, and theme directories to their appropriate locations mentioned above +* Move any module directories such as `message_post_evt` to `./mods/` + +# 0.0.6-alpha to 0.0.7-alpha +No issues + +# 0.0.5-alpha to 0.0.6-alpha +No issues + +# 0.0.4-alpha to 0.0.5-alpha +No 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+](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 +``` + +### 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: + +```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 [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 22b09d80187b8816c8c0301fb147dbd9248ebc7a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 30 Nov 2017 11:39:01 -0700 Subject: [PATCH 0084/1013] Fix unpipe crash --- core/telnet_bridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 3232228a..fa1754a5 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -48,7 +48,7 @@ class TelnetClientConnection extends EventEmitter { this.pipeRestored = true; // client may have bailed - if(_.has(this, 'client.term.output')) { + if(null !== _.get(this, 'client.term.output', null)) { if(this.bridgeConnection) { this.client.term.output.unpipe(this.bridgeConnection); } From 1849d275f5cfd2f1cc671bab815e26f101ede6a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 30 Nov 2017 17:15:18 -0700 Subject: [PATCH 0085/1013] Make @systemModule implicit; require @userModule for user modules --- UPGRADE.md | 4 +++ config/menu.hjson | 86 +++++++++++++++++++++++------------------------ core/asset.js | 17 +++++----- docs/doors.md | 4 +-- 4 files changed, 58 insertions(+), 53 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index e1a6f42f..c7651fcd 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -44,11 +44,15 @@ ENiGMA 0.0.8-alpha comes with some structure changes: * `./mods` is now reserved for actual user addon modules * Themes have been moved from `./mods/themes` to `./art/themes` +With the change to the `./mods` directory, `@systemModule` is now implied for `module` declarations in `menu.hjson`. To use a user module in `./mods` you must specify `@userModule`! + With the above changes, you'll need to to at least: * Move your `~/.config/enigma-bbs/config.hjson` to `./config/config.hjson` or utlize the `--config` option. * Move your `prompt.hjson` and `menu.hjson` (e.g. `myboardname.hjson`) to `./config` * Move any non-theme art files, and theme directories to their appropriate locations mentioned above * Move any module directories such as `message_post_evt` to `./mods/` +* Move any certificates, pub/private keys, etc. from `./misc` to `./config` +* Specify user modules as `@userModule:my_module_name` # 0.0.6-alpha to 0.0.7-alpha No issues diff --git a/config/menu.hjson b/config/menu.hjson index ce5e8a38..cac6e647 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -220,7 +220,7 @@ } newUserApplication: { - module: @systemModule:nua + module: nua art: NUA options: { menuFlags: [ "noHistory" ] @@ -341,7 +341,7 @@ // Canceling this form logs off vs falling back to matrix // newUserApplicationSsh: { - module: @systemModule:nua + module: nua art: NUA fallback: logoff options: { @@ -445,7 +445,7 @@ newUserFeedbackToSysOp: { desc: Feedback to SysOp - module: @systemModule:msg_area_post_fse + module: msg_area_post_fse next: [ { acs: AS2 @@ -579,7 +579,7 @@ fullLoginSequenceLastCallers: { desc: Last Callers - module: @systemModule:last_callers + module: last_callers art: LASTCALL options: { pause: true @@ -589,7 +589,7 @@ } fullLoginSequenceWhosOnline: { desc: Who's Online - module: @systemModule:whos_online + module: whos_online art: WHOSON options: { pause: true } next: fullLoginSequenceOnelinerz @@ -597,7 +597,7 @@ fullLoginSequenceOnelinerz: { desc: Viewing Onelinerz - module: @systemModule:onelinerz + module: onelinerz next: [ { // calls >= 2 @@ -709,7 +709,7 @@ fullLoginSequenceNewScan: { desc: Performing New Scan - module: @systemModule:new_scan + module: new_scan art: NEWSCAN next: fullLoginSequenceSysStats config: { @@ -732,7 +732,7 @@ newScanMessageList: { desc: New Messages - module: @systemModule:msg_list + module: msg_list art: NEWMSGS config: { menuViewPost: messageAreaViewPost @@ -772,7 +772,7 @@ } newScanFileBaseList: { - module: @systemModule:file_area_list + module: file_area_list desc: New Files config: { art: { @@ -1019,14 +1019,14 @@ mainMenuLastCallers: { desc: Last Callers - module: @systemModule:last_callers + module: last_callers art: LASTCALL options: { pause: true } } mainMenuWhosOnline: { desc: Who's Online - module: @systemModule:whos_online + module: whos_online art: WHOSON options: { pause: true } } @@ -1045,7 +1045,7 @@ mainMenuUserList: { desc: User Listing - module: @systemModule:user_list + module: user_list art: USERLST form: { 0: { @@ -1066,7 +1066,7 @@ } mainMenuUserConfig: { - module: @systemModule:user_config + module: user_config art: CONFSCR form: { 0: { @@ -1157,7 +1157,7 @@ mainMenuGlobalNewScan: { desc: Performing New Scan - module: @systemModule:new_scan + module: new_scan art: NEWSCAN config: { messageListMenu: newScanMessageList @@ -1166,7 +1166,7 @@ mainMenuFeedbackToSysOp: { desc: Feedback to SysOp - module: @systemModule:msg_area_post_fse + module: msg_area_post_fse config: { art: { header: MSGEHDR @@ -1286,7 +1286,7 @@ mainMenuOnelinerz: { desc: Viewing Onelinerz - module: @systemModule:onelinerz + module: onelinerz options: { cls: true } @@ -1372,7 +1372,7 @@ mainMenuRumorz: { desc: Rumorz - module: @systemModule:rumorz + module: rumorz options: { cls: true } @@ -1458,7 +1458,7 @@ ercClient: { art: erc - module: @systemModule:erc_client + module: erc_client config: { host: localhost port: 5001 @@ -1510,7 +1510,7 @@ bbsList: { desc: Viewing BBS List - module: @systemModule:bbs_list + module: bbs_list options: { cls: true } @@ -1661,7 +1661,7 @@ // doorPimpWars: { desc: Playing PimpWars - module: @systemModule:abracadabra + module: abracadabra config: { name: PimpWars dropFileType: DORINFO @@ -1684,7 +1684,7 @@ // doorTradeWars2002BBSLink: { desc: Playing TW 2002 (BBSLink) - module: @systemModule:bbs_link + module: bbs_link config: { sysCode: XXXXXXXX authCode: XXXXXXXX @@ -1696,7 +1696,7 @@ // DoorParty! support. You'll need to register to obtain credentials doorParty: { desc: Using DoorParty! - module: @systemModule:door_party + module: door_party config: { username: XXXXXXXX password: XXXXXXXX @@ -1707,7 +1707,7 @@ // CombatNet support. You'll need to register at http://combatnet.us/ to obtain credentials combatNet: { desc: Using CombatNet - module: @systemModule:combatnet + module: combatnet config: { bbsTag: CBNxxx password: XXXXXXXXX @@ -1716,7 +1716,7 @@ telnetBridgeAgency: { desc: Connected to HappyLand BBS - module: @systemModule:telnet_bridge + module: telnet_bridge config: { host: agency.bbs.geek.nz } @@ -1779,7 +1779,7 @@ messageAreaChangeCurrentConference: { art: CCHANGE - module: @systemModule:msg_conf_list + module: msg_conf_list form: { 0: { mci: { @@ -1810,7 +1810,7 @@ messageAreaChangeCurrentArea: { // :TODO: rename this art to ACHANGE art: CHANGE - module: @systemModule:msg_area_list + module: msg_area_list form: { 0: { mci: { @@ -1839,7 +1839,7 @@ } messageAreaMessageList: { - module: @systemModule:msg_list + module: msg_list art: MSGLIST config: { menuViewPost: messageAreaViewPost @@ -1875,7 +1875,7 @@ } messageAreaViewPost: { - module: @systemModule:msg_area_view_fse + module: msg_area_view_fse config: { art: { header: MSGVHDR @@ -1991,7 +1991,7 @@ } messageAreaReplyPost: { - module: @systemModule:msg_area_post_fse + module: msg_area_post_fse config: { art: { header: MSGEHDR @@ -2150,7 +2150,7 @@ // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu messageAreaNewPost: { desc: Posting message, - module: @systemModule:msg_area_post_fse + module: msg_area_post_fse config: { art: { header: MSGEHDR @@ -2306,7 +2306,7 @@ mailMenuCreateMessage: { desc: Mailing Someone - module: @systemModule:msg_area_post_fse + module: msg_area_post_fse config: { art: { header: MSGEHDR @@ -2423,7 +2423,7 @@ } mailMenuInbox: { - module: @systemModule:msg_list + module: msg_list art: MSGLIST config: { menuViewPost: messageAreaViewPost @@ -2501,7 +2501,7 @@ } fileBaseListEntries: { - module: @systemModule:file_area_list + module: file_area_list desc: Browsing Files config: { art: { @@ -2669,7 +2669,7 @@ fileBaseBrowseByAreaSelect: { desc: Browsing File Areas - module: @systemModule:file_base_area_select + module: file_base_area_select art: FAREASEL form: { 0: { @@ -2725,7 +2725,7 @@ } fileBaseSearch: { - module: @systemModule:file_base_search + module: file_base_search desc: Searching Files art: FSEARCH form: { @@ -2799,7 +2799,7 @@ fileAreaFilterEditor: { desc: File Filter Editor - module: @systemModule:file_area_filter_edit + module: file_area_filter_edit art: FFILEDT form: { 0: { @@ -2889,7 +2889,7 @@ fileBaseDownloadManager: { desc: Download Manager - module: @systemModule:file_base_download_manager + module: file_base_download_manager config: { art: { queueManager: FDLMGR @@ -2950,7 +2950,7 @@ fileBaseWebDownloadManager: { desc: Web D/L Manager - module: @systemModule:file_base_web_download_manager + module: file_base_web_download_manager config: { art: { queueManager: FWDLMGR @@ -3017,7 +3017,7 @@ fileTransferProtocolSelection: { desc: Protocol selection - module: @systemModule:file_transfer_protocol_select + module: file_transfer_protocol_select art: FPROSEL form: { 0: { @@ -3049,7 +3049,7 @@ fileBaseUploadFiles: { desc: Uploading - module: @systemModule:upload + module: upload config: { art: { options: ULOPTS @@ -3187,7 +3187,7 @@ sendFilesToUser: { desc: Downloading - module: @systemModule:file_transfer + module: file_transfer config: { // defaults - generally use extraArgs protocol: zmodem8kSexyz @@ -3197,7 +3197,7 @@ recvFilesFromUser: { desc: Uploading - module: @systemModule:file_transfer + module: file_transfer config: { // defaults - generally use extraArgs protocol: zmodem8kSexyz @@ -3569,7 +3569,7 @@ "art" : "test.ans" }, "demoFullScreenEditor" : { - "module" : "@systemModule:fse", + "module" : "fse", "config" : { "editorType" : "netMail", "art" : { diff --git a/core/asset.js b/core/asset.js index 0731a1b7..9f2831b7 100644 --- a/core/asset.js +++ b/core/asset.js @@ -21,7 +21,7 @@ const ALL_ASSETS = [ 'art', 'menu', 'method', - 'module', + 'userModule', 'systemMethod', 'systemModule', 'prompt', @@ -58,12 +58,12 @@ function getAssetWithShorthand(spec, defaultType) { assert(_.isString(asset.type)); return asset; - } else { - return { - type : defaultType, - asset : spec, - }; } + + return { + type : defaultType, + asset : spec, + }; } function getArtAsset(spec) { @@ -78,13 +78,14 @@ function getArtAsset(spec) { } function getModuleAsset(spec) { - const asset = getAssetWithShorthand(spec, 'module'); + const asset = getAssetWithShorthand(spec, 'systemModule'); if(!asset) { return null; } - assert( ['module', 'systemModule' ].indexOf(asset.type) > -1); + assert( ['userModule', 'systemModule' ].includes(asset.type) ); + return asset; } diff --git a/docs/doors.md b/docs/doors.md index 0ca55c23..5f72d761 100644 --- a/docs/doors.md +++ b/docs/doors.md @@ -183,7 +183,7 @@ The module `door_party` provides native support for [DoorParty!](http://www.thro ```hjson doorParty: { desc: Using DoorParty! - module: @systemModule:door_party + module: door_party config: { username: XXXXXXXX password: XXXXXXXX @@ -200,7 +200,7 @@ The `combatnet` module provides native support for [CombatNet](http://combatnet. ````hjson combatNet: { desc: Using CombatNet - module: @systemModule:combatnet + module: combatnet config: { bbsTag: CBNxxx password: XXXXXXXXX From 7f80f4a7af7ee481141253f1a5b01921b099e4be Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Dec 2017 19:06:07 -0700 Subject: [PATCH 0086/1013] * Fix 'noHistory' flag and it's usage to be more natural * Add 'popParent' menu flag (works like 'noHistory' used to) --- config/menu.hjson | 14 +++++--------- core/file_base_area_select.js | 2 +- core/file_base_search.js | 2 +- core/menu_stack.js | 9 +++++++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index cac6e647..0f1fa168 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -215,16 +215,14 @@ desc: Applying options: { pause: true - cls: true + cls: true + menuFlags: [ "noHistory" ] } } newUserApplication: { module: nua art: NUA - options: { - menuFlags: [ "noHistory" ] - } next: [ { // Initial SysOp does not send feedback to themselves @@ -333,6 +331,7 @@ options: { pause: true cls: true + menuFlags: [ "noHistory" ] } } @@ -344,9 +343,6 @@ module: nua art: NUA fallback: logoff - options: { - menuFlags: [ "noHistory" ] - } next: newUserFeedbackToSysOpPreamble form: { 0: { @@ -2720,7 +2716,7 @@ art: FBNORES options: { pause: true - menuFlags: [ "noHistory" ] + menuFlags: [ "noHistory", "popParent" ] } } @@ -3011,7 +3007,7 @@ art: FEMPTYQ options: { pause: true - menuFlags: [ "noHistory" ] + menuFlags: [ "noHistory", "popParent" ] } } diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 8abb668e..5ec266fd 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -40,7 +40,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { extraArgs : { filterCriteria : filterCriteria, }, - menuFlags : [ 'noHistory' ], + menuFlags : [ 'popParent' ], }; return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); diff --git a/core/file_base_search.js b/core/file_base_search.js index adb618d0..2ff27f8a 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -112,7 +112,7 @@ exports.getModule = class FileBaseSearch extends MenuModule { extraArgs : { filterCriteria : filterCriteria, }, - menuFlags : [ 'noHistory' ], + menuFlags : [ 'popParent' ], }; return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); diff --git a/core/menu_stack.js b/core/menu_stack.js index f4b29460..b4bebea6 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -129,15 +129,19 @@ module.exports = class MenuStack { } else { self.client.log.debug( { menuName : name }, 'Goto menu module'); + const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags; + if(currentModuleInfo) { // save stack state currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); currentModuleInfo.instance.leave(); - const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags; + if(currentModuleInfo.menuFlags.includes('noHistory')) { + this.pop(); + } - if(menuFlags.includes('noHistory')) { + if(menuFlags.includes('popParent')) { this.pop().instance.leave(); // leave & remove current } } @@ -146,6 +150,7 @@ module.exports = class MenuStack { name : name, instance : modInst, extraArgs : loadOpts.extraArgs, + menuFlags : menuFlags, }); // restore previous state if requested From 1c5a00313b1d38ceb0cedfb0475f6f0e3090ecf4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 12 Dec 2017 21:32:01 -0700 Subject: [PATCH 0087/1013] Minor adjustment on tags to allow comma separated/etc. --- core/file_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 32903315..6edd7ef8 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -528,8 +528,8 @@ module.exports = class FileEntry { } 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(','); + // build list of quoted tags; filter.tags comes in as a space and/or comma separated values + const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${tag}"` ).join(','); appendWhereClause( `f.file_id IN ( From fc40641eebfb80a7b44504888afb75a6b4affdfa Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 31 Dec 2017 17:54:11 -0700 Subject: [PATCH 0088/1013] NetMail avail to oputil & export - WIP --- .eslintrc.json | 3 +- config/prompt.hjson | 14 + core/config.js | 17 +- core/ftn_mail_packet.js | 52 +- core/ftn_util.js | 15 + core/message.js | 6 +- core/oputil/oputil_file_base.js | 2 +- core/oputil/oputil_help.js | 8 + core/oputil/oputil_main.js | 20 +- core/oputil/oputil_message_base.js | 150 ++++ core/scanner_tossers/ftn_bso.js | 1078 ++++++++++++++++++---------- package.json | 16 +- 12 files changed, 961 insertions(+), 420 deletions(-) create mode 100644 core/oputil/oputil_message_base.js diff --git a/.eslintrc.json b/.eslintrc.json index c7757f0d..5e9b45b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,6 +24,7 @@ "error", "always" ], - "comma-dangle": 0 + "comma-dangle": 0, + "no-trailing-spaces" :"warn" } } \ No newline at end of file diff --git a/config/prompt.hjson b/config/prompt.hjson index 83f01ec5..b6bf6691 100644 --- a/config/prompt.hjson +++ b/config/prompt.hjson @@ -72,6 +72,20 @@ } } + loginSequenceFlavorSelect: { + art: LOGINSEL + mci: { + TM1: { + argName: promptValue + items: [ "yes", "no" ] + focus: true + focusItemIndex: 1 + hotKeys: { Y: 0, N: 1 } + hotKeySubmit: true + } + } + } + loginGlobalNewScan: { art: GNSPMPT mci: { diff --git a/core/config.js b/core/config.js index 73f0fa28..34be676e 100644 --- a/core/config.js +++ b/core/config.js @@ -19,7 +19,7 @@ function hasMessageConferenceAndArea(config) { assert(_.isObject(config.messageConferences)); // we create one ourself! const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { - return 'system_internal' !== confTag; + return 'system_internal' !== confTag; }); if(0 === nonInternalConfs.length) { @@ -53,12 +53,12 @@ function init(configPath, options, cb) { 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, options); @@ -70,7 +70,7 @@ function init(configPath, options, cb) { }); }, function mergeWithDefaultConfig(configJson, callback) { - + const mergedConfig = _.mergeWith( getDefaultConfig(), configJson, (conf1, conf2) => { @@ -616,10 +616,11 @@ function getDefaultConfig() { scannerTossers : { ftn_bso : { paths : { - outbound : paths.join(__dirname, './../mail/ftn_out/'), - inbound : paths.join(__dirname, './../mail/ftn_in/'), - secInbound : paths.join(__dirname, './../mail/ftn_secin/'), - reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. + outbound : paths.join(__dirname, './../mail/ftn_out/'), + inbound : paths.join(__dirname, './../mail/ftn_in/'), + secInbound : paths.join(__dirname, './../mail/ftn_secin/'), + reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. + outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), // set 'retain' to a valid path to keep good pkt files }, diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 095628ea..137a2583 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -393,9 +393,19 @@ function Packet(options) { }; function addKludgeLine(line) { - const sepIndex = line.indexOf(':'); - const key = line.substr(0, sepIndex).toUpperCase(); - const value = line.substr(sepIndex + 1).trim(); + // + // We have to special case INTL/TOPT/FMPT as they don't contain + // a ':' name/value separator like the rest of the kludge lines... because stupdity. + // + let key = line.substr(0, 4); + let value; + if( ['INTL', 'TOPT', 'FMPT' ].includes(key)) { + value = line.substr(4).trim(); + } else { + const sepIndex = line.indexOf(':'); + key = line.substr(0, sepIndex).toUpperCase(); + value = line.substr(sepIndex + 1).trim(); + } // // Allow mapped value to be either a key:value if there is only @@ -639,7 +649,7 @@ function Packet(options) { this.getMessageEntryBuffer = function(message, options, cb) { - function getAppendMeta(k, m) { + function getAppendMeta(k, m, sepChar=':') { let append = ''; if(m) { let a = m; @@ -647,7 +657,7 @@ function Packet(options) { a = [ a ]; } a.forEach(v => { - append += `${k}: ${v}\r`; + append += `${k}${sepChar} ${v}\r`; }); } return append; @@ -693,10 +703,21 @@ function Packet(options) { msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) } + // :TODO: DRY with similar function in this file! Object.keys(message.meta.FtnKludge).forEach(k => { - // we want PATH to be last - if('PATH' !== k) { - msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); + switch(k) { + case 'PATH' : + break; // skip & save for last + + case 'FMPT' : + case 'TOPT' : + case 'INTL' : + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar + break; + + default : + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); + break; } }); @@ -810,14 +831,14 @@ function Packet(options) { // :TODO: Put this in it's own method let msgBody = ''; - function appendMeta(k, m) { + function appendMeta(k, m, sepChar=':') { if(m) { let a = m; if(!_.isArray(a)) { a = [ a ]; } a.forEach(v => { - msgBody += `${k}: ${v}\r`; + msgBody += `${k}${sepChar} ${v}\r`; }); } } @@ -832,9 +853,14 @@ function Packet(options) { } Object.keys(message.meta.FtnKludge).forEach(k => { - // we want PATH to be last - if('PATH' !== k) { - appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); + switch(k) { + case 'PATH' : break; // skip & save for last + + case 'FMPT' : + case 'TOPT' : + case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar + + default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; } }); diff --git a/core/ftn_util.js b/core/ftn_util.js index 4d779c4a..68ddf343 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -26,6 +26,7 @@ exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; exports.getOrigin = getOrigin; exports.getTearLine = getTearLine; exports.getVia = getVia; +exports.getIntl = getIntl; exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; @@ -222,6 +223,20 @@ function getVia(address) { return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; } +// +// Creates a INTL kludge value as per FTS-4001 +// http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac +// +function getIntl(toAddress, fromAddress) { + // + // INTL differs from 'standard' kludges in that there is no ':' after "INTL" + // + // ""INTL "" "" + // "...These addresses shall be given on the form :/" + // + return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`; +} + function getAbbreviatedNetNodeList(netNodes) { let abbrList = ''; let currNet; diff --git a/core/message.js b/core/message.js index a251ef50..b5b1e541 100644 --- a/core/message.js +++ b/core/message.js @@ -124,11 +124,13 @@ Message.FtnPropertyNames = { // Note: kludges are stored with their names as-is Message.prototype.setLocalToUserId = function(userId) { - this.meta.System.local_to_user_id = userId; + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; }; Message.prototype.setLocalFromUserId = function(userId) { - this.meta.System.local_from_user_id = userId; + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; }; Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index a00f0889..d6a18026 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -8,7 +8,7 @@ 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 Errors = require('../../core/enig_error.js').Errors; +const Errors = require('../enig_error.js').Errors; const async = require('async'); const fs = require('graceful-fs'); diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 1a2b42bf..b4c48267 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -84,6 +84,14 @@ general information: FILENAME_WC filename with * and ? wildcard support. may match 0:n entries SHA full or partial SHA-256 FILE_ID a file identifier. see file.sqlite3 +`, + MessageBase : + `usage: oputil.js mb [] + + actions: + areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s) + one or more commands may be supplied. commands that are multi + part such as "%COMPRESS ZIP" should be quoted. ` }; diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js index 83af6b5e..aa373ef2 100644 --- a/core/oputil/oputil_main.js +++ b/core/oputil/oputil_main.js @@ -7,6 +7,7 @@ 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 handleMessageBaseCommand = require('./oputil_message_base.js').handleMessageBaseCommand; const handleConfigCommand = require('./oputil_config.js').handleConfigCommand; const getHelpFor = require('./oputil_help.js').getHelpFor; @@ -26,19 +27,10 @@ module.exports = function() { } switch(argv._[0]) { - case 'user' : - handleUserCommand(); - break; - - case 'config' : - handleConfigCommand(); - break; - - case 'fb' : - handleFileBaseCommand(); - break; - - default: - return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND); + case 'user' : return handleUserCommand(); + case 'config' : return handleConfigCommand(); + case 'fb' : return handleFileBaseCommand(); + case 'mb' : return handleMessageBaseCommand(); + default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND); } }; diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js new file mode 100644 index 00000000..77a63e9f --- /dev/null +++ b/core/oputil/oputil_message_base.js @@ -0,0 +1,150 @@ +/* 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 Address = require('../ftn_address.js'); +const Errors = require('../enig_error.js').Errors; + +// deps +const async = require('async'); + +exports.handleMessageBaseCommand = handleMessageBaseCommand; + +function areaFix() { + // + // oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS] + // + if(argv._.length < 3) { + return printUsageAndSetExitCode( + getHelpFor('MessageBase'), + ExitCodes.ERROR + ); + } + + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases(callback); + }, + function validateAddress(callback) { + const addrArg = argv._.slice(-1)[0]; + const ftnAddr = Address.fromString(addrArg); + + if(!ftnAddr) { + return callback(Errors.Invalid(`"${addrArg}" is not a valid FTN address`)); + } + + // + // We need to validate the address targets a system we know unless + // the --force option is used + // + // :TODO: + return callback(null, ftnAddr); + }, + function fetchFromUser(ftnAddr, callback) { + // + // --from USER || +op from system + // + // If possible, we want the user ID of the supplied user as well + // + const User = require('../user.js'); + + if(argv.from) { + User.getUserIdAndName(argv.from, (err, userId, fromName) => { + if(err) { + return callback(null, ftnAddr, argv.from, 0); + } + + // fromName is the same as argv.from, but case may be differnet (yet correct) + return callback(null, ftnAddr, fromName, userId); + }); + } else { + User.getUserName(User.RootUserID, (err, fromName) => { + return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID); + }); + } + }, + function createMessage(ftnAddr, fromName, fromUserId, callback) { + // + // Build message as commands separated by line feed + // + // We need to remove quotes from arguments. These are required + // in the case of e.g. removing an area: "-SOME_AREA" would end + // up confusing minimist, therefor they must be quoted: "'-SOME_AREA'" + // + const messageBody = argv._.slice(2, -1).map(arg => { + return arg.replace(/["']/g, ''); + }).join('\n') + '\n'; + + const Message = require('../message.js'); + + const message = new Message({ + toUserName : argv.to || 'AreaFix', + fromUserName : fromName, + subject : argv.password || '', + message : messageBody, + areaTag : Message.WellKnownAreaTags.Private, // mark private + meta : { + FtnProperty : { + [ Message.FtnPropertyNames.FtnDestZone ] : ftnAddr.zone, + [ Message.FtnPropertyNames.FtnDestNetwork ] : ftnAddr.net, + [ Message.FtnPropertyNames.FtnDestNode ] : ftnAddr.node, + } + } + }); + + if(ftnAddr.point) { + message.meta.FtnProperty[Message.FtnPropertyNames.FtnDestPoint] = ftnAddr.point; + } + + if(0 !== fromUserId) { + message.setLocalFromUserId(fromUserId); + } + + return callback(null, message); + }, + function persistMessage(message, callback) { + // + // :TODO: + // - Persist message in private outgoing (sysop out box) + // - Make necessary changes such that the message is exported properly + // + console.log(message); + message.persist(err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`); + } + } + ); +} + +function handleMessageBaseCommand() { + + function errUsage() { + return printUsageAndSetExitCode( + getHelpFor('MessageBase'), + ExitCodes.ERROR + ); + } + + if(true === argv.help) { + return errUsage(); + } + + const action = argv._[1]; + + return({ + areafix : areaFix, + }[action] || errUsage)(); +} \ No newline at end of file diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index f4e5091d..996dd3ef 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -43,14 +43,14 @@ exports.moduleInfo = { /* :TODO: - * Support (approx) max bundle size + * Support (approx) max bundle size * 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 - -*/ + +*/ exports.getModule = FTNMessageScanTossModule; @@ -58,32 +58,31 @@ const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:|@immediate)([^\0]+)?$/; function FTNMessageScanTossModule() { MessageScanTossModule.call(this); - - let self = this; + + const self = this; this.archUtil = ArchiveUtil.getInstance(); if(_.has(Config, 'scannerTossers.ftn_bso')) { - this.moduleConfig = Config.scannerTossers.ftn_bso; + this.moduleConfig = Config.scannerTossers.ftn_bso; } - + this.getDefaultNetworkName = function() { if(this.moduleConfig.defaultNetwork) { return this.moduleConfig.defaultNetwork.toLowerCase(); } - + const networkNames = Object.keys(Config.messageNetworks.ftn.networks); if(1 === networkNames.length) { return networkNames[0].toLowerCase(); } }; - - + this.getDefaultZone = function(networkName) { if(_.isNumber(Config.messageNetworks.ftn.networks[networkName].defaultZone)) { return Config.messageNetworks.ftn.networks[networkName].defaultZone; } - + // non-explicit: default to local address zone const networkLocalAddress = Config.messageNetworks.ftn.networks[networkName].localAddress; if(networkLocalAddress) { @@ -91,45 +90,45 @@ function FTNMessageScanTossModule() { return addr.zone; } }; - + /* this.isDefaultDomainZone = function(networkName, address) { - const defaultNetworkName = this.getDefaultNetworkName(); + const defaultNetworkName = this.getDefaultNetworkName(); return(networkName === defaultNetworkName && address.zone === this.moduleConfig.defaultZone); }; */ - + this.getNetworkNameByAddress = function(remoteAddress) { return _.findKey(Config.messageNetworks.ftn.networks, network => { - const localAddress = Address.fromString(network.localAddress); + const localAddress = Address.fromString(network.localAddress); return !_.isUndefined(localAddress) && localAddress.isEqual(remoteAddress); }); }; - + this.getNetworkNameByAddressPattern = function(remoteAddressPattern) { return _.findKey(Config.messageNetworks.ftn.networks, network => { - const localAddress = Address.fromString(network.localAddress); + const localAddress = Address.fromString(network.localAddress); return !_.isUndefined(localAddress) && localAddress.isPatternMatch(remoteAddressPattern); - }); + }); }; - + this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper return _.findKey(Config.messageNetworks.ftn.areas, areaConf => { return areaConf.tag.toUpperCase() === ftnAreaTag; }); }; - + this.getExportType = function(nodeConfig) { - return _.isString(nodeConfig.exportType) ? nodeConfig.exportType.toLowerCase() : 'crash'; + return _.isString(nodeConfig.exportType) ? nodeConfig.exportType.toLowerCase() : 'crash'; }; - + /* this.getSeenByAddresses = function(messageSeenBy) { if(!_.isArray(messageSeenBy)) { messageSeenBy = [ messageSeenBy ]; } - + let seenByAddrs = []; messageSeenBy.forEach(sb => { seenByAddrs = seenByAddrs.concat(ftnUtil.parseAbbreviatedNetNodeList(sb)); @@ -137,13 +136,13 @@ function FTNMessageScanTossModule() { return seenByAddrs; }; */ - + this.messageHasValidMSGID = function(msg) { - return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0; + return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0; }; - + /* - this.getOutgoingPacketDir = function(networkName, destAddress) { + this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { let dir = this.moduleConfig.paths.outbound; if(!this.isDefaultDomainZone(networkName, destAddress)) { const hexZone = `000${destAddress.zone.toString(16)}`.substr(-3); @@ -152,31 +151,31 @@ function FTNMessageScanTossModule() { return dir; }; */ - - this.getOutgoingPacketDir = function(networkName, destAddress) { + + this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { networkName = networkName.toLowerCase(); - + let dir = this.moduleConfig.paths.outbound; - - const defaultNetworkName = this.getDefaultNetworkName(); + + const defaultNetworkName = this.getDefaultNetworkName(); const defaultZone = this.getDefaultZone(networkName); - + let zoneExt; if(defaultZone !== destAddress.zone) { zoneExt = '.' + `000${destAddress.zone.toString(16)}`.substr(-3); } else { zoneExt = ''; } - + if(defaultNetworkName === networkName) { dir = paths.join(dir, `outbound${zoneExt}`); } else { dir = paths.join(dir, `${networkName}${zoneExt}`); } - + return dir; }; - + this.getOutgoingPacketFileName = function(basePath, messageId, isTemp, fileCase) { // // Generating an outgoing packet file name comes with a few issues: @@ -189,15 +188,15 @@ function FTNMessageScanTossModule() { // There are a lot of systems in use here for the name: // * HEX CRC16/32 of data // * HEX UNIX timestamp - // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second) + // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second) // See https://groups.google.com/forum/#!searchin/alt.bbs.mystic/netmail$20filename/alt.bbs.mystic/m1xLnY8i1pU/YnG2excdl6MJ // * SBBSEcho uses DDHHMMSS - see https://github.com/ftnapps/pkg-sbbs/blob/master/docs/fidonet.txt // * We already have a system for 8-character serial number gernation that is // used for e.g. in FTS-0009.001 MSGIDs... let's use that! - // + // const name = ftnUtil.getMessageSerialNumber(messageId); const ext = (true === isTemp) ? 'pk_' : 'pkt'; - + let fileName = `${name}.${ext}`; if('upper' === fileCase) { fileName = fileName.toUpperCase(); @@ -205,10 +204,10 @@ function FTNMessageScanTossModule() { return paths.join(basePath, fileName); }; - + this.getOutgoingFlowFileExtension = function(destAddress, flowType, exportType, fileCase) { let ext; - + switch(flowType) { case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break; case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break; @@ -220,20 +219,20 @@ function FTNMessageScanTossModule() { if('upper' === fileCase) { ext = ext.toUpperCase(); } - - return ext; + + return ext; }; this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { - let basename; - + let basename; + const ext = self.getOutgoingFlowFileExtension( - destAddress, - flowType, - exportType, + destAddress, + flowType, + exportType, fileCase ); - + if(destAddress.point) { } else { @@ -242,32 +241,32 @@ function FTNMessageScanTossModule() { // node. This seems to match what Mystic does // basename = - `0000${destAddress.net.toString(16)}`.substr(-4) + - `0000${destAddress.node.toString(16)}`.substr(-4); + `0000${destAddress.net.toString(16)}`.substr(-4) + + `0000${destAddress.node.toString(16)}`.substr(-4); } if('upper' === fileCase) { basename = basename.toUpperCase(); } - + return paths.join(basePath, `${basename}.${ext}`); }; - + this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { const appendLines = fileRefs.reduce( (content, ref) => { return content + `${directive}${ref}\n`; }, ''); - + fs.appendFile(filePath, appendLines, err => { cb(err); }); }; - + this.getOutgoingBundleFileName = function(basePath, sourceAddress, destAddress, cb) { // // Base filename is constructed as such: - // * If this |destAddress| is *not* a point address, we use NNNNnnnn where - // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded + // * If this |destAddress| is *not* a point address, we use NNNNnnnn where + // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded // hex of dest node - source node. // * If |destAddress| is a point, NNNN becomes 0000 and nnnn becomes 'p' + // 3 digit 0 padded hex point @@ -279,19 +278,19 @@ function FTNMessageScanTossModule() { const pointHex = `000${destAddress.point}`.substr(-3); basename = `0000p${pointHex}`; } else { - basename = - `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) + - `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4); + basename = + `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) + + `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4); } - + // // We need to now find the first entry that does not exist starting // with dd0 to ddz // - const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); + const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); let fileName = `${basename}.${moment().format('dd').toLowerCase()}`; async.detectSeries(EXT_SUFFIXES, (suffix, callback) => { - const checkFileName = fileName + suffix; + const checkFileName = fileName + suffix; fs.stat(paths.join(basePath, checkFileName), err => { callback(null, (err && 'ENOENT' === err.code) ? true : false); }); @@ -299,32 +298,40 @@ function FTNMessageScanTossModule() { if(finalSuffix) { return cb(null, paths.join(basePath, fileName + finalSuffix)); } - - return cb(new Error('Could not acquire a bundle filename!')); + + return cb(new Error('Could not acquire a bundle filename!')); }); }; - + this.prepareMessage = function(message, options) { // // Set various FTN kludges/etc. // message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - - message.meta.FtnProperty.ftn_orig_node = options.network.localAddress.node; - message.meta.FtnProperty.ftn_dest_node = options.destAddress.node; - message.meta.FtnProperty.ftn_orig_network = options.network.localAddress.net; - message.meta.FtnProperty.ftn_dest_network = options.destAddress.net; - message.meta.FtnProperty.ftn_cost = 0; - message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); - // :TODO: Need an explicit isNetMail() check - let ftnAttribute = + // :TODO: Only set DEST information for EchoMail - NetMail should already have it in message + message.meta.FtnProperty.ftn_orig_node = options.network.localAddress.node; + //message.meta.FtnProperty.ftn_dest_node = options.destAddress.node; + message.meta.FtnProperty.ftn_orig_network = options.network.localAddress.net; + //message.meta.FtnProperty.ftn_dest_network = options.destAddress.net; + message.meta.FtnProperty.ftn_cost = 0; + + // tear line and origin can both go in EchoMail & NetMail + message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); + message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(options.network.localAddress); + + // :TODO: Need an explicit isNetMail() check or more generally + let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system - + if(message.isPrivate()) { + // These should be set for Private/NetMail already + assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_node))); + assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_network))); + ftnAttribute |= ftnMailPacket.Packet.Attribute.Private; - + // // NetMail messages need a FRL-1005.001 "Via" line // http://ftsc.org/docs/frl-1005.001 @@ -334,7 +341,25 @@ function FTNMessageScanTossModule() { } message.meta.FtnKludge.Via = message.meta.FtnKludge.Via || []; message.meta.FtnKludge.Via.push(ftnUtil.getVia(options.network.localAddress)); + + // + // We need to set INTL, and possibly FMPT and/or TOPT + // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac + // + message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.network.localAddress, options.destAddress); + + if(_.isNumber(options.network.localAddress.point) && options.network.localAddress.point > 0) { + message.meta.FtnKludge.FMPT = options.network.localAddress.point; + } + + if(_.get(message, 'meta.FtnProperty.ftn_dest_point', 0) > 0) { + message.meta.FtnKludge.TOPT = message.meta.FtnProperty.ftn_dest_point; + } } else { + // We need to set some destination info for EchoMail + message.meta.FtnProperty.ftn_dest_node = options.destAddress.node; + message.meta.FtnProperty.ftn_dest_network = options.destAddress.net; + // // Set appropriate attribute flag for export type // @@ -343,52 +368,51 @@ function FTNMessageScanTossModule() { case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break; // :TODO: Others? } - + // // EchoMail requires some additional properties & kludges - // - message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(options.network.localAddress); - message.meta.FtnProperty.ftn_area = Config.messageNetworks.ftn.areas[message.areaTag].tag; - + // + message.meta.FtnProperty.ftn_area = Config.messageNetworks.ftn.areas[message.areaTag].tag; + // // When exporting messages, we should create/update SEEN-BY // with remote address(s) we are exporting to. // - const seenByAdditions = + const seenByAdditions = [ `${options.network.localAddress.net}/${options.network.localAddress.node}` ].concat(Config.messageNetworks.ftn.areas[message.areaTag].uplinks); - message.meta.FtnProperty.ftn_seen_by = + message.meta.FtnProperty.ftn_seen_by = ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); // // And create/update PATH for ourself // - message.meta.FtnKludge.PATH = + message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, options.network.localAddress); } - + message.meta.FtnProperty.ftn_attr_flags = ftnAttribute; - + // // Additional kludges // // Check for existence of MSGID as we may already have stored it from a previous // export that failed to finish - // + // if(!message.meta.FtnKludge.MSGID) { message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier(message, options.network.localAddress); } - + message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset(); - + // // According to FSC-0046: - // + // // "When a Conference Mail processor adds a TID to a message, it may not // add a PID. An existing TID should, however, be replaced. TIDs follow // the same format used for PIDs, as explained above." // - message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier(); - + message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier(); + // // Determine CHRS and actual internal encoding name. If the message has an // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. @@ -403,77 +427,77 @@ function FTNMessageScanTossModule() { encoding = encFromChars; } } - + // // Ensure we ended up with something useable. If not, back to utf8! // if(!iconv.encodingExists(encoding)) { Log.debug( { encoding : encoding }, 'Unknown encoding. Falling back to utf8'); encoding = 'utf8'; - } - + } + options.encoding = encoding; // save for later message.meta.FtnKludge.CHRS = ftnUtil.getCharacterSetIdentifierByEncoding(encoding); - // :TODO: FLAGS kludge? + // :TODO: FLAGS kludge? }; - + this.setReplyKludgeFromReplyToMsgId = function(message, cb) { // // Look up MSGID kludge for |message.replyToMsgId|, if any. // If found, we can create a REPLY kludge with the previously // discovered MSGID. // - + if(0 === message.replyToMsgId) { return cb(null); // nothing to do } - - Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => { + + Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => { if(!err) { - assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')'); + assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')'); // got a MSGID - create a REPLY message.meta.FtnKludge.REPLY = msgIdVal; } - + cb(null); // this method always passes - }); + }); }; - + // check paths, Addresses, etc. this.isAreaConfigValid = function(areaConfig) { if(!areaConfig || !_.isString(areaConfig.tag) || !_.isString(areaConfig.network)) { return false; } - + if(_.isString(areaConfig.uplinks)) { areaConfig.uplinks = areaConfig.uplinks.split(' '); } - + return (_.isArray(areaConfig.uplinks)); }; - - + + this.hasValidConfiguration = function() { if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config, 'messageNetworks.ftn.areas')) { return false; } - + // :TODO: need to check more! - + return true; }; - + this.parseScheduleString = function(schedStr) { if(!schedStr) { return; // nothing to parse! } - + let schedule = {}; - + const m = SCHEDULE_REGEXP.exec(schedStr); if(m) { schedStr = schedStr.substr(0, m.index).trim(); - + if('@watch:' === m[1]) { schedule.watchFile = m[2]; } else if('@immediate' === m[1]) { @@ -485,46 +509,112 @@ function FTNMessageScanTossModule() { const sched = later.parse.text(schedStr); if(-1 === sched.error) { schedule.sched = sched; - } + } } - + // return undefined if we couldn't parse out anything useful if(!_.isEmpty(schedule)) { return schedule; - } + } }; - + this.getAreaLastScanId = function(areaTag, cb) { - const sql = + const sql = `SELECT area_tag, message_id FROM message_area_last_scan WHERE scan_toss = "ftn_bso" AND area_tag = ? LIMIT 1;`; - + msgDb.get(sql, [ areaTag ], (err, row) => { - cb(err, row ? row.message_id : 0); + return cb(err, row ? row.message_id : 0); }); }; - + this.setAreaLastScanId = function(areaTag, lastScanId, cb) { const sql = `REPLACE INTO message_area_last_scan (scan_toss, area_tag, message_id) VALUES ("ftn_bso", ?, ?);`; - + msgDb.run(sql, [ areaTag, lastScanId ], err => { - cb(err); + return cb(err); }); }; - + + this.getNodeConfigByAddress = function(addr) { + addr = _.isString(addr) ? Address.fromString(addr) : addr; + + // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy + return _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { + return addr.isPatternMatch(nodeAddrWildcard); + }); + }; + + // :TODO: deprecate this in favor of getNodeConfigByAddress() this.getNodeConfigKeyByAddress = function(uplink) { - // :TODO: sort by least # of '*' & take top? const nodeKey = _.filter(Object.keys(this.moduleConfig.nodes), addr => { return Address.fromString(addr).isPatternMatch(uplink); })[0]; return nodeKey; }; - + + this.exportNetMailMessagePacket = function(message, exportOpts, cb) { + // + // For NetMail, we always create a *single* packet per message. + // + async.series( + [ + function generalPrep(callback) { + // :TODO: Double check Via spec -- seems like it may be wrong + self.prepareMessage(message, exportOpts); + + return self.setReplyKludgeFromReplyToMsgId(message, callback); + }, + function createPacket(callback) { + const packet = new ftnMailPacket.Packet(); + + const packetHeader = new ftnMailPacket.PacketHeader( + exportOpts.network.localAddress, + exportOpts.destAddress, + exportOpts.nodeConfig.packetType + ); + + packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; + + // use current message ID for filename seed + exportOpts.pktFileName = self.getOutgoingPacketFileName( + self.exportTempDir, + message.messageId, + false, // createTempPacket=false + exportOpts.fileCase + ); + + const ws = fs.createWriteStream(exportOpts.pktFileName); + + packet.writeHeader(ws, packetHeader); + + packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { + if(err) { + return callback(err); + } + + ws.write(msgBuf); + + packet.writeTerminator(ws); + + ws.end(); + ws.once('finish', () => { + return callback(null); + }); + }); + } + ], + err => { + return cb(err); + } + ); + }; + this.exportMessagesByUuid = function(messageUuids, exportOpts, cb) { // // This method has a lot of madness going on: @@ -538,7 +628,7 @@ function FTNMessageScanTossModule() { let ws; let remainMessageBuf; let remainMessageId; - const createTempPacket = !_.isString(exportOpts.nodeConfig.archiveType) || 0 === exportOpts.nodeConfig.archiveType.length; + const createTempPacket = !_.isString(exportOpts.nodeConfig.archiveType) || 0 === exportOpts.nodeConfig.archiveType.length; function finalizePacket(cb) { packet.writeTerminator(ws); @@ -547,10 +637,10 @@ function FTNMessageScanTossModule() { return cb(null); }); } - + async.each(messageUuids, (msgUuid, nextUuid) => { let message = new Message(); - + async.series( [ function finalizePrevious(callback) { @@ -565,47 +655,47 @@ function FTNMessageScanTossModule() { if(err) { return callback(err); } - + // General preperation self.prepareMessage(message, exportOpts); - + self.setReplyKludgeFromReplyToMsgId(message, err => { callback(err); }); }); }, - function createNewPacket(callback) { - if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { + function createNewPacket(callback) { + if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { packet = new ftnMailPacket.Packet(); - + const packetHeader = new ftnMailPacket.PacketHeader( exportOpts.network.localAddress, exportOpts.destAddress, exportOpts.nodeConfig.packetType); packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - + // use current message ID for filename seed const pktFileName = self.getOutgoingPacketFileName( - self.exportTempDir, - message.messageId, + self.exportTempDir, + message.messageId, createTempPacket, exportOpts.fileCase ); exportedFiles.push(pktFileName); - + ws = fs.createWriteStream(pktFileName); - + currPacketSize = packet.writeHeader(ws, packetHeader); - + if(remainMessageBuf) { currPacketSize += packet.writeMessageEntry(ws, remainMessageBuf); remainMessageBuf = null; - } + } } - - callback(null); + + callback(null); }, function appendMessage(callback) { packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { @@ -614,16 +704,16 @@ function FTNMessageScanTossModule() { } currPacketSize += msgBuf.length; - + if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { - remainMessageBuf = msgBuf; // save for next packet - remainMessageId = message.messageId; + remainMessageBuf = msgBuf; // save for next packet + remainMessageId = message.messageId; } else { ws.write(msgBuf); } - + return callback(null); - }); + }); }, function storeStateFlags0Meta(callback) { message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), err => { @@ -641,9 +731,9 @@ function FTNMessageScanTossModule() { }); } else { callback(null); - } + } } - ], + ], err => { nextUuid(err); } @@ -665,26 +755,26 @@ function FTNMessageScanTossModule() { if(remainMessageBuf) { // :TODO: DRY this with the code above -- they are basically identical packet = new ftnMailPacket.Packet(); - + const packetHeader = new ftnMailPacket.PacketHeader( exportOpts.network.localAddress, exportOpts.destAddress, exportOpts.nodeConfig.packetType); packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - + // use current message ID for filename seed const pktFileName = self.getOutgoingPacketFileName( - self.exportTempDir, - remainMessageId, + self.exportTempDir, + remainMessageId, createTempPacket, exportOpts.filleCase ); exportedFiles.push(pktFileName); - + ws = fs.createWriteStream(pktFileName); - + packet.writeHeader(ws, packetHeader); ws.write(remainMessageBuf); return finalizePacket(callback); @@ -696,18 +786,168 @@ function FTNMessageScanTossModule() { err => { cb(err, exportedFiles); } - ); - } + ); + } }); }; - - this.exportMessagesToUplinks = function(messageUuids, areaConfig, cb) { + + this.getNetMailRoute = function(dstAddr) { + // + // messageNetworks.ftn.netMail.routes{} full|wildcard -> full adddress lookup + // + const routes = _.get(Config, 'messageNetworks.ftn.netMail.routes'); + if(!routes) { + return; + } + + return _.find(routes, (route, addrWildcard) => { + return dstAddr.isPatternMatch(addrWildcard); + }); + + /* + const route = _.find(routes, (route, addrWildcard) => { + return dstAddr.isPatternMatch(addrWildcard); + }); + + if(route && route.address) { + return Address.fromString(route.address); + } + */ + }; + + this.getAcceptableNetMailNetworkInfoFromAddress = function(dstAddr, cb) { + // + // Attempt to find an acceptable network configuration using the following + // lookup order (most to least explicit config): + // + // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config + // - Where we send may not be where dstAddress is (it's routed!); use network found in route + // for local address + // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config + // - Where we send is direct to dstAddr; use scannerTossers.ftn_bso.defaultNetwork to + // for local address + // 3) Nodelist DB lookup (use default config) + // - Where we send is direct to dstAddr + // + //const routeAddress = this.getNetMailRouteAddress(dstAddr) || dstAddr; + const route = this.getNetMailRoute(dstAddr); + + let routeAddress; + let networkName; + if(route) { + routeAddress = Address.fromString(route.address); + networkName = route.network || Config.scannerTossers.ftn_bso.defaultNetwork; + } else { + routeAddress = dstAddr; + networkName = Config.scannerTossers.ftn_bso.defaultNetwork; + } + + const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { + return routeAddress.isPatternMatch(nodeAddrWildcard); + }) || { + packetType : '2+', + encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding, + }; + + return cb( + config ? null : Errors.DoesNotExist(`No configuration found for ${dstAddr.toString()}`), + config, routeAddress, networkName + ); + }; + + this.exportNetMailMessagesToUplinks = function(messagesOrMessageUuids, cb) { + // for each message/UUID, find where to send the thing + async.each(messagesOrMessageUuids, (msgOrUuid, nextMessageOrUuid) => { + + const exportOpts = {}; + const message = new Message(); + + async.series( + [ + function loadMessage(callback) { + if(_.isString(msgOrUuid)) { + message.load( { uuid : msgOrUuid }, err => { + return callback(err, message); + }); + } else { + return callback(null, msgOrUuid); + } + }, + function discoverUplink(callback) { + const dstAddr = new Address({ + zone : parseInt(message.meta.FtnProperty.ftn_dest_zone), + net : parseInt(message.meta.FtnProperty.ftn_dest_network), + node : parseInt(message.meta.FtnProperty.ftn_dest_node), + point : parseInt(message.meta.FtnProperty.ftn_dest_point) || null, // point is optional + }); + + return self.getAcceptableNetMailNetworkInfoFromAddress(dstAddr, (err, config, routeAddress, networkName) => { + if(err) { + return callback(err); + } + + + + exportOpts.nodeConfig = config; + exportOpts.destAddress = routeAddress; + exportOpts.fileCase = config.fileCase || 'lower'; + exportOpts.network = Config.messageNetworks.ftn.networks[networkName]; + exportOpts.networkName = networkName; + + if(!exportOpts.network) { + return callback(Errors.DoesNotExist(`No configuration found for network ${networkName}`)); + } + + return callback(null); + }); + }, + function createOutgoingDir(callback) { + // ensure outgoing NetMail directory exists + return fse.mkdirs(Config.scannerTossers.ftn_bso.paths.outboundNetMail, callback); + }, + function exportPacket(callback) { + return self.exportNetMailMessagePacket(message, exportOpts, callback); + }, + function moveToOutgoing(callback) { + const newExt = exportOpts.fileCase === 'lower' ? '.pkt' : '.PKT'; + const newPath = paths.join( + Config.scannerTossers.ftn_bso.paths.outboundNetMail, + `${paths.basename(exportOpts.pktFileName, paths.extname(exportOpts.pktFileName))}${newExt}` + ); + + return fse.move(exportOpts.pktFileName, newPath, callback); + }, + function storeStateFlags0Meta(callback) { + return message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), callback); + }, + function storeMsgIdMeta(callback) { + // Store meta as if we had imported this message -- for later reference + if(message.meta.FtnKludge.MSGID) { + return message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, callback); + } + + return callback(null); + } + ], + err => { + if(err) { + Log.warn( { error :err.message }, 'Error exporting message' ); + } + return nextMessageOrUuid(null); + } + ); + }, err => { + return cb(err); + }); + }; + + this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) { async.each(areaConfig.uplinks, (uplink, nextUplink) => { const nodeConfigKey = self.getNodeConfigKeyByAddress(uplink); if(!nodeConfigKey) { return nextUplink(); } - + const exportOpts = { nodeConfig : self.moduleConfig.nodes[nodeConfigKey], network : Config.messageNetworks.ftn.networks[areaConfig.network], @@ -715,14 +955,14 @@ function FTNMessageScanTossModule() { networkName : areaConfig.network, fileCase : self.moduleConfig.nodes[nodeConfigKey].fileCase || 'lower', }; - + if(_.isString(exportOpts.network.localAddress)) { - exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); + exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); } - - const outgoingDir = self.getOutgoingPacketDir(exportOpts.networkName, exportOpts.destAddress); + + const outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); const exportType = self.getExportType(exportOpts.nodeConfig); - + async.waterfall( [ function createOutgoingDir(callback) { @@ -746,17 +986,17 @@ function FTNMessageScanTossModule() { if(err) { return callback(err); } - + // adjust back to temp path const tempBundlePath = paths.join(self.exportTempDir, paths.basename(bundlePath)); - + self.archUtil.compressTo( - exportOpts.nodeConfig.archiveType, + exportOpts.nodeConfig.archiveType, tempBundlePath, exportedFileNames, err => { callback(err, [ tempBundlePath ] ); } - ); + ); }); } else { callback(null, exportedFileNames); @@ -769,18 +1009,18 @@ function FTNMessageScanTossModule() { // // For a given temporary .pk_ file, we need to move it to the outoing // directory with the appropriate BSO style filename. - // + // const newExt = self.getOutgoingFlowFileExtension( exportOpts.destAddress, - 'mail', + 'mail', exportType, exportOpts.fileCase ); - + const newPath = paths.join( - outgoingDir, + outgoingDir, `${paths.basename(oldPath, ext)}${newExt}`); - + fse.move(oldPath, newPath, nextFile); } else { const newPath = paths.join(outgoingDir, paths.basename(oldPath)); @@ -789,25 +1029,25 @@ function FTNMessageScanTossModule() { Log.warn( { oldPath : oldPath, newPath : newPath, error : err.toString() }, 'Failed moving temporary bundle file!'); - + return nextFile(); } - + // // For bundles, we need to append to the appropriate flow file // const flowFilePath = self.getOutgoingFlowFileName( - outgoingDir, + outgoingDir, exportOpts.destAddress, 'ref', exportType, exportOpts.fileCase ); - + // directive of '^' = delete file after transfer self.flowFileAppendRefs(flowFilePath, [ newPath ], '^', err => { if(err) { - Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!'); + Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!'); } nextFile(); }); @@ -823,10 +1063,10 @@ function FTNMessageScanTossModule() { } nextUplink(); } - ); + ); }, cb); // complete }; - + this.setReplyToMsgIdFtnReplyKludge = function(message, cb) { // // Given a FTN REPLY kludge, set |message.replyToMsgId|, if possible, @@ -838,12 +1078,12 @@ function FTNMessageScanTossModule() { // nothing to do return cb(); } - + Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => { if(msgIds && msgIds.length > 0) { // expect a single match, but dupe checking is not perfect - warn otherwise if(1 === msgIds.length) { - message.replyToMsgId = msgIds[0]; + message.replyToMsgId = msgIds[0]; } else { Log.warn( { msgIds : msgIds, replyKludge : message.meta.FtnKludge.REPLY }, 'Found 2:n MSGIDs matching REPLY kludge!'); } @@ -851,14 +1091,14 @@ function FTNMessageScanTossModule() { cb(); }); }; - + this.importEchoMailToArea = function(localAreaTag, header, message, cb) { async.series( [ - function validateDestinationAddress(callback) { - const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`; + function validateDestinationAddress(callback) { + const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`; const localNetworkName = self.getNetworkNameByAddressPattern(localNetworkPattern); - + callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); }, function checkForDupeMSGID(callback) { @@ -881,7 +1121,7 @@ function FTNMessageScanTossModule() { }, function basicSetup(callback) { message.areaTag = localAreaTag; - + // // If we *allow* dupes (disabled by default), then just generate // a random UUID. Otherwise, don't assign the UUID just yet. It will be @@ -891,8 +1131,8 @@ function FTNMessageScanTossModule() { // just generate a UUID & therefor always allow for dupes message.uuid = uuidV4(); } - - callback(null); + + callback(null); }, function setReplyToMessageId(callback) { self.setReplyToMsgIdFtnReplyKludge(message, () => { @@ -902,15 +1142,15 @@ function FTNMessageScanTossModule() { function persistImport(callback) { // mark as imported message.meta.System.state_flags0 = Message.StateFlags0.Imported.toString(); - + // save to disc message.persist(err => { - callback(err); + callback(err); }); } - ], + ], err => { - cb(err); + cb(err); } ); }; @@ -924,37 +1164,37 @@ function FTNMessageScanTossModule() { message.message += `${message.meta.FtnProperty.ftn_origin}\r\n`; } }; - + // - // Ref. implementations on import: + // Ref. implementations on import: // * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c // https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/handle.c // this.importMessagesFromPacketFile = function(packetPath, password, cb) { let packetHeader; - + const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later - + let importStats = { areaSuccess : {}, // areaTag->count areaFail : {}, // areaTag->count otherFail : 0, }; - + new ftnMailPacket.Packet(packetOpts).read(packetPath, (entryType, entryData, next) => { if('header' === entryType) { packetHeader = entryData; - + const localNetworkName = self.getNetworkNameByAddress(packetHeader.destAddress); if(!_.isString(localNetworkName)) { const addrString = new Address(packetHeader.destAddress).toString(); return next(new Error(`No local configuration for packet addressed to ${addrString}`)); } else { - - // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?! + + // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?! return next(null); } - + } else if('message' === entryType) { const message = entryData; const areaTag = message.meta.FtnProperty.ftn_area; @@ -972,37 +1212,37 @@ function FTNMessageScanTossModule() { message.message); self.appendTearAndOrigin(message); - + self.importEchoMailToArea(localAreaTag, packetHeader, message, err => { if(err) { // bump area fail stats importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; - + if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A'; Log.info( - { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, + { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, 'Not importing non-unique message'); - + return next(null); } } else { // bump area success importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; } - + return next(err); }); } else { // // No local area configured for this import // - // :TODO: Handle the "catch all" case, if configured + // :TODO: Handle the "catch all" case, if configured Log.warn( { areaTag : areaTag }, 'No local area configured for this packet file!'); - + // bump generic failure importStats.otherFail += 1; - + return next(null); } } else { @@ -1027,7 +1267,7 @@ function FTNMessageScanTossModule() { } else { Log.info(finalStats, 'Import complete'); } - + cb(err); }); }; @@ -1048,7 +1288,7 @@ function FTNMessageScanTossModule() { if(!_.isString(self.moduleConfig.paths.retain)) { return cb(null); } - + archivePath = paths.join(self.moduleConfig.paths.retain, `good-pkt-${ts}--${fn}`); } else if('good' !== status) { archivePath = paths.join(self.moduleConfig.paths.reject, `${status}-${type}--${ts}-${fn}`); @@ -1066,7 +1306,7 @@ function FTNMessageScanTossModule() { return cb(null); // never fatal }); }; - + this.importPacketFilesFromDirectory = function(importDir, password, cb) { async.waterfall( [ @@ -1081,12 +1321,12 @@ function FTNMessageScanTossModule() { function importPacketFiles(packetFiles, callback) { let rejects = []; async.eachSeries(packetFiles, (packetFile, nextFile) => { - self.importMessagesFromPacketFile(paths.join(importDir, packetFile), '', err => { + self.importMessagesFromPacketFile(paths.join(importDir, packetFile), '', err => { if(err) { - Log.debug( - { path : paths.join(importDir, packetFile), error : err.toString() }, + Log.debug( + { path : paths.join(importDir, packetFile), error : err.toString() }, 'Failed to import packet file'); - + rejects.push(packetFile); } nextFile(); @@ -1097,13 +1337,13 @@ function FTNMessageScanTossModule() { }); }, function handleProcessedFiles(packetFiles, rejects, callback) { - async.each(packetFiles, (packetFile, nextFile) => { + async.each(packetFiles, (packetFile, nextFile) => { // possibly archive, then remove original const fullPath = paths.join(importDir, packetFile); self.maybeArchiveImportFile( - fullPath, - 'pkt', - rejects.includes(packetFile) ? 'reject' : 'good', + fullPath, + 'pkt', + rejects.includes(packetFile) ? 'reject' : 'good', () => { fs.unlink(fullPath, () => { return nextFile(null); @@ -1120,7 +1360,7 @@ function FTNMessageScanTossModule() { } ); }; - + this.importFromDirectory = function(inboundType, importDir, cb) { async.waterfall( [ @@ -1138,7 +1378,7 @@ function FTNMessageScanTossModule() { const fext = paths.extname(f); return bundleRegExp.test(fext); }); - + async.map(files, (file, transform) => { const fullPath = paths.join(importDir, file); self.archUtil.detectType(fullPath, (err, archName) => { @@ -1151,48 +1391,48 @@ function FTNMessageScanTossModule() { }, function importBundles(bundleFiles, callback) { let rejects = []; - + async.each(bundleFiles, (bundleFile, nextFile) => { if(_.isUndefined(bundleFile.archName)) { - Log.warn( + Log.warn( { fileName : bundleFile.path }, 'Unknown bundle archive type'); - + rejects.push(bundleFile.path); - + return nextFile(); // unknown archive type } Log.debug( { bundleFile : bundleFile }, 'Processing bundle' ); - + self.archUtil.extractTo( bundleFile.path, self.importTempDir, bundleFile.archName, err => { - if(err) { + if(err) { Log.warn( { path : bundleFile.path, error : err.message }, 'Failed to extract bundle'); - + rejects.push(bundleFile.path); } - - nextFile(); + + nextFile(); } ); }, err => { if(err) { return callback(err); } - + // // All extracted - import .pkt's // self.importPacketFilesFromDirectory(self.importTempDir, '', err => { // :TODO: handle |err| callback(null, bundleFiles, rejects); - }); + }); }); }, function handleProcessedBundleFiles(bundleFiles, rejects, callback) { @@ -1209,7 +1449,7 @@ function FTNMessageScanTossModule() { return nextFile(null); }); } - ); + ); }, err => { callback(err); }); @@ -1225,41 +1465,45 @@ function FTNMessageScanTossModule() { } ); }; - + this.createTempDirectories = function(cb) { temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => { if(err) { return cb(err); } - + self.exportTempDir = tempDir; - + temptmp.mkdir( { prefix : 'enigftnimport-' }, (err, tempDir) => { self.importTempDir = tempDir; - + cb(err); }); }); }; - + // Starts an export block - returns true if we can proceed this.exportingStart = function() { if(!this.exportRunning) { this.exportRunning = true; return true; } - + return false; }; // ends an export block - this.exportingEnd = function() { - this.exportRunning = false; + this.exportingEnd = function(cb) { + this.exportRunning = false; + + if(cb) { + return cb(null); + } }; this.copyTicAttachment = function(src, dst, isUpdate, cb) { if(isUpdate) { - fse.copy(src, dst, err => { + fse.copy(src, dst, { overwrite : true }, err => { return cb(err, dst); }); } else { @@ -1274,8 +1518,6 @@ function FTNMessageScanTossModule() { }; this.processSingleTicFile = function(ticFileInfo, cb) { - const self = this; - Log.debug( { tic : ticFileInfo.path, file : ticFileInfo.getAsString('File') }, 'Processing TIC file'); async.waterfall( @@ -1389,8 +1631,8 @@ function FTNMessageScanTossModule() { // // We may have TIC auto-tagging for this node and/or specific (remote) area // - const hashTags = - localInfo.hashTags || + const hashTags = + localInfo.hashTags || _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ if(hashTags) { @@ -1462,7 +1704,7 @@ function FTNMessageScanTossModule() { if(isUpdate) { // we need to *update* an existing record/file - localInfo.fileEntry.fileId = localInfo.existingFileId; + localInfo.fileEntry.fileId = localInfo.existingFileId; } const dst = paths.join(areaStorageDir, localInfo.fileEntry.fileName); @@ -1476,13 +1718,13 @@ function FTNMessageScanTossModule() { if(dst !== finalPath) { localInfo.fileEntry.fileName = paths.basename(finalPath); } - + localInfo.fileEntry.persist(isUpdate, err => { return callback(err, localInfo); }); - }); + }); }, - // :TODO: from here, we need to re-toss files if needed, before they are removed + // :TODO: from here, we need to re-toss files if needed, before they are removed function cleanupOldFile(localInfo, callback) { if(!localInfo.existingFileId) { return callback(null, localInfo); @@ -1490,7 +1732,7 @@ function FTNMessageScanTossModule() { const oldStorageDir = getAreaStorageDirectoryByTag(localInfo.oldStorageTag); const oldPath = paths.join(oldStorageDir, localInfo.oldFileName); - + fs.unlink(oldPath, err => { if(err) { Log.warn( { error : err.message, oldPath : oldPath }, 'Failed removing old physical file during TIC replacement'); @@ -1527,6 +1769,145 @@ function FTNMessageScanTossModule() { return cb(err); }); }; + + + this.performEchoMailExport = function(cb) { + // + // Select all messages with a |message_id| > |lastScanId|. + // Additionally exclude messages with the System state_flags0 which will be present for + // imported or already exported messages + // + // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! + // + const getNewUuidsSql = + `SELECT message_id, message_uuid + FROM message m + WHERE area_tag = ? AND message_id > ? AND + (SELECT COUNT(message_id) + FROM message_meta + WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0 + ORDER BY message_id;` + ; + + async.each(Object.keys(Config.messageNetworks.ftn.areas), (areaTag, nextArea) => { + const areaConfig = Config.messageNetworks.ftn.areas[areaTag]; + if(!this.isAreaConfigValid(areaConfig)) { + return nextArea(); + } + + // + // For each message that is newer than that of the last scan + // we need to export to each configured associated uplink(s) + // + async.waterfall( + [ + function getLastScanId(callback) { + self.getAreaLastScanId(areaTag, callback); + }, + function getNewUuids(lastScanId, callback) { + msgDb.all(getNewUuidsSql, [ areaTag, lastScanId ], (err, rows) => { + if(err) { + callback(err); + } else { + if(0 === rows.length) { + let nothingToDoErr = new Error('Nothing to do!'); + nothingToDoErr.noRows = true; + callback(nothingToDoErr); + } else { + callback(null, rows); + } + } + }); + }, + function exportToConfiguredUplinks(msgRows, callback) { + const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only + self.exportEchoMailMessagesToUplinks(uuidsOnly, areaConfig, err => { + const newLastScanId = msgRows[msgRows.length - 1].message_id; + + Log.info( + { areaTag : areaTag, messagesExported : msgRows.length, newLastScanId : newLastScanId }, + 'Export complete'); + + callback(err, newLastScanId); + }); + }, + function updateLastScanId(newLastScanId, callback) { + self.setAreaLastScanId(areaTag, newLastScanId, callback); + } + ], + () => { + return nextArea(); + } + ); + }, + err => { + return cb(err); + }); + }; + + this.performNetMailExport = function(cb) { + // + // Select all messages with a |message_id| > |lastScanId| in the private area + // that also *do not* have a local user ID meta value but *do* have a FTN dest + // network meta value. + // + // Just like EchoMail, we additionally exclude messages with the System state_flags0 + // which will be present for imported or already exported messages + // + // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! + // + // :TODO: fill out the rest of the consts here + // :TODO: this statement is crazy ugly + const getNewUuidsSql = + `SELECT message_id, message_uuid + FROM message m + WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND + (SELECT COUNT(message_id) + FROM message_meta + WHERE message_id = m.message_id AND meta_category = 'System' AND + (meta_name = 'state_flags0' OR meta_name='local_to_user_id')) = 0 + AND + (SELECT COUNT(message_id) + FROM message_meta + WHERE message_id = m.message_id AND meta_category='FtnProperty' AND meta_name='ftn_dest_network') = 1 + ORDER BY message_id; + `; + + async.waterfall( + [ + function getLastScanId(callback) { + return self.getAreaLastScanId(Message.WellKnownAreaTags.Private, callback); + }, + function getNewUuids(lastScanId, callback) { + msgDb.all(getNewUuidsSql, [ lastScanId ], (err, rows) => { + if(err) { + return callback(err); + } + + if(0 === rows.length) { + return cb(null); // note |cb| -- early bail out! + } + + return callback(null, rows); + }); + }, + function exportMessages(rows, callback) { + const messageUuids = rows.map(r => r.message_uuid); + return self.exportNetMailMessagesToUplinks(messageUuids, callback); + } + ], + err => { + return cb(err); + } + ); + }; + + this.isNetMailMessage = function(message) { + return message.isPrivate() && + null === _.get(message.meta, 'System.LocalToUserID', null) && + null !== _.get(message.meta, 'FtnProperty.ftn_dest_network') + ; + }; } require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); @@ -1574,19 +1955,19 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD if(err) { // archive rejected TIC stuff (.TIC + attach) async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { - if(!path) { // possibly rejected due to "File" not existing/etc. + if(!path) { // possibly rejected due to "File" not existing/etc. return nextPath(null); } self.maybeArchiveImportFile( - path, - 'tic', + path, + 'tic', 'reject', () => { return nextPath(null); } ); - }, + }, () => { self.removeAssocTicFiles(ticFileInfo, () => { return nextTicInfo(null); @@ -1596,7 +1977,7 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD self.removeAssocTicFiles(ticFileInfo, () => { return nextTicInfo(null); }); - } + } }); }, err => { return callback(err); @@ -1611,59 +1992,59 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD FTNMessageScanTossModule.prototype.startup = function(cb) { Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`); - + let importing = false; - + let self = this; - + function tryImportNow(reasonDesc) { if(!importing) { importing = true; - + Log.info( { module : exports.moduleInfo.name }, reasonDesc); - + self.performImport( () => { importing = false; }); } } - + this.createTempDirectories(err => { if(err) { Log.warn( { error : err.toStrong() }, 'Failed creating temporary directories!'); return cb(err); } - + if(_.isObject(this.moduleConfig.schedule)) { const exportSchedule = this.parseScheduleString(this.moduleConfig.schedule.export); if(exportSchedule) { Log.debug( - { - schedule : this.moduleConfig.schedule.export, + { + schedule : this.moduleConfig.schedule.export, schedOK : -1 === exportSchedule.sched.error, next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), immediate : exportSchedule.immediate ? true : false, }, 'Export schedule loaded' ); - + if(exportSchedule.sched) { this.exportTimer = later.setInterval( () => { if(this.exportingStart()) { Log.info( { module : exports.moduleInfo.name }, 'Performing scheduled message scan/export...'); - + this.performExport( () => { this.exportingEnd(); }); } }, exportSchedule.sched); } - + if(_.isBoolean(exportSchedule.immediate)) { this.exportImmediate = exportSchedule.immediate; } } - + const importSchedule = this.parseScheduleString(this.moduleConfig.schedule.import); if(importSchedule) { Log.debug( @@ -1675,13 +2056,13 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { }, 'Import schedule loaded' ); - - if(importSchedule.sched) { + + if(importSchedule.sched) { this.importTimer = later.setInterval( () => { - tryImportNow('Performing scheduled message import/toss...'); + tryImportNow('Performing scheduled message import/toss...'); }, importSchedule.sched); } - + if(_.isString(importSchedule.watchFile)) { const watcher = sane( paths.dirname(importSchedule.watchFile), @@ -1711,22 +2092,22 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { } } } - + FTNMessageScanTossModule.super_.prototype.startup.call(this, cb); }); }; FTNMessageScanTossModule.prototype.shutdown = function(cb) { Log.info('FidoNet Scanner/Tosser shutting down'); - + if(this.exportTimer) { this.exportTimer.clear(); } - + if(this.importTimer) { this.importTimer.clear(); } - + // // Clean up temp dir/files we created // @@ -1737,9 +2118,9 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { paths : paths, sessionId : temptmp.sessionId, }; - + Log.trace(fullStats, 'Temporary directories cleaned up'); - + FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }); @@ -1750,9 +2131,9 @@ FTNMessageScanTossModule.prototype.performImport = function(cb) { if(!this.hasValidConfiguration()) { return cb(new Error('Missing or invalid configuration')); } - + const self = this; - + async.each( [ 'inbound', 'secInbound' ], (inboundType, nextDir) => { self.importFromDirectory(inboundType, self.moduleConfig.paths[inboundType], () => { return nextDir(null); @@ -1768,77 +2149,18 @@ FTNMessageScanTossModule.prototype.performExport = function(cb) { if(!this.hasValidConfiguration()) { return cb(new Error('Missing or invalid configuration')); } - - // - // Select all messages with a |message_id| > |lastScanId|. - // Additionally exclude messages with the System state_flags0 which will be present for - // imported or already exported messages - // - // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! - // - const getNewUuidsSql = - `SELECT message_id, message_uuid - FROM message m - WHERE area_tag = ? AND message_id > ? AND - (SELECT COUNT(message_id) - FROM message_meta - WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0 - ORDER BY message_id;`; - - let self = this; - - async.each(Object.keys(Config.messageNetworks.ftn.areas), (areaTag, nextArea) => { - const areaConfig = Config.messageNetworks.ftn.areas[areaTag]; - if(!this.isAreaConfigValid(areaConfig)) { - return nextArea(); - } - - // - // For each message that is newer than that of the last scan - // we need to export to each configured associated uplink(s) - // - async.waterfall( - [ - function getLastScanId(callback) { - self.getAreaLastScanId(areaTag, callback); - }, - function getNewUuids(lastScanId, callback) { - msgDb.all(getNewUuidsSql, [ areaTag, lastScanId ], (err, rows) => { - if(err) { - callback(err); - } else { - if(0 === rows.length) { - let nothingToDoErr = new Error('Nothing to do!'); - nothingToDoErr.noRows = true; - callback(nothingToDoErr); - } else { - callback(null, rows); - } - } - }); - }, - function exportToConfiguredUplinks(msgRows, callback) { - const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only - self.exportMessagesToUplinks(uuidsOnly, areaConfig, err => { - const newLastScanId = msgRows[msgRows.length - 1].message_id; - - Log.info( - { areaTag : areaTag, messagesExported : msgRows.length, newLastScanId : newLastScanId }, - 'Export complete'); - - callback(err, newLastScanId); - }); - }, - function updateLastScanId(newLastScanId, callback) { - self.setAreaLastScanId(areaTag, newLastScanId, callback); - } - ], - () => { - return nextArea(); + + const self = this; + + async.eachSeries( [ 'EchoMail', 'NetMail' ], (type, nextType) => { + self[`perform${type}Export`]( err => { + if(err) { + Log.warn( { error : err.message, type : type }, 'Error(s) during export' ); } - ); - }, err => { - return cb(err); + return nextType(null); // try next, always + }); + }, () => { + return cb(null); }); }; @@ -1849,27 +2171,37 @@ FTNMessageScanTossModule.prototype.record = function(message) { if(true !== this.exportImmediate || !this.hasValidConfiguration()) { return; } - - if(message.isPrivate()) { - // :TODO: support NetMail + + const info = { uuid : message.uuid, subject : message.subject }; + + function exportLog(err) { + if(err) { + Log.warn(info, 'Failed exporting message'); + } else { + Log.info(info, 'Message exported'); + } + } + + if(this.isNetMailMessage(message)) { + Object.assign(info, { type : 'NetMail' } ); + + if(this.exportingStart()) { + this.exportNetMailMessagesToUplinks( [ message.uuid ], err => { + this.exportingEnd( () => exportLog(err) ); + }); + } } else if(message.areaTag) { + Object.assign(info, { type : 'EchoMail' } ); + const areaConfig = Config.messageNetworks.ftn.areas[message.areaTag]; if(!this.isAreaConfigValid(areaConfig)) { return; } - + if(this.exportingStart()) { - this.exportMessagesToUplinks( [ message.uuid ], areaConfig, err => { - const info = { uuid : message.uuid, subject : message.subject }; - - if(err) { - Log.warn(info, 'Failed exporting message'); - } else { - Log.info(info, 'Message exported'); - } - - this.exportingEnd(); + this.exportEchoMailMessagesToUplinks( [ message.uuid ], areaConfig, err => { + this.exportingEnd( () => exportLog(err) ); }); - } - } + } + } }; diff --git a/package.json b/package.json index 566a454d..678628f1 100644 --- a/package.json +++ b/package.json @@ -27,32 +27,32 @@ "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "fs-extra": "^4.0.1", + "fs-extra": "^5.0.0", + "glob": "^7.1.2", "graceful-fs": "^4.1.11", "hashids": "^1.1.1", "hjson": "^3.1.0", "iconv-lite": "^0.4.18", - "inquirer": "^3.2.3", + "inquirer": "^4.0.1", "later": "1.2.0", "lodash": "^4.17.4", "mime-types": "^2.1.17", "minimist": "1.2.x", - "moment": "^2.18.1", - "node-glob": "^1.2.0", - "nodemailer": "^4.1.0", + "moment": "^2.20.0", + "nodemailer": "^4.4.1", "ptyw.js": "NuSkooler/ptyw.js", "rlogin": "^1.0.0", "sane": "^2.2.0", "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.9", - "sqlite3-trans" : "^1.2.0", + "sqlite3-trans": "^1.2.0", "ssh2": "^0.5.5", "temptmp": "^1.0.0", "uuid": "^3.1.0", "uuid-parse": "^1.0.0", - "ws": "^3.1.0", + "ws": "^3.3.3", "xxhash": "^0.2.4", - "yazl" : "^2.4.2" + "yazl": "^2.4.2" }, "devDependencies": {}, "engines": { From 11a19d899e99e4f46a1732b436a788000b11c13f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 31 Dec 2017 18:45:39 -0700 Subject: [PATCH 0089/1013] * Use per-network outbound for NetMail just like EchoMail * Use BSO style FLO file for NetMail * Some code cleanup --- core/config.js | 87 ++++++++++++++++----------------- core/scanner_tossers/ftn_bso.js | 25 ++++++++-- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/core/config.js b/core/config.js index 34be676e..f9f277a8 100644 --- a/core/config.js +++ b/core/config.js @@ -2,7 +2,6 @@ 'use strict'; // ENiGMA½ -const miscUtil = require('./misc_util.js'); // deps const fs = require('graceful-fs'); @@ -32,7 +31,7 @@ function hasMessageConferenceAndArea(config) { _.forEach(nonInternalConfs, confTag => { if(_.has(config.messageConferences[confTag], 'areas') && Object.keys(config.messageConferences[confTag].areas) > 0) - { + { result = true; return false; // stop iteration } @@ -67,12 +66,12 @@ function init(configPath, options, cb) { } return callback(null, configJson); - }); + }); }, function mergeWithDefaultConfig(configJson, callback) { const mergedConfig = _.mergeWith( - getDefaultConfig(), + getDefaultConfig(), configJson, (conf1, conf2) => { // Arrays should always concat if(_.isArray(conf1)) { @@ -147,15 +146,13 @@ function getDefaultConfig() { webMax : 255, requireActivation : false, // require SysOp activation? false = auto-activate - invalidUsernames : [], groups : [ 'users', 'sysops' ], // built in groups defaultGroups : [ 'users' ], // default groups new users belong to newUserNames : [ 'new', 'apply' ], // Names reserved for applying - // :TODO: Mystic uses TRASHCAN.DAT for this -- is there a reason to support something like that? - badUserNames : [ + badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all', 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' ], @@ -187,7 +184,7 @@ function getDefaultConfig() { menus : { cls : true, // Clear screen before each menu by default? - }, + }, paths : { config : paths.join(__dirname, './../config/'), @@ -202,11 +199,11 @@ function getDefaultConfig() { themes : paths.join(__dirname, './../art/themes/'), logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such db : paths.join(__dirname, './../db/'), - modsDb : paths.join(__dirname, './../db/mods/'), + modsDb : paths.join(__dirname, './../db/mods/'), dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ misc : paths.join(__dirname, './../misc/'), }, - + loginServers : { telnet : { port : 8888, @@ -219,7 +216,7 @@ function getDefaultConfig() { // // Private key in PEM format - // + // // Generating your PK: // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 // @@ -248,7 +245,7 @@ function getDefaultConfig() { resetPassword : { // // The following templates have these variables available to them: - // + // // * %BOARDNAME% : Name of BBS // * %USERNAME% : Username of whom to reset password // * %TOKEN% : Reset token @@ -263,10 +260,10 @@ function getDefaultConfig() { // resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), }, - + http : { enabled : false, - port : 8080, + port : 8080, }, https : { enabled : false, @@ -283,10 +280,10 @@ function getDefaultConfig() { }, Exiftool : { cmd : 'exiftool', - args : [ + args : [ '-charset', 'utf8', '{filePath}', // exclude the following: - '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', + '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', '--metadatadate', '--xmptoolkit' ] @@ -305,7 +302,7 @@ function getDefaultConfig() { // // // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads - // :TODO: textual : bool -- if text, we can view. + // :TODO: textual : bool -- if text, we can view. // :TODO: asText : { cmd, args[] } -> viewable text // @@ -388,7 +385,7 @@ function getDefaultConfig() { sig : '526172211a0700', offset : 0, archiveHandler : 'Rar', - }, + }, 'application/gzip' : { desc : 'Gzip Archive', sig : '1f8b', @@ -400,28 +397,28 @@ function getDefaultConfig() { desc : 'BZip2 Archive', sig : '425a68', offset : 0, - archiveHandler : '7Zip', + archiveHandler : '7Zip', }, 'application/x-lzh-compressed' : { desc : 'LHArc Archive', sig : '2d6c68', offset : 2, - archiveHandler : 'Lha', + archiveHandler : 'Lha', }, 'application/x-7z-compressed' : { desc : '7-Zip Archive', sig : '377abcaf271c', offset : 0, - archiveHandler : '7Zip', + archiveHandler : '7Zip', } // :TODO: update archives::formats to fall here // * archive handler -> archiveHandler (consider archive if archiveHandler present) // * sig, offset, ... // * mime-db -> exts lookup - // * + // * }, - + archives : { archivers : { '7Zip' : { @@ -514,7 +511,7 @@ function getDefaultConfig() { list : { cmd : 'tar', args : [ '-tvf', '{archivePath}' ], - entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$', + entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$', }, extract : { cmd : 'tar', @@ -523,7 +520,7 @@ function getDefaultConfig() { } }, }, - + fileTransferProtocols : { // // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ @@ -539,7 +536,7 @@ function getDefaultConfig() { recvCmd : 'sexyz', recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], - } + } }, xmodemSexyz : { @@ -570,7 +567,7 @@ function getDefaultConfig() { name : 'ZModem 8k', type : 'external', sort : 2, - external : { + external : { sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" sendArgs : [ // :TODO: try -q @@ -581,11 +578,11 @@ function getDefaultConfig() { '--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 - } + escapeTelnet : true, // set to true to escape Telnet codes such as IAC + } } }, - + messageAreaDefaults : { // // The following can be override per-area as well @@ -594,11 +591,11 @@ function getDefaultConfig() { maxAgeDays : 0, // 0 = unlimited }, - messageConferences : { + messageConferences : { system_internal : { name : 'System Internal', desc : 'Built in conference for private messages, bulletins, etc.', - + areas : { private_mail : { name : 'Private Mail', @@ -612,7 +609,7 @@ function getDefaultConfig() { } } }, - + scannerTossers : { ftn_bso : { paths : { @@ -620,7 +617,7 @@ function getDefaultConfig() { inbound : paths.join(__dirname, './../mail/ftn_in/'), secInbound : paths.join(__dirname, './../mail/ftn_secin/'), reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. - outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), + //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), // set 'retain' to a valid path to keep good pkt files }, @@ -644,7 +641,7 @@ function getDefaultConfig() { }, fileBase: { - // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: + // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: areaStoragePrefix : paths.join(__dirname, './../file_base/'), maxDescFileByteSize : 471859, // ~1/4 MB @@ -654,12 +651,12 @@ function getDefaultConfig() { // These are NOT case sensitive // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. - desc : [ + desc : [ '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' ], // common README filename - https://en.wikipedia.org/wiki/README - descLong : [ + descLong : [ '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' ], }, @@ -675,7 +672,7 @@ function getDefaultConfig() { '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... //'\\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[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 '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries @@ -692,7 +689,7 @@ function getDefaultConfig() { // // File area storage location tag/value pairs. // Non-absolute paths are relative to |areaStoragePrefix|. - // + // storageTags : { sys_msg_attach : 'sys_msg_attach', sys_temp_download : 'sys_temp_download', @@ -712,20 +709,20 @@ function getDefaultConfig() { } } }, - + eventScheduler : { - + events : { trimMessageAreas : { // may optionally use [or ]@watch:/path/to/file schedule : 'every 24 hours', - + // action: // - @method:path/to/module.js:theMethodName // (path is relative to engima base dir) // - // - @execute:/path/to/something/executable.sh - // + // - @execute:/path/to/something/executable.sh + // action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', }, @@ -739,7 +736,7 @@ function getDefaultConfig() { action : '@method:core/web_password_reset.js:performMaintenanceTask', args : [ '24 hours' ] // items older than this will be removed } - } + } }, misc : { diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 996dd3ef..ac6ead52 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -893,6 +893,8 @@ function FTNMessageScanTossModule() { exportOpts.fileCase = config.fileCase || 'lower'; exportOpts.network = Config.messageNetworks.ftn.networks[networkName]; exportOpts.networkName = networkName; + exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); + exportOpts.exportType = self.getExportType(config); if(!exportOpts.network) { return callback(Errors.DoesNotExist(`No configuration found for network ${networkName}`)); @@ -902,20 +904,33 @@ function FTNMessageScanTossModule() { }); }, function createOutgoingDir(callback) { - // ensure outgoing NetMail directory exists - return fse.mkdirs(Config.scannerTossers.ftn_bso.paths.outboundNetMail, callback); + // ensure outgoing NetMail directory exists + return fse.mkdirs(exportOpts.outgoingDir, callback); + //return fse.mkdirs(Config.scannerTossers.ftn_bso.paths.outboundNetMail, callback); }, function exportPacket(callback) { return self.exportNetMailMessagePacket(message, exportOpts, callback); }, function moveToOutgoing(callback) { const newExt = exportOpts.fileCase === 'lower' ? '.pkt' : '.PKT'; - const newPath = paths.join( - Config.scannerTossers.ftn_bso.paths.outboundNetMail, + exportOpts.exportedToPath = paths.join( + exportOpts.outgoingDir, + //Config.scannerTossers.ftn_bso.paths.outboundNetMail, `${paths.basename(exportOpts.pktFileName, paths.extname(exportOpts.pktFileName))}${newExt}` ); - return fse.move(exportOpts.pktFileName, newPath, callback); + return fse.move(exportOpts.pktFileName, exportOpts.exportedToPath, callback); + }, + function prepareFloFile(callback) { + const flowFilePath = self.getOutgoingFlowFileName( + exportOpts.outgoingDir, + exportOpts.destAddress, + 'ref', + exportOpts.exportType, + exportOpts.fileCase + ); + + return self.flowFileAppendRefs(flowFilePath, [ exportOpts.exportedToPath ], '^', callback); }, function storeStateFlags0Meta(callback) { return message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), callback); From 6d929237d22e7e9d90bc1bf7f202400d6bba746f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Jan 2018 13:32:55 -0700 Subject: [PATCH 0090/1013] * Handle import of NetMail messages * Add NetMail aliases support (name -> localname lookup, e.g. "root" -> "NuSkooler" * Minor code changes / cleanup --- core/fse.js | 6 +- core/message_area.js | 4 +- core/scanner_tossers/ftn_bso.js | 155 ++++++++++++++++++++------------ 3 files changed, 102 insertions(+), 63 deletions(-) diff --git a/core/fse.js b/core/fse.js index 2ccc6951..d3262433 100644 --- a/core/fse.js +++ b/core/fse.js @@ -264,12 +264,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul isEditMode() { return 'edit' === this.editorMode; } - + isViewMode() { return 'view' === this.editorMode; } - isLocalEmail() { + isPrivateMail() { return Message.WellKnownAreaTags.Private === this.messageAreaTag; } @@ -411,7 +411,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return callback(null); }, function populateLocalUserInfo(callback) { - if(self.isLocalEmail()) { + if(self.isPrivateMail()) { self.message.setLocalFromUserId(self.client.user.userId); if(self.toUserId > 0) { diff --git a/core/message_area.js b/core/message_area.js index 484d22ec..aa893073 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -347,13 +347,13 @@ function getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, what) 'COUNT() AS count' : 'message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count'; - let sql = + let sql = `SELECT ${selectWhat} FROM message WHERE area_tag = "${areaTag}" AND message_id > ${lastMessageId}`; if(Message.isPrivateAreaTag(areaTag)) { - sql += + sql += ` AND message_id in ( SELECT message_id FROM message_meta diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index ac6ead52..354755fd 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -20,6 +20,7 @@ const getDescFromFileName = require('../file_base_area.js').getDescFromFileNa const copyFileWithCollisionHandling = require('../file_util.js').copyFileWithCollisionHandling; const getAreaStorageDirectoryByTag = require('../file_base_area.js').getAreaStorageDirectoryByTag; const isValidStorageTag = require('../file_base_area.js').isValidStorageTag; +const User = require('../user.js'); // deps const moment = require('moment'); @@ -310,22 +311,17 @@ function FTNMessageScanTossModule() { message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - // :TODO: Only set DEST information for EchoMail - NetMail should already have it in message message.meta.FtnProperty.ftn_orig_node = options.network.localAddress.node; - //message.meta.FtnProperty.ftn_dest_node = options.destAddress.node; message.meta.FtnProperty.ftn_orig_network = options.network.localAddress.net; - //message.meta.FtnProperty.ftn_dest_network = options.destAddress.net; message.meta.FtnProperty.ftn_cost = 0; // tear line and origin can both go in EchoMail & NetMail message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(options.network.localAddress); - // :TODO: Need an explicit isNetMail() check or more generally - let ftnAttribute = - ftnMailPacket.Packet.Attribute.Local; // message from our system + let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system - if(message.isPrivate()) { + if(self.isNetMailMessage(message)) { // These should be set for Private/NetMail already assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_node))); assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_network))); @@ -438,7 +434,6 @@ function FTNMessageScanTossModule() { options.encoding = encoding; // save for later message.meta.FtnKludge.CHRS = ftnUtil.getCharacterSetIdentifierByEncoding(encoding); - // :TODO: FLAGS kludge? }; this.setReplyKludgeFromReplyToMsgId = function(message, cb) { @@ -565,7 +560,6 @@ function FTNMessageScanTossModule() { async.series( [ function generalPrep(callback) { - // :TODO: Double check Via spec -- seems like it may be wrong self.prepareMessage(message, exportOpts); return self.setReplyKludgeFromReplyToMsgId(message, callback); @@ -886,8 +880,6 @@ function FTNMessageScanTossModule() { return callback(err); } - - exportOpts.nodeConfig = config; exportOpts.destAddress = routeAddress; exportOpts.fileCase = config.fileCase || 'lower'; @@ -904,9 +896,8 @@ function FTNMessageScanTossModule() { }); }, function createOutgoingDir(callback) { - // ensure outgoing NetMail directory exists + // ensure outgoing NetMail directory exists return fse.mkdirs(exportOpts.outgoingDir, callback); - //return fse.mkdirs(Config.scannerTossers.ftn_bso.paths.outboundNetMail, callback); }, function exportPacket(callback) { return self.exportNetMailMessagePacket(message, exportOpts, callback); @@ -915,7 +906,6 @@ function FTNMessageScanTossModule() { const newExt = exportOpts.fileCase === 'lower' ? '.pkt' : '.PKT'; exportOpts.exportedToPath = paths.join( exportOpts.outgoingDir, - //Config.scannerTossers.ftn_bso.paths.outboundNetMail, `${paths.basename(exportOpts.pktFileName, paths.extname(exportOpts.pktFileName))}${newExt}` ); @@ -1107,14 +1097,29 @@ function FTNMessageScanTossModule() { }); }; - this.importEchoMailToArea = function(localAreaTag, header, message, cb) { + this.getLocalUserNameFromAlias = function(lookup) { + lookup = lookup.toLowerCase(); + + const aliases = _.get(Config, 'messageNetworks.ftn.netMail.aliases'); + if(!aliases) { + return lookup; // keep orig + } + + const alias = _.find(aliases, (localName, alias) => { + return alias.toLowerCase() === lookup; + }); + + return alias || lookup; + }; + + this.importMailToArea = function(config, header, message, cb) { async.series( [ function validateDestinationAddress(callback) { const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`; const localNetworkName = self.getNetworkNameByAddressPattern(localNetworkPattern); - callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); + return callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); }, function checkForDupeMSGID(callback) { // @@ -1135,23 +1140,49 @@ function FTNMessageScanTossModule() { }); }, function basicSetup(callback) { - message.areaTag = localAreaTag; + message.areaTag = config.localAreaTag; // // If we *allow* dupes (disabled by default), then just generate // a random UUID. Otherwise, don't assign the UUID just yet. It will be // generated at persist() time and should be consistent across import/exports // - if(Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) { + if(true === _.get(Config, [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { // just generate a UUID & therefor always allow for dupes message.uuid = uuidV4(); } - callback(null); + return callback(null); }, function setReplyToMessageId(callback) { self.setReplyToMsgIdFtnReplyKludge(message, () => { - callback(null); + return callback(null); + }); + }, + function setupPrivateMessage(callback) { + // + // If this is a private message (e.g. NetMail) we set the local user ID + // + if(Message.WellKnownAreaTags.Private !== config.localAreaTag) { + return callback(null); + } + + const lookupName = self.getLocalUserNameFromAlias(message.toUserName); + + // :TODO: take into account aliasing, e.g. "root" -> SysOp + User.getUserIdAndName(lookupName, (err, localToUserId, localUserName) => { + if(err) { + return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); + } + + // we do this after such that error cases can be preseved above + if(lookupName !== message.toUserName) { + message.toUserName = localUserName; + } + + // set the meta information - used elsehwere for retrieval + message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId; + return callback(null); }); }, function persistImport(callback) { @@ -1160,7 +1191,7 @@ function FTNMessageScanTossModule() { // save to disc message.persist(err => { - callback(err); + return callback(err); }); } ], @@ -1214,45 +1245,15 @@ function FTNMessageScanTossModule() { const message = entryData; const areaTag = message.meta.FtnProperty.ftn_area; + let localAreaTag; if(areaTag) { - // - // EchoMail - // - const localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag); - if(localAreaTag) { - message.uuid = Message.createMessageUUID( - localAreaTag, - message.modTimestamp, - message.subject, - message.message); + localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag); - self.appendTearAndOrigin(message); - - self.importEchoMailToArea(localAreaTag, packetHeader, message, err => { - if(err) { - // bump area fail stats - importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; - - if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { - const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A'; - Log.info( - { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, - 'Not importing non-unique message'); - - return next(null); - } - } else { - // bump area success - importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; - } - - return next(err); - }); - } else { + if(!localAreaTag) { // // No local area configured for this import // - // :TODO: Handle the "catch all" case, if configured + // :TODO: Handle the "catch all" area bucket case if configured Log.warn( { areaTag : areaTag }, 'No local area configured for this packet file!'); // bump generic failure @@ -1262,11 +1263,49 @@ function FTNMessageScanTossModule() { } } else { // - // NetMail + // No area tag: If marked private in attributes, this is a NetMail // - Log.warn('NetMail import not yet implemented!'); - return next(null); + if(message.meta.FtnProperty.ftn_attr_flags & ftnMailPacket.Packet.Attribute.Private) { + localAreaTag = Message.WellKnownAreaTags.Private; + } else { + Log.warn('Non-private message without area tag'); + importStats.otherFail += 1; + return next(null); + } } + + message.uuid = Message.createMessageUUID( + localAreaTag, + message.modTimestamp, + message.subject, + message.message); + + self.appendTearAndOrigin(message); + + const importConfig = { + localAreaTag : localAreaTag, + }; + + self.importMailToArea(importConfig, packetHeader, message, err => { + if(err) { + // bump area fail stats + importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; + + if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { + const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A'; + Log.info( + { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, + 'Not importing non-unique message'); + + return next(null); + } + } else { + // bump area success + importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; + } + + return next(err); + }); } }, err => { // From bbd70f2fea294f4de9102a86794c2e5e6a60e2a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Jan 2018 15:13:56 -0700 Subject: [PATCH 0091/1013] Minor log changes --- core/oputil/oputil_message_base.js | 10 ++++------ core/scanner_tossers/ftn_bso.js | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 77a63e9f..ac1d42fc 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -109,13 +109,11 @@ function areaFix() { return callback(null, message); }, function persistMessage(message, callback) { - // - // :TODO: - // - Persist message in private outgoing (sysop out box) - // - Make necessary changes such that the message is exported properly - // - console.log(message); + // :TODO: Persist message in private outgoing (sysop out box) (TBD: implementation) message.persist(err => { + if(!err) { + console.log('AreaFix message persisted and will be exported as per configuration'); + } return callback(err); }); } diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 354755fd..e8124f1a 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -2051,11 +2051,11 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { let self = this; - function tryImportNow(reasonDesc) { + function tryImportNow(reasonDesc, extraInfo) { if(!importing) { importing = true; - Log.info( { module : exports.moduleInfo.name }, reasonDesc); + Log.info( Object.assign({ module : exports.moduleInfo.name }, extraInfo), reasonDesc); self.performImport( () => { importing = false; @@ -2129,7 +2129,7 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { watcher.on(event, (fileName, fileRoot) => { const eventPath = paths.join(fileRoot, fileName); if(paths.join(fileRoot, fileName) === importSchedule.watchFile) { - tryImportNow(`Performing import/toss due to @watch: ${eventPath} (${event})`); + tryImportNow('Performing import/toss due to @watch', { eventPath, event } ); } }); }); @@ -2140,7 +2140,7 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { // fse.exists(importSchedule.watchFile, exists => { if(exists) { - tryImportNow(`Performing import/toss due to @watch: ${importSchedule.watchFile} (initial exists)`); + tryImportNow('Performing import/toss due to @watch', { eventPath : importSchedule.watchFile, event : 'initial exists' } ); } }); } From e7109b0f0c61f9f09915f2cb7f3b728342127ab8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Jan 2018 17:50:27 -0700 Subject: [PATCH 0092/1013] Minor fix --- core/scanner_tossers/ftn_bso.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index e8124f1a..35cf6316 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1959,7 +1959,7 @@ function FTNMessageScanTossModule() { this.isNetMailMessage = function(message) { return message.isPrivate() && null === _.get(message.meta, 'System.LocalToUserID', null) && - null !== _.get(message.meta, 'FtnProperty.ftn_dest_network') + null !== _.get(message.meta, 'FtnProperty.ftn_dest_network', null) ; }; } From 84a1f70fc26ecd1be1275971a82773250f23638e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Jan 2018 18:10:38 -0700 Subject: [PATCH 0093/1013] * Add some user lookup functionality * Fix INTL to/from order * Remove VIA kludge when initially creating a NetMail message --- core/scanner_tossers/ftn_bso.js | 8 ++++--- core/user.js | 38 ++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 35cf6316..a72c45ff 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -332,17 +332,20 @@ function FTNMessageScanTossModule() { // NetMail messages need a FRL-1005.001 "Via" line // http://ftsc.org/docs/frl-1005.001 // + // :TODO: We need to do this when FORWARDING NetMail + /* if(_.isString(message.meta.FtnKludge.Via)) { message.meta.FtnKludge.Via = [ message.meta.FtnKludge.Via ]; } message.meta.FtnKludge.Via = message.meta.FtnKludge.Via || []; message.meta.FtnKludge.Via.push(ftnUtil.getVia(options.network.localAddress)); + */ // // We need to set INTL, and possibly FMPT and/or TOPT // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // - message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.network.localAddress, options.destAddress); + message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, options.network.localAddress); if(_.isNumber(options.network.localAddress.point) && options.network.localAddress.point > 0) { message.meta.FtnKludge.FMPT = options.network.localAddress.point; @@ -1169,8 +1172,7 @@ function FTNMessageScanTossModule() { const lookupName = self.getLocalUserNameFromAlias(message.toUserName); - // :TODO: take into account aliasing, e.g. "root" -> SysOp - User.getUserIdAndName(lookupName, (err, localToUserId, localUserName) => { + User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { if(err) { return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); } diff --git a/core/user.js b/core/user.js index fb125f6b..db9245f5 100644 --- a/core/user.js +++ b/core/user.js @@ -414,12 +414,48 @@ module.exports = class User { if(row) { return cb(null, row.id, row.user_name); } - + return cb(Errors.DoesNotExist('No matching username')); } ); } + static getUserIdAndNameByRealName(realName, cb) { + userDb.get( + `SELECT id, user_name + FROM user + WHERE id = ( + SELECT user_id + FROM user_property + WHERE prop_name='real_name' AND prop_value=? + );`, + [ realName ], + (err, row) => { + if(err) { + return cb(err); + } + + if(row) { + return cb(null, row.id, row.user_name); + } + + return cb(Errors.DoesNotExist('No matching real name')); + } + ); + } + + static getUserIdAndNameByLookup(lookup, cb) { + User.getUserIdAndName(lookup, (err, userId, userName) => { + if(err) { + User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => { + return cb(err, userId, userName); + }); + } else { + return cb(null, userId, userName); + } + }); + } + static getUserName(userId, cb) { userDb.get( `SELECT user_name From b97f96ce184434eac0e9d1c139a641cef7ee2336 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Jan 2018 18:43:05 -0700 Subject: [PATCH 0094/1013] * Fix Via parsing * Use LIKE for real name lookup --- core/ftn_mail_packet.js | 12 +++++++----- core/user.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 137a2583..c94eb425 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -397,10 +397,10 @@ function Packet(options) { // We have to special case INTL/TOPT/FMPT as they don't contain // a ':' name/value separator like the rest of the kludge lines... because stupdity. // - let key = line.substr(0, 4); + let key = line.substr(0, 4).trim(); let value; - if( ['INTL', 'TOPT', 'FMPT' ].includes(key)) { - value = line.substr(4).trim(); + if( ['INTL', 'TOPT', 'FMPT', 'Via' ].includes(key)) { + value = line.substr(key.length).trim(); } else { const sepIndex = line.indexOf(':'); key = line.substr(0, sepIndex).toUpperCase(); @@ -709,13 +709,14 @@ function Packet(options) { case 'PATH' : break; // skip & save for last + case 'Via' : case 'FMPT' : case 'TOPT' : - case 'INTL' : + case 'INTL' : msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar break; - default : + default : msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; } @@ -856,6 +857,7 @@ function Packet(options) { switch(k) { case 'PATH' : break; // skip & save for last + case 'Via' : case 'FMPT' : case 'TOPT' : case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar diff --git a/core/user.js b/core/user.js index db9245f5..09d26163 100644 --- a/core/user.js +++ b/core/user.js @@ -427,7 +427,7 @@ module.exports = class User { WHERE id = ( SELECT user_id FROM user_property - WHERE prop_name='real_name' AND prop_value=? + WHERE prop_name='real_name' AND prop_value LIKE ? );`, [ realName ], (err, row) => { From f967ce1ce683082994c0e47be321a5183a51f1bd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 5 Jan 2018 22:02:36 -0700 Subject: [PATCH 0095/1013] * Fix String vs Address when creating (NetMail) packets causing orig address info to not be recorded correctly --- core/ftn_mail_packet.js | 44 ++++++++++++++++++++++----------- core/ftn_util.js | 2 +- core/scanner_tossers/ftn_bso.js | 21 ++++++++-------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index c94eb425..d84b4a69 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -19,15 +19,6 @@ const moment = require('moment'); exports.Packet = Packet; -/* - :TODO: things - * Test SAUCE ignore/extraction - * FSP-1010 for netmail (see SBBS) - * Syncronet apparently uses odd origin lines - * Origin lines starting with "#" instead of "*" ? - -*/ - const FTN_PACKET_HEADER_SIZE = 58; // fixed header size const FTN_PACKET_HEADER_TYPE = 2; const FTN_PACKET_MESSAGE_TYPE = 2; @@ -63,7 +54,7 @@ class PacketHeader { this.capWord = 0x0001; this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap - + this.prodCodeHi = 0xfe; // see above this.prodRevHi = 0; } @@ -358,9 +349,9 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.origPoint, 50); buffer.writeUInt16LE(packetHeader.destPoint, 52); buffer.writeUInt32LE(packetHeader.prodData, 54); - + ws.write(buffer); - + return buffer.length; }; @@ -646,9 +637,29 @@ function Packet(options) { }); }); }; - + + this.sanatizeFtnProperties = function(message) { + [ + Message.FtnPropertyNames.FtnOrigNode, + Message.FtnPropertyNames.FtnDestNode, + Message.FtnPropertyNames.FtnOrigNetwork, + Message.FtnPropertyNames.FtnDestNetwork, + Message.FtnPropertyNames.FtnAttrFlags, + Message.FtnPropertyNames.FtnCost, + Message.FtnPropertyNames.FtnOrigZone, + Message.FtnPropertyNames.FtnDestZone, + Message.FtnPropertyNames.FtnOrigPoint, + Message.FtnPropertyNames.FtnDestPoint, + Message.FtnPropertyNames.FtnAttribute, + ].forEach( propName => { + if(message.meta.FtnProperty[propName]) { + message.meta.FtnProperty[propName] = parseInt(message.meta.FtnProperty[propName]) || 0; + } + }); + }; + this.getMessageEntryBuffer = function(message, options, cb) { - + function getAppendMeta(k, m, sepChar=':') { let append = ''; if(m) { @@ -667,7 +678,10 @@ function Packet(options) { [ function prepareHeaderAndKludges(callback) { const basicHeader = new Buffer(34); - + + // ensure address FtnProperties are numbers + self.sanatizeFtnProperties(message); + basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4); diff --git a/core/ftn_util.js b/core/ftn_util.js index 68ddf343..a02d8e1c 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -189,7 +189,7 @@ function getQuotePrefix(name) { // function getOrigin(address) { const origin = _.has(Config, 'messageNetworks.originLine') ? - Config.messageNetworks.originLine : + Config.messageNetworks.originLine : Config.general.boardName; const addrStr = new Address(address).toString('5D'); diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index a72c45ff..f6072ff3 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -308,16 +308,18 @@ function FTNMessageScanTossModule() { // // Set various FTN kludges/etc. // + const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version + message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - message.meta.FtnProperty.ftn_orig_node = options.network.localAddress.node; - message.meta.FtnProperty.ftn_orig_network = options.network.localAddress.net; + message.meta.FtnProperty.ftn_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_orig_network = localAddress.net; message.meta.FtnProperty.ftn_cost = 0; // tear line and origin can both go in EchoMail & NetMail message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); - message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(options.network.localAddress); + message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(localAddress); let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system @@ -345,10 +347,10 @@ function FTNMessageScanTossModule() { // We need to set INTL, and possibly FMPT and/or TOPT // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // - message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, options.network.localAddress); + message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, localAddress); - if(_.isNumber(options.network.localAddress.point) && options.network.localAddress.point > 0) { - message.meta.FtnKludge.FMPT = options.network.localAddress.point; + if(_.isNumber(localAddress.point) && localAddress.point > 0) { + message.meta.FtnKludge.FMPT = localAddress.point; } if(_.get(message, 'meta.FtnProperty.ftn_dest_point', 0) > 0) { @@ -378,15 +380,14 @@ function FTNMessageScanTossModule() { // with remote address(s) we are exporting to. // const seenByAdditions = - [ `${options.network.localAddress.net}/${options.network.localAddress.node}` ].concat(Config.messageNetworks.ftn.areas[message.areaTag].uplinks); + [ `${localAddress.net}/${localAddress.node}` ].concat(Config.messageNetworks.ftn.areas[message.areaTag].uplinks); message.meta.FtnProperty.ftn_seen_by = ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); // // And create/update PATH for ourself // - message.meta.FtnKludge.PATH = - ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, options.network.localAddress); + message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, localAddress); } message.meta.FtnProperty.ftn_attr_flags = ftnAttribute; @@ -398,7 +399,7 @@ function FTNMessageScanTossModule() { // export that failed to finish // if(!message.meta.FtnKludge.MSGID) { - message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier(message, options.network.localAddress); + message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier(message, localAddress); } message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset(); From ab12fb5d79c8356823ea46396127ea75abf9ee6b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 5 Jan 2018 22:03:33 -0700 Subject: [PATCH 0096/1013] Lookup username and real name in various scenarios --- core/fse.js | 4 ++-- core/message.js | 2 +- core/oputil/oputil_message_base.js | 2 +- core/system_view_validate.js | 32 +++++++++++++++++++++--------- main.js | 2 +- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/core/fse.js b/core/fse.js index d3262433..4a6c8eb1 100644 --- a/core/fse.js +++ b/core/fse.js @@ -413,13 +413,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul function populateLocalUserInfo(callback) { if(self.isPrivateMail()) { self.message.setLocalFromUserId(self.client.user.userId); - + if(self.toUserId > 0) { self.message.setLocalToUserId(self.toUserId); callback(null); } else { // we need to look it up - User.getUserIdAndName(self.message.toUserName, function userInfo(err, toUserId) { + User.getUserIdAndNameByLookup(self.message.toUserName, function userInfo(err, toUserId) { if(err) { callback(err); } else { diff --git a/core/message.js b/core/message.js index b5b1e541..7fb57d20 100644 --- a/core/message.js +++ b/core/message.js @@ -112,7 +112,7 @@ Message.FtnPropertyNames = { FtnDestZone : 'ftn_dest_zone', FtnOrigPoint : 'ftn_orig_point', FtnDestPoint : 'ftn_dest_point', - + FtnAttribute : 'ftn_attribute', FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index ac1d42fc..04d26435 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -55,7 +55,7 @@ function areaFix() { const User = require('../user.js'); if(argv.from) { - User.getUserIdAndName(argv.from, (err, userId, fromName) => { + User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => { if(err) { return callback(null, ftnAddr, argv.from, 0); } diff --git a/core/system_view_validate.js b/core/system_view_validate.js index 63c7b2bc..07bbcab2 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -9,13 +9,14 @@ const Log = require('./logger.js').log; // deps const fs = require('graceful-fs'); -exports.validateNonEmpty = validateNonEmpty; -exports.validateMessageSubject = validateMessageSubject; -exports.validateUserNameAvail = validateUserNameAvail; -exports.validateUserNameExists = validateUserNameExists; -exports.validateEmailAvail = validateEmailAvail; -exports.validateBirthdate = validateBirthdate; -exports.validatePasswordSpec = validatePasswordSpec; +exports.validateNonEmpty = validateNonEmpty; +exports.validateMessageSubject = validateMessageSubject; +exports.validateUserNameAvail = validateUserNameAvail; +exports.validateUserNameExists = validateUserNameExists; +exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists; +exports.validateEmailAvail = validateEmailAvail; +exports.validateBirthdate = validateBirthdate; +exports.validatePasswordSpec = validatePasswordSpec; function validateNonEmpty(data, cb) { return cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); @@ -42,11 +43,12 @@ function validateUserNameAvail(data, cb) { } else if(/^[0-9]+$/.test(data)) { return cb(new Error('Username cannot be a number')); } else { - User.getUserIdAndName(data, function userIdAndName(err) { + // a new user name cannot be an existing user name or an existing real name + User.getUserIdAndNameByLookup(data, function userIdAndName(err) { if(!err) { // err is null if we succeeded -- meaning this user exists already return cb(new Error('Username unavailable')); } - + return cb(null); }); } @@ -65,6 +67,18 @@ function validateUserNameExists(data, cb) { }); } +function validateUserNameOrRealNameExists(data, cb) { + const invalidUserNameError = new Error('Invalid username'); + + if(0 === data.length) { + return cb(invalidUserNameError); + } + + User.getUserIdAndNameByLookup(data, err => { + return cb(err ? invalidUserNameError : null); + }); +} + function validateEmailAvail(data, cb) { // // This particular method allows empty data - e.g. no email entered diff --git a/main.js b/main.js index 0bc7bee9..53a320f6 100755 --- a/main.js +++ b/main.js @@ -7,6 +7,6 @@ ENiGMA½ entry point If this file does not run directly, ensure it's executable: - > chmod u+x main.js + > chmod u+x main.js */ require('./core/bbs.js').main(); \ No newline at end of file From 99244aa2e4e7946b0cab6344493449c0d322273c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 6 Jan 2018 13:24:35 -0700 Subject: [PATCH 0097/1013] * Use Zone:Net/* for lookup before defualt local address when setting 'from' for NetMail --- core/oputil/oputil_message_base.js | 2 +- core/scanner_tossers/ftn_bso.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 04d26435..1312f2a4 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -112,7 +112,7 @@ function areaFix() { // :TODO: Persist message in private outgoing (sysop out box) (TBD: implementation) message.persist(err => { if(!err) { - console.log('AreaFix message persisted and will be exported as per configuration'); + console.log('AreaFix message persisted and will be exported at next scheduled scan'); } return callback(err); }); diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index f6072ff3..123c0b7a 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -822,8 +822,9 @@ function FTNMessageScanTossModule() { // - Where we send may not be where dstAddress is (it's routed!); use network found in route // for local address // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config - // - Where we send is direct to dstAddr; use scannerTossers.ftn_bso.defaultNetwork to - // for local address + // - Where we send is direct to dstAddr; + // - Attempt to match address in messageNetworks.ftn.networks{}, else + // use scannerTossers.ftn_bso.defaultNetwork to for local address // 3) Nodelist DB lookup (use default config) // - Where we send is direct to dstAddr // @@ -836,8 +837,12 @@ function FTNMessageScanTossModule() { routeAddress = Address.fromString(route.address); networkName = route.network || Config.scannerTossers.ftn_bso.defaultNetwork; } else { - routeAddress = dstAddr; - networkName = Config.scannerTossers.ftn_bso.defaultNetwork; + routeAddress = dstAddr; + + networkName = this.getNetworkNameByAddressPattern(`${dstAddr.zone}:${dstAddr.net}/*`); + if(!networkName) { + networkName = Config.scannerTossers.ftn_bso.defaultNetwork; + } } const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { From c4c30e0c0d5b3acb7d57fae2a4b0d596413d8495 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Jan 2018 18:43:04 -0700 Subject: [PATCH 0098/1013] Add some logging --- core/file_area_web.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/file_area_web.js b/core/file_area_web.js index a8d095d6..6440eb58 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -284,6 +284,8 @@ class FileAreaWebAccess { routeWebRequest(req, resp) { const hashId = paths.basename(req.url); + Log.debug( { hashId : hashId, url : req.url }, 'File area web request'); + this.loadServedHashId(hashId, (err, servedItem) => { if(err) { @@ -305,6 +307,8 @@ class FileAreaWebAccess { } routeWebRequestForSingleFile(servedItem, req, resp) { + Log.debug( { servedItem : servedItem }, 'Single file web request'); + const fileEntry = new FileEntry(); servedItem.fileId = servedItem.fileIds[0]; @@ -348,6 +352,8 @@ class FileAreaWebAccess { } routeWebRequestForBatchArchive(servedItem, req, resp) { + Log.debug( { servedItem : servedItem }, 'Batch file web request'); + // // We are going to build an on-the-fly zip file stream of 1:n // files in the batch. @@ -392,8 +398,14 @@ class FileAreaWebAccess { }); }, function createAndServeStream(filePaths, callback) { + Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request'); + const zipFile = new yazl.ZipFile(); + zipFile.on('error', err => { + Log.warn( { error : err.message }, 'Error adding file to batch web request archive'); + }); + filePaths.forEach(fp => { zipFile.addFile( fp, // path to physical file From d225d78fa961d8839d0f8d36280815e9111a10d6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Jan 2018 19:38:36 -0700 Subject: [PATCH 0099/1013] At least for now, use FTN-compliant MSGID for NetMail exports --- core/ftn_util.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/ftn_util.js b/core/ftn_util.js index a02d8e1c..39093fab 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -135,9 +135,20 @@ function getMessageSerialNumber(messageId) { // // ENiGMA½: .@<5dFtnAddress> // -function getMessageIdentifier(message, address) { +// 0.0.8-alpha: +// Made compliant with FTN spec *when exporting NetMail* due to +// Mystic rejecting messages with the true-unique version. +// Strangely, Synchronet uses the unique format and Mystic does +// OK with it. Will need to research further. Note also that +// g00r00 was kind enough to fix Mystic to allow for the Sync/Enig +// format, but that will only help when using newer Mystic versions. +// +function getMessageIdentifier(message, address, isNetMail = false) { const addrStr = new Address(address).toString('5D'); - return `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`; + return isNetMail ? + `${addrStr} ${getMessageSerialNumber(message.messageId)}` : + `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` + ; } // From f939babe728318ce0dcaac4fe0868ddfbfff9c66 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Jan 2018 20:48:47 -0700 Subject: [PATCH 0100/1013] Updates and isNetmail=isPrivate --- core/scanner_tossers/ftn_bso.js | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 123c0b7a..bba0c313 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -399,7 +399,11 @@ function FTNMessageScanTossModule() { // export that failed to finish // if(!message.meta.FtnKludge.MSGID) { - message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier(message, localAddress); + message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier( + message, + localAddress, + message.isPrivate() // true = isNetMail + ); } message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset(); @@ -819,32 +823,29 @@ function FTNMessageScanTossModule() { // lookup order (most to least explicit config): // // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config - // - Where we send may not be where dstAddress is (it's routed!); use network found in route - // for local address + // - Where we send may not be where dstAddress is (it's routed!) // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config - // - Where we send is direct to dstAddr; - // - Attempt to match address in messageNetworks.ftn.networks{}, else - // use scannerTossers.ftn_bso.defaultNetwork to for local address - // 3) Nodelist DB lookup (use default config) // - Where we send is direct to dstAddr // - //const routeAddress = this.getNetMailRouteAddress(dstAddr) || dstAddr; + // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address + // falling back to Config.scannerTossers.ftn_bso.defaultNetwork + // const route = this.getNetMailRoute(dstAddr); let routeAddress; let networkName; if(route) { routeAddress = Address.fromString(route.address); - networkName = route.network || Config.scannerTossers.ftn_bso.defaultNetwork; + networkName = route.network; } else { routeAddress = dstAddr; - - networkName = this.getNetworkNameByAddressPattern(`${dstAddr.zone}:${dstAddr.net}/*`); - if(!networkName) { - networkName = Config.scannerTossers.ftn_bso.defaultNetwork; - } } + networkName = networkName || + this.getNetworkNameByAddressPattern(`${routeAddress.zone}:${routeAddress.net}/*`) || + Config.scannerTossers.ftn_bso.defaultNetwork + ; + const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return routeAddress.isPatternMatch(nodeAddrWildcard); }) || { From ad60e5a7dfe28850a0df0b4e00a0f15c3860c3c8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Jan 2018 22:13:29 -0700 Subject: [PATCH 0101/1013] Split AreaFix with \r\n --- core/oputil/oputil_message_base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 1312f2a4..2ab93104 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -79,7 +79,7 @@ function areaFix() { // const messageBody = argv._.slice(2, -1).map(arg => { return arg.replace(/["']/g, ''); - }).join('\n') + '\n'; + }).join('\r\n') + '\n'; const Message = require('../message.js'); From 30fd001db3d05232b4326d612a85f059eb00658e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jan 2018 21:12:07 -0700 Subject: [PATCH 0102/1013] Fixed servedItem log --- core/file_area_web.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/file_area_web.js b/core/file_area_web.js index 6440eb58..b12f4f7d 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -152,16 +152,18 @@ class FileAreaWebAccess { return cb(Errors.Invalid('Invalid or unknown hash ID')); } - return cb( - null, - { - hashId : hashId, - userId : decoded[0], - hashIdType : decoded[1], - fileIds : decoded.slice(2), - expireTimestamp : moment(result.expire_timestamp), - } - ); + const servedItem = { + hashId : hashId, + userId : decoded[0], + hashIdType : decoded[1], + expireTimestamp : moment(result.expire_timestamp), + }; + + if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { + servedItem.fileIds = decoded.slice(2); + } + + return cb(null, servedItem); } ); } From a2e8fa65105b8415b6f4ee878fef24a1ac5b17f1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jan 2018 21:16:06 -0700 Subject: [PATCH 0103/1013] Add allowOlder opt to setFileBaseLastViewdFileIdForUser() --- core/file_base_filter.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/file_base_filter.js b/core/file_base_filter.js index a7185316..320d36d3 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -136,9 +136,14 @@ module.exports = class FileBaseFilters { return parseInt((user.properties.user_file_base_last_viewed || 0)); } - static setFileBaseLastViewedFileIdForUser(user, fileId, cb) { + static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { + if(!cb && _.isFunction(allowOlder)) { + cb = allowOlder; + allowOlder = false; + } + const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user); - if(fileId < current) { + if(!allowOlder && fileId < current) { if(cb) { cb(null); } From 4e4ee6b8ce2efca5a5e8a906b77ea169f7456a65 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jan 2018 21:16:37 -0700 Subject: [PATCH 0104/1013] cleanup --- core/file_base_search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_base_search.js b/core/file_base_search.js index 2ff27f8a..27656123 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -56,7 +56,7 @@ exports.getModule = class FileBaseSearch extends MenuModule { }, function populateAreas(callback) { self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); - + const areasView = vc.getView(MciViewIds.search.area); areasView.setItems( self.availAreas.map( a => a.name ) ); areasView.redraw(); From fa1bffeaf847fb37ee75fa8e8943270272d46188 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jan 2018 21:17:26 -0700 Subject: [PATCH 0105/1013] Fix limit when fetching entries, allow moment timestamps --- core/file_entry.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 6edd7ef8..8bf7a69d 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -13,10 +13,11 @@ const paths = require('path'); const fse = require('fs-extra'); const { unlink, readFile } = require('graceful-fs'); const crypto = require('crypto'); +const moment = require('moment'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', - 'desc', 'desc_long', 'upload_timestamp' + 'desc', 'desc_long', 'upload_timestamp' ]; const FILE_WELL_KNOWN_META = { @@ -424,7 +425,11 @@ module.exports = class FileEntry { let sqlWhere = ''; let sqlOrderBy; const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; - + + if(moment.isMoment(filter.newerThanTimestamp)) { + filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); + } + function getOrderByWithCast(ob) { if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { return `ORDER BY CAST(${ob} AS INTEGER)`; @@ -444,7 +449,7 @@ module.exports = class FileEntry { if(filter.sort && filter.sort.length > 0) { if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? - sql = + sql = `SELECT DISTINCT f.file_id FROM file f, file_meta m`; @@ -461,7 +466,7 @@ module.exports = class FileEntry { WHERE file_id = f.file_id) AS avg_rating FROM file f`; - + sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { sql = @@ -472,7 +477,7 @@ module.exports = class FileEntry { } } } else { - sql = + sql = `SELECT DISTINCT f.file_id FROM file f`; @@ -552,12 +557,14 @@ module.exports = class FileEntry { appendWhereClause(`f.file_id > ${filter.newerThanFileId}`); } - sql += `${sqlWhere} ${sqlOrderBy};`; + sql += `${sqlWhere} ${sqlOrderBy}`; if(_.isNumber(filter.limit)) { - sql += `LIMIT ${filter.limit}`; + sql += ` LIMIT ${filter.limit}`; } + sql += ';'; + const matchingFileIds = []; fileDb.each(sql, (err, fileId) => { if(fileId) { From c5e3220c1d040e794748df92677eead85395e228 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jan 2018 21:17:59 -0700 Subject: [PATCH 0106/1013] Add support for finding messages by date for msg pointers --- core/message_area.js | 46 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index aa893073..e81c85e1 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -2,17 +2,19 @@ '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 sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +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; +const { getISOTimestampString } = require('./database.js'); // deps const async = require('async'); const _ = require('lodash'); const assert = require('assert'); +const moment = require('moment'); exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; @@ -28,6 +30,7 @@ exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; exports.getMessageListForArea = getMessageListForArea; exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; +exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; exports.getMessageAreaLastReadId = getMessageAreaLastReadId; exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.persistMessage = persistMessage; @@ -482,6 +485,28 @@ function getMessageListForArea(options, areaTag, cb) { ); } +function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { + if(moment.isMoment(newerThanTimestamp)) { + newerThanTimestamp = getISOTimestampString(newerThanTimestamp); + } + + msgDb.get( + `SELECT message_id + FROM message + WHERE area_tag = ? AND DATETIME(modified_timestamp) > DATETIME("${newerThanTimestamp}", "+1 seconds") + ORDER BY modified_timestamp ASC + LIMIT 1;`, + [ areaTag ], + (err, row) => { + if(err) { + return cb(err); + } + + return cb(null, row ? row.message_id : null); + } + ); +} + function getMessageAreaLastReadId(userId, areaTag, cb) { msgDb.get( 'SELECT message_id ' + @@ -494,7 +519,12 @@ function getMessageAreaLastReadId(userId, areaTag, cb) { ); } -function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) { +function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) { + if(!cb && _.isFunction(allowOlder)) { + cb = allowOlder; + allowOlder = false; + } + // :TODO: likely a better way to do this... async.waterfall( [ @@ -505,7 +535,7 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) { }); }, function update(lastId, callback) { - if(messageId > lastId) { + if(allowOlder || messageId > lastId) { msgDb.run( 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + 'VALUES (?, ?, ?);', From 00deb3fe72dda04ed9cafcbbef9d468373b7cfdd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jan 2018 21:39:14 -0700 Subject: [PATCH 0107/1013] * Add concept of external flavor to import/exported mails, e.g. 'ftn' * Add to/from remote user meta for opaqe addrs, e.g. 'ftn' flavor can use FTN-style addresses * Allow replys from inbox to a NetMail --- core/fse.js | 54 ++++++++++++++++++------------ core/message.js | 22 ++++++++++++ core/new_scan.js | 5 +-- core/oputil/oputil_message_base.js | 12 ++----- core/scanner_tossers/ftn_bso.js | 49 +++++++++++++++++---------- 5 files changed, 93 insertions(+), 49 deletions(-) diff --git a/core/fse.js b/core/fse.js index 4a6c8eb1..d84de1e9 100644 --- a/core/fse.js +++ b/core/fse.js @@ -411,30 +411,42 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return callback(null); }, function populateLocalUserInfo(callback) { - if(self.isPrivateMail()) { - self.message.setLocalFromUserId(self.client.user.userId); - - if(self.toUserId > 0) { - self.message.setLocalToUserId(self.toUserId); - callback(null); - } else { - // we need to look it up - User.getUserIdAndNameByLookup(self.message.toUserName, function userInfo(err, toUserId) { - if(err) { - callback(err); - } else { - self.message.setLocalToUserId(toUserId); - callback(null); - } - }); - } - } else { - callback(null); + if(!self.isPrivateMail()) { + return callback(null); } + + // :TODO: shouldn't local from user ID be set for all mail? + self.message.setLocalFromUserId(self.client.user.userId); + + if(self.toUserId > 0) { + self.message.setLocalToUserId(self.toUserId); + return callback(null); + } + + // + // If the message we're replying to is from a remote user + // don't try to look up the local user ID. Instead, mark the mail + // for export with the remote to address. + // + if(self.replyToMessage.isFromRemoteUser()) { + self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]); + self.message.setExternalFlavor(self.replyToMessage.meta.System[Message.SystemMetaNames.ExternalFlavor]); + return callback(null); + } + + // we need to look it up + User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => { + if(err) { + return callback(err); + } + + self.message.setLocalToUserId(toUserId); + return callback(null); + }); } ], - function complete(err) { - cb(err, self.message); + err => { + return cb(err, self.message); } ); } diff --git a/core/message.js b/core/message.js index 7fb57d20..360b211a 100644 --- a/core/message.js +++ b/core/message.js @@ -76,6 +76,10 @@ function Message(options) { this.isPrivate = function() { return Message.isPrivateAreaTag(this.areaTag); }; + + this.isFromRemoteUser = function() { + return null !== _.get(this, 'meta.System.remote_from_user', null); + }; } Message.WellKnownAreaTags = { @@ -93,6 +97,14 @@ Message.SystemMetaNames = { LocalFromUserID : 'local_from_user_id', StateFlags0 : 'state_flags0', // See Message.StateFlags0 ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. + ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.ExternalFlavors + RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address + RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address +}; + +// Types for Message.SystemMetaNames.ExternalFlavor meta +Message.ExternalFlavors = { + FTN : 'ftn', // FTN style }; Message.StateFlags0 = { @@ -133,6 +145,16 @@ Message.prototype.setLocalFromUserId = function(userId) { this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; }; +Message.prototype.setRemoteToUser = function(remoteTo) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; +}; + +Message.prototype.setExternalFlavor = function(flavor) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; +}; + Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { assert(_.isString(areaTag)); assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); diff --git a/core/new_scan.js b/core/new_scan.js index a75a5b2d..f3e851fb 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -170,6 +170,7 @@ exports.getModule = class NewScanModule extends MenuModule { const filterCriteria = { newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), areaTag : getAvailableFileAreaTags(this.client), + order : 'ascending', // oldest first }; FileEntry.findFiles( @@ -179,11 +180,11 @@ exports.getModule = class NewScanModule extends MenuModule { return cb(err ? err : Errors.DoesNotExist('No more new files')); } - FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[0] ); + FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[fileIds.length - 1] ); const menuOpts = { extraArgs : { - fileList : fileIds, + fileList : fileIds, }, }; diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 2ab93104..b1c6673e 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -90,18 +90,13 @@ function areaFix() { message : messageBody, areaTag : Message.WellKnownAreaTags.Private, // mark private meta : { - FtnProperty : { - [ Message.FtnPropertyNames.FtnDestZone ] : ftnAddr.zone, - [ Message.FtnPropertyNames.FtnDestNetwork ] : ftnAddr.net, - [ Message.FtnPropertyNames.FtnDestNode ] : ftnAddr.node, + System : { + [ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it + [ Message.SystemMetaNames.ExternalFlavor ] : Message.ExternalFlavors.FTN, // on FTN-style network } } }); - if(ftnAddr.point) { - message.meta.FtnProperty[Message.FtnPropertyNames.FtnDestPoint] = ftnAddr.point; - } - if(0 !== fromUserId) { message.setLocalFromUserId(fromUserId); } @@ -109,7 +104,6 @@ function areaFix() { return callback(null, message); }, function persistMessage(message, callback) { - // :TODO: Persist message in private outgoing (sysop out box) (TBD: implementation) message.persist(err => { if(!err) { console.log('AreaFix message persisted and will be exported at next scheduled scan'); diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index bba0c313..c7e3edc9 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -45,12 +45,8 @@ exports.moduleInfo = { /* :TODO: * Support (approx) max bundle size - * 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 - */ exports.getModule = FTNMessageScanTossModule; @@ -878,12 +874,7 @@ function FTNMessageScanTossModule() { } }, function discoverUplink(callback) { - const dstAddr = new Address({ - zone : parseInt(message.meta.FtnProperty.ftn_dest_zone), - net : parseInt(message.meta.FtnProperty.ftn_dest_network), - node : parseInt(message.meta.FtnProperty.ftn_dest_node), - point : parseInt(message.meta.FtnProperty.ftn_dest_point) || null, // point is optional - }); + const dstAddr = new Address(message.meta.System[Message.SystemMetaNames.RemoteToUser]); return self.getAcceptableNetMailNetworkInfoFromAddress(dstAddr, (err, config, routeAddress, networkName) => { if(err) { @@ -1152,6 +1143,9 @@ function FTNMessageScanTossModule() { function basicSetup(callback) { message.areaTag = config.localAreaTag; + // indicate this was imported from FTN + message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.ExternalFlavors.FTN; + // // If we *allow* dupes (disabled by default), then just generate // a random UUID. Otherwise, don't assign the UUID just yet. It will be @@ -1177,6 +1171,22 @@ function FTNMessageScanTossModule() { return callback(null); } + // + // Create a meta value for the *remote* from user. In the case here with FTN, + // their fully qualified FTN from address + // + const intlKludge = _.get(message, 'meta.FtnKludge.INTL'); + if(intlKludge && intlKludge.length > 0) { + let fromAddress = intlKludge.split(' ')[0]; + + const fromPointKludge = _.get(message, 'meta.FtnKludge.FMPT'); + if(fromPointKludge) { + fromAddress += `.${fromPointKludge}`; + } + + message.meta.System[Message.SystemMetaNames.RemoteFromUser] = fromAddress; + } + const lookupName = self.getLocalUserNameFromAlias(message.toUserName); User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { @@ -1911,8 +1921,7 @@ function FTNMessageScanTossModule() { this.performNetMailExport = function(cb) { // // Select all messages with a |message_id| > |lastScanId| in the private area - // that also *do not* have a local user ID meta value but *do* have a FTN dest - // network meta value. + // that are schedule for export to FTN-style networks. // // Just like EchoMail, we additionally exclude messages with the System state_flags0 // which will be present for imported or already exported messages @@ -1927,12 +1936,18 @@ function FTNMessageScanTossModule() { WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND (SELECT COUNT(message_id) FROM message_meta - WHERE message_id = m.message_id AND meta_category = 'System' AND - (meta_name = 'state_flags0' OR meta_name='local_to_user_id')) = 0 + WHERE message_id = m.message_id + AND meta_category = 'System' + AND (meta_name = 'state_flags0' OR meta_name = 'local_to_user_id') + ) = 0 AND (SELECT COUNT(message_id) FROM message_meta - WHERE message_id = m.message_id AND meta_category='FtnProperty' AND meta_name='ftn_dest_network') = 1 + WHERE message_id = m.message_id + AND meta_category = 'System' + AND meta_name = '${Message.SystemMetaNames.ExternalFlavor}' + AND meta_value = '${Message.ExternalFlavors.FTN}' + ) = 1 ORDER BY message_id; `; @@ -1967,8 +1982,8 @@ function FTNMessageScanTossModule() { this.isNetMailMessage = function(message) { return message.isPrivate() && - null === _.get(message.meta, 'System.LocalToUserID', null) && - null !== _.get(message.meta, 'FtnProperty.ftn_dest_network', null) + null === _.get(message, 'meta.System.LocalToUserID', null) && + Message.ExternalFlavors.FTN === _.get(message, 'meta.System.external_flavor', null) ; }; } From 27fcd40900921a421ff93d90be096379b37d44b9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 12 Jan 2018 19:06:33 -0700 Subject: [PATCH 0108/1013] Fix remote from user @ import --- core/scanner_tossers/ftn_bso.js | 40 +++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index c7e3edc9..8b86e850 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1113,6 +1113,32 @@ function FTNMessageScanTossModule() { return alias || lookup; }; + this.getAddressesFromNetMailMessage = function(message) { + const intlKludge = _.get(message, 'meta.FtnKludge.INTL'); + + if(!intlKludge) { + return {}; + } + + let [ to, from ] = intlKludge.split(' '); + if(!to || !from) { + return {}; + } + + const fromPoint = _.get(message, 'meta.FtnKludge.FMPT'); + const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); + + if(fromPoint) { + from += `.${fromPoint}`; + } + + if(toPoint) { + to += `.${toPoint}`; + } + + return { to : Address.fromString(to), from : Address.fromString(from) }; + }; + this.importMailToArea = function(config, header, message, cb) { async.series( [ @@ -1175,18 +1201,14 @@ function FTNMessageScanTossModule() { // Create a meta value for the *remote* from user. In the case here with FTN, // their fully qualified FTN from address // - const intlKludge = _.get(message, 'meta.FtnKludge.INTL'); - if(intlKludge && intlKludge.length > 0) { - let fromAddress = intlKludge.split(' ')[0]; + const { from } = self.getAddressesFromNetMailMessage(message); - const fromPointKludge = _.get(message, 'meta.FtnKludge.FMPT'); - if(fromPointKludge) { - fromAddress += `.${fromPointKludge}`; - } - - message.meta.System[Message.SystemMetaNames.RemoteFromUser] = fromAddress; + if(!from) { + return callback(Errors.Invalid('Cannot import FTN NetMail without valid INTL line')); } + message.meta.System[Message.SystemMetaNames.RemoteFromUser] = from.toString(); + const lookupName = self.getLocalUserNameFromAlias(message.toUserName); User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { From 2bc8e417e4a66ba1c3a4ed557e768685d61e803b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 12 Jan 2018 23:44:22 -0700 Subject: [PATCH 0109/1013] Fix non-reply crash --- core/fse.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/fse.js b/core/fse.js index d84de1e9..824704dc 100644 --- a/core/fse.js +++ b/core/fse.js @@ -342,8 +342,6 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // really don't like ANSI messages in UTF-8 encoding (they should!) // msgOpts.meta = { System : { 'explicit_encoding' : Config.scannerTossers.ftn_bso.packetAnsiMsgEncoding || 'cp437' } }; - // :TODO: change to \r\nESC[A - //msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}${msgOpts.message}`; msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; } } @@ -428,7 +426,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // don't try to look up the local user ID. Instead, mark the mail // for export with the remote to address. // - if(self.replyToMessage.isFromRemoteUser()) { + if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) { self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]); self.message.setExternalFlavor(self.replyToMessage.meta.System[Message.SystemMetaNames.ExternalFlavor]); return callback(null); From 08ea798d53afa4b0d16c4b22e5f7e0bd22e18783 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 13 Jan 2018 08:57:13 -0700 Subject: [PATCH 0110/1013] Rename to AddressFlavor --- core/mail_util.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++ core/message.js | 10 +++--- 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 core/mail_util.js diff --git a/core/mail_util.js b/core/mail_util.js new file mode 100644 index 00000000..654b1617 --- /dev/null +++ b/core/mail_util.js @@ -0,0 +1,81 @@ +/* jslint node: true */ +'use strict'; + +const Address = require('./ftn_address.js'); +const Message = require('./message.js'); + +exports.getAddressedToInfo = getAddressedToInfo; + +const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +/* + Input Output + ---------------------------------------------------------------------------------------------------- + User { name : 'User', flavor : 'local' } + Some User { name : 'Some User', flavor : 'local' } + JoeUser @ 1:103/75 { name : 'JoeUser', flavor : 'ftn', remote : '1:103/75' } + Bob@1:103/705@fidonet.org { name : 'Bob', flavor : 'ftn', remote : '1:103/705@fidonet.org' } + 1:103/705@fidonet.org { flavor : 'ftn', remote : '1:103/705@fidonet.org' } + Jane <23:4/100> { name : 'Jane', flavor : 'ftn', remote : '23:4/100' } + 43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' } + foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' } + Bar { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } +*/ +function getAddressedToInfo(input) { + input = input.trim(); + + const firstAtPos = input.indexOf('@'); + + if(firstAtPos < 0) { + let addr = Address.fromString(input); + if(Address.isValidAddress(addr)) { + return { flavor : Message.AddressFlavor.FTN, remote : input }; + } + + const lessThanPos = input.indexOf('<'); + if(lessThanPos < 0) { + return { name : input, flavor : Message.AddressFlavor.Local }; + } + + const greaterThanPos = input.indexOf('>'); + if(greaterThanPos < lessThanPos) { + return { name : input, flavor : Message.AddressFlavor.Local }; + } + + addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); + if(Address.isValidAddress(addr)) { + return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; + } + + return { name : input, flavor : Message.AddressFlavor.Local }; + } + + const lessThanPos = input.indexOf('<'); + const greaterThanPos = input.indexOf('>'); + if(lessThanPos > 0 && greaterThanPos > lessThanPos) { + const addr = input.slice(lessThanPos + 1, greaterThanPos); + const m = addr.match(EMAIL_REGEX); + if(m) { + return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr }; + } + + return { name : input, flavor : Message.AddressFlavor.Local }; + } + + let m = input.match(EMAIL_REGEX); + if(m) { + return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; + } + + let addr = Address.fromString(input); // 5D? + if(Address.isValidAddress(addr)) { + return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; + } + + addr = Address.fromString(input.slice(firstAtPos + 1).trim()); + if(Address.isValidAddress(addr)) { + return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; + } + + return { name : input, flavor : Message.AddressFlavor.Local }; +} diff --git a/core/message.js b/core/message.js index 360b211a..5d3c8db9 100644 --- a/core/message.js +++ b/core/message.js @@ -97,14 +97,16 @@ Message.SystemMetaNames = { LocalFromUserID : 'local_from_user_id', StateFlags0 : 'state_flags0', // See Message.StateFlags0 ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. - ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.ExternalFlavors - RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address - RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address + ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor + RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address + RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address }; // Types for Message.SystemMetaNames.ExternalFlavor meta -Message.ExternalFlavors = { +Message.AddressFlavor = { + Local : 'local', // local / non-remote addressing FTN : 'ftn', // FTN style + Email : 'email', }; Message.StateFlags0 = { From 149f8bd9f541940ca4602a872584832cf5d0b0f8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 13 Jan 2018 08:57:54 -0700 Subject: [PATCH 0111/1013] Add valid check methods --- core/ftn_address.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/ftn_address.js b/core/ftn_address.js index 9edb3819..f0936e1d 100644 --- a/core/ftn_address.js +++ b/core/ftn_address.js @@ -20,6 +20,15 @@ module.exports = class Address { } } + static isValidAddress(addr) { + return addr && addr.isValid(); + } + + isValid() { + // FTN address is valid if we have at least a net/node + return _.isNumber(this.net) && _.isNumber(this.node); + } + isEqual(other) { if(_.isString(other)) { other = Address.fromString(other); From 9a00b3eb156b95c7cfcf78b4b0577a6f7587736a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 13 Jan 2018 08:58:28 -0700 Subject: [PATCH 0112/1013] Add validateGeneralMailAddressedTo() --- core/system_view_validate.js | 39 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/core/system_view_validate.js b/core/system_view_validate.js index 07bbcab2..e2a5b2e0 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -2,9 +2,11 @@ 'use strict'; // ENiGMA½ -const User = require('./user.js'); -const Config = require('./config.js').config; -const Log = require('./logger.js').log; +const User = require('./user.js'); +const Config = require('./config.js').config; +const Log = require('./logger.js').log; +const { getAddressedToInfo } = require('./mail_util.js'); +const Message = require('./message.js'); // deps const fs = require('graceful-fs'); @@ -14,6 +16,7 @@ exports.validateMessageSubject = validateMessageSubject; exports.validateUserNameAvail = validateUserNameAvail; exports.validateUserNameExists = validateUserNameExists; exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists; +exports.validateGeneralMailAddressedTo = validateGeneralMailAddressedTo; exports.validateEmailAvail = validateEmailAvail; exports.validateBirthdate = validateBirthdate; exports.validatePasswordSpec = validatePasswordSpec; @@ -55,30 +58,44 @@ function validateUserNameAvail(data, cb) { } } -function validateUserNameExists(data, cb) { - const invalidUserNameError = new Error('Invalid username'); +const invalidUserNameError = () => new Error('Invalid username'); +function validateUserNameExists(data, cb) { if(0 === data.length) { - return cb(invalidUserNameError); + return cb(invalidUserNameError()); } User.getUserIdAndName(data, (err) => { - return cb(err ? invalidUserNameError : null); + return cb(err ? invalidUserNameError() : null); }); } function validateUserNameOrRealNameExists(data, cb) { - const invalidUserNameError = new Error('Invalid username'); - if(0 === data.length) { - return cb(invalidUserNameError); + return cb(invalidUserNameError()); } User.getUserIdAndNameByLookup(data, err => { - return cb(err ? invalidUserNameError : null); + return cb(err ? invalidUserNameError() : null); }); } +function validateGeneralMailAddressedTo(data, cb) { + // + // Allow any supported addressing: + // - Local username or real name + // - Supported remote flavors such as FTN, email, ... + // + // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules. + const addressedToInfo = getAddressedToInfo(data); + + if(Message.AddressFlavor.FTN === addressedToInfo.flavor) { + return cb(null); + } + + return validateUserNameOrRealNameExists(data, cb); +} + function validateEmailAvail(data, cb) { // // This particular method allows empty data - e.g. no email entered From 84fd0ff6d21a23fb07af3ea11c233da6bc2715ba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 13 Jan 2018 09:06:50 -0700 Subject: [PATCH 0113/1013] Add ability to send directly to a NetMail address --- core/fse.js | 13 +++++++++++++ core/oputil/oputil_message_base.js | 2 +- core/scanner_tossers/ftn_bso.js | 6 +++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/fse.js b/core/fse.js index 824704dc..51b8dc5c 100644 --- a/core/fse.js +++ b/core/fse.js @@ -15,6 +15,7 @@ const stringFormat = require('./string_format.js'); const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); const Config = require('./config.js').config; +const { getAddressedToInfo } = require('./mail_util.js'); // deps const async = require('async'); @@ -432,6 +433,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return callback(null); } + // + // Detect if the user is attempting to send to a remote mail type that we support + // + // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such + const addressedToInfo = getAddressedToInfo(self.message.toUserName); + if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) { + self.message.setRemoteToUser(addressedToInfo.remote); + self.message.setExternalFlavor(addressedToInfo.flavor); + self.message.toUserName = addressedToInfo.name; + return callback(null); + } + // we need to look it up User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => { if(err) { diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index b1c6673e..6e89cf73 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -92,7 +92,7 @@ function areaFix() { meta : { System : { [ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it - [ Message.SystemMetaNames.ExternalFlavor ] : Message.ExternalFlavors.FTN, // on FTN-style network + [ Message.SystemMetaNames.ExternalFlavor ] : Message.AddressFlavor.FTN, // on FTN-style network } } }); diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 8b86e850..4285884e 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1170,7 +1170,7 @@ function FTNMessageScanTossModule() { message.areaTag = config.localAreaTag; // indicate this was imported from FTN - message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.ExternalFlavors.FTN; + message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.FTN; // // If we *allow* dupes (disabled by default), then just generate @@ -1968,7 +1968,7 @@ function FTNMessageScanTossModule() { WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = '${Message.SystemMetaNames.ExternalFlavor}' - AND meta_value = '${Message.ExternalFlavors.FTN}' + AND meta_value = '${Message.AddressFlavor.FTN}' ) = 1 ORDER BY message_id; `; @@ -2005,7 +2005,7 @@ function FTNMessageScanTossModule() { this.isNetMailMessage = function(message) { return message.isPrivate() && null === _.get(message, 'meta.System.LocalToUserID', null) && - Message.ExternalFlavors.FTN === _.get(message, 'meta.System.external_flavor', null) + Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null) ; }; } From 011c863547d1bc9fe320cc6ad3d84d4136b36a04 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 13 Jan 2018 09:08:17 -0700 Subject: [PATCH 0114/1013] Use validateGeneralMailAddressedTo for private mail box --- config/menu.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/menu.hjson b/config/menu.hjson index 0f1fa168..2225f274 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -2324,7 +2324,7 @@ ET2: { argName: to focus: true - validate: @systemMethod:validateUserNameExists + validate: @systemMethod:validateGeneralMailAddressedTo } ET3: { argName: subject From e7b0e4af30dfe8a709670c82bc07f03ae6c2a982 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 13:52:40 -0700 Subject: [PATCH 0115/1013] Add private exported + sent mail cleanup to trimMessageAreasScheduledEvent() scheduled event --- core/config.js | 5 +- core/fse.js | 2 +- core/message_area.js | 87 ++++++++++++++++++++++++--------- core/scanner_tossers/ftn_bso.js | 8 ++- 4 files changed, 75 insertions(+), 27 deletions(-) diff --git a/core/config.js b/core/config.js index f9f277a8..62ce6032 100644 --- a/core/config.js +++ b/core/config.js @@ -598,8 +598,9 @@ function getDefaultConfig() { areas : { private_mail : { - name : 'Private Mail', - desc : 'Private user to user mail/email', + name : 'Private Mail', + desc : 'Private user to user mail/email', + maxExternalSentAgeDays : 30, // max external "outbox" item age }, local_bulletin : { diff --git a/core/fse.js b/core/fse.js index 51b8dc5c..b266a9c9 100644 --- a/core/fse.js +++ b/core/fse.js @@ -990,7 +990,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.viewControllers.body.switchFocus(1); this.observeEditorEvents(); - }; + } switchToFooter() { this.viewControllers.header.setFocus(false); diff --git a/core/message_area.js b/core/message_area.js index e81c85e1..53dd3086 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -598,11 +598,11 @@ function trimMessageAreasScheduledEvent(args, cb) { LIMIT -1 OFFSET ${areaInfo.maxMessages} );`, [ areaInfo.areaTag.toLowerCase() ], - err => { + function result(err) { // no arrow func; need this if(err) { - Log.error( { areaInfo : areaInfo, err : err, type : 'maxMessages' }, 'Error trimming message area'); + Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area'); } else { - Log.debug( { areaInfo : areaInfo, type : 'maxMessages' }, 'Area trimmed successfully'); + Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully'); } return cb(err); } @@ -618,21 +618,25 @@ function trimMessageAreasScheduledEvent(args, cb) { `DELETE FROM message WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, [ areaInfo.areaTag ], - err => { + function result(err) { // no arrow func; need this if(err) { - Log.warn( { areaInfo : areaInfo, err : err, type : 'maxAgeDays' }, 'Error trimming message area'); + Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area'); } else { - Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays' }, 'Area trimmed successfully'); + Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully'); } return cb(err); } ); } - + async.waterfall( - [ + [ function getAreaTags(callback) { - let areaTags = []; + const areaTags = []; + + // + // We use SQL here vs API such that no-longer-used tags are picked up + // msgDb.each( `SELECT DISTINCT area_tag FROM message;`, @@ -640,7 +644,11 @@ function trimMessageAreasScheduledEvent(args, cb) { if(err) { return callback(err); } - areaTags.push(row.area_tag); + + // We treat private mail special + if(!Message.isPrivateAreaTag(row.area_tag)) { + areaTags.push(row.area_tag); + } }, err => { return callback(err, areaTags); @@ -652,30 +660,26 @@ function trimMessageAreasScheduledEvent(args, cb) { // determine maxMessages & maxAgeDays per area areaTags.forEach(areaTag => { - + let maxMessages = Config.messageAreaDefaults.maxMessages; let maxAgeDays = Config.messageAreaDefaults.maxAgeDays; - + const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here if(area) { - if(area.maxMessages) { - maxMessages = area.maxMessages; - } - if(area.maxAgeDays) { - maxAgeDays = area.maxAgeDays; - } + maxMessages = area.maxMessages || maxMessages; + maxAgeDays = area.maxAgeDays || maxAgeDays; } areaInfos.push( { areaTag : areaTag, maxMessages : maxMessages, maxAgeDays : maxAgeDays, - } ); + } ); }); return callback(null, areaInfos); }, - function trimAreas(areaInfos, callback) { + function trimGeneralAreas(areaInfos, callback) { async.each( areaInfos, (areaInfo, next) => { @@ -691,11 +695,50 @@ function trimMessageAreasScheduledEvent(args, cb) { }, callback ); - } + }, + function trimExternalPrivateSentMail(callback) { + // + // *External* (FTN, email, ...) outgoing is cleaned up *after export* + // if it is older than the configured |maxExternalSentAgeDays| days + // + // Outgoing externally exported private mail is: + // - In the 'private_mail' area + // - Marked exported (state_flags0 exported bit set) + // - Marked with any external flavor (we don't mark local) + // + const maxExternalSentAgeDays = _.get( + Config, + 'messageConferences.system_internal.areas.private_mail.maxExternalSentAgeDays', + 30 + ); + + msgDb.run( + `DELETE FROM message + WHERE message_id IN ( + SELECT m.message_id + FROM message m + JOIN message_meta mms + ON m.message_id = mms.message_id AND + (mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported})) + JOIN message_meta mmf + ON m.message_id = mmf.message_id AND + (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}') + WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days') + );`, + function results(err) { // no arrow func; need this + if(err) { + Log.warn( { error : err.message }, 'Error trimming private externally sent messages'); + } else { + Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully'); + } + } + ); + + return callback(null); + } ], err => { return cb(err); } ); - } \ No newline at end of file diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 4285884e..830e9daa 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1884,7 +1884,11 @@ function FTNMessageScanTossModule() { ORDER BY message_id;` ; - async.each(Object.keys(Config.messageNetworks.ftn.areas), (areaTag, nextArea) => { + // we shouldn't, but be sure we don't try to pick up private mail here + const areaTags = Object.keys(Config.messageNetworks.ftn.areas) + .filter(areaTag => Message.WellKnownAreaTags.Private !== areaTag); + + async.each(areaTags, (areaTag, nextArea) => { const areaConfig = Config.messageNetworks.ftn.areas[areaTag]; if(!this.isAreaConfigValid(areaConfig)) { return nextArea(); @@ -1948,10 +1952,10 @@ function FTNMessageScanTossModule() { // Just like EchoMail, we additionally exclude messages with the System state_flags0 // which will be present for imported or already exported messages // - // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! // // :TODO: fill out the rest of the consts here // :TODO: this statement is crazy ugly + // :TODO: this should really check for extenral "flavor" and not-already-exported state_flags0 const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m From ab6e097a4f3ccfe60097b68a0c87ba012f77caa0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 15:00:27 -0700 Subject: [PATCH 0116/1013] FTN packet dump util --- util/dump_ftn_packet.js | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 util/dump_ftn_packet.js diff --git a/util/dump_ftn_packet.js b/util/dump_ftn_packet.js new file mode 100755 index 00000000..144d18f3 --- /dev/null +++ b/util/dump_ftn_packet.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const { Packet } = require('../core/ftn_mail_packet.js'); + +const argv = require('minimist')(process.argv.slice(2)); + +function main() { + if(0 === argv._.length) { + console.error('usage: dump_ftn_packet.js PATH'); + process.exitCode = -1; + return; + } + + const packet = new Packet(); + const packetPath = argv._[0]; + + packet.read( + packetPath, + (dataType, data, next) => { + if('header' === dataType) { + console.info('--- header ---'); + console.info(`Created : ${data.created.format('dddd, MMMM Do YYYY, h:mm:ss a')}`); + console.info(`Dst. Addr : ${data.destAddress.toString()}`); + console.info(`Src. Addr : ${data.origAddress.toString()}`); + console.info('--- raw header ---'); + console.info(data); + console.info('--------------'); + console.info(''); + } else if('message' === dataType) { + console.info('--- message ---'); + console.info(`To : ${data.toUserName}`); + console.info(`From : ${data.fromUserName}`); + console.info(`Subject : ${data.subject}`); + console.info('--- raw message ---'); + console.info(data); + console.info('---------------'); + } + + return next(null); + }, + () => { + console.info(''); + console.info('--- EOF --- '); + console.info(''); + } + ); +} + +main(); From 136d21276c18f8ef5a2c4c3d8551230c78254e74 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 17:09:23 -0700 Subject: [PATCH 0117/1013] Add new scan pointers for mb/fb --- art/themes/luciano_blocktronics/FMENU.ANS | Bin 3587 -> 3635 bytes art/themes/luciano_blocktronics/MSGMNU.ANS | Bin 3361 -> 3510 bytes .../luciano_blocktronics/SETFNSDATE.ANS | Bin 0 -> 407 bytes .../luciano_blocktronics/SETMNSDATE.ANS | Bin 0 -> 512 bytes config/menu.hjson | 85 ++++++ core/scanner_tossers/ftn_bso.js | 7 +- core/set_newscan_date.js | 261 ++++++++++++++++++ 7 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 art/themes/luciano_blocktronics/SETFNSDATE.ANS create mode 100644 art/themes/luciano_blocktronics/SETMNSDATE.ANS create mode 100644 core/set_newscan_date.js diff --git a/art/themes/luciano_blocktronics/FMENU.ANS b/art/themes/luciano_blocktronics/FMENU.ANS index a187491b8ea1d3615241ca70d469f5c0097bc40e..55879dd7e6b50547ef71dbaa3c7090cac8a5ddea 100644 GIT binary patch delta 94 zcmZpc*(|f+E1RLEvvjm!Zh?Yyw1Ks;S#EJ^i9%j#xk7PrVxB@ler8@tYLP-pVo9oW yw6S?EP^rP>?`)Y&CT5en*+VC%u`4qfSx)}R?l$=*yYglij+v~CCnn$HRRsXf`5%V> delta 49 zcmdli(=4;$D;txE*<>~LOh(Je57{-C3@s*4WDlJj!67r5kHeYC%ye@h#~fD18I$?= FQ~`Uh4jBLd diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS index e27fed731dfca52ef127d946b6eb39994b54c8a0..ce6f4815645341d1f226f2de457a4156a048fe62 100644 GIT binary patch delta 752 zcmZ9KJxc>Y5QZ@^iCzp?iJC-KG^m|>ce$u(`~WQ!D?u?;FlS82DtHH$e$;}-Ep60N zEDVB$jac|A?%)dlfPX-6W^Q9J)v+`0`_6N-dq4V`zOJNZZoqW;A9ZF<<05FOu9rR12oc%JcSl>7A=O5UlAFF8Pbi`>FY;@ zQshF~Jh z&^}?#Uipw!%B~F!(&_EL{fzHsLnm!W7tcz8JH|VZs|jO}6c$kzO}%Ky^Tdp@g#?~* zDGk`(5)(PRh=O7Sg{cb!6BK7FK|fOG_e#!y`vy14q~TLYYH`5O~K9I(0WEebd@4L zz}5etOCg2(CptNK&b{e&^PTT?PQHxiZ09WDa-(8(uPwb?$oodc<=x)Pyj4K7;Q9<_ z2s-#>s(Fb+ZS29aO@u9za490*^6viH#kxX@XOWFP`0?7M2wuet;D)+VO*W^`_RqRx~F8)dM%NJwsWDz8jS&)rC%CHY0s# z59$1o@Qy}fzZH41)>Y7Em-Sob*Mp$y)gybVOe)_S5xw^L)uPH|*hrSZlHeszv?6Xb zOrEaF9xk=FsgV;MMso8ue={m2@`YxqpgXFI4X>&7DJjhRA?_t%7^are^1?1TnPp)5 z9Bk=#(^13#ZSrdJqkpFqm47~tFT^sGf!P8%4u}5a4lJZs6a}w+m_FW9KLl%q;RF+5 n7>WUtY$X(oF(K%f0-y3R7!au5f{h3c7m&+b#J1)gb{#;Yt9)nkj8wUvvtRFf zI^Ec$!;G)UW@QqjrgLBXByi9}aTc{quZ=}4DSxUMWfvtT^)Lv+a2`|wSJiQU3Y(Gk H-S>O}v}aWs literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/SETMNSDATE.ANS b/art/themes/luciano_blocktronics/SETMNSDATE.ANS new file mode 100644 index 0000000000000000000000000000000000000000..4d3b43a314582ba6d98d9721010a2432cb717c88 GIT binary patch literal 512 zcmb`DF;BxV5QUQsWo5(5EoH@yk*Ub!1gm051-QWADWtAcsTU*~1pk14jX2w(LJUlJ zuyd#PzIzwucy-Pzd4UCUs$ou+_(``!U`|w0VQZ|_3j?*b2A1X#Y@?TOrB{ZLmjomX zrh)*3VQwBM3j60_1ENrgMHr(ulzT0_ZKmVCp5sH;P84Mu{t3M?u_&B1{Ek9{l?Vo2}oiWAq=M=8jaA6w_ZwuBj&D z{`CpB<8PC(VRMdWG4bu61X`~5yW+&^`^In{M*q_(xtMdo)kmIXd47`}W$_$e*DX9O IbgB;b7b=f&VgLXD literal 0 HcmV?d00001 diff --git a/config/menu.hjson b/config/menu.hjson index 2225f274..8eaedb1e 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1766,6 +1766,10 @@ value: { command: "]" } action: @systemMethod:nextArea } + { + value: { command: "D" } + action: @menu:messageAreaSetNewScanDate + } { value: 1 action: @menu:messageArea @@ -1803,6 +1807,47 @@ } } + messageAreaSetNewScanDate: { + module: set_newscan_date + desc: Message Base + art: SETMNSDATE + config: { + target: message + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + SM2: { + argName: targetSelection + submit: false + justify: right + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + messageAreaChangeCurrentArea: { // :TODO: rename this art to ACHANGE art: CHANGE @@ -2493,9 +2538,49 @@ value: { menuOption: "S" } action: @menu:fileBaseSearch } + { + value: { menuOption: "P" } + action: @menu:fileBaseSetNewScanDate + } ] } + fileBaseSetNewScanDate: { + module: set_newscan_date + desc: File Base + art: SETFNSDATE + config: { + target: file + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + fileBaseListEntries: { module: file_area_list desc: Browsing Files diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 830e9daa..8da71d02 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -349,8 +349,8 @@ function FTNMessageScanTossModule() { message.meta.FtnKludge.FMPT = localAddress.point; } - if(_.get(message, 'meta.FtnProperty.ftn_dest_point', 0) > 0) { - message.meta.FtnKludge.TOPT = message.meta.FtnProperty.ftn_dest_point; + if(_.isNumber(options.destAddress.point) && options.destAddress.point > 0) { + message.meta.FtnKludge.TOPT = options.destAddress.point; } } else { // We need to set some destination info for EchoMail @@ -1954,8 +1954,7 @@ function FTNMessageScanTossModule() { // // // :TODO: fill out the rest of the consts here - // :TODO: this statement is crazy ugly - // :TODO: this should really check for extenral "flavor" and not-already-exported state_flags0 + // :TODO: this statement is crazy ugly -- use JOIN / NOT EXISTS for state_flags & 0x02 const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js new file mode 100644 index 00000000..0e29e999 --- /dev/null +++ b/core/set_newscan_date.js @@ -0,0 +1,261 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const Errors = require('./enig_error.js').Errors; +const FileEntry = require('./file_entry.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const { getAvailableFileAreaTags } = require('./file_base_area.js'); +const { + getSortedAvailMessageConferences, + getSortedAvailMessageAreasByConfTag, + updateMessageAreaLastReadId, + getMessageIdNewerThanTimestampByArea +} = require('./message_area.js'); +const stringFormat = require('./string_format.js'); + +// deps +const async = require('async'); +const moment = require('moment'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Set New Scan Date', + desc : 'Sets new scan date for applicable scans', + author : 'NuSkooler', +}; + +const MciViewIds = { + main : { + scanDate : 1, + targetSelection : 2, + } +}; + +// :TODO: for messages, we could insert "conf - all areas" into targets, and allow such + +exports.getModule = class SetNewScanDate extends MenuModule { + constructor(options) { + super(options); + + const config = this.menuConfig.config; + + this.target = config.target || 'message'; + this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; + + this.menuMethods = { + scanDateSubmit : (formData, extraArgs, cb) => { + let scanDate = _.get(formData, 'value.scanDate'); + if(!scanDate) { + return cb(Errors.MissingParam('"scanDate" missing from form data')); + } + + scanDate = moment(scanDate, this.scanDateFormat); + if(!scanDate.isValid()) { + return cb(Errors.Invalid(`"${_.get(formData, 'value.scanDate')}" is not a valid date`)); + } + + const targetSelection = _.get(formData, 'value.targetSelection'); // may be undefined if N/A + + this[`setNewScanDateFor${_.capitalize(this.target)}Base`](targetSelection, scanDate, () => { + return this.prevMenu(cb); + }); + }, + }; + } + + setNewScanDateForMessageBase(targetSelection, scanDate, cb) { + const target = this.targetSelections[targetSelection]; + if(!target) { + return cb(Errors.UnexpectedState('Unable to get target in which to set new scan')); + } + + // selected area, or all of 'em + let updateAreaTags; + if('' === target.area.areaTag) { + updateAreaTags = this.targetSelections + .map( targetSelection => targetSelection.area.areaTag ) + .filter( areaTag => areaTag ); // remove the blank 'all' entry + } else { + updateAreaTags = [ target.area.areaTag ]; + } + + async.each(updateAreaTags, (areaTag, nextAreaTag) => { + getMessageIdNewerThanTimestampByArea(areaTag, scanDate, (err, messageId) => { + if(err) { + return nextAreaTag(err); + } + + if(!messageId) { + return nextAreaTag(null); // nothing to do + } + + messageId = Math.max(messageId - 1, 0); + + return updateMessageAreaLastReadId( + this.client.user.userId, + areaTag, + messageId, + true, // allowOlder + nextAreaTag + ); + }); + }, err => { + return cb(err); + }); + } + + setNewScanDateForFileBase(targetSelection, scanDate, cb) { + // + // ENiGMA doesn't currently have the concept of per-area + // scan pointers for users, so we use all areas avail + // to the user. + // + const filterCriteria = { + areaTag : getAvailableFileAreaTags(this.client), + newerThanTimestamp : scanDate, + limit : 1, + orderBy : 'upload_timestamp', + order : 'ascending', + }; + + FileEntry.findFiles(filterCriteria, (err, fileIds) => { + if(err) { + return cb(err); + } + + if(!fileIds || 0 === fileIds.length) { + // nothing to do + return cb(null); + } + + const pointerFileId = Math.max(fileIds[0] - 1, 0); + + return FileBaseFilters.setFileBaseLastViewedFileIdForUser( + this.client.user, + pointerFileId, + true, // allowOlder + cb + ); + }); + } + + loadAvailMessageBaseSelections(cb) { + // + // Create an array of objects with conf/area information per entry, + // sorted naturally or via the 'sort' member in config + // + const selections = []; + getSortedAvailMessageConferences(this.client).forEach(conf => { + getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).forEach(area => { + selections.push({ + conf : { + confTag : conf.confTag, + name : conf.conf.name, + desc : conf.conf.desc, + }, + area : { + areaTag : area.areaTag, + name : area.area.name, + desc : area.area.desc, + } + }); + }); + }); + + selections.unshift({ + conf : { + confTag : '', + name : 'All conferences', + desc : 'All conferences', + }, + area : { + areaTag : '', + name : 'All areas', + desc : 'All areas', + } + }); + + // Find current conf/area & move it directly under "All" + const currConfTag = this.client.user.properties.message_conf_tag; + const currAreaTag = this.client.user.properties.message_area_tag; + if(currConfTag && currAreaTag) { + const confAreaIndex = selections.findIndex( confArea => { + return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; + }); + + if(confAreaIndex > -1) { + selections.splice(1, 0, selections.splice(confAreaIndex, 1)[0]); + } + } + + this.targetSelections = selections; + + return cb(null); + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + const self = this; + const vc = self.addViewController( 'main', new ViewController( { client : this.client } ) ); + + async.series( + [ + function validateConfig(callback) { + if(![ 'message', 'file' ].includes(self.target)) { + return callback(Errors.Invalid(`Invalid "target" in config: ${self.target}`)); + } + // :TOD0: validate scanDateFormat + return callback(null); + }, + function loadFromConfig(callback) { + return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + }, + function loadAvailSelections(callback) { + switch(self.target) { + case 'message' : + return self.loadAvailMessageBaseSelections(callback); + + default : + return callback(null); + } + }, + function populateForm(callback) { + const today = moment(); + + const scanDateView = vc.getView(MciViewIds.main.scanDate); + + // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now + const scanDateFormat = self.scanDateFormat.replace(/[\/\-. ]/g, ''); + scanDateView.setText(today.format(scanDateFormat)); + + if('message' === self.target) { + const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}'; + const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat; + + const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); + + targetSelectionView.setItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); + targetSelectionView.setFocusItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); + + targetSelectionView.setFocusItemIndex(0); + } + + self.viewControllers.main.resetInitialFocus(); + //vc.switchFocus(MciViewIds.main.scanDate); + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } +}; From 8804bcc5b26fd4cc3c306201272a5176e7fdcf35 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 19:40:40 -0700 Subject: [PATCH 0118/1013] * Luciano Blocktronics themed web download manager --- art/themes/luciano_blocktronics/FWDLMGR.ANS | Bin 0 -> 2410 bytes art/themes/luciano_blocktronics/theme.hjson | 21 ++++++++++++++++++++ config/menu.hjson | 6 +++++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 art/themes/luciano_blocktronics/FWDLMGR.ANS diff --git a/art/themes/luciano_blocktronics/FWDLMGR.ANS b/art/themes/luciano_blocktronics/FWDLMGR.ANS new file mode 100644 index 0000000000000000000000000000000000000000..b0e7fffc2dc2b2135b582710406cce810fe22ed9 GIT binary patch literal 2410 zcmb_eO;Zy=6a;T_^`yD%#k2bnvT9BNQfL(c6@)`hh(S`7(GtOfzl!4TWUJ<%Sa$cD zncVGGr#G!i_glpWE;|`-=^~tW4o>#>4n{lK zWVAop9e-fd(EO_!u**8;LlxZxmVz;?Dsc|n5pjLFV0zRPJa;}{%)XwwnHkHR+1!u$ zFByofU;?PpC9z74qNwt~JwKil@FuW9BFf09vI?*fOiH)SvV%YK0S0?fq4I-X!KeBM zpMur8<*P1AOI#;KJ-QhXRVfgVFeN2bk&Le*etCzMR#;jKS#qPAbYlPno7pY7f_BDx2LA=Xp`F|;l9!WAmoxY6 zC-wl;4KH) zW@4{6G#i^06d43v_+CvSAvv6oNGe)d9MFScvV*yCOF0%x>Xh_hu>5uRpwOW|!bDBb zD9Ka0@gc-eG?d95ri5}1f^;ccTk`MM^Q&aXX0X!A zP$U!(+};{t{X4YaqeTyL+}uR%IzEw~VTTeaSw|h?3s<#5Gu&_(_+~>{8R87Z8h_DO zGFm=UPdmwEnyjEH7YLhUB?|rqb=aFVyzV^P;3ICL!MxCT$JE%+-LZPMqz%{H387R- zKO6R+hu6O}e&gBU=;QV{&k^~*o-!}$ye!H`kJr}L*Wax@UDNaA?baka-W}N~ef|OA Cy7z?u literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index d553ed83..19f63194 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -787,6 +787,27 @@ } } + fileBaseWebDownloadManager: { + config: { + queueListFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" + focusQueueListFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" + queueManagerInfoFormat10: "|03batch|08: |03{webBatchDlLink}" + queueManagerInfoFormat11: "|03exp |08: |03{webBatchDlExpire}" + } + + 0: { + mci: { + VM1: { + height: 8 + } + HM2: { + width: 50 + focusTextStyle: first lower + } + } + } + } + fileBaseUploadFiles: { config: { // processing diff --git a/config/menu.hjson b/config/menu.hjson index 8eaedb1e..386c6f89 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -2530,6 +2530,10 @@ value: { menuOption: "D" } action: @menu:fileBaseDownloadManager } + { + value: { menuOption: "W" } + action: @menu:fileBaseWebDownloadManager + } { value: { menuOption: "U" } action: @menu:fileBaseUploadFiles @@ -3262,7 +3266,7 @@ art: ULNOAREA options: { pause: true - menuFlags: [ "noHistory" ] + menuFlags: [ "noHistory", "popParent" ] } } From 28e4a5d8a298e6cee958f9725c6361f9b065d10d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 20:28:19 -0700 Subject: [PATCH 0119/1013] Add WHATSNEW.md --- WHATSNEW.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 WHATSNEW.md diff --git a/WHATSNEW.md b/WHATSNEW.md new file mode 100644 index 00000000..667c296f --- /dev/null +++ b/WHATSNEW.md @@ -0,0 +1,26 @@ +# Whats New +This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. + +## 0.0.8-alpha +* [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. +* File descriptions (FILE_ID.DIZ, etc.) now support Renegade |## pipe, PCBoard, and other less common color codes found commonly in BBS era scene releases. +* New menu stack flags: `noHistory` now works as expected, and a new addition of `popParent`. See the default `menu.hjson` for usage. +* File structure changes making ENiGMA½ much easier to maintain and run in Docker. Thanks to RiPuk ([Dave Stephens](https://github.com/davestephens))! See [UPGRADE.md](UPGRADE.md) for details. +* Switch to pure JS [xxhash](https://github.com/mscdex/node-xxhash) instead of farmhash. Too many issues on ARM and other less popular CPUs with farmhash ([Dave Stephens](https://github.com/davestephens)) +* Native [CombatNet](http://combatnet.us/) support! ([Dave Stephens](https://github.com/davestephens)) +* Fix various issues with legacy DOS Telnet terminals. Note that some may still have issues with extensive CPR usage by ENiGMA½ that will be addressed in a future release. +* Added web (http://, https://) based download manager including batch downloads. Clickable links if using [VTXClient](https://github.com/codewar65/VTX_ClientServer)! +* General VTX hyperlink support for web links +* DEL vs Backspace key differences in FSE +* Correly parse oddball `INTL`, `TOPT`, `FMPT`, `Via`, etc. FTN kludge lines +* NetMail support! You can now send and receive NetMail. To send a NetMail address a external user using `Name
` format from your personal email menu. For example, `Foo Bar <123:123/123>`. The system also detects other formats such asa `Name @ address` (`Foo Bar@123:123/123`) +* `oputil.js`: Added `mb areafix` command to quickly send AreaFix messages from the command line. You can manually send them from personal mail as well. +* `oputil.js fb rm|remove|del|delete` functionality to remove file base entries +* Users can now (re)set File and Message base pointers +* Add `--update` option to `oputil.js fb scan` +* Fix @watch path support for event scheduler including FTN, e.g. when looking for a `toss!.now` file produced by Binkd. + +...LOTS more! + +## Pre 0.0.8-alpha +See GitHub \ No newline at end of file From 90706f87986f42a3e9faf807da3c44f3b0d9e280 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 22:00:00 -0700 Subject: [PATCH 0120/1013] * Move to Node.js 8.x LTS * Update some packages --- WHATSNEW.md | 4 ++++ package.json | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 667c296f..594418a7 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,6 +1,10 @@ # Whats New This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. +## 0.0.9-alpha +* Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! + + ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. * File descriptions (FILE_ID.DIZ, etc.) now support Renegade |## pipe, PCBoard, and other less common color codes found commonly in BBS era scene releases. diff --git a/package.json b/package.json index 678628f1..b44cc894 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.8-alpha", + "version": "0.0.9-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -33,12 +33,12 @@ "hashids": "^1.1.1", "hjson": "^3.1.0", "iconv-lite": "^0.4.18", - "inquirer": "^4.0.1", + "inquirer": "^5.0.0", "later": "1.2.0", "lodash": "^4.17.4", "mime-types": "^2.1.17", "minimist": "1.2.x", - "moment": "^2.20.0", + "moment": "^2.20.1", "nodemailer": "^4.4.1", "ptyw.js": "NuSkooler/ptyw.js", "rlogin": "^1.0.0", @@ -50,7 +50,7 @@ "temptmp": "^1.0.0", "uuid": "^3.1.0", "uuid-parse": "^1.0.0", - "ws": "^3.3.3", + "ws": "^4.0.0", "xxhash": "^0.2.4", "yazl": "^2.4.2" }, From a106050ba3c958b7099cc881f31b23517248e272 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 09:41:18 -0700 Subject: [PATCH 0121/1013] Fix attempts to load bad path --- core/events.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/events.js b/core/events.js index 8e16a374..5febb463 100644 --- a/core/events.js +++ b/core/events.js @@ -48,17 +48,17 @@ module.exports = new class Events extends events.EventEmitter { } async.each(files, (moduleName, nextModule) => { - modulePath = paths.join(modulePath, moduleName); + const fullModulePath = paths.join(modulePath, moduleName); try { - const mod = require(modulePath); - + const mod = require(fullModulePath); + if(_.isFunction(mod.registerEvents)) { // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? mod.registerEvents(this); } } catch(e) { - + Log.warn( { error : e }, 'Exception during module "registerEvents"'); } return nextModule(null); From ac1433e84beda36d89044e7f889b317bf21771ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 12:22:11 -0700 Subject: [PATCH 0122/1013] * Code cleanup and eslint since -- remove unused variables, clean up RegExs, so on... --- core/abracadabra.js | 10 +- core/acs.js | 4 +- core/ansi_escape_parser.js | 56 +++-- core/ansi_prep.js | 22 +- core/ansi_term.js | 82 ++++---- core/archive_util.js | 44 ++-- core/art.js | 16 +- core/asset.js | 8 +- core/bbs.js | 2 +- core/bbs_link.js | 10 +- core/bbs_list.js | 16 +- core/button_view.js | 2 +- core/client.js | 46 ++--- core/client_connections.js | 2 +- core/client_term.js | 14 +- core/color_codes.js | 10 +- core/combatnet.js | 92 ++++----- core/conf_area_util.js | 2 +- core/config.js | 4 +- core/config_cache.js | 2 +- core/connect.js | 2 +- core/crc.js | 4 +- core/database.js | 12 +- core/door.js | 10 +- core/door_party.js | 42 ++-- core/download_queue.js | 6 +- core/edit_text_view.js | 4 +- core/enig_error.js | 2 +- core/enigma_assert.js | 4 +- core/erc_client.js | 16 +- core/event_scheduler.js | 82 ++++---- core/exodus.js | 4 +- core/file_area_filter_edit.js | 28 +-- core/file_area_list.js | 54 ++--- core/file_area_web.js | 12 +- core/file_base_area.js | 70 +++---- core/file_base_area_select.js | 2 +- core/file_base_download_manager.js | 8 +- core/file_base_filter.js | 16 +- core/file_base_search.js | 2 +- core/file_base_web_download_manager.js | 27 ++- core/file_entry.js | 24 +-- core/file_transfer.js | 40 ++-- core/file_transfer_protocol_select.js | 4 +- core/file_util.js | 4 +- core/fnv1a.js | 10 +- core/fse.js | 134 ++++++------ core/ftn_address.js | 8 +- core/ftn_mail_packet.js | 146 ++++++------- core/ftn_util.js | 62 +++--- core/horizontal_menu_view.js | 4 +- core/key_entry_view.js | 4 +- core/last_callers.js | 12 +- core/logger.js | 8 +- core/login_server_module.js | 8 +- core/mail_packet.js | 2 +- core/mail_util.js | 2 +- core/mask_edit_text_view.js | 16 +- core/mci_view_factory.js | 25 ++- core/menu_module.js | 32 +-- core/menu_stack.js | 10 +- core/menu_util.js | 44 ++-- core/menu_view.js | 14 +- core/message.js | 112 +++++----- core/message_area.js | 98 ++++----- core/mime_util.js | 2 +- core/misc_util.js | 4 +- core/mod_mixins.js | 6 +- core/msg_area_list.js | 20 +- core/msg_area_post_fse.js | 4 +- core/msg_area_view_fse.js | 8 +- core/msg_conf_list.js | 18 +- core/msg_list.js | 38 ++-- core/msg_network.js | 8 +- core/msg_scan_toss_module.js | 6 +- core/multi_line_edit_text_view.js | 29 ++- core/new_scan.js | 42 ++-- core/nua.js | 16 +- core/onelinerz.js | 26 +-- core/plugin_module.js | 2 +- core/predefined_mci.js | 8 +- core/rumorz.js | 18 +- core/sauce.js | 8 +- core/scanner_tossers/ftn_bso.js | 25 ++- core/set_newscan_date.js | 4 +- core/spinner_menu_view.js | 23 +-- core/stat_log.js | 18 +- core/stats.js | 30 --- core/status_bar_view.js | 64 ------ core/string_format.js | 30 +-- core/string_util.js | 275 ++++--------------------- core/system_menu_method.js | 18 +- core/system_view_validate.js | 8 +- core/telnet_bridge.js | 20 +- core/text_view.js | 50 ++--- core/theme.js | 140 ++++++------- core/tic_file_info.js | 20 +- core/ticker_text_view.js | 94 --------- core/toggle_menu_view.js | 12 +- core/upload.js | 52 ++--- core/user.js | 40 ++-- core/user_config.js | 46 ++--- core/user_group.js | 54 +++-- core/user_list.js | 6 +- core/user_login.js | 8 +- core/uuid_util.js | 10 +- core/vertical_menu_view.js | 16 +- core/view.js | 16 +- core/view_controller.js | 70 +++---- core/web_password_reset.js | 19 +- core/whos_online.js | 2 +- core/word_wrap.js | 166 +++------------ 112 files changed, 1375 insertions(+), 1898 deletions(-) delete mode 100644 core/stats.js delete mode 100644 core/status_bar_view.js delete mode 100644 core/ticker_text_view.js diff --git a/core/abracadabra.js b/core/abracadabra.js index 85d1e205..0ac17887 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -87,11 +87,11 @@ exports.getModule = class AbracadabraModule extends MenuModule { [ function validateNodeCount(callback) { if(self.config.nodeMax > 0 && - _.isNumber(activeDoorNodeInstances[self.config.name]) && + _.isNumber(activeDoorNodeInstances[self.config.name]) && activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) { - self.client.log.info( - { + self.client.log.info( + { name : self.config.name, activeCount : activeDoorNodeInstances[self.config.name] }, @@ -118,11 +118,11 @@ exports.getModule = class AbracadabraModule extends MenuModule { } else { activeDoorNodeInstances[self.config.name] = 1; } - + callback(null); } }, - function generateDropfile(callback) { + function generateDropfile(callback) { self.dropFile = new DropFile(self.client, self.config.dropFileType); var fullPath = self.dropFile.fullPath; diff --git a/core/acs.js b/core/acs.js index f2e04b9f..9532ad78 100644 --- a/core/acs.js +++ b/core/acs.js @@ -13,7 +13,7 @@ class ACS { constructor(client) { this.client = client; } - + check(acs, scope, defaultAcs) { acs = acs ? acs[scope] : defaultAcs; acs = acs || defaultAcs; @@ -22,7 +22,7 @@ class ACS { } catch(e) { Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); return false; - } + } } // diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 7e777618..feb7b164 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -3,7 +3,9 @@ const miscUtil = require('./misc_util.js'); const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; +// deps const events = require('events'); const util = require('util'); const _ = require('lodash'); @@ -24,7 +26,7 @@ function ANSIEscapeParser(options) { this.graphicRendition = {}; this.parseState = { - re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex }; options = miscUtil.valueWithDefault(options, { @@ -46,7 +48,7 @@ function ANSIEscapeParser(options) { self.column = Math.max(self.column, 1); self.column = Math.min(self.column, self.termWidth); // can't move past term width self.row = Math.max(self.row, 1); - + self.positionUpdated(); }; @@ -63,7 +65,7 @@ function ANSIEscapeParser(options) { delete self.savedPosition; self.positionUpdated(); -// self.rowUpdated(); + // self.rowUpdated(); }; self.clearScreen = function() { @@ -71,7 +73,7 @@ function ANSIEscapeParser(options) { self.emit('clear screen'); }; -/* + /* self.rowUpdated = function() { self.emit('row update', self.row + self.scrollBack); };*/ @@ -95,7 +97,7 @@ function ANSIEscapeParser(options) { start = pos; self.column = 1; - + self.positionUpdated(); break; @@ -132,7 +134,7 @@ function ANSIEscapeParser(options) { if(self.column > self.termWidth) { self.column = 1; self.row += 1; - + self.positionUpdated(); } @@ -142,17 +144,9 @@ function ANSIEscapeParser(options) { } } - function getProcessedMCI(mci) { - if(self.mciReplaceChar.length > 0) { - return ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + new Array(mci.length + 1).join(self.mciReplaceChar); - } else { - return mci; - } - } - function parseMCI(buffer) { // :TODO: move this to "constants" seciton @ top - var mciRe = /\%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; + var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; var pos = 0; var match; var mciCode; @@ -186,27 +180,23 @@ function ANSIEscapeParser(options) { self.graphicRenditionForErase = _.clone(self.graphicRendition); } - - self.emit('mci', { - mci : mciCode, + + self.emit('mci', { + mci : mciCode, id : id ? parseInt(id, 10) : null, - args : args, + args : args, SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) }); if(self.mciReplaceChar.length > 0) { const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); - - self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[\;m]/).slice(0, 3)); + + self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)); literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); } else { literal(match[0]); } - - //literal(getProcessedMCI(match[0])); - - //self.emit('chunk', getProcessedMCI(match[0])); } } while(0 !== mciRe.lastIndex); @@ -220,7 +210,7 @@ function ANSIEscapeParser(options) { self.parseState = { // ignore anything past EOF marker, if any buffer : input.split(String.fromCharCode(0x1a), 1)[0], - re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex stop : false, }; }; @@ -290,14 +280,14 @@ function ANSIEscapeParser(options) { break; } } - - parseMCI(lastBit) + + parseMCI(lastBit); } self.emit('complete'); }; -/* + /* self.parse = function(buffer, savedRe) { // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: move this to "constants" section @ top @@ -382,12 +372,12 @@ function ANSIEscapeParser(options) { break; // save position - case 's' : + case 's' : self.saveCursorPosition(); break; // restore position - case 'u' : + case 'u' : self.restoreCursorPosition(); break; @@ -422,7 +412,7 @@ function ANSIEscapeParser(options) { case 1 : case 2 : - case 22 : + case 22 : self.graphicRendition.intensity = arg; break; @@ -448,7 +438,7 @@ function ANSIEscapeParser(options) { break; default : - console.log('Unknown attribute: ' + arg); // :TODO: Log properly + Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); break; } } diff --git a/core/ansi_prep.js b/core/ansi_prep.js index 45b93d32..a4c894d8 100644 --- a/core/ansi_prep.js +++ b/core/ansi_prep.js @@ -4,7 +4,7 @@ // ENiGMA½ const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSI = require('./ansi_term.js'); -const { +const { splitTextAtTerms, renderStringLength } = require('./string_util.js'); @@ -41,7 +41,7 @@ module.exports = function ansiPrep(input, options, cb) { if(canvas[row]) { return; } - + canvas[row] = Array.from( { length : options.cols}, () => new Object() ); } @@ -113,17 +113,17 @@ module.exports = function ansiPrep(input, options, cb) { const lastCol = getLastPopulatedColumn(row) + 1; let i; - line = options.indent ? + line = options.indent ? output.length > 0 ? ' '.repeat(options.indent) : '' : ''; - + for(i = 0; i < lastCol; ++i) { const col = row[i]; - sgr = !options.asciiMode && 0 === i ? + sgr = !options.asciiMode && 0 === i ? col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : ''; - + if(!options.asciiMode && col.sgr) { sgr += ANSI.getSGRFromGraphicRendition(col.sgr); } @@ -148,7 +148,7 @@ module.exports = function ansiPrep(input, options, cb) { if(options.exportMode) { // // If we're in export mode, we do some additional hackery: - // + // // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) // if a line must wrap early, we'll place a ESC[A ESC[C where // represents chars to get back to the position we were previously at @@ -157,8 +157,8 @@ module.exports = function ansiPrep(input, options, cb) { // // :TODO: this would be better to do as part of the processing above, but this will do for now const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with - let exportOutput = ''; - + let exportOutput = ''; + let m; let afterSeq; let wantMore; @@ -184,7 +184,7 @@ module.exports = function ansiPrep(input, options, cb) { splitAt = m.index; wantMore = false; // can't eat up any more } - + break; // seq's beyond this point are >= MAX_CHARS } } @@ -203,7 +203,7 @@ module.exports = function ansiPrep(input, options, cb) { exportOutput += `${part}\r\n`; if(fullLine.length > 0) { // more to go for this line? - exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; + exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; } else { exportOutput += ANSI.up(); } diff --git a/core/ansi_term.js b/core/ansi_term.js index 7eb10ec2..0a1eaa41 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -3,7 +3,7 @@ // // ANSI Terminal Support Resources -// +// // ANSI-BBS // * http://ansi-bbs.org/ // @@ -31,7 +31,7 @@ // For a board, we need to support the semi-standard ANSI-BBS "spec" which // is bastardized mix of DOS ANSI.SYS, cterm.txt, bansi.txt and a little other. // This gives us NetRunner, SyncTERM, EtherTerm, most *nix terminals, compatibilitiy -// with legit oldschool DOS terminals, and so on. +// with legit oldschool DOS terminals, and so on. // // ENiGMA½ @@ -113,7 +113,7 @@ const CONTROL = { // // Support: // * SyncTERM: Works as expected - // * NetRunner: + // * NetRunner: // // General Notes: // See also notes in bansi.txt and cterm.txt about the various @@ -160,7 +160,7 @@ const SGRValues = { negative : 7, hidden : 8, - normal : 22, // + normal : 22, // steady : 25, positive : 27, @@ -203,7 +203,7 @@ function getBGColorValue(name) { // :TODO: Create mappings for aliases... maybe make this a map to values instead // :TODO: Break this up in to two parts: // 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm) -// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. +// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. // ...we can then have getFontFromSAUCEName(sauceFontName) // Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings @@ -215,45 +215,45 @@ function getBGColorValue(name) { // const SYNCTERM_FONT_AND_ENCODING_TABLE = [ 'cp437', - 'cp1251', - 'koi8_r', - 'iso8859_2', - 'iso8859_4', - 'cp866', - 'iso8859_9', - 'haik8', - 'iso8859_8', - 'koi8_u', - 'iso8859_15', + 'cp1251', + 'koi8_r', + 'iso8859_2', 'iso8859_4', - 'koi8_r_b', - 'iso8859_4', - 'iso8859_5', - 'ARMSCII_8', + 'cp866', + 'iso8859_9', + 'haik8', + 'iso8859_8', + 'koi8_u', 'iso8859_15', - 'cp850', - 'cp850', - 'cp885', - 'cp1251', - 'iso8859_7', - 'koi8-r_c', - 'iso8859_4', - 'iso8859_1', - 'cp866', - 'cp437', - 'cp866', + 'iso8859_4', + 'koi8_r_b', + 'iso8859_4', + 'iso8859_5', + 'ARMSCII_8', + 'iso8859_15', + 'cp850', + 'cp850', 'cp885', - 'cp866_u', - 'iso8859_1', - 'cp1131', - 'c64_upper', + 'cp1251', + 'iso8859_7', + 'koi8-r_c', + 'iso8859_4', + 'iso8859_1', + 'cp866', + 'cp437', + 'cp866', + 'cp885', + 'cp866_u', + 'iso8859_1', + 'cp1131', + 'c64_upper', 'c64_lower', - 'c128_upper', - 'c128_lower', - 'atari', - 'pot_noodle', + 'c128_upper', + 'c128_lower', + 'atari', + 'pot_noodle', 'mo_soul', - 'microknight_plus', + 'microknight_plus', 'topaz_plus', 'microknight', 'topaz', @@ -289,7 +289,7 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = { 'topaz' : 'topaz', 'amiga_topaz_1' : 'topaz', 'amiga_topaz_1+' : 'topaz_plus', - 'topazplus' : 'topaz_plus', + 'topazplus' : 'topaz_plus', 'topaz_plus' : 'topaz_plus', 'amiga_topaz_2' : 'topaz', 'amiga_topaz_2+' : 'topaz_plus', @@ -349,7 +349,7 @@ function setCursorStyle(cursorStyle) { return `${ESC_CSI}${ps} q`; } return ''; - + } // Create methods such as up(), nextLine(),... diff --git a/core/archive_util.js b/core/archive_util.js index 6d2644c8..00e48ed8 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -23,7 +23,7 @@ class Archiver { } ok() { - return this.canCompress() && this.canDecompress(); + return this.canCompress() && this.canDecompress(); } can(what) { @@ -41,7 +41,7 @@ class Archiver { } module.exports = class ArchiveUtil { - + constructor() { this.archivers = {}; this.longestSignature = 0; @@ -93,7 +93,7 @@ module.exports = class ArchiveUtil { getArchiver(mimeTypeOrExtension) { mimeTypeOrExtension = resolveMimeType(mimeTypeOrExtension); - + if(!mimeTypeOrExtension) { // lookup returns false on failure return; } @@ -103,21 +103,23 @@ module.exports = class ArchiveUtil { return _.get( Config, [ 'archives', 'archivers', archiveHandler ] ); } } - + haveArchiver(archType) { return this.getArchiver(archType) ? true : false; } - detectTypeWithBuf(buf, cb) { - // :TODO: implement me! + // :TODO: implement me: + /* + detectTypeWithBuf(buf, cb) { } + */ detectType(path, cb) { fs.open(path, 'r', (err, fd) => { if(err) { return cb(err); } - + const buf = new Buffer(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { if(err) { @@ -140,7 +142,7 @@ module.exports = class ArchiveUtil { }); return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); - }); + }); }); } @@ -153,15 +155,15 @@ module.exports = class ArchiveUtil { err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); } }); - + proc.once('exit', exitCode => { return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); - }); + }); } compressTo(archType, archivePath, files, cb) { const archiver = this.getArchiver(archType); - + if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } @@ -189,13 +191,13 @@ module.exports = class ArchiveUtil { if(!cb && _.isFunction(fileList)) { cb = fileList; fileList = []; - haveFileList = false; + haveFileList = false; } else { haveFileList = true; } const archiver = this.getArchiver(archType); - + if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } @@ -211,7 +213,7 @@ module.exports = class ArchiveUtil { 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 @@ -230,9 +232,9 @@ module.exports = class ArchiveUtil { listEntries(archivePath, archType, cb) { const archiver = this.getArchiver(archType); - + if(!archiver) { - return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { @@ -240,7 +242,7 @@ module.exports = class ArchiveUtil { }; const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); - + let proc; try { proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); @@ -251,7 +253,7 @@ module.exports = class ArchiveUtil { let output = ''; proc.on('data', data => { // :TODO: hack for: execvp(3) failed.: No such file or directory - + output += data; }); @@ -273,16 +275,16 @@ module.exports = class ArchiveUtil { } return cb(null, entries); - }); + }); } - + getPtyOpts() { return { // :TODO: cwd name : 'enigma-archiver', cols : 80, rows : 24, - env : process.env, + env : process.env, }; } }; diff --git a/core/art.js b/core/art.js index 19e0bafe..546014bd 100644 --- a/core/art.js +++ b/core/art.js @@ -33,7 +33,7 @@ const SUPPORTED_ART_TYPES = { '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, - '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, + '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, // :TODO: extentions for wwiv, renegade, celerity, syncronet, ... // :TODO: extension for atari @@ -93,7 +93,7 @@ function getArtFromPath(path, options, cb) { } return result; - } + } if(options.readSauce === true) { sauce.readSAUCE(data, (err, sauce) => { @@ -164,7 +164,7 @@ function getArt(name, options, cb) { const bn = paths.basename(file, fext).toLowerCase(); if(options.random) { const suppliedBn = paths.basename(name, fext).toLowerCase(); - + // // Random selection enabled. We'll allow for // basename1.ext, basename2.ext, ... @@ -208,7 +208,7 @@ function getArt(name, options, cb) { return getArtFromPath(readPath, options, cb); } - + return cb(new Error(`No matching art for supplied criteria: ${name}`)); }); } @@ -287,7 +287,7 @@ function display(client, art, options, cb) { return cb(null, mciMap, extraInfo); } - if(!options.disableMciCache) { + if(!options.disableMciCache) { artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE); // see if we have a mciMap cached for this art @@ -307,7 +307,7 @@ function display(client, art, options, cb) { if(mciCprQueue.length > 0) { mciMap[mciCprQueue.shift()].position = pos; - if(parseComplete && 0 === mciCprQueue.length) { + if(parseComplete && 0 === mciCprQueue.length) { return completed(); } } @@ -345,7 +345,7 @@ function display(client, art, options, cb) { }); } - ansiParser.on('literal', literal => client.term.write(literal, false) ); + ansiParser.on('literal', literal => client.term.write(literal, false) ); ansiParser.on('control', control => client.term.rawWrite(control) ); ansiParser.on('complete', () => { @@ -353,7 +353,7 @@ function display(client, art, options, cb) { if(0 === mciCprQueue.length) { return completed(); - } + } }); let initSeq = ''; diff --git a/core/asset.js b/core/asset.js index 9f2831b7..3f44a604 100644 --- a/core/asset.js +++ b/core/asset.js @@ -31,7 +31,7 @@ const ALL_ASSETS = [ const ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*'); -function parseAsset(s) { +function parseAsset(s) { const m = ASSET_RE.exec(s); if(m) { @@ -68,7 +68,7 @@ function getAssetWithShorthand(spec, defaultType) { function getArtAsset(spec) { const asset = getAssetWithShorthand(spec, 'art'); - + if(!asset) { return null; } @@ -79,7 +79,7 @@ function getArtAsset(spec) { function getModuleAsset(spec) { const asset = getAssetWithShorthand(spec, 'systemModule'); - + if(!asset) { return null; } @@ -105,7 +105,7 @@ function resolveConfigAsset(spec) { return conf; } else { return spec; - } + } } function resolveSystemStatAsset(spec) { diff --git a/core/bbs.js b/core/bbs.js index 43bf7cf3..c2f54802 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -182,7 +182,7 @@ function initialize(cb) { return database.initializeDatabases(callback); }, function initMimeTypes(callback) { - return require('./mime_util.js').startup(callback); + return require('./mime_util.js').startup(callback); }, function initStatLog(callback) { return require('./stat_log.js').init(callback); diff --git a/core/bbs_link.js b/core/bbs_link.js index be341115..15416c2e 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -23,10 +23,10 @@ const packageJson = require('../package.json'); authCode: XXXXX schemeCode: XXXX door: lord - + // default hoss: games.bbslink.net host: games.bbslink.net - + // defualt port: 23 port: 23 } @@ -49,7 +49,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { this.config = options.menuConfig.config; this.config.host = this.config.host || 'games.bbslink.net'; this.config.port = this.config.port || 23; - } + } initSequence() { let token; @@ -141,7 +141,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { self.client.once('end', function clientEnd() { self.client.log.info('Connection ended. Terminating BBSLink connection'); clientTerminated = true; - bridgeConnection.end(); + bridgeConnection.end(); }); }); @@ -170,7 +170,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { ], function complete(err) { if(err) { - self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); + self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); } if(!clientTerminated) { diff --git a/core/bbs_list.js b/core/bbs_list.js index 33a7ff59..5c81f478 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -4,7 +4,7 @@ // ENiGMA½ const MenuModule = require('./menu_module.js').MenuModule; -const { +const { getModDatabasePath, getTransactionDatabase } = require('./database.js'); @@ -39,7 +39,7 @@ const MciViewIds = { SelectedBBSLoc : 6, SelectedBBSSoftware : 7, SelectedBBSNotes : 8, - SelectedBBSSubmitter : 9, + SelectedBBSSubmitter : 9, }, add : { BBSName : 1, @@ -49,7 +49,7 @@ const MciViewIds = { Location : 5, Software : 6, Notes : 7, - Error : 8, + Error : 8, } }; @@ -190,12 +190,12 @@ exports.getModule = class BBSListModule extends MenuModule { drawSelectedEntry(entry) { if(!entry) { - Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { this.setViewText('view', MciViewIds.view[mciName], ''); }); } else { const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)'; - + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; if(MciViewIds.view[mciName]) { @@ -270,7 +270,7 @@ exports.getModule = class BBSListModule extends MenuModule { (err, row) => { if (!err) { self.entries.push({ - id : row.id, + id : row.id, bbsName : row.bbs_name, sysOp : row.sysop, telnet : row.telnet, @@ -306,9 +306,9 @@ exports.getModule = class BBSListModule extends MenuModule { entriesView.on('index update', idx => { const entry = self.entries[idx]; - + self.drawSelectedEntry(entry); - + if(!entry) { self.selectedBBS = -1; } else { diff --git a/core/button_view.js b/core/button_view.js index 570adc09..d5b858c7 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -22,7 +22,7 @@ ButtonView.prototype.onKeyPress = function(ch, key) { if(this.isKeyMapped('accept', key.name) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); - delete this.submitData; + delete this.submitData; } else { ButtonView.super_.prototype.onKeyPress.call(this, ch, key); } diff --git a/core/client.js b/core/client.js index 424748a6..58700c8f 100644 --- a/core/client.js +++ b/core/client.js @@ -52,8 +52,8 @@ exports.Client = Client; // Resources & Standards: // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // -const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9\;]+)(R)/; -const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[\=\?]([0-9a-zA-Z\;]+)(c)/; +const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; +const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ @@ -64,19 +64,19 @@ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_ESC_CODE_ANYWHERE = new RegExp( [ - RE_FUNCTION_KEYCODE_ANYWHERE.source, - RE_META_KEYCODE_ANYWHERE.source, + RE_FUNCTION_KEYCODE_ANYWHERE.source, + RE_META_KEYCODE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source, RE_DEV_ATTR_RESPONSE_ANYWHERE.source, /\u001b./.source ].join('|')); -function Client(input, output) { +function Client(/*input, output*/) { stream.call(this); const self = this; - + this.user = new User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.lastKeyPressMs = Date.now(); @@ -125,9 +125,9 @@ function Client(input, output) { if(!termClient) { if(_.startsWith(deviceAttr, '67;84;101;114;109')) { - // + // // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt - // + // // Known clients: // * SyncTERM // @@ -139,11 +139,11 @@ function Client(input, output) { }; this.isMouseInput = function(data) { - return /\x1b\[M/.test(data) || - /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || + return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex + /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex /\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || - /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || + /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[(O|I)/.test(data); }; @@ -163,7 +163,7 @@ function Client(input, output) { 'OE' : { name : 'clear' }, 'OF' : { name : 'end' }, 'OH' : { name : 'home' }, - + // xterm/rxvt '[11~' : { name : 'f1' }, '[12~' : { name : 'f2' }, @@ -290,7 +290,7 @@ function Client(input, output) { if(self.cprOffset) { cprArgs[0] = cprArgs[0] + self.cprOffset; cprArgs[1] = cprArgs[1] + self.cprOffset; - } + } self.emit('cursor position report', cprArgs); } } @@ -299,7 +299,7 @@ function Client(input, output) { var termClient = self.getTermClient(parts[1]); if(termClient) { self.term.termClient = termClient; - } + } } else if('\r' === s) { key.name = 'return'; } else if('\n' === s) { @@ -347,10 +347,10 @@ function Client(input, output) { key.meta = true; key.shift = /^[A-Z]$/.test(parts[1]); } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { - var code = + var code = (parts[1] || '') + (parts[2] || '') + (parts[4] || '') + (parts[9] || ''); - + var modifier = (parts[3] || parts[8] || 1) - 1; key.ctrl = !!(modifier & 4); @@ -375,7 +375,7 @@ function Client(input, output) { // // Adjust name for CTRL/Shift/Meta modifiers // - key.name = + key.name = (key.ctrl ? 'ctrl + ' : '') + (key.meta ? 'meta + ' : '') + (key.shift ? 'shift + ' : '') + @@ -446,7 +446,7 @@ Client.prototype.end = function () { } clearInterval(this.idleCheck); - + try { // // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH @@ -482,7 +482,7 @@ Client.prototype.isLocal = function() { /////////////////////////////////////////////////////////////////////////////// // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something -Client.prototype.defaultHandlerMissingMod = function(err) { +Client.prototype.defaultHandlerMissingMod = function() { var self = this; function handler(err) { @@ -493,12 +493,12 @@ Client.prototype.defaultHandlerMissingMod = function(err) { self.term.write('This has been logged for your SysOp to review.\n'); self.term.write('\nGoodbye!\n'); - + //self.term.write(err); //if(miscUtil.isDevelopment() && err.stack) { // self.term.write('\n' + err.stack + '\n'); - //} + //} self.end(); } @@ -516,8 +516,8 @@ Client.prototype.terminalSupports = function(query) { case 'vtx_hyperlink' : return 'vtx' === termClient; - - default : + + default : return false; } }; diff --git a/core/client_connections.js b/core/client_connections.js index 7e74e29d..d81d0922 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -95,7 +95,7 @@ function removeClient(client) { clientId : client.session.id }, 'Client disconnected' - ); + ); Events.emit('codes.l33t.enigma.system.disconnected', { client : client, connectionCount : clientConnections.length } ); } diff --git a/core/client_term.js b/core/client_term.js index b313841e..b944988d 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -15,8 +15,6 @@ exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { this.output = output; - var self = this; - var outputEncoding = 'cp437'; assert(iconv.encodingExists(outputEncoding)); @@ -56,7 +54,7 @@ function ClientTerminal(output) { }, set : function(ttype) { termType = ttype.toLowerCase(); - + if(this.isANSI()) { this.outputEncoding = 'cp437'; } else { @@ -137,7 +135,7 @@ ClientTerminal.prototype.isANSI = function() { // // syncterm: // * SyncTERM - // + // // xterm: // * PuTTY // @@ -168,7 +166,7 @@ ClientTerminal.prototype.rawWrite = function(s, cb) { if(cb) { return cb(err); } - + if(err) { Log.warn( { error : err.message }, 'Failed writing to socket'); } @@ -178,18 +176,18 @@ ClientTerminal.prototype.rawWrite = function(s, cb) { ClientTerminal.prototype.pipeWrite = function(s, spec, cb) { spec = spec || 'renegade'; - + var conv = { enigma : enigmaToAnsi, renegade : renegadeToAnsi, }[spec] || renegadeToAnsi; - + this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| }; ClientTerminal.prototype.encode = function(s, convertLineFeeds) { convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; - + if(convertLineFeeds && _.isString(s)) { s = s.replace(/\n/g, '\r\n'); } diff --git a/core/color_codes.js b/core/color_codes.js index 2e368aa3..db9f4fe5 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -68,14 +68,14 @@ function enigmaToAnsi(s, client) { attr = ansi.sgr(['normal', val - 8, 'bold']); } - result += s.substr(lastIndex, m.index - lastIndex) + attr; + result += s.substr(lastIndex, m.index - lastIndex) + attr; } lastIndex = re.lastIndex; } result = (0 === result.length ? s : result + s.substr(lastIndex)); - + return result; } @@ -145,7 +145,7 @@ function renegadeToAnsi(s, client) { } // convert to number - val = parseInt(val, 10); + val = parseInt(val, 10); if(isNaN(val)) { val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal } @@ -160,7 +160,7 @@ function renegadeToAnsi(s, client) { lastIndex = re.lastIndex; } - return (0 === result.length ? s : result + s.substr(lastIndex)); + return (0 === result.length ? s : result + s.substr(lastIndex)); } // @@ -180,7 +180,7 @@ function renegadeToAnsi(s, client) { // * http://wiki.synchro.net/custom:colors // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(\@X([0-9A-F]{2}))|(\@([0-9A-F]{2})\@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex let m; let result = ''; diff --git a/core/combatnet.js b/core/combatnet.js index 6cde9c7b..217e6d17 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -25,10 +25,10 @@ exports.getModule = class CombatNetModule extends MenuModule { this.config.host = this.config.host || 'bbs.combatnet.us'; this.config.rloginPort = this.config.rloginPort || 4513; } - + initSequence() { const self = this; - + async.series( [ function validateConfig(callback) { @@ -45,59 +45,59 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.term.write('Connecting to CombatNet, please wait...\n'); const restorePipeToNormal = function() { - self.client.term.output.removeListener('data', sendToRloginBuffer); + self.client.term.output.removeListener('data', sendToRloginBuffer); }; - const rlogin = new RLogin( - { 'clientUsername' : self.config.password, - 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, - 'host' : self.config.host, - 'port' : self.config.rloginPort, - 'terminalType' : self.client.term.termClient, - 'terminalSpeed' : 57600 - } - ); + const rlogin = new RLogin( + { 'clientUsername' : self.config.password, + 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, + 'host' : self.config.host, + 'port' : self.config.rloginPort, + 'terminalType' : self.client.term.termClient, + 'terminalSpeed' : 57600 + } + ); - // If there was an error ... - rlogin.on('error', err => { - self.client.log.info(`CombatNet rlogin client error: ${err.message}`); - restorePipeToNormal(); - callback(err); - }); + // If there was an error ... + rlogin.on('error', err => { + self.client.log.info(`CombatNet rlogin client error: ${err.message}`); + restorePipeToNormal(); + return callback(err); + }); - // If we've been disconnected ... - rlogin.on('disconnect', () => { - self.client.log.info(`Disconnected from CombatNet`); - restorePipeToNormal(); - callback(null); - }); + // If we've been disconnected ... + rlogin.on('disconnect', () => { + self.client.log.info('Disconnected from CombatNet'); + restorePipeToNormal(); + return callback(null); + }); - function sendToRloginBuffer(buffer) { - rlogin.send(buffer); - }; + function sendToRloginBuffer(buffer) { + rlogin.send(buffer); + } - rlogin.on("connect", - /* The 'connect' event handler will be supplied with one argument, + rlogin.on('connect', + /* The 'connect' event handler will be supplied with one argument, a boolean indicating whether or not the connection was established. */ - function(state) { - if(state) { - self.client.log.info('Connected to CombatNet'); - self.client.term.output.on('data', sendToRloginBuffer); + function(state) { + if(state) { + self.client.log.info('Connected to CombatNet'); + self.client.term.output.on('data', sendToRloginBuffer); - } else { - return callback(new Error('Failed to establish establish CombatNet connection')); - } - } - ); + } else { + return callback(new Error('Failed to establish establish CombatNet connection')); + } + } + ); - // If data (a Buffer) has been received from the server ... - rlogin.on("data", (data) => { - self.client.term.rawWrite(data); - }); + // If data (a Buffer) has been received from the server ... + rlogin.on('data', (data) => { + self.client.term.rawWrite(data); + }); - // connect... - rlogin.connect(); + // connect... + rlogin.connect(); // note: no explicit callback() until we're finished! } @@ -106,10 +106,10 @@ exports.getModule = class CombatNetModule extends MenuModule { if(err) { self.client.log.warn( { error : err.message }, 'CombatNet error'); } - + // if the client is still here, go to previous self.prevMenu(); } ); - } + } }; diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 5dabfb73..6b71061b 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -10,7 +10,7 @@ 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; diff --git a/core/config.js b/core/config.js index 62ce6032..79824db5 100644 --- a/core/config.js +++ b/core/config.js @@ -653,12 +653,12 @@ function getDefaultConfig() { // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. desc : [ - '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' + '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape ], // common README filename - https://en.wikipedia.org/wiki/README descLong : [ - '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' + '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape ], }, diff --git a/core/config_cache.js b/core/config_cache.js index 8b57e125..875e1b2e 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -58,7 +58,7 @@ util.inherits(ConfigCache, events.EventEmitter); ConfigCache.prototype.getConfigWithOptions = function(options, cb) { assert(_.isString(options.filePath)); -// var self = this; + // var self = this; var isCached = (options.filePath in this.cache); if(options.forceReCache || !isCached) { diff --git a/core/connect.js b/core/connect.js index b94fa586..aae1b8c6 100644 --- a/core/connect.js +++ b/core/connect.js @@ -98,7 +98,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) { source : 'ANSI CPR' }, 'Window size updated' - ); + ); return done(null); }; diff --git a/core/crc.js b/core/crc.js index 886dad1d..e4bd8551 100644 --- a/core/crc.js +++ b/core/crc.js @@ -10,7 +10,7 @@ exports.CRC32 = class CRC32 { } update(input) { - input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); + input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); return input.length > 10240 ? this.update_8(input) : this.update_4(input); } @@ -47,7 +47,7 @@ exports.CRC32 = class CRC32 { this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; } } - + finalize() { return (this.crc ^ (-1)) >>> 0; } diff --git a/core/database.js b/core/database.js index 14b3bf95..10331fc0 100644 --- a/core/database.js +++ b/core/database.js @@ -36,12 +36,12 @@ function getModDatabasePath(moduleInfo, suffix) { // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) // We expect that moduleInfo defines packageName which will be the base of the modules // filename. An optional suffix may be supplied as well. - // - const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; + // + const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; assert(_.isObject(moduleInfo)); assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); - + let full = moduleInfo.packageName; if(suffix) { full += `.${suffix}`; @@ -198,7 +198,7 @@ const DB_INIT_TABLE = { DELETE FROM message_fts WHERE docid=old.rowid; END;` ); - + dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN DELETE FROM message_fts WHERE docid=old.rowid; @@ -256,14 +256,14 @@ const DB_INIT_TABLE = { UNIQUE(user_id, area_tag) );` ); - + dbs.message.run( `CREATE TABLE IF NOT EXISTS message_area_last_scan ( scan_toss VARCHAR NOT NULL, area_tag VARCHAR NOT NULL, message_id INTEGER NOT NULL, UNIQUE(scan_toss, area_tag) - );` + );` ); return cb(null); diff --git a/core/door.js b/core/door.js index 5670db1e..525a7a02 100644 --- a/core/door.js +++ b/core/door.js @@ -20,7 +20,7 @@ function Door(client, exeInfo) { this.exeInfo = exeInfo; this.exeInfo.encoding = this.exeInfo.encoding || 'cp437'; this.exeInfo.encoding = this.exeInfo.encoding.toLowerCase(); - let restored = false; + let restored = false; // // Members of exeInfo: @@ -52,7 +52,7 @@ function Door(client, exeInfo) { }; this.prepareSocketIoServer = function(cb) { - if('socket' === self.exeInfo.io) { + if('socket' === self.exeInfo.io) { const sockServer = createServer(conn => { sockServer.getConnections( (err, count) => { @@ -60,11 +60,11 @@ function Door(client, exeInfo) { // We expect only one connection from our DOOR/emulator/etc. if(!err && count <= 1) { self.client.term.output.pipe(conn); - + conn.on('data', self.doorDataHandler); conn.once('end', () => { - return self.restoreIo(conn); + return self.restoreIo(conn); }); conn.once('error', err => { @@ -117,7 +117,7 @@ Door.prototype.run = function() { rows : self.client.term.termHeight, // :TODO: cwd env : self.exeInfo.env, - }); + }); if('stdio' === self.exeInfo.io) { self.client.log.debug('Using stdio for door I/O'); diff --git a/core/door_party.js b/core/door_party.js index 762f626b..a64f92c8 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -24,13 +24,13 @@ exports.getModule = class DoorPartyModule extends MenuModule { 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.config.rloginPort = this.config.rloginPort || 513; } - + initSequence() { let clientTerminated; const self = this; - + async.series( [ function validateConfig(callback) { @@ -48,26 +48,26 @@ exports.getModule = class DoorPartyModule extends MenuModule { function establishSecureConnection(callback) { self.client.term.write(resetScreen()); self.client.term.write('Connecting to DoorParty, please wait...\n'); - + const sshClient = new SSHClient(); - + let pipeRestored = false; let pipedStream; const restorePipe = function() { if(pipedStream && !pipeRestored && !clientTerminated) { - self.client.term.output.unpipe(pipedStream); + self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); - } - }; - + } + }; + sshClient.on('ready', () => { // track client termination so we can clean up early self.client.once('end', () => { self.client.log.info('Connection ended. Terminating DoorParty connection'); clientTerminated = true; - sshClient.end(); + sshClient.end(); }); - + // establish tunnel for rlogin sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { if(err) { @@ -79,17 +79,17 @@ exports.getModule = class DoorPartyModule extends MenuModule { // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. // [XA]nuskooler // - const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; + const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); - + pipedStream = stream; // :TODO: this is hacky... self.client.term.output.pipe(stream); - + stream.on('data', d => { // :TODO: we should just pipe this... self.client.term.rawWrite(d); }); - + stream.on('close', () => { restorePipe(); sshClient.end(); @@ -100,32 +100,32 @@ exports.getModule = class DoorPartyModule extends MenuModule { sshClient.on('error', err => { self.client.log.info(`DoorParty SSH client error: ${err.message}`); }); - + sshClient.on('close', () => { restorePipe(); callback(null); }); - + sshClient.connect( { host : self.config.host, port : self.config.sshPort, username : self.config.username, password : self.config.password, }); - + // note: no explicit callback() until we're finished! - } + } ], err => { if(err) { self.client.log.warn( { error : err.message }, 'DoorParty error'); } - + // if the client is stil here, go to previous if(!clientTerminated) { self.prevMenu(); } } ); - } + } }; diff --git a/core/download_queue.js b/core/download_queue.js index 6bfbd47f..0f45b04d 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -59,14 +59,14 @@ module.exports = class DownloadQueue { } toProperty() { return JSON.stringify(this.client.user.downloadQueue); } - + loadFromProperty(prop) { try { this.client.user.downloadQueue = JSON.parse(prop); } catch(e) { this.client.user.downloadQueue = []; - this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); + this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); } - } + } }; diff --git a/core/edit_text_view.js b/core/edit_text_view.js index 8e55ae53..0db02638 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -16,7 +16,7 @@ function EditTextView(options) { options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.resizable = false; - + TextView.call(this, options); this.cursorPos = { row : 0, col : 0 }; @@ -44,7 +44,7 @@ EditTextView.prototype.onKeyPress = function(ch, key) { } } } - + return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); } else if(this.isKeyMapped('clearLine', key.name)) { this.text = ''; diff --git a/core/enig_error.js b/core/enig_error.js index 49627b9c..b0dd2335 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -14,7 +14,7 @@ class EnigError extends Error { if(typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); } else { - this.stack = (new Error(message)).stack; + this.stack = (new Error(message)).stack; } } } diff --git a/core/enigma_assert.js b/core/enigma_assert.js index 2001825d..9217ea49 100644 --- a/core/enigma_assert.js +++ b/core/enigma_assert.js @@ -3,14 +3,14 @@ // ENiGMA½ const Config = require('./config.js').config; -const Log = require('./logger.js').log; +const Log = require('./logger.js').log; // deps const assert = require('assert'); module.exports = function(condition, message) { if(Config.debug.assertsEnabled) { - assert.apply(this, arguments); + assert.apply(this, arguments); } else if(!(condition)) { const stack = new Error().stack; Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); diff --git a/core/erc_client.js b/core/erc_client.js index 4fb549f6..ccc70199 100644 --- a/core/erc_client.js +++ b/core/erc_client.js @@ -37,12 +37,12 @@ var MciViewIds = { function ErcClientModule(options) { MenuModule.prototype.ctorShim.call(this, options); - const self = this; + const self = this; this.config = options.menuConfig.config; this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; - this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; - + this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; + this.finishedLoading = function() { async.waterfall( [ @@ -63,12 +63,12 @@ function ErcClientModule(options) { }; const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); - + chatMessageView.setText('Connecting to server...'); chatMessageView.redraw(); - + self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - + // :TODO: Track actual client->enig connection for optional prevMenu @ final CB self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); @@ -98,12 +98,12 @@ function ErcClientModule(options) { } chatMessageView.addText(text); - + if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? chatMessageView.deleteLine(0); chatMessageView.scrollDown(); } - + chatMessageView.redraw(); self.viewControllers.menu.switchFocus(MciViewIds.InputArea); } diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 8b3d3239..0366d5ba 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -24,8 +24,8 @@ exports.moduleInfo = { author : 'NuSkooler', }; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:)([^\0]+)?$/; -const ACTION_REGEXP = /\@(method|execute)\:([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; +const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; class ScheduledEvent { constructor(events, name) { @@ -34,32 +34,32 @@ class ScheduledEvent { this.action = this.parseActionSpec(events[name].action); if(this.action) { this.action.args = events[name].args || []; - } + } } - + get isValid() { if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { return false; } - + if('method' === this.action.type && !this.action.location) { return false; } - - return true; + + return true; } - + parseScheduleString(schedStr) { if(!schedStr) { return false; } - + let schedule = {}; - + const m = SCHEDULE_REGEXP.exec(schedStr); if(m) { schedStr = schedStr.substr(0, m.index).trim(); - + if('@watch:' === m[1]) { schedule.watchFile = m[2]; } @@ -69,15 +69,15 @@ class ScheduledEvent { const sched = later.parse.text(schedStr); if(-1 === sched.error) { schedule.sched = sched; - } + } } - + // return undefined if we couldn't parse out anything useful if(!_.isEmpty(schedule)) { return schedule; } } - + parseActionSpec(actionSpec) { if(actionSpec) { if('@' === actionSpec[0]) { @@ -86,7 +86,7 @@ class ScheduledEvent { if(m[2].indexOf(':') > -1) { const parts = m[2].split(':'); return { - type : m[1], + type : m[1], location : parts[0], what : parts[1], }; @@ -98,12 +98,12 @@ class ScheduledEvent { } } } else { - return { + return { type : 'execute', what : actionSpec, }; - } - } + } + } } executeAction(reason, cb) { @@ -119,14 +119,14 @@ class ScheduledEvent { { error : err.toString(), eventName : this.name, action : this.action }, 'Error performing scheduled event action'); } - + return cb(err); }); } catch(e) { Log.warn( { error : e.toString(), eventName : this.name, action : this.action }, 'Failed to perform scheduled event action'); - + return cb(e); } } else if('execute' === this.action.type) { @@ -135,18 +135,18 @@ class ScheduledEvent { name : this.name, cols : 80, rows : 24, - env : process.env, + env : process.env, }; const proc = pty.spawn(this.action.what, this.action.args, opts); - proc.once('exit', exitCode => { + proc.once('exit', exitCode => { if(exitCode) { Log.warn( { eventName : this.name, action : this.action, exitCode : exitCode }, 'Bad exit code while performing scheduled event action'); } - return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); + return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); }); } } @@ -154,58 +154,58 @@ class ScheduledEvent { function EventSchedulerModule(options) { PluginModule.call(this, options); - + if(_.has(Config, 'eventScheduler')) { this.moduleConfig = Config.eventScheduler; } - + const self = this; this.runningActions = new Set(); - + this.performAction = function(schedEvent, reason) { if(self.runningActions.has(schedEvent.name)) { return; // already running - } - + } + self.runningActions.add(schedEvent.name); schedEvent.executeAction(reason, () => { self.runningActions.delete(schedEvent.name); - }); + }); }; } // convienence static method for direct load + start EventSchedulerModule.loadAndStart = function(cb) { const loadModuleEx = require('./module_util.js').loadModuleEx; - + const loadOpts = { name : path.basename(__filename, '.js'), path : __dirname, }; - + loadModuleEx(loadOpts, (err, mod) => { if(err) { return cb(err); } - + const modInst = new mod.getModule(); modInst.startup( err => { return cb(err, modInst); - }); + }); }); }; EventSchedulerModule.prototype.startup = function(cb) { - + this.eventTimers = []; const self = this; - + if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { const events = Object.keys(this.moduleConfig.events).map( name => { return new ScheduledEvent(this.moduleConfig.events, name); }); - + events.forEach( schedEvent => { if(!schedEvent.isValid) { Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); @@ -213,7 +213,7 @@ EventSchedulerModule.prototype.startup = function(cb) { } Log.debug( - { + { eventName : schedEvent.name, schedule : this.moduleConfig.events[schedEvent.name].schedule, action : schedEvent.action, @@ -222,9 +222,9 @@ EventSchedulerModule.prototype.startup = function(cb) { 'Scheduled event loaded' ); - if(schedEvent.schedule.sched) { + if(schedEvent.schedule.sched) { this.eventTimers.push(later.setInterval( () => { - self.performAction(schedEvent, 'Schedule'); + self.performAction(schedEvent, 'Schedule'); }, schedEvent.schedule.sched)); } @@ -255,7 +255,7 @@ EventSchedulerModule.prototype.startup = function(cb) { } }); } - + cb(null); }; @@ -263,6 +263,6 @@ EventSchedulerModule.prototype.shutdown = function(cb) { if(this.eventTimers) { this.eventTimers.forEach( et => et.clear() ); } - + cb(null); }; diff --git a/core/exodus.js b/core/exodus.js index e77183ee..8b3c7548 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -23,7 +23,7 @@ const SSHClient = require('ssh2').Client; /* Configuration block: - + someDoor: { module: exodus config: { @@ -61,7 +61,7 @@ exports.getModule = class ExodusModule extends MenuModule { this.config = options.menuConfig.config || {}; this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; this.config.ticketPort = this.config.ticketPort || 1984, - this.config.ticketPath = this.config.ticketPath || '/exodus'; + this.config.ticketPath = this.config.ticketPath || '/exodus'; this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); this.config.sshHost = this.config.sshHost || this.config.ticketHost; this.config.sshPort = this.config.sshPort || 22; diff --git a/core/file_area_filter_edit.js b/core/file_area_filter_edit.js index 4a53096c..cc4c22c7 100644 --- a/core/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -65,7 +65,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { prevFilter : (formData, extraArgs, cb) => { this.currentFilterIndex -= 1; if(this.currentFilterIndex < 0) { - this.currentFilterIndex = this.filtersArray.length - 1; + this.currentFilterIndex = this.filtersArray.length - 1; } this.loadDataForFilter(this.currentFilterIndex); return cb(null); @@ -116,21 +116,21 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { if(newActive) { filters.setActive(newActive.uuid); } else { - // nothing to set active to + // nothing to set active to this.client.user.removeProperty('file_base_filter_active_uuid'); } } // update UI this.updateActiveLabel(); - + if(this.filtersArray.length > 0) { this.loadDataForFilter(this.currentFilterIndex); } else { this.clearForm(); } return cb(null); - }); + }); }, viewValidationListener : (err, cb) => { @@ -161,7 +161,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } } } - + mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { @@ -178,7 +178,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { }, 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 ) ); @@ -194,7 +194,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { return cb(err); } ); - }); + }); } getCurrentFilter() { @@ -212,7 +212,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { const activeFilter = FileBaseFilters.getActiveFilter(this.client); if(activeFilter) { const activeFormat = this.menuConfig.config.activeFormat || '{name}'; - this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); + this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); } } @@ -256,7 +256,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setAreaIndexFromCurrentFilter() { let index; - const filter = this.getCurrentFilter(); + 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; @@ -295,7 +295,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setFilterValuesFromFormData(filter, formData) { filter.name = formData.value.name; filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); - filter.terms = formData.value.searchTerms; + filter.terms = formData.value.searchTerms; filter.tags = formData.value.tags; filter.order = this.getOrderBy(formData.value.orderByIndex); filter.sort = this.getSortBy(formData.value.sortByIndex); @@ -304,7 +304,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { 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); @@ -316,11 +316,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { // 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); } @@ -334,6 +334,6 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { this.setAreaIndexFromCurrentFilter(); this.setSortByFromCurrentFilter(); this.setOrderByFromCurrentFilter(); - } + } } }; diff --git a/core/file_area_list.js b/core/file_area_list.js index 3bcfd7c2..87e794cb 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -96,7 +96,7 @@ exports.getModule = class FileAreaList extends MenuModule { } this.menuMethods = { - nextFile : (formData, extraArgs, cb) => { + nextFile : (formData, extraArgs, cb) => { if(this.fileListPosition + 1 < this.fileList.length) { this.fileListPosition += 1; @@ -131,7 +131,7 @@ exports.getModule = class FileAreaList extends MenuModule { toggleQueue : (formData, extraArgs, cb) => { this.dlQueue.toggle(this.currentFileEntry); this.updateQueueIndicator(); - return cb(null); + return cb(null); }, showWebDownloadLink : (formData, extraArgs, cb) => { return this.fetchAndDisplayWebDownloadLink(cb); @@ -217,7 +217,7 @@ exports.getModule = class FileAreaList extends MenuModule { const hashTagsSep = config.hashTagsSep || ', '; const isQueuedIndicator = config.isQueuedIndicator || 'Y'; const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; - + const entryInfo = currEntry.entryInfo = { fileId : currEntry.fileId, areaTag : currEntry.areaTag, @@ -232,7 +232,7 @@ exports.getModule = class FileAreaList extends MenuModule { hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, webDlLink : '', // :TODO: fetch web any existing web d/l link - webDlExpire : '', // :TODO: fetch web d/l link expire time + webDlExpire : '', // :TODO: fetch web d/l link expire time }; // @@ -257,7 +257,7 @@ exports.getModule = class FileAreaList extends MenuModule { // create a rating string, e.g. "**---" const userRatingTicked = config.userRatingTicked || '*'; - const userRatingUnticked = config.userRatingUnticked || ''; + const userRatingUnticked = config.userRatingUnticked || ''; entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); if(entryInfo.userRating < 5) { @@ -270,7 +270,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(ErrNotEnabled === err.reasonCode) { entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; } else { - entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; + entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; } } else { const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; @@ -339,10 +339,10 @@ exports.getModule = class FileAreaList extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } - + self.viewControllers[name].setFocus(true); return callback(null); - + }, ], err => { @@ -357,7 +357,7 @@ exports.getModule = class FileAreaList extends MenuModule { async.series( [ function fetchEntryData(callback) { - if(self.fileList) { + if(self.fileList) { return callback(null); } return self.loadFileIds(false, callback); // false=do not force @@ -371,14 +371,14 @@ exports.getModule = class FileAreaList extends MenuModule { function prepArtAndViewController(callback) { return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); }, - function loadCurrentFileInfo(callback) { + function loadCurrentFileInfo(callback) { self.currentFileEntry = new FileEntry(); self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { if(err) { return callback(err); } - + return self.populateCurrentEntryInfo(callback); }); }, @@ -422,7 +422,7 @@ exports.getModule = class FileAreaList extends MenuModule { return callback(null); } ], - err => { + err => { if(cb) { return cb(err); } @@ -448,7 +448,7 @@ exports.getModule = class FileAreaList extends MenuModule { function listenNavChanges(callback) { const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu); navMenu.setFocusItemIndex(0); - + navMenu.on('index update', index => { const sectionName = { 0 : 'general', @@ -481,7 +481,7 @@ exports.getModule = class FileAreaList extends MenuModule { } ); } - + fetchAndDisplayWebDownloadLink(cb) { const self = this; @@ -492,11 +492,11 @@ exports.getModule = class FileAreaList extends MenuModule { if(self.currentFileEntry.webDlExpireTime < moment()) { return callback(null); } - + const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempDownload( - self.client, + self.client, self.currentFileEntry, { expireTime : expireTime }, (err, url) => { @@ -517,8 +517,8 @@ exports.getModule = class FileAreaList extends MenuModule { }, function updateActiveViews(callback) { self.updateCustomViewTextsWithFilter( - 'browse', - MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, + 'browse', + MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, { filter : [ '{webDlLink}', '{webDlExpire}' ] } ); return callback(null); @@ -527,7 +527,7 @@ exports.getModule = class FileAreaList extends MenuModule { err => { return cb(err); } - ); + ); } updateQueueIndicator() { @@ -535,8 +535,8 @@ exports.getModule = class FileAreaList extends MenuModule { const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; this.currentFileEntry.entryInfo.isQueued = stringFormat( - this.dlQueue.isQueued(this.currentFileEntry) ? - isQueuedIndicator : + this.dlQueue.isQueued(this.currentFileEntry) ? + isQueuedIndicator : isNotQueuedIndicator ); @@ -558,7 +558,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(!areaInfo) { return cb(Errors.Invalid('Invalid area tag')); } - + const filePath = this.currentFileEntry.filePath; const archiveUtil = ArchiveUtil.getInstance(); @@ -574,7 +574,7 @@ exports.getModule = class FileAreaList extends MenuModule { populateFileListing() { const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); - + if(this.currentFileEntry.entryInfo.archiveType) { this.cacheArchiveEntries( (err, cacheStatus) => { if(err) { @@ -586,7 +586,7 @@ exports.getModule = class FileAreaList extends MenuModule { if('re-cached' === cacheStatus) { const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; - + fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); @@ -594,7 +594,7 @@ exports.getModule = class FileAreaList extends MenuModule { } }); } else { - fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); + fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); } } @@ -608,7 +608,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(self.lastDetailsViewController) { self.lastDetailsViewController.detachClientEvents(); } - return callback(null); + return callback(null); }, function prepArtAndViewController(callback) { @@ -616,7 +616,7 @@ exports.getModule = class FileAreaList extends MenuModule { self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1)); } - gotoTopPos(); + gotoTopPos(); if(clearArea) { self.client.term.rawWrite(ansi.reset()); diff --git a/core/file_area_web.js b/core/file_area_web.js index b12f4f7d..b8a630fc 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -59,7 +59,7 @@ class FileAreaWebAccess { return callback(null); // not enabled, but no error } } - ], + ], err => { return cb(err); } @@ -193,7 +193,7 @@ class FileAreaWebAccess { getExistingTempDownloadServeItem(client, fileEntry, cb) { if(!this.isEnabled()) { return cb(notEnabledError()); - } + } const hashId = this.getSingleFileHashId(client, fileEntry); this.loadServedHashId(hashId, (err, servedItem) => { @@ -201,10 +201,10 @@ class FileAreaWebAccess { return cb(err); } - servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); + servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); return cb(null, servedItem); - }); + }); } _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { @@ -219,7 +219,7 @@ class FileAreaWebAccess { } this.scheduleExpire(hashId, expireTime); - + return cb(null); } ); @@ -476,7 +476,7 @@ class FileAreaWebAccess { StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); StatLog.incrementSystemStat('dl_total_count', 1); StatLog.incrementSystemStat('dl_total_bytes', dlBytes); - + return callback(null); } ], diff --git a/core/file_base_area.js b/core/file_base_area.js index f3845cc1..6c7318d6 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -61,8 +61,8 @@ 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 _.omitBy(allAreas, areaInfo => { + + return _.omitBy(allAreas, areaInfo => { if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { return true; } @@ -102,7 +102,7 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { defaultArea = _.findKey(Config.fileBase.areas, (area, areaTag) => { return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); }); - + return defaultArea; } @@ -110,7 +110,7 @@ function getFileAreaByTag(areaTag) { const areaInfo = Config.fileBase.areas[areaTag]; if(areaInfo) { areaInfo.areaTag = areaTag; // convienence! - areaInfo.storage = getAreaStorageLocations(areaInfo); + areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } } @@ -165,13 +165,13 @@ function getAreaDefaultStorageDirectory(areaInfo) { } function getAreaStorageLocations(areaInfo) { - - const storageTags = Array.isArray(areaInfo.storageTags) ? - areaInfo.storageTags : + + const storageTags = Array.isArray(areaInfo.storageTags) ? + areaInfo.storageTags : [ areaInfo.storageTags || '' ]; const avail = Config.fileBase.storageTags; - + return _.compact(storageTags.map(storageTag => { if(avail[storageTag]) { return { @@ -230,7 +230,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); function getMatch(input) { - if(input) { + if(input) { let m; for(let i = 0; i < patterns.length; ++i) { m = patterns[i].exec(input); @@ -249,7 +249,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { // const maxYear = moment().add(2, 'year').year(); const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); - + if(match && match[1]) { let year; if(2 === match[1].length) { @@ -316,7 +316,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { if(err) { return callback(err); - } + } const descFiles = { desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, @@ -327,7 +327,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { }); }); }, - function readDescFiles(descFiles, callback) { + function readDescFiles(descFiles, callback) { async.each(Object.keys(descFiles), (descType, next) => { const path = descFiles[descType]; if(!path) { @@ -341,7 +341,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { // skip entries that are too large const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; - + if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) { logDebug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); return next(null); @@ -353,7 +353,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437. + // 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'); @@ -389,10 +389,10 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries } const archiveUtil = ArchiveUtil.getInstance(); - + // ensure we only extract one - there should only be one anyway -- we also just need the fileName const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName); - + archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { if(err) { return callback(err); @@ -540,7 +540,7 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { }); }, () => { return cb(null); - }); + }); } function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) { @@ -586,10 +586,6 @@ function addNewFileEntry(fileEntry, filePath, cb) { ); } -function updateFileEntry(fileEntry, filePath, cb) { - -} - const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ]; function scanFile(filePath, options, iterator, cb) { @@ -664,7 +660,7 @@ function scanFile(filePath, options, iterator, cb) { return callIter(callback); }); }, - function processPhysicalFileGeneric(callback) { + function processPhysicalFileGeneric(callback) { stepInfo.bytesProcessed = 0; const hashes = {}; @@ -690,7 +686,7 @@ function scanFile(filePath, options, iterator, cb) { stream.on('data', data => { stream.pause(); // until iterator compeltes - stepInfo.bytesProcessed += data.length; + stepInfo.bytesProcessed += data.length; stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); // @@ -710,13 +706,13 @@ function scanFile(filePath, options, iterator, cb) { updateHashes(data); }); - } + } }); stream.on('end', () => { fileEntry.meta.byte_size = stepInfo.bytesProcessed; - async.each(hashesToCalc, (hashName, nextHash) => { + async.each(hashesToCalc, (hashName, nextHash) => { if('sha256' === hashName) { stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); } else if('sha1' === hashName || 'md5' === hashName) { @@ -747,7 +743,9 @@ function scanFile(filePath, options, iterator, cb) { populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { if(err) { populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - // :TODO: log err + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } return callback(null); // ignore err }); } else { @@ -756,7 +754,9 @@ function scanFile(filePath, options, iterator, cb) { }); } else { populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - // :TODO: log err + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } return callback(null); // ignore err }); } @@ -773,7 +773,7 @@ function scanFile(filePath, options, iterator, cb) { return callback(null, dupeEntries); }); } - ], + ], (err, dupeEntries) => { if(err) { return cb(err); @@ -858,12 +858,12 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { // :TODO: Look @ db entries for area that were *not* processed above return callback(null); } - ], + ], err => { return nextLocation(err); } ); - }, + }, err => { return cb(err); }); @@ -874,14 +874,14 @@ function getDescFromFileName(fileName) { const ext = paths.extname(fileName); const name = paths.basename(fileName, ext); - return _.upperFirst(name.replace(/[\-_.+]/g, ' ').replace(/\s+/g, ' ')); + return _.upperFirst(name.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); } // // Return an object of stats about an area(s) // // { -// +// // totalFiles : , // totalBytes : , // areas : { @@ -892,7 +892,7 @@ function getDescFromFileName(fileName) { // } // } // -function getAreaStats(cb) { +function getAreaStats(cb) { FileDb.all( `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size FROM file f, file_meta m @@ -928,9 +928,9 @@ function getAreaStats(cb) { // method exposed for event scheduler function updateAreaStatsScheduledEvent(args, cb) { - getAreaStats( (err, stats) => { + getAreaStats( (err, stats) => { if(!err) { - StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); } return cb(err); diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 5ec266fd..87dfe9f4 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -38,7 +38,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { const menuOpts = { extraArgs : { - filterCriteria : filterCriteria, + filterCriteria : filterCriteria, }, menuFlags : [ 'popParent' ], }; diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 7444af56..88ed2ddd 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -59,8 +59,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); }, - viewItemInfo : (formData, extraArgs, cb) => { - }, removeItem : (formData, extraArgs, cb) => { const selectedItem = this.dlQueue.items[formData.value.queueItem]; if(!selectedItem) { @@ -74,7 +72,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); - + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); } @@ -230,10 +228,10 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } - + self.viewControllers[name].setFocus(true); return callback(null); - + }, ], err => { diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 320d36d3..d8b566b7 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -8,7 +8,7 @@ const uuidV4 = require('uuid/v4'); module.exports = class FileBaseFilters { constructor(client) { this.client = client; - + this.load(); } @@ -25,7 +25,7 @@ module.exports = class FileBaseFilters { 'est_release_year', 'byte_size', 'file_name', - ]; + ]; } toArray() { @@ -40,11 +40,11 @@ module.exports = class FileBaseFilters { add(filterInfo) { const filterUuid = uuidV4(); - + filterInfo.tags = this.cleanTags(filterInfo.tags); - + this.filters[filterUuid] = filterInfo; - + return filterUuid; } @@ -94,18 +94,18 @@ module.exports = class FileBaseFilters { } cleanTags(tags) { - return tags.toLowerCase().replace(/,?\s+|\,/g, ' ').trim(); + 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; } diff --git a/core/file_base_search.js b/core/file_base_search.js index 27656123..06dac204 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -110,7 +110,7 @@ exports.getModule = class FileBaseSearch extends MenuModule { const menuOpts = { extraArgs : { - filterCriteria : filterCriteria, + filterCriteria : filterCriteria, }, menuFlags : [ 'popParent' ], }; diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index dea7c5a8..13b7de33 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -32,13 +32,13 @@ const MciViewIds = { queueManager : { queue : 1, navMenu : 2, - + customRangeStart : 10, } }; exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { - + constructor(options) { super(options); @@ -58,7 +58,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); - + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); }, @@ -109,7 +109,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { displayFileInfoForFileEntry(fileEntry) { this.updateCustomViewTextsWithFilter( - 'queueManager', + 'queueManager', MciViewIds.queueManager.customRangeStart, fileEntry, { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... ); @@ -142,7 +142,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempBatchDownload( - this.client, + this.client, this.dlQueue.items, { expireTime : expireTime @@ -162,7 +162,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { this.updateCustomViewTextsWithFilter( 'queueManager', - MciViewIds.queueManager.customRangeStart, + MciViewIds.queueManager.customRangeStart, formatObj, { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } ); @@ -187,13 +187,13 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { if(err) { if(ErrNotEnabled === err.reasonCode) { - return nextFileEntry(err); // we should have caught this prior + return nextFileEntry(err); // we should have caught this prior } const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); - + FileAreaWeb.createAndServeTempDownload( - self.client, + self.client, fileEntry, { expireTime : expireTime }, (err, url) => { @@ -202,13 +202,13 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } fileEntry.webDlLinkRaw = url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); return nextFileEntry(null); } ); - } else { + } else { fileEntry.webDlLinkRaw = serveItem.url; fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); @@ -272,10 +272,10 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } - + self.viewControllers[name].setFocus(true); return callback(null); - + }, ], err => { @@ -284,4 +284,3 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { ); } }; - \ No newline at end of file diff --git a/core/file_entry.js b/core/file_entry.js index 8bf7a69d..861b9d79 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -15,7 +15,7 @@ const { unlink, readFile } = require('graceful-fs'); const crypto = require('crypto'); const moment = require('moment'); -const FILE_TABLE_MEMBERS = [ +const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', 'desc', 'desc_long', 'upload_timestamp' ]; @@ -47,7 +47,7 @@ module.exports = class FileEntry { // values we always want dl_count : 0, }; - + this.hashTags = options.hashTags || new Set(); this.fileName = options.fileName; this.storageTag = options.storageTag; @@ -173,7 +173,7 @@ module.exports = class FileEntry { async.each(Object.keys(self.meta), (n, next) => { const v = self.meta[n]; return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); - }, + }, err => { return callback(err, trans); }); @@ -185,7 +185,7 @@ module.exports = class FileEntry { }, err => { return callback(err, trans); - }); + }); } ], (err, trans) => { @@ -203,10 +203,10 @@ module.exports = class FileEntry { static getAreaStorageDirectoryByTag(storageTag) { const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); - + // absolute paths as-is if(storageLocation && '/' === storageLocation.charAt(0)) { - return storageLocation; + return storageLocation; } // relative to |areaStoragePrefix| @@ -283,7 +283,7 @@ module.exports = class FileEntry { transOrDb.serialize( () => { transOrDb.run( `INSERT OR IGNORE INTO hash_tag (hash_tag) - VALUES (?);`, + VALUES (?);`, [ hashTag ] ); @@ -321,7 +321,7 @@ module.exports = class FileEntry { err => { return cb(err); } - ); + ); } loadRating(cb) { @@ -352,7 +352,7 @@ module.exports = class FileEntry { } static get WellKnownMetaValues() { - return Object.keys(FILE_WELL_KNOWN_META); + return Object.keys(FILE_WELL_KNOWN_META); } static findFileBySha(sha, cb) { @@ -469,7 +469,7 @@ module.exports = class FileEntry { sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { - sql = + sql = `SELECT DISTINCT f.file_id, f.${filter.sort} FROM file f`; @@ -531,7 +531,7 @@ module.exports = class FileEntry { )` ); } - + if(filter.tags && filter.tags.length > 0) { // build list of quoted tags; filter.tags comes in as a space and/or comma separated values const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${tag}"` ).join(','); @@ -617,7 +617,7 @@ module.exports = class FileEntry { const srcPath = srcFileEntry.filePath; const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); - + if(!dstDir) { return cb(Errors.Invalid('Invalid storage tag')); } diff --git a/core/file_transfer.js b/core/file_transfer.js index 4e81bf72..bb3d362c 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -65,7 +65,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } if(options.extraArgs.sendQueue) { - this.sendQueue = options.extraArgs.sendQueue; + this.sendQueue = options.extraArgs.sendQueue; } if(options.extraArgs.recvFileName) { @@ -107,7 +107,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return { path : item }; } else { return item; - } + } }); this.sentFileIds = []; @@ -137,7 +137,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.sendQueue.forEach(f => { f.sent = true; sentFiles.push(f.path); - + }); this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); @@ -160,7 +160,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.sendQueue.forEach(f => { f.sent = true; sentFiles.push(f.path); - + }); this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); @@ -180,16 +180,16 @@ exports.getModule = class TransferFileModule extends MenuModule { } return next(err); }); - }, err => { + }, err => { return cb(err); }); - } + } } */ moveFileWithCollisionHandling(src, dst, cb) { // - // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. + // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // in the case of collisions. // const dstPath = paths.dirname(dst); @@ -283,7 +283,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); }, () => { return cb(null); - }); + }); }); } }); @@ -309,7 +309,7 @@ exports.getModule = class TransferFileModule extends MenuModule { temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { if(err) { - return callback(err); // failed to create it + return callback(err); // failed to create it } fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); @@ -334,7 +334,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return callback(null, args); } - ], + ], (err, args) => { return cb(err, args); } @@ -364,7 +364,7 @@ exports.getModule = class TransferFileModule extends MenuModule { const externalProc = pty.spawn(cmd, args, { cols : this.client.term.termWidth, rows : this.client.term.termHeight, - cwd : this.recvDirectory, + cwd : this.recvDirectory, }); this.client.setTemporaryDirectDataHandler(data => { @@ -376,7 +376,7 @@ exports.getModule = class TransferFileModule extends MenuModule { externalProc.write(data); } }); - + externalProc.on('data', data => { // needed for things like sz/rz if(external.escapeTelnet) { @@ -393,12 +393,12 @@ exports.getModule = class TransferFileModule extends MenuModule { externalProc.once('exit', (exitCode) => { this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); - + this.restorePipeAfterExternalProc(); externalProc.removeAllListeners(); return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); - }); + }); } executeExternalProtocolHandlerForSend(filePaths, cb) { @@ -413,7 +413,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.executeExternalProtocolHandler(args, err => { return cb(err); - }); + }); }); } @@ -434,7 +434,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return { sentFileIds : this.sentFileIds }; } else { return { recvFilePaths : this.recvFilePaths }; - } + } } updateSendStats(cb) { @@ -478,11 +478,11 @@ exports.getModule = class TransferFileModule extends MenuModule { fileIds.forEach(fileId => { FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); }); - + return cb(null); }); } - + updateRecvStats(cb) { let uploadBytes = 0; let uploadCount = 0; @@ -519,7 +519,7 @@ exports.getModule = class TransferFileModule extends MenuModule { function validateConfig(callback) { if(self.isSending()) { if(!Array.isArray(self.sendQueue)) { - self.sendQueue = [ self.sendQueue ]; + self.sendQueue = [ self.sendQueue ]; } } @@ -555,7 +555,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); } }, - function cleanupTempFiles(callback) { + function cleanupTempFiles(callback) { temptmp.cleanup( paths => { Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); }); diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index f1b3dbed..299c8af2 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -64,7 +64,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); } else { return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); - } + } }, }; } @@ -118,7 +118,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { protListView.redraw(); - return callback(null); + return callback(null); } ], err => { diff --git a/core/file_util.js b/core/file_util.js index 9452b23f..0f91e71a 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -66,11 +66,11 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { (err, finalPath) => { return cb(err, finalPath); } - ); + ); } // -// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. +// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // in the case of collisions. // function moveFileWithCollisionHandling(src, dst, cb) { diff --git a/core/fnv1a.js b/core/fnv1a.js index f7714936..743986d6 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -7,7 +7,7 @@ let _ = require('lodash'); module.exports = class FNV1a { constructor(data) { this.hash = 0x811c9dc5; - + if(!_.isUndefined(data)) { this.update(data); } @@ -17,7 +17,7 @@ module.exports = class FNV1a { if(_.isNumber(data)) { data = data.toString(); } - + if(_.isString(data)) { data = new Buffer(data); } @@ -28,8 +28,8 @@ module.exports = class FNV1a { for(let b of data) { this.hash = this.hash ^ b; - this.hash += - (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + + this.hash += + (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + (this.hash << 4) + (this.hash << 1); } @@ -46,5 +46,5 @@ module.exports = class FNV1a { get value() { return this.hash & 0xffffffff; } -} +}; diff --git a/core/fse.js b/core/fse.js index b266a9c9..02134f31 100644 --- a/core/fse.js +++ b/core/fse.js @@ -39,7 +39,7 @@ exports.moduleInfo = { TL2 - To TL3 - Subject TL4 - Area name - + TL5 - Date/Time (TODO: format) TL6 - Message number TL7 - Mesage total (in area) @@ -50,7 +50,7 @@ exports.moduleInfo = { TL12 - User1 TL13 - User2 - + Footer - Viewing HM1 - Menu (prev/next/etc.) @@ -61,14 +61,14 @@ exports.moduleInfo = { TL12 - User1 (fmt message object) TL13 - User2 - + */ const MciCodeIds = { ViewModeHeader : { From : 1, To : 2, Subject : 3, - + DateTime : 5, MsgNum : 6, MsgTotal : 7, @@ -78,9 +78,9 @@ const MciCodeIds = { ReplyToMsgID : 11, // :TODO: ConfName - + }, - + ViewModeFooter : { MsgNum : 6, MsgTotal : 7, @@ -90,7 +90,7 @@ const MciCodeIds = { From : 1, To : 2, Subject : 3, - + ErrorMsg : 13, }, }; @@ -116,12 +116,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // toUserId // this.editorType = config.editorType; - this.editorMode = config.editorMode; - + 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; @@ -160,7 +160,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul 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) } @@ -179,26 +179,24 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul 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; + return cb(err); + } - case 'editorMenu' : - self.viewControllers.body.setFocus(false); - self.viewControllers.footerEditorMenu.switchFocus(1); - break; + switch(self.footerMode) { + case 'editor' : + if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { + self.viewControllers.footerEditorMenu.detachClientEvents(); + } + self.viewControllers.body.switchFocus(1); + self.observeEditorEvents(); + break; - default : throw new Error('Unexpected mode'); - } + case 'editorMenu' : + self.viewControllers.body.setFocus(false); + self.viewControllers.footerEditorMenu.switchFocus(1); + break; + + default : throw new Error('Unexpected mode'); } return cb(null); @@ -210,9 +208,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); }, appendQuoteEntry: function(formData, extraArgs, cb) { - // :TODO: Dont' use magic # ID's here + // :TODO: Dont' use magic # ID's here const quoteMsgView = self.viewControllers.quoteBuilder.getView(1); - + if(self.newQuoteBlock) { self.newQuoteBlock = false; @@ -220,7 +218,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul quoteMsgView.addText(self.getQuoteByHeader()); } - + const quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote); quoteMsgView.addText(quoteText); @@ -339,7 +337,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // // Ensure first characters indicate ANSI for detection down // the line (other boards/etc.). We also set explicit_encoding - // to packetAnsiMsgEncoding (generally cp437) as various boards + // to packetAnsiMsgEncoding (generally cp437) as various boards // really don't like ANSI messages in UTF-8 encoding (they should!) // msgOpts.meta = { System : { 'explicit_encoding' : Config.scannerTossers.ftn_bso.packetAnsiMsgEncoding || 'cp437' } }; @@ -351,7 +349,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); } - + setMessage(message) { this.message = message; @@ -495,7 +493,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // :TODO: We'd like to delete up to N rows, but this does not work // in NetRunner: self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); - + self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); } callback(null); @@ -534,7 +532,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul art[n], self.client, { font : self.menuConfig.font }, - function displayed(err, artData) { + function displayed(err) { next(err); } ); @@ -561,7 +559,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul function complete(err) { cb(err); } - ); + ); } switchFooter(cb) { @@ -645,14 +643,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul ], function complete(err) { if(err) { - // :TODO: This needs properly handled! - console.log(err) + self.client.log.warn( { error : err.message }, 'FSE init error'); } else { self.isReady = true; self.finishedLoading(); } } - ); + ); } createInitialViews(mciData, cb) { @@ -666,7 +663,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul menuLoadOpts.mciMap = mciData.header.mciMap; self.addViewController( - 'header', + 'header', new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) ).loadFromMenuConfig(menuLoadOpts, function headerReady(err) { callback(err); @@ -713,7 +710,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function setInitialData(callback) { - switch(self.editorMode) { + switch(self.editorMode) { case 'view' : if(self.message) { self.initHeaderViewMode(); @@ -726,7 +723,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } } break; - + case 'edit' : { const fromView = self.viewControllers.header.getView(1); @@ -747,9 +744,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul callback(null); }, function setInitialFocus(callback) { - + switch(self.editorMode) { - case 'edit' : + case 'edit' : self.switchToHeader(); break; @@ -763,10 +760,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } ], function complete(err) { - if(err) { - console.error(err) - } - cb(err); + return cb(err); } ); } @@ -774,7 +768,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul mciReadyHandler(mciData, cb) { this.createInitialViews(mciData, err => { - // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in + // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in // place - if this is for existing usernames else validate spec /* @@ -787,7 +781,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul view.clearText(); self.viewControllers.headers.switchFocus(2); } - }); + }); } });*/ @@ -813,7 +807,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(modeView) { this.client.term.rawWrite(ansi.savePos()); modeView.setText('insert' === mode ? 'INS' : 'OVR'); - this.client.term.rawWrite(ansi.restorePos()); + this.client.term.rawWrite(ansi.restorePos()); } } } @@ -824,7 +818,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul initHeaderViewMode() { assert(_.isObject(this.message)); - + this.setHeaderText(MciCodeIds.ViewModeHeader.From, this.message.fromUserName); this.setHeaderText(MciCodeIds.ViewModeHeader.To, this.message.toUserName); this.setHeaderText(MciCodeIds.ViewModeHeader.Subject, this.message.subject); @@ -881,7 +875,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // this.newQuoteBlock = true; const self = this; - + async.waterfall( [ function clearAndDisplayArt(callback) { @@ -892,23 +886,23 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul self.client.term.rawWrite( ansi.goto(self.header.height + 1, 1) + ansi.deleteLine(24 - self.header.height)); - + theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { callback(err, artData); }); }, function createViewsIfNecessary(artData, callback) { var formId = self.getFormId('quoteBuilder'); - + if(_.isUndefined(self.viewControllers.quoteBuilder)) { var menuLoadOpts = { callingMenu : self, formId : formId, - mciMap : artData.mciMap, + mciMap : artData.mciMap, }; - + self.addViewController( - 'quoteBuilder', + 'quoteBuilder', new ViewController( { client : self.client, formId : formId } ) ).loadFromMenuConfig(menuLoadOpts, function quoteViewsReady(err) { callback(err); @@ -954,10 +948,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul ], function complete(err) { if(err) { - console.log(err) // :TODO: needs real impl. + self.client.log.warn( { error : err.message }, 'Error displaying quote builder'); } } - ); + ); } observeEditorEvents() { @@ -1004,22 +998,22 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul var body = this.viewControllers.body.getView(1); body.redraw(); this.viewControllers.body.switchFocus(1); - + // :TODO: create method (DRY) - + this.updateTextEditMode(body.getTextEditMode()); this.updateEditModePosition(body.getEditPosition()); this.observeEditorEvents(); } - + quoteBuilderFinalize() { // :TODO: fix magic #'s const quoteMsgView = this.viewControllers.quoteBuilder.getView(1); const msgView = this.viewControllers.body.getView(1); - + let quoteLines = quoteMsgView.getData().trim(); - + if(quoteLines.length > 0) { if(this.replyIsAnsi) { const bodyMessageView = this.viewControllers.body.getView(1); @@ -1027,7 +1021,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } msgView.addText(`${quoteLines}\n\n`); } - + quoteMsgView.setText(''); this.footerMode = 'editor'; @@ -1040,14 +1034,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul getQuoteByHeader() { let quoteFormat = this.menuConfig.config.quoteFormats; - if(Array.isArray(quoteFormat)) { + 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 || this.client.currentTheme.helpers.getDateTimeFormat(); - return stringFormat(quoteFormat, { + const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + return stringFormat(quoteFormat, { dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), userName : this.replyToMessage.fromUserName, }); diff --git a/core/ftn_address.js b/core/ftn_address.js index f0936e1d..6b1e57e0 100644 --- a/core/ftn_address.js +++ b/core/ftn_address.js @@ -3,8 +3,8 @@ const _ = require('lodash'); -const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i; -const FTN_PATTERN_REGEXP = /^([0-9\*]+:)?([0-9\*]+)(\/[0-9\*]+)?(\.[0-9\*]+)?(@[a-z0-9\-\.\*]+)?$/i; +const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i; +const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; module.exports = class Address { constructor(addr) { @@ -133,7 +133,7 @@ module.exports = class Address { static fromString(addrStr) { const m = FTN_ADDRESS_REGEXP.exec(addrStr); - + if(m) { // start with a 2D let addr = { @@ -165,7 +165,7 @@ module.exports = class Address { let addrStr = `${this.zone}:${this.net}`; - // allow for e.g. '4D' or 5 + // allow for e.g. '4D' or 5 const dim = parseInt(dimensions.toString()[0]); if(dim >= 3) { diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index d84b4a69..d2f003e9 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -56,7 +56,7 @@ class PacketHeader { this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap this.prodCodeHi = 0xfe; // see above - this.prodRevHi = 0; + this.prodRevHi = 0; } get origAddress() { @@ -84,9 +84,9 @@ class PacketHeader { // See FSC-48 // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 - /*if(address.point) { + /*if(address.point) { this.auxNet = address.origNet; - this.origNet = -1; + this.origNet = -1; } else { this.origNet = address.net; this.auxNet = 0; @@ -158,16 +158,16 @@ exports.PacketHeader = PacketHeader; // // * Type 2 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete) // * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001 -// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 +// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 // and http://ftsc.org/docs/fsc-0048.002 -// +// // Additional resources: // * Writeup on differences between type 2, 2.2, and 2+: // http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // function Packet(options) { var self = this; - + this.options = options || {}; this.parsePacketHeader = function(packetBuffer, cb) { @@ -240,11 +240,11 @@ function Packet(options) { // // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" // - const capWordValidateSwapped = + const capWordValidateSwapped = ((packetHeader.capWordValidate & 0xff) << 8) | ((packetHeader.capWordValidate >> 8) & 0xff); - if(capWordValidateSwapped === packetHeader.capWord && + if(capWordValidateSwapped === packetHeader.capWord && 0 != packetHeader.capWord && packetHeader.capWord & 0x0001) { @@ -260,7 +260,7 @@ function Packet(options) { // :TODO: should fill bytes be 0? } } - + packetHeader.created = moment({ year : packetHeader.year, month : packetHeader.month - 1, // moment uses 0 indexed months @@ -269,36 +269,36 @@ function Packet(options) { minute : packetHeader.minute, second : packetHeader.second }); - + let ph = new PacketHeader(); _.assign(ph, packetHeader); cb(null, ph); }); }; - + this.getPacketHeaderBuffer = function(packetHeader) { let buffer = new Buffer(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); buffer.writeUInt16LE(packetHeader.year, 4); - buffer.writeUInt16LE(packetHeader.month, 6); + buffer.writeUInt16LE(packetHeader.month, 6); buffer.writeUInt16LE(packetHeader.day, 8); buffer.writeUInt16LE(packetHeader.hour, 10); buffer.writeUInt16LE(packetHeader.minute, 12); buffer.writeUInt16LE(packetHeader.second, 14); - + buffer.writeUInt16LE(packetHeader.baud, 16); buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); buffer.writeUInt16LE(packetHeader.destNet, 22); buffer.writeUInt8(packetHeader.prodCodeLo, 24); buffer.writeUInt8(packetHeader.prodRevHi, 25); - + const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); pass.copy(buffer, 26); - + buffer.writeUInt16LE(packetHeader.origZone, 34); buffer.writeUInt16LE(packetHeader.destZone, 36); buffer.writeUInt16LE(packetHeader.auxNet, 38); @@ -311,7 +311,7 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.origPoint, 50); buffer.writeUInt16LE(packetHeader.destPoint, 52); buffer.writeUInt32LE(packetHeader.prodData, 54); - + return buffer; }; @@ -321,22 +321,22 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); buffer.writeUInt16LE(packetHeader.year, 4); - buffer.writeUInt16LE(packetHeader.month, 6); + buffer.writeUInt16LE(packetHeader.month, 6); buffer.writeUInt16LE(packetHeader.day, 8); buffer.writeUInt16LE(packetHeader.hour, 10); buffer.writeUInt16LE(packetHeader.minute, 12); buffer.writeUInt16LE(packetHeader.second, 14); - + buffer.writeUInt16LE(packetHeader.baud, 16); buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); buffer.writeUInt16LE(packetHeader.destNet, 22); buffer.writeUInt8(packetHeader.prodCodeLo, 24); buffer.writeUInt8(packetHeader.prodRevHi, 25); - + const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); pass.copy(buffer, 26); - + buffer.writeUInt16LE(packetHeader.origZone, 34); buffer.writeUInt16LE(packetHeader.destZone, 36); buffer.writeUInt16LE(packetHeader.auxNet, 38); @@ -376,9 +376,9 @@ function Packet(options) { // likely need to re-decode as the specified encoding // * SAUCE is binary-ish data, so we need to inspect for it before any // decoding occurs - // + // let messageBodyData = { - message : [], + message : [], kludgeLines : {}, // KLUDGE:[value1, value2, ...] map seenBy : [], }; @@ -411,7 +411,7 @@ function Packet(options) { messageBodyData.kludgeLines[key] = value; } } - + let encoding = 'cp437'; async.series( @@ -426,12 +426,12 @@ function Packet(options) { if(!err) { // we read some SAUCE - don't re-process that portion into the body messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); -// messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); + // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); messageBodyData.sauce = theSauce; } else { - console.log(err) + Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read'); } - callback(null); // failure to read SAUCE is OK + return callback(null); // failure to read SAUCE is OK }); } else { callback(null); @@ -482,7 +482,7 @@ function Packet(options) { Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII'); decoded = iconv.decode(messageBodyBuffer, 'ascii'); } - + const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); let endOfMessage = false; @@ -491,13 +491,13 @@ function Packet(options) { messageBodyData.message.push(''); return; } - + if(line.startsWith('AREA:')) { messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); } else if(line.startsWith('--- ')) { // Tear Lines are tracked allowing for specialized display/etc. messageBodyData.tearLine = line; - } else if(/^[ ]{1,2}\* Origin\: /.test(line)) { // To spec is " * Origin: ..." + } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." messageBodyData.originLine = line; endOfMessage = true; // Anything past origin is not part of the message body } else if(line.startsWith('SEEN-BY:')) { @@ -523,7 +523,7 @@ function Packet(options) { } ); }; - + this.parsePacketMessages = function(packetBuffer, iterator, cb) { binary.parse(packetBuffer) .word16lu('messageType') @@ -540,22 +540,22 @@ function Packet(options) { .scan('message', NULL_TERM_BUFFER) .tap(function tapped(msgData) { // no arrow function; want classic this if(!msgData.messageType) { - // end marker -- no more messages + // end marker -- no more messages return cb(null); } - + if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { return cb(new Error('Unsupported message type: ' + msgData.messageType)); } - - const read = + + const read = 14 + // fixed header size msgData.modDateTime.length + 1 + msgData.toUserName.length + 1 + msgData.fromUserName.length + 1 + msgData.subject.length + 1 + msgData.message.length + 1; - + // // Convert null terminated arrays to strings // @@ -575,7 +575,7 @@ function Packet(options) { subject : convMsgData.subject, modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime), }); - + msg.meta.FtnProperty = {}; msg.meta.FtnProperty.ftn_orig_node = msgData.ftn_orig_node; msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node; @@ -587,31 +587,31 @@ function Packet(options) { self.processMessageBody(msgData.message, messageBodyData => { msg.message = messageBodyData.message; msg.meta.FtnKludge = messageBodyData.kludgeLines; - + if(messageBodyData.tearLine) { msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; - + if(self.options.keepTearAndOrigin) { msg.message += `\r\n${messageBodyData.tearLine}\r\n`; } } - + if(messageBodyData.seenBy.length > 0) { msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; } - + if(messageBodyData.area) { msg.meta.FtnProperty.ftn_area = messageBodyData.area; } - + if(messageBodyData.originLine) { msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; - + 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 @@ -619,7 +619,7 @@ function Packet(options) { 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) { let next = function(e) { @@ -629,12 +629,12 @@ function Packet(options) { self.parsePacketMessages(nextBuf, iterator, cb); } }; - + iterator('message', msg, next); } else { cb(null); } - }); + }); }); }; @@ -702,7 +702,7 @@ function Packet(options) { // // message: unbound length, NULL term'd - // + // // We need to build in various special lines - kludges, area, // seen-by, etc. // @@ -716,7 +716,7 @@ function Packet(options) { if(message.meta.FtnProperty.ftn_area) { msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) } - + // :TODO: DRY with similar function in this file! Object.keys(message.meta.FtnKludge).forEach(k => { switch(k) { @@ -731,7 +731,7 @@ function Packet(options) { break; default : - msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; } }); @@ -780,22 +780,22 @@ function Packet(options) { // msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']); - + let msgBodyEncoded; try { msgBodyEncoded = iconv.encode(msgBody + '\0', options.encoding); } catch(e) { msgBodyEncoded = iconv.encode(msgBody + '\0', 'ascii'); } - + return callback( - null, - Buffer.concat( [ - basicHeader, - toUserNameBuf, - fromUserNameBuf, + null, + Buffer.concat( [ + basicHeader, + toUserNameBuf, + fromUserNameBuf, subjectBuf, - msgBodyEncoded + msgBodyEncoded ]) ); } @@ -808,7 +808,7 @@ function Packet(options) { this.writeMessage = function(message, ws, options) { let basicHeader = new Buffer(34); - + basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4); @@ -827,7 +827,7 @@ function Packet(options) { let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); - + encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); @@ -839,7 +839,7 @@ function Packet(options) { // // message: unbound length, NULL term'd - // + // // We need to build in various special lines - kludges, area, // seen-by, etc. // @@ -866,7 +866,7 @@ function Packet(options) { if(message.meta.FtnProperty.ftn_area) { msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) } - + Object.keys(message.meta.FtnKludge).forEach(k => { switch(k) { case 'PATH' : break; // skip & save for last @@ -889,8 +889,8 @@ function Packet(options) { if(message.meta.FtnProperty.ftn_tear_line) { msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`; } - - // + + // // Origin line should be near the bottom of a message // if(message.meta.FtnProperty.ftn_origin) { @@ -918,11 +918,11 @@ function Packet(options) { if(err) { return callback(err); } - + let next = function(e) { callback(e); }; - + iterator('header', header, next); }); }, @@ -934,7 +934,7 @@ function Packet(options) { } ], cb // complete - ); + ); }; } @@ -954,7 +954,7 @@ Packet.Attribute = { InTransit : 0x0020, Orphan : 0x0040, KillSent : 0x0080, - Local : 0x0100, // Message is from *this* system + Local : 0x0100, // Message is from *this* system Hold : 0x0200, Reserved0 : 0x0400, FileRequest : 0x0800, @@ -998,7 +998,7 @@ Packet.prototype.writeHeader = function(ws, packetHeader) { Packet.prototype.writeMessageEntry = function(ws, msgEntry) { ws.write(msgEntry); - return msgEntry.length; + return msgEntry.length; }; Packet.prototype.writeTerminator = function(ws) { @@ -1014,11 +1014,11 @@ Packet.prototype.writeStream = function(ws, messages, options) { if(!_.isBoolean(options.terminatePacket)) { options.terminatePacket = true; } - + if(_.isObject(options.packetHeader)) { this.writePacketHeader(options.packetHeader, ws); } - + options.encoding = options.encoding || 'utf8'; messages.forEach(msg => { @@ -1034,12 +1034,12 @@ Packet.prototype.write = function(path, packetHeader, messages, options) { if(!_.isArray(messages)) { messages = [ messages ]; } - + options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' this.writeStream( fs.createWriteStream(path), // :TODO: specify mode/etc. messages, - { packetHeader : packetHeader, terminatePacket : true } - ); + Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options) + ); }; diff --git a/core/ftn_util.js b/core/ftn_util.js index 39093fab..03040484 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -45,7 +45,7 @@ exports.getQuotePrefix = getQuotePrefix; // See list here: https://github.com/Mithgol/node-fidonet-jam -function stringToNullPaddedBuffer(s, bufLen) { +function stringToNullPaddedBuffer(s, bufLen) { let buffer = new Buffer(bufLen).fill(0x00); let enc = iconv.encode(s, 'CP437').slice(0, bufLen); for(let i = 0; i < enc.length; ++i) { @@ -56,7 +56,7 @@ function stringToNullPaddedBuffer(s, bufLen) { // // Convert a FTN style DateTime string to a Date object -// +// // :TODO: Name the next couple methods better - for FTN *packets* function getDateFromFtnDateTime(dateTime) { // @@ -103,7 +103,7 @@ function getMessageSerialNumber(messageId) { // // Return a FTS-0009.001 compliant MSGID value given a message // See http://ftsc.org/docs/fts-0009.001 -// +// // "A MSGID line consists of the string "^AMSGID:" (where ^A is a // control-A (hex 01) and the double-quotes are not part of the // string), followed by a space, the address of the originating @@ -113,9 +113,9 @@ function getMessageSerialNumber(messageId) { // ^AMSGID: origaddr serialno // // The originating address should be specified in a form that -// constitutes a valid return address for the originating network. +// constitutes a valid return address for the originating network. // If the originating address is enclosed in double-quotes, the -// entire string between the beginning and ending double-quotes is +// entire string between the beginning and ending double-quotes is // considered to be the orginating address. A double-quote character // within a quoted address is represented by by two consecutive // double-quote characters. The serial number may be any eight @@ -123,13 +123,13 @@ function getMessageSerialNumber(messageId) { // messages from a given system may have the same serial number // within a three years. The manner in which this serial number is // generated is left to the implementor." -// +// // // Examples & Implementations // // Synchronet: .@ // 2606.agora-agn_tst@46:1/142 19609217 -// +// // Mystic: // 46:3/102 46686263 // @@ -145,10 +145,10 @@ function getMessageSerialNumber(messageId) { // function getMessageIdentifier(message, address, isNetMail = false) { const addrStr = new Address(address).toString('5D'); - return isNetMail ? + return isNetMail ? `${addrStr} ${getMessageSerialNumber(message.messageId)}` : `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` - ; + ; } // @@ -166,7 +166,7 @@ function getProductIdentifier() { } // -// Return a FRL-1004 style time zone offset for a +// Return a FRL-1004 style time zone offset for a // 'TZUTC' kludge line // // http://ftsc.org/docs/frl-1004.002 @@ -178,10 +178,10 @@ function getUTCTimeZoneOffset() { // // Get a FSC-0032 style quote prefix // http://ftsc.org/docs/fsc-0032.001 -// +// function getQuotePrefix(name) { let initials; - + const parts = name.split(' '); if(parts.length > 1) { // First & Last initials - (Bryan Ashby -> BA) @@ -199,7 +199,7 @@ function getQuotePrefix(name) { // http://ftsc.org/docs/fts-0004.001 // function getOrigin(address) { - const origin = _.has(Config, 'messageNetworks.originLine') ? + const origin = _.has(Config, 'messageNetworks.originLine') ? Config.messageNetworks.originLine : Config.general.boardName; @@ -220,16 +220,12 @@ function getVia(address) { /* FRL-1005.001 states teh following format: - ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] + ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] */ const addrStr = new Address(address).toString('5D'); const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); - - const version = packageJson.version - .replace(/\-/g, '.') - .replace(/alpha/,'a') - .replace(/beta/,'b'); + const version = getCleanEnigmaVersion(); return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; } @@ -272,7 +268,7 @@ function parseAbbreviatedNetNodeList(netNodes) { const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; let net; let m; - let results = []; + let results = []; while(null !== (m = re.exec(netNodes))) { if(m[1] && m[2]) { net = parseInt(m[1]); @@ -288,7 +284,7 @@ function parseAbbreviatedNetNodeList(netNodes) { // // Return a FTS-0004.001 SEEN-BY entry(s) that include // all pre-existing SEEN-BY entries with the addition -// of |additions|. +// of |additions|. // // See http://ftsc.org/docs/fts-0004.001 // and notes at http://ftsc.org/docs/fsc-0043.002. @@ -324,9 +320,9 @@ function getUpdatedSeenByEntries(existingEntries, additions) { if(!_.isArray(existingEntries)) { existingEntries = [ existingEntries ]; } - + if(!_.isString(additions)) { - additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); + additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); } additions = additions.sort(Address.getComparator()); @@ -361,13 +357,13 @@ const ENCODING_TO_FTS_5003_001_CHARS = { // level 1 - generally should not be used ascii : [ 'ASCII', 1 ], 'us-ascii' : [ 'ASCII', 1 ], - + // level 2 - 8 bit, ASCII based cp437 : [ 'CP437', 2 ], cp850 : [ 'CP850', 2 ], - + // level 3 - reserved - + // level 4 utf8 : [ 'UTF-8', 4 ], 'utf-8' : [ 'UTF-8', 4 ], @@ -381,7 +377,7 @@ function getCharacterSetIdentifierByEncoding(encodingName) { function getEncodingFromCharacterSetIdentifier(chrs) { const ident = chrs.split(' ')[0].toUpperCase(); - + // :TODO: fill in the rest!!! return { // level 1 @@ -399,7 +395,7 @@ function getEncodingFromCharacterSetIdentifier(chrs) { 'SWISS' : 'iso-646', 'UK' : 'iso-646', 'ISO-10' : 'iso-646-10', - + // level 2 'CP437' : 'cp437', 'CP850' : 'cp850', @@ -414,15 +410,15 @@ function getEncodingFromCharacterSetIdentifier(chrs) { 'LATIN-2' : 'iso-8859-2', 'LATIN-5' : 'iso-8859-9', 'LATIN-9' : 'iso-8859-15', - + // level 4 'UTF-8' : 'utf8', - + // deprecated stuff - 'IBMPC' : 'cp1250', // :TODO: validate + 'IBMPC' : 'cp1250', // :TODO: validate '+7_FIDO' : 'cp866', - '+7' : 'cp866', + '+7' : 'cp866', 'MAC' : 'macroman', // :TODO: validate - + }[ident]; } \ No newline at end of file diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 28f4c29d..81d477ad 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -63,7 +63,7 @@ function HorizontalMenuView(options) { } var text = strUtil.stylizeString( - item.text, + item.text, this.hasFocus && item.focused ? self.focusTextStyle : self.textStyle); var drawWidth = text.length + self.getSpacer().length * 2; // * 2 = sides @@ -72,7 +72,7 @@ function HorizontalMenuView(options) { ansi.goto(self.position.row, item.col) + (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()) + strUtil.pad(text, drawWidth, self.fillChar, 'center') - ); + ); }; } diff --git a/core/key_entry_view.js b/core/key_entry_view.js index cf1ba008..304f8ef3 100644 --- a/core/key_entry_view.js +++ b/core/key_entry_view.js @@ -44,7 +44,7 @@ module.exports = class KeyEntryView extends View { if(key && 'tab' === key.name && !this.eatTabKey) { return this.emit('action', 'next', key); } - + this.emit('action', 'accept'); // NOTE: we don't call super here. KeyEntryView is a special snowflake. } @@ -69,7 +69,7 @@ module.exports = class KeyEntryView extends View { } break; } - + super.setPropertyValue(propName, propValue); } diff --git a/core/last_callers.js b/core/last_callers.js index 3a889468..1bc1f422 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -20,7 +20,7 @@ const _ = require('lodash'); location affiliation ts - + */ exports.moduleInfo = { @@ -65,7 +65,7 @@ exports.getModule = class LastCallersModule extends MenuModule { function fetchHistory(callback) { callersView = vc.getView(MciCodeIds.CallerList); - // fetch up + // fetch up StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { loginHistory = lh; @@ -82,12 +82,12 @@ exports.getModule = class LastCallersModule extends MenuModule { loginHistory = noOpLoginHistory; } } - + // // Finally, we need to trim up the list to the needed size // loginHistory = loginHistory.slice(0, callersView.dimens.height); - + return callback(err); }); }, @@ -99,10 +99,10 @@ exports.getModule = class LastCallersModule extends MenuModule { const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; async.each( - loginHistory, + loginHistory, (item, next) => { item.userId = parseInt(item.log_value); - item.ts = moment(item.timestamp).format(dateTimeFormat); + item.ts = moment(item.timestamp).format(dateTimeFormat); User.getUserName(item.userId, (err, userName) => { if(err) { diff --git a/core/logger.js b/core/logger.js index f90aec41..1f1efde2 100644 --- a/core/logger.js +++ b/core/logger.js @@ -12,7 +12,7 @@ module.exports = class Log { static init() { const Config = require('./config.js').config; const logPath = Config.paths.logs; - + const err = this.checkLogPath(logPath); if(err) { console.error(err.message); // eslint-disable-line no-console @@ -29,9 +29,9 @@ module.exports = class Log { err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. }; - // try to remove sensitive info by default, e.g. 'password' fields + // try to remove sensitive info by default, e.g. 'password' fields [ 'formData', 'formValue' ].forEach(keyName => { - serializers[keyName] = (fd) => Log.hideSensitive(fd); + serializers[keyName] = (fd) => Log.hideSensitive(fd); }); this.log = bunyan.createLogger({ @@ -46,7 +46,7 @@ module.exports = class Log { if(!fs.statSync(logPath).isDirectory()) { return new Error(`${logPath} is not a directory`); } - + return null; } catch(e) { if('ENOENT' === e.code) { diff --git a/core/login_server_module.js b/core/login_server_module.js index 212d2e27..72958a0c 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -28,7 +28,7 @@ module.exports = class LoginServerModule extends ServerModule { } 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... } @@ -37,7 +37,7 @@ module.exports = class LoginServerModule extends ServerModule { // // 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 = {}; } @@ -68,7 +68,7 @@ module.exports = class LoginServerModule extends ServerModule { client.on('close', err => { const logFunc = err ? logger.log.info : logger.log.debug; logFunc( { clientId : client.session.id }, 'Connection closed'); - + clientConns.removeClient(client); }); @@ -80,7 +80,7 @@ module.exports = class LoginServerModule extends ServerModule { // likely just doesn't exist client.term.write('\nIdle timeout expired. Goodbye!\n'); client.end(); - } + } }); }); } diff --git a/core/mail_packet.js b/core/mail_packet.js index 3fb8b2d2..fbbb3e76 100644 --- a/core/mail_packet.js +++ b/core/mail_packet.js @@ -33,4 +33,4 @@ MailPacket.prototype.write = function(options) { // emits 'packet' event per packet constructed // assert(_.isArray(options.messages)); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/core/mail_util.js b/core/mail_util.js index 654b1617..4e959389 100644 --- a/core/mail_util.js +++ b/core/mail_util.js @@ -6,7 +6,7 @@ const Message = require('./message.js'); exports.getAddressedToInfo = getAddressedToInfo; -const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /* Input Output diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index f99774e6..2c0b6021 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -25,7 +25,7 @@ exports.MaskEditTextView = MaskEditTextView; // :TODO: // * Hint, e.g. YYYY/MM/DD // * Return values with literals in place -// +// function MaskEditTextView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); @@ -49,7 +49,7 @@ function MaskEditTextView(options) { this.drawText = function(s) { var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - + assert(textToDraw.length <= self.patternArray.length); // draw out the text we have so far @@ -105,7 +105,7 @@ MaskEditTextView.maskPatternCharacterRegEx = { MaskEditTextView.prototype.setText = function(text) { MaskEditTextView.super_.prototype.setText.call(this, text); - + if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() this.patternArrayPos = this.patternArray.length; } @@ -130,14 +130,14 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.clientBackspace(); } else { while(this.patternArrayPos > 0) { - if(_.isRegExp(this.patternArray[this.patternArrayPos])) { + if(_.isRegExp(this.patternArray[this.patternArrayPos])) { this.text = this.text.substr(0, this.text.length - 1); this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); this.clientBackspace(); break; } this.patternArrayPos--; - } + } } } @@ -162,7 +162,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.text += ch; this.patternArrayPos++; - while(this.patternArrayPos < this.patternArray.length && + while(this.patternArrayPos < this.patternArray.length && !_.isRegExp(this.patternArray[this.patternArrayPos])) { this.patternArrayPos++; @@ -186,11 +186,11 @@ MaskEditTextView.prototype.setPropertyValue = function(propName, value) { MaskEditTextView.prototype.getData = function() { var rawData = MaskEditTextView.super_.prototype.getData.call(this); - + if(!rawData || 0 === rawData.length) { return rawData; } - + var data = ''; assert(rawData.length <= this.patternArray.length); diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index c5b95bb3..eb783d41 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -4,13 +4,12 @@ // ENiGMA½ const TextView = require('./text_view.js').TextView; const EditTextView = require('./edit_text_view.js').EditTextView; -const ButtonView = require('./button_view.js').ButtonView; +const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; -const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; -const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; -const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; +const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; +const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; -//const StatusBarView = require('./status_bar_view.js').StatusBarView; const KeyEntryView = require('./key_entry_view.js'); const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; @@ -37,7 +36,7 @@ MCIViewFactory.UserViewCodes = [ 'XY', ]; -MCIViewFactory.prototype.createFromMCI = function(mci, cb) { +MCIViewFactory.prototype.createFromMCI = function(mci) { assert(mci.code); assert(mci.id > 0); assert(mci.position); @@ -78,7 +77,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { // switch(mci.code) { // Text Label (Text View) - case 'TL' : + case 'TL' : setOption(0, 'textStyle'); setOption(1, 'justify'); setWidth(2); @@ -105,14 +104,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { break; // Multi Line Edit Text - case 'MT' : + case 'MT' : // :TODO: apply params view = new MultiLineEditTextView(options); break; // Pre-defined Label (Text View) // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove - case 'PL' : + case 'PL' : if(mci.args.length > 0) { options.text = getPredefinedMCIValue(this.client, mci.args[0]); if(options.text) { @@ -126,7 +125,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { break; // Button - case 'BT' : + case 'BT' : if(mci.args.length > 0) { options.dimens = { width : parseInt(mci.args[0], 10) }; } @@ -144,14 +143,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { setOption(0, 'itemSpacing'); setOption(1, 'justify'); setOption(2, 'textStyle'); - + setFocusOption(0, 'focusTextStyle'); view = new VerticalMenuView(options); break; // Horizontal Menu - case 'HM' : + case 'HM' : setOption(0, 'itemSpacing'); setOption(1, 'textStyle'); @@ -165,7 +164,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { setOption(1, 'justify'); setFocusOption(0, 'focusTextStyle'); - + view = new SpinnerMenuView(options); break; diff --git a/core/menu_module.js b/core/menu_module.js index 884389ca..783cc40b 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -17,17 +17,17 @@ const assert = require('assert'); const _ = require('lodash'); exports.MenuModule = class MenuModule extends PluginModule { - + constructor(options) { - super(options); + super(options); 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.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; this.viewControllers = {}; @@ -70,7 +70,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } ); }, - function moveToPromptLocation(callback) { + function moveToPromptLocation(callback) { if(self.menuConfig.prompt) { // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements } @@ -171,10 +171,10 @@ exports.MenuModule = class MenuModule extends PluginModule { } nextMenu(cb) { - if(!this.haveNext()) { + if(!this.haveNext()) { return this.prevMenu(cb); // no next, go to prev } - + return this.client.menuStack.next(cb); } @@ -210,7 +210,7 @@ exports.MenuModule = class MenuModule extends PluginModule { haveNext() { return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); } - + autoNextMenu(cb) { const self = this; @@ -221,8 +221,8 @@ exports.MenuModule = class MenuModule extends PluginModule { return self.prevMenu(cb); } } - - if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { + + if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { if(this.hasNextTimeout()) { setTimeout( () => { return gotoNextMenu(); @@ -297,10 +297,10 @@ exports.MenuModule = class MenuModule extends PluginModule { if(options.clearScreen) { this.client.term.rawWrite(ansi.resetScreen()); } - + return theme.displayThemedAsset( - name, - this.client, + name, + this.client, Object.assign( { font : this.menuConfig.config.font }, options ), (err, artData) => { if(cb) { @@ -361,7 +361,7 @@ exports.MenuModule = class MenuModule extends PluginModule { pausePrompt(position, cb) { if(!cb && _.isFunction(position)) { cb = position; - position = null; + position = null; } this.optionalMoveToPosition(position); @@ -390,7 +390,7 @@ exports.MenuModule = class MenuModule extends PluginModule { if(!view) { return; } - + if(appendMultiLine && (view instanceof MultiLineEditTextView)) { view.addText(text); } else { @@ -401,7 +401,7 @@ exports.MenuModule = class MenuModule extends PluginModule { updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { options = options || {}; - let textView; + let textView; let customMciId = startId; const config = this.menuConfig.config; const endId = options.endId || 99; // we'll fail to get a view before 99 diff --git a/core/menu_stack.js b/core/menu_stack.js index b4bebea6..26e88cc5 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -78,19 +78,19 @@ module.exports = class MenuStack { // :TODO: leave() should really take a cb... this.pop().instance.leave(); // leave & remove current - + const previousModuleInfo = this.pop(); // get previous if(previousModuleInfo) { const opts = { - extraArgs : previousModuleInfo.extraArgs, + extraArgs : previousModuleInfo.extraArgs, savedState : previousModuleInfo.savedState, lastMenuResult : menuResult, }; return this.goto(previousModuleInfo.name, opts, cb); } - + return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); } @@ -106,14 +106,14 @@ module.exports = class MenuStack { if(currentModuleInfo && name === currentModuleInfo.name) { if(cb) { - cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); + cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); } return; } const loadOpts = { name : name, - client : self.client, + client : self.client, }; if(_.isObject(options)) { diff --git a/core/menu_util.js b/core/menu_util.js index d9e5a1a6..a86690e5 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -42,7 +42,7 @@ function getMenuConfig(client, name, cb) { } else { callback(null); } - } + } ], function complete(err) { cb(err, menuConfig); @@ -53,7 +53,7 @@ function getMenuConfig(client, name, cb) { function loadMenu(options, cb) { assert(_.isObject(options)); assert(_.isString(options.name)); - assert(_.isObject(options.client)); + assert(_.isObject(options.client)); async.waterfall( [ @@ -88,7 +88,7 @@ function loadMenu(options, cb) { return callback(err, modData); }); - }, + }, function createModuleInstance(modData, callback) { Log.trace( { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, @@ -98,11 +98,11 @@ function loadMenu(options, cb) { try { moduleInstance = new modData.mod.getModule({ menuName : options.name, - menuConfig : modData.config, + menuConfig : modData.config, extraArgs : options.extraArgs, client : options.client, lastMenuResult : options.lastMenuResult, - }); + }); } catch(e) { return callback(e); } @@ -143,7 +143,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); cb(null, formForId[mciReqKey]); return; - } + } // // Generic match @@ -184,24 +184,24 @@ function handleAction(client, formData, conf, cb) { switch(actionAsset.type) { case 'method' : - case 'systemMethod' : + case 'systemMethod' : if(_.isString(actionAsset.location)) { return callModuleMenuMethod( - client, - actionAsset, - paths.join(Config.paths.mods, actionAsset.location), - formData, - conf.extraArgs, + client, + actionAsset, + paths.join(Config.paths.mods, actionAsset.location), + formData, + conf.extraArgs, cb); } else if('systemMethod' === actionAsset.type) { // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () // :TODO: Probably better as system_method.js return callModuleMenuMethod( - client, - actionAsset, - paths.join(__dirname, 'system_menu_method.js'), - formData, - conf.extraArgs, + client, + actionAsset, + paths.join(__dirname, 'system_menu_method.js'), + formData, + conf.extraArgs, cb); } else { // local to current module @@ -209,7 +209,7 @@ function handleAction(client, formData, conf, cb) { if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); } - + const err = new Error('Method does not exist'); client.log.warn( { method : actionAsset.asset }, err.message); return cb(err); @@ -222,14 +222,14 @@ function handleAction(client, formData, conf, cb) { function handleNext(client, nextSpec, conf, cb) { assert(_.isString(nextSpec) || _.isArray(nextSpec)); - + if(_.isArray(nextSpec)) { nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); } - + const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); // :TODO: getAssetWithShorthand() can return undefined - handle it! - + conf = conf || {}; const extraArgs = conf.extraArgs || {}; @@ -252,7 +252,7 @@ function handleNext(client, nextSpec, conf, cb) { const err = new Error('Method does not exist'); client.log.warn( { method : nextAsset.asset }, err.message); - return cb(err); + return cb(err); } case 'menu' : diff --git a/core/menu_view.js b/core/menu_view.js index 41f1302f..5aed54ca 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -16,7 +16,7 @@ exports.MenuView = MenuView; function MenuView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - + View.call(this, options); this.disablePipe = options.disablePipe || false; @@ -65,11 +65,11 @@ util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { const self = this; - if(items) { + if(items) { this.items = []; items.forEach( itemText => { this.items.push( - { + { text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) } ); @@ -79,7 +79,7 @@ MenuView.prototype.setItems = function(items) { MenuView.prototype.removeItem = function(index) { this.items.splice(index, 1); - + if(this.focusItems) { this.focusItems.splice(index, 1); } @@ -95,7 +95,7 @@ MenuView.prototype.getCount = function() { return this.items.length; }; -MenuView.prototype.getItems = function() { +MenuView.prototype.getItems = function() { return this.items.map( item => { return item.text; }); @@ -140,7 +140,7 @@ MenuView.prototype.onKeyPress = function(ch, key) { MenuView.prototype.setFocusItems = function(items) { const self = this; - + if(items) { this.focusItems = []; items.forEach( itemText => { @@ -183,7 +183,7 @@ MenuView.prototype.setHotKeys = function(hotKeys) { this.hotKeys[key.toLowerCase()] = hotKeys[key]; } } else { - this.hotKeys = hotKeys; + this.hotKeys = hotKeys; } } }; diff --git a/core/message.js b/core/message.js index 5d3c8db9..7cac5c79 100644 --- a/core/message.js +++ b/core/message.js @@ -9,9 +9,9 @@ const getISOTimestampString = require('./database.js').getISOTimestampString; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); -const { +const { isAnsi, isFormattedLine, - splitTextAtTerms, + splitTextAtTerms, renderSubstr } = require('./string_util.js'); @@ -45,7 +45,7 @@ function Message(options) { this.fromUserName = options.fromUserName || ''; this.subject = options.subject || ''; this.message = options.message || ''; - + if(_.isDate(options.modTimestamp) || moment.isMoment(options.modTimestamp)) { this.modTimestamp = moment(options.modTimestamp); } else if(_.isString(options.modTimestamp)) { @@ -115,7 +115,7 @@ Message.StateFlags0 = { Exported : 0x00000002, // exported to foreign system }; -Message.FtnPropertyNames = { +Message.FtnPropertyNames = { FtnOrigNode : 'ftn_orig_node', FtnDestNode : 'ftn_dest_node', FtnOrigNetwork : 'ftn_orig_network', @@ -166,12 +166,12 @@ Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { if(!moment.isMoment(modTimestamp)) { modTimestamp = moment(modTimestamp); } - + areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); - + return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); }; @@ -180,8 +180,8 @@ Message.getMessageIdByUuid = function(uuid, cb) { `SELECT message_id FROM message WHERE message_uuid = ? - LIMIT 1;`, - [ uuid ], + LIMIT 1;`, + [ uuid ], (err, row) => { if(err) { cb(err); @@ -210,30 +210,30 @@ Message.getMessageIdsByMetaValue = function(category, name, value, cb) { }; Message.getMetaValuesByMessageId = function(messageId, category, name, cb) { - const sql = + const sql = `SELECT meta_value FROM message_meta WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - + msgDb.all(sql, [ messageId, category, name ], (err, rows) => { if(err) { return cb(err); } - + if(0 === rows.length) { return cb(new Error('No value for category/name')); } - + // single values are returned without an array if(1 === rows.length) { return cb(null, rows[0].meta_value); } - + cb(null, rows.map(r => r.meta_value)); // map to array of values only }); }; -Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { +Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { async.waterfall( [ function getMessageId(callback) { @@ -256,22 +256,22 @@ Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { Message.prototype.loadMeta = function(cb) { /* Example of loaded this.meta: - + meta: { System: { - local_to_user_id: 1234, + local_to_user_id: 1234, }, FtnProperty: { ftn_seen_by: [ "1/102 103", "2/42 52 65" ] } - } - */ - - const sql = + } + */ + + const sql = `SELECT meta_category, meta_name, meta_value FROM message_meta WHERE message_id = ?;`; - + let self = this; msgDb.each(sql, [ this.messageId ], (err, row) => { if(!(row.meta_category in self.meta)) { @@ -279,12 +279,12 @@ Message.prototype.loadMeta = function(cb) { self.meta[row.meta_category][row.meta_name] = row.meta_value; } else { if(!(row.meta_name in self.meta[row.meta_category])) { - self.meta[row.meta_category][row.meta_name] = row.meta_value; + self.meta[row.meta_category][row.meta_name] = row.meta_value; } else { if(_.isString(self.meta[row.meta_category][row.meta_name])) { - self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; + self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; } - + self.meta[row.meta_category][row.meta_name].push(row.meta_value); } } @@ -315,7 +315,7 @@ Message.prototype.load = function(options, cb) { if(!msgRow) { return callback(new Error('Message (no longer) available')); } - + self.messageId = msgRow.message_id; self.areaTag = msgRow.area_tag; self.messageUuid = msgRow.message_uuid; @@ -356,13 +356,13 @@ Message.prototype.persistMetaValue = function(category, name, value, transOrDb, const metaStmt = transOrDb.prepare( `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) VALUES (?, ?, ?, ?);`); - + if(!_.isArray(value)) { value = [ value ]; } - + let self = this; - + async.each(value, (v, next) => { metaStmt.run(self.messageId, category, name, v, err => { next(err); @@ -379,7 +379,7 @@ Message.prototype.persist = function(cb) { } const self = this; - + async.waterfall( [ function beginTransaction(callback) { @@ -398,7 +398,7 @@ Message.prototype.persist = function(cb) { trans.run( `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) - VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, [ 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) { @@ -415,15 +415,15 @@ Message.prototype.persist = function(cb) { } /* Example of self.meta: - + meta: { System: { - local_to_user_id: 1234, + local_to_user_id: 1234, }, FtnProperty: { ftn_seen_by: [ "1/102 103", "2/42 52 65" ] } - } + } */ async.each(Object.keys(self.meta), (category, nextCat) => { async.each(Object.keys(self.meta[category]), (name, nextName) => { @@ -433,10 +433,10 @@ Message.prototype.persist = function(cb) { }, err => { nextCat(err); }); - + }, err => { callback(err, trans); - }); + }); }, function storeHashTags(trans, callback) { // :TODO: hash tag support @@ -470,21 +470,21 @@ Message.prototype.getQuoteLines = function(options, cb) { if(!options.termWidth || !options.termHeight || !options.cols) { return cb(Errors.MissingParam()); } - + options.startCol = options.startCol || 1; options.includePrefix = _.get(options, 'includePrefix', true); options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting - + /* Some long text that needs to be wrapped and quoted should look right after - doing so, don't ya think? yeah I think so + doing so, don't ya think? yeah I think so - Nu> Some long text that needs to be wrapped and quoted should look right + Nu> Some long text that needs to be wrapped and quoted should look right Nu> after doing so, don't ya think? yeah I think so - Ot> Nu> Some long text that needs to be wrapped and quoted should look + Ot> Nu> Some long text that needs to be wrapped and quoted should look Ot> Nu> right after doing so, don't ya think? yeah I think so */ @@ -498,7 +498,7 @@ Message.prototype.getQuoteLines = function(options, cb) { tabHandling : 'expand', tabWidth : 4, }; - + return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; }); @@ -527,44 +527,44 @@ Message.prototype.getQuoteLines = function(options, cb) { cols : options.cols, rows : 'auto', startCol : options.startCol, - forceLineTerm : true, + forceLineTerm : true, }, (err, prepped) => { prepped = prepped || this.message; - + let lastSgr = ''; const split = splitTextAtTerms(prepped); - + const quoteLines = []; const focusQuoteLines = []; // // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) - // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to + // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do // the trick and allow them to leave them alone! // split.forEach(l => { quoteLines.push(`${lastSgr}${l}`); - + focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); - lastSgr = (l.match(/(?:\x1b\x5b)[\?=;0-9]*m(?!.*(?:\x1b\x5b)[\?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex + lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex }); quoteLines[quoteLines.length - 1] += options.ansiResetSgr; - + return cb(null, quoteLines, focusQuoteLines, true); } ); } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}\> )+(?:[A-Za-z0-9]{2}\>)*) */; + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; const quoted = []; const input = _.trimEnd(this.message).replace(/\b/g, ''); - + // find *last* tearline let tearLinePos = this.getTearLinePosition(input); tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string - + input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { // // For each paragraph, a state machine: @@ -612,7 +612,7 @@ Message.prototype.getQuoteLines = function(options, cb) { buf += ` ${line}`; } break; - + case 'quote_line' : if(quoteMatch) { const rem = line.slice(quoteMatch[0].length); @@ -628,7 +628,7 @@ Message.prototype.getQuoteLines = function(options, cb) { state = 'line'; } break; - + default : if(isFormattedLine(line)) { quoted.push(getFormattedLine(line)); @@ -637,12 +637,12 @@ Message.prototype.getQuoteLines = function(options, cb) { buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any } break; - } + } }); - + quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); }); - + input.slice(tearLinePos).split(/\r?\n/).forEach(l => { quoted.push(...getWrapped(l)); }); diff --git a/core/message_area.js b/core/message_area.js index 53dd3086..3c42e23f 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -40,9 +40,9 @@ function getAvailableMessageConferences(client, options) { options = options || { includeSystemInternal : false }; assert(client || true === options.noClient); - - // perform ACS check per conf & omit system_internal if desired - return _.omitBy(Config.messageConferences, (conf, confTag) => { + + // perform ACS check per conf & omit system_internal if desired + return _.omitBy(Config.messageConferences, (conf, confTag) => { if(!options.includeSystemInternal && 'system_internal' === confTag) { return true; } @@ -60,15 +60,15 @@ function getSortedAvailMessageConferences(client, options) { }); sortAreasOrConfs(confs, 'conf'); - + return confs; } // Return an *object* of available areas within |confTag| function getAvailableMessageAreasByConfTag(confTag, options) { options = options || {}; - - // :TODO: confTag === "" then find default + + // :TODO: confTag === "" then find default if(_.has(Config.messageConferences, [ confTag, 'areas' ])) { const areas = Config.messageConferences[confTag].areas; @@ -92,9 +92,9 @@ function getSortedAvailMessageAreasByConfTag(confTag, options) { area : v, }; }); - + sortAreasOrConfs(areas, 'area'); - + return areas; } @@ -103,7 +103,7 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { // Find the first conference marked 'default'. If found, // inspect |client| against *read* ACS using defaults if not // specified. - // + // // If the above fails, just go down the list until we get one // that passes. // @@ -116,14 +116,14 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { const conf = Config.messageConferences[defaultConf]; if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { return defaultConf; - } + } } // just use anything we can defaultConf = _.findKey(Config.messageConferences, (conf, confTag) => { return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); }); - + return defaultConf; } @@ -138,19 +138,19 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { confTag = confTag || getDefaultMessageConferenceTag(client); if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) { - const areaPool = Config.messageConferences[confTag].areas; + const areaPool = Config.messageConferences[confTag].areas; let defaultArea = _.findKey(areaPool, o => o.default); if(defaultArea) { const area = areaPool[defaultArea]; if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { return defaultArea; - } + } } - + defaultArea = _.findKey(areaPool, (area) => { - return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); + return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); }); - + return defaultArea; } } @@ -159,18 +159,6 @@ function getMessageConferenceByTag(confTag) { return Config.messageConferences[confTag]; } -function getMessageConfByAreaTag(areaTag) { - const confs = Config.messageConferences; - let conf; - _.forEach(confs, (v) => { - if(_.has(v, [ 'areas', areaTag ])) { - conf = v; - return false; // stop iteration - } - }); - return conf; -} - function getMessageConfTagByAreaTag(areaTag) { const confs = Config.messageConferences; return Object.keys(confs).find( (confTag) => { @@ -194,9 +182,9 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { if(_.has(v, [ 'areas', areaTag ])) { area = v.areas[areaTag]; return false; // stop iteration - } + } }); - + return area; } } @@ -206,7 +194,7 @@ function changeMessageConference(client, confTag, cb) { [ function getConf(callback) { const conf = getMessageConferenceByTag(confTag); - + if(conf) { callback(null, conf); } else { @@ -216,7 +204,7 @@ function changeMessageConference(client, confTag, cb) { function getDefaultAreaInConf(conf, callback) { const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); const area = getMessageAreaByTag(areaTag, confTag); - + if(area) { callback(null, conf, { areaTag : areaTag, area : area } ); } else { @@ -229,7 +217,7 @@ function changeMessageConference(client, confTag, cb) { } else { return callback(null, conf, areaInfo); } - }, + }, function changeConferenceAndArea(conf, areaInfo, callback) { const newProps = { message_conf_tag : confTag, @@ -258,12 +246,12 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { [ function getArea(callback) { const area = getMessageAreaByTag(areaTag); - return callback(area ? null : new Error('Invalid message areaTag'), area); + return callback(area ? null : new Error('Invalid message areaTag'), area); }, function validateAccess(area, callback) { - // - // Need at least *read* to access the area - // + // + // Need at least *read* to access the area + // if(!client.acs.hasMessageAreaRead(area)) { return callback(new Error('Access denied to message area')); } else { @@ -294,7 +282,7 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { } // -// Temporairly -- e.g. non-persisted -- change to an area and it's +// Temporairly -- e.g. non-persisted -- change to an area and it's // associated underlying conference. ACS is checked for both. // // This is useful for example when doing a new scan @@ -312,7 +300,7 @@ function tempChangeMessageConfAndArea(client, areaTag) { if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { return false; } - + client.user.properties.message_conf_tag = confTag; client.user.properties.message_area_tag = areaTag; @@ -324,7 +312,7 @@ function changeMessageArea(client, areaTag, cb) { } function getMessageFromRow(row) { - return { + return { messageId : row.message_id, messageUuid : row.message_uuid, replyToMsgId : row.reply_to_message_id, @@ -346,8 +334,8 @@ function getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, what) // // * Only messages > |lastMessageId| should be returned/counted // - const selectWhat = ('count' === what) ? - 'COUNT() AS count' : + const selectWhat = ('count' === what) ? + 'COUNT() AS count' : 'message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count'; let sql = @@ -386,7 +374,7 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) { msgDb.get(sql, (err, row) => { return callback(err, row ? row.count : 0); }); - } + } ], cb ); @@ -421,7 +409,7 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { function complete(err) { cb(err, msgList); } - ); + ); } function getMessageListForArea(options, areaTag, cb) { @@ -435,7 +423,7 @@ function getMessageListForArea(options, areaTag, cb) { /* [ - { + { messageId, messageUuid, replyToId, toUserName, fromUserName, subject, modTimestamp, status(new|old), viewCount @@ -448,13 +436,13 @@ function getMessageListForArea(options, areaTag, cb) { async.series( [ function fetchMessages(callback) { - let sql = + 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 = ?`; if(Message.isPrivateAreaTag(areaTag)) { - sql += + sql += ` AND message_id IN ( SELECT message_id FROM message_meta @@ -462,7 +450,7 @@ function getMessageListForArea(options, areaTag, cb) { )`; } - sql += ' ORDER BY message_id;'; + sql += ' ORDER BY message_id;'; msgDb.each( sql, @@ -551,12 +539,12 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) ], function complete(err, didUpdate) { if(err) { - Log.debug( - { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, + Log.debug( + { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, 'Failed updating area last read ID'); } else { if(true === didUpdate) { - Log.trace( + Log.trace( { userId : userId, areaTag : areaTag, messageId : messageId }, 'Area last read ID updated'); } @@ -574,7 +562,7 @@ function persistMessage(message, cb) { }, function recordToMessageNetworks(callback) { return msgNetRecord(message, callback); - } + } ], cb ); @@ -582,7 +570,7 @@ function persistMessage(message, cb) { // method exposed for event scheduler function trimMessageAreasScheduledEvent(args, cb) { - + function trimMessageAreaByMaxMessages(areaInfo, cb) { if(0 === areaInfo.maxMessages) { return cb(null); @@ -605,7 +593,7 @@ function trimMessageAreasScheduledEvent(args, cb) { Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully'); } return cb(err); - } + } ); } @@ -690,7 +678,7 @@ function trimMessageAreasScheduledEvent(args, cb) { trimMessageAreaByMaxAgeDays(areaInfo, err => { return next(err); - }); + }); }); }, callback diff --git a/core/mime_util.js b/core/mime_util.js index 1e2bd32c..a110572c 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -36,6 +36,6 @@ function resolveMimeType(query) { if(mimeTypes.extensions[query]) { return query; // alreaed a mime-type } - + return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined } \ No newline at end of file diff --git a/core/misc_util.js b/core/misc_util.js index afe33dee..70bbb5e2 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -36,10 +36,10 @@ function resolvePath(path) { function getCleanEnigmaVersion() { return packageJson.version - .replace(/\-/g, '.') + .replace(/-/g, '.') .replace(/alpha/,'a') .replace(/beta/,'b') - ; + ; } // See also ftn_util.js getTearLine() & getProductIdentifier() diff --git a/core/mod_mixins.js b/core/mod_mixins.js index 291e0cc9..48546825 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -5,7 +5,7 @@ const messageArea = require('../core/message_area.js'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { - + tempMessageConfAndAreaSwitch(messageAreaTag) { messageAreaTag = messageAreaTag || this.messageAreaTag; if(!messageAreaTag) { @@ -14,7 +14,7 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { this.prevMessageConfAndArea = { confTag : this.client.user.properties.message_conf_tag, - areaTag : this.client.user.properties.message_area_tag, + areaTag : this.client.user.properties.message_area_tag, }; if(!messageArea.tempChangeMessageConfAndArea(this.client, this.messageAreaTag)) { @@ -25,7 +25,7 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { tempMessageConfAndAreaRestore() { if(this.prevMessageConfAndArea) { this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; - this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; + this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; } } }; diff --git a/core/msg_area_list.js b/core/msg_area_list.js index eaedbef8..96b0a51c 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -36,7 +36,7 @@ exports.moduleInfo = { const MciViewIds = { AreaList : 1, SelAreaInfo1 : 2, - SelAreaInfo2 : 3, + SelAreaInfo2 : 3, }; exports.getModule = class MessageAreaListModule extends MenuModule { @@ -61,7 +61,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); self.prevMenuOnTimeout(1000, cb); - } else { + } else { if(_.isString(area.art)) { const dispOptions = { client : self.client, @@ -72,7 +72,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { displayThemeArt(dispOptions, () => { // pause by default, unless explicitly told not to - if(_.has(area, 'options.pause') && false === area.options.pause) { + if(_.has(area, 'options.pause') && false === area.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { self.pausePrompt( () => { @@ -98,9 +98,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }, timeout); } - updateGeneralAreaInfoViews(areaIndex) { - // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! - /* experimental: not yet avail + // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! + /* + updateGeneralAreaInfoViews(areaIndex) { const areaInfo = self.messageAreas[areaIndex]; [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { @@ -109,8 +109,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { v.setFormatObject(areaInfo.area); } }); - */ } + */ mciReady(mciData, cb) { super.mciReady(mciData, err => { @@ -137,7 +137,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { 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 => { @@ -145,7 +145,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { index : i++, areaTag : v.area.areaTag, name : v.area.name, - desc : v.area.desc, + desc : v.area.desc, }); })); @@ -155,7 +155,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { index : i++, areaTag : v.area.areaTag, name : v.area.name, - desc : v.area.desc, + desc : v.area.desc, }); })); diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index c13f39a6..3ffef698 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -48,9 +48,9 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { self.client.log.info( { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, 'Message persisted' - ); + ); } - + return self.nextMenu(cb); } ); diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index 02915f79..0f25c63f 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -69,7 +69,7 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break; case 'page up' : bodyView.keyPressPageUp(); break; - case 'page down' : bodyView.keyPressPageDown(); break; + case 'page down' : bodyView.keyPressPageDown(); break; } // :TODO: need to stop down/page down if doing so would push the last @@ -83,13 +83,13 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { const modOpts = { extraArgs : { messageAreaTag : self.messageAreaTag, - replyToMessage : self.message, - } + replyToMessage : self.message, + } }; return self.gotoMenu(extraArgs.menu, modOpts, cb); } - + self.client.log(extraArgs, 'Missing extraArgs.menu'); return cb(null); } diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index 6f42cf36..43e57820 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -21,10 +21,10 @@ exports.moduleInfo = { const MciViewIds = { ConfList : 1, - + // :TODO: // # areas in conf .... see Obv/2, iNiQ, ... - // + // }; exports.getModule = class MessageConfListModule extends MenuModule { @@ -33,16 +33,16 @@ exports.getModule = class MessageConfListModule extends MenuModule { 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 + conf = conf.conf; // what we want is embedded messageArea.changeMessageConference(self.client, confTag, err => { - if(err) { + if(err) { self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); setTimeout( () => { @@ -59,7 +59,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { displayThemeArt(dispOptions, () => { // pause by default, unless explicitly told not to - if(_.has(conf, 'options.pause') && false === conf.options.pause) { + if(_.has(conf, 'options.pause') && false === conf.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { self.pausePrompt( () => { @@ -108,7 +108,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { 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 => { @@ -116,7 +116,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { index : i++, confTag : v.conf.confTag, name : v.conf.name, - desc : v.conf.desc, + desc : v.conf.desc, }); })); @@ -126,7 +126,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { index : i++, confTag : v.conf.confTag, name : v.conf.name, - desc : v.conf.desc, + desc : v.conf.desc, }); })); diff --git a/core/msg_list.js b/core/msg_list.js index e5a69e80..19ef30cd 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -26,7 +26,7 @@ const moment = require('moment'); MCI codes: VM1 : Message list - TL2 : Message info 1: { msgNumSelected, msgNumTotal } + TL2 : Message info 1: { msgNumSelected, msgNumTotal } */ exports.moduleInfo = { @@ -84,9 +84,9 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // 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)); + const logMsgList = (this.messageList.length <= 4) ? + this.messageList : + this.messageList.slice(0, 2).concat(this.messageList.slice(-2)); return { messageAreaTag : this.messageAreaTag, @@ -158,14 +158,14 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( 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); + return callback(err); }); }, function getLastReadMesageId(callback) { @@ -187,15 +187,15 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( 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 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}'; + 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 @@ -211,10 +211,10 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( msgListView.on('index update', idx => { self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, + MCICodesIDs.MsgInfo1, stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.messageList.length } )); }); - + if(self.initialFocusIndex > 0) { // note: causes redraw() msgListView.setFocusItemIndex(self.initialFocusIndex); @@ -228,29 +228,29 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, + 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'); + self.client.log.error( { error : err.message }, 'Error loading message list'); } return cb(err); } ); - }); + }); } getSaveState() { - return { initialFocusIndex : this.initialFocusIndex }; + return { initialFocusIndex : this.initialFocusIndex }; } restoreSavedState(savedState) { if(savedState) { this.initialFocusIndex = savedState.initialFocusIndex; - } + } } getMenuResult() { diff --git a/core/msg_network.js b/core/msg_network.js index 9e0813f4..890d1bcf 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -38,17 +38,17 @@ function startup(cb) { function shutdown(cb) { async.each( - msgNetworkModules, + msgNetworkModules, (msgNetModule, next) => { msgNetModule.shutdown( () => { return next(); }); - }, + }, () => { msgNetworkModules = []; return cb(null); } - ); + ); } function recordMessage(message, cb) { @@ -59,7 +59,7 @@ function recordMessage(message, cb) { // async.each(msgNetworkModules, (modInst, next) => { modInst.record(message); - next(); + next(); }, err => { cb(err); }); diff --git a/core/msg_scan_toss_module.js b/core/msg_scan_toss_module.js index 8172d77f..9b3598c1 100644 --- a/core/msg_scan_toss_module.js +++ b/core/msg_scan_toss_module.js @@ -13,12 +13,12 @@ function MessageScanTossModule() { require('util').inherits(MessageScanTossModule, PluginModule); MessageScanTossModule.prototype.startup = function(cb) { - cb(null); + return cb(null); }; MessageScanTossModule.prototype.shutdown = function(cb) { - cb(null); + return cb(null); }; -MessageScanTossModule.prototype.record = function(message) { +MessageScanTossModule.prototype.record = function(/*message*/) { }; \ No newline at end of file diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 68d8b3d6..bc73e903 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -4,7 +4,6 @@ const View = require('./view.js').View; const strUtil = require('./string_util.js'); const ansi = require('./ansi_term.js'); -const colorCodes = require('./color_codes.js'); const wordWrapText = require('./word_wrap.js').wordWrapText; const ansiPrep = require('./ansi_prep.js'); @@ -12,11 +11,11 @@ const assert = require('assert'); const _ = require('lodash'); // :TODO: Determine CTRL-* keys for various things - // See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt - // http://wiki.synchro.net/howto:editor:slyedit#edit_mode - // http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html +// See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt +// http://wiki.synchro.net/howto:editor:slyedit#edit_mode +// http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html - /* Mystic +/* Mystic [^B] Reformat Paragraph [^O] Show this help file [^I] Insert tab space [^Q] Enter quote mode [^K] Cut current line of text [^V] Toggle insert/overwrite @@ -179,8 +178,8 @@ function MultiLineEditTextView(options) { for(let i = startIndex; i < endIndex; ++i) { //${self.getSGRFor('text')} - self.client.term.write( - `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, + self.client.term.write( + `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, false // convertLineFeeds ); } @@ -268,7 +267,7 @@ function MultiLineEditTextView(options) { if(remain > 0) { text += ' '.repeat(remain + 1); -// text += new Array(remain + 1).join(' '); + // text += new Array(remain + 1).join(' '); } return text; @@ -291,7 +290,7 @@ function MultiLineEditTextView(options) { lines.forEach(line => { text += line.text.replace(re, '\t'); - + if(options.forceLineTerms || (eolMarker && line.eol)) { text += eolMarker; } @@ -459,7 +458,7 @@ function MultiLineEditTextView(options) { self.getRenderText(index).slice(self.cursorPos.col - c.length) + ansi.goto(absPos.row, absPos.col) + ansi.showCursor(), false - ); + ); } }; @@ -502,7 +501,7 @@ function MultiLineEditTextView(options) { } return wordWrapText( - s, + s, { width : width, tabHandling : tabHandling || 'expand', @@ -1122,19 +1121,19 @@ MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'mode' : + case 'mode' : this.mode = value; if('preview' === value && !this.specialKeyMap.next) { this.specialKeyMap.next = [ 'tab' ]; - } + } break; - case 'autoScroll' : + case 'autoScroll' : this.autoScroll = value; break; case 'tabSwitchesView' : - this.tabSwitchesView = value; + this.tabSwitchesView = value; this.specialKeyMap.next = this.specialKeyMap.next || []; this.specialKeyMap.next.push('tab'); break; diff --git a/core/new_scan.js b/core/new_scan.js index f3e851fb..b088e47f 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -25,8 +25,8 @@ exports.moduleInfo = { * :TODO: * * User configurable new scan: Area selection (avail from messages area) (sep module) * * Add status TL/VM (either/both should update if present) - * * - + * * + */ const MciCodeIds = { @@ -37,7 +37,7 @@ const MciCodeIds = { const Steps = { MessageConfs : 'messageConferences', FileBase : 'fileBase', - + Finished : 'finished', }; @@ -53,7 +53,7 @@ exports.getModule = class NewScanModule extends MenuModule { // :TODO: Make this conf/area specific: const config = this.menuConfig.config; - this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; + 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'; @@ -62,16 +62,16 @@ exports.getModule = class NewScanModule extends MenuModule { updateScanStatus(statusText) { this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); } - + newScanMessageConference(cb) { - // lazy init + // lazy init if(!this.sortedMessageConfs) { - const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. + const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => { return { confTag : k, - conf : v, + conf : v, }; }); @@ -91,27 +91,27 @@ exports.getModule = class NewScanModule extends MenuModule { this.currentScanAux.conf = this.currentScanAux.conf || 0; this.currentScanAux.area = this.currentScanAux.area || 0; } - + const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; this.newScanMessageArea(currentConf, () => { if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) { - this.currentScanAux.conf += 1; + this.currentScanAux.conf += 1; this.currentScanAux.area = 0; - + return this.newScanMessageConference(cb); // recursive to next conf } this.updateScanStatus(this.scanCompleteMsg); return cb(Errors.DoesNotExist('No more conferences')); - }); + }); } - + newScanMessageArea(conf, cb) { - // :TODO: it would be nice to cache this - must be done by conf! + // :TODO: it would be nice to cache this - must be done by conf! 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. @@ -207,20 +207,20 @@ exports.getModule = class NewScanModule extends MenuModule { performScanCurrentStep(cb) { switch(this.currentStep) { - case Steps.MessageConfs : - this.newScanMessageConference( () => { + case Steps.MessageConfs : + this.newScanMessageConference( () => { this.currentStep = Steps.FileBase; return this.performScanCurrentStep(cb); }); break; - + case Steps.FileBase : this.newScanFileBase( () => { this.currentStep = Steps.Finished; - return this.performScanCurrentStep(cb); + return this.performScanCurrentStep(cb); }); break; - + default : return cb(null); } } @@ -241,7 +241,7 @@ exports.getModule = class NewScanModule extends MenuModule { // :TODO: display scan step/etc. - async.series( + async.series( [ function loadFromConfig(callback) { const loadOpts = { diff --git a/core/nua.js b/core/nua.js index 7939e739..61dbeb62 100644 --- a/core/nua.js +++ b/core/nua.js @@ -22,10 +22,10 @@ const MciViewIds = { }; exports.getModule = class NewUserAppModule extends MenuModule { - + constructor(options) { super(options); - + const self = this; this.menuMethods = { @@ -40,7 +40,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { viewValidationListener : function(err, cb) { const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); let newFocusId; - + if(err) { errMsgView.setText(err.message); err.view.clearText(); @@ -67,14 +67,14 @@ exports.getModule = class NewUserAppModule extends MenuModule { // // We have to disable ACS checks for initial default areas as the user is not yet ready - // + // let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck // can't store undefined! confTag = confTag || ''; areaTag = areaTag || ''; - + newUser.properties = { real_name : formData.value.realName, birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format @@ -84,12 +84,12 @@ exports.getModule = class NewUserAppModule extends MenuModule { email_address : formData.value.email, web_address : formData.value.web, account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format - + message_conf_tag : confTag, message_area_tag : areaTag, term_height : self.client.term.termHeight, - term_width : self.client.term.termWidth, + term_width : self.client.term.termWidth, // :TODO: Other defaults // :TODO: should probably have a place to create defaults/etc. @@ -100,7 +100,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { } else { newUser.properties.theme_id = Config.defaults.theme; } - + // :TODO: User.create() should validate email uniqueness! newUser.create(formData.value.password, err => { if(err) { diff --git a/core/onelinerz.js b/core/onelinerz.js index 9e89addf..65599ba9 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -20,7 +20,7 @@ const async = require('async'); const _ = require('lodash'); const moment = require('moment'); -/* +/* Module :TODO: * Add pipe code support - override max length & monitor *display* len as user types in order to allow for actual display len with color @@ -73,7 +73,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { self.client.log.warn( { error : err.message }, 'Failed saving oneliner'); } - self.clearAddForm(); + self.clearAddForm(); return self.displayViewScreen(true, cb); // true=cls }); @@ -89,7 +89,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { } }; } - + initSequence() { const self = this; async.series( @@ -136,7 +136,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'view', + 'view', new ViewController( { client : self.client, formId : FormIds.View } ) ); @@ -149,7 +149,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } else { self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); + self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); return callback(null); } }, @@ -216,7 +216,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { [ function clearAndDisplayArt(callback) { self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(ansi.resetScreen()); + self.client.term.rawWrite(ansi.resetScreen()); theme.displayThemedAsset( self.menuConfig.config.art.add, @@ -230,7 +230,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'add', + 'add', new ViewController( { client : self.client, formId : FormIds.Add } ) ); @@ -269,7 +269,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { [ function openDatabase(callback) { self.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo), + getModDatabasePath(exports.moduleInfo), err => { return callback(err); } @@ -284,10 +284,10 @@ exports.getModule = class OnelinerzModule extends MenuModule { oneliner VARCHAR NOT NULL, timestamp DATETIME NOT NULL );` - , - err => { - return callback(err); - }); + , + err => { + return callback(err); + }); } ], err => { @@ -327,7 +327,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { err => { return cb(err); } - ); + ); } beforeArt(cb) { diff --git a/core/plugin_module.js b/core/plugin_module.js index 31ba6f01..da9410b0 100644 --- a/core/plugin_module.js +++ b/core/plugin_module.js @@ -3,5 +3,5 @@ exports.PluginModule = PluginModule; -function PluginModule(options) { +function PluginModule(/*options*/) { } diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 7fe921b3..47370a66 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -92,7 +92,7 @@ const PREDEFINED_MCI_GENERATORS = { ST : function serverName(client) { return client.session.serverName; }, FN : function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); - return activeFilter ? activeFilter.name : ''; + return activeFilter ? activeFilter.name : ''; }, DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes @@ -160,7 +160,7 @@ const PREDEFINED_MCI_GENERATORS = { }, OA : function systemArchitecture() { return os.arch(); }, - + SC : function systemCpuModel() { // // Clean up CPU strings a bit for better display @@ -190,7 +190,7 @@ const PREDEFINED_MCI_GENERATORS = { // System File Base, Up/Download Info // // :TODO: DD - Today's # of downloads (iNiQUiTY) - // + // SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, SO : function systemByteDownload() { const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); @@ -221,7 +221,7 @@ const PREDEFINED_MCI_GENERATORS = { // -> Include FTN/etc. // :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/rumorz.js b/core/rumorz.js index b83853f0..da1ab5f6 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -52,9 +52,9 @@ exports.getModule = class RumorzModule extends MenuModule { addEntry : (formData, extraArgs, cb) => { if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { const rumor = formData.value.rumor.trim(); // remove any trailing ws - + StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { - this.clearAddForm(); + this.clearAddForm(); return this.displayViewScreen(true, cb); // true=cls }); } else { @@ -77,7 +77,7 @@ exports.getModule = class RumorzModule extends MenuModule { const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); newEntryView.setText(''); - + // preview is optional if(previewView) { previewView.setText(''); @@ -130,7 +130,7 @@ exports.getModule = class RumorzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'view', + 'view', new ViewController( { client : self.client, formId : FormIds.View } ) ); @@ -143,7 +143,7 @@ exports.getModule = class RumorzModule extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } else { self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); + self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); return callback(null); } }, @@ -186,7 +186,7 @@ exports.getModule = class RumorzModule extends MenuModule { [ function clearAndDisplayArt(callback) { self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(resetScreen()); + self.client.term.rawWrite(resetScreen()); theme.displayThemedAsset( self.config.art.add, @@ -200,7 +200,7 @@ exports.getModule = class RumorzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'add', + 'add', new ViewController( { client : self.client, formId : FormIds.Add } ) ); @@ -220,7 +220,7 @@ exports.getModule = class RumorzModule extends MenuModule { }, function initPreviewUpdates(callback) { const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); - const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); if(previewView) { let timerId; entryView.on('key press', () => { @@ -230,7 +230,7 @@ exports.getModule = class RumorzModule extends MenuModule { if(focused === entryView) { previewView.setText(entryView.getData()); focused.setFocus(true); - } + } }, 500); }); } diff --git a/core/sauce.js b/core/sauce.js index 295a6069..b976450b 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -8,7 +8,9 @@ exports.readSAUCE = readSAUCE; const SAUCE_SIZE = 128; const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' -const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' + +// :TODO read comments +//const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' exports.SAUCE_SIZE = SAUCE_SIZE; // :TODO: SAUCE should be a class @@ -51,7 +53,7 @@ function readSAUCE(data, cb) { if(!SAUCE_ID.equals(vars.id)) { return cb(new Error('No SAUCE record present')); - } + } var ver = iconv.decode(vars.version, 'cp437'); @@ -137,7 +139,7 @@ var SAUCE_FONT_TO_ENCODING_HINT = { }; ['437', '720', '737', '775', '819', '850', '852', '855', '857', '858', -'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) { + '860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) { var codec = 'cp' + page; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 8da71d02..20ab435f 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1213,7 +1213,30 @@ function FTNMessageScanTossModule() { User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { if(err) { - return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); + // + // Couldn't find a local username. If the toUserName itself is a FTN address + // we can only assume the message is to the +op, else we'll have to fail. + // + const toUserNameAsAddress = Address.fromString(message.toUserName); + if(toUserNameAsAddress.isValid()) { + + Log.info( + { toUserName : message.toUserName, fromUserName : message.fromUserName }, + 'No local "to" username for FTN message. Appears to be a FTN address only; assuming addressed to SysOp' + ); + + User.getUserName(User.RootUserID, (err, sysOpUserName) => { + if(err) { + return callback(Errors.UnexpectedState('Failed to get SysOp user information')); + } + + message.meta.System[Message.SystemMetaNames.LocalToUserID] = User.RootUserID; + message.toUserName = sysOpUserName; + return callback(null); + }); + } else { + return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); + } } // we do this after such that error cases can be preseved above diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index 0e29e999..efcb1f19 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -43,7 +43,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { const config = this.menuConfig.config; this.target = config.target || 'message'; - this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; + this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; this.menuMethods = { scanDateSubmit : (formData, extraArgs, cb) => { @@ -232,7 +232,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { const scanDateView = vc.getView(MciViewIds.main.scanDate); // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now - const scanDateFormat = self.scanDateFormat.replace(/[\/\-. ]/g, ''); + const scanDateFormat = self.scanDateFormat.replace(/[/\-. ]/g, ''); scanDateView.setText(today.format(scanDateFormat)); if('message' === self.target) { diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index 65ac10af..e9145662 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -1,13 +1,12 @@ /* jslint node: true */ 'use strict'; -var MenuView = require('./menu_view.js').MenuView; -var ansi = require('./ansi_term.js'); -var strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); -var util = require('util'); -var assert = require('assert'); -var _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); exports.SpinnerMenuView = SpinnerMenuView; @@ -16,7 +15,7 @@ function SpinnerMenuView(options) { options.cursor = options.cursor || 'hide'; MenuView.call(this, options); - + var self = this; /* @@ -29,7 +28,7 @@ function SpinnerMenuView(options) { //assert(!self.positionCacheExpired); assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - + self.drawItem(this.focusedItemIndex); }; @@ -66,19 +65,19 @@ SpinnerMenuView.prototype.setFocus = function(focused) { SpinnerMenuView.prototype.setFocusItemIndex = function(index) { SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - + this.updateSelection(); // will redraw }; SpinnerMenuView.prototype.onKeyPress = function(ch, key) { if(key) { - if(this.isKeyMapped('up', key.name)) { + if(this.isKeyMapped('up', key.name)) { if(0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; } else { this.focusedItemIndex--; } - + this.updateSelection(); return; } else if(this.isKeyMapped('down', key.name)) { @@ -87,7 +86,7 @@ SpinnerMenuView.prototype.onKeyPress = function(ch, key) { } else { this.focusedItemIndex++; } - + this.updateSelection(); return; } diff --git a/core/stat_log.js b/core/stat_log.js index d6a53d28..edb2d98c 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -10,14 +10,14 @@ const moment = require('moment'); /* System Event Log & Stats ------------------------ - + System & user specific: * Events for generating various statistics, logs such as last callers, etc. * Stats such as counters User specific stats are simply an alternate interface to user properties, while system wide entries are handled on their own. Both are read accessible non-blocking - making them easily available for MCI codes for example. + making them easily available for MCI codes for example. */ class StatLog { constructor() { @@ -66,7 +66,7 @@ class StatLog { TimestampDesc : 'timestamp_desc', Random : 'random', }; - } + } setNonPeristentSystemStat(statName, statValue) { this.systemStats[statName] = statValue; @@ -139,7 +139,7 @@ class StatLog { return cb(new Error(`Value for ${statName} is not a number!`)); } - newValue += incrementBy; + newValue += incrementBy; } else { newValue = incrementBy; } @@ -201,19 +201,19 @@ class StatLog { } } ); - break; + break; case 'forever' : default : // nop break; - } + } } ); } getSystemLogEntries(logName, order, limit, cb) { - let sql = + let sql = `SELECT timestamp, log_value FROM system_event_log WHERE log_name = ?`; @@ -228,7 +228,7 @@ class StatLog { sql += ' ORDER BY timestamp DESC'; break; - case 'random' : + case 'random' : sql += ' ORDER BY RANDOM()'; } @@ -279,7 +279,7 @@ class StatLog { ); } ); - } + } } module.exports = new StatLog(); diff --git a/core/stats.js b/core/stats.js deleted file mode 100644 index ecc472de..00000000 --- a/core/stats.js +++ /dev/null @@ -1,30 +0,0 @@ -/* jslint node: true */ -'use strict'; - -var userDb = require('./database.js').dbs.user; - -exports.getSystemLoginHistory = getSystemLoginHistory; - -function getSystemLoginHistory(numRequested, cb) { - - numRequested = Math.max(1, numRequested); - - var loginHistory = []; - - userDb.each( - 'SELECT user_id, user_name, timestamp ' + - 'FROM user_login_history ' + - 'ORDER BY timestamp DESC ' + - 'LIMIT ' + numRequested + ';', - function historyRow(err, histEntry) { - loginHistory.push( { - userId : histEntry.user_id, - userName : histEntry.user_name, - timestamp : histEntry.timestamp, - } ); - }, - function complete(err, recCount) { - cb(err, loginHistory); - } - ); -} diff --git a/core/status_bar_view.js b/core/status_bar_view.js deleted file mode 100644 index ed47ca7d..00000000 --- a/core/status_bar_view.js +++ /dev/null @@ -1,64 +0,0 @@ -/* jslint node: true */ -'use strict'; - -var View = require('./view.js').View; -var TextView = require('./text_view.js').TextView; - -var assert = require('assert'); -var _ = require('lodash'); - -function StatusBarView(options) { - View.call(this, options); - - var self = this; - - -} - -require('util').inherits(StatusBarView, View); - -StatusBarView.prototype.redraw = function() { - - StatusBarView.super_.prototype.redraw.call(this); - -}; - -StatusBarView.prototype.setPanels = function(panels) { - -/* - "panels" : [ - { - "text" : "things and stuff", - "width" 20, - ... - }, - { - "width" : 40 // no text, etc... = spacer - } - ] - - |---------------------------------------------| - | stuff | -*/ - assert(_.isArray(panels)); - - this.panels = []; - - var tvOpts = { - cursor : 'hide', - position : { row : this.position.row, col : 0 }, - }; - - panels.forEach(function panel(p) { - assert(_.isObject(p)); - assert(_.has(p, 'width')); - - if(p.text) { - this.panels.push( new TextView( { })) - } else { - this.panels.push( { width : p.width } ); - } - }); - -}; - diff --git a/core/string_format.js b/core/string_format.js index 7fb7109a..eba715d5 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -5,7 +5,7 @@ const EnigError = require('./enig_error.js').EnigError; const { pad, - stylizeString, + stylizeString, renderStringLength, renderSubstr, formatByteSize, formatByteSizeAbbr, @@ -172,15 +172,15 @@ function formatNumberHelper(n, precision, type) { case 'b' : return n.toString(2); case 'o' : return n.toString(8); case 'x' : return n.toString(16); - case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); - case 'f' : return n.toFixed(precision); + case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); + case 'f' : return n.toFixed(precision); case 'g' : // we don't want useless trailing zeros. parseFloat -> back to string fixes this for us return parseFloat(n.toPrecision(precision || 1)).toString(); case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%'; case '' : return formatNumberHelper(n, precision, 'd'); - + default : throw new ValueError(`Unknown format code "${type}" for object of type 'float'`); } @@ -207,7 +207,7 @@ function formatNumber(value, tokens) { if('' !== tokens.precision) { throw new ValueError('Precision not allowed in integer format specifier'); - } + } } else if( [ 'e', 'E', 'f', 'F', 'g', 'G', '%' ].indexOf(type) > - 1) { if(tokens['#']) { throw new ValueError('Alternate form (#) not allowed in float format specifier'); @@ -215,7 +215,7 @@ function formatNumber(value, tokens) { } const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type); - const sign = value < 0 || 1 / value < 0 ? + const sign = value < 0 || 1 / value < 0 ? '-' : '-' === tokens.sign ? '' : tokens.sign; @@ -223,7 +223,7 @@ function formatNumber(value, tokens) { if(tokens[',']) { const match = /^(\d*)(.*)$/.exec(s); - const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; + const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; if('=' !== align) { return pad(sign + separated, width, fill, getPadAlign(align)); @@ -246,7 +246,7 @@ function formatNumber(value, tokens) { if(0 === width) { return sign + prefix + s; - } + } if('=' === align) { return sign + prefix + pad(s, width - sign.length - prefix.length, fill, getPadAlign('>')); @@ -272,9 +272,9 @@ const transformers = { styleL33t : (s) => stylizeString(s, 'l33t'), // :TODO: - // toMegs(), toKilobytes(), ... - // toList(), toCommaList(), - + // toMegs(), toKilobytes(), ... + // toList(), toCommaList(), + sizeWithAbbr : (n) => formatByteSize(n, true, 2), sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), sizeAbbr : (n) => formatByteSizeAbbr(n), @@ -293,14 +293,14 @@ function transformValue(transformerName, value) { } // :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc. -const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:\!([^:}]+))?(?:\:([^}]+))?}/g; +const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:!([^:}]+))?(?::([^}]+))?}/g; function getValue(obj, path) { const value = _.get(obj, path); if(!_.isUndefined(value)) { return _.isFunction(value) ? value() : value; } - + throw new KeyError(quote(path)); } @@ -350,7 +350,7 @@ module.exports = function format(fmt, obj) { // remainder if(pos < fmt.length) { out += fmt.slice(pos); - } + } - return out; + return out; }; diff --git a/core/string_util.js b/core/string_util.js index 238aeeee..a4544754 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -3,7 +3,6 @@ // ENiGMA½ const miscUtil = require('./misc_util.js'); -const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSI = require('./ansi_term.js'); // deps @@ -53,12 +52,12 @@ function stylizeString(s, style) { switch(style) { // None/normal case 'normal' : - case 'N' : + case 'N' : return s; // UPPERCASE - case 'upper' : - case 'U' : + case 'upper' : + case 'U' : return s.toUpperCase(); // lowercase @@ -107,8 +106,8 @@ function stylizeString(s, style) { return stylized; // Small i's: DEMENTiA - case 'small i' : - case 'i' : + case 'small i' : + case 'i' : return s.toUpperCase().replace(/I/g, 'i'); // mIxeD CaSE (random upper/lower) @@ -128,7 +127,7 @@ function stylizeString(s, style) { case '3' : for(i = 0; i < len; ++i) { c = SIMPLE_ELITE_MAP[s[i].toLowerCase()]; - stylized += c || s[i]; + stylized += c || s[i]; } return stylized; } @@ -147,11 +146,11 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { useRenderLen = miscUtil.valueWithDefault(useRenderLen, true); const renderLen = useRenderLen ? renderStringLength(s) : s.length; - const padlen = len >= renderLen ? len - renderLen : 0; + const padlen = len >= renderLen ? len - renderLen : 0; switch(dir) { case 'L' : - case 'left' : + case 'left' : s = padSGR + new Array(padlen).join(padChar) + stringSGR + s; break; @@ -162,10 +161,10 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { const right = Math.ceil(padlen / 2); const left = padlen - right; s = padSGR + new Array(left + 1).join(padChar) + stringSGR + s + padSGR + new Array(right + 1).join(padChar); - } + } break; - case 'R' : + case 'R' : case 'right' : s = stringSGR + s + padSGR + new Array(padlen).join(padChar); break; @@ -184,7 +183,7 @@ function replaceAt(s, n, t) { return s.substring(0, n) + t + s.substring(n + 1); } -const RE_NON_PRINTABLE = +const RE_NON_PRINTABLE = /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/; // eslint-disable-line no-control-regex function isPrintable(s) { @@ -198,11 +197,6 @@ function isPrintable(s) { return !RE_NON_PRINTABLE.test(s); } -function stringLength(s) { - // :TODO: See https://mathiasbynens.be/notes/javascript-unicode - return s.length; -} - function stripAllLineFeeds(s) { return s.replace(/\r?\n|[\r\u2028\u2029]/g, ''); } @@ -256,7 +250,7 @@ function renderSubstr(str, start, length) { match = re.exec(str); if(match) { - if(match.index > pos) { + if(match.index > pos) { s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); start = 0; // start offset applies only once out += s; @@ -269,7 +263,7 @@ function renderSubstr(str, start, length) { // remainder if(pos + start < str.length && renderLen < length) { - out += str.slice(pos + start, (pos + start + (length - renderLen))); + out += str.slice(pos + start, (pos + start + (length - renderLen))); //out += str.slice(pos + start, Math.max(1, pos + (length - renderLen - 1))); } @@ -277,7 +271,7 @@ function renderSubstr(str, start, length) { } // -// Method to return the "rendered" length taking into account Pipe and ANSI color codes. +// Method to return the "rendered" length taking into account Pipe and ANSI color codes. // // We additionally account for ANSI *forward* movement ESC sequences // in the form of ESC[C where is the "go forward" character count. @@ -291,40 +285,40 @@ function renderStringLength(s) { const re = ANSI_OR_PIPE_REGEXP; re.lastIndex = 0; // we recycle the rege; reset - + // // Loop counting only literal (non-control) sequences // paying special attention to ESC[C which means forward - // + // do { pos = re.lastIndex; m = re.exec(s); - + if(m) { if(m.index > pos) { len += s.slice(pos, m.index).length; } - + if('C' === m[3]) { // ESC[C is foward/right len += parseInt(m[2], 10) || 0; } - } + } } while(0 !== re.lastIndex); - + if(pos < s.length) { len += s.slice(pos).length; } - + return len; } -const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) +const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) function formatByteSizeAbbr(byteSize) { if(0 === byteSize) { return BYTE_SIZE_ABBRS[0]; // B } - + return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } @@ -332,7 +326,7 @@ function formatByteSize(byteSize, withAbbr = false, decimals = 2) { 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 += ` ${BYTE_SIZE_ABBRS[i]}`; + result += ` ${BYTE_SIZE_ABBRS[i]}`; } return result; } @@ -351,7 +345,7 @@ function formatCount(count, withAbbr = false, decimals = 2) { const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); if(withAbbr) { - result += `${COUNT_ABBRS[i]}`; + result += `${COUNT_ABBRS[i]}`; } return result; } @@ -359,7 +353,7 @@ function formatCount(count, withAbbr = false, decimals = 2) { // :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; -const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([\?=;0-9]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex +const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([?=;0-9]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex const ANSI_OPCODES_ALLOWED_CLEAN = [ //'A', 'B', // up, down //'C', 'D', // right, left @@ -370,17 +364,17 @@ function cleanControlCodes(input, options) { let m; let pos; let cleaned = ''; - + options = options || {}; - + // // Loop through |input| adding only allowed ESC // sequences and literals to |cleaned| - // + // do { pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; m = REGEXP_ANSI_CONTROL_CODES.exec(input); - + if(m) { if(m.index > pos) { cleaned += input.slice(pos, m.index); @@ -394,205 +388,17 @@ function cleanControlCodes(input, options) { cleaned += m[0]; } } - + } while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex); - + // remainder if(pos < input.length) { cleaned += input.slice(pos); } - + return cleaned; } -function prepAnsi(input, options, cb) { - if(!input) { - return cb(null, ''); - } - - options.termWidth = options.termWidth || 80; - options.termHeight = options.termHeight || 25; - options.cols = options.cols || options.termWidth || 80; - options.rows = options.rows || options.termHeight || 'auto'; - options.startCol = options.startCol || 1; - options.exportMode = options.exportMode || false; - - const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); - const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); - - const state = { - row : 0, - col : 0, - }; - - let lastRow = 0; - - function ensureRow(row) { - if(Array.isArray(canvas[row])) { - return; - } - - canvas[row] = Array.from( { length : options.cols}, () => new Object() ); - } - - parser.on('position update', (row, col) => { - state.row = row - 1; - state.col = col - 1; - - lastRow = Math.max(state.row, lastRow); - }); - - 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 c of literal) { - if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) { - ensureRow(state.row); - - canvas[state.row][state.col].char = c; - - if(state.sgr) { - canvas[state.row][state.col].sgr = state.sgr; - state.sgr = null; - } - } - - state.col += 1; - } - }); - - parser.on('control', (match, opCode) => { - // - // Movement is handled via 'position update', so we really only care about - // display opCodes - // - switch(opCode) { - case 'm' : - state.sgr = (state.sgr || '') + match; - break; - - default : - break; - } - }); - - function getLastPopulatedColumn(row) { - let col = row.length; - while(--col > 0) { - if(row[col].char || row[col].sgr) { - break; - } - } - return col; - } - - parser.on('complete', () => { - let output = ''; - let lastSgr = ''; - let line; - - canvas.slice(0, lastRow + 1).forEach(row => { - const lastCol = getLastPopulatedColumn(row) + 1; - - let i; - line = ''; - for(i = 0; i < lastCol; ++i) { - const col = row[i]; - if(col.sgr) { - lastSgr = col.sgr; - } - line += `${col.sgr || ''}${col.char || ' '}`; - } - - output += line; - - if(i < row.length) { - output += `${ANSI.blackBG()}${row.slice(i).map( () => ' ').join('')}${lastSgr}`; - } - - //if(options.startCol + options.cols < options.termWidth || options.forceLineTerm) { - if(options.startCol + i < options.termWidth || options.forceLineTerm) { - output += '\r\n'; - } - }); - - if(options.exportMode) { - // - // If we're in export mode, we do some additional hackery: - // - // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) - // if a line must wrap early, we'll place a ESC[A ESC[C where - // represents chars to get back to the position we were previously at - // - // * Replace contig spaces with ESC[C as well to save... space. - // - // :TODO: this would be better to do as part of the processing above, but this will do for now - const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with - let exportOutput = ''; - - let m; - let afterSeq; - let wantMore; - let renderStart; - - splitTextAtTerms(output).forEach(fullLine => { - renderStart = 0; - - while(fullLine.length > 0) { - let splitAt; - const ANSI_REGEXP = ANSI.getFullMatchRegExp(); - wantMore = true; - - while((m = ANSI_REGEXP.exec(fullLine))) { - afterSeq = m.index + m[0].length; - - if(afterSeq < MAX_CHARS) { - // after current seq - splitAt = afterSeq; - } else { - if(m.index < MAX_CHARS) { - // before last found seq - splitAt = m.index; - wantMore = false; // can't eat up any more - } - - break; // seq's beyond this point are >= MAX_CHARS - } - } - - if(splitAt) { - if(wantMore) { - splitAt = Math.min(fullLine.length, MAX_CHARS - 1); - } - } else { - splitAt = Math.min(fullLine.length, MAX_CHARS - 1); - } - - const part = fullLine.slice(0, splitAt); - fullLine = fullLine.slice(splitAt); - renderStart += renderStringLength(part); - exportOutput += `${part}\r\n`; - - if(fullLine.length > 0) { // more to go for this line? - exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; - } else { - exportOutput += ANSI.up(); - } - } - }); - - return cb(null, exportOutput); - } - - return cb(null, output); - }); - - parser.parse(input); -} - function isAnsiLine(line) { return isAnsi(line);// || renderStringLength(line) < line.length; } @@ -622,22 +428,23 @@ function isFormattedLine(line) { return false; } +// :TODO: rename to containsAnsi() function isAnsi(input) { if(!input || 0 === input.length) { return false; } - + // // * ANSI found - limited, just colors // * Full ANSI art - // * - // + // * + // // FULL ANSI art: // * SAUCE present & reports as ANSI art // * ANSI clear screen within first 2-3 codes // * ANSI movement codes (goto, right, left, etc.) - // - // * + // + // * /* readSAUCE(input, (err, sauce) => { if(!err && ('ANSi' === sauce.fileType || 'ANSiMation' === sauce.fileType)) { @@ -647,8 +454,8 @@ function isAnsi(input) { */ // :TODO: if a similar method is kept, use exec() until threshold - const ANSI_DET_REGEXP = /(?:\x1b\x5b)[\?=;0-9]*?[ABCDEFGHJKLMSTfhlmnprsu]/g; // eslint-disable-line no-control-regex - const m = input.match(ANSI_DET_REGEXP) || []; + const ANSI_DET_REGEXP = /(?:\x1b\x5b)[?=;0-9]*?[ABCDEFGHJKLMSTfhlmnprsu]/g; // eslint-disable-line no-control-regex + const m = input.match(ANSI_DET_REGEXP) || []; return m.length >= 4; // :TODO: do this reasonably, e.g. a percent or soemthing } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index f968a493..8a20af02 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -33,7 +33,7 @@ function login(callingMenu, formData, extraArgs, cb) { return callingMenu.prevMenu(cb); } } - + // success! return callingMenu.nextMenu(cb); }); @@ -72,7 +72,7 @@ function prevMenu(callingMenu, formData, extraArgs, cb) { callingMenu.prevMenu( err => { if(err) { - callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); + callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); } return cb(err); }); @@ -119,7 +119,7 @@ function nextConf(callingMenu, formData, extraArgs, cb) { if(err) { return cb(err); // logged within changeMessageConference() } - + return reloadMenu(callingMenu, cb); }); } @@ -132,7 +132,7 @@ function prevArea(callingMenu, formData, extraArgs, cb) { if(err) { return cb(err); // logged within changeMessageArea() } - + return reloadMenu(callingMenu, cb); }); } @@ -155,10 +155,10 @@ function nextArea(callingMenu, formData, extraArgs, cb) { } function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { - const username = formData.value.username || callingMenu.client.user.username; + const username = formData.value.username || callingMenu.client.user.username; const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; - + WebPasswordReset.sendForgotPasswordEmail(username, err => { if(err) { callingMenu.client.log.warn( { err : err.message }, 'Failed sending forgot password email'); @@ -166,8 +166,8 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { if(extraArgs.next) { return callingMenu.gotoMenu(extraArgs.next, cb); - } - - return logoff(callingMenu, formData, extraArgs, cb); + } + + return logoff(callingMenu, formData, extraArgs, cb); }); } diff --git a/core/system_view_validate.js b/core/system_view_validate.js index e2a5b2e0..beb6bcce 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -99,7 +99,7 @@ function validateGeneralMailAddressedTo(data, cb) { function validateEmailAvail(data, cb) { // // This particular method allows empty data - e.g. no email entered - // + // if(!data || 0 === data.length) { return cb(null); } @@ -110,7 +110,7 @@ function validateEmailAvail(data, cb) { // // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation // - const emailRegExp = /[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; + const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; if(!emailRegExp.test(data)) { return cb(new Error('Invalid email address')); } @@ -121,8 +121,8 @@ function validateEmailAvail(data, cb) { } else if(uids.length > 0) { return cb(new Error('Email address not unique')); } - - return cb(null); + + return cb(null); }); } diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index fa1754a5..30db1207 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -42,7 +42,7 @@ class TelnetClientConnection extends EventEmitter { this.client = client; } - + restorePipe() { if(!this.pipeRestored) { this.pipeRestored = true; @@ -68,14 +68,14 @@ class TelnetClientConnection extends EventEmitter { this.bridgeConnection.on('data', data => { this.client.term.rawWrite(data); - // + // // Wait for a terminal type request, and send it eactly once. // This is enough (in additional to other negotiations handled in telnet.js) // to get us in on most systems // if(!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) { this.termSent = true; - this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); + this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); } }); @@ -102,9 +102,9 @@ class TelnetClientConnection extends EventEmitter { // actual/current terminal type. // let bufs = buffers(); - + bufs.push(new Buffer( - [ + [ 255, // IAC 250, // SB 24, // TERMINAL-TYPE @@ -113,9 +113,9 @@ class TelnetClientConnection extends EventEmitter { )); bufs.push( - new Buffer(this.client.term.termType), // e.g. "ansi" + new Buffer(this.client.term.termType), // e.g. "ansi" new Buffer( [ 255, 240 ] ) // IAC, SE - ); + ); return bufs.toBuffer(); } @@ -128,9 +128,9 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { this.config = options.menuConfig.config; // defaults - this.config.port = this.config.port || 23; + this.config.port = this.config.port || 23; } - + initSequence() { let clientTerminated; const self = this; @@ -158,7 +158,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`); const telnetConnection = new TelnetClientConnection(self.client); - + telnetConnection.on('connected', () => { self.client.log.info(connectOpts, 'Telnet bridge connection established'); diff --git a/core/text_view.js b/core/text_view.js index f1b3ee7e..8bd38213 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -35,7 +35,7 @@ function TextView(options) { this.justify = options.justify || 'right'; this.resizable = miscUtil.valueWithDefault(options.resizable, true); this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); - + if(_.isString(options.textOverflow)) { this.textOverflow = options.textOverflow; } @@ -44,19 +44,19 @@ function TextView(options) { this.textMaskChar = options.textMaskChar; } -/* + /* this.drawText = function(s) { - // + // // |<- this.maxLength // ABCDEFGHIJK // |ABCDEFG| ^_ this.text.length // ^-- this.dimens.width // - let textToDraw = _.isString(this.textMaskChar) ? - new Array(s.length + 1).join(this.textMaskChar) : + let textToDraw = _.isString(this.textMaskChar) ? + new Array(s.length + 1).join(this.textMaskChar) : stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - + if(textToDraw.length > this.dimens.width) { if(this.hasFocus) { if(this.horizScroll) { @@ -64,7 +64,7 @@ function TextView(options) { } } else { if(textToDraw.length > this.dimens.width) { - if(this.textOverflow && + if(this.textOverflow && this.dimens.width > this.textOverflow.length && textToDraw.length - this.textOverflow.length >= this.textOverflow.length) { @@ -72,7 +72,7 @@ function TextView(options) { } else { textToDraw = textToDraw.substr(0, this.dimens.width); } - } + } } } @@ -89,7 +89,7 @@ function TextView(options) { this.drawText = function(s) { - // + // // |<- this.maxLength // ABCDEFGHIJK // |ABCDEFG| ^_ this.text.length @@ -97,26 +97,26 @@ function TextView(options) { // let renderLength = renderStringLength(s); // initial; may be adjusted below: - let textToDraw = _.isString(this.textMaskChar) ? - new Array(renderLength + 1).join(this.textMaskChar) : + let textToDraw = _.isString(this.textMaskChar) ? + new Array(renderLength + 1).join(this.textMaskChar) : stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - + renderLength = renderStringLength(textToDraw); - + if(renderLength >= this.dimens.width) { if(this.hasFocus) { if(this.horizScroll) { textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength); } } else { - if(this.textOverflow && + if(this.textOverflow && this.dimens.width > this.textOverflow.length && renderLength - this.textOverflow.length >= this.textOverflow.length) { - textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; + textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; } else { textToDraw = renderSubstr(textToDraw, 0, this.dimens.width); - } + } } } @@ -128,7 +128,7 @@ function TextView(options) { this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), this.getStyleSGR(1) || this.getSGR() - ), + ), false // no converting CRLF needed ); }; @@ -136,7 +136,7 @@ function TextView(options) { this.getEndOfTextColumn = function() { var offset = Math.min(this.text.length, this.dimens.width); - return this.position.col + offset; + return this.position.col + offset; }; this.setText(options.text || '', false); // false=do not redraw now @@ -168,7 +168,7 @@ TextView.prototype.setFocus = function(focused) { TextView.super_.prototype.setFocus.call(this, focused); this.redraw(); - + this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); this.client.term.write(this.getFocusSGR()); }; @@ -184,7 +184,7 @@ TextView.prototype.setText = function(text, redraw) { text = text.toString(); } - text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. + text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. var widthDelta = 0; if(this.text && this.text !== text) { @@ -199,7 +199,7 @@ TextView.prototype.setText = function(text, redraw) { } // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); if(this.autoScale.width) { this.dimens.width = renderStringLength(this.text) + widthDelta; @@ -214,7 +214,7 @@ TextView.prototype.setText = function(text, redraw) { TextView.prototype.setText = function(text) { if(!_.isString(text)) { text = text.toString(); - } + } var widthDelta = 0; if(this.text && this.text !== text) { @@ -227,7 +227,7 @@ TextView.prototype.setText = function(text) { this.text = this.text.substr(0, this.maxLength); } - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); //if(this.resizable) { // this.dimens.width = this.text.length + widthDelta; @@ -254,9 +254,9 @@ TextView.prototype.setPropertyValue = function(propName, value) { if(true === value) { this.textMaskChar = this.client.currentTheme.helpers.getPasswordChar(); } - break; + break; } - + TextView.super_.prototype.setPropertyValue.call(this, propName, value); }; diff --git a/core/theme.js b/core/theme.js index 0796b8ab..a1045a18 100644 --- a/core/theme.js +++ b/core/theme.js @@ -87,8 +87,8 @@ function loadTheme(themeID, cb) { if(err) { return cb(err); } - - if(!_.isObject(theme.info) || + + if(!_.isObject(theme.info) || !_.isString(theme.info.name) || !_.isString(theme.info.author)) { @@ -114,16 +114,16 @@ const IMMUTABLE_MCI_PROPERTIES = [ function getMergedTheme(menuConfig, promptConfig, theme) { assert(_.isObject(menuConfig)); assert(_.isObject(theme)); - - // :TODO: merge in defaults (customization.defaults{} ) - // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - - // - // Create a *clone* of menuConfig (menu.hjson) then bring in - // promptConfig (prompt.hjson) - // + + // :TODO: merge in defaults (customization.defaults{} ) + // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") + + // + // Create a *clone* of menuConfig (menu.hjson) then bring in + // promptConfig (prompt.hjson) + // var mergedTheme = _.cloneDeep(menuConfig); - + if(_.isObject(promptConfig.prompts)) { mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); } @@ -136,8 +136,8 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // merge customizer to disallow immutable MCI properties - // - var mciCustomizer = function(objVal, srcVal, key) { + // + var mciCustomizer = function(objVal, srcVal, key) { return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; }; @@ -159,69 +159,69 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } else { if(_.has(src, [ formKey, 'mci' ])) { mergeMciProperties(dest, src[formKey].mci); - } + } } } - - // - // menu.hjson can have a couple different structures: - // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block - // (this allows multiple layout types defined by one menu for example) - // - // 2) Non-explicit declaration: 'mci' directly under 'form:' - // - // theme.hjson has it's own mix: - // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) - // - // 2) Non-explicit: 'mci' directly under an entry - // - // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up - // with menu.hjson in #1. - // - // * When theming an explicit menu.hjson entry (1), we will use a matching explicit - // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" - // and fall back to generic if a match is not found. - // - // * 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) { + + // + // menu.hjson can have a couple different structures: + // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block + // (this allows multiple layout types defined by one menu for example) + // + // 2) Non-explicit declaration: 'mci' directly under 'form:' + // + // theme.hjson has it's own mix: + // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) + // + // 2) Non-explicit: 'mci' directly under an entry + // + // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up + // with menu.hjson in #1. + // + // * When theming an explicit menu.hjson entry (1), we will use a matching explicit + // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" + // and fall back to generic if a match is not found. + // + // * 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 + return k === k.toUpperCase(); // remove anything not uppercase }); - + menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - var applyFrom; + 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) { @@ -232,7 +232,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // 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; @@ -241,9 +241,9 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } else if('prompts' === sectionName) { // no 'form' or form keys for prompts -- direct to mci applyToForm(mergedThemeMenu, menuTheme); - } + } } - + // // Finished merging for this menu/prompt // @@ -259,13 +259,13 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } }); }); - + return mergedTheme; } function initAvailableThemes(cb) { - + async.waterfall( [ function loadMenuConfig(callback) { @@ -285,9 +285,9 @@ function initAvailableThemes(cb) { } return callback( - null, - menuConfig, - promptConfig, + null, + menuConfig, + promptConfig, files.filter( f => { // sync normally not allowed -- initAvailableThemes() is a startup-only method, however return fs.statSync(paths.join(Config.paths.themes, f)).isDirectory(); @@ -363,7 +363,7 @@ function setClientTheme(client, themeId) { logMsg = 'Failed setting theme by system default ID; Using the first one we can find'; } } - + client.log.debug( { themeId : themeId, info : client.currentTheme.info }, logMsg); } @@ -371,7 +371,7 @@ function getThemeArt(options, cb) { // // options - required: // name - // + // // options - optional // client - needed for user's theme/etc. // themeId @@ -388,7 +388,7 @@ function getThemeArt(options, cb) { // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... // :TODO: Some of these options should only be set if not provided! options.asAnsi = true; // always convert to ANSI - options.readSauce = true; // read SAUCE, if avail + options.readSauce = true; // read SAUCE, if avail options.random = _.get(options, 'random', true); // FILENAME.EXT support // @@ -406,7 +406,7 @@ function getThemeArt(options, cb) { // if('/' === options.name.charAt(0)) { // just take the path as-is - options.basePath = paths.dirname(options.name); + options.basePath = paths.dirname(options.name); } else if(options.name.indexOf('/') > -1) { // make relative to base BBS dir options.basePath = paths.join(__dirname, '../', paths.dirname(options.name)); @@ -432,7 +432,7 @@ function getThemeArt(options, cb) { if(artInfo || Config.defaults.theme === options.themeId) { return callback(null, artInfo); } - + options.basePath = paths.join(Config.paths.themes, Config.defaults.theme); art.getArt(options.name, options, (err, artInfo) => { return callback(null, artInfo); @@ -442,11 +442,11 @@ function getThemeArt(options, cb) { if(artInfo) { return callback(null, artInfo); } - + options.basePath = Config.paths.art; art.getArt(options.name, options, (err, artInfo) => { return callback(err, artInfo); - }); + }); } ], function complete(err, artInfo) { @@ -483,7 +483,7 @@ function displayThemeArt(options, cb) { /* function displayThemedPrompt(name, client, options, cb) { - + async.waterfall( [ function loadConfig(callback) { @@ -511,14 +511,14 @@ function displayThemedPrompt(name, client, options, cb) { // // 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, + promptConfig.art, client, dispOptions, (err, artData) => { @@ -576,7 +576,7 @@ function displayThemedPrompt(name, client, options, cb) { } displayThemedAsset( - promptConfig.art, + promptConfig.art, client, dispOptions, (err, artInfo) => { @@ -593,7 +593,7 @@ function displayThemedPrompt(name, client, options, cb) { // 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); @@ -627,7 +627,7 @@ function displayThemedPrompt(name, client, options, cb) { 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 { @@ -654,7 +654,7 @@ function displayThemedPrompt(name, client, options, cb) { // // Pause prompts are a special prompt by the name 'pause'. -// +// function displayThemedPause(client, options, cb) { if(!cb && _.isFunction(options)) { @@ -699,7 +699,7 @@ function displayThemedAsset(assetSpec, client, options, cb) { }); break; - case 'method' : + case 'method' : // :TODO: fetch & render via method break; diff --git a/core/tic_file_info.js b/core/tic_file_info.js index d2216d66..27e31e4c 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -28,7 +28,7 @@ module.exports = class TicFileInfo { static get requiredFields() { return [ - 'Area', 'Origin', 'From', 'File', 'Crc', + 'Area', 'Origin', 'From', 'File', 'Crc', // :TODO: validate this: //'Path', 'Seenby' // these two are questionable; some systems don't send them? ]; @@ -43,16 +43,16 @@ module.exports = class TicFileInfo { if(value) { // // We call toString() on values to ensure numbers, addresses, etc. are converted - // + // joinWith = joinWith || ''; if(Array.isArray(value)) { return value.map(v => v.toString() ).join(joinWith); } - + return value.toString(); } } - + get filePath() { return paths.join(paths.dirname(this.path), this.getAsString('File')); } @@ -86,7 +86,7 @@ module.exports = class TicFileInfo { const localInfo = { areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), }; - + if(!localInfo.areaTag) { return callback(Errors.Invalid(`No local area for "Area" of ${area}`)); } @@ -112,7 +112,7 @@ module.exports = class TicFileInfo { return callback(null, localInfo); }, function checksumAndSize(localInfo, callback) { - const crcTic = self.get('Crc'); + const crcTic = self.get('Crc'); const stream = fs.createReadStream(self.filePath); const crc = new CRC32(); let sizeActual = 0; @@ -193,7 +193,7 @@ module.exports = class TicFileInfo { // This is an optional keyword." // const to = this.get('To'); - + if(!to) { return allowNonExplicit; } @@ -219,10 +219,10 @@ module.exports = class TicFileInfo { let key; let value; let entry; - + lines.forEach(line => { keyEnd = line.search(/\s/); - + if(keyEnd < 0) { keyEnd = line.length; } @@ -253,7 +253,7 @@ module.exports = class TicFileInfo { value = parseInt(value, 16); break; - case 'size' : + case 'size' : value = parseInt(value, 10); break; diff --git a/core/ticker_text_view.js b/core/ticker_text_view.js deleted file mode 100644 index 6574880b..00000000 --- a/core/ticker_text_view.js +++ /dev/null @@ -1,94 +0,0 @@ -/* jslint node: true */ -'use strict'; - -var View = require('./view.js').View; -var miscUtil = require('./misc_util.js'); -var strUtil = require('./string_util.js'); -var ansi = require('./ansi_term.js'); -var util = require('util'); -var assert = require('assert'); - -exports.TickerTextView = TickerTextView; - -function TickerTextView(options) { - View.call(this, options); - - var self = this; - - this.text = options.text || ''; - this.tickerStyle = options.tickerStyle || 'rightToLeft'; - assert(this.tickerStyle in TickerTextView.TickerStyles); - - // :TODO: Ticker |text| should have ANSI stripped before calculating any lengths/etc. - // strUtil.ansiTextLength(s) - // strUtil.pad(..., ignoreAnsi) - // strUtil.stylizeString(..., ignoreAnsi) - - this.tickerState = {}; - switch(this.tickerStyle) { - case 'rightToLeft' : - this.tickerState.pos = this.position.row + this.dimens.width; - break; - } - - - self.onTickerInterval = function() { - switch(self.tickerStyle) { - case 'rightToLeft' : self.updateRightToLeftTicker(); break; - } - }; - - self.updateRightToLeftTicker = function() { - // if pos < start - // drawRemain() - // if pos + remain > end - // drawRemain(0, spaceFor) - // else - // drawString() + remainPading - }; - -} - -util.inherits(TickerTextView, View); - -TickerTextView.TickerStyles = { - leftToRight : 1, - rightToLeft : 2, - bounce : 3, - slamLeft : 4, - slamRight : 5, - slamBounce : 6, - decrypt : 7, - typewriter : 8, -}; -Object.freeze(TickerTextView.TickerStyles); - -/* -TickerTextView.TICKER_STYLES = [ - 'leftToRight', - 'rightToLeft', - 'bounce', - 'slamLeft', - 'slamRight', - 'slamBounce', - 'decrypt', - 'typewriter', -]; -*/ - -TickerTextView.prototype.controllerAttached = function() { - // :TODO: call super -}; - -TickerTextView.prototype.controllerDetached = function() { - // :TODO: call super - -}; - -TickerTextView.prototype.setText = function(text) { - this.text = strUtil.stylizeString(text, this.textStyle); - - if(!this.dimens || !this.dimens.width) { - this.dimens.width = Math.ceil(this.text.length / 2); - } -}; \ No newline at end of file diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index 35676193..27ae2169 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -1,13 +1,11 @@ /* jslint node: true */ 'use strict'; -var MenuView = require('./menu_view.js').MenuView; -var ansi = require('./ansi_term.js'); -var strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); -var util = require('util'); -var assert = require('assert'); -var _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); exports.ToggleMenuView = ToggleMenuView; @@ -44,7 +42,7 @@ ToggleMenuView.prototype.redraw = function() { var item = this.items[i]; var text = strUtil.stylizeString( item.text, i === this.focusedItemIndex && this.hasFocus ? this.focusTextStyle : this.textStyle); - + if(1 === i) { //console.log(this.styleColor1) //var sepColor = this.getANSIColor(this.styleColor1 || this.getColor()); diff --git a/core/upload.js b/core/upload.js index 5a49a0ca..b4130433 100644 --- a/core/upload.js +++ b/core/upload.js @@ -85,7 +85,7 @@ exports.getModule = class UploadModule extends MenuModule { fileDetailsContinue : (formData, extraArgs, cb) => { // see displayFileDetailsPageForUploadEntry() for this hackery: - cb(null); + cb(null); return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any }, @@ -119,7 +119,7 @@ exports.getModule = class UploadModule extends MenuModule { return cb(null); } - }; + }; } getSaveState() { @@ -143,12 +143,12 @@ exports.getModule = class UploadModule extends MenuModule { isBlindUpload() { return 'blind' === this.uploadType; } isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); } - + initSequence() { const self = this; if(0 === this.availAreas.length) { - // + // return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail'); } @@ -185,7 +185,7 @@ exports.getModule = class UploadModule extends MenuModule { // 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 @@ -203,8 +203,8 @@ exports.getModule = class UploadModule extends MenuModule { // Upon completion, we'll re-enter the module with some file paths handed to us // return this.gotoMenu( - this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', - modOpts, + this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', + modOpts, cb ); }); @@ -219,7 +219,7 @@ exports.getModule = class UploadModule extends MenuModule { const fmtObj = Object.assign( {}, stepInfo); let stepIndicatorFmt = ''; - let logStepFmt; + let logStepFmt; const fmtConfig = this.menuConfig.config; @@ -228,7 +228,7 @@ exports.getModule = class UploadModule extends MenuModule { const indicator = { }; const self = this; - + function updateIndicator(mci, isFinished) { indicator.mci = mci; @@ -253,7 +253,7 @@ exports.getModule = class UploadModule extends MenuModule { updateIndicator(MciViewIds.processing.calcHashIndicator); break; - case 'hash_finish' : + case 'hash_finish' : stepIndicatorFmt = fmtConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; updateIndicator(MciViewIds.processing.calcHashIndicator, true); break; @@ -263,7 +263,7 @@ exports.getModule = class UploadModule extends MenuModule { updateIndicator(MciViewIds.processing.archiveListIndicator); break; - case 'archive_list_finish' : + case 'archive_list_finish' : fmtObj.archivedFileCount = stepInfo.archiveEntries.length; stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; updateIndicator(MciViewIds.processing.archiveListIndicator, true); @@ -273,7 +273,7 @@ exports.getModule = class UploadModule extends MenuModule { stepIndicatorFmt = fmtConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; break; - case 'desc_files_start' : + case 'desc_files_start' : stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files'; updateIndicator(MciViewIds.processing.descFileIndicator); break; @@ -289,7 +289,7 @@ exports.getModule = class UploadModule extends MenuModule { } fmtObj.stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); - + if(this.hasProcessingArt) { this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj, { appendMultiLine : true } ); @@ -339,7 +339,7 @@ exports.getModule = class UploadModule extends MenuModule { 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) { @@ -389,7 +389,7 @@ exports.getModule = class UploadModule extends MenuModule { // name changed; ajust before persist newEntry.fileName = paths.basename(finalPath); } - + return nextEntry(null); // still try next file } @@ -474,7 +474,7 @@ exports.getModule = class UploadModule extends MenuModule { if(err) { return nextDupe(err); } - + const areaInfo = getFileAreaByTag(dupe.areaTag); if(areaInfo) { dupe.areaName = areaInfo.name; @@ -553,12 +553,12 @@ exports.getModule = class UploadModule extends MenuModule { return callback(null, scanResults); } - return self.displayDupesPage(scanResults.dupes, () => { + return self.displayDupesPage(scanResults.dupes, () => { return callback(null, scanResults); }); }, function prepDetails(scanResults, callback) { - return self.prepDetailsForUpload(scanResults, callback); + return self.prepDetailsForUpload(scanResults, callback); }, function startMovingAndPersistingToDatabase(scanResults, callback) { // @@ -583,14 +583,14 @@ exports.getModule = class UploadModule extends MenuModule { displayOptionsPage(cb) { const self = this; - + async.series( [ function prepArtAndViewController(callback) { return self.prepViewControllerWithArt( - 'options', - FormIds.options, - { clearScreen : true, trailingLF : false }, + 'options', + FormIds.options, + { clearScreen : true, trailingLF : false }, callback ); }, @@ -621,7 +621,7 @@ exports.getModule = class UploadModule extends MenuModule { fileNameView.setText(sanatizeFilename(fileNameView.getData())); } }); - + self.uploadType = 'blind'; uploadTypeView.setFocusItemIndex(0); // default to blind fileNameView.setText(blindFileNameText); @@ -658,14 +658,14 @@ exports.getModule = class UploadModule extends MenuModule { displayFileDetailsPageForUploadEntry(fileEntry, cb) { const self = this; - + async.waterfall( [ function prepArtAndViewController(callback) { return self.prepViewControllerWithArt( - 'fileDetails', + 'fileDetails', FormIds.fileDetails, - { clearScreen : true, trailingLF : false }, + { clearScreen : true, trailingLF : false }, err => { return callback(err); } diff --git a/core/user.js b/core/user.js index 09d26163..76c493ea 100644 --- a/core/user.js +++ b/core/user.js @@ -49,7 +49,7 @@ module.exports = class User { active : 2, }; } - + isAuthenticated() { return true === this.authenticated; } @@ -83,7 +83,7 @@ module.exports = class User { groupNames = [ groupNames ]; } - const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); + const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); return isMember; } @@ -91,11 +91,11 @@ module.exports = class User { if(this.isRoot() || this.isGroupMember('sysops')) { return 100; } - + if(this.isGroupMember('users')) { return 30; } - + return 10; // :TODO: Is this what we want? } @@ -203,14 +203,14 @@ module.exports = class User { if(err) { return callback(err); } - + self.userId = this.lastID; // Do not require activation for userId 1 (root/admin) if(User.RootUserID === self.userId) { self.properties.account_status = User.AccountStatus.active; } - + return callback(null, trans); } ); @@ -220,7 +220,7 @@ module.exports = class User { if(err) { return callback(err); } - + self.properties.pw_pbkdf2_salt = info.salt; self.properties.pw_pbkdf2_dk = info.dk; return callback(null, trans); @@ -283,8 +283,8 @@ module.exports = class User { userDb.run( `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);`, - [ this.userId, propName, propValue ], + VALUES (?, ?, ?);`, + [ this.userId, propName, propValue ], err => { if(cb) { return cb(err); @@ -334,7 +334,7 @@ module.exports = class User { if(err) { return cb(err); } - + stmt.finalize( () => { return cb(null); }); @@ -346,7 +346,7 @@ module.exports = class User { if(err) { return cb(err); } - + const newProperties = { pw_pbkdf2_salt : info.salt, pw_pbkdf2_dk : info.dk, @@ -395,7 +395,7 @@ module.exports = class User { } ); } - + static isRootUserId(userId) { return (User.RootUserID === userId); } @@ -466,11 +466,11 @@ module.exports = class User { if(err) { return cb(err); } - + if(row) { return cb(null, row.user_name); } - + return cb(Errors.DoesNotExist('No matching user ID')); } ); @@ -498,7 +498,7 @@ module.exports = class User { if(err) { return cb(err); } - properties[row.prop_name] = row.prop_value; + properties[row.prop_name] = row.prop_value; }, (err) => { return cb(err, err ? null : properties); }); @@ -512,12 +512,12 @@ module.exports = class User { `SELECT user_id FROM user_property WHERE prop_name = ? AND prop_value = ?;`, - [ propName, propValue ], + [ propName, propValue ], (err, row) => { if(row) { userIds.push(row.user_id); } - }, + }, () => { return cb(null, userIds); } @@ -557,7 +557,7 @@ module.exports = class User { return nextUser(err, user); } ); - }, + }, (err, transformed) => { return cb(err, transformed); }); @@ -594,14 +594,14 @@ module.exports = class User { }); } - static generatePasswordDerivedKey(password, salt, cb) { + static generatePasswordDerivedKey(password, salt, cb) { password = new Buffer(password).toString('hex'); crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => { if(err) { return cb(err); } - + return cb(null, dk.toString('hex')); }); } diff --git a/core/user_config.js b/core/user_config.js index 432cdade..70e1f068 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -28,10 +28,10 @@ const MciCodeIds = { TermHeight : 8, Theme : 9, Password : 10, - PassConfirm : 11, + PassConfirm : 11, ThemeInfo : 20, ErrorMsg : 21, - + SaveCancel : 25, }; @@ -52,11 +52,11 @@ exports.getModule = class UserConfigModule extends MenuModule { 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 @@ -64,23 +64,23 @@ exports.getModule = class UserConfigModule extends MenuModule { 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); @@ -93,13 +93,13 @@ exports.getModule = class UserConfigModule extends MenuModule { } 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(), @@ -108,15 +108,15 @@ exports.getModule = class UserConfigModule extends MenuModule { affiliation : formData.value.affils, email_address : formData.value.email, web_address : formData.value.web, - term_height : formData.value.termHeight.toString(), + 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 => { + self.client.user.persistProperties(newProperties, err => { if(err) { self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); // :TODO: warn end user! @@ -126,7 +126,7 @@ exports.getModule = class UserConfigModule extends MenuModule { // 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) { @@ -155,7 +155,7 @@ exports.getModule = class UserConfigModule extends MenuModule { } const self = this; - const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); + const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); let currentThemeIdIndex = 0; async.series( @@ -164,7 +164,7 @@ exports.getModule = class UserConfigModule extends MenuModule { vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); }, function prepareAvailableThemes(callback) { - self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { + self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { return { themeId : themeId, name : t.info.name, @@ -173,11 +173,11 @@ exports.getModule = class UserConfigModule extends MenuModule { 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) { @@ -191,19 +191,19 @@ exports.getModule = class UserConfigModule extends MenuModule { 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); } ], diff --git a/core/user_group.js b/core/user_group.js index 3903f2c3..db444296 100644 --- a/core/user_group.js +++ b/core/user_group.js @@ -1,11 +1,10 @@ /* jslint node: true */ 'use strict'; -var userDb = require('./database.js').dbs.user; -var Config = require('./config.js').config; +const userDb = require('./database.js').dbs.user; -var async = require('async'); -var _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); exports.getGroupsForUser = getGroupsForUser; exports.addUserToGroup = addUserToGroup; @@ -13,23 +12,22 @@ exports.addUserToGroups = addUserToGroups; exports.removeUserFromGroup = removeUserFromGroup; function getGroupsForUser(userId, cb) { - var sql = - 'SELECT group_name ' + - 'FROM user_group_member ' + - 'WHERE user_id=?;'; + const sql = + `SELECT group_name + FROM user_group_member + WHERE user_id=?;`; - var groups = []; + const groups = []; - userDb.each(sql, [ userId ], function rowData(err, row) { + userDb.each(sql, [ userId ], (err, row) => { if(err) { - cb(err); - return; - } else { - groups.push(row.group_name); + return cb(err); } + + groups.push(row.group_name); }, - function complete() { - cb(null, groups); + () => { + return cb(null, groups); }); } @@ -40,31 +38,31 @@ function addUserToGroup(userId, groupName, transOrDb, cb) { } transOrDb.run( - 'REPLACE INTO user_group_member (group_name, user_id) ' + - 'VALUES(?, ?);', + `REPLACE INTO user_group_member (group_name, user_id) + VALUES(?, ?);`, [ groupName, userId ], - function complete(err) { - cb(err); + err => { + return cb(err); } ); } function addUserToGroups(userId, groups, transOrDb, cb) { - async.each(groups, function item(groupName, next) { - addUserToGroup(userId, groupName, transOrDb, next); - }, function complete(err) { - cb(err); + async.each(groups, (groupName, nextGroupName) => { + return addUserToGroup(userId, groupName, transOrDb, nextGroupName); + }, err => { + return cb(err); }); } function removeUserFromGroup(userId, groupName, cb) { userDb.run( - 'DELETE FROM user_group_member ' + - 'WHERE group_name=? AND user_id=?;', + `DELETE FROM user_group_member + WHERE group_name=? AND user_id=?;`, [ groupName, userId ], - function complete(err) { - cb(err); + err => { + return cb(err); } ); } diff --git a/core/user_list.js b/core/user_list.js index be85c586..30313a28 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -12,7 +12,7 @@ const _ = require('lodash'); /* Available listFormat/focusListFormat object members: - + userId : User ID userName : User name/handle lastLoginTs : Last login timestamp @@ -99,7 +99,7 @@ exports.getModule = class UserListModule extends MenuModule { userListView.redraw(); callback(null); } - ], + ], function complete(err) { if(err) { self.client.log.error( { error : err.toString() }, 'Error loading user list'); @@ -108,5 +108,5 @@ exports.getModule = class UserListModule extends MenuModule { } ); }); - } + } }; diff --git a/core/user_login.js b/core/user_login.js index 4bd9176c..37c3b306 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -39,8 +39,8 @@ function userLogin(client, username, password, cb) { if(existingClientConnection) { client.log.info( { - existingClientId : existingClientConnection.session.id, - username : user.username, + existingClientId : existingClientConnection.session.id, + username : user.username, userId : user.userId }, 'Already logged in' @@ -57,7 +57,7 @@ function userLogin(client, username, password, cb) { // 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'); + client.log.info('Successful login'); async.parallel( [ @@ -72,7 +72,7 @@ function userLogin(client, username, password, cb) { return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); }, function updateUserLoginCount(callback) { - return StatLog.incrementUserStat(user, 'login_count', 1, callback); + return StatLog.incrementUserStat(user, 'login_count', 1, callback); }, function recordLoginHistory(callback) { const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers diff --git a/core/uuid_util.js b/core/uuid_util.js index d8023f95..de64ec30 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -9,15 +9,15 @@ 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(key)) { key = new Buffer(key); } - + let digest = createHash('sha1').update( Buffer.concat( [ namespaceUuid, key ] )).digest(); @@ -31,8 +31,8 @@ function createNamedUUID(namespaceUuid, key) { u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 u[9] = digest[9]; - + digest.copy(u, 10, 10, 16); - + return u; } \ No newline at end of file diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 2cc2ad7a..847b9407 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -15,7 +15,7 @@ exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { options.cursor = options.cursor || 'hide'; options.justify = options.justify || 'right'; // :TODO: default to center - + MenuView.call(this, options); const self = this; @@ -80,7 +80,7 @@ function VerticalMenuView(options) { self.client.term.write( ansi.goto(item.row, self.position.col) + - sgr + + sgr + strUtil.pad(text, this.dimens.width, this.fillChar, this.justify) ); }; @@ -89,7 +89,7 @@ function VerticalMenuView(options) { util.inherits(VerticalMenuView, MenuView); VerticalMenuView.prototype.redraw = function() { - VerticalMenuView.super_.prototype.redraw.call(this); + VerticalMenuView.super_.prototype.redraw.call(this); // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such if(this.positionCacheExpired) { @@ -106,14 +106,14 @@ VerticalMenuView.prototype.redraw = function() { let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; let row = this.position.row + 1; const endRow = (row + this.oldDimens.height) - 2; - + while(row <= endRow) { seq += ansi.goto(row, this.position.col) + blank; row += 1; } this.client.term.write(seq); delete this.oldDimens; - } + } if(this.items.length) { let row = this.position.row; @@ -206,7 +206,7 @@ VerticalMenuView.prototype.removeItem = function(index) { VerticalMenuView.prototype.focusNext = function() { if(this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; - + this.viewWindow = { top : 0, bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 @@ -228,7 +228,7 @@ VerticalMenuView.prototype.focusNext = function() { VerticalMenuView.prototype.focusPrevious = function() { if(0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; - + this.viewWindow = { //top : this.items.length - this.maxVisibleItems, top : Math.max(this.items.length - this.maxVisibleItems, 0), @@ -279,7 +279,7 @@ VerticalMenuView.prototype.focusNextPageItem = function() { // // Jump to current + up to page size or bottom // If already at the bottom, jump to top - // + // if(this.items.length - 1 === this.focusedItemIndex) { return this.focusNext(); // will jump to top } diff --git a/core/view.js b/core/view.js index fccca541..1829f26d 100644 --- a/core/view.js +++ b/core/view.js @@ -39,7 +39,7 @@ function View(options) { var self = this; this.client = options.client; - + this.cursor = options.cursor || 'show'; this.cursorStyle = options.cursorStyle || 'default'; @@ -72,7 +72,7 @@ function View(options) { } else { this.dimens = { width : options.width || 0, - height : 0 + height : 0 }; } @@ -106,7 +106,7 @@ function View(options) { this.restoreCursor = function() { //this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); - }; + }; } util.inherits(View, events.EventEmitter); @@ -150,7 +150,7 @@ View.prototype.setDimension = function(dimens) { View.prototype.setHeight = function(height) { height = parseInt(height) || 1; - height = Math.min(height, this.client.term.termHeight); + height = Math.min(height, this.client.term.termHeight); this.dimens.height = height; this.autoScale.height = false; @@ -182,9 +182,9 @@ View.prototype.setPropertyValue = function(propName, value) { case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; case 'focus' : this.setFocus(value); break; - - case 'text' : - if('setText' in this) { + + case 'text' : + if('setText' in this) { this.setText(value); } break; @@ -248,7 +248,7 @@ View.prototype.setFocus = function(focused) { this.restoreCursor(); }; -View.prototype.onKeyPress = function(ch, key) { +View.prototype.onKeyPress = function(ch, key) { enigAssert(this.hasFocus, 'View does not have focus'); enigAssert(this.acceptsInput, 'View does not accept input'); diff --git a/core/view_controller.js b/core/view_controller.js index ecf36be5..55b1bd1c 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -22,7 +22,7 @@ var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; function ViewController(options) { assert(_.isObject(options)); assert(_.isObject(options.client)); - + events.EventEmitter.call(this); var self = this; @@ -54,14 +54,14 @@ function ViewController(options) { self.client.log.warn( { err : err }, 'Error during handleAction()'); } } - + self.waitActionCompletion = false; }); }; this.clientKeyPressHandler = function(ch, key) { // - // Process key presses treating form submit mapped keys special. + // Process key presses treating form submit mapped keys special. // Everything else is forwarded on to the focused View, if any. // var actionForKey = key ? self.actionKeyMap[key.name] : self.actionKeyMap[ch]; @@ -92,7 +92,7 @@ function ViewController(options) { self.nextFocus(); break; - case 'accept' : + case 'accept' : if(self.focusedView && self.focusedView.submit) { // :TODO: need to do validation here!!! var focusedView = self.focusedView; @@ -166,37 +166,23 @@ function ViewController(options) { var propAsset; var propValue; - function callModuleMethod(path) { - if('' === paths.extname(path)) { - path += '.js'; - } - - try { - var methodMod = require(path); - // :TODO: fix formData & extraArgs - return methodMod[propAsset.asset](self.client.currentMenuModule, {}, {} ); - } catch(e) { - self.client.log.error( { error : e.toString(), methodName : propAsset.asset }, 'Failed to execute asset method'); - } - } - - for(var propName in conf) { + for(var propName in conf) { propAsset = asset.getViewPropertyAsset(conf[propName]); if(propAsset) { switch(propAsset.type) { case 'config' : - propValue = asset.resolveConfigAsset(conf[propName]); + propValue = asset.resolveConfigAsset(conf[propName]); break; - + case 'sysStat' : propValue = asset.resolveSystemStatAsset(conf[propName]); break; // :TODO: handle @art (e.g. text : @art ...) - case 'method' : + case 'method' : case 'systemMethod' : - if('validate' === propName) { + if('validate' === propName) { // :TODO: handle propAsset.location for @method script specification if('systemMethod' === propAsset.type) { // :TODO: implementation validation @systemMethod handling! @@ -211,7 +197,7 @@ function ViewController(options) { } } else { if(_.isString(propAsset.location)) { - + // :TODO: clean this code up! } else { if('systemMethod' === propAsset.type) { // :TODO: @@ -227,7 +213,7 @@ function ViewController(options) { } break; - default : + default : propValue = propValue = conf[propName]; break; } @@ -238,7 +224,7 @@ function ViewController(options) { if(!_.isUndefined(propValue)) { view.setPropertyValue(propName, propValue); } - } + } }; this.applyViewConfig = function(config, cb) { @@ -251,7 +237,7 @@ function ViewController(options) { if(null === mciMatch) { self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); return; - } + } var viewId = parseInt(mciMatch[2]); assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used @@ -261,7 +247,7 @@ function ViewController(options) { } var view = self.getView(viewId); - + if(!view) { self.client.log.warn( { viewId : viewId }, 'Cannot find view'); nextItem(null); @@ -278,7 +264,7 @@ function ViewController(options) { nextItem(null); }, - function complete(err) { + function complete(err) { // default to highest ID if no 'submit' entry present if(!submitId) { var highestIdView = self.getView(highestId); @@ -310,7 +296,7 @@ function ViewController(options) { if(_.isUndefined(actionValue)) { return false; } - + if(_.isNumber(actionValue) || _.isString(actionValue)) { if(_.isUndefined(formValue[actionValue])) { return false; @@ -337,10 +323,10 @@ function ViewController(options) { } self.client.log.trace( - { + { formValue : formValue, actionValue : actionValue - }, + }, 'Action match' ); @@ -412,7 +398,7 @@ ViewController.prototype.detachClientEvents = function() { if(!this.attached) { return; } - + this.client.removeListener('key press', this.clientKeyPressHandler); for(var id in this.views) { @@ -465,7 +451,7 @@ ViewController.prototype.switchFocus = function(id) { self.validateView(focusedView, function validated(err, newFocusedViewId) { if(err) { - var newFocusedView = self.getView(newFocusedViewId) || focusedView; + var newFocusedView = self.getView(newFocusedViewId) || focusedView; self.setViewFocusWithEvents(newFocusedView, true); } else { self.attachClientEvents(); @@ -524,7 +510,7 @@ ViewController.prototype.setViewOrder = function(order) { ViewController.prototype.redrawAll = function(initialFocusId) { this.client.term.rawWrite(ansi.hideCursor()); - + for(var id in this.views) { if(initialFocusId === id) { continue; // will draw @ focus @@ -538,7 +524,7 @@ ViewController.prototype.redrawAll = function(initialFocusId) { ViewController.prototype.loadFromPromptConfig = function(options, cb) { assert(_.isObject(options)); assert(_.isObject(options.mciMap)); - + var self = this; var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; var initialFocusId = 1; // default to first @@ -560,7 +546,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { callback(null); } }, - function prepareFormSubmission(callback) { + function prepareFormSubmission(callback) { if(false === self.noInput) { self.on('submit', function promptSubmit(formData) { @@ -610,7 +596,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { // // * '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. + // of the specified view. // * If 'action' is present, that action will be procesed when // triggered by key(s) // @@ -681,7 +667,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { callback(err); }); }, - /* + /* function applyThemeCustomization(callback) { formConfig = formConfig || {}; formConfig.mci = formConfig.mci || {}; @@ -701,7 +687,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { //console.log('after theme...') //console.log(self.client.currentMenuModule.menuConfig.config) - + callback(null); }, */ @@ -764,7 +750,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { // // * '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. + // of the specified view. // * If 'action' is present, that action will be procesed when // triggered by key(s) // @@ -807,7 +793,7 @@ ViewController.prototype.formatMCIString = function(format) { return format.replace(/{(\d+)}/g, function replacer(match, number) { view = self.getView(number); - + if(!view) { return match; } diff --git a/core/web_password_reset.js b/core/web_password_reset.js index c2d6852d..6ea1e6f8 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -13,13 +13,12 @@ const Log = require('./logger.js').log; // deps const async = require('async'); -const _ = require('lodash'); const crypto = require('crypto'); const fs = require('graceful-fs'); const url = require('url'); const querystring = require('querystring'); -const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = +const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = `%USERNAME%: a password reset has been requested for your account on %BOARDNAME%. @@ -46,7 +45,7 @@ class WebPasswordReset { } async.waterfall( - [ + [ function getEmailAddress(callback) { if(!username) { return callback(Errors.MissingParam('Missing "username"')); @@ -81,7 +80,7 @@ class WebPasswordReset { email_password_reset_token : token, email_password_reset_token_ts : getISOTimestampString(), }; - + // we simply place the reset token in the user's properties user.persistProperties(newProperties, err => { return callback(err, user); @@ -111,13 +110,13 @@ class WebPasswordReset { .replace(/%USERNAME%/g, user.username) .replace(/%TOKEN%/g, user.properties.email_password_reset_token) .replace(/%RESET_URL%/g, resetUrl) - ; + ; } textTemplate = replaceTokens(textTemplate); if(htmlTemplate) { htmlTemplate = replaceTokens(htmlTemplate); - } + } const message = { to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, @@ -128,7 +127,11 @@ class WebPasswordReset { }; sendMail(message, (err, info) => { - // :TODO: Log me! + if(err) { + Log.warn( { error : err.message }, 'Failed sending password reset email' ); + } else { + Log.debug( { info : info }, 'Successfully sent password reset email'); + } return callback(err); }); @@ -162,7 +165,7 @@ class WebPasswordReset { path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate handler : WebPasswordReset.routeResetPasswordGet, }, - // POST handler for performing the actual reset + // POST handler for performing the actual reset { method : 'POST', path : '^\\/reset_password$', diff --git a/core/whos_online.js b/core/whos_online.js index 6abd76ef..f832bc10 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -53,7 +53,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule { const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); - + onlineListView.setItems(_.map(onlineList, oe => { if(oe.authenticated) { oe.timeOn = _.upperFirst(oe.timeOn.humanize()); diff --git a/core/word_wrap.js b/core/word_wrap.js index 0a4b122d..f246ad23 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,29 +1,31 @@ /* jslint node: true */ 'use strict'; -var assert = require('assert'); -var _ = require('lodash'); -const renderStringLength = require('./string_util.js').renderStringLength; +const renderStringLength = require('./string_util.js').renderStringLength; -exports.wordWrapText = wordWrapText2; +// deps +const assert = require('assert'); +const _ = require('lodash'); + +exports.wordWrapText = wordWrapText; const SPACE_CHARS = [ - ' ', '\f', '\n', '\r', '\v', + ' ', '\f', '\n', '\r', '\v', '​\u00a0', '\u1680', '​\u180e', '\u2000​', '\u2001', '\u2002', '​\u2003', '\u2004', - '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', + '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', '\u202f', '\u205f​', '\u3000', ]; -const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); +const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); -function wordWrapText2(text, options) { +function wordWrapText(text, options) { assert(_.isObject(options)); assert(_.isNumber(options.width)); - + options.tabHandling = options.tabHandling || 'expand'; options.tabWidth = options.tabWidth || 4; - options.tabChar = options.tabChar || ' '; - + options.tabChar = options.tabChar || ' '; + //const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g'); // // For a given word, match 0->options.width chars -- alwasy include a full trailing ESC @@ -31,7 +33,7 @@ function wordWrapText2(text, options) { // // :TODO: Need to create ansi.getMatchRegex or something - this is used all over const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}\\x1b\\[[\\?=;0-9]*[ABCDEFGHJKLMSTfhlmnprsu]|.{0,${options.width}}`, 'g'); - + let m; let word; let c; @@ -39,30 +41,30 @@ function wordWrapText2(text, options) { let i = 0; let wordStart = 0; let result = { wrapped : [ '' ], renderLen : [] }; - + function expandTab(column) { const remainWidth = options.tabWidth - (column % options.tabWidth); return new Array(remainWidth).join(options.tabChar); } - + function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { renderLen = renderStringLength(w); - + if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { result.firstWrapRange = { start : wordStart, end : wordStart + w.length }; } - + result.wrapped[++i] = w; result.renderLen[i] = renderLen; } else { - result.wrapped[i] += w; - result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; + result.wrapped[i] += w; + result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; } }); } - + // // Some of the way we word wrap is modeled after Sublime Test 3: // @@ -74,10 +76,10 @@ function wordWrapText2(text, options) { // "\t" may resolve to " " and must fit within the space. // // * If a word is ultimately too long to fit, break it up until it does. - // + // while(null !== (m = REGEXP_WORD_WRAP.exec(text))) { word = text.substring(wordStart, REGEXP_WORD_WRAP.lastIndex - 1); - + c = m[0].charAt(0); if(SPACE_CHARS.indexOf(c) > -1) { word += m[0]; @@ -89,129 +91,13 @@ function wordWrapText2(text, options) { word += m[0]; } } - + appendWord(); wordStart = REGEXP_WORD_WRAP.lastIndex + m[0].length - 1; } - + word = text.substring(wordStart); appendWord(); - + return result; } - -function wordWrapText(text, options) { - // - // options.*: - // width : word wrap width - // tabHandling : expand (default=expand) - // tabWidth : tab width if tabHandling is 'expand' (default=4) - // tabChar : character to use for tab expansion - // - assert(_.isObject(options), 'Missing options!'); - assert(_.isNumber(options.width), 'Missing options.width!'); - - options.tabHandling = options.tabHandling || 'expand'; - - if(!_.isNumber(options.tabWidth)) { - options.tabWidth = 4; - } - - options.tabChar = options.tabChar || ' '; - - // - // Notes - // * Sublime Text 3 for example considers spaces after a word - // part of said word. For example, "word " would be wraped - // in it's entirity. - // - // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. - // "\t" may resolve to " " and must fit within the space. - // - // * If a word is ultimately too long to fit, break it up until it does. - // - // RegExp below is JavaScript '\s' minus the '\t' - // - var re = new RegExp( - '\t|[ \f\n\r\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004\u2005\u2006​' + - '\u2007\u2008​\u2009\u200a​\u2028\u2029​\u202f\u205f​\u3000]', 'g'); - var m; - var wordStart = 0; - var results = { wrapped : [ '' ] }; - var i = 0; - var word; - var wordLen; - - function expandTab(col) { - var remainWidth = options.tabWidth - (col % options.tabWidth); - return new Array(remainWidth).join(options.tabChar); - } - - // :TODO: support wrapping pipe code text (e.g. ignore color codes, expand MCI codes) - - function addWord() { - word.match(new RegExp('.{0,' + options.width + '}', 'g')).forEach(function wrd(w) { - //wordLen = self.getStringLength(w); - - if(results.wrapped[i].length + w.length > options.width) { - //if(results.wrapped[i].length + wordLen > width) { - if(0 === i) { - results.firstWrapRange = { start : wordStart, end : wordStart + w.length }; - //results.firstWrapRange = { start : wordStart, end : wordStart + wordLen }; - } - // :TODO: Must handle len of |w| itself > options.width & split how ever many times required (e.g. handle paste) - results.wrapped[++i] = w; - } else { - results.wrapped[i] += w; - } - }); - } - - while((m = re.exec(text)) !== null) { - word = text.substring(wordStart, re.lastIndex - 1); - - switch(m[0].charAt(0)) { - case ' ' : - word += m[0]; - break; - - case '\t' : - // - // Expand tab given position - // - // Nice info here: http://c-for-dummies.com/blog/?p=424 - // - if('expand' === options.tabHandling) { - word += expandTab(results.wrapped[i].length + word.length) + options.tabChar; - } else { - word += m[0]; - } - break; - } - - addWord(); - wordStart = re.lastIndex + m[0].length - 1; - } - - // - // Remainder - // - word = text.substring(wordStart); - addWord(); - - return results; -} - -//const input = 'Hello, |04World! This |08i|02s a test it is \x1b[20Conly a test of the emergency broadcast system. What you see is not a joke!'; -//const input = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five enturies, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; - -/* -const iconv = require('iconv-lite'); -const input = iconv.decode(require('graceful-fs').readFileSync('/home/nuskooler/Downloads/msg_out.txt'), 'cp437'); - -const opts = { - width : 80, -}; - -console.log(wordWrapText2(input, opts).wrapped, 'utf8') -*/ \ No newline at end of file From a8d5e8477982ace10a8e0047f0244aeab605439d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 16:08:35 -0700 Subject: [PATCH 0123/1013] * Fix justification 'right' vs 'left': They were flipped (durp!). Right aligned is now really that, etc. You may need to update your theme.hjson/similar! --- art/themes/luciano_blocktronics/theme.hjson | 14 ++++++------ core/string_format.js | 6 +++--- core/string_util.js | 24 ++++++++++----------- core/text_view.js | 2 +- core/vertical_menu_view.js | 2 +- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 19f63194..433f8884 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -722,15 +722,15 @@ } SM4: { width: 14 - justify: right + justify: left } SM5: { width: 14 - justify: right + justify: left } SM6: { width: 14 - justify: right + justify: left } BT7: { focusTextStyle: first lower @@ -748,15 +748,15 @@ } SM3: { width: 14 - justify: right + justify: left } SM4: { width: 14 - justify: right + justify: left } SM5: { width: 14 - justify: right + justify: left } ET6: { width: 26 @@ -826,7 +826,7 @@ mci: { SM1: { width: 14 - justify: right + justify: left focusTextStyle: first lower } diff --git a/core/string_format.js b/core/string_format.js index eba715d5..38c2047f 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -122,10 +122,10 @@ function quote(s) { function getPadAlign(align) { return { - '<' : 'right', - '>' : 'left', + '<' : 'left', + '>' : 'right', '^' : 'center', - }[align] || '<'; + }[align] || '>'; } function formatString(value, tokens) { diff --git a/core/string_util.js b/core/string_util.js index a4544754..c846fc38 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -135,23 +135,21 @@ function stylizeString(s, style) { return s; } -// Based on http://www.webtoolkit.info/ -// :TODO: Look into lodash padLeft, padRight, etc. -function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { - len = miscUtil.valueWithDefault(len, 0); - padChar = miscUtil.valueWithDefault(padChar, ' '); - dir = miscUtil.valueWithDefault(dir, 'right'); - stringSGR = miscUtil.valueWithDefault(stringSGR, ''); - padSGR = miscUtil.valueWithDefault(padSGR, ''); - useRenderLen = miscUtil.valueWithDefault(useRenderLen, true); +function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen) { + len = len || 0; + padChar = padChar || ' '; + justify = justify || 'left'; + stringSGR = stringSGR || ''; + padSGR = padSGR || ''; + useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; const renderLen = useRenderLen ? renderStringLength(s) : s.length; const padlen = len >= renderLen ? len - renderLen : 0; - switch(dir) { + switch(justify) { case 'L' : case 'left' : - s = padSGR + new Array(padlen).join(padChar) + stringSGR + s; + s = `${stringSGR}${s}${padSGR}${Array(padlen).join(padChar)}`; break; case 'C' : @@ -160,13 +158,13 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { { const right = Math.ceil(padlen / 2); const left = padlen - right; - s = padSGR + new Array(left + 1).join(padChar) + stringSGR + s + padSGR + new Array(right + 1).join(padChar); + s = `${padSGR}${Array(left + 1).join(padChar)}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; } break; case 'R' : case 'right' : - s = stringSGR + s + padSGR + new Array(padlen).join(padChar); + s = `${padSGR}${Array(padlen).join(padChar)}${stringSGR}${s}`; break; default : break; diff --git a/core/text_view.js b/core/text_view.js index 8bd38213..ea9c352a 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -32,7 +32,7 @@ function TextView(options) { } this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'right'; + this.justify = options.justify || 'left'; this.resizable = miscUtil.valueWithDefault(options.resizable, true); this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 847b9407..9fe4cc7d 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -14,7 +14,7 @@ exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { options.cursor = options.cursor || 'hide'; - options.justify = options.justify || 'right'; // :TODO: default to center + options.justify = options.justify || 'left'; MenuView.call(this, options); From d1593ed159316a26b9df8768a100f8485e0449da Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 20:30:55 -0700 Subject: [PATCH 0124/1013] * Fix bug where 'submit' property was ignored in favor of highest MCI ID always; Will now properly set view with 'submit' to true else rely on highest ID --- core/view_controller.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/core/view_controller.js b/core/view_controller.js index 55b1bd1c..71911f28 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -228,25 +228,25 @@ function ViewController(options) { }; this.applyViewConfig = function(config, cb) { - var highestId = 1; - var submitId; - var initialFocusId = 1; + let highestId = 1; + let submitId; + let initialFocusId = 1; async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) { - var mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? + const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? if(null === mciMatch) { self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); return; } - var viewId = parseInt(mciMatch[2]); + const viewId = parseInt(mciMatch[2]); assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used if(viewId > highestId) { highestId = viewId; } - var view = self.getView(viewId); + const view = self.getView(viewId); if(!view) { self.client.log.warn( { viewId : viewId }, 'Cannot find view'); @@ -254,7 +254,7 @@ function ViewController(options) { return; } - var mciConf = config.mci[mci]; + const mciConf = config.mci[mci]; self.setViewPropertiesFromMCIConf(view, mciConf); @@ -262,9 +262,13 @@ function ViewController(options) { initialFocusId = viewId; } + if(true === view.submit) { + submitId = viewId; + } + nextItem(null); }, - function complete(err) { + err => { // default to highest ID if no 'submit' entry present if(!submitId) { var highestIdView = self.getView(highestId); @@ -275,7 +279,7 @@ function ViewController(options) { } } - cb(err, { initialFocusId : initialFocusId } ); + return cb(err, { initialFocusId : initialFocusId } ); }); }; From 827d793a2dae8ef5db725af4ac57017330443c20 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 20:31:37 -0700 Subject: [PATCH 0125/1013] Add notes about left/right justify --- UPGRADE.md | 5 +++++ WHATSNEW.md | 1 + 2 files changed, 6 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index c7651fcd..5975b96e 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -37,6 +37,11 @@ npm install 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.8-alpha to 0.0.9-alpha +* Development is now against Node.js 8.x LTS. Follow your standard upgrade path to update to Node 8.x before using 0.0.9-alpha. +* The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. + + # 0.0.7-alpha to 0.0.8-alpha ENiGMA 0.0.8-alpha comes with some structure changes: * Configuration files are defaulted to `./config`. Related, the `--config` option now points to a configuration **directory** diff --git a/WHATSNEW.md b/WHATSNEW.md index 594418a7..d5d5fee2 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -3,6 +3,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.9-alpha * Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! +* Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!) ## 0.0.8-alpha From 05a93cae891157d26aa72ff0fb58242ef37e6472 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 20:31:55 -0700 Subject: [PATCH 0126/1013] Default to left justification --- core/spinner_menu_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index e9145662..72b8b2f7 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -11,7 +11,7 @@ const assert = require('assert'); exports.SpinnerMenuView = SpinnerMenuView; function SpinnerMenuView(options) { - options.justify = options.justify || 'center'; + options.justify = options.justify || 'left'; options.cursor = options.cursor || 'hide'; MenuView.call(this, options); From 23e77dcb319e50c16a6d821c03f12d13d8eb8749 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 21:05:55 -0700 Subject: [PATCH 0127/1013] Uncommeng out a deprecated function - will fix later; need for now --- core/msg_area_list.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 96b0a51c..fcc44b23 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -99,8 +99,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { } // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! - /* - updateGeneralAreaInfoViews(areaIndex) { + updateGeneralAreaInfoViews(areaIndex) { + /* const areaInfo = self.messageAreas[areaIndex]; [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { @@ -109,8 +109,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { v.setFormatObject(areaInfo.area); } }); + */ } - */ mciReady(mciData, cb) { super.mciReady(mciData, err => { From 78ca1e9c4f8dedfd16d0b0577bbb02889ed202de Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 21:06:16 -0700 Subject: [PATCH 0128/1013] * Ensure explicit by-MCI key forms are properly themed, e.g. form: { 3: { HM1: { ... }}} --- core/theme.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/theme.js b/core/theme.js index a1045a18..5c056c49 100644 --- a/core/theme.js +++ b/core/theme.js @@ -122,7 +122,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // Create a *clone* of menuConfig (menu.hjson) then bring in // promptConfig (prompt.hjson) // - var mergedTheme = _.cloneDeep(menuConfig); + const mergedTheme = _.cloneDeep(menuConfig); if(_.isObject(promptConfig.prompts)) { mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); @@ -137,7 +137,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // merge customizer to disallow immutable MCI properties // - var mciCustomizer = function(objVal, srcVal, key) { + const mciCustomizer = function(objVal, srcVal, key) { return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; }; @@ -191,30 +191,30 @@ function getMergedTheme(menuConfig, promptConfig, theme) { applyThemeMciBlock(form.mci, menuTheme, formKey); } else { - var menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { + const menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { return k === k.toUpperCase(); // remove anything not uppercase }); menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - var applyFrom; + let applyFrom; if(_.has(menuTheme, [ mciKey, 'mci' ])) { applyFrom = menuTheme[mciKey]; } else { applyFrom = menuTheme; } - applyThemeMciBlock(form[mciKey].mci, applyFrom); + applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey); }); } } [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { - var createdFormSection = false; - var mergedThemeMenu = mergedTheme[sectionName][menuName]; + let createdFormSection = false; + const mergedThemeMenu = mergedTheme[sectionName][menuName]; if(_.has(theme, [ 'customization', sectionName, menuName ])) { - var menuTheme = theme.customization[sectionName][menuName]; + const menuTheme = theme.customization[sectionName][menuName]; // config block is direct assign/overwrite // :TODO: should probably be _.merge() From 16c8fd0afc57e2bd876e06df198e9807ee9f39ed Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 21:40:13 -0700 Subject: [PATCH 0129/1013] Fix focusTextStyle for VerticalMenuView (lightbar) --- core/vertical_menu_view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 9fe4cc7d..a7e566eb 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -68,11 +68,11 @@ function VerticalMenuView(options) { const focusItem = self.focusItems[index]; text = strUtil.stylizeString( focusItem ? focusItem.text : item.text, - self.textStyle + self.focusTextStyle ); sgr = ''; } else { - text = strUtil.stylizeString(item.text, self.textStyle); + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } From b1cea5edd72868771e30aed37b729863501022af Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 15:16:10 -0700 Subject: [PATCH 0130/1013] Add in reason if available, to error message --- core/enig_error.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/enig_error.js b/core/enig_error.js index b0dd2335..c6eb8097 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -11,6 +11,10 @@ class EnigError extends Error { this.reason = reason; this.reasonCode = reasonCode; + if(this.reason) { + this.message += `: ${this.reason}`; + } + if(typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); } else { From afe0c88cfc14a515ed1c3aee7efd73910b0056ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 15:16:35 -0700 Subject: [PATCH 0131/1013] NetMail non-HUB fixes * Properly separate FTN *packet* header vs *message* header DST/SRC information * Change routes{} handling: These are now *require* for out-of-HUB routing such that Enig will know where to send messages --- core/ftn_mail_packet.js | 97 ++++++++++++++++-------------- core/message.js | 8 +++ core/scanner_tossers/ftn_bso.js | 101 ++++++++++++++++---------------- 3 files changed, 113 insertions(+), 93 deletions(-) diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index d2f003e9..1c60e1f0 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -524,13 +524,13 @@ function Packet(options) { ); }; - this.parsePacketMessages = function(packetBuffer, iterator, cb) { + this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { binary.parse(packetBuffer) .word16lu('messageType') - .word16lu('ftn_orig_node') - .word16lu('ftn_dest_node') - .word16lu('ftn_orig_network') - .word16lu('ftn_dest_network') + .word16lu('ftn_msg_orig_node') + .word16lu('ftn_msg_dest_node') + .word16lu('ftn_msg_orig_net') + .word16lu('ftn_msg_dest_net') .word16lu('ftn_attr_flags') .word16lu('ftn_cost') .scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max @@ -569,20 +569,28 @@ function Packet(options) { // contain an origin line, kludges, SAUCE in the case // of ANSI files, etc. // - let msg = new Message( { + const msg = new Message( { toUserName : convMsgData.toUserName, fromUserName : convMsgData.fromUserName, subject : convMsgData.subject, modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime), }); - msg.meta.FtnProperty = {}; - msg.meta.FtnProperty.ftn_orig_node = msgData.ftn_orig_node; - msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node; - msg.meta.FtnProperty.ftn_orig_network = msgData.ftn_orig_network; - msg.meta.FtnProperty.ftn_dest_network = msgData.ftn_dest_network; - msg.meta.FtnProperty.ftn_attr_flags = msgData.ftn_attr_flags; - msg.meta.FtnProperty.ftn_cost = msgData.ftn_cost; + // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) + msg.meta.FtnProperty = { + ftn_orig_node : header.origNode, + ftn_dest_node : header.destNode, + ftn_orig_network : header.origNet, + ftn_dest_network : header.destNet, + + ftn_attr_flags : msgData.ftn_attr_flags, + ftn_cost : msgData.ftn_cost, + + ftn_msg_orig_node : msgData.ftn_msg_orig_node, + ftn_msg_dest_node : msgData.ftn_msg_dest_node, + ftn_msg_orig_net : msgData.ftn_msg_orig_net, + ftn_msg_dest_net : msgData.ftn_msg_dest_net, + }; self.processMessageBody(msgData.message, messageBodyData => { msg.message = messageBodyData.message; @@ -622,11 +630,11 @@ function Packet(options) { const nextBuf = packetBuffer.slice(read); if(nextBuf.length > 0) { - let next = function(e) { + const next = function(e) { if(e) { cb(e); } else { - self.parsePacketMessages(nextBuf, iterator, cb); + self.parsePacketMessages(header, nextBuf, iterator, cb); } }; @@ -651,6 +659,10 @@ function Packet(options) { Message.FtnPropertyNames.FtnOrigPoint, Message.FtnPropertyNames.FtnDestPoint, Message.FtnPropertyNames.FtnAttribute, + Message.FtnPropertyNames.FtnMsgOrigNode, + Message.FtnPropertyNames.FtnMsgDestNode, + Message.FtnPropertyNames.FtnMsgOrigNet, + Message.FtnPropertyNames.FtnMsgDestNet, ].forEach( propName => { if(message.meta.FtnProperty[propName]) { message.meta.FtnProperty[propName] = parseInt(message.meta.FtnProperty[propName]) || 0; @@ -658,6 +670,25 @@ function Packet(options) { }); }; + this.writeMessageHeader = function(message, buf) { + // ensure address FtnProperties are numbers + self.sanatizeFtnProperties(message); + + const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; + const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; + + buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); + buf.writeUInt16LE(destNode, 4); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); + buf.writeUInt16LE(destNet, 8); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); + + const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); + dateTimeBuffer.copy(buf, 14); + }; + this.getMessageEntryBuffer = function(message, options, cb) { function getAppendMeta(k, m, sepChar=':') { @@ -678,20 +709,7 @@ function Packet(options) { [ function prepareHeaderAndKludges(callback) { const basicHeader = new Buffer(34); - - // ensure address FtnProperties are numbers - self.sanatizeFtnProperties(message); - - basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); - - const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); - dateTimeBuffer.copy(basicHeader, 14); + self.writeMessageHeader(message, basicHeader); // // To, from, and subject must be NULL term'd and have max lengths as per spec. @@ -808,17 +826,7 @@ function Packet(options) { this.writeMessage = function(message, ws, options) { let basicHeader = new Buffer(34); - - basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); - - const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); - dateTimeBuffer.copy(basicHeader, 14); + self.writeMessageHeader(message, basicHeader); ws.write(basicHeader); @@ -911,7 +919,7 @@ function Packet(options) { }; this.parsePacketBuffer = function(packetBuffer, iterator, cb) { - async.series( + async.waterfall( [ function processHeader(callback) { self.parsePacketHeader(packetBuffer, (err, header) => { @@ -919,15 +927,16 @@ function Packet(options) { return callback(err); } - let next = function(e) { - callback(e); + const next = function(e) { + return callback(e, header); }; iterator('header', header, next); }); }, - function processMessages(callback) { + function processMessages(header, callback) { self.parsePacketMessages( + header, packetBuffer.slice(FTN_PACKET_HEADER_SIZE), iterator, callback); diff --git a/core/message.js b/core/message.js index 7cac5c79..f99efa8a 100644 --- a/core/message.js +++ b/core/message.js @@ -116,8 +116,10 @@ Message.StateFlags0 = { }; Message.FtnPropertyNames = { + // packet header oriented FtnOrigNode : 'ftn_orig_node', FtnDestNode : 'ftn_dest_node', + // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping FtnOrigNetwork : 'ftn_orig_network', FtnDestNetwork : 'ftn_dest_network', FtnAttrFlags : 'ftn_attr_flags', @@ -127,6 +129,12 @@ Message.FtnPropertyNames = { FtnOrigPoint : 'ftn_orig_point', FtnDestPoint : 'ftn_dest_point', + // message header oriented + FtnMsgOrigNode : 'ftn_msg_orig_node', + FtnMsgDestNode : 'ftn_msg_dest_node', + FtnMsgOrigNet : 'ftn_msg_orig_net', + FtnMsgDestNet : 'ftn_msg_dest_net', + FtnAttribute : 'ftn_attribute', FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 20ab435f..36ee815f 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -306,12 +306,26 @@ function FTNMessageScanTossModule() { // const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version + // :TODO: create Address.toMeta() / similar message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - message.meta.FtnProperty.ftn_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_orig_network = localAddress.net; - message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_orig_network = localAddress.net; + message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; + + const destAddress = options.routeAddress || options.destAddress; + message.meta.FtnProperty.ftn_dest_node = destAddress.node; + message.meta.FtnProperty.ftn_dest_network = destAddress.net; + + if(destAddress.zone) { + message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; + } + if(destAddress.point) { + message.meta.FtnProperty.ftn_dest_point = destAddress.point; + } // tear line and origin can both go in EchoMail & NetMail message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); @@ -320,9 +334,11 @@ function FTNMessageScanTossModule() { let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system if(self.isNetMailMessage(message)) { - // These should be set for Private/NetMail already - assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_node))); - assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_network))); + // + // Set route and message destination properties -- they may differ + // + message.meta.FtnProperty.ftn_msg_dest_node = options.destAddress.node; + message.meta.FtnProperty.ftn_msg_dest_net = options.destAddress.net; ftnAttribute |= ftnMailPacket.Packet.Attribute.Private; @@ -353,10 +369,6 @@ function FTNMessageScanTossModule() { message.meta.FtnKludge.TOPT = options.destAddress.point; } } else { - // We need to set some destination info for EchoMail - message.meta.FtnProperty.ftn_dest_node = options.destAddress.node; - message.meta.FtnProperty.ftn_dest_network = options.destAddress.net; - // // Set appropriate attribute flag for export type // @@ -573,7 +585,7 @@ function FTNMessageScanTossModule() { const packetHeader = new ftnMailPacket.PacketHeader( exportOpts.network.localAddress, - exportOpts.destAddress, + exportOpts.routeAddress, exportOpts.nodeConfig.packetType ); @@ -801,57 +813,44 @@ function FTNMessageScanTossModule() { return _.find(routes, (route, addrWildcard) => { return dstAddr.isPatternMatch(addrWildcard); }); - - /* - const route = _.find(routes, (route, addrWildcard) => { - return dstAddr.isPatternMatch(addrWildcard); - }); - - if(route && route.address) { - return Address.fromString(route.address); - } - */ }; - this.getAcceptableNetMailNetworkInfoFromAddress = function(dstAddr, cb) { + this.getNetMailRouteInfoFromAddress = function(destAddress, cb) { // - // Attempt to find an acceptable network configuration using the following - // lookup order (most to least explicit config): + // Attempt to find route information for |destAddress|: // // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config - // - Where we send may not be where dstAddress is (it's routed!) + // - Where we send may not be where destAddress is (it's routed!) // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config - // - Where we send is direct to dstAddr + // - Where we send is direct to destAddress // // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address // falling back to Config.scannerTossers.ftn_bso.defaultNetwork // - const route = this.getNetMailRoute(dstAddr); + const route = this.getNetMailRoute(destAddress); let routeAddress; let networkName; + let isRouted; if(route) { routeAddress = Address.fromString(route.address); networkName = route.network; + isRouted = true; } else { - routeAddress = dstAddr; + routeAddress = destAddress; + isRouted = false; } - networkName = networkName || - this.getNetworkNameByAddressPattern(`${routeAddress.zone}:${routeAddress.net}/*`) || - Config.scannerTossers.ftn_bso.defaultNetwork - ; + networkName = networkName || this.getNetworkNameByAddress(routeAddress); const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return routeAddress.isPatternMatch(nodeAddrWildcard); - }) || { - packetType : '2+', - encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding, - }; + }) || { packetType : '2+', encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding }; + // we should never be failing here; we may just be using defaults. return cb( - config ? null : Errors.DoesNotExist(`No configuration found for ${dstAddr.toString()}`), - config, routeAddress, networkName + networkName ? null : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), + { destAddress, routeAddress, networkName, config, isRouted } ); }; @@ -876,21 +875,22 @@ function FTNMessageScanTossModule() { function discoverUplink(callback) { const dstAddr = new Address(message.meta.System[Message.SystemMetaNames.RemoteToUser]); - return self.getAcceptableNetMailNetworkInfoFromAddress(dstAddr, (err, config, routeAddress, networkName) => { + self.getNetMailRouteInfoFromAddress(dstAddr, (err, routeInfo) => { if(err) { return callback(err); } - exportOpts.nodeConfig = config; - exportOpts.destAddress = routeAddress; - exportOpts.fileCase = config.fileCase || 'lower'; - exportOpts.network = Config.messageNetworks.ftn.networks[networkName]; - exportOpts.networkName = networkName; + exportOpts.nodeConfig = routeInfo.config; + exportOpts.destAddress = dstAddr; + exportOpts.routeAddress = routeInfo.routeAddress; + exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; + exportOpts.network = Config.messageNetworks.ftn.networks[routeInfo.networkName]; + exportOpts.networkName = routeInfo.networkName; exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); - exportOpts.exportType = self.getExportType(config); + exportOpts.exportType = self.getExportType(routeInfo.config); if(!exportOpts.network) { - return callback(Errors.DoesNotExist(`No configuration found for network ${networkName}`)); + return callback(Errors.DoesNotExist(`No configuration found for network ${routeInfo.networkName}`)); } return callback(null); @@ -937,12 +937,15 @@ function FTNMessageScanTossModule() { ], err => { if(err) { - Log.warn( { error :err.message }, 'Error exporting message' ); + Log.warn( { error : err.message }, 'Error exporting message' ); } return nextMessageOrUuid(null); } ); }, err => { + if(err) { + Log.warn( { error : err.message }, 'Error(s) during NetMail export'); + } return cb(err); }); }; @@ -962,6 +965,7 @@ function FTNMessageScanTossModule() { fileCase : self.moduleConfig.nodes[nodeConfigKey].fileCase || 'lower', }; + if(_.isString(exportOpts.network.localAddress)) { exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); } @@ -2031,8 +2035,7 @@ function FTNMessageScanTossModule() { this.isNetMailMessage = function(message) { return message.isPrivate() && null === _.get(message, 'meta.System.LocalToUserID', null) && - Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null) - ; + Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null); }; } From 70a2bc5160a6c25a0f14f50fdbb9ed16738f08ff Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 18:32:15 -0700 Subject: [PATCH 0132/1013] Rework BSO-style flow file generation * Add point address NNNNnnnn.pnt sub dir support * Use *route* address in case of non-direct destinations --- core/scanner_tossers/ftn_bso.js | 50 ++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 36ee815f..6a5c0c60 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -221,7 +221,13 @@ function FTNMessageScanTossModule() { }; this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { - let basename; + // + // Refs + // * http://ftsc.org/docs/fts-5005.003 + // * http://wiki.synchro.net/ref:fidonet_files#flow_files + // + let controlFileBaseName; + let pointDir; const ext = self.getOutgoingFlowFileExtension( destAddress, @@ -230,32 +236,50 @@ function FTNMessageScanTossModule() { fileCase ); - if(destAddress.point) { + const netComponent = `0000${destAddress.net.toString(16)}`.substr(-4); + const nodeComponent = `0000${destAddress.node.toString(16)}`.substr(-4); + if(destAddress.point) { + // point's go in an extra subdir, e.g. outbound/NNNNnnnn.pnt/00000001.pnt (for a point of 1) + pointDir = `${netComponent}${nodeComponent}.pnt`; + controlFileBaseName = `00000000${destAddress.point.toString(16)}`.substr(-8); } else { + pointDir = ''; + // // Use |destAddress| nnnnNNNN.??? where nnnn is dest net and NNNN is dest // node. This seems to match what Mystic does // - basename = - `0000${destAddress.net.toString(16)}`.substr(-4) + - `0000${destAddress.node.toString(16)}`.substr(-4); + controlFileBaseName = `${netComponent}${nodeComponent}`; } + // + // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." + // ...but we let the user override. + // if('upper' === fileCase) { - basename = basename.toUpperCase(); + controlFileBaseName = controlFileBaseName.toUpperCase(); + pointDir = pointDir.toUpperCase(); } - return paths.join(basePath, `${basename}.${ext}`); + return paths.join(basePath, pointDir, `${controlFileBaseName}.${ext}`); }; this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { - const appendLines = fileRefs.reduce( (content, ref) => { - return content + `${directive}${ref}\n`; - }, ''); + // + // We have to ensure the *directory* of |filePath| exists here esp. + // for cases such as point destinations where a subdir may be + // present in the path that doesn't yet exist. + // + const flowFileDir = paths.dirname(filePath); + fse.mkdirs(flowFileDir, () => { // note not checking err; let's try appendFile + const appendLines = fileRefs.reduce( (content, ref) => { + return content + `${directive}${ref}\n`; + }, ''); - fs.appendFile(filePath, appendLines, err => { - cb(err); + fs.appendFile(filePath, appendLines, err => { + return cb(err); + }); }); }; @@ -915,7 +939,7 @@ function FTNMessageScanTossModule() { function prepareFloFile(callback) { const flowFilePath = self.getOutgoingFlowFileName( exportOpts.outgoingDir, - exportOpts.destAddress, + exportOpts.routeAddress, 'ref', exportOpts.exportType, exportOpts.fileCase From 5caf7a9fce3a3b3fcc5585e7cac71a3a8c34f3e2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 18:47:19 -0700 Subject: [PATCH 0133/1013] Move NetMail routes to scannerTossers: { ftn_bso: { ... } } where it belongs in config.hjson --- core/scanner_tossers/ftn_bso.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 6a5c0c60..a24024ee 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -827,9 +827,9 @@ function FTNMessageScanTossModule() { this.getNetMailRoute = function(dstAddr) { // - // messageNetworks.ftn.netMail.routes{} full|wildcard -> full adddress lookup + // Route full|wildcard -> full adddress/network lookup // - const routes = _.get(Config, 'messageNetworks.ftn.netMail.routes'); + const routes = _.get(Config, 'scannerTossers.ftn_bso.netMail.routes'); if(!routes) { return; } @@ -843,7 +843,7 @@ function FTNMessageScanTossModule() { // // Attempt to find route information for |destAddress|: // - // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config + // 1) Routes: scannerTossers.ftn_bso.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config // - Where we send may not be where destAddress is (it's routed!) // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config // - Where we send is direct to destAddress From bc55317a4b979517c2640377b3588c3873ea2fa4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 19:30:10 -0700 Subject: [PATCH 0134/1013] Fix drawing when focus items set - we should not attempt to stylize! --- core/vertical_menu_view.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index a7e566eb..5b27c36d 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -66,10 +66,7 @@ function VerticalMenuView(options) { let sgr; if(item.focused && self.hasFocusItems()) { const focusItem = self.focusItems[index]; - text = strUtil.stylizeString( - focusItem ? focusItem.text : item.text, - self.focusTextStyle - ); + text = focusItem ? focusItem.text : item.text; sgr = ''; } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); From c1f971d2d997a2664229fdb02c5c72b460084acd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 19:30:21 -0700 Subject: [PATCH 0135/1013] Code readability --- core/bbs_list.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/bbs_list.js b/core/bbs_list.js index 5c81f478..74c792c6 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -150,7 +150,10 @@ exports.getModule = class BBSListModule extends MenuModule { 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 ], + [ + 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'); From 8bfad971a15ff2a348216e8a74439f4b37f9ca9d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 21 Jan 2018 11:58:19 -0700 Subject: [PATCH 0136/1013] Finish conversion from 'binary' -> 'binary-parser' * FTN packets * SAUCE --- core/ftn_mail_packet.js | 475 +++++++++++++++++++---------------- core/sauce.js | 180 ++++++------- core/servers/login/telnet.js | 271 +++++++++++--------- core/string_util.js | 2 +- package.json | 2 +- 5 files changed, 506 insertions(+), 424 deletions(-) diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 1c60e1f0..bb72c40a 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -8,10 +8,11 @@ const Address = require('./ftn_address.js'); const strUtil = require('./string_util.js'); const Log = require('./logger.js').log; const ansiPrep = require('./ansi_prep.js'); +const Errors = require('./enig_error.js').Errors; const _ = require('lodash'); const assert = require('assert'); -const binary = require('binary'); +const { Parser } = require('binary-parser'); const fs = require('graceful-fs'); const async = require('async'); const iconv = require('iconv-lite'); @@ -23,7 +24,6 @@ const FTN_PACKET_HEADER_SIZE = 58; // fixed header size const FTN_PACKET_HEADER_TYPE = 2; const FTN_PACKET_MESSAGE_TYPE = 2; const FTN_PACKET_BAUD_TYPE_2_2 = 2; -const NULL_TERM_BUFFER = new Buffer( [ 0x00 ] ); // SAUCE magic header + version ("00") const FTN_MESSAGE_SAUCE_HEADER = new Buffer('SAUCE00'); @@ -173,108 +173,103 @@ function Packet(options) { this.parsePacketHeader = function(packetBuffer, cb) { assert(Buffer.isBuffer(packetBuffer)); - if(packetBuffer.length < FTN_PACKET_HEADER_SIZE) { - cb(new Error('Buffer too small')); - return; + let packetHeader; + try { + packetHeader = new Parser() + .uint16le('origNode') + .uint16le('destNode') + .uint16le('year') + .uint16le('month') + .uint16le('day') + .uint16le('hour') + .uint16le('minute') + .uint16le('second') + .uint16le('baud') + .uint16le('packetType') + .uint16le('origNet') + .uint16le('destNet') + .int8('prodCodeLo') + .int8('prodRevLo') // aka serialNo + .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 + .uint16le('origZone') + .uint16le('destZone') + // + // The following is "filler" in FTS-0001, specifics in + // FSC-0045 and FSC-0048 + // + .uint16le('auxNet') + .uint16le('capWordValidate') + .int8('prodCodeHi') + .int8('prodRevHi') + .uint16le('capWord') + .uint16le('origZone2') + .uint16le('destZone2') + .uint16le('origPoint') + .uint16le('destPoint') + .uint32le('prodData') + .parse(packetBuffer); + } catch(e) { + return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); + } + + // Convert password from NULL padded array to string + packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); + + if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { + return cb(Errors.Invalid(`Unsupported FTN packet header type: ${packetHeader.packetType}`)); } // - // Start out reading as if this is a FSC-0048 2+ packet + // What kind of packet do we really have here? // - binary.parse(packetBuffer) - .word16lu('origNode') - .word16lu('destNode') - .word16lu('year') - .word16lu('month') - .word16lu('day') - .word16lu('hour') - .word16lu('minute') - .word16lu('second') - .word16lu('baud') - .word16lu('packetType') - .word16lu('origNet') - .word16lu('destNet') - .word8('prodCodeLo') - .word8('prodRevLo') // aka serialNo - .buffer('password', 8) // null padded C style string - .word16lu('origZone') - .word16lu('destZone') + // :TODO: adjust values based on version discovered + if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { + packetHeader.version = '2.2'; + + // See FSC-0045 + packetHeader.origPoint = packetHeader.year; + packetHeader.destPoint = packetHeader.month; + + packetHeader.destDomain = packetHeader.origZone2; + packetHeader.origDomain = packetHeader.auxNet; + } else { // - // The following is "filler" in FTS-0001, specifics in - // FSC-0045 and FSC-0048 + // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" // - .word16lu('auxNet') - .word16lu('capWordValidate') - .word8('prodCodeHi') - .word8('prodRevHi') - .word16lu('capWord') - .word16lu('origZone2') - .word16lu('destZone2') - .word16lu('origPoint') - .word16lu('destPoint') - .word32lu('prodData') - .tap(packetHeader => { - // Convert password from NULL padded array to string - //packetHeader.password = ftn.stringFromFTN(packetHeader.password); - packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); + const capWordValidateSwapped = + ((packetHeader.capWordValidate & 0xff) << 8) | + ((packetHeader.capWordValidate >> 8) & 0xff); - if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { - cb(new Error('Unsupported header type: ' + packetHeader.packetType)); - return; + if(capWordValidateSwapped === packetHeader.capWord && + 0 != packetHeader.capWord && + packetHeader.capWord & 0x0001) + { + packetHeader.version = '2+'; + + // See FSC-0048 + if(-1 === packetHeader.origNet) { + packetHeader.origNet = packetHeader.auxNet; } + } else { + packetHeader.version = '2'; - // - // What kind of packet do we really have here? - // - // :TODO: adjust values based on version discovered - if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { - packetHeader.version = '2.2'; + // :TODO: should fill bytes be 0? + } + } - // See FSC-0045 - packetHeader.origPoint = packetHeader.year; - packetHeader.destPoint = packetHeader.month; + packetHeader.created = moment({ + year : packetHeader.year, + month : packetHeader.month - 1, // moment uses 0 indexed months + date : packetHeader.day, + hour : packetHeader.hour, + minute : packetHeader.minute, + second : packetHeader.second + }); - packetHeader.destDomain = packetHeader.origZone2; - packetHeader.origDomain = packetHeader.auxNet; - } else { - // - // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" - // - const capWordValidateSwapped = - ((packetHeader.capWordValidate & 0xff) << 8) | - ((packetHeader.capWordValidate >> 8) & 0xff); + const ph = new PacketHeader(); + _.assign(ph, packetHeader); - if(capWordValidateSwapped === packetHeader.capWord && - 0 != packetHeader.capWord && - packetHeader.capWord & 0x0001) - { - packetHeader.version = '2+'; - - // See FSC-0048 - if(-1 === packetHeader.origNet) { - packetHeader.origNet = packetHeader.auxNet; - } - } else { - packetHeader.version = '2'; - - // :TODO: should fill bytes be 0? - } - } - - packetHeader.created = moment({ - year : packetHeader.year, - month : packetHeader.month - 1, // moment uses 0 indexed months - date : packetHeader.day, - hour : packetHeader.hour, - minute : packetHeader.minute, - second : packetHeader.second - }); - - let ph = new PacketHeader(); - _.assign(ph, packetHeader); - - cb(null, ph); - }); + return cb(null, ph); }; this.getPacketHeaderBuffer = function(packetHeader) { @@ -454,21 +449,30 @@ function Packet(options) { // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam const FTN_CHRS_PREFIX = new Buffer( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" const FTN_CHRS_SUFFIX = new Buffer( [ 0x0d ] ); - binary.parse(messageBodyBuffer) - .scan('prefix', FTN_CHRS_PREFIX) - .scan('content', FTN_CHRS_SUFFIX) - .tap(chrsData => { - if(chrsData.prefix && chrsData.content && chrsData.content.length > 0) { - const chrs = iconv.decode(chrsData.content, 'CP437'); - const chrsEncoding = ftn.getEncodingFromCharacterSetIdentifier(chrs); - if(chrsEncoding) { - encoding = chrsEncoding; - } - callback(null); - } else { - callback(null); - } - }); + + let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); + if(chrsPrefixIndex < 0) { + return callback(null); + } + + chrsPrefixIndex += FTN_CHRS_PREFIX.length; + + const chrsEndIndex = messageBodyBuffer.indexOf(FTN_CHRS_SUFFIX, chrsPrefixIndex); + if(chrsEndIndex < 0) { + return callback(null); + } + + let chrsContent = messageBodyBuffer.slice(chrsPrefixIndex, chrsEndIndex); + if(0 === chrsContent.length) { + return callback(null); + } + + chrsContent = iconv.decode(chrsContent, 'CP437'); + const chrsEncoding = ftn.getEncodingFromCharacterSetIdentifier(chrsContent); + if(chrsEncoding) { + encoding = chrsEncoding; + } + return callback(null); }, function extractMessageData(callback) { // @@ -525,125 +529,160 @@ function Packet(options) { }; this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { - binary.parse(packetBuffer) - .word16lu('messageType') - .word16lu('ftn_msg_orig_node') - .word16lu('ftn_msg_dest_node') - .word16lu('ftn_msg_orig_net') - .word16lu('ftn_msg_dest_net') - .word16lu('ftn_attr_flags') - .word16lu('ftn_cost') - .scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max - .scan('toUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max - .scan('fromUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max - .scan('subject', NULL_TERM_BUFFER) // :TODO: 72 bytes max6 - .scan('message', NULL_TERM_BUFFER) - .tap(function tapped(msgData) { // no arrow function; want classic this - if(!msgData.messageType) { - // end marker -- no more messages - return cb(null); + // + // Check for end-of-messages marker up front before parse so we can easily + // tell the difference between end and bad header + // + if(packetBuffer.length < 3) { + const peek = packetBuffer.slice(0, 2); + if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { + // end marker - no more messages + return cb(null); + } + // else fall through & hit exception below to log error + } + + let msgData; + try { + msgData = new Parser() + .uint16le('messageType') + .uint16le('ftn_msg_orig_node') + .uint16le('ftn_msg_dest_node') + .uint16le('ftn_msg_orig_net') + .uint16le('ftn_msg_dest_net') + .uint16le('ftn_attr_flags') + .uint16le('ftn_cost') + // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved + .array('modDateTime', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('toUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('fromUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('subject', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('message', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .parse(packetBuffer); + } catch(e) { + return cb(Errors.Invalid(`Failed to parse FTN message header: ${e.message}`)); + } + + if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { + return cb(Errors.Invalid(`Unsupported FTN message type: ${msgData.messageType}`)); + } + + // + // Convert null terminated arrays to strings + // + [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { + msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); + }); + + // Technically the following fields have length limits as per fts-0001.016: + // * modDateTime : 20 bytes + // * toUserName : 36 bytes + // * fromUserName : 36 bytes + // * subject : 72 bytes + + // + // The message body itself is a special beast as it may + // contain an origin line, kludges, SAUCE in the case + // of ANSI files, etc. + // + const msg = new Message( { + toUserName : msgData.toUserName, + fromUserName : msgData.fromUserName, + subject : msgData.subject, + modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), + }); + + // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) + msg.meta.FtnProperty = { + ftn_orig_node : header.origNode, + ftn_dest_node : header.destNode, + ftn_orig_network : header.origNet, + ftn_dest_network : header.destNet, + + ftn_attr_flags : msgData.ftn_attr_flags, + ftn_cost : msgData.ftn_cost, + + ftn_msg_orig_node : msgData.ftn_msg_orig_node, + ftn_msg_dest_node : msgData.ftn_msg_dest_node, + ftn_msg_orig_net : msgData.ftn_msg_orig_net, + ftn_msg_dest_net : msgData.ftn_msg_dest_net, + }; + + self.processMessageBody(msgData.message, messageBodyData => { + msg.message = messageBodyData.message; + msg.meta.FtnKludge = messageBodyData.kludgeLines; + + if(messageBodyData.tearLine) { + msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; + + if(self.options.keepTearAndOrigin) { + msg.message += `\r\n${messageBodyData.tearLine}\r\n`; } + } - if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { - return cb(new Error('Unsupported message type: ' + msgData.messageType)); + if(messageBodyData.seenBy.length > 0) { + msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; + } + + if(messageBodyData.area) { + msg.meta.FtnProperty.ftn_area = messageBodyData.area; + } + + if(messageBodyData.originLine) { + msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; + + if(self.options.keepTearAndOrigin) { + msg.message += `${messageBodyData.originLine}\r\n`; } + } - const read = - 14 + // fixed header size - msgData.modDateTime.length + 1 + - msgData.toUserName.length + 1 + - msgData.fromUserName.length + 1 + - msgData.subject.length + 1 + - msgData.message.length + 1; + // + // 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); + } - // - // Convert null terminated arrays to strings - // - let convMsgData = {}; - [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { - convMsgData[k] = iconv.decode(msgData[k], 'CP437'); - }); + // :TODO: Parser should give is this info: + const bytesRead = + 14 + // fixed header size + msgData.modDateTime.length + 1 + // +1 = NULL + msgData.toUserName.length + 1 + // +1 = NULL + msgData.fromUserName.length + 1 + // +1 = NULL + msgData.subject.length + 1 + // +1 = NULL + msgData.message.length; // includes NULL - // - // The message body itself is a special beast as it may - // contain an origin line, kludges, SAUCE in the case - // of ANSI files, etc. - // - const msg = new Message( { - toUserName : convMsgData.toUserName, - fromUserName : convMsgData.fromUserName, - subject : convMsgData.subject, - modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime), - }); - - // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) - msg.meta.FtnProperty = { - ftn_orig_node : header.origNode, - ftn_dest_node : header.destNode, - ftn_orig_network : header.origNet, - ftn_dest_network : header.destNet, - - ftn_attr_flags : msgData.ftn_attr_flags, - ftn_cost : msgData.ftn_cost, - - ftn_msg_orig_node : msgData.ftn_msg_orig_node, - ftn_msg_dest_node : msgData.ftn_msg_dest_node, - ftn_msg_orig_net : msgData.ftn_msg_orig_net, - ftn_msg_dest_net : msgData.ftn_msg_dest_net, + const nextBuf = packetBuffer.slice(bytesRead); + if(nextBuf.length > 0) { + const next = function(e) { + if(e) { + cb(e); + } else { + self.parsePacketMessages(header, nextBuf, iterator, cb); + } }; - self.processMessageBody(msgData.message, messageBodyData => { - msg.message = messageBodyData.message; - msg.meta.FtnKludge = messageBodyData.kludgeLines; - - if(messageBodyData.tearLine) { - msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; - - if(self.options.keepTearAndOrigin) { - msg.message += `\r\n${messageBodyData.tearLine}\r\n`; - } - } - - if(messageBodyData.seenBy.length > 0) { - msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; - } - - if(messageBodyData.area) { - msg.meta.FtnProperty.ftn_area = messageBodyData.area; - } - - if(messageBodyData.originLine) { - msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; - - 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) { - const next = function(e) { - if(e) { - cb(e); - } else { - self.parsePacketMessages(header, nextBuf, iterator, cb); - } - }; - - iterator('message', msg, next); - } else { - cb(null); - } - }); - }); + iterator('message', msg, next); + } else { + cb(null); + } + }); }; this.sanatizeFtnProperties = function(message) { diff --git a/core/sauce.js b/core/sauce.js index b976450b..9ee75b47 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -1,8 +1,11 @@ /* jslint node: true */ 'use strict'; -var binary = require('binary'); -var iconv = require('iconv-lite'); +const Errors = require('./enig_error.js').Errors; + +// deps +const iconv = require('iconv-lite'); +const { Parser } = require('binary-parser'); exports.readSAUCE = readSAUCE; @@ -25,103 +28,107 @@ const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; function readSAUCE(data, cb) { if(data.length < SAUCE_SIZE) { - cb(new Error('No SAUCE record present')); - return; + return cb(Errors.DoesNotExist('No SAUCE record present')); } - var offset = data.length - SAUCE_SIZE; - var sauceRec = data.slice(offset); + let sauceRec; + try { + sauceRec = new Parser() + .buffer('id', { length : 5 } ) + .buffer('version', { length : 2 } ) + .buffer('title', { length: 35 } ) + .buffer('author', { length : 20 } ) + .buffer('group', { length: 20 } ) + .buffer('date', { length: 8 } ) + .uint32le('fileSize') + .int8('dataType') + .int8('fileType') + .uint16le('tinfo1') + .uint16le('tinfo2') + .uint16le('tinfo3') + .uint16le('tinfo4') + .int8('numComments') + .int8('flags') + // :TODO: does this need to be optional? + .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 + .parse(data.slice(data.length - SAUCE_SIZE)); + } catch(e) { + return cb(Errors.Invalid('Invalid SAUCE record')); + } - binary.parse(sauceRec) - .buffer('id', 5) - .buffer('version', 2) - .buffer('title', 35) - .buffer('author', 20) - .buffer('group', 20) - .buffer('date', 8) - .word32lu('fileSize') - .word8('dataType') - .word8('fileType') - .word16lu('tinfo1') - .word16lu('tinfo2') - .word16lu('tinfo3') - .word16lu('tinfo4') - .word8('numComments') - .word8('flags') - .buffer('tinfos', 22) // SAUCE 00.5 - .tap(function onVars(vars) { - if(!SAUCE_ID.equals(vars.id)) { - return cb(new Error('No SAUCE record present')); - } + if(!SAUCE_ID.equals(sauceRec.id)) { + return cb(Errors.DoesNotExist('No SAUCE record present')); + } - var ver = iconv.decode(vars.version, 'cp437'); + const ver = iconv.decode(sauceRec.version, 'cp437'); - if('00' !== ver) { - return cb(new Error('Unsupported SAUCE version: ' + ver)); - } + if('00' !== ver) { + return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); + } - if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) { - return cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType)); - } + if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) { + return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`)); + } - var sauce = { - id : iconv.decode(vars.id, 'cp437'), - version : iconv.decode(vars.version, 'cp437').trim(), - title : iconv.decode(vars.title, 'cp437').trim(), - author : iconv.decode(vars.author, 'cp437').trim(), - group : iconv.decode(vars.group, 'cp437').trim(), - date : iconv.decode(vars.date, 'cp437').trim(), - fileSize : vars.fileSize, - dataType : vars.dataType, - fileType : vars.fileType, - tinfo1 : vars.tinfo1, - tinfo2 : vars.tinfo2, - tinfo3 : vars.tinfo3, - tinfo4 : vars.tinfo4, - numComments : vars.numComments, - flags : vars.flags, - tinfos : vars.tinfos, - }; + const sauce = { + id : iconv.decode(sauceRec.id, 'cp437'), + version : iconv.decode(sauceRec.version, 'cp437').trim(), + title : iconv.decode(sauceRec.title, 'cp437').trim(), + author : iconv.decode(sauceRec.author, 'cp437').trim(), + group : iconv.decode(sauceRec.group, 'cp437').trim(), + date : iconv.decode(sauceRec.date, 'cp437').trim(), + fileSize : sauceRec.fileSize, + dataType : sauceRec.dataType, + fileType : sauceRec.fileType, + tinfo1 : sauceRec.tinfo1, + tinfo2 : sauceRec.tinfo2, + tinfo3 : sauceRec.tinfo3, + tinfo4 : sauceRec.tinfo4, + numComments : sauceRec.numComments, + flags : sauceRec.flags, + tinfos : sauceRec.tinfos, + }; - var dt = SAUCE_DATA_TYPES[sauce.dataType]; - if(dt && dt.parser) { - sauce[dt.name] = dt.parser(sauce); - } + const dt = SAUCE_DATA_TYPES[sauce.dataType]; + if(dt && dt.parser) { + sauce[dt.name] = dt.parser(sauce); + } - cb(null, sauce); - }); + return cb(null, sauce); } // :TODO: These need completed: -var SAUCE_DATA_TYPES = {}; -SAUCE_DATA_TYPES[0] = { name : 'None' }; -SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE }; -SAUCE_DATA_TYPES[2] = 'Bitmap'; -SAUCE_DATA_TYPES[3] = 'Vector'; -SAUCE_DATA_TYPES[4] = 'Audio'; -SAUCE_DATA_TYPES[5] = 'BinaryText'; -SAUCE_DATA_TYPES[6] = 'XBin'; -SAUCE_DATA_TYPES[7] = 'Archive'; -SAUCE_DATA_TYPES[8] = 'Executable'; +const SAUCE_DATA_TYPES = { + 0 : { name : 'None' }, + 1 : { name : 'Character', parser : parseCharacterSAUCE }, + 2 : 'Bitmap', + 3 : 'Vector', + 4 : 'Audio', + 5 : 'BinaryText', + 6 : 'XBin', + 7 : 'Archive', + 8 : 'Executable', +}; -var SAUCE_CHARACTER_FILE_TYPES = {}; -SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII'; -SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi'; -SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation'; -SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script'; -SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard'; -SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar'; -SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML'; -SAUCE_CHARACTER_FILE_TYPES[7] = 'Source'; -SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw'; +const SAUCE_CHARACTER_FILE_TYPES = { + 0 : 'ASCII', + 1 : 'ANSi', + 2 : 'ANSiMation', + 3 : 'RIP script', + 4 : 'PCBoard', + 5 : 'Avatar', + 6 : 'HTML', + 7 : 'Source', + 8 : 'TundraDraw', +}; // // Map of SAUCE font -> encoding hint // // Note that this is the same mapping that x84 uses. Be compatible! // -var SAUCE_FONT_TO_ENCODING_HINT = { +const SAUCE_FONT_TO_ENCODING_HINT = { 'Amiga MicroKnight' : 'amiga', 'Amiga MicroKnight+' : 'amiga', 'Amiga mOsOul' : 'amiga', @@ -138,9 +145,11 @@ var SAUCE_FONT_TO_ENCODING_HINT = { 'IBM VGA' : 'cp437', }; -['437', '720', '737', '775', '819', '850', '852', '855', '857', '858', - '860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) { - var codec = 'cp' + page; +[ + '437', '720', '737', '775', '819', '850', '852', '855', '857', '858', + '860', '861', '862', '863', '864', '865', '866', '869', '872' +].forEach( page => { + const codec = 'cp' + page; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; @@ -149,7 +158,7 @@ var SAUCE_FONT_TO_ENCODING_HINT = { }); function parseCharacterSAUCE(sauce) { - var result = {}; + const result = {}; result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; @@ -157,11 +166,12 @@ function parseCharacterSAUCE(sauce) { // convience: create ansiFlags sauce.ansiFlags = sauce.flags; - var i = 0; + let i = 0; while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { ++i; } - var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); + + const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); if(fontName.length > 0) { result.fontName = fontName; } diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index a6fa0deb..6ffb49a9 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -2,16 +2,17 @@ 'use strict'; // ENiGMA½ -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); -const Config = require('../../config.js').config; -const EnigAssert = require('../../enigma_assert.js'); +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); +const Config = require('../../config.js').config; +const EnigAssert = require('../../enigma_assert.js'); +const { stringFromNullTermBuffer } = require('../../string_util.js'); // deps const net = require('net'); const buffers = require('buffers'); -const binary = require('binary'); +const { Parser } = require('binary-parser'); const util = require('util'); //var debug = require('debug')('telnet'); @@ -218,46 +219,42 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { return MORE_DATA_REQUIRED; } - let end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes + const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes if(-1 === end) { return MORE_DATA_REQUIRED; } - // eat up and process the header - let buf = bufs.splice(0, 4).toBuffer(); - binary.parse(buf) - .word8('iac1') - .word8('sb') - .word8('ttype') - .word8('is') - .tap(function(vars) { - EnigAssert(vars.iac1 === COMMANDS.IAC); - EnigAssert(vars.sb === COMMANDS.SB); - EnigAssert(vars.ttype === OPTIONS.TERMINAL_TYPE); - EnigAssert(vars.is === SB_COMMANDS.IS); - }); - - // eat up the rest - end -= 4; - buf = bufs.splice(0, end).toBuffer(); - - // - // From this point -> |end| is our ttype - // - // Look for trailing NULL(s). Clients such as NetRunner do this. - // If none is found, we take the entire buffer - // - let trimAt = 0; - for(; trimAt < buf.length; ++trimAt) { - if(0x00 === buf[trimAt]) { - break; - } + let ttypeCmd; + try { + ttypeCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint8('is') + .array('ttype', { + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC + }) + // note we read iac2 above + .uint8('se') + .parse(bufs.toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing TTYP telnet command'); + return event; } - event.ttype = buf.toString('ascii', 0, trimAt); + EnigAssert(COMMANDS.IAC === ttypeCmd.iac1); + EnigAssert(COMMANDS.SB === ttypeCmd.sb); + EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); + EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); + EnigAssert(ttypeCmd.ttype.length > 0); + // note we found IAC_SE above - // pop off the terminating IAC SE - bufs.splice(0, 2); + // some terminals such as NetRunner provide a NULL-terminated buffer + // slice to remove IAC + event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); + + bufs.splice(0, end); } return event; @@ -272,25 +269,30 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { return MORE_DATA_REQUIRED; } - event.buf = bufs.splice(0, 9).toBuffer(); - binary.parse(event.buf) - .word8('iac1') - .word8('sb') - .word8('naws') - .word16bu('width') - .word16bu('height') - .word8('iac2') - .word8('se') - .tap(function(vars) { - EnigAssert(vars.iac1 == COMMANDS.IAC); - EnigAssert(vars.sb == COMMANDS.SB); - EnigAssert(vars.naws == OPTIONS.WINDOW_SIZE); - EnigAssert(vars.iac2 == COMMANDS.IAC); - EnigAssert(vars.se == COMMANDS.SE); + let nawsCmd; + try { + nawsCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint16be('width') + .uint16be('height') + .uint8('iac2') + .uint8('se') + .parse(bufs.splice(0, 9).toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing NAWS telnet command'); + return event; + } - event.cols = event.columns = event.width = vars.width; - event.rows = event.height = vars.height; - }); + EnigAssert(COMMANDS.IAC === nawsCmd.iac1); + EnigAssert(COMMANDS.SB === nawsCmd.sb); + EnigAssert(OPTIONS.WINDOW_SIZE === nawsCmd.opt); + EnigAssert(COMMANDS.IAC === nawsCmd.iac2); + EnigAssert(COMMANDS.SE === nawsCmd.se); + + event.cols = event.columns = event.width = nawsCmd.width; + event.rows = event.height = nawsCmd.height; } return event; }; @@ -321,78 +323,109 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { return MORE_DATA_REQUIRED; } - // eat up and process the header - let buf = bufs.splice(0, 4).toBuffer(); - binary.parse(buf) - .word8('iac1') - .word8('sb') - .word8('newEnv') - .word8('isOrInfo') // initial=IS, updates=INFO - .tap(function(vars) { - EnigAssert(vars.iac1 === COMMANDS.IAC); - EnigAssert(vars.sb === COMMANDS.SB); - EnigAssert(vars.newEnv === OPTIONS.NEW_ENVIRONMENT || vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP); - EnigAssert(vars.isOrInfo === SB_COMMANDS.IS || vars.isOrInfo === SB_COMMANDS.INFO); + // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. - event.type = vars.isOrInfo; - - if(vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP) { - // :TODO: bring all this into Telnet class - Log.log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); - } - }); - - // eat up the rest - end -= 4; - buf = bufs.splice(0, end).toBuffer(); - - // - // This part can become messy. The basic spec is: - // IAC SB NEW-ENVIRON IS type ... [ VALUE ... ] [ type ... [ VALUE ... ] [ ... ] ] IAC SE - // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html - // - // Start by splitting up the remaining buffer. Keep the delimiters - // as prefixes we can use for processing. - // - // :TODO: Currently not supporting ESCaped values (ESC + ). Probably not really in the wild, but we should be compliant - // :TODO: Could probably just convert this to use a regex & handle delims + escaped values... in any case, this is sloppy... - const params = []; - let p = 0; - let j; - let l; - for(j = 0, l = buf.length; j < l; ++j) { - if(NEW_ENVIRONMENT_DELIMITERS.indexOf(buf[j]) === -1) { - continue; - } - - params.push(buf.slice(p, j)); - p = j; + let envCmd; + try { + envCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint8('isOrInfo') // IS=initial, INFO=updates + .array('envBlock', { + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC + }) + // note we consume IAC above + .uint8('se') + .parse(bufs.splice(0, bufs.length).toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing NEW-ENVIRON telnet command'); + return event; } - // remainder - if(p < l) { - params.push(buf.slice(p, l)); + EnigAssert(COMMANDS.IAC === envCmd.iac1); + EnigAssert(COMMANDS.SB === envCmd.sb); + EnigAssert(OPTIONS.NEW_ENVIRONMENT === envCmd.opt || OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt); + EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); + + if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { + // :TODO: we should probably support this for legacy clients? + Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); } + const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC + + if(envBuf.length < 4) { // TYPE + single char name + sep + single char value + // empty env block + return event; + } + + const States = { + Name : 1, + Value : 2, + }; + + let state = States.Name; + const setVars = {}; + const delVars = []; let varName; - event.envVars = {}; - // :TODO: handle cases where a variable was present in a previous exchange, but missing here...e.g removed - for(j = 0; j < params.length; ++j) { - if(params[j].length < 2) { - continue; - } + // :TODO: handle ESC type!!! + while(envBuf.length) { + switch(state) { + case States.Name : + { + const type = parseInt(envBuf.splice(0, 1)); + if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { + return event; // fail :( + } - let cmd = params[j].readUInt8(); - if(cmd === NEW_ENVIRONMENT_COMMANDS.VAR || cmd === NEW_ENVIRONMENT_COMMANDS.USERVAR) { - varName = params[j].slice(1).toString('utf8'); // :TODO: what encoding should this really be? - } else { - event.envVars[varName] = params[j].slice(1).toString('utf8'); // :TODO: again, what encoding? + let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); + if(-1 === nameEnd) { + nameEnd = envBuf.length; + } + + varName = envBuf.splice(0, nameEnd); + if(!varName) { + return event; // something is wrong. + } + + varName = Buffer.from(varName).toString('ascii'); + + const next = parseInt(envBuf.splice(0, 1)); + if(NEW_ENVIRONMENT_COMMANDS.VALUE === next) { + state = States.Value; + } else { + state = States.Name; + delVars.push(varName); // no value; del this var + } + } + break; + + case States.Value : + { + let valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VAR); + if(-1 === valueEnd) { + valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.USERVAR); + } + if(-1 === valueEnd) { + valueEnd = envBuf.length; + } + + let value = envBuf.splice(0, valueEnd); + if(value) { + value = Buffer.from(value).toString('ascii'); + setVars[varName] = value; + } + state = States.Name; + } + break; } } - // pop off remaining IAC SE - bufs.splice(0, 2); + // :TODO: Handle deleting previously set vars via delVars + event.type = envCmd.isOrInfo; + event.envVars = setVars; } return event; diff --git a/core/string_util.js b/core/string_util.js index c846fc38..f47bd6b5 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -204,7 +204,7 @@ function debugEscapedString(s) { } function stringFromNullTermBuffer(buf, encoding) { - let nullPos = buf.indexOf(new Buffer( [ 0x00 ] )); + let nullPos = buf.indexOf( 0x00 ); if(-1 === nullPos) { nullPos = buf.length; } diff --git a/package.json b/package.json index b44cc894..8c7a7a5d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ ], "dependencies": { "async": "^2.5.0", - "binary": "0.3.x", + "binary-parser": "^1.3.2", "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", From 94f3721bf8f753c9af358ae36b0fd191414fa708 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 21 Jan 2018 20:49:38 -0700 Subject: [PATCH 0137/1013] Prompt when already logged in --- core/servers/login/ssh.js | 76 ++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index cd9ca1a9..d14bdbbd 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -41,14 +41,19 @@ function SSHClient(clientConn) { clientConn.on('authentication', function authAttempt(ctx) { const username = ctx.username || ''; const password = ctx.password || ''; - + self.isNewUser = (Config.users.newUserNames || []).indexOf(username) > -1; self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); function terminateConnection() { ctx.reject(); - clientConn.end(); + return clientConn.end(); + } + + function alreadyLoggedIn(username) { + ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); + return terminateConnection(); } // @@ -65,15 +70,13 @@ function SSHClient(clientConn) { userLogin(self, ctx.username, ctx.password, function authResult(err) { if(err) { if(err.existingConn) { - // :TODO: Can we display somthing here? - terminateConnection(); - return; - } else { - return ctx.reject(SSHClient.ValidAuthMethods); + return alreadyLoggedIn(username); } - } else { - ctx.accept(); + + return ctx.reject(SSHClient.ValidAuthMethods); } + + ctx.accept(); }); } else { if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { @@ -85,7 +88,7 @@ function SSHClient(clientConn) { return ctx.reject(); } - let interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; + const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; ctx.prompt(interactivePrompt, function retryPrompt(answers) { loginAttempts += 1; @@ -93,37 +96,36 @@ function SSHClient(clientConn) { userLogin(self, username, (answers[0] || ''), err => { if(err) { if(err.existingConn) { - // :TODO: can we display something here? - terminateConnection(); - } else { - if(loginAttempts >= Config.general.loginAttempts) { - terminateConnection(); - } else { - const artOpts = { - client : self, - name : 'SSHPMPT.ASC', - readSauce : false, - }; - - theme.getThemeArt(artOpts, (err, artInfo) => { - if(err) { - interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; - } else { - const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? - Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : - '(No new user names enabled!)'; - - interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; - } - return ctx.prompt(interactivePrompt, retryPrompt); - }); - } + return alreadyLoggedIn(username); } + + if(loginAttempts >= Config.general.loginAttempts) { + return terminateConnection(); + } + + const artOpts = { + client : self, + name : 'SSHPMPT.ASC', + readSauce : false, + }; + + theme.getThemeArt(artOpts, (err, artInfo) => { + if(err) { + interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; + } else { + const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? + Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : + '(No new user names enabled!)'; + + interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; + } + return ctx.prompt(interactivePrompt, retryPrompt); + }); } else { ctx.accept(); } - }); - }); + }); + }); } }); From 50074d77656a1e03b93374e985bcf0e641462fd9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 21 Jan 2018 20:49:49 -0700 Subject: [PATCH 0138/1013] Remove unused require --- core/string_util.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/string_util.js b/core/string_util.js index f47bd6b5..4539ea38 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -2,7 +2,6 @@ 'use strict'; // ENiGMA½ -const miscUtil = require('./misc_util.js'); const ANSI = require('./ansi_term.js'); // deps From cc74616a93f390273695762f6b8a35b9b3960424 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:34:10 -0700 Subject: [PATCH 0139/1013] Next at end of list goes to previous menu by default --- core/file_area_list.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/file_area_list.js b/core/file_area_list.js index 87e794cb..f42bf93e 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -75,6 +75,7 @@ exports.getModule = class FileAreaList extends MenuModule { this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); this.fileList = _.get(options, 'extraArgs.fileList'); + this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); if(this.fileList) { // we'll need to adjust position as well! @@ -103,6 +104,10 @@ exports.getModule = class FileAreaList extends MenuModule { return this.displayBrowsePage(true, cb); // true=clerarScreen } + if(this.lastFileNextExit) { + return this.prevMenu(cb); + } + return cb(null); }, prevFile : (formData, extraArgs, cb) => { From ec1876084cc4e079daab67cabd162b21eb4bdde9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:34:32 -0700 Subject: [PATCH 0140/1013] Add sanatizeString() method --- core/database.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/database.js b/core/database.js index 10331fc0..2717a545 100644 --- a/core/database.js +++ b/core/database.js @@ -19,6 +19,7 @@ const dbs = {}; exports.getTransactionDatabase = getTransactionDatabase; exports.getModDatabasePath = getModDatabasePath; exports.getISOTimestampString = getISOTimestampString; +exports.sanatizeString = sanatizeString; exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; @@ -59,6 +60,25 @@ function getISOTimestampString(ts) { return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } +function sanatizeString(s) { + return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex + switch (c) { + case '\0' : return '\\0'; + case '\x08' : return '\\b'; + case '\x09' : return '\\t'; + case '\x1a' : return '\\z'; + case '\n' : return '\\n'; + case '\r' : return '\\r'; + + case '"' : + case '\'' : + case '\\' : + case '%' : + return `\\${c}`; + } + }); +} + function initializeDatabases(cb) { async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { From 70b5d7a124b50447a99aa3709880d27bb77931c2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:36:16 -0700 Subject: [PATCH 0141/1013] MAJOR refactor of Message class * ES6 class vs old style * Add findMessages(filter, ...) similar to FileEntry.findFiles() allowing many filter types used throughout the system --- core/message.js | 1258 ++++++++++++++++++++++++++--------------------- 1 file changed, 711 insertions(+), 547 deletions(-) diff --git a/core/message.js b/core/message.js index f99efa8a..6c47a934 100644 --- a/core/message.js +++ b/core/message.js @@ -5,9 +5,11 @@ 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; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); +const { + sanatizeString, + getISOTimestampString } = require('./database.js'); const { isAnsi, isFormattedLine, @@ -25,74 +27,15 @@ const assert = require('assert'); const moment = require('moment'); const iconvEncode = require('iconv-lite').encode; -module.exports = Message; - const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); -function Message(options) { - options = options || {}; - - this.messageId = options.messageId || 0; // always generated @ persist - this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid; - - if(options.uuid) { - // note: new messages have UUID generated @ time of persist. See also Message.createMessageUUID() - this.uuid = options.uuid; - } - - this.replyToMsgId = options.replyToMsgId || 0; - this.toUserName = options.toUserName || ''; - this.fromUserName = options.fromUserName || ''; - this.subject = options.subject || ''; - this.message = options.message || ''; - - if(_.isDate(options.modTimestamp) || moment.isMoment(options.modTimestamp)) { - this.modTimestamp = moment(options.modTimestamp); - } else if(_.isString(options.modTimestamp)) { - this.modTimestamp = moment(options.modTimestamp); - } - - this.viewCount = options.viewCount || 0; - - this.meta = { - System : {}, // we'll always have this one - }; - - if(_.isObject(options.meta)) { - _.defaultsDeep(this.meta, options.meta); - } - - if(options.meta) { - this.meta = options.meta; - } - - this.hashTags = options.hashTags || []; - - this.isValid = function() { - // :TODO: validate as much as possible - return true; - }; - - this.isPrivate = function() { - return Message.isPrivateAreaTag(this.areaTag); - }; - - this.isFromRemoteUser = function() { - return null !== _.get(this, 'meta.System.remote_from_user', null); - }; -} - -Message.WellKnownAreaTags = { +const WELL_KNOWN_AREA_TAGS = { Invalid : '', Private : 'private_mail', Bulletin : 'local_bulletin', }; -Message.isPrivateAreaTag = function(areaTag) { - return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; -}; - -Message.SystemMetaNames = { +const SYSTEM_META_NAMES = { LocalToUserID : 'local_to_user_id', LocalFromUserID : 'local_from_user_id', StateFlags0 : 'state_flags0', // See Message.StateFlags0 @@ -103,19 +46,20 @@ Message.SystemMetaNames = { }; // Types for Message.SystemMetaNames.ExternalFlavor meta -Message.AddressFlavor = { +const ADDRESS_FLAVOR = { Local : 'local', // local / non-remote addressing FTN : 'ftn', // FTN style Email : 'email', }; -Message.StateFlags0 = { +const STATE_FLAGS0 = { None : 0x00000000, Imported : 0x00000001, // imported from foreign system Exported : 0x00000002, // exported to foreign system }; -Message.FtnPropertyNames = { +// :TODO: these should really live elsewhere... +const FTN_PROPERTY_NAMES = { // packet header oriented FtnOrigNode : 'ftn_orig_node', FtnDestNode : 'ftn_dest_node', @@ -143,518 +87,738 @@ Message.FtnPropertyNames = { FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; -// Note: kludges are stored with their names as-is +module.exports = class Message { + constructor( + { + messageId = 0, areaTag = Message.WellKnownAreaTags.Invalid, uuid, replyToMsgId = 0, + toUserName = '', fromUserName = '', subject = '', message = '', modTimestamp = moment(), + meta, hashTags = [], + } = { } + ) + { + this.messageId = messageId; + this.areaTag = areaTag; + this.uuid = uuid; + this.replyToMsgId = replyToMsgId; + this.toUserName = toUserName; + this.fromUserName = fromUserName; + this.subject = subject; + this.message = message; -Message.prototype.setLocalToUserId = function(userId) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; -}; + if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { + modTimestamp = moment(modTimestamp); + } -Message.prototype.setLocalFromUserId = function(userId) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; -}; + this.modTimestamp = modTimestamp; -Message.prototype.setRemoteToUser = function(remoteTo) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; -}; + this.meta = {}; + _.defaultsDeep(this.meta, { System : {} }, meta); -Message.prototype.setExternalFlavor = function(flavor) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; -}; - -Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { - assert(_.isString(areaTag)); - assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); - assert(_.isString(subject)); - assert(_.isString(body)); - - if(!moment.isMoment(modTimestamp)) { - modTimestamp = moment(modTimestamp); + this.hashTags = hashTags; } - areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); - modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); - subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); - body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); + isValid() { return true; } // :TODO: obviously useless; look into this or remove it - return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); -}; + static isPrivateAreaTag(areaTag) { + return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; + } -Message.getMessageIdByUuid = function(uuid, cb) { - msgDb.get( - `SELECT message_id - FROM message - WHERE message_uuid = ? - LIMIT 1;`, - [ uuid ], - (err, row) => { - if(err) { - cb(err); + isPrivate() { + return Message.isPrivateAreaTag(this.areaTag); + } + + isFromRemoteUser() { + return null !== _.get(this, 'meta.System.remote_from_user', null); + } + + static get WellKnownAreaTags() { + return WELL_KNOWN_AREA_TAGS; + } + + static get SystemMetaNames() { + return SYSTEM_META_NAMES; + } + + static get AddressFlavor() { + return ADDRESS_FLAVOR; + } + + static get StateFlags0() { + return STATE_FLAGS0; + } + + static get FtnPropertyNames() { + return FTN_PROPERTY_NAMES; + } + + setLocalToUserId(userId) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; + } + + setLocalFromUserId(userId) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; + } + + setRemoteToUser(remoteTo) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; + } + + setExternalFlavor(flavor) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; + } + + static createMessageUUID(areaTag, modTimestamp, subject, body) { + assert(_.isString(areaTag)); + assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); + assert(_.isString(subject)); + assert(_.isString(body)); + + if(!moment.isMoment(modTimestamp)) { + modTimestamp = moment(modTimestamp); + } + + areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); + modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); + subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); + body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); + + return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); + } + + /* + Find message IDs or UUIDs by filter. Available filters/options: + + filter.uuids - use with resultType='id' + filter.ids - use with resultType='uuid' + filter.toUserName + filter.fromUserName + filter.replyToMesageId + filter.newerThanTimestamp + filter.newerThanMessageId + *filter.confTag - all area tags in confTag + filter.areaTag + *filter.metaTuples - {category, name, value} + + *filter.terms - FTS search + + filter.sort = modTimestamp | messageId + filter.order = ascending | (descending) + + filter.limit + filter.resultType = (id) | uuid | count + filter.extraFields = [] + + filter.privateTagUserId = - if set, only private messages belonging to are processed + (any other areaTag or confTag filters will be ignored) + + *=NYI + */ + static findMessages(filter, cb) { + filter = filter || {}; + + filter.resultType = filter.resultType || 'id'; + filter.extraFields = filter.extraFields || []; + + const field = 'id' === filter.resultType ? 'message_id' : 'message_uuid'; + + if(moment.isMoment(filter.newerThanTimestamp)) { + filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); + } + + let sql; + if('count' === filter.resultType) { + sql = + `SELECT COUNT() AS count + FROM message m`; + + } else { + sql = + `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} + FROM message m`; + } + + const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; + let sqlOrderBy; + let sqlWhere = ''; + + function appendWhereClause(clause) { + if(sqlWhere) { + sqlWhere += ' AND '; } else { + sqlWhere += ' WHERE '; + } + sqlWhere += clause; + } + + // currently only avail sort + if('modTimestamp' === filter.sort) { + sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; + } else { + sqlOrderBy = `ORDER BY m.message_id ${sqlOrderDir}`; + } + + if(Array.isArray(filter.ids)) { + appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`); + } + + if(Array.isArray(filter.uuids)) { + const uuidList = filter.uuids.map(u => `"${u}"`).join(', '); + appendWhereClause(`m.message_id IN (${uuidList})`); + } + + + if(_.isNumber(filter.privateTagUserId)) { + appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); + appendWhereClause( + `m.message_id IN ( + SELECT message_id + FROM message_meta + WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} + )`); + } else { + let areaTags = []; + if(filter.confTag && filter.confTag.length > 0) { + // :TODO: grab areas from conf -> add to areaTags[] + } + + if(areaTags.length > 0 || filter.areaTag && filter.areaTag.length > 0) { + if(Array.isArray(filter.areaTag)) { + areaTags = areaTags.concat(filter.areaTag); + } else if(_.isString(filter.areaTag)) { + areaTags.push(filter.areaTag); + } + + areaTags = _.uniq(areaTags); // remove any dupes + + if(areaTags.length > 1) { + const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); + appendWhereClause(`m.area_tag IN(${areaList})`); + } else { + appendWhereClause(`m.area_tag = "${areaTags[0]}"`); + } + } + } + + [ 'toUserName', 'fromUserName', 'replyToMessageId' ].forEach(field => { + if(_.isString(filter[field]) && filter[field].length > 0) { + appendWhereClause(`m.${_.snakeCase(field)} = "${sanatizeString(filter[field])}"`); + } + }); + + if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { + appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); + } + + if(_.isNumber(filter.newerThanMessageId)) { + appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); + } + + sql += `${sqlWhere} ${sqlOrderBy}`; + + if(_.isNumber(filter.limit)) { + sql += ` LIMIT ${filter.limit}`; + } + + sql += ';'; + + if('count' === filter.resultType) { + msgDb.get(sql, (err, row) => { + return cb(err, row ? row.count : 0); + }); + } else { + const matches = []; + const extra = filter.extraFields.length > 0; + msgDb.each(sql, (err, row) => { + if(_.isObject(row)) { + matches.push(extra ? row : row[field]); + } + }, err => { + return cb(err, matches); + }); + } + } + + // :TODO: use findMessages, by uuid, limit=1 + static getMessageIdByUuid(uuid, cb) { + msgDb.get( + `SELECT message_id + FROM message + WHERE message_uuid = ? + LIMIT 1;`, + [ uuid ], + (err, row) => { + if(err) { + return cb(err); + } + const success = (row && row.message_id); - cb(success ? null : new Error('No match'), success ? row.message_id : null); - } - } - ); -}; - -Message.getMessageIdsByMetaValue = function(category, name, value, cb) { - msgDb.all( - `SELECT message_id - FROM message_meta - WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, - [ category, name, value ], - (err, rows) => { - if(err) { - cb(err); - } else { - cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) - } - } - ); -}; - -Message.getMetaValuesByMessageId = function(messageId, category, name, cb) { - const sql = - `SELECT meta_value - FROM message_meta - WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - - msgDb.all(sql, [ messageId, category, name ], (err, rows) => { - if(err) { - return cb(err); - } - - if(0 === rows.length) { - return cb(new Error('No value for category/name')); - } - - // single values are returned without an array - if(1 === rows.length) { - return cb(null, rows[0].meta_value); - } - - cb(null, rows.map(r => r.meta_value)); // map to array of values only - }); -}; - -Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { - async.waterfall( - [ - function getMessageId(callback) { - Message.getMessageIdByUuid(uuid, (err, messageId) => { - callback(err, messageId); - }); - }, - function getMetaValues(messageId, callback) { - Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { - callback(err, values); - }); - } - ], - (err, values) => { - cb(err, values); - } - ); -}; - -Message.prototype.loadMeta = function(cb) { - /* - Example of loaded this.meta: - - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ - - const sql = - `SELECT meta_category, meta_name, meta_value - FROM message_meta - WHERE message_id = ?;`; - - let self = this; - msgDb.each(sql, [ this.messageId ], (err, row) => { - if(!(row.meta_category in self.meta)) { - self.meta[row.meta_category] = { }; - self.meta[row.meta_category][row.meta_name] = row.meta_value; - } else { - if(!(row.meta_name in self.meta[row.meta_category])) { - self.meta[row.meta_category][row.meta_name] = row.meta_value; - } else { - if(_.isString(self.meta[row.meta_category][row.meta_name])) { - self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; - } - - self.meta[row.meta_category][row.meta_name].push(row.meta_value); - } - } - }, err => { - cb(err); - }); -}; - -Message.prototype.load = function(options, cb) { - assert(_.isString(options.uuid)); - - var self = this; - - async.series( - [ - function loadMessage(callback) { - msgDb.get( - 'SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' + - 'message, modified_timestamp, view_count ' + - 'FROM message ' + - 'WHERE message_uuid=? ' + - 'LIMIT 1;', - [ options.uuid ], - (err, msgRow) => { - if(err) { - return callback(err); - } - if(!msgRow) { - return callback(new Error('Message (no longer) available')); - } - - self.messageId = msgRow.message_id; - self.areaTag = msgRow.area_tag; - self.messageUuid = msgRow.message_uuid; - self.replyToMsgId = msgRow.reply_to_message_id; - self.toUserName = msgRow.to_user_name; - self.fromUserName = msgRow.from_user_name; - self.subject = msgRow.subject; - self.message = msgRow.message; - self.modTimestamp = moment(msgRow.modified_timestamp); - self.viewCount = msgRow.view_count; - - callback(err); - } + return cb( + success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`), + success ? row.message_id : null ); - }, - function loadMessageMeta(callback) { - self.loadMeta(err => { - callback(err); - }); - }, - function loadHashTags(callback) { - // :TODO: - callback(null); - } - ], - function complete(err) { - cb(err); - } - ); -}; - -Message.prototype.persistMetaValue = function(category, name, value, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = msgDb; - } - - const metaStmt = transOrDb.prepare( - `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) - VALUES (?, ?, ?, ?);`); - - if(!_.isArray(value)) { - value = [ value ]; - } - - let self = this; - - async.each(value, (v, next) => { - metaStmt.run(self.messageId, category, name, v, err => { - next(err); - }); - }, err => { - cb(err); - }); -}; - -Message.prototype.persist = function(cb) { - - if(!this.isValid()) { - return cb(new Error('Cannot persist invalid message!')); - } - - const self = this; - - async.waterfall( - [ - function beginTransaction(callback) { - return msgDb.beginTransaction(callback); - }, - function storeMessage(trans, callback) { - // generate a UUID for this message if required (general case) - const msgTimestamp = moment(); - if(!self.uuid) { - self.uuid = Message.createMessageUUID( - self.areaTag, - msgTimestamp, - self.subject, - self.message); - } - - trans.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, getISOTimestampString(msgTimestamp) ], - function inserted(err) { // use non-arrow function for 'this' scope - if(!err) { - self.messageId = this.lastID; - } - - return callback(err, trans); - } - ); - }, - function storeMeta(trans, callback) { - if(!self.meta) { - return callback(null, trans); - } - /* - Example of self.meta: - - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ - async.each(Object.keys(self.meta), (category, nextCat) => { - async.each(Object.keys(self.meta[category]), (name, nextName) => { - self.persistMetaValue(category, name, self.meta[category][name], trans, err => { - nextName(err); - }); - }, err => { - nextCat(err); - }); - - }, err => { - callback(err, trans); - }); - }, - function storeHashTags(trans, callback) { - // :TODO: hash tag support - return callback(null, trans); - } - ], - (err, trans) => { - if(trans) { - trans[err ? 'rollback' : 'commit'](transErr => { - return cb(err ? err : transErr, self.messageId); - }); - } else { - return cb(err); - } - } - ); -}; - -Message.prototype.getFTNQuotePrefix = function(source) { - source = source || 'fromUserName'; - - return ftnUtil.getQuotePrefix(this[source]); -}; - -Message.prototype.getTearLinePosition = function(input) { - const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); - return m ? m.index : -1; -}; - -Message.prototype.getQuoteLines = function(options, cb) { - if(!options.termWidth || !options.termHeight || !options.cols) { - return cb(Errors.MissingParam()); - } - - options.startCol = options.startCol || 1; - options.includePrefix = _.get(options, 'includePrefix', true); - options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); - options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting - - /* - Some long text that needs to be wrapped and quoted should look right after - doing so, don't ya think? yeah I think so - - Nu> Some long text that needs to be wrapped and quoted should look right - Nu> after doing so, don't ya think? yeah I think so - - Ot> Nu> Some long text that needs to be wrapped and quoted should look - Ot> Nu> right after doing so, don't ya think? yeah I think so - - */ - const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; - - function getWrapped(text, extraPrefix) { - extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; - - const wrapOpts = { - width : options.cols - (quotePrefix.length + extraPrefix.length), - tabHandling : 'expand', - tabWidth : 4, - }; - - return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { - return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; - }); - } - - function getFormattedLine(line) { - // for pre-formatted text, we just append a line truncated to fit - let newLen; - const total = line.length + quotePrefix.length; - - if(total > options.cols) { - newLen = options.cols - total; - } else { - newLen = total; - } - - return `${quotePrefix}${line.slice(0, newLen)}`; - } - - if(options.isAnsi) { - ansiPrep( - this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF - { - termWidth : options.termWidth, - termHeight : options.termHeight, - cols : options.cols, - rows : 'auto', - startCol : options.startCol, - forceLineTerm : true, - }, - (err, prepped) => { - prepped = prepped || this.message; - - let lastSgr = ''; - const split = splitTextAtTerms(prepped); - - const quoteLines = []; - const focusQuoteLines = []; - - // - // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) - // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to - // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do - // the trick and allow them to leave them alone! - // - split.forEach(l => { - quoteLines.push(`${lastSgr}${l}`); - - focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); - lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex - }); - - quoteLines[quoteLines.length - 1] += options.ansiResetSgr; - - return cb(null, quoteLines, focusQuoteLines, true); } ); - } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; - const quoted = []; - const input = _.trimEnd(this.message).replace(/\b/g, ''); + } - // find *last* tearline - let tearLinePos = this.getTearLinePosition(input); - tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string + // :TODO: use findMessages + static getMessageIdsByMetaValue(category, name, value, cb) { + msgDb.all( + `SELECT message_id + FROM message_meta + WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, + [ category, name, value ], + (err, rows) => { + if(err) { + return cb(err); + } + return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) + } + ); + } - input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { - // - // For each paragraph, a state machine: - // - New line - line - // - New (pre)quoted line - quote_line - // - Continuation of new/quoted line - // - // Also: - // - Detect pre-formatted lines & try to keep them as-is - // - let state; - let buf = ''; - let quoteMatch; + static getMetaValuesByMessageId(messageId, category, name, cb) { + const sql = + `SELECT meta_value + FROM message_meta + WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - if(quoted.length > 0) { - // - // Preserve paragraph seperation. - // - // FSC-0032 states something about leaving blank lines fully blank - // (without a prefix) but it seems nicer (and more consistent with other systems) - // to put 'em in. - // - quoted.push(quotePrefix); + msgDb.all(sql, [ messageId, category, name ], (err, rows) => { + if(err) { + return cb(err); } - paragraph.split(/\r?\n/).forEach(line => { - if(0 === line.trim().length) { - // see blank line notes above - return quoted.push(quotePrefix); + if(0 === rows.length) { + return cb(Errors.DoesNotExist('No value for category/name')); + } + + // single values are returned without an array + if(1 === rows.length) { + return cb(null, rows[0].meta_value); + } + + return cb(null, rows.map(r => r.meta_value)); // map to array of values only + }); + } + + static getMetaValuesByMessageUuid(uuid, category, name, cb) { + async.waterfall( + [ + function getMessageId(callback) { + Message.getMessageIdByUuid(uuid, (err, messageId) => { + return callback(err, messageId); + }); + }, + function getMetaValues(messageId, callback) { + Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { + return callback(err, values); + }); + } + ], + (err, values) => { + return cb(err, values); + } + ); + } + + loadMeta(cb) { + /* + Example of loaded this.meta: + + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ + const sql = + `SELECT meta_category, meta_name, meta_value + FROM message_meta + WHERE message_id = ?;`; + + const self = this; // :TODO: not required - arrow functions below: + msgDb.each(sql, [ this.messageId ], (err, row) => { + if(!(row.meta_category in self.meta)) { + self.meta[row.meta_category] = { }; + self.meta[row.meta_category][row.meta_name] = row.meta_value; + } else { + if(!(row.meta_name in self.meta[row.meta_category])) { + self.meta[row.meta_category][row.meta_name] = row.meta_value; + } else { + if(_.isString(self.meta[row.meta_category][row.meta_name])) { + self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; + } + + self.meta[row.meta_category][row.meta_name].push(row.meta_value); + } + } + }, err => { + return cb(err); + }); + } + + load(options, cb) { + assert(_.isString(options.uuid)); + + const self = this; + + async.series( + [ + function loadMessage(callback) { + msgDb.get( + `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, + message, modified_timestamp, view_count + FROM message + WHERE message_uuid=? + LIMIT 1;`, + [ options.uuid ], + (err, msgRow) => { + if(err) { + return callback(err); + } + + if(!msgRow) { + return callback(Errors.DoesNotExist('Message (no longer) available')); + } + + self.messageId = msgRow.message_id; + self.areaTag = msgRow.area_tag; + self.messageUuid = msgRow.message_uuid; + self.replyToMsgId = msgRow.reply_to_message_id; + self.toUserName = msgRow.to_user_name; + self.fromUserName = msgRow.from_user_name; + self.subject = msgRow.subject; + self.message = msgRow.message; + self.modTimestamp = moment(msgRow.modified_timestamp); + + return callback(err); + } + ); + }, + function loadMessageMeta(callback) { + self.loadMeta(err => { + return callback(err); + }); + }, + function loadHashTags(callback) { + // :TODO: + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } + + persistMetaValue(category, name, value, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = msgDb; + } + + const metaStmt = transOrDb.prepare( + `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) + VALUES (?, ?, ?, ?);`); + + if(!_.isArray(value)) { + value = [ value ]; + } + + const self = this; + + async.each(value, (v, next) => { + metaStmt.run(self.messageId, category, name, v, err => { + return next(err); + }); + }, err => { + return cb(err); + }); + } + + persist(cb) { + if(!this.isValid()) { + return cb(Errors.Invalid('Cannot persist invalid message!')); + } + + const self = this; + + async.waterfall( + [ + function beginTransaction(callback) { + return msgDb.beginTransaction(callback); + }, + function storeMessage(trans, callback) { + // generate a UUID for this message if required (general case) + const msgTimestamp = moment(); + if(!self.uuid) { + self.uuid = Message.createMessageUUID( + self.areaTag, + msgTimestamp, + self.subject, + self.message + ); + } + + trans.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, getISOTimestampString(msgTimestamp) ], + function inserted(err) { // use non-arrow function for 'this' scope + if(!err) { + self.messageId = this.lastID; + } + + return callback(err, trans); + } + ); + }, + function storeMeta(trans, callback) { + if(!self.meta) { + return callback(null, trans); + } + /* + Example of self.meta: + + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ + async.each(Object.keys(self.meta), (category, nextCat) => { + async.each(Object.keys(self.meta[category]), (name, nextName) => { + self.persistMetaValue(category, name, self.meta[category][name], trans, err => { + return nextName(err); + }); + }, err => { + return nextCat(err); + }); + + }, err => { + return callback(err, trans); + }); + }, + function storeHashTags(trans, callback) { + // :TODO: hash tag support + return callback(null, trans); + } + ], + (err, trans) => { + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr, self.messageId); + }); + } else { + return cb(err); + } + } + ); + } + + // :TODO: FTN stuff doesn't have any business here + getFTNQuotePrefix(source) { + source = source || 'fromUserName'; + + return ftnUtil.getQuotePrefix(this[source]); + } + + getTearLinePosition(input) { + const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); + return m ? m.index : -1; + } + + getQuoteLines(options, cb) { + if(!options.termWidth || !options.termHeight || !options.cols) { + return cb(Errors.MissingParam()); + } + + options.startCol = options.startCol || 1; + options.includePrefix = _.get(options, 'includePrefix', true); + options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); + options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); + options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting + + /* + Some long text that needs to be wrapped and quoted should look right after + doing so, don't ya think? yeah I think so + + Nu> Some long text that needs to be wrapped and quoted should look right + Nu> after doing so, don't ya think? yeah I think so + + Ot> Nu> Some long text that needs to be wrapped and quoted should look + Ot> Nu> right after doing so, don't ya think? yeah I think so + + */ + const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; + + function getWrapped(text, extraPrefix) { + extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; + + const wrapOpts = { + width : options.cols - (quotePrefix.length + extraPrefix.length), + tabHandling : 'expand', + tabWidth : 4, + }; + + return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { + return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; + }); + } + + function getFormattedLine(line) { + // for pre-formatted text, we just append a line truncated to fit + let newLen; + const total = line.length + quotePrefix.length; + + if(total > options.cols) { + newLen = options.cols - total; + } else { + newLen = total; + } + + return `${quotePrefix}${line.slice(0, newLen)}`; + } + + if(options.isAnsi) { + ansiPrep( + this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF + { + termWidth : options.termWidth, + termHeight : options.termHeight, + cols : options.cols, + rows : 'auto', + startCol : options.startCol, + forceLineTerm : true, + }, + (err, prepped) => { + prepped = prepped || this.message; + + let lastSgr = ''; + const split = splitTextAtTerms(prepped); + + const quoteLines = []; + const focusQuoteLines = []; + + // + // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) + // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to + // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do + // the trick and allow them to leave them alone! + // + split.forEach(l => { + quoteLines.push(`${lastSgr}${l}`); + + focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); + lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex + }); + + quoteLines[quoteLines.length - 1] += options.ansiResetSgr; + + return cb(null, quoteLines, focusQuoteLines, true); + } + ); + } else { + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; + const quoted = []; + const input = _.trimEnd(this.message).replace(/\b/g, ''); + + // find *last* tearline + let tearLinePos = this.getTearLinePosition(input); + tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string + + input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { + // + // For each paragraph, a state machine: + // - New line - line + // - New (pre)quoted line - quote_line + // - Continuation of new/quoted line + // + // Also: + // - Detect pre-formatted lines & try to keep them as-is + // + let state; + let buf = ''; + let quoteMatch; + + if(quoted.length > 0) { + // + // Preserve paragraph seperation. + // + // FSC-0032 states something about leaving blank lines fully blank + // (without a prefix) but it seems nicer (and more consistent with other systems) + // to put 'em in. + // + quoted.push(quotePrefix); } - quoteMatch = line.match(QUOTE_RE); + paragraph.split(/\r?\n/).forEach(line => { + if(0 === line.trim().length) { + // see blank line notes above + return quoted.push(quotePrefix); + } - switch(state) { - case 'line' : - if(quoteMatch) { - if(isFormattedLine(line)) { - quoted.push(getFormattedLine(line.replace(/\s/, ''))); + quoteMatch = line.match(QUOTE_RE); + + switch(state) { + case 'line' : + if(quoteMatch) { + if(isFormattedLine(line)) { + quoted.push(getFormattedLine(line.replace(/\s/, ''))); + } else { + quoted.push(...getWrapped(buf, quoteMatch[1])); + state = 'quote_line'; + buf = line; + } } else { - quoted.push(...getWrapped(buf, quoteMatch[1])); - state = 'quote_line'; + buf += ` ${line}`; + } + break; + + case 'quote_line' : + if(quoteMatch) { + const rem = line.slice(quoteMatch[0].length); + if(!buf.startsWith(quoteMatch[0])) { + quoted.push(...getWrapped(buf, quoteMatch[1])); + buf = rem; + } else { + buf += ` ${rem}`; + } + } else { + quoted.push(...getWrapped(buf)); buf = line; + state = 'line'; } - } else { - buf += ` ${line}`; - } - break; + break; - case 'quote_line' : - if(quoteMatch) { - const rem = line.slice(quoteMatch[0].length); - if(!buf.startsWith(quoteMatch[0])) { - quoted.push(...getWrapped(buf, quoteMatch[1])); - buf = rem; + default : + if(isFormattedLine(line)) { + quoted.push(getFormattedLine(line)); } else { - buf += ` ${rem}`; + state = quoteMatch ? 'quote_line' : 'line'; + buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any } - } else { - quoted.push(...getWrapped(buf)); - buf = line; - state = 'line'; - } - break; + break; + } + }); - default : - if(isFormattedLine(line)) { - quoted.push(getFormattedLine(line)); - } else { - state = quoteMatch ? 'quote_line' : 'line'; - buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any - } - break; - } + quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); }); - quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); - }); + input.slice(tearLinePos).split(/\r?\n/).forEach(l => { + quoted.push(...getWrapped(l)); + }); - input.slice(tearLinePos).split(/\r?\n/).forEach(l => { - quoted.push(...getWrapped(l)); - }); - - return cb(null, quoted, null, false); + return cb(null, quoted, null, false); + } } }; From 3d575f764534234ecc999d86694cc5bf4b307434 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:37:26 -0700 Subject: [PATCH 0142/1013] Default renderLen array --- core/word_wrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/word_wrap.js b/core/word_wrap.js index f246ad23..ecb728a5 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -40,7 +40,7 @@ function wordWrapText(text, options) { let renderLen; let i = 0; let wordStart = 0; - let result = { wrapped : [ '' ], renderLen : [] }; + let result = { wrapped : [ '' ], renderLen : [ 0 ] }; function expandTab(column) { const remainWidth = options.tabWidth - (column % options.tabWidth); From b6bda7f45f459bd241249282b7331083f742eb7b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:38:50 -0700 Subject: [PATCH 0143/1013] much cleaner code --- core/theme.js | 56 ++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/core/theme.js b/core/theme.js index 5c056c49..2c984ae0 100644 --- a/core/theme.js +++ b/core/theme.js @@ -34,47 +34,31 @@ function refreshThemeHelpers(theme) { // theme.helpers = { getPasswordChar : function() { - var pwChar = Config.defaults.passwordChar; - if(_.has(theme, 'customization.defaults.general')) { - var themePasswordChar = theme.customization.defaults.general.passwordChar; - if(_.isString(themePasswordChar)) { - pwChar = themePasswordChar.substr(0, 1); - } else if(_.isNumber(themePasswordChar)) { - pwChar = String.fromCharCode(themePasswordChar); - } + let pwChar = _.get( + theme, + 'customization.defaults.general.passwordChar', + Config.defaults.passwordChar + ); + + if(_.isString(pwChar)) { + pwChar = pwChar.substr(0, 1); + } else if(_.isNumber(pwChar)) { + pwChar = String.fromCharCode(pwChar); } + return pwChar; }, - getDateFormat : function(style) { - style = style || 'short'; - - var format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY'; - - if(_.has(theme, 'customization.defaults.dateFormat')) { - return theme.customization.defaults.dateFormat[style] || format; - } - return format; + getDateFormat : function(style = 'short') { + const format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY'; + return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, - getTimeFormat : function(style) { - style = style || 'short'; - - var format = Config.defaults.timeFormat[style] || 'h:mm a'; - - if(_.has(theme, 'customization.defaults.timeFormat')) { - return theme.customization.defaults.timeFormat[style] || format; - } - return format; + getTimeFormat : function(style = 'short') { + const format = Config.defaults.timeFormat[style] || 'h:mm a'; + return _.get(theme, `customization.defaults.timeFormat.${style}`, format); }, - getDateTimeFormat : function(style) { - style = style || 'short'; - - var format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; - - if(_.has(theme, 'customization.defaults.dateTimeFormat')) { - return theme.customization.defaults.dateTimeFormat[style] || format; - } - - return format; + getDateTimeFormat : function(style = 'short') { + const format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); } }; } From cc119297e83f33107ede69b85261f5878c376620 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:39:53 -0700 Subject: [PATCH 0144/1013] wcValue -> wildcards (readability) --- core/file_entry.js | 2 +- core/scanner_tossers/ftn_bso.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 861b9d79..b95c7acb 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -496,7 +496,7 @@ module.exports = class FileEntry { if(filter.metaPairs && filter.metaPairs.length > 0) { filter.metaPairs.forEach(mp => { - if(mp.wcValue) { + if(mp.wildcards) { // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); appendWhereClause( diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index a24024ee..8c87b61f 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1718,9 +1718,9 @@ function FTNMessageScanTossModule() { const metaPairs = [ { - name : 'short_file_name', - value : replaces.toUpperCase(), // we store upper as well - wcValue : true, // value may contain wildcards + name : 'short_file_name', + value : replaces.toUpperCase(), // we store upper as well + wildcards : true, // value may contain wildcards }, { name : 'tic_origin', From f350e3d446c81e1bd797f42f4bfd317a55dfeb73 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:41:53 -0700 Subject: [PATCH 0145/1013] Use private message list header for 'inbox' --- art/themes/luciano_blocktronics/PRVMSGLIST.ANS | Bin 0 -> 2291 bytes config/menu.hjson | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 art/themes/luciano_blocktronics/PRVMSGLIST.ANS diff --git a/art/themes/luciano_blocktronics/PRVMSGLIST.ANS b/art/themes/luciano_blocktronics/PRVMSGLIST.ANS new file mode 100644 index 0000000000000000000000000000000000000000..911f1f2030dd91fe3cdf1e638635457a2565362e GIT binary patch literal 2291 zcmb_e%Wl&^6is)iTQ-zP;7!)pPT~l%Dp7@ysDvm~psWgtc!+5QrSK8_Ox175U)Yj= zg>%lmGj$pO5p94VOh|>h5Wqwizz-0DG~(4?(bX0DQFs zWf_0%ow>_2dT2*J&3;zb7H~F++jUJVa*xY!_H}iJk32l$x(cJ0&r0Mifn^orF+Kpx z!sUA}N5GQ630?B=zz{G@=|oN}+p_#QcNNaBkO4YW?o)=f)_S`&TwcBXw2fM?6A+*H zE&|`_iBYf0BaBXui{WxQpOlowR+@m} zvQpb5SHx5K8I>>0oLQJHbM~+17Sqx029en-PbtT<#H_{iY;PBsQVx~#9fcHQ(ubx9sJ8=*i~x%l>P43E z9zftST&_N^-hGpkQux_VV)ttibKOYPT03l+i6GXJ4Yb;16OP^6fQdT-L^-3PD4`RS zHCQZ3P*a~y1MKO%T7Q4rBqre8z>jl=qNtW%v}L$+aPPk4NMeMG?<~$!PIRJzX9#2k!>@@!WtIXQUu8i2~wg^4qFq z2ijONXNlJij9aD#_au?dEdtq%&fVpUpocN3{Rz Date: Fri, 26 Jan 2018 21:42:43 -0700 Subject: [PATCH 0146/1013] Use new Message.findMessages() functionality --- core/message_area.js | 225 +++++++++++++------------------------------ 1 file changed, 69 insertions(+), 156 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index 3c42e23f..fb068e13 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -320,177 +320,90 @@ function getMessageFromRow(row) { fromUserName : row.from_user_name, subject : row.subject, modTimestamp : row.modified_timestamp, - viewCount : row.view_count, }; } -function getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, what) { - // - // Helper for building SQL to fetch either a full message list or simply - // a count of new messages based on |what|. - // - // * If |areaTag| is Message.WellKnownAreaTags.Private, - // only messages addressed to |userId| should be returned/counted. - // - // * Only messages > |lastMessageId| should be returned/counted - // - const selectWhat = ('count' === what) ? - 'COUNT() AS count' : - 'message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count'; - - let sql = - `SELECT ${selectWhat} - FROM message - WHERE area_tag = "${areaTag}" AND message_id > ${lastMessageId}`; - - if(Message.isPrivateAreaTag(areaTag)) { - sql += - ` AND message_id in ( - SELECT message_id - FROM message_meta - WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${userId} - )`; - } - - if('count' === what) { - sql += ';'; - } else { - sql += ' ORDER BY message_id;'; - } - - return sql; -} - function getNewMessageCountInAreaForUser(userId, areaTag, cb) { - async.waterfall( - [ - function getLastMessageId(callback) { - getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) { - callback(null, lastMessageId || 0); // note: willingly ignoring any errors here! - }); - }, - function getCount(lastMessageId, callback) { - const sql = getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, 'count'); - msgDb.get(sql, (err, row) => { - return callback(err, row ? row.count : 0); - }); - } - ], - cb - ); + getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { + lastMessageId = lastMessageId || 0; + + const filter = { + areaTag, + newerThanMessageId : lastMessageId, + resultType : 'count', + }; + + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = userId; + } + + Message.findMessages(filter, (err, count) => { + return cb(err, count); + }); + }); } function getNewMessagesInAreaForUser(userId, areaTag, cb) { - // - // If |areaTag| is Message.WellKnownAreaTags.Private, - // only messages addressed to |userId| should be returned. - // - // Only messages > lastMessageId should be returned - // - let msgList = []; + getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { + lastMessageId = lastMessageId || 0; - async.waterfall( - [ - function getLastMessageId(callback) { - getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) { - callback(null, lastMessageId || 0); // note: willingly ignoring any errors here! - }); - }, - function getMessages(lastMessageId, callback) { - const sql = getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, 'messages'); + const filter = { + areaTag, + newerThanMessageId : lastMessageId, + sort : 'messageId', + order : 'ascending', + extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + }; - msgDb.each(sql, function msgRow(err, row) { - if(!err) { - msgList.push(getMessageFromRow(row)); - } - }, callback); - } - ], - function complete(err) { - cb(err, msgList); + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = userId; } - ); -} -function getMessageListForArea(options, areaTag, cb) { - // - // options.client (required) - // - - options.client.log.debug( { areaTag : areaTag }, 'Fetching available messages'); - - assert(_.isObject(options.client)); - - /* - [ - { - messageId, messageUuid, replyToId, toUserName, fromUserName, subject, modTimestamp, - status(new|old), - viewCount - } - ] - */ - - let msgList = []; - - async.series( - [ - function fetchMessages(callback) { - 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 = ?`; - - if(Message.isPrivateAreaTag(areaTag)) { - sql += - ` AND message_id IN ( - SELECT message_id - FROM message_meta - WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${options.client.user.userId} - )`; - } - - sql += ' ORDER BY message_id;'; - - msgDb.each( - sql, - [ areaTag.toLowerCase() ], - (err, row) => { - if(!err) { - msgList.push(getMessageFromRow(row)); - } - }, - callback - ); - }, - function fetchStatus(callback) { - callback(null);// :TODO: fixmeh. - } - ], - function complete(err) { - cb(err, msgList); - } - ); -} - -function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { - if(moment.isMoment(newerThanTimestamp)) { - newerThanTimestamp = getISOTimestampString(newerThanTimestamp); - } - - msgDb.get( - `SELECT message_id - FROM message - WHERE area_tag = ? AND DATETIME(modified_timestamp) > DATETIME("${newerThanTimestamp}", "+1 seconds") - ORDER BY modified_timestamp ASC - LIMIT 1;`, - [ areaTag ], - (err, row) => { + Message.findMessages(filter, (err, messages) => { if(err) { return cb(err); } - return cb(null, row ? row.message_id : null); + return cb(null, messages.map(msg => getMessageFromRow(msg))); + }); + }); +} + +function getMessageListForArea(client, areaTag, cb) { + const filter = { + areaTag, + sort : 'messageId', + order : 'ascending', + extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + }; + + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = client.user.userId; + } + + Message.findMessages(filter, (err, messages) => { + if(err) { + return cb(err); + } + + return cb(null, messages.map(msg => getMessageFromRow(msg))); + }); +} + +function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { + Message.findMessages( + { + areaTag, + newerThanTimestamp, + sort : 'modTimestamp', + order : 'ascending', + limit : 1, + }, + (err, id) => { + if(err) { + return cb(err); + } + return cb(null, id ? id[0] : null); } ); } From 303259841fc5fb69d0638ab46ae01846097398d3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:43:08 -0700 Subject: [PATCH 0147/1013] options -> client, since client was only option ;) --- core/msg_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/msg_list.js b/core/msg_list.js index 19ef30cd..72ee20f5 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -159,7 +159,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return callback(0 === self.messageList.length ? new Error('No messages in area') : null); } - messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) { + messageArea.getMessageListForArea(self.client, self.messageAreaTag, function msgs(err, msgList) { if(!msgList || 0 === msgList.length) { return callback(new Error('No messages in area')); } From a3e257aee3b086a2e150e525a9d06b9dd8df05f2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:44:07 -0700 Subject: [PATCH 0148/1013] Fix FSE word wrap bug when no barriers could be located in a > width string --- core/multi_line_edit_text_view.js | 63 ++++++++++++------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index bc73e903..1be0591f 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -56,9 +56,9 @@ const _ = require('lodash'); // To-Do // // * Index pos % for emit scroll events -// * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap) +// * Some of this should be async'd where there is lots of processing (e.g. word wrap) // * Fix backspace when col=0 (e.g. bs to prev line) -// * Add back word delete +// * Add word delete (CTRL+????) // * @@ -336,13 +336,10 @@ function MultiLineEditTextView(options) { */ this.updateTextWordWrap = function(index) { - var nextEolIndex = self.getNextEndOfLineIndex(index); - var wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); - var newLines = wrapped.wrapped; + const nextEolIndex = self.getNextEndOfLineIndex(index); + const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); + const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); - for(var i = 0; i < newLines.length; ++i) { - newLines[i] = { text : newLines[i] }; - } newLines[newLines.length - 1].eol = true; Array.prototype.splice.apply( @@ -420,44 +417,40 @@ function MultiLineEditTextView(options) { self.textLines[index].text.slice(col) ].join(''); - //self.cursorPos.col++; self.cursorPos.col += c.length; - var cursorOffset; - var absPos; + let cursorOffset; + let absPos; if(self.getTextLength(index) > self.dimens.width) { // // Update word wrapping and |cursorOffset| if the cursor // was within the bounds of the wrapped text // - var lastCol = self.cursorPos.col - c.length; - var firstWrapRange = self.updateTextWordWrap(index); + const lastCol = self.cursorPos.col - c.length; + const firstWrapRange = self.updateTextWordWrap(index); if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { cursorOffset = self.cursorPos.col - firstWrapRange.start; + } else { + cursorOffset = firstWrapRange.end; } // redraw from current row to end of visible area self.redrawRows(self.cursorPos.row, self.dimens.height); - if(!_.isUndefined(cursorOffset)) { - self.cursorBeginOfNextLine(); - self.cursorPos.col += cursorOffset; - self.client.term.rawWrite(ansi.right(cursorOffset)); - } else { - self.moveClientCursorToCursorPos(); - } + self.cursorBeginOfNextLine(); + self.cursorPos.col += cursorOffset; + self.client.term.rawWrite(ansi.right(cursorOffset)); } else { // // We must only redraw from col -> end of current visible line // absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); + self.client.term.write( - ansi.hideCursor() + - self.getSGRFor('text') + - self.getRenderText(index).slice(self.cursorPos.col - c.length) + - ansi.goto(absPos.row, absPos.col) + - ansi.showCursor(), false + `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`, + false // convertLineFeeds ); } }; @@ -495,16 +488,12 @@ function MultiLineEditTextView(options) { return new Array(self.getRemainingTabWidth(col)).join(expandChar); }; - this.wordWrapSingleLine = function(s, tabHandling, width) { - if(!_.isNumber(width)) { - width = self.dimens.width; - } - + this.wordWrapSingleLine = function(line, tabHandling = 'expand') { return wordWrapText( - s, + line, { - width : width, - tabHandling : tabHandling || 'expand', + width : self.dimens.width, + tabHandling : tabHandling, tabWidth : self.tabWidth, tabChar : '\t', } @@ -615,11 +604,7 @@ function MultiLineEditTextView(options) { let wrapped; text.forEach(line => { - wrapped = self.wordWrapSingleLine( - line, // line to wrap - 'expand', // tabHandling - self.dimens.width - ).wrapped; + wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; self.setTextLines(wrapped, index, true); // true=termWithEol index += wrapped.length; @@ -784,7 +769,7 @@ function MultiLineEditTextView(options) { var index = self.getTextLinesIndex(); var nextEolIndex = self.getNextEndOfLineIndex(index); var text = self.getContiguousText(index, nextEolIndex); - var newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; + const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); for(var i = 1; i < newLines.length; ++i) { From 974ee1b389768d5a1337626e7a9b443efad86485 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:45:08 -0700 Subject: [PATCH 0149/1013] MAJOR *POSSIBLY BREAKING* changes in FSE * WIP on cleanup to use 'standard' MCI formatting / theming used elsewhere in system * Some MCI ID changes (e.g. FSE in edit mode %TL13 -> %TL4); update your theme.hjson / artwork! --- art/themes/luciano_blocktronics/MSGEHDR.ANS | Bin 1578 -> 1578 bytes core/fse.js | 216 +++++++++----------- 2 files changed, 100 insertions(+), 116 deletions(-) diff --git a/art/themes/luciano_blocktronics/MSGEHDR.ANS b/art/themes/luciano_blocktronics/MSGEHDR.ANS index b2ed34e7eb397451cec927bfb56d220142458478..c455a9a3eca82f881c2e882359903293b244c516 100644 GIT binary patch delta 15 WcmZ3*vx;Yf8Y`2D!e$LtCPn}whXa8C delta 15 WcmZ3*vx;Yf8Y`2b@n#KHCPn}w 0) { self.message.setLocalToUserId(self.toUserId); return callback(null); @@ -695,12 +686,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function prepareViewStates(callback) { var header = self.viewControllers.header; - var from = header.getView(1); + var from = header.getView(MciViewIds.header.from); from.acceptsFocus = false; //from.setText(self.client.user.username); // :TODO: make this a method - var body = self.viewControllers.body.getView(1); + var body = self.viewControllers.body.getView(MciViewIds.body.message); self.updateTextEditMode(body.getTextEditMode()); self.updateEditModePosition(body.getEditPosition()); @@ -716,7 +707,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul self.initHeaderViewMode(); self.initFooterViewMode(); - var bodyMessageView = self.viewControllers.body.getView(1); + var bodyMessageView = self.viewControllers.body.getView(MciViewIds.body.message); if(bodyMessageView && _.has(self, 'message.message')) { //self.setBodyMessageViewText(); bodyMessageView.setText(cleanControlCodes(self.message.message)); @@ -726,7 +717,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul case 'edit' : { - const fromView = self.viewControllers.header.getView(1); + const fromView = self.viewControllers.header.getView(MciViewIds.header.from); const area = getMessageAreaByTag(self.messageAreaTag); if(area && area.realNames) { fromView.setText(self.client.user.properties.real_name || self.client.user.username); @@ -817,24 +808,20 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } initHeaderViewMode() { - assert(_.isObject(this.message)); + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.message.toUserName); + this.setHeaderText(MciViewIds.header.subject, this.message.subject); + this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); + this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); + this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); - 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.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); } initHeaderReplyEditMode() { assert(_.isObject(this.replyToMessage)); - this.setHeaderText(MciCodeIds.ReplyEditModeHeader.To, this.replyToMessage.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); // // We want to prefix the subject with "RE: " only if it's not already @@ -845,12 +832,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul newSubj = `RE: ${newSubj}`; } - this.setHeaderText(MciCodeIds.ReplyEditModeHeader.Subject, newSubj); + this.setHeaderText(MciViewIds.header.subject, newSubj); } initFooterViewMode() { - this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgNum, (this.messageIndex + 1).toString() ); - this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgTotal, this.messageTotal.toString() ); + this.setViewText('footerView', MciViewIds.ViewModeFooter.msgNum, (this.messageIndex + 1).toString() ); + this.setViewText('footerView', MciViewIds.ViewModeFooter.msgTotal, this.messageTotal.toString() ); } displayHelp(cb) { @@ -913,8 +900,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } }, function loadQuoteLines(callback) { - const quoteView = self.viewControllers.quoteBuilder.getView(3); - const bodyView = self.viewControllers.body.getView(1); + const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); + const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); self.replyToMessage.getQuoteLines( { @@ -935,16 +922,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul quoteView.setItems(quoteLines); quoteView.setFocusItems(focusQuoteLines); + self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg).setFocus(false); + self.viewControllers.quoteBuilder.switchFocus(MciViewIds.quoteBuilder.quoteLines); + return callback(null); } ); }, - function setViewFocus(callback) { - self.viewControllers.quoteBuilder.getView(1).setFocus(false); - self.viewControllers.quoteBuilder.switchFocus(3); - - callback(null); - } ], function complete(err) { if(err) { @@ -955,7 +939,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } observeEditorEvents() { - const bodyView = this.viewControllers.body.getView(1); + const bodyView = this.viewControllers.body.getView(MciViewIds.body.message); bodyView.on('edit position', pos => { this.updateEditModePosition(pos); @@ -968,7 +952,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul /* this.observeViewPosition = function() { - self.viewControllers.body.getView(1).on('edit position', function positionUpdate(pos) { + self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) { console.log(pos.percent + ' / ' + pos.below) }); }; @@ -995,7 +979,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul switchFromQuoteBuilderToBody() { this.viewControllers.quoteBuilder.setFocus(false); - var body = this.viewControllers.body.getView(1); + var body = this.viewControllers.body.getView(MciViewIds.body.message); body.redraw(); this.viewControllers.body.switchFocus(1); @@ -1009,14 +993,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul quoteBuilderFinalize() { // :TODO: fix magic #'s - const quoteMsgView = this.viewControllers.quoteBuilder.getView(1); - const msgView = this.viewControllers.body.getView(1); + const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); + const msgView = this.viewControllers.body.getView(MciViewIds.body.message); let quoteLines = quoteMsgView.getData().trim(); if(quoteLines.length > 0) { if(this.replyIsAnsi) { - const bodyMessageView = this.viewControllers.body.getView(1); + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`; } msgView.addText(`${quoteLines}\n\n`); From 7a2df5685516c6dc89de8d3858b33fd5d6c1bdb9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 27 Jan 2018 22:21:48 -0700 Subject: [PATCH 0150/1013] Menu items can now be arrays of objects * Allows custom members of each item * 'data' overrides selection (vs returning the index) * 'text' is the default member for text if no formatters are supplied * formatters: 'itemFormat' and 'focusItemFormat', e.g. "{member1} - {member2}" --- core/horizontal_menu_view.js | 36 ++++++++++++++++--------- core/menu_view.js | 51 +++++++++++++++++++++++++++++------- core/toggle_menu_view.js | 6 ++--- core/vertical_menu_view.js | 15 ++++++----- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 81d477ad..ee1d5318 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -1,12 +1,14 @@ /* jslint node: true */ 'use strict'; -var MenuView = require('./menu_view.js').MenuView; -var ansi = require('./ansi_term.js'); -var strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const { pipeToAnsi } = require('./color_codes.js'); +const { goto } = require('./ansi_term.js'); -var assert = require('assert'); -var _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); exports.HorizontalMenuView = HorizontalMenuView; @@ -57,21 +59,29 @@ function HorizontalMenuView(options) { this.drawItem = function(index) { assert(!this.positionCacheExpired); - var item = self.items[index]; + const item = self.items[index]; if(!item) { return; } - var text = strUtil.stylizeString( - item.text, - this.hasFocus && item.focused ? self.focusTextStyle : self.textStyle); + let text; + let sgr; + if(item.focused && self.hasFocusItems()) { + const focusItem = self.focusItems[index]; + text = focusItem ? focusItem.text : item.text; + sgr = ''; + } else if(this.complexItems) { + text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } - var drawWidth = text.length + self.getSpacer().length * 2; // * 2 = sides + const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2); self.client.term.write( - ansi.goto(self.position.row, item.col) + - (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()) + - strUtil.pad(text, drawWidth, self.fillChar, 'center') + `${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}` ); }; } diff --git a/core/menu_view.js b/core/menu_view.js index 5aed54ca..56899077 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -63,17 +63,37 @@ function MenuView(options) { util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { - const self = this; + if(Array.isArray(items)) { + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if(stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - if(items) { - this.items = []; - items.forEach( itemText => { - this.items.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others }); + + if(this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; + this.focusItemFormat = this.focusItemFormat || this.itemFormat; + } } }; @@ -96,12 +116,20 @@ MenuView.prototype.getCount = function() { }; MenuView.prototype.getItems = function() { + if(this.complexItems) { + return this.items; + } + return this.items.map( item => { return item.text; }); }; MenuView.prototype.getItem = function(index) { + if(this.complexItems) { + return this.items[index]; + } + return this.items[index].text; }; @@ -170,6 +198,11 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'justify' : this.justify = value; break; case 'focusItemIndex' : this.focusedItemIndex = value; break; + + case 'itemFormat' : + case 'focusItemFormat' : + this[propName] = value; + break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index 27ae2169..39d7ef95 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -113,9 +113,9 @@ ToggleMenuView.prototype.getData = function() { }; ToggleMenuView.prototype.setItems = function(items) { + items = items.slice(0, 2); // switch/toggle only works with two elements + ToggleMenuView.super_.prototype.setItems.call(this, items); - this.items = this.items.splice(0, 2); // switch/toggle only works with two elements - - this.dimens.width = this.items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) + this.dimens.width = items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) }; diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 5b27c36d..e1d6cd17 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -5,6 +5,8 @@ const MenuView = require('./menu_view.js').MenuView; const ansi = require('./ansi_term.js'); const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps const util = require('util'); @@ -68,17 +70,16 @@ function VerticalMenuView(options) { const focusItem = self.focusItems[index]; text = focusItem ? focusItem.text : item.text; sgr = ''; + } else if(this.complexItems) { + text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } - text += self.getSGR(); - self.client.term.write( - ansi.goto(item.row, self.position.col) + - sgr + - strUtil.pad(text, this.dimens.width, this.fillChar, this.justify) + `${ansi.goto(item.row, self.position.col)}${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}` ); }; } @@ -176,7 +177,9 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) { }; VerticalMenuView.prototype.getData = function() { - return this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return item.data ? item.data : this.focusedItemIndex; + //return this.focusedItemIndex; }; VerticalMenuView.prototype.setItems = function(items) { From 342c37b38828c0573417cf8a91adebe51e96a75b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 12:56:35 -0700 Subject: [PATCH 0151/1013] Allow extraArgs such that we can launch from menu items easier --- core/telnet_bridge.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 30db1207..6fb81d21 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -126,8 +126,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { constructor(options) { super(options); - this.config = options.menuConfig.config; - // defaults + this.config = Object.assign({}, _.get(options, 'menuConf.config'), options.extraArgs); this.config.port = this.config.port || 23; } @@ -152,10 +151,10 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { host : self.config.host, }; - let clientTerminated; - self.client.term.write(resetScreen()); - self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`); + self.client.term.write( + ` Connecting to ${connectOpts.host}, please wait...\n` + ); const telnetConnection = new TelnetClientConnection(self.client); From 90427ac89d0fb7978882bddd17d3f39f92afcd8a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 12:56:58 -0700 Subject: [PATCH 0152/1013] Notes on changes --- UPGRADE.md | 1 + WHATSNEW.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 5975b96e..bec189ed 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -40,6 +40,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or # 0.0.8-alpha to 0.0.9-alpha * Development is now against Node.js 8.x LTS. Follow your standard upgrade path to update to Node 8.x before using 0.0.9-alpha. * The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. +* Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork. # 0.0.7-alpha to 0.0.8-alpha diff --git a/WHATSNEW.md b/WHATSNEW.md index d5d5fee2..99c47786 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -4,6 +4,12 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.9-alpha * Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! * Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!) +* Menu items can now be arrays of *objects* not just arrays of strings. + * The properties `itemFormat` and `focusItemFormat` allow you to supply the string format for items. For example if a menu object is `{ "userName" : "Bob", "age" : 35 }`, a `itemFormat` might be `|04{userName} |08- |14{age}`. + * If no `itemFormat` is supplied, the default formatter is `{text}`. + * Setting the `data` member of an object will cause form submissions to use this value instead of the selected items index. + * See the default `luciano_blocktronics` `matrix` menu for example usage. +* You can now set the `sort` property on a menu to sort items. If `true` items are sorted by `text`. If the value is a string, it represents the key in menu objects to sort by. ## 0.0.8-alpha From 70eefc008a997f367c5c6aa44cc83f6b08290f64 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 12:59:20 -0700 Subject: [PATCH 0153/1013] Update matrix example to show item formatting --- art/themes/luciano_blocktronics/theme.hjson | 3 +- config/menu.hjson | 33 +++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 433f8884..27bd44ca 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -22,7 +22,8 @@ matrix: { mci: { VM1: { - focusTextStyle: first lower + itemFormat: "|03{text}" + focusItemFormat: "|11{text!styleFirstLower}" } } } diff --git a/config/menu.hjson b/config/menu.hjson index b8d26521..3fb9c229 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -67,26 +67,47 @@ submit: true focus: true argName: navSelect - // :TODO: need a good way to localize these ... Standard Orig->Lookup seems good. - items: [ "login", "apply", "forgot pw", "log off" ] + // + // To enable forgot password, you will need to have the web server + // enabled and mail/SMTP configured. Once that is in place, swap out + // the commented lines below as well as in the submit block + // + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + { + text: forgot pass + data: forgot + } + { + text: log off + data: logoff + } + ] } } submit: { *: [ { - value: { navSelect: 0 } + value: { navSelect: "login" } action: @menu:login } { - value: { navSelect: 1 }, + value: { navSelect: "apply" } action: @menu:newUserApplicationPre } { - value: { navSelect: 2 } + value: { navSelect: "forgot" } action: @menu:forgotPassword } { - value: { navSelect: 3 }, + value: { navSelect: "logoff" } action: @menu:logoff } ] From b6317e05415dcaaeb8763d9d08d114b438faf9ac Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 13:02:24 -0700 Subject: [PATCH 0154/1013] File Base area selection using new simplified formatting --- config/menu.hjson | 4 ++-- core/file_base_area_select.js | 35 +++++++++-------------------------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index 3fb9c229..cf2c71dd 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -2782,14 +2782,14 @@ mci: { VM1: { focus: true - argName: areaSelect + argName: areaTag } } submit: { *: [ { - value: { areaSelect: null } + value: { areaTag: null } action: @method:selectArea } ] diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 87dfe9f4..f762ab1d 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -3,8 +3,7 @@ // enigma-bbs const MenuModule = require('./menu_module.js').MenuModule; -const stringFormat = require('./string_format.js'); -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const { getSortedAvailableFileAreas } = require('./file_base_area.js'); const StatLog = require('./stat_log.js'); // deps @@ -24,16 +23,10 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { constructor(options) { super(options); - this.config = this.menuConfig.config || {}; - - this.loadAvailAreas(); - this.menuMethods = { selectArea : (formData, extraArgs, cb) => { - const area = this.availAreas[formData.value.areaSelect] || 0; - const filterCriteria = { - areaTag : area.areaTag, + areaTag : formData.value.areaTag, }; const menuOpts = { @@ -48,10 +41,6 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { }; } - loadAvailAreas() { - this.availAreas = getSortedAvailableFileAreas(this.client); - } - mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { @@ -60,35 +49,29 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { const self = this; - async.series( + async.waterfall( [ function mergeAreaStats(callback) { const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; - self.availAreas.forEach(area => { + // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override + const availAreas = getSortedAvailableFileAreas(self.client); + availAreas.forEach(area => { const stats = areaStats.areas[area.areaTag]; area.totalFiles = stats ? stats.files : 0; area.totalBytes = stats ? stats.bytes : 0; }); - return callback(null); + return callback(null, availAreas); }, - function prepView(callback) { + function prepView(availAreas, callback) { self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => { if(err) { return callback(err); } const areaListView = vc.getView(MciViewIds.areaList); - - const areaListFormat = self.config.areaListFormat || '{name}'; - - areaListView.setItems(self.availAreas.map(a => stringFormat(areaListFormat, a) ) ); - - if(self.config.areaListFocusFormat) { - areaListView.setFocusItems(self.availAreas.map(a => stringFormat(self.config.areaListFocusFormat, a) ) ); - } - + areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } ))); areaListView.redraw(); return callback(null); From 999033ec154848adf5d261e187b18825fd93d28c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 13:03:11 -0700 Subject: [PATCH 0155/1013] New menu sorting, fix up default SGR --- art/themes/luciano_blocktronics/MATRIX.ANS | Bin 4797 -> 5117 bytes core/horizontal_menu_view.js | 2 +- core/menu_view.js | 33 +++++++++++++++++++-- core/vertical_menu_view.js | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/MATRIX.ANS b/art/themes/luciano_blocktronics/MATRIX.ANS index 4e18372379762657be552e9bb01e2e8df65401ee..14219543d7a87cf9e911adada5c27c3a5f07c8bd 100644 GIT binary patch delta 884 zcmZ8fyG{Z@6b%sMO#~8~MKTEzO$>3D#RbGht+hdoLP-1or?J4qM>N(pdTmH(EUat< zYU5W}?IzpV8xlLkxihoL4$aQq$2oV-nfqS)U7puEdb6rG%^Hns9aE`%(&4VbWXKZ>NN6-6pIlSm!Ukw1dkbEdX;DJF zu~qm=786)9BttIMF7a5d2fIMELK#5k>&1R;d8a|@mXo45gR({7Fcbpi^hXLKbPlbc z<4L*@%yVI!*P`m?SQH8wu_)cl3deF<#KL`M{|4El^R^u)u5H_{LzL}foMJPjIBvQ~ hdN#|H<6tA1$+~i}C7eGIgNp}P&7KCIa?sB%{R4tV2Ot0d delta 567 zcmYjOJxjx25XQ7k+w@CQ5F~!QpjEdfO`En<#rkd)CyQu<;Gk2@;vk49qPQ011{EB2 zaaM#srjGA2! zLjU<-Y`LsEnqB2YHcO5dP+aCQ#ykuzrifV_R;9jF;qW6hVLdX67$zUIqO~!?da)A; z8EJW&zPj?BT=yHor~D#Ug7hrDk){-f^hpK`uLUdVwSm{#_s z8i!luDP6Jt{K)_-8no)CH%5n-jTLyB%*R0~g z7XE`|rubLUhgRlvhETtPG2)ZV5Wvwe%*-pkTZUg9&320ZJS#L1jsEAP(Cl)_!9Hle zzcmvAZn9SseofUuQ&BawhpF60JUqkTJ2xw3Xid}|8b7Bt;YeK!D7f~6+TILzD>*_a o%o9R*F7wY66b9EL{d7Y#h>lNSkbB{I$p;U*`D3ZA!dHIb7Z850UjP6A diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index ee1d5318..02f9c06e 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -71,7 +71,7 @@ function HorizontalMenuView(options) { text = focusItem ? focusItem.text : item.text; sgr = ''; } else if(this.complexItems) { - text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); diff --git a/core/menu_view.js b/core/menu_view.js index 56899077..f15491cf 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -64,6 +64,8 @@ util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { + this.sorted = false; + // // Items can be an array of strings or an array of objects. // @@ -91,13 +93,38 @@ MenuView.prototype.setItems = function(items) { }); if(this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; - this.focusItemFormat = this.focusItemFormat || this.itemFormat; + this.itemFormat = this.itemFormat || '{text}'; } } }; +MenuView.prototype.setSort = function(sort) { + if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; + } + + const key = true === sort ? 'text' : sort; + if('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort( (a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if(!a1) { + return -1; + } + if(!b1) { + return 1; + } + return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); + }); + + this.sorted = true; +}; + MenuView.prototype.removeItem = function(index) { + this.sorted = false; this.items.splice(index, 1); if(this.focusItems) { @@ -203,6 +230,8 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'focusItemFormat' : this[propName] = value; break; + + case 'sort' : this.setSort(value); break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index e1d6cd17..6f083193 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -71,7 +71,7 @@ function VerticalMenuView(options) { text = focusItem ? focusItem.text : item.text; sgr = ''; } else if(this.complexItems) { - text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); From c81aa001f47fe868b2217c9682e8469ba6c61088 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 13:22:47 -0700 Subject: [PATCH 0156/1013] Fix typo --- core/telnet_bridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 6fb81d21..447efe4c 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -126,7 +126,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConf.config'), options.extraArgs); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config.port = this.config.port || 23; } From 06ea2d1600b5275962454f15ba6ff7822ea7c4fc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 19:30:21 -0700 Subject: [PATCH 0157/1013] Code readability --- core/bbs_list.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/bbs_list.js b/core/bbs_list.js index 33a7ff59..ecf41c76 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -150,7 +150,10 @@ exports.getModule = class BBSListModule extends MenuModule { 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 ], + [ + 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'); From 26849ba4fa943aa47d958890cff48162112504c2 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Wed, 31 Jan 2018 23:35:54 +0000 Subject: [PATCH 0158/1013] * Docs theme to match ENiGMA website * New docs layout ready for github pages serving * Tonnes of new docs * Update gitignore * Probably other stuff too --- .gitignore | 5 +- docs/404.html | 24 ++ docs/Gemfile | 31 ++ docs/Gemfile.lock | 101 ++++++ docs/_config.yml | 28 ++ docs/_includes/nav.md | 67 ++++ docs/_layouts/default.html | 41 +++ docs/_layouts/page.html | 8 + docs/_layouts/post.html | 25 ++ docs/_sass/_default_colors.scss | 16 + docs/_sass/jekyll-theme-hacker.scss | 326 ++++++++++++++++++ docs/_sass/rouge-base16-dark.scss | 87 +++++ docs/about.md | 19 - docs/art/general.md | 6 + docs/art/mci.md | 135 ++++++++ docs/art/themes.md | 25 ++ docs/assets/css/style.scss | 4 + docs/assets/images/bkg.png | Bin 0 -> 1218 bytes docs/assets/images/bullet.png | Bin 0 -> 603 bytes docs/assets/images/enigma-logo.png | Bin 0 -> 16643 bytes .../archivers.md} | 5 +- .../config-hjson.md} | 19 +- docs/configuration/creating-config.md | 27 ++ docs/configuration/directory-structure.md | 20 ++ docs/configuration/editing-hjson.md | 16 + docs/configuration/menu-hjson.md | 101 ++++++ docs/configuration/sysop-setup.md | 6 + docs/file_base.md | 89 ----- docs/filebase/acs.md | 25 ++ docs/filebase/first-file-area.md | 71 ++++ docs/filebase/index.md | 19 + docs/filebase/tic-support.md | 100 ++++++ docs/filebase/uploads.md | 8 + docs/filebase/web-access.md | 11 + docs/index.md | 150 ++------ docs/installation/docker.md | 21 ++ docs/installation/install-script.md | 16 + docs/installation/installation-methods.md | 13 + docs/installation/manual.md | 74 ++++ docs/installation/network.md | 6 + docs/{rpi.md => installation/os-hardware.md} | 28 +- docs/installation/production.md | 12 + docs/installation/testing.md | 44 +++ docs/mci.md | 113 ------ docs/menu_system.md | 86 ----- docs/messageareas/bso-import-export.md | 74 ++++ .../configuring-a-message-area.md | 65 ++++ docs/messageareas/message-networks.md | 67 ++++ docs/messageareas/netmail.md | 38 ++ docs/modding.md | 15 - docs/modding/door-servers.md | 61 ++++ docs/modding/existing-mods.md | 11 + docs/{doors.md => modding/local-doors.md} | 74 +--- docs/mods.md | 9 - docs/msg_conf_area.md | 57 --- docs/msg_networks.md | 196 ----------- docs/oputil/index.md | 17 + docs/servers/ssh.md | 36 ++ docs/servers/telnet.md | 4 + docs/{web_server.md => servers/web-server.md} | 11 +- .../websocket.md} | 6 +- docs/troubleshooting/monitoring-logs.md | 15 + 62 files changed, 1974 insertions(+), 810 deletions(-) create mode 100644 docs/404.html create mode 100644 docs/Gemfile create mode 100644 docs/Gemfile.lock create mode 100644 docs/_config.yml create mode 100644 docs/_includes/nav.md create mode 100644 docs/_layouts/default.html create mode 100644 docs/_layouts/page.html create mode 100644 docs/_layouts/post.html create mode 100644 docs/_sass/_default_colors.scss create mode 100644 docs/_sass/jekyll-theme-hacker.scss create mode 100644 docs/_sass/rouge-base16-dark.scss delete mode 100644 docs/about.md create mode 100644 docs/art/general.md create mode 100644 docs/art/mci.md create mode 100644 docs/art/themes.md create mode 100644 docs/assets/css/style.scss create mode 100644 docs/assets/images/bkg.png create mode 100644 docs/assets/images/bullet.png create mode 100644 docs/assets/images/enigma-logo.png rename docs/{archive.md => configuration/archivers.md} (98%) rename docs/{config.md => configuration/config-hjson.md} (85%) create mode 100644 docs/configuration/creating-config.md create mode 100644 docs/configuration/directory-structure.md create mode 100644 docs/configuration/editing-hjson.md create mode 100644 docs/configuration/menu-hjson.md create mode 100644 docs/configuration/sysop-setup.md delete mode 100644 docs/file_base.md create mode 100644 docs/filebase/acs.md create mode 100644 docs/filebase/first-file-area.md create mode 100644 docs/filebase/index.md create mode 100644 docs/filebase/tic-support.md create mode 100644 docs/filebase/uploads.md create mode 100644 docs/filebase/web-access.md create mode 100644 docs/installation/docker.md create mode 100644 docs/installation/install-script.md create mode 100644 docs/installation/installation-methods.md create mode 100644 docs/installation/manual.md create mode 100644 docs/installation/network.md rename docs/{rpi.md => installation/os-hardware.md} (52%) create mode 100644 docs/installation/production.md create mode 100644 docs/installation/testing.md delete mode 100644 docs/mci.md delete mode 100644 docs/menu_system.md create mode 100644 docs/messageareas/bso-import-export.md create mode 100644 docs/messageareas/configuring-a-message-area.md create mode 100644 docs/messageareas/message-networks.md create mode 100644 docs/messageareas/netmail.md delete mode 100644 docs/modding.md create mode 100644 docs/modding/door-servers.md create mode 100644 docs/modding/existing-mods.md rename docs/{doors.md => modding/local-doors.md} (76%) delete mode 100644 docs/mods.md delete mode 100644 docs/msg_conf_area.md delete mode 100644 docs/msg_networks.md create mode 100644 docs/oputil/index.md create mode 100644 docs/servers/ssh.md create mode 100644 docs/servers/telnet.md rename docs/{web_server.md => servers/web-server.md} (82%) rename docs/{vtx_web_client.md => servers/websocket.md} (96%) create mode 100644 docs/troubleshooting/monitoring-logs.md diff --git a/.gitignore b/.gitignore index ee9fc3d8..5a58c717 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ logs/ db/ dropfiles/ -node_modules/ \ No newline at end of file +node_modules/ +docs/_site/ +docs/.sass-cache/ +` \ No newline at end of file diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 00000000..c472b4ea --- /dev/null +++ b/docs/404.html @@ -0,0 +1,24 @@ +--- +layout: default +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..1a0104dd --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,31 @@ +source "https://rubygems.org" + +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +gem "jekyll", "~> 3.7.0" + +# This is the default theme for new Jekyll sites. You may change this to anything you like. +gem "hacker" + +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +# gem "github-pages", group: :jekyll_plugins + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.6" + gem 'jekyll-seo-tag' + gem 'jekyll-theme-hacker' + gem 'jekyll-sitemap' + gem 'jemoji' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] + diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 00000000..f49b797b --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,101 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (4.2.9) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + colorator (1.1.0) + concurrent-ruby (1.0.5) + em-websocket (0.5.1) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + eventmachine (1.2.5) + ffi (1.9.18) + forwardable-extended (2.6.0) + gemoji (3.0.0) + hacker (0.0.1) + html-pipeline (2.7.1) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.6.0) + i18n (0.9.1) + concurrent-ruby (~> 1.0) + jekyll (3.7.0) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 0.7) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (~> 1.14) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-feed (0.9.2) + jekyll (~> 3.3) + jekyll-sass-converter (1.5.1) + sass (~> 3.4) + jekyll-seo-tag (2.4.0) + jekyll (~> 3.3) + jekyll-sitemap (1.1.1) + jekyll (~> 3.3) + jekyll-theme-hacker (0.1.0) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-watch (2.0.0) + listen (~> 3.0) + jemoji (0.8.1) + activesupport (~> 4.0, >= 4.2.9) + gemoji (~> 3.0) + html-pipeline (~> 2.2) + jekyll (>= 3.0) + kramdown (1.16.2) + liquid (4.0.0) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) + mercenary (0.3.6) + mini_portile2 (2.3.0) + minitest (5.11.1) + nokogiri (1.8.1) + mini_portile2 (~> 2.3.0) + pathutil (0.16.1) + forwardable-extended (~> 2.6) + public_suffix (3.0.1) + rb-fsevent (0.10.2) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + rouge (3.1.0) + ruby_dep (1.5.0) + safe_yaml (1.0.4) + sass (3.5.5) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + thread_safe (0.3.6) + tzinfo (1.2.4) + thread_safe (~> 0.1) + +PLATFORMS + ruby + +DEPENDENCIES + hacker + jekyll (~> 3.7.0) + jekyll-feed (~> 0.6) + jekyll-seo-tag + jekyll-sitemap + jekyll-theme-hacker + jemoji + tzinfo-data + +BUNDLED WITH + 1.16.1 diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..b791e4fb --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,28 @@ +title: ENiGMA½ BBS Software +email: your-email@example.com +description: >- # this means to ignore newlines until "baseurl:" + ENiGMA½ BBS is modern open source BBS software with a nostalgic flair, written in Node.js. +url: +logo: /assets/images/enigma-logo.png + +# Build settings +markdown: kramdown +theme: jekyll-theme-hacker +plugins: + - jekyll-feed + - jekyll-seo-tag + - jekyll-sitemap + - jemoji + +# Exclude from processing. +# The following items will not be processed, by default. Create a custom list +# to override the default setting. +exclude: + - Gemfile + - Gemfile.lock + - node_modules + - vendor/bundle/ + - vendor/cache/ + - vendor/gems/ + - vendor/ruby/ + - .idea diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md new file mode 100644 index 00000000..f8fbb643 --- /dev/null +++ b/docs/_includes/nav.md @@ -0,0 +1,67 @@ + - Installation + - [Installation Methods](/installation/installation-methods) + - [Install script](/installation/install-script) + - [Docker](/installation/docker) + - [Manual installation](/installation/manual) + - [OS / Hardware Specific](/installation/os-hardware) + - Raspberry Pi + - Windows + - [Your Network Setup](/installation/network) + - [Testing Your Installation](/installation/testing) + - [Production Installation](/installation/production) + + - Configuration + - [Creating Config Files](/configuration/creating-config) + - [SysOp Setup](/configuration/sysop-setup) + - [Editing hjson](/configuration/editing-hjson) + - [config.hjson](/configuration/config-hjson) + - [menu.hjson](/configuration/menu-hjson) + - prompt.hjson + - [Directory Structure](/configuration/directory-structure) + - [Archivers](/configuration/archivers) + - Scheduled jobs + - SMTP + + - File Base + - [About](/filebase/) + - [Configuring a File Area](/filebase/first-file-area) + - [ACS model](/filebase/acs) + - [Uploads](/filebase/uploads) + - [Web Access](/filebase/web-access) + - [TIC Support](/filebase/tic-support) (Importing from FTN networks) + - Tips and tricks + - Network mounts and symlinks + + - Message Areas + - [Configuring a Message Area](/messageareas/configuring-a-message-area) + - [Message networks](/messageareas/message-networks) + - [BSO Import & Export](/messageareas/bso-import-export) + - [Netmail](/messageareas/netmail) + + - Art + - [General](/art/general) + - [Themes](/art/themes) + - [MCI Codes](/art/mci) + + - Servers + - Login Servers + - [Telnet](/servers/telnet) + - [SSH](/servers/ssh) + - [WebSocket](/servers/websocket) + - Build your own + - Content Servers + - [Web](/servers/web-server) + + - Modding + - [Local Doors](/modding/local-doors) + - [Door Servers](/modding/door-servers) + - DoorParty + - BBSLink + - Combatnet + - Exodus + - [Existing Mods](/modding/existing-mods) + + - [Oputil](/oputil/) + + - Troubleshooting + - [Monitoring Logs](/troubleshooting/monitoring-logs) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 00000000..8b01d854 --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,41 @@ + + + + + + + + + {% seo %} + + + + + Fork me on GitHub + +
+
+
+ {{ content }} +
+
+
+ + {% if site.google_analytics %} + + {% endif %} + + diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html new file mode 100644 index 00000000..404d540a --- /dev/null +++ b/docs/_layouts/page.html @@ -0,0 +1,8 @@ +--- +layout: default +--- + +
+

{{ page.title }}

+ {{ content }} +
\ No newline at end of file diff --git a/docs/_layouts/post.html b/docs/_layouts/post.html new file mode 100644 index 00000000..f3def1e6 --- /dev/null +++ b/docs/_layouts/post.html @@ -0,0 +1,25 @@ +--- +layout: default +--- + +
+

{{ page.title }}

+ + {{ content }} +
+ + \ No newline at end of file diff --git a/docs/_sass/_default_colors.scss b/docs/_sass/_default_colors.scss new file mode 100644 index 00000000..4227975a --- /dev/null +++ b/docs/_sass/_default_colors.scss @@ -0,0 +1,16 @@ +$apple-blossom: #ac4142; +$alto: #d0d0d0; +$bouquet: #aa759f; +$enigma-purple: #8900aa; +$chelsea-cucumber: #90a959; +$cod-grey: #151515; +$conifer: #b5e853; +$dove-grey: #666; +$gallery: #eaeaea; +$grey: #888; +$gulf-stream: #75b5aa; +$hippie-blue: #6a9fb5; +$potters-clay: #8f5536; +$rajah: #f4bf75; +$raw-sienna: #d28445; +$silver-chalice: #aaa; diff --git a/docs/_sass/jekyll-theme-hacker.scss b/docs/_sass/jekyll-theme-hacker.scss new file mode 100644 index 00000000..e02b5aad --- /dev/null +++ b/docs/_sass/jekyll-theme-hacker.scss @@ -0,0 +1,326 @@ +@import "rouge-base16-dark"; +@import "default_colors"; + +$body-background: $cod-grey !default; +$body-foreground: $gallery !default; +$header: $conifer !default; +$blockquote-color: $silver-chalice !default; +$blockquote-border: $dove-grey !default; + +body { + margin: 0; + padding: 0; + background: $body-background url("../images/bkg.png") 0 0; + color: $body-foreground; + font-size: 16px; + line-height: 1.5; + font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; +} + +/* General & 'Reset' Stuff */ + +.container { + width: 90%; + /*max-width: 1000px;*/ + margin: 0 auto; +} + +section { + display: block; + margin: 0 0 20px 0; +} + +h1, h2, h3, h4, h5, h6 { + margin: 0 0 20px; +} + +li { + line-height: 1.4 ; +} + +/* Header,
+ header - container + h1 - project name + h2 - project description +*/ + +header { + background: rgba(0, 0, 0, 0.1); + width: 100%; + border-bottom: 1px dashed $conifer; //header; + padding: 20px 0; + margin: 0 0 40px 0; + text-align: center; +} + +header h1 { + font-size: 30px; + line-height: 1.5; + margin: 0 0 0 -40px; + font-weight: bold; + font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; + color: $conifer;//$header; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), + 0 0 5px rgba(181, 232, 83, 0.1), + 0 0 10px rgba(181, 232, 83, 0.1); + letter-spacing: -1px; + -webkit-font-smoothing: antialiased; +} + +header h1:before { + content: "./ "; + font-size: 24px; +} + +header h2 { + font-size: 18px; + font-weight: 300; + color: #666; +} + +header img { + padding: 0px; + margin: 0px; + max-width: 100%; +} + +#downloads .btn { + display: inline-block; + text-align: center; + margin: 0; +} + +/* Main Content +*/ +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 22rem; + height: 100%; + text-align: left; + border-right: 1px dashed $conifer; //header; + overflow-y: scroll; + display: block; + float: left; + + .logo { + padding-top: 20px; + max-width: 100%; + } + + ul { + padding-bottom: 0px; + } + ul li { + margin-bottom: 0px; + padding-top: 5px; + } +} + +.main_area { + padding-left: 22em; + padding-top: 20px; +} + +#main_content { + + -webkit-font-smoothing: antialiased; +} +section img { + max-width: 100% +} + +h1, h2, h3, h4, h5, h6 { + font-weight: normal; + font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; + color: $header; + letter-spacing: -0.03em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), + 0 0 5px rgba(181, 232, 83, 0.1), + 0 0 10px rgba(181, 232, 83, 0.1); +} + +#main_content h1 { + font-size: 30px; +} + +#main_content h2 { + font-size: 24px; +} + +#main_content h3 { + font-size: 18px; +} + +#main_content h4 { + font-size: 14px; +} + +#main_content h5 { + font-size: 12px; + text-transform: uppercase; + margin: 0 0 5px 0; +} + +#main_content h6 { + font-size: 12px; + text-transform: uppercase; + color: #999; + margin: 0 0 5px 0; +} + +dt { + font-style: italic; + font-weight: bold; +} + +ul { + padding-bottom: 5px; +} + +ul li { + list-style-image:url('../images/bullet.png'); + margin-bottom: 10px; + padding-top: 10px; +} + +nav { + text-align: left; + + ul { + list-style: none; + margin-left: 0; + padding-left: 0; + margin-top: 20; + margin-bottom: 0; + padding-bottom: 0; + + li { + display: inline-block; + margin-left: 0.5em; + padding-right: 20px; + + a { + color: $enigma-purple; + text-decoration: none; + } + + a:hover { + color: $bouquet; + } + } + } +} + + +blockquote { + color: $blockquote-color; + padding-left: 10px; + border-left: 1px dotted $blockquote-border; +} + +pre { + background: rgba(0, 0, 0, 0.9); + border: 1px solid rgba(255, 255, 255, 0.15); + padding: 10px; + font-size: 16px; + color: #b5e853; + border-radius: 2px; + text-wrap: normal; + overflow: auto; + overflow-y: hidden; +} + +code.highlighter-rouge { + background: rgba(0,0,0,0.9); + border: 1px solid rgba(255, 255, 255, 0.15); + padding: 0px 3px; + margin: 0px -3px; + color: #aa759f; + border-radius: 2px; +} + +table { + width: 100%; + margin: 0 0 20px 0; +} + +th { + text-align: left; + border-bottom: 1px dashed #b5e853; + padding: 5px 10px; +} + +td { + padding: 5px 10px; +} + +hr { + height: 0; + border: 0; + border-bottom: 1px dashed #b5e853; + color: #b5e853; +} + +/* Buttons +*/ + +.btn { + display: inline-block; + background: -webkit-linear-gradient(top, rgba(40, 40, 40, 0.3), rgba(35, 35, 35, 0.3) 50%, rgba(10, 10, 10, 0.3) 50%, rgba(0, 0, 0, 0.3)); + padding: 8px 18px; + border-radius: 50px; + border: 2px solid rgba(0, 0, 0, 0.7); + border-bottom: 2px solid rgba(0, 0, 0, 0.7); + border-top: 2px solid rgba(0, 0, 0, 1); + color: rgba(255, 255, 255, 0.8); + font-family: Helvetica, Arial, sans-serif; + font-weight: bold; + font-size: 13px; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.75); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.btn:hover { + background: -webkit-linear-gradient(top, rgba(40, 40, 40, 0.6), rgba(35, 35, 35, 0.6) 50%, rgba(10, 10, 10, 0.8) 50%, rgba(0, 0, 0, 0.8)); +} + +.btn .icon { + display: inline-block; + width: 16px; + height: 16px; + margin: 1px 8px 0 0; + float: left; +} + +.btn-github .icon { + opacity: 0.6; + background: url("../images/blacktocat.png") 0 0 no-repeat; +} + +/* Links + a, a:hover, a:visited +*/ + +a { + color: #63c0f5; + text-shadow: 0 0 5px rgba(104, 182, 255, 0.5); +} + +/* Clearfix */ + +.cf:before, .cf:after { + content:""; + display:table; +} + +.cf:after { + clear:both; +} + +.cf { + zoom:1; +} diff --git a/docs/_sass/rouge-base16-dark.scss b/docs/_sass/rouge-base16-dark.scss new file mode 100644 index 00000000..7f839e96 --- /dev/null +++ b/docs/_sass/rouge-base16-dark.scss @@ -0,0 +1,87 @@ +/* + generated by rouge http://rouge.jneen.net/ + original base16 by Chris Kempson (https://github.com/chriskempson/base16) +*/ + +@import "default_colors"; + +.highlight { + + $plaintext: $alto !default; + $string: $chelsea-cucumber !default; + $literal: $chelsea-cucumber !default; + $keyword: $bouquet !default; + $error-foreground: $cod-grey !default; + $error-background: $apple-blossom !default; + $comment: $grey !default; + $preprocessor: $rajah !default; + $name-space: $rajah !default; + $name-attribute: $hippie-blue !default; + $operator: $rajah !default; + $keyword-type: $raw-sienna !default; + $regex: $gulf-stream !default; + $string-escape: $potters-clay !default; + $deleted: $apple-blossom !default; + $header: $hippie-blue !default; + + color: $plaintext; + + table td { padding: 5px; } + table pre { margin: 0; } + .w { + color: $plaintext; + } + .err { + color: $error-foreground; + background-color: $error-background; + } + .c, .cd, .cm, .c1, .cs { + color: $comment; + } + .cp { + color: $preprocessor; + } + .o, .ow { + color: $operator; + } + .p, .pi { + color: $plaintext; + } + .gi { + color: $string; + } + .gd { + color: $deleted; + } + .gh { + color: $header; + font-weight: bold; + } + .k, .kn, .kp, .kr, .kv { + color: $keyword; + } + .kc, .kt, .kd { + color: $keyword-type; + } + .s, .sb, .sc, .sd, .s2, .sh, .sx, .s1 { + color: $string; + } + .sr { + color: $regex; + } + .si, .se { + color: $string-escape; + } + .nt, .nn, .nc, .no{ + color: $name-space; + } + .na { + color: $name-attribute; + } + .m, .mf, .mh, .mi, .il, .mo, .mb, .mx { + color: $literal; + } + .ss { + color: $string; + } +} diff --git a/docs/about.md b/docs/about.md deleted file mode 100644 index 50656011..00000000 --- a/docs/about.md +++ /dev/null @@ -1,19 +0,0 @@ -# About ENiGMA½ - -## High Level Feature Overview - * 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/art/general.md b/docs/art/general.md new file mode 100644 index 00000000..c43d915c --- /dev/null +++ b/docs/art/general.md @@ -0,0 +1,6 @@ +--- +layout: page +title: General +--- +General art lives in the `art/general` directory. 'General' art is ANSI you want to stay consistent across themes, +such as a welcome ANSI or a rotation of logoff ANSIs. diff --git a/docs/art/mci.md b/docs/art/mci.md new file mode 100644 index 00000000..4befc411 --- /dev/null +++ b/docs/art/mci.md @@ -0,0 +1,135 @@ +--- +layout: page +title: MCI Codes +--- +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 instantiate 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. + +## Predefined MCI Codes +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](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) +for a full listing. Many codes attempt to pay homage to Oblivion/2, +iNiQUiTY, etc. + +| Code | Description | +|------|--------------| +| `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 | +| `SD` | Total downloads, system wide | +| `SO` | Total downloaded amount, system wide (formatted to appropriate bytes/megs/etc.) | +| `SU` | Total uploads, system wide | +| `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | + +A special `XY` MCI code may also be utilized for placement identification when creating menus. + + +## 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`): Old-school BBSers may recognize this as a lightbar menu. + +| Code | Name | Description | +|------|----------------------|------------------| +| `TL` | Text Label | Displays text | +| `ET` | Edit Text | Collect user input | +| `ME` | Masked Edit Text | Collect user input using a *mask* | +| `MT` | Multi Line Text Edit | Multi line edit control | +| `BT` | Button | A button | +| `VM` | Vertical Menu | A vertical menu aka a vertical lightbar | +| `HM` | Horizontal Menu | A horizontal menu aka a horizontal lightbar | +| `SM` | Spinner Menu | A spinner input control | +| `TM` | Toggle Menu | A toggle menu commonly used for Yes/No style input | +| `KE` | Key Entry | A *single* key input control | + + +Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to +see additional information. + + +## Properties & Theming +Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. + +### Common Properties + +| Property | Description | +|-------------|--------------| +| `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 | +| `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! + + +### Text Styles + +Standard style types available for `textStyle` and `focusTextStyle`: + +| Style | Description | +|----------|--------------| +| `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 diff --git a/docs/art/themes.md b/docs/art/themes.md new file mode 100644 index 00000000..37631454 --- /dev/null +++ b/docs/art/themes.md @@ -0,0 +1,25 @@ +--- +layout: page +title: Themes +--- +:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included +`luciano_blocktronics' theme. Create your own and make changes to that instead: + +1. Copy `/art/themes/luciano_blocktronics` to `art/themes/your_board_theme` +2. Update the `info` block at the top of the theme.hjson file: + + info: { + name: Awesome Theme + author: Cool Artist + group: Sick Group + enabled: true + } + +3. Specify it in the `defaults` section of `config.hjson`. The name supplied should match the name of the +directory you created in step 1: + + ```hjson + defaults: { + theme: your_board_theme + } + ``` diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss new file mode 100644 index 00000000..5f1392bd --- /dev/null +++ b/docs/assets/css/style.scss @@ -0,0 +1,4 @@ +--- +--- + +@import 'jekyll-theme-hacker'; diff --git a/docs/assets/images/bkg.png b/docs/assets/images/bkg.png new file mode 100644 index 0000000000000000000000000000000000000000..d10e5caf1adb7466673bc9cef83621fe855caa3f GIT binary patch literal 1218 zcmeAS@N?(olHy`uVBq!ia0y~yU=jggMrNQ$#TNBtK#DEEC&X1sN=jNkczmscWgJUHsE1R2ws}X9=R$~%X@*3=-vhL@-NfkR)!pBoqVTzzlmJ$gw$h( za!N&gzAjH}?UE-yuB@->I#KvPao+dRzrL@oCvVpee)R8SO|Rip8+r3Dza~8XxNqSP zmGuR0<=?XLN*y=&ko@A*R?n;zmFGjmpL|X=el)GDnT?nCc-hJ9h-*>(U-v7SP21O( zoNu0S!J*))p{S5clIq(!Rnv%W88O`}e-}D7AF~mxZfpV+3kwX-$FQx)Y;JvA%`)@d zWYLYgd8JI0aw{r!Iv89Q>eyImcyRi@1&bblZJ%(NN9SER*F|RI`9Vy^n|kYj&a|m< zH+i{|PR}_n7 z-gd`kwp)J%TC%IUg`6V4t}fWH9jMCX-LAENwM{str7XO1;E!GNEuasI-gOzw-pnlj z>{){B-p0ScV3upi-pM7+*5to^wpdM0Lo6uhXKvGLxTUb-aiDN)U^44#klW=8|4*sV zntfrzKU=lq{IC4u?*i3*Ut)0K@G9flAeW??UW>QI-$6b&yzF|LRw2jj$QO!RxR2R@ z!p-JEk`UjzM%QUGt5 zsq{^Wh=(VtSvB4F14E?hZ{X9Y?R-}fa@G1cS_4|1{K5DvW*TQF%BQWRm-W8rcnHXW5F_LnOa!0te-Gud{_`k1m{6m9unPa(^B$ zKx8W_XRUv@r+&lLzc+3*l|BZA<&8P3LiGe?mL9UxX48rf2YHj@p3l_yh=pvc)~tHC z{nO?fprlo3bGdkWi`e7f5A45eIR!{NSV4~V*iRMLf15OnS&>IfjrQ$5*USNy<-2E*PZ71vSAxg$R}ir z*HrJV2QIesUSGbBCF#9c#svfK2|7o#H8-glPWYpw_zvjL75X#RJY~u`5Y-X(HrD(= zUCM<84_%p-W@|H8MF_Nfy!EQ%2FOzxtQJYFtLINQ#Ng@b=d#Wzp$P!*bu5qo literal 0 HcmV?d00001 diff --git a/docs/assets/images/bullet.png b/docs/assets/images/bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f8de1a7e0d469d77b8852396dd10f338ae23f2 GIT binary patch literal 603 zcmeAS@N?(olHy`uVBq!ia0vp^57${dCd>QY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fI&pI+kyVsc_(xbi&_G6SPZ!4!i_=@D?9F0w6md%rmzMVCWO3ZMStxs>APeV` z6HToPk{)y4eARTTV}i&**7yU5F8RqPByJ7f7l(V(z*fP#y%N{ZA*k8}SM*o+)sk0^| zVWo&jS?rFxude<7%Cf>m>EMbm?l6Vrx6ksBe>HsTzdZg`C*I{YbAygX z`No(wBmKJvw3*sg? literal 0 HcmV?d00001 diff --git a/docs/assets/images/enigma-logo.png b/docs/assets/images/enigma-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..94f652aeabed0302d06b927b41c59c473ba745cb GIT binary patch literal 16643 zcma*P1yq#n7cWX9EuwTNAV`;Vsh~(piAZ10R+s4I2iJ_vUkKmX4y z59j;8kQ`-p2p&OC%5dhyQ?8An_IJ+?_P(e2^3Mump!{dyi~qadf;>s_zV9#LfR5%U zN*CDuQ6zUMy;5KfEwYajbB8we^ESV-0D^9_-g5$9>p%0wIMO&Rqzl;fooGWX!;yJ65H;gdNv-7NGGSa)4+F&KnFN=|0W`d27 z${0GV7)LYzB|rR$1@B`yOPD%le1MC2W7k#G zXshM!W7n?$Cx@8%1yV(X(y&e+bl7B8`lm0Wc&CVHIhqEY;E!@rp*IjyF~U%e7=$Jh z^+Cz{GUmwoXL3QQ)hphl(<@nUuo|*z2hw*Sjt82c!j{u6^l+z%eCkW}{9y~&LkLK; z&sYe?H5t8lfFZM5LDQx8>%&b>(e#E4g{>4y^T=ztw*VaojclT?j%TVxa6R;}ATr+Y zjxN%%|BbF(_?tV%DdtB!N4{at;Wm?6yyOpJwRTcs5-caZK9+75EEsUQW07!J5|Mpb zr(Ik*=PzI{=M9jyug3YgEZ{4gm#)Bj>C6!&`!3yNFHebL%{J%L4m zyF@Xw9xVv$;Y_*7)Y+NJc05G46=C#=aY zw_<-2$?2OHm?rO`cs;A35`vWbnV@g{9vG?MisRUi^I{0($alqQe;$TBvmiBnI+Gdo z3~(qx=VRInFz;xlz+NKpokyBo5b9%5613lQDjRLT#)~w>om#*GzRr=olUIL#caf>x zKItcIJ`u3{EG|mIFBbYGwysUa6St3rUId5gueJDc&iC;&3OwkxeEhaw_}9>T%-#nT zJY4=EL9sLTUmE_SDG3k8GeOl5+0Q+KM+QY@%ifL1`TPT$ae7U*im#q24ZveHwa~uZ z>6|BFBlh$on&01cwEr5n=D}=ct%WaHyM?%08;LEbSZ}5$)G|$IL#m0$SaYq23qFMSyP&xZkox7vGxF`iJy|PDJi^vj`wi-WT&TkS;-*eN&WUtrLPB zZuWy=X2;tfrX^1!GJR^rC3(+w7(|?XGx;e^biTSrnIjxusrth(nq|;0d~YI0jaIHQ zo%`wa{L97lAE*y^>Y1nN;i6KWa#AY(`E!7HSAUTW^_}&>DDUfQpK-?^BUS%}_|(w& z;yWe4FhRYtAbGC10}rk~Ug`EabT<$1o)ZD&<5N51;tqP^V%4}yS5~}ugVl6$-epD+ zC&@bS@>fK$Nt9w(XQx@I^4Zz@{=5cmv+t$Xtj();?}!3|OZcBvyIT6v&tMs8`uq29 z5pN>V4Bbs~whI?P^s|E>K2-4wI0{iWEhXXJ|`#?*PRi>sWh{c|w)R zL*ftD3hF&w#4+7-{CqES=n{ATWugcrE$-*#>|hDd zPi))55C!A?YDa{3SPTU!o$&D;FBTDHu)X8>u#t9oDbZ}_=| z-2>M-7lsSI(Vzc0s*xtHIwcVoxnOY>@!Z-F$G7q?2W2KqaZO043NLNVQ*!oVR0+sC ziuFf)7W^2bOm?i0t_{*+PP3zz;}tlpJMkjNI}A9J^1siKt*Dvc&OzEX4(ti&#S1vx zhGFxw)62}nr;X_%wsEevYeT64VGOm4Rn`mCu4|_u0Zmz{A(cE|{v(O|gzWEAj(&iu zGUY7{n!MN9;5w{28_umlv>n0~#_3%mE~#uv4b}!9=(4P1+<^O=a-W3mSn>3yt zn15qn#MkO=rCkn}?hJ=Emtch-%jUjwW}+WpIMD5{Cgsl97krLn?hP#U@up2rnA%I@ zB<>$BFN1{y33w1(->ucm`)r>_{6`o>_01(_kukv95SnBZJtEBz7jXZAqrzt}q``N_ zmCA>&=cFgg3+k9YR=+j^i0wU~tv2Hjp?GY;Uz;fWR9_LvHtu2SHAb>U!H zdU)}zD7!W77`-Le@1(@t^EZ*^dTOcBjZIHaFRtbuAgJRuOU8?P7(~+;2sQDf^m=0Z zeD<5#k^bpvT+Q6ik9$!|h=mTW4s@3UB!^6fptT@%=1%q=&gdd3 zr2Z|h^eCS*AN}k?{ou1kSU4`E#&1L?t;I90)J-cJ{TX>r?>d6BqKz&Q5;E1eI``ej za5si3t!0NQbbEiMx?7}^#Z03kr&5!HgxJM96DX`>rmN zk36my=z})Yy)-nul%oZK$VNuiwtoP3p$+IUIh6Z8c)s`gFe;3J&RR2s({PR-T{4s^ zo}r_W4u2MHInjU=MT*Ga5-!g5F@Wz$yMPt+^ghV3*}S?f9=__BtnloMJU??Pa9tu- zTF@2l+|&<*)W!x@-lpKXx_ZxdauQI8pyvb#_P#$xX&A3W!|qcsdZEAH2^3am#~I~pK)01s$b$p94*{*k%=cYB)Zqh_oI}qFNT`X zM}*>t>kEvBjH%ft09Usm;u4$IGiD|BGZf0&jzn{HGQBJfrN21u9?0-k1x^j6%bcrG zFC3C-UV1Dwrc^AU=c{8^AGdd^BiD=k5KGfmax-QJkMZ%vVGXWD7&}Xu$)2eEzQSm; z_V6=;5JZ~eI?&C&MGX*27kgdlj(O{^1WfU zhIbAITlmx}=E@9%$V?o<5%6cEq2;z&uj@+7jz`3!Sp;T{C`g+JO>o4LHnDCoLFK&z z%RpV<81Iq=)<;=ejiPVLL~UZH&UlOi1I0-)=4x4%VrZ*{XR(VH!{?Xdf1g*#vnaKpqyLVN zTYUWZv1ZA?zJ7dePO1WJ_X}=QR%L`P7bK{PIleaX65&ZpkSko)A5;5uV)(7*dmNi% zL|?gHn47?!>0%zpq2U5v*r?mrEl!(PcdIh}Zb@As&?k=g`YV}>J7#}{-Wk#NTfVpVmHlC7j`X`QUBe%GA`9 z+bxea5&oS`#CYWYpZL+u;p$|o(mDve*P@V9SwYS`6v;|;Pye+s(J1+M#w|90Q*rLx(R8ZV1Y&_G~yD=mG9Hq#LiT!?vJ=4gk3uW=WbnfK&qbilAgXk6|p4EGLfxh`!g*L!A`H2m(ygQUg_#d z=4ibBjw0ze;e5a-2TU7=b37#W%f7iW5CBMYbUwNQICa>zL=j7BDRev6ewC2J_!2sw z|E4;<__9q2T_5spj#&{e+h5;5=4Dl)dN(Y^dI@CguY0@lOu#}PT$YVrO0xtXDj_v;sjJWmN+%DRgA$B!d~PXY>*jLUvJGp<@-gun)Uwgn6Yz6Oj8i`o({}7$gYuCK;XH%{snuxSm-%5SoRW$N+Lvu(Y*t(|n zW^ytA`;4BMEu_0)d`;VY@VNromHnGk|JS0tB}#UM zsDC;6y0Q1pfh8G4?#sHV2MPa07hTlifAL2`^Y8Cxibj1CIU%9Gl5=LX0+&AmHrKE) z(Ls3j>{WMnL1Co*M4|ccHI*y@;q7)a%{6baeLfA+GyeLp%;BPOfe3#v{n9x}XEVjH zU3Zu*fx9U{!3Pq|GSrtNknHpgHPD0>l9M|w_zIbBA7hsPUDwIx+=LD!9r6h}Dx70R zgvI2H-Q4iLS5S?%?@s`7P0gv4)p_Z{Of%p%B9XTh*VAtA5{p`YF1aG@C9R%h~_#6tQF*&hO^Rcx; zwKNJ0QKl|UIzp+}1l1VAw2HHNZZE>~zw>PoIL5?7fyKEzfj6UzsRc%dfu6SMb{`_dGgI@MoL!1MHD%JcBA$hY)1@}|X%*z8j@z0VRdYZpjw?6i9 zJ~*m0e8g=k?NIp2^Q%ZB(X?Wpy1b#>p@NrJo$G?#^RLzh1z;4xeT+4cT(^V#>n)x& z-UX_Zl*JF1DVHqy7A1ERg!f##?(Mtg$F3e)r=u_mOC?Y0-D{`vbbKu+(*H@)r6x7| z=M&~hEzmvECWub*KMCQ(`J^Rl>x|JGds6Y{!9rYH{SLgXXd&RE{lSBv+zZVsND$CO zA4f9@CFoFSI3N6JtzXy-+auMuhYx)4Mh@H-AgLLQvW!c6CV4Ms{#U`W>Q)%s@tA2aB#drP4me(sE6D2)sEPi;K&-vbVF;{O;))Uz1Jb>lJ_Ee%>xS&{!OJU@> ztKYA=VA@Y5-HOJ=b&2848*k2qH~X08Ntx)i`|iT6#vlJPg^HNOXJy5wM@D;6mlqSg ze{OA>&rcmLI!nV{X8K-JDEdQOy#>wXa6|9w>e~rHHgC0fs0CEB;M}yVC3|o0DsSn9 zp`kxtwo04aaUcrvWo)#nGPloEvTohLl4A*sG|Z>^;XIPDgK$lnZoI<|-95oZeW$Eh zPd8agxvmAkpIJE0c6Y#7G-W1&h2bE~XOY(2Gt=r+?{2sB2T}(FakrtHTefHC7ClcG zeDnE`ta&|g@`0!neMiUA$&ZR539bo{dQr!jW`D2O&xnbr(Ze@#2X$G`h_5 z!7bn{#yX3f81(1y&gH{M{E8X}IQZHL@%e+y+MMaH7+If3krYjXHB3v#AAkf@&pKb? zT_?zVbcMj10F_C>h6dgnsJ_&i|ve@be+%{ZtXAI^}wy=Af zczKtya`QCKu6Zc1^p1WxCihbbg=LX5?sE^-MXL7QlM~1w#b;W|#+mbe5E-6M*?EvY zr*qXUNT6}g*F^0B$KRY)uHZ_9=pyWxb0@}(CpiKzVzot&jE6Yx9kG5-$`JebPIr*bt}KAc)z_^2^RY*%TSJGMKQLDW&l@F0Fi3PPTeOk$`8{%Xrq04)6wQ4)9*R{{+5gZ$SM%q)4US+VYVf zMJ8YR3kgPksj(0`;p`J&Br!4fLJsZ#@#VZ$uM0*#gw+2JcrGQrIL;4y=szBIpW!kB zNB-%|syV}qNSAW}`3!TF3m<@|m(olzbRVBWQlt5jU1ak_8PXj$ce4zU_iZ>N7Tj3q zoelO8D~|A>KKUzkfgpl3js5)g8XC7-RmAxYwkR1$z0d1e$f~ZZSABs>ecSY%SWPMj z+k?NAQmvPn53~ScUTKpxjFx&T0KO5f-;L?y)XW~9Z}O1!^4XhL){H;C&@O%$P0dH# z3dSd)_B}&N%fycGB%1t8)Ig%%R+}y3T$NsB-CjbZcVG?B*_<|_jQ8SN)_Rf+furl4 zIdWgB7d5fWU}6Dh7pC{4!SyqL2oc$5&c{&P*jVc)b}{~27F2x%QZ^XY3s z!RJjlH?Pi8)gLtCBKpg0LcGnQQJI*0Iv3u%+ezI1(zha{ej#kxL4Q^k9?4;nJUt?=oMU$=s##_?c8v&p5d!ZG^U-vLYfk6veOV-k za^DLS@SB=XmpIm4H$8t1wIOe9YubRXpH7?l>o;g3Fe*rgzwsN(w8Fe+P#`9Nhyfd;3*3Qn{Z*-q1!FKl$_2*kj28OB+V?t@ewZ$ zZFC#%wL34~h1-vTk{!g5b>poPwW>_by87t8l3|%>O3N!9swQeJ-gDMM?Zs`6zciap z4q=bf4T23E6CkW>26&>>(Jv{ZJ?nq}PD_!A4q3-!w4>FIV9$Ji zN)MZJmCN=R+TG!!*^e-qVkgI1_XVt8o8M_yDfkexDW>`Li*}qa1|Hjx+S~!N*hS0- z=2us}wT(M0crB$m3YMgP87q4uSnV2o{i{z&Rh6XFofn7xd50E2CyIBx($+v}3G}}I z=q7oAU$8GcR-L=U3WtOw@d&LDm4(N%DJ6zDU+wM9?`T{M6X^{`_vW{Hkr=U$(IWil z6L&Y&o_Hx`Dnr8ujNVQNQ*&v@dAJDT3)I^qjNd))f0h3_&>?~wK2gxWEpTv1cjw_^ z-T^#*i>;{3eZxV`As0{7;X?b-e3iG|jywK#shsdbk#mGcY(P+)GtB8jHFgVk@Cz&L z$Vdt?L7z_^6)NK$m71Zx<9?hb4cCH5^aMxW0>Z2{hh)z`!goA<)(rlFMgX|Y!8_!Q zCw6))-=9gMF9A#7b>g}r;cx8l$deNk>>v?ng+CMTeP?P4&6M=u7H*vN>f+DNIclxl zFK1UZ{h%_aBI%OfQs|vv1bSZo=<#TISdptg{gC%r&(CRceAoG6+KNu;PBgLuBqv}M zh)+vgwXpTTYmr9iq$w=cCnT760(BF&j|UI7{c{~~Liy}YpKN4ijNBH|HeTCXF6?3!y!dxPLGdUKl*-Ni5b zz*`7#-zMpEFbDRUG5$XWp++ZW&O+J zA_bpib=WNAImGSN5Zkvp(TOq8df z+WpJxpMZ{$?#QCrXE5M)^NJC6ls{Nl&X+ajmei}%u4X%w!k3q(J4Wono$%9XHkq{W zm;OZASyfuve|rJ)B$7Im8)CMrlFhl{lGVVWvIHRKu$823VOK@zm=lN#0REJ3accP7 zQ}%$WjrLg{_57;Frx9W*$Nr#W?LFNw3_oU@r2@E*Fv)w`7^%x$+?U~)RT}7QEh{hF zyQZ9+CH{iz_`=TFxy~J5qEx?9jM$|YWQygcKfAXg-r_nT_i72SzEoaPd{i!7N?w~k z=Eq|5T@YEvUnsMP{N>0=2CHykRqN7f4_pIx-^K<-1>&n>}bjW3!Z}BL9O!fQfUmeY-F4BZ*&U$>tuAY9vh_;h1)jwxa z(}h)zDhD?9PQLTawFNzcp0UGSRev5mqs53#zKcaxQBK3HCrDg4*fOGWTRg;+`a)z# zMxQg^0dySD+S%ZGZ<$$hFPT}&UwcG}%T%@s<86h*EK6Oj zJM>rz2V0XQvYjJ~J!Ct^JE)=5o>CyR77Pi`asu`gJrO{UfJo>T&)^_nb+FoWPJ&RBAOF+{!1L-Acw1gXVW^IQ~kWrr-W6)j| zTbqZ(!}VhHK39EfW5Ef+k&t?crPFa04^AafTU!8Mjn=R^+nLQaa|6{mpX8X`Fh?)h z0>P{uBAGBIT--AZWv3_uO*@R8dN}`9!(b_{w)K>F)}hHHm3uokk*#b-PO9dc?YcLq z4bWP^gW??m%#lDhv@f2K*FK7QX!RMF;T@j*<;y=fvW5ZmBKQR4zPtU5nac{5yjYXD z#L^?@QP)X}xYjk(jf1&K#f`}$K}T_YU!^tT5)hcOg%-fWN!o2K%4VD{5odDOWw)@j zj%Reib75y7GiWl3{X3nHPW@}-z7OD6ZJ*Y3MWnOotcv=eB(l)l0C(bJA-)6bj}Ax8 zfh$cR@)2XxVV^~xwDg7_v`PMZhvwAF+$F7fDQjX(G5Ze!a6Gcfjz>q&7k!MWAtX{7 z(OOCX;8@q^`#yimD=OBj`*g6MpW^p(S$^pb?Z6a6Y5FQ9vU?qmmqwV#ybWphLpA`6Mg&)mVrn^6AZ}U zbF;OVP#Tks;${rWC?zeO)e^1fyPBXBmXmba@k9DUnI-D=akT&V1K52;d?|W^Ysq_2 zjKH{$ut)xpgPlG6M6ZbD^oE5b+NYsa6*`q|CO?X2!~{SHV4I$Efz30G;f~4A{)Hqj z7VVfvG@HUfl&-Z)rEc$geCl06d}TuC^agH@tOlR5cpPIgm$Zp$B0Ii5QUc$D303Sw zKcEaGH`7hJ@vt_ zPyaS1^OmNx^7GmJ_6Dz=19Votnz$WB|B>Fkf=U-$*1j_>znj8Y>L6sk18x{ZALQ6&?&Bz@9x@x@~GxlP1v1#h`Y(eAF@1V${TFIXF=p@ zVL>M@6bg2=3uADfzj#^5C-O%@eAU{v`koMZpOKc8%B?PCP>1W=)3CO^t3dL!lfT=gyx!Qkk0Gi4@i z{<>)2A}9NQVsl~s|6n2sZZ9hfV!v&;X(dY9cC!zo6B*Tn4YDTpQ5{n45( zsi!sTfXSPGX{<>y2nQU%U((XMCsDk~+u7gazyJxs!1@u??e*rSn?^`wMMWflmO~BT z0tDPA0M1cHS@0rD%e?z-%V0rR~e`!}{qvC2}gOx8zsPS%#6CM-Kd)_J2t+ZKiTutpUy zxQKUGq5m6Ej@3rygBIo;`rF%o%fq@!l055as?5z159!1aGd^-{H}=`!&C3@)InnP0 zgvEEf+VPAO$d+nb)BD75#T9xN$I)nQv$#^1ToxDKb1~MF0GGH#c3dV?eOnr+voGY5 zQ4n8Mz+iCvIHj4uD2?Sv0V?M5@!qt!75;OlNobXRH@C6?c94TD% znJZu^FuR1O@`Eyt?Z+MCtd@(}Zuw)zrPo9JD;J1(ioX$?(M2BW)1s}{^DQUN`k4|+TOQcDt(Fv^&H0*OeX@M+Fi5eQndjx9j zxAF%%6&qZIKrd@sk?~4Z_tKIo4l~|(C^sPee|+1%D|=x&Af5@iDP%SxZ6L&i;CPYz zh6Jt%v`+Z_ciU9+Rsp?&f9}_#J+fZ#4ux^ch@-g@kA6#jLX!o^5Cyu~0(4|QG4reC?i6ozAW45 za(pO@TgsLD5QyM08oK0JQuz#fwz9z=Os2a>(w$2ZUgIQYXAu>#Srcvbu`Ah-$i%PmDy z#-5c5nnwG}dCV>e(+?tLG9gTRMR+ya;!~ODQctwvRm&;lv5cprD8iNzCc^M!r+IdN zGqhpp%w=^o(fr6tUsnh(N)aBkx3$SSxeq)o3I36jUTY(DwMeV*oSv+80r4>|&*lvo zEVwF5$3^m7NXD0_prTNrv zgE+|B9-ST3smpgd|KcQtdXl=D8vyk5`xLXCj&ue2Icw>X$sl{(Xc7ld4TJNt7rrL zIU}XWjl2(F;_MKgc3Rfy%YAU|lz)8-COi;E4;|yyIrpB>8P%LTK&lIhl|$>s$Htb+ zGOeWs+>klLgkbpM5>_UFFv2K`%mhfe*VNaNz~CD3RJC=jtu6HWQoJ0DJ#CbO!RPpI z;RV^396`64BOTe;B@CtMn2q~4oC>>jD()nWgf21ta4})>{!>_49?xZ~@q{|6+IEu} zDrRFMk!Ub{lnJhrL&h71fS=AFDrvBD_U5mW%R|veo<}2`B52p}*i_l=^Qz@RvJA7k z_Rpt9GNnp9`0Pfehhuh;=fSfsjeMkHT+me4g?HuysR8$4BW-*C;WY=TI}7A+>rG*svJ5~ zTZUV_mH z7Bm31WVsZ5opznxv=itr5Y$>+>r0&#@uWjK zDoB2kw9&+m`?V(ntD@*+#qL}L$7eGv;iAG1S~s>Oe+WZ^^v&hlMOHpX-Y~4_xL_B8 zmkg&XW0pOn;PWr++`rH}$Q&EqP~YJFDwqO|{#;^}daB}X^Icc?^{!hTd!F*y-mGe% zv+Uu!%)LDq2aQs5R;oZ#_0mGA3m(TOIfv;Ze;xq8DRwDFNeK^H>lo{oLn$!Z#UK(| z?Vo$j2Y>I5(4MsnPLb_TV9bNb{=(`elKy@0tYo^J_3_11=mCw#*#!Ys&rEdyAtiq3{;`2oqVLn-_lS}rR_rq)};wmqKw0y@sDUZ2ZETM^6!KNWZ&6-k{gQ^P!x z;3bL^xHLtuTdo9#%0$yrw|p_{ArVHg1T{KMn>xKAr9ZPqpI_093VKg-4K#eQTHF?a zvi^FFDP|!#p{H6qNKM`|jlH_BU)$PM+yYbF5;iQRe{r4Di(|6q$sqWT?e#`*NKTH0 zLOx5EvJybVs%UWB2v$EK-d2y`m+zwr4a$sH3R%TcY}?MfkY9`H4#j<4=T}uJa3RJ) zPDo*H3$TP4WCu)=5@M$lCTxdBZ?j=^p+J6af2KXFM%h|Am=JiTA4Y0-(%k#RN_~wF zAAMBw^$I`5Hes0Z8vi4|rCEaVIV%W1&~){1&G1jv%xk>97l6co8~Gc6TVD?HDuyiB z8eHupJTK_oBEHZ1J^puRGovbgnVs;5HN#b5^yPp@;A-Oj1^4Z_LOM;93nnFh!}@}2 zt6kTJ?oL-&g_-}?I8rZ{r~}i=t~Av^%5D8JcTNOZD3#RJGX4u4Nfyan5=NH$R^paB zB>*z=;(+6vQ|H^4fF*^}BYTBsc$0F8Yq}D$%l%3tM~gQA2$PgH2WB4_FhnL!ZKmExJf7xC4G)bMStJ-2 zoK67jFg#yxlD-@uTk=`ZD*Awe@m$VS3g9LlvJKovk3?(?H8P0Pniw1tJC@fiipj4M zw_*hm-mth%a-tCmMyN5W*3iVw?iyi`^I_d<+k?8-JhEQppP5w%Mbd+=4KMnFPxkf` zlp1deL9MWh>EKTpF)f}$7N_a97Vj%gqPAKSx^%$4UwJB8yL1*+91lEhqcpp9c*dhB z;;NjdZZG;sKc?__lW6S{FYY*iZ25yC`F0IkEfI92`zhwYAbeGt;(we_f*EmoRn}u! zH<@E^ks;Ha>%=GH!R|f;;>+*GrS0YERs?TJ8^uxv&wh2@Mi7i^X55gw0`7s;?xn2x zK#e-`V!6?rDs8hI7t>R(o^SEihJzDkrDqIpnlw&|sAwDG+6wE%Mq7UN(w1!1$SA9B zX=8fq?;43&g{k#km5f!-w^s)NdkP=0IQJ9^e_qj+jX%4f-A1&`qbr~}mH{_h7&KLq zBb=c$H~6KV|8?)xVeBc2Pb}H5x;Wb_Xb=`;%t9ZWr#3sTsIoE0Q>n{&=UZbQ$Trcb zPfFA}ygZW>UtVrb4=ybk(VqO!Ef2@=zpCpSAnV8~&EWw%V{u1Ez4k0@IUA8+y{E~# zT>GI&Ai9i7DObz&6*gGS{ssH@f@3we%|8z?(L^&cCs;1$cE`49r)DSbYnt)ryJtxOW24y9m+!c-{w0)c3k{GwKT1N%EkP-&FOqp;XyNYd;cm7* zK`Xob8uF*e`~F~mR+kT_nuM3VeKykGlyy3p2m9he)!cjc2v%C0;Ch-O?JdlaMm`9# zB4dc8D~g08xql|Y{A_2gLt)3T(&sR17wkz6;nIO8=HU5^3d30h1Ft4>Y`P+d;v(>; zM(+d9TL2rQS~-RBz*V+Z)YUd*_aik(+?9hm+WL>V7ACcBIvH0r00+rZ$Q)qN7<*Fo z(rTDAew%yZ z{TbstJvD_1c!XMq?i(8$|FIB)NNw|Kq>07)QE?J1+WPpMuXH3!1AAZ7cJoeeWaO%9 zU>tX^_(&5a*Uyp??i7Aw6<1dQeIqygqHO;Xsk=lC)3eb(^IfID7amI3h=`M(X%Cdv zls$b&#i{YdA;T0`=SI6h#i@t2P zx0|okNR(CXi#=`R&Fi*gJ2oracE_A(!l8O%bo|qt>sIr_rizxEdq&4Xt_#UL<~_u= z-j$)xY}P1%7AqbhU~;*Qpc@3HpLf`7YfQ;}K}vJM(6uEtpGR z^bQrRCh(dS5HClfXch6W<%{iI`yrG%MO^+j zS1Ty6=g>vESXDvs%{OYghY9YBhQ%azNB{rn(!*2*g&mTne0A-%$}oEW)TIGo=|tQK#RJX@bzZcTZV}+iJikip#tgSu1g|AQQPncpAWnUrTHj|E(lHz6tM$(S&Uv`dQhT`6^KNv7{ z-TGTBgStTU$R}P(06Lf^u?>M+a{0C|{`H1$Kx%a&NeNd=XRG^P?p&7~3^KK|}^ zNd&P=ey%J@>smV{q1@+ACQvK>&3r&Wi!-eLv(L`Va@zAmHI6%@Mn_NY{yqrh=et5= zYhcRKFbq=a(mTJ%pr11KOhAb84AuCGQ6q|^eWVC8t^K~!=*IYFo(`o5!Bw8Un^=O9 zK;>CBL|ZTe678tdN*zN(hR1O#hfUBxNMra=!=it54*CY!*_|jFvVAi4Q}tgk^liah zRF6i^k$@Ry{L0$TuF7?({ZY_by>q}mOj>H3uE!{7Zk8wu z&5|o_#F3e#eKf#6WD7mp{7Rk;Yh!>x(A2i<50=+bP01P4fXgJNg5eb?(noXh<^CFX zM}d>!@$XdKe>>&>JI|Grbk{L&b5WWE8ct>!&U#diN8-#pQ3Rzu&AIcFVGP)Cx5vxe zFyk4HdS}=h)`Wwk9JgLNNauHz>eK4J@y+idt+KAnOHOu=H@QtE>>m_0)kJ=B2DQLo zPZ%Q+ap7x26UC`ZK&rVptc|U$+{#J;$AAETf8Y?wRnxkS#OYtpAKvqBPol?_`X_fbGKNGyXp=(kf3=R$n#U3ti z@GtT9^gNjNfZ`9Pq{y)&t*v9sZ?8_vj9cDMgYHZP-R3}S(ql#bmF$Jv4A@)liJ*nb z^vybvJA0e(^FO_OcQYcfR@O0xtrp1zxyRtk#s>S?ao9)v`tDmi4#TAj+2XN@yUE4` z%EmSwnlCw+I(T|yB!N=8GmQNK5mfy)z{_h#;?l|%kf`D|;Wh1c)#?}5tXDsxNk#N3 z{a8OHRQ{Hz-sSY~!p)o`JbSwaV|-@d&L=h2g0>n}h?WbBErG@k%29S5E*DH2amxd? zleR+wrg*up$z$Y}q{!?5{@4o1*FWEV@!FoEB$r0J0N@?inzwEzgo>ze4}s?5))eA*f5c0 zjG@3FB9vU8v|BNacAwhnsee~k;|Jqu=jsoZbvS$6rt&UD--&Ga1LC{Oo$O+c9kc#B z5&JY>Qe0cEt*t5;?7^(Y`Z7m|l)O=!_~bmil=l%i7+sr!e|=VfhQYR5?zH)ps3_#s zP;M|%17Ix>eSS7Ry|ka!`~B;iiQSPhN42Tj)3dXhV02KQQ3mF2EC-|Aw*}fK=jQ=~ zKr(E?kl|LcpUZc?nQ}lyC#m4^Y)ygle526C2mci!OtuBVTCtT6S7JV#3fvK*WsG=> zmm&adnoT*~mR!{1#JlNtWkr8|1UQ|9bU1G5Ui2${ZRe0XPKTzox63KbPL&(_U9P2i zwcVXcQbP88|Ff88K}_Q_Mk#)8jBtEcUa6?jtsk76+!V9IMC#*42S6Z5r zuS@ksf+x9c@8iy&k1Alizyo5&;M-H)fK|#qAU%WK!tba!*?>$24UQ&6AqcQ$V+qR; zZEf|hYZw7veBwM<>7iN94)83$69-)hOkekrGRK6bgYc(U?RsR3189Rp?0oHfAZh`p z%GmWIJ;G3Y`vj%f1+&rjmbhbGvD*L9aDH2#e|xExc~7TEYN&>H%Kme=qMJ?QFYI5% ze)2y%s&pmP$b26VN7xL9god8v{s^wf%$=$;7HO3?ohU45Se7II=ly*Lv7WN+fl+Iw z8K^tPvWUB`W2RYTz2vIX8 zeW22G1ctbN+5%2-;gzfFD_VdolaN8puT!Nw_IN8E2J$c84YsJeB3@;S-JL@JOyPf8 zGyhw}&)43ldro)xl7^fLk~ZoZ(@BfA7A-9)sp3UGPkTpXqFU<=diQK~##^Fn=nj{+ z;+8i1^Q*XcSsGvM06qFy&PqdK8Bg=E=YHF{L*|`2GUA>TITYV3 z)*S>{Jsz6Kbc&3O+}Phg$NOi*&fV#MAN&5FySf{X=4KnLch;=0DJdxltEVI;15YPm zzAr%Y?t1|~$-v$b{M#sacaZ-#iz~B$|E@Xre_08GCurON|G|Rw>g-$0TVpA|)=;c? IWft~702|8K8UO$Q literal 0 HcmV?d00001 diff --git a/docs/archive.md b/docs/configuration/archivers.md similarity index 98% rename from docs/archive.md rename to docs/configuration/archivers.md index 68453098..26755162 100644 --- a/docs/archive.md +++ b/docs/configuration/archivers.md @@ -1,4 +1,7 @@ -# File Archives & Archivers +--- +layout: page +title: 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 diff --git a/docs/config.md b/docs/configuration/config-hjson.md similarity index 85% rename from docs/config.md rename to docs/configuration/config-hjson.md index 98a6730f..10ca119d 100644 --- a/docs/config.md +++ b/docs/configuration/config-hjson.md @@ -1,6 +1,7 @@ -# 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. - +--- +layout: page +title: config.hjson +--- ## System Configuration The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location 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. @@ -31,12 +32,6 @@ general: { While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)! -### Specific Areas of Interest -* [Message Conferences](msg_conf_area.md) -* [Message Networks](msg_networks.md) -* [File Base](file_base.md) -* [File Archives & Archivers](archives.md) -* [Web Server](web_server.md) ### A Sample Configuration Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked. @@ -125,9 +120,3 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all } } ``` - -## See Also -* [Modding](modding.md) -* [Doors](doors.md) -* [MCI Codes](mci.md) -* [Menu System docs](menu_system.md) diff --git a/docs/configuration/creating-config.md b/docs/configuration/creating-config.md new file mode 100644 index 00000000..c8495f1c --- /dev/null +++ b/docs/configuration/creating-config.md @@ -0,0 +1,27 @@ +--- +layout: page +title: Creating Initial Config Files +--- +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. + +## config.hjson +Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your +enigma-bbs root directory: +``` +./oputil.js config new +``` + +You will be asked a series of questions to create an initial configuration. + +## menu.hjson and prompt.hjson + +Create your own copy of `/config/menu.hjson` and `/config/prompt.hjson`, and specify it in the +`general` section of `config.hjson`: + +````hjson +general: { + menuFile: my-menu.hjson + promptFile: my-prompt.hjson +} +```` \ No newline at end of file diff --git a/docs/configuration/directory-structure.md b/docs/configuration/directory-structure.md new file mode 100644 index 00000000..f27878c7 --- /dev/null +++ b/docs/configuration/directory-structure.md @@ -0,0 +1,20 @@ +--- +layout: page +title: Directory Structure +--- +All paths mentioned here are relative to the ENiGMA½ checkout directory. + +| Directory | Description | +|---------------------|-----------------------------------------------------------------------------------------------------------| +| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art](/art/general). +| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes](/art/themes). +| `/config` | config.hjson, [menu.hjson](/configuration/menu-hjson) and prompt.hjson storage. Also default path for SSL certs and public/private keys +| `/db` | All ENiGMA½ databases in Sqlite3 format +| `/docs` | These docs ;-) +| `/dropfiles` | Dropfiles created for [local doors](/modding/local-doors) +| `/logs` | Logs. See [Monitoring Logs](/troubleshooting/monitoring-logs) +| `/misc` | Stuff with no other home; reset password templates, common password lists, other random bits +| `/mods` | User mods. See [Modding](/modding/existing-mods) +| `/node_modules` | External libraries required by ENiGMA½, installed when you run `npm install` +| `/util` | Various tools used in running/debugging ENiGMA½ +| `/www` | ENiGMA½'s built in webserver root directory \ No newline at end of file diff --git a/docs/configuration/editing-hjson.md b/docs/configuration/editing-hjson.md new file mode 100644 index 00000000..fde61290 --- /dev/null +++ b/docs/configuration/editing-hjson.md @@ -0,0 +1,16 @@ +--- +layout: page +title: Editing Hjson +--- +Hjson is a syntax extension to JSON. It's NOT a proposal to replace JSON or to incorporate it into the JSON spec itself. +It's intended to be used like a user interface for humans, to read and edit before passing the JSON data to the machine. + +You can find more info at the [Hjson.org website](http://hjson.org/). + +## Editor Plugins +### Visual Studio Code + +[Visual Studio Code](https://code.visualstudio.com/) has a nice Hjson extension that can be installed from +within the IDE. It provides syntax highlighting to make it clear when you've made a syntax mistake within +a config file. + diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md new file mode 100644 index 00000000..3596e95a --- /dev/null +++ b/docs/configuration/menu-hjson.md @@ -0,0 +1,101 @@ +--- +layout: page +title: menu.hjson +--- +:warning: ***IMPORTANT!*** Before making any customisations, create your own copy of `/config/menu.hjson`, and specify it in the +`general` section of `config.hjson`: + +````hjson +general: { + menuFile: my-menu.hjson +} +```` +This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` + +## The Basics +Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. + +Entries in `menu.hjson` are objects defining a menu. A menu in this sense is something the user can see +or visit. Examples include but are not limited to: + +* Classical Main, Messages, and File menus +* Art file display +* Module driven menus such as door launchers + + +Each entry in `menu.hjson` defines an object that represents a menu. These objects live within the `menus` +parent object. Each object's *key* is a menu name you can reference within other menus in the system. + +## Example +Let's look a couple basic menu entries: + +```hjson +telnetConnected: { + art: CONNECT + next: matrix + options: { nextTimeout: 1500 } +} +``` + +The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in +the Telnet server's config). + +An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents +a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If +desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. + +The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after +`telnetConnected`. + +Finally, an `options` object may contain various common options for menus. In this case, `nextTimeout` +tells the system to proceed to the `next` entry automatically after 1500ms. + +Now let's look at `matrix`, the `next` entry from `telnetConnected`: + +```hjson +matrix: { + art: matrix + desc: Login Matrix + form: { + 0: { + VM: { + mci: { + VM1: { + submit: true + focus: true + items: [ "login", "apply", "log off" ] + argName: matrixSubmit + } + } + submit: { + *: [ + { + value: { matrixSubmit: 0 } + action: @menu:login + } + { + value: { matrixSubmit: 1 }, + action: @menu:newUserApplication + } + { + value: { matrixSubmit: 2 }, + action: @menu:logoff + } + ] + } + } + } + } +} +``` + +In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form +by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` +(*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` +as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` for this +action as `matrixSubmit`. + +The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). + Upon submit, the first match will be executed. For example, if the user selects "login", the first entry + with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go + to `login` menu). diff --git a/docs/configuration/sysop-setup.md b/docs/configuration/sysop-setup.md new file mode 100644 index 00000000..b8c4beb6 --- /dev/null +++ b/docs/configuration/sysop-setup.md @@ -0,0 +1,6 @@ +--- +layout: page +title: SysOp Setup +--- +SySop privileges will be granted to the first user to log into a fresh ENiGMA½ installation. + diff --git a/docs/file_base.md b/docs/file_base.md deleted file mode 100644 index 893a93de..00000000 --- a/docs/file_base.md +++ /dev/null @@ -1,89 +0,0 @@ -# File Bases -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 Approach -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 (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 -* 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` -- specifically in the `fileBase` section. - -```hjson -fileBase: { - areaStoragePrefix: /path/to/somewhere/ - - storageTags: { - /* ... */ - } - - areas: { - /* ... */ - } -} -``` - -(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: - -```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 ACS 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**. - -## 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 `fb` command has tools for managing file bases. For example, to import existing files found within **all** storage locations tied to an area and set tags `tag1` and `tag2` to each import: - -```bash -oputil.js fb scan some_area --tags tag1,tag2 -``` - -See `oputil.js fb --help` for additional information. \ No newline at end of file diff --git a/docs/filebase/acs.md b/docs/filebase/acs.md new file mode 100644 index 00000000..50527928 --- /dev/null +++ b/docs/filebase/acs.md @@ -0,0 +1,25 @@ +--- +layout: page +title: ACS +--- + +If no `acs` block is supplied in a file area definition, 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. + +## Example File Area Config with ACS + +```hjson +areas: { + retro_pc: { + name: Retro PC + desc: Oldschool PC/DOS + storageTags: [ "retro_pc", "retro_pc_bbs" ] + acs: { + write: GM[users] + } + } +} +``` \ No newline at end of file diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md new file mode 100644 index 00000000..58d4a5c1 --- /dev/null +++ b/docs/filebase/first-file-area.md @@ -0,0 +1,71 @@ +--- +layout: page +title: Configuring a File Base +--- +## ENiGMA½ File Base Key Concepts +Like many things in ENiGMA½, configuration of file base(s) is handled via `config.hjson` -- specifically +in the `fileBase` section. First, there are a couple of concepts you should understand. + + +### Storage tags + +**Storage Tags** define paths to a physical (file) storage locations that are 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 (defaults to ` ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + +------------------------------------------------------------------------------- + +System started! +``` +Grab your favourite telnet client, connect to localhost:8888 and test out your installation. + +## Points of Interest + +* The default port for Telnet is 8888 and 8889 for SSH. + * Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See + [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired. +* The first user you create when logging in will be automatically be added to the `sysops` group. + +## Telnet Software + +If you don't have any telnet software, these are compatible with ENiGMA½: + +* [SyncTERM](http://syncterm.bbsdev.net/) +* [EtherTerm](https://github.com/M-griffin/EtherTerm) +* [NetRunner](http://mysticbbs.com/downloads.html) \ No newline at end of file diff --git a/docs/mci.md b/docs/mci.md deleted file mode 100644 index 61ffee04..00000000 --- a/docs/mci.md +++ /dev/null @@ -1,113 +0,0 @@ -# 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 -* `SD`: Total downloads, system wide -* `SO`: Total downloaded amount, system wide (formatted to appropriate bytes/megs/etc.) -* `SU`: Total uploads, system wide -* `SP`: Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) - - -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. 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 -* `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! - - -#### 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 diff --git a/docs/menu_system.md b/docs/menu_system.md deleted file mode 100644 index aef51199..00000000 --- a/docs/menu_system.md +++ /dev/null @@ -1,86 +0,0 @@ -# Menu System -ENiGMA½'s menu system is highly flexible and moddable. The possibilities are almost endless! - -This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` (or whatever you reference in `config.hjson` using the `menuFile` property — see below). By modifying your `menu.hjson` you will be able to create a custom experience unique to your board. - -The default `menu.hjson` file lives within the `config` 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: yourboardname.hjson -} -``` - -You can start by copying the default `mods/menu.hjson` to `yourboardname.hjson`. - -## The Basics -Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. - -Entries in `menu.hjson` are objects defining a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: -* Classical Main, Messages, and File menus -* Art file display -* Module driven menus such as door launchers - - -Each entry in `menu.hjson` defines an object that represents a menu. These objects live within the `menus` parent object. Each object's *key* is a menu name you can reference within other menus in the system. - -## Example -Let's look a couple basic menu entries: - -```hjson -telnetConnected: { - art: CONNECT - next: matrix - options: { nextTimeout: 1500 } -} -``` - -The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). - -An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. - -The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`. - -Finally, an `options` object may contain various common options for menus. In this case, `nextTimeout` tells the system to proceed to the `next` entry automatically after 1500ms. - -Now let's look at `matrix`, the `next` entry from `telnetConnected`: -```hjson -matrix: { - art: matrix - desc: Login Matrix - form: { - 0: { - VM: { - mci: { - VM1: { - submit: true - focus: true - items: [ "login", "apply", "log off" ] - argName: matrixSubmit - } - } - submit: { - *: [ - { - value: { matrixSubmit: 0 } - action: @menu:login - } - { - value: { matrixSubmit: 1 }, - action: @menu:newUserApplication - } - { - value: { matrixSubmit: 2 }, - action: @menu:logoff - } - ] - } - } - } - } -} -``` - -In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` for this action as `matrixSubmit`. - -The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go to `login` menu). diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md new file mode 100644 index 00000000..49e533d8 --- /dev/null +++ b/docs/messageareas/bso-import-export.md @@ -0,0 +1,74 @@ +--- +layout: page +title: BSO Import / Export +--- +The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and +scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` +under `scannerTossers::ftn_bso`. + +| Config Item | Required | Description | +|-------------------------|----------|---------------------------------------------------------------------------------| +| `defaultZone` | :+1: | Sets the default BSO outbound zone +| `defaultNetwork` | :-1: | Sets the default network name from `messageNetworks.ftn.networks`. **Required if more than one network is defined**. +| `paths` | :-1: | Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`. +| `packetTargetByteSize` | :-1: | Overrides the system *target* packet (.pkt) size of 512000 bytes (512k) +| `bundleTargetByteSize` | :-1: | Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M) +| `schedule` | :+1: | See Scheduling +| `nodes` | :+1: | See Nodes + +## Scheduling +Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. +Each entry is allowed a "free form" text and/or special indicators for immediate export or watch +file triggers. + + * `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. + * `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. + * Free form text can be things like `at 5:00 pm` or `every 2 hours`. + +See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. + +### Example Configuration + +```hjson +{ + scannerTossers: { + ftn_bso: { + schedule: { + import: every 1 hours or @watch:/path/to/watchfile.ext + export: every 1 hours or @immediate + } + } + } +} +``` + +## Nodes +The `nodes` section defines how to export messages for one or more uplinks. + +A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may +contain wildcard(s) for net/zone/node/point/domain. + +| Config Item | Required | Description | +|------------------|----------|---------------------------------------------------------------------------------| +| `packetType` | :-1: | `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability | +| `packetPassword` | :-1: | Password for the packet | +| `encoding` | :-1: | Encoding to use for message bodies; Defaults to `utf-8` | +| `archiveType` | :-1: | Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration) | + +**Example**: +```hjson +{ + scannerTossers: { + ftn_bso: { + nodes: { + "46:*": { + packetType: 2+ + packetPassword: mypass + encoding: cp437 + archiveType: zip + } + } + } + } +} +``` \ No newline at end of file diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/messageareas/configuring-a-message-area.md new file mode 100644 index 00000000..5ba396ea --- /dev/null +++ b/docs/messageareas/configuring-a-message-area.md @@ -0,0 +1,65 @@ +--- +layout: page +title: Configuring a Message Area +--- +**Message Conferences** and **Areas** allow for grouping of message base topics. + +## Message Conferences +Message Conferences are the top level container for 1:n Message Areas via the `messageConferences` section +in `config.hjson`. Common message conferences may include a local conference and one or more conferences +each dedicated to a particular message network such as FsxNet, AgoraNet, etc. + +Each conference is represented by a entry under `messageConferences`. **The areas key is the conferences tag**. + +| Config Item | Required | Description | +|-------------|----------|---------------------------------------------------------------------------------| +| `name` | :+1: | Friendly conference name | +| `desc` | :+1: | Friendly conference description | +| `sort` | :-1: | If supplied, provides a key used for sorting | +| `default` | :-1: | Specify `true` to make this the default conference (e.g. assigned to new users) | +| `areas` | :+1: | Container of 1:n areas described below | + +### Example + +```hjson +{ + messageConferences: { + local: { + name: Local + desc: Local discussion + sort: 1 + default: true + } + } +} +``` + +## Message Areas +Message Areas are topic specific containers for messages that live within a particular conference. # +**The area's key is its area tag**. For example, "General Discussion" may live under a Local conference +while an AgoraNet conference may contain "BBS Discussion". + +| Config Item | Required | Description | +|-------------|----------|---------------------------------------------------------------------------------| +| `name` | :+1: | Friendly area name | +| `desc` | :+1: | Friendly area discription | +| `sort` | :-1: | If supplied, provides a key used for sorting | +| `default` | :-1: | Specify `true` to make this the default area (e.g. assigned to new users) | + +### Example + +```hjson +messageConferences: { + local: { + // ... see above ... + areas: { + enigma_dev: { // Area tag - required elsewhere! + name: ENiGMA 1/2 Development + desc: ENiGMA 1/2 discussion! + sort: 1 + default: true + } + } + } +} +``` \ No newline at end of file diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md new file mode 100644 index 00000000..bb98b0b7 --- /dev/null +++ b/docs/messageareas/message-networks.md @@ -0,0 +1,67 @@ +--- +layout: page +title: Message Networks +--- +Configuring message networks in ENiGMA½ requires three specific pieces of config - the network and your +assigned address on it, the message areas (echos) of the network you wish to map to ENiGMA½ message areas, +then the schedule and routes to send mail packets on the network. + +## FTN Networks + +FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`. + +The `networks` section contains a sub section for each network you wish you join your board to. +Each entry's key name is referenced elsewhere in `config.hjson` for FTN oriented configurations. + +### Example Configuration + +```hjson +{ + messageNetworks: { + ftn: { + networks: { + agoranet: { + localAddress: "46:3/102" + } + fsxnet: { + localAddress: "21:4/333" + } + } + } + } +} +``` + +## Message Areas + +The `areas` section describes a mapping of local **area tags** configured in your `messageConferences` (see +[Configuring a Message Area][/messageareas/configuring-a-message-area]) to a message network (described +above), a FTN specific area tag, and remote uplink address(s). + +This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. + +When ENiGMA½ imports messages, they will be placed in the local area that matches key under `areas`. + +| Config Item | Required | Description | +|-------------|----------|----------------------------------------------------------| +| `network` | :+1: | Associated network from the `networks` section above | +| `tag` | :+1: | FTN area tag | +| `uplinks` | :+1: | An array of FTN address uplink(s) for this network | + +### Example Configuration + +```hjson +{ + messageNetworks: { + ftn: { + areas: { + agoranet_bbs: { // tag found within messageConferences + network: agoranet + tag: AGN_BBS + uplinks: "46:1/100" + } + } + } + } +} +``` \ No newline at end of file diff --git a/docs/messageareas/netmail.md b/docs/messageareas/netmail.md new file mode 100644 index 00000000..3e947e68 --- /dev/null +++ b/docs/messageareas/netmail.md @@ -0,0 +1,38 @@ +--- +layout: page +title: Netmail +--- +ENiGMA support import and export of Netmail from the Private Mail area. `RiPuk @ 21:1/136` and +`RiPuk <21:1/136>` 'To' address formats are supported. + +## Netmail Routing + +A configuration block must be added to the `scannerTossers::ftn_bso` `config.hjson` section to tell the +ENiGMA½ tosser where to route netmail. + +The following configuration would tell ENiGMA½ to route all netmail addressed to 21:* through 21:1/100, +and all 46:* netmail through 46:1/100: + +````hjson + +scannerTossers: { + + /* other scannerTosser config removed for clarity */ + + ftn_bso: { + netMail: { + routes: { + "21:*" : { + address: "21:1/100" + network: fsxnet + } + "46:*" : { + address: "46:1/100" + network: agoranet + } + } + } + } +} +```` +The `network` tag must match the networks defined in `messageNetworks::ftn::networks` within `config.hjson`. \ No newline at end of file diff --git a/docs/modding.md b/docs/modding.md deleted file mode 100644 index 609729b4..00000000 --- a/docs/modding.md +++ /dev/null @@ -1,15 +0,0 @@ -# Modding - -## General Configuraiton -See [Configuration](config.md) - -## Menus -See [Menu System](menu_system.md) - -## Theming -Take a look at how the default `luciano_blocktronics` theme found under `art/themes` works! - -TODO document me! - -## Add-On Modules -See [Mods](mods.md) \ No newline at end of file diff --git a/docs/modding/door-servers.md b/docs/modding/door-servers.md new file mode 100644 index 00000000..5deaf89b --- /dev/null +++ b/docs/modding/door-servers.md @@ -0,0 +1,61 @@ +--- +layout: page +title: Door Servers +--- +## The bbs_link Module +Native support for [BBSLink](http://www.bbslink.net/) doors is provided via the `bbs_link` module. + +Configuration for a BBSLink door is straight forward. Take a look at the following example for launching Tradewars 2002: + +```hjson +doorTradeWars2002BBSLink: { + desc: Playing TW 2002 (BBSLink) + module: bbs_link + config: { + sysCode: XXXXXXXX + authCode: XXXXXXXX + schemeCode: XXXXXXXX + door: tw + } +} + +``` + +Fill in your credentials in `sysCode`, `authCode`, and `schemeCode` and that's it! + +## The door_party Module +The module `door_party` provides native support for [DoorParty!](http://www.throwbackbbs.com/) Configuration is quite easy: + +```hjson +doorParty: { + desc: Using DoorParty! + module: door_party + config: { + username: XXXXXXXX + password: XXXXXXXX + bbsTag: XX + } +} +``` + +Fill in `username`, `password`, and `bbsTag` with credentials provided to you and you should be in business! + +## The CombatNet Module +The `combatnet` module provides native support for [CombatNet](http://combatnet.us/). Add the following to your menu config: + +````hjson +combatNet: { + desc: Using CombatNet + module: combatnet + config: { + bbsTag: CBNxxx + password: XXXXXXXXX + } +} +```` +Update `bbsTag` (in the format CBNxxx) and `password` with the details provided when you register, then +you should be ready to rock! + +## The Exodus Module + +TBC \ No newline at end of file diff --git a/docs/modding/existing-mods.md b/docs/modding/existing-mods.md new file mode 100644 index 00000000..af1b2b74 --- /dev/null +++ b/docs/modding/existing-mods.md @@ -0,0 +1,11 @@ +--- +layout: page +title: Existing Mods +--- +| Name | Author | Description | +|-----------------------------|-------------|-------------| +| Married Bob Fetch Event | NuSkooler | An event for fetching the latest Married Bob ANSI's for display on you board. ACiDic release [ACD-MB4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MB4E.ZIP). Can also be [found on GitHub](https://github.com/NuSkooler/enigma-bbs-married_bob_evt) | +| Latest Files Announcement | NuSkooler | An event for posting the latest file arrivals of your board to message areas such as FTN style networks. ACiDic release [ACD-LFA1.ZIP](https://l33t.codes/outgoing/ACD/ACD-LFA1.ZIP). Also [found on GitHub](https://github.com/NuSkooler/enigma-bbs-latest_files_announce_evt) | +| Message Post Event | NuSkooler | An event for posting messages/ads to networks. ACiDic release [ACD-MP4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MP4E.ZIP) | + +See also [ACiDic BBS Mods by NuSkooler](https://l33t.codes/acidic-mods-by-myself/) \ No newline at end of file diff --git a/docs/doors.md b/docs/modding/local-doors.md similarity index 76% rename from docs/doors.md rename to docs/modding/local-doors.md index 5f72d761..b9620748 100644 --- a/docs/doors.md +++ b/docs/modding/local-doors.md @@ -1,8 +1,7 @@ -# Doors -ENiGMA½ supports a variety of methods for interacting with doors — not limited to: -* `abracadabra` module: Standard in/out (stdio) capture or temporary socket server that can be used with [DOSEMU](http://www.dosemu.org/), [DOSBox](http://www.dosbox.com/), [QEMU](http://wiki.qemu.org/Main_Page), etc. -* `bbs_link` module for interaction with [BBSLink](http://www.bbslink.net/) - +--- +layout: page +title: Local Doors +--- ## The abracadabra Module The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and process I/O through stdio or a temporary TCP server. @@ -133,7 +132,7 @@ Note the `qemu-system-i386` line. We're telling QEMU to launch and use localtime For doors that do not *require* a FOSSIL driver, it is recommended to not load or use one unless you are having issues. -#### Step 4: Create a menu entry +#### Step 3: Create a menu entry Finally we can create a `menu.hjson` entry using the `abracadabra` module: ```hjson doorLORD: { @@ -155,70 +154,15 @@ doorLORD: { } ``` - -## The bbs_link Module -Native support for [BBSLink](http://www.bbslink.net/) doors is provided via the `bbs_link` module. - -Configuration for a BBSLink door is straight forward. Take a look at the following example for launching Tradewars 2002: - -```hjson -doorTradeWars2002BBSLink: { - desc: Playing TW 2002 (BBSLink) - module: bbs_link - config: { - sysCode: XXXXXXXX - authCode: XXXXXXXX - schemeCode: XXXXXXXX - door: tw - } -} - -``` - -Fill in your credentials in `sysCode`, `authCode`, and `schemeCode` and that's it! - -## The door_party Module -The module `door_party` provides native support for [DoorParty!](http://www.throwbackbbs.com/) Configuration is quite easy: - -```hjson -doorParty: { - desc: Using DoorParty! - module: door_party - config: { - username: XXXXXXXX - password: XXXXXXXX - bbsTag: XX - } -} -``` - -Fill in `username`, `password`, and `bbsTag` with credentials provided to you and you should be in business! - -## The CombatNet Module -The `combatnet` module provides native support for [CombatNet](http://combatnet.us/). Add the following to your menu config: - -````hjson -combatNet: { - desc: Using CombatNet - module: combatnet - config: { - bbsTag: CBNxxx - password: XXXXXXXXX - } -} -```` -Update `bbsTag` (in the format CBNxxx) and `password` with the details provided when you register, then -you should be ready to rock! - -# Resources +## Resources ### DOSBox * Custom DOSBox builds http://home.arcor.de/h-a-l-9000/ -## Door Downloads & Support Sites -### General +### Door Downloads & Support Sites +#### General * http://bbsfiles.com/ * http://bbstorrents.bbses.info/ -### L.O.R.D. +#### L.O.R.D. * http://lord.lordlegacy.com/ \ No newline at end of file diff --git a/docs/mods.md b/docs/mods.md deleted file mode 100644 index f73a3fc6..00000000 --- a/docs/mods.md +++ /dev/null @@ -1,9 +0,0 @@ -# Mods -Custom mods should be added to `/enigma-install-path/mods`. - -## Existing Mods -* **Married Bob Fetch Event**: An event for fetching the latest Married Bob ANSI's for display on you board. ACiDic release [ACD-MB4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MB4E.ZIP). Can also be [found on GitHub](https://github.com/NuSkooler/enigma-bbs-married_bob_evt) -* **Latest Files Announcement**: An event for posting the latest file arrivals of your board to message areas such as FTN style networks. ACiDic release [ACD-LFA1.ZIP](https://l33t.codes/outgoing/ACD/ACD-LFA1.ZIP) Also [found on GitHub](https://github.com/NuSkooler/enigma-bbs-latest_files_announce_evt) -* **Message Post Event**: An event for posting messages/ads to networks. ACiDic release [ACD-MP4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MP4E.ZIP) - -See also [ACiDic BBS Mods by Myself](https://l33t.codes/acidic-mods-by-myself/) \ No newline at end of file diff --git a/docs/msg_conf_area.md b/docs/msg_conf_area.md deleted file mode 100644 index 05c11441..00000000 --- a/docs/msg_conf_area.md +++ /dev/null @@ -1,57 +0,0 @@ -# Message Conferences & Areas -**Message Conferences** and **Areas** allow for grouping of message base topics. - -## Message Conferences -Message Conferences are the top level container for 1:n Message Areas via the `messageConferences` section in `config.hjson`. Common message conferences may include a local conference and one or more conferences each dedicated to a particular message network such as FidoNet, AgoraNet, etc. - -Each conference is represented by a entry under `messageConferences`. **The areas key is the conferences tag**. - -**Members**: -* `name` (required): Friendly conference name -* `desc` (required): Friendly conference description -* `sort` (optional): If supplied, provides a key used for sorting -* `default` (optional): Specify `true` to make this the default conference (e.g. assigned to new users) -* `areas`: Container of 1:n areas described below - -**Example**: -```hjson -{ - messageConferences: { - local: { - name: Local - desc: Local discussion - sort: 1 - default: true - } - } -} -``` - -## Message Areas -Message Areas are topic specific containers for messages that live within a particular conference. **The areas key is it's areas tag**. For example, "General Discussion" may live under a Local conference while an AgoraNet conference may contain "BBS Discussion". - -**Members**: -* `name` (required): Friendly area name -* `desc` (required): Friendly area discription -* `sort` (optional): If supplied, provides a key used for sorting -* `default` (optional): Specify `true` to make this the default area (e.g. assigned to new users) - -**Example**: -```hjson -messageConferences: { - local: { - // ... see above ... - areas: { - local_enigma_dev: { - name: ENiGMA 1/2 Development - desc: Discussion related to features and development of ENiGMA 1/2! - sort: 1 - default: true - } - } - } -} -``` - -## Message Networks -ENiGMA½ has the ability to network with other systems via [Message Networks](msg_networks.md). Message **area tags** (described above) are utilized to map foreign areas with locally defined areas. \ No newline at end of file diff --git a/docs/msg_networks.md b/docs/msg_networks.md deleted file mode 100644 index 11fa9202..00000000 --- a/docs/msg_networks.md +++ /dev/null @@ -1,196 +0,0 @@ -# Message Networks -Message networks are configured in `messageNetworks` section of `config.hjson`. Each network type has it's own sub section such as `ftn` for FidoNet Technology Network (FTN) style networks. Message Networks tie directly with [Message Areas](msg_conf_area.md) that are also defined in `config.hjson`. - -**Members**: - * `ftn`: Configure FTN networks (described below) - * `originLine` (optional): Overrwrite the default origin line for networks that support it. For example: `originLine: Xibalba - xibalba.l33t.codes:44510` - -## FidoNet Technology Network (FTN) -FTN networks are configured under the `messageNetworks.ftn` section of `config.hjson`. - -### Networks -The `networks` section contains a sub section for network(s) you wish you join your board with. Each entry's key name can be referenced elsewhere in `config.hjson` for FTN oriented configurations. - -**Members**: - * `localAddress` (required): FTN address of **your local system** - -**Example**: -```hjson -{ - messageNetworks: { - ftn: { - networks: { - agoranet: { - localAddress: "46:3/102" - } - } - } - } -} -``` - -### Areas -The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages (In fact you can import AREAS.BBS using `oputil.js`!) - -When importing, messages will be placed in the local area that matches key under `areas`. - -**Members**: - * `network` (required): Associated network from the `networks` section - * `tag` (required): FTN area tag - * `uplinks`: An array of FTN address uplink(s) for this network - -**Example**: -```hjson -{ - messageNetworks: { - ftn: { - areas: { - agoranet_bbs: { /* found within messageConferences */ - network: agoranet - tag: AGN_BBS - uplinks: "46:1/100" - } - } - } - } -} -``` - -### BSO Import / Export -The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. - -**Members**: - * `defaultZone` (required): Sets the default BSO outbound zone - * `defaultNetwork` (optional): Sets the default network name from `messageNetworks.ftn.networks`. **Required if more than one network is defined**. - * `paths` (optional): Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`. - * `packetTargetByteSize` (optional): Overrides the system *target* packet (.pkt) size of 512000 bytes (512k) - * `bundleTargetByteSize` (optional): Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M) - * `schedule` (required): See Scheduling - * `nodes` (required): See Nodes - -#### Nodes -The `nodes` section defines how to export messages for one or more uplinks. - -A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain. - -**Members**: - * `packetType` (optional): `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability - * `packetPassword` (optional): Password for the packet - * `encoding` (optional): Encoding to use for message bodies; Defaults to `utf-8` - * `archiveType` (optional): Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration) - -**Example**: -```hjson -{ - scannerTossers: { - ftn_bso: { - nodes: { - "46:*": { - packetType: 2+ - packetPassword: mypass - encoding: cp437 - archiveType: zip - } - } - } - } -} -``` - -#### TIC Support -ENiGMA½ supports TIC files. This is handled by mapping TIC areas to local file areas. - -Under a given node (like the one configured above), TIC configuration may be supplied: - -```hjson -{ - scannerTossers: { - ftn_bso: { - nodes: { - "46:*": { - packetType: 2+ - packetPassword: mypass - encoding: cp437 - archiveType: zip - tic: { - password: TESTY-TEST - uploadBy: Agoranet TIC - allowReplace: true - } - } - } - } - } -} -``` - -You then need to configure the mapping between TIC areas you want to carry, and the file -base area for them to be tossed to. Start by creating a storage tag and file base, if you haven't -already: - -````hjson -fileBase: { - areaStoragePrefix: /home/bbs/file_areas/ - - storageTags: { - msg_network: "msg_network" - } - - areas: { - msgNetworks: { - name: Message Networks - desc: Message networks news & info - storageTags: [ - "msg_network" - ] - } - } -} - -```` -and then create the mapping between the TIC area and the file area created: - -````hjson -ticAreas: { - agn_node: { - areaTag: msgNetworks - hashTags: agoranet,nodelist - storageTag: msg_network - } - - agn_info: { - areaTag: msgNetworks - hashTags: agoranet,infopack - storageTag: msg_network - } -} - -```` -Multiple TIC areas can be mapped to a single file base area. - - -#### Scheduling -Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers. - - * `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. - * `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. - * Free form text can be things like `at 5:00 pm` or `every 2 hours`. - -See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. - -**Example**: -```hjson -{ - scannerTossers: { - ftn_bso: { - schedule: { - import: every 1 hours or @watch:/path/to/watchfile.ext - export: every 1 hours or @immediate - } - } - } -} -``` - -## More Information -* [ENiGMA 1/2 + Binkd on CentOS 7](https://www.l33t.codes/enigma-12-binkd-on-centos-7/) \ No newline at end of file diff --git a/docs/oputil/index.md b/docs/oputil/index.md new file mode 100644 index 00000000..b23a7362 --- /dev/null +++ b/docs/oputil/index.md @@ -0,0 +1,17 @@ +--- +layout: page +title: oputil +--- + +oputil is the ENiGMA½ command line utility for maintaining users, file areas and message areas, as well as +generating your initial ENiGMA½ config. + +## File areas +The `oputil.js` +op utilty `fb` command has tools for managing file bases. For example, to import existing +files found within **all** storage locations tied to an area and set tags `tag1` and `tag2` to each import: + +```bash +oputil.js fb scan some_area --tags tag1,tag2 +``` + +See `oputil.js fb --help` for additional information. \ No newline at end of file diff --git a/docs/servers/ssh.md b/docs/servers/ssh.md new file mode 100644 index 00000000..ca0b62ec --- /dev/null +++ b/docs/servers/ssh.md @@ -0,0 +1,36 @@ +--- +layout: page +title: SSH Server +--- +## Generate a SSH Private Key + +To utilize the SSH server, an SSH Private Key will need generated. From the ENiGMA installation directory: + +```bash +openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 +``` + +You then need to enable the SSH server in your `config.hjson`: + +```hjson +{ + loginServers: { + ssh: { + enabled: true + port: 8889 + privateKeyPass: YOUR_PK_PASS + } + } +} +``` + +### SSH Server Options + +| Option | Description +|---------------------|--------------------------------------------------------------------------------------| +| `privateKeyPem` | Path to private key file +| `privateKeyPass` | Password to private key file +| `firstMenu` | First menu an SSH connected user is presented with +| `firstMenuNewUser` | Menu presented to user when logging in with `users::newUserNames` in your config.hjson (defaults to `new` and `apply`) +| `enabled` | Enable/disable SSH server +| `port` | Configure a custom port for the SSH server diff --git a/docs/servers/telnet.md b/docs/servers/telnet.md new file mode 100644 index 00000000..155e6726 --- /dev/null +++ b/docs/servers/telnet.md @@ -0,0 +1,4 @@ +--- +layout: page +title: Telnet Server +--- \ No newline at end of file diff --git a/docs/web_server.md b/docs/servers/web-server.md similarity index 82% rename from docs/web_server.md rename to docs/servers/web-server.md index 1f87cd80..816c28a2 100644 --- a/docs/web_server.md +++ b/docs/servers/web-server.md @@ -1,6 +1,10 @@ -# Web Server +--- +layout: page +title: Web Server +--- 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! +[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 @@ -40,6 +44,9 @@ contentServers: { } ``` +If no certificate paths are supplied, ENiGMA½ will assume the defaults of `/config/https_cert.pem` and +`/config/https_cert_key.pem` accordingly. + ### Static Routes Static files live relative to the `contentServers::web::staticRoot` path which defaults to `enigma-bbs/www`. diff --git a/docs/vtx_web_client.md b/docs/servers/websocket.md similarity index 96% rename from docs/vtx_web_client.md rename to docs/servers/websocket.md index 99c45f93..30614006 100644 --- a/docs/vtx_web_client.md +++ b/docs/servers/websocket.md @@ -1,3 +1,7 @@ +--- +layout: page +title: Web Socket / Web Interface Server +--- # VTX Web Client ENiGMA supports the VTX websocket client for connecting to your BBS from a web page. Example usage can be found at [Xibalba](https://l33t.codes/vtx/xibalba.html) and [fORCE9](https://bbs.force9.org/vtx/force9.html). @@ -82,6 +86,6 @@ webserver, and unpack it to a temporary directory. otherwise. 9. If you navigate to http://your-hostname.here/vtx.html, you should see a splash screen like the following: - ![VTXClient](images/vtxclient.png "VTXClient") + ![VTXClient](../images/vtxclient.png "VTXClient") \ No newline at end of file diff --git a/docs/troubleshooting/monitoring-logs.md b/docs/troubleshooting/monitoring-logs.md new file mode 100644 index 00000000..7c04cb40 --- /dev/null +++ b/docs/troubleshooting/monitoring-logs.md @@ -0,0 +1,15 @@ +--- +layout: page +title: Monitoring Logs +--- +ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a +JSON object. + +Start by installing bunyan and making it available on your path: + + npm install bunyan -g + +To tail logs in a colorized and pretty format, issue the following command: + + tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan + From ec87d11c31ae430194d4aacbe8c7d702de094940 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:36:31 -0700 Subject: [PATCH 0159/1013] Fix FileEntry.findFiles() terms MATCH expr --- core/file_entry.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index b95c7acb..8738fcbd 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -3,7 +3,10 @@ const fileDb = require('./database.js').dbs.file; const Errors = require('./enig_error.js').Errors; -const getISOTimestampString = require('./database.js').getISOTimestampString; +const { + getISOTimestampString, + sanatizeString +} = require('./database.js'); const Config = require('./config.js').config; // deps @@ -523,11 +526,12 @@ module.exports = class FileEntry { } if(filter.terms && filter.terms.length > 0) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex appendWhereClause( `f.file_id IN ( SELECT rowid FROM file_fts - WHERE file_fts MATCH "${filter.terms.replace(/"/g,'""')}" + WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" )` ); } From cb8d331415418e55b1946bab97d178a494eb0a6b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:37:03 -0700 Subject: [PATCH 0160/1013] Add 'data' member support to getData() --- core/horizontal_menu_view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 02f9c06e..d9921c96 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -163,5 +163,6 @@ HorizontalMenuView.prototype.onKeyPress = function(ch, key) { }; HorizontalMenuView.prototype.getData = function() { - return this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; \ No newline at end of file From d244cd25fa4324947f6bb019e80240599473eb75 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:38:02 -0700 Subject: [PATCH 0161/1013] Add getViewsByMciCode() * Store MCI code in View when created from MCI * Allow retrieval by MCI code --- core/mci_view_factory.js | 4 ++++ core/view_controller.js | 26 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index eb783d41..eab2787c 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -199,5 +199,9 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { break; } + if(view) { + view.mciCode = mci.code; + } + return view; }; diff --git a/core/view_controller.js b/core/view_controller.js index 71911f28..65a5a1b3 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -140,9 +140,9 @@ function ViewController(options) { }; this.createViewsFromMCI = function(mciMap, cb) { - async.each(Object.keys(mciMap), function entry(name, nextItem) { - var mci = mciMap[name]; - var view = self.mciViewFactory.createFromMCI(mci); + async.each(Object.keys(mciMap), (name, nextItem) => { + const mci = mciMap[name]; + const view = self.mciViewFactory.createFromMCI(mci); if(view) { if(false === self.noInput) { @@ -152,11 +152,11 @@ function ViewController(options) { self.addView(view); } - nextItem(null); + return nextItem(null); }, - function complete(err) { + err => { self.setViewOrder(); - cb(err); + return cb(err); }); }; @@ -426,6 +426,20 @@ ViewController.prototype.getView = function(id) { return this.views[id]; }; +ViewController.prototype.getViewsByMciCode = function(mciCode) { + if(!Array.isArray(mciCode)) { + mciCode = [ mciCode ]; + } + + const views = []; + _.each(this.views, v => { + if(mciCode.includes(v.mciCode)) { + views.push(v); + } + }); + return views; +}; + ViewController.prototype.getFocusedView = function() { return this.focusedView; }; From 783f142e20facfd1a3b35f93e38eea64192e3c9e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:41:13 -0700 Subject: [PATCH 0162/1013] Add refreshPredefinedMciViewsByCode() --- core/menu_module.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 783cc40b..a6fb3b4d 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -1,15 +1,16 @@ /* 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 MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; -const Errors = require('../core/enig_error.js').Errors; +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; +const Errors = require('../core/enig_error.js').Errors; +const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); // deps const async = require('async'); @@ -423,4 +424,17 @@ exports.MenuModule = class MenuModule extends PluginModule { ++customMciId; } } + + refreshPredefinedMciViewsByCode(formName, mciCodes) { + const form = _.get(this, [ 'viewControllers', formName] ); + if(form) { + form.getViewsByMciCode(mciCodes).forEach(v => { + if(!v.setText) { + return; + } + + v.setText(getPredefinedMCIValue(this.client, v.mciCode)); + }); + } + } }; From 0eee701bf6ee457ae9b82b515d0b123e241bff6d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:42:20 -0700 Subject: [PATCH 0163/1013] Add 'data' member support to getData() --- core/spinner_menu_view.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index 72b8b2f7..829255ca 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -7,6 +7,7 @@ const strUtil = require('./string_util.js'); const util = require('util'); const assert = require('assert'); +const _ = require('lodash'); exports.SpinnerMenuView = SpinnerMenuView; @@ -29,7 +30,8 @@ function SpinnerMenuView(options) { assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - self.drawItem(this.focusedItemIndex); + this.drawItem(this.focusedItemIndex); + this.emit('index update', this.focusedItemIndex); }; this.drawItem = function() { @@ -96,7 +98,8 @@ SpinnerMenuView.prototype.onKeyPress = function(ch, key) { }; SpinnerMenuView.prototype.getData = function() { - return this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; SpinnerMenuView.prototype.setItems = function(items) { From cc2ee9c586607e3fbcd3e0d51a24624b21eea3dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:42:43 -0700 Subject: [PATCH 0164/1013] Add ESC support - WIP, not fully functional --- core/telnet_bridge.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 447efe4c..95ace6c6 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -158,7 +158,17 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { const telnetConnection = new TelnetClientConnection(self.client); + const connectionKeyPressHandler = (ch, key) => { + if('escape' === key.name) { + self.client.removeListener('key press', connectionKeyPressHandler); + telnetConnection.disconnect(); + } + }; + + self.client.on('key press', connectionKeyPressHandler); + telnetConnection.on('connected', () => { + self.client.removeListener('key press', connectionKeyPressHandler); self.client.log.info(connectOpts, 'Telnet bridge connection established'); if(self.config.font) { @@ -173,6 +183,8 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { }); telnetConnection.on('end', err => { + self.client.removeListener('key press', connectionKeyPressHandler); + if(err) { self.client.log.info(`Telnet bridge connection error: ${err.message}`); } From 837326e15ac41b02ccdbf10d951e1ba9c7985e70 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:45:03 -0700 Subject: [PATCH 0165/1013] MANY changes around message listing / viewing * If messageList is used, alwasy require items to contain areaTag * Standardize messageList a bit - still WIP, needs cleaned up * Lof of changes around area/conf tracking in relation to messages and message listings * Work for message searching * Clean up of various code, much to do... --- core/fse.js | 76 ++++++++++-------- core/menu_stack.js | 2 +- core/message.js | 72 ++++++++++++------ core/message_area.js | 35 ++------- core/message_base_search.js | 148 ++++++++++++++++++++++++++++++++++++ core/mod_mixins.js | 17 +++-- core/msg_area_view_fse.js | 10 +++ core/msg_list.js | 112 ++++++++++++++------------- core/vertical_menu_view.js | 3 +- 9 files changed, 329 insertions(+), 146 deletions(-) create mode 100644 core/message_base_search.js diff --git a/core/fse.js b/core/fse.js index de8fb1f9..c0135919 100644 --- a/core/fse.js +++ b/core/fse.js @@ -104,6 +104,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.editorMode = config.editorMode; if(config.messageAreaTag) { + // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs this.messageAreaTag = config.messageAreaTag; } @@ -127,6 +128,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } } + this.noUpdateLastReadId = _.get(options, 'extraArgs.noUpdateLastReadId', config.noUpdateLastReadId) || false; + console.log(this.noUpdateLastReadId); + this.isReady = false; if(_.has(options, 'extraArgs.message')) { @@ -342,49 +346,56 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); } + updateLastReadId(cb) { + if(this.noUpdateLastReadId) { + return cb(null); + } + + return updateMessageAreaLastReadId( + this.client.user.userId, this.messageAreaTag, this.message.messageId, cb + ); + } + setMessage(message) { this.message = message; - updateMessageAreaLastReadId( - this.client.user.userId, this.messageAreaTag, this.message.messageId, () => { + this.updateLastReadId( () => { + if(this.isReady) { + this.initHeaderViewMode(); + this.initFooterViewMode(); - if(this.isReady) { - this.initHeaderViewMode(); - this.initFooterViewMode(); + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); + let msg = this.message.message; - const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); - let msg = this.message.message; - - if(bodyMessageView && _.has(this, 'message.message')) { + if(bodyMessageView && _.has(this, 'message.message')) { + // + // We handle ANSI messages differently than standard messages -- this is required as + // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted + // how the author wanted it + // + if(isAnsi(msg)) { // - // We handle ANSI messages differently than standard messages -- this is required as - // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted - // how the author wanted it + // Find tearline - we want to color it differently. // - if(isAnsi(msg)) { - // - // Find tearline - we want to color it differently. - // - const tearLinePos = this.message.getTearLinePosition(msg); + const tearLinePos = this.message.getTearLinePosition(msg); - if(tearLinePos > -1) { - msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text')); - } - - bodyMessageView.setAnsi( - msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF - { - prepped : false, - forceLineTerm : true, - } - ); - } else { - bodyMessageView.setText(cleanControlCodes(msg)); + if(tearLinePos > -1) { + msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text')); } + + bodyMessageView.setAnsi( + msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF + { + prepped : false, + forceLineTerm : true, + } + ); + } else { + bodyMessageView.setText(cleanControlCodes(msg)); } } } - ); + }); } getMessage(cb) { @@ -816,6 +827,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); + + // if we changed conf/area we need to update any related standard MCI view + this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); } initHeaderReplyEditMode() { diff --git a/core/menu_stack.js b/core/menu_stack.js index 26e88cc5..073dece5 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -117,7 +117,7 @@ module.exports = class MenuStack { }; if(_.isObject(options)) { - loadOpts.extraArgs = options.extraArgs; + loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); loadOpts.lastMenuResult = options.lastMenuResult; } diff --git a/core/message.js b/core/message.js index 6c47a934..f7f980f7 100644 --- a/core/message.js +++ b/core/message.js @@ -87,6 +87,12 @@ const FTN_PROPERTY_NAMES = { FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; +// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! +const MESSAGE_ROW_MAP = { + reply_to_message_id : 'replyToMsgId', + modified_timestamp : 'modTimestamp' +}; + module.exports = class Message { constructor( { @@ -189,6 +195,16 @@ module.exports = class Message { return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); } + static getMessageFromRow(row) { + const msg = {}; + _.each(row, (v, k) => { + // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! + k = MESSAGE_ROW_MAP[k] || _.camelCase(k); + msg[k] = v; + }); + return msg; + } + /* Find message IDs or UUIDs by filter. Available filters/options: @@ -199,11 +215,10 @@ module.exports = class Message { filter.replyToMesageId filter.newerThanTimestamp filter.newerThanMessageId - *filter.confTag - all area tags in confTag - filter.areaTag + filter.areaTag - note if you want by conf, send in all areas for a conf *filter.metaTuples - {category, name, value} - *filter.terms - FTS search + filter.terms - FTS search filter.sort = modTimestamp | messageId filter.order = ascending | (descending) @@ -223,7 +238,13 @@ module.exports = class Message { filter.resultType = filter.resultType || 'id'; filter.extraFields = filter.extraFields || []; - const field = 'id' === filter.resultType ? 'message_id' : 'message_uuid'; + if('messageList' === filter.resultType) { + filter.extraFields = _.uniq(filter.extraFields.concat( + [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ] + )); + } + + const field = 'uuid' === filter.resultType ? 'message_uuid' : 'message_id'; if(moment.isMoment(filter.newerThanTimestamp)) { filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); @@ -280,32 +301,23 @@ module.exports = class Message { WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} )`); } else { - let areaTags = []; - if(filter.confTag && filter.confTag.length > 0) { - // :TODO: grab areas from conf -> add to areaTags[] - } - - if(areaTags.length > 0 || filter.areaTag && filter.areaTag.length > 0) { + if(filter.areaTag && filter.areaTag.length > 0) { if(Array.isArray(filter.areaTag)) { - areaTags = areaTags.concat(filter.areaTag); - } else if(_.isString(filter.areaTag)) { - areaTags.push(filter.areaTag); - } - - areaTags = _.uniq(areaTags); // remove any dupes - - if(areaTags.length > 1) { const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); appendWhereClause(`m.area_tag IN(${areaList})`); - } else { - appendWhereClause(`m.area_tag = "${areaTags[0]}"`); + } else if(_.isString(filter.areaTag)) { + appendWhereClause(`m.area_tag = "${filter.areaTag}"`); } } } - [ 'toUserName', 'fromUserName', 'replyToMessageId' ].forEach(field => { + if(_.isNumber(filter.replyToMessageId)) { + appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`); + } + + [ 'toUserName', 'fromUserName' ].forEach(field => { if(_.isString(filter[field]) && filter[field].length > 0) { - appendWhereClause(`m.${_.snakeCase(field)} = "${sanatizeString(filter[field])}"`); + appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanatizeString(filter[field])}"`); } }); @@ -317,6 +329,17 @@ module.exports = class Message { appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); } + if(filter.terms && filter.terms.length > 0) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + appendWhereClause( + `m.message_id IN ( + SELECT rowid + FROM message_fts + WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" + )` + ); + } + sql += `${sqlWhere} ${sqlOrderBy}`; if(_.isNumber(filter.limit)) { @@ -332,9 +355,12 @@ module.exports = class Message { } else { const matches = []; const extra = filter.extraFields.length > 0; + + const rowConv = 'messageList' === filter.resultType ? Message.getMessageFromRow : row => row; + msgDb.each(sql, (err, row) => { if(_.isObject(row)) { - matches.push(extra ? row : row[field]); + matches.push(extra ? rowConv(row) : row[field]); } }, err => { return cb(err, matches); diff --git a/core/message_area.js b/core/message_area.js index fb068e13..985da046 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -8,13 +8,11 @@ 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; -const { getISOTimestampString } = require('./database.js'); // deps const async = require('async'); const _ = require('lodash'); const assert = require('assert'); -const moment = require('moment'); exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; @@ -169,6 +167,7 @@ function getMessageConfTagByAreaTag(areaTag) { function getMessageAreaByTag(areaTag, optionalConfTag) { const confs = Config.messageConferences; + // :TODO: this could be cached if(_.isString(optionalConfTag)) { if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { return confs[optionalConfTag].areas[areaTag]; @@ -311,18 +310,6 @@ function changeMessageArea(client, areaTag, cb) { changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); } -function getMessageFromRow(row) { - return { - messageId : row.message_id, - messageUuid : row.message_uuid, - replyToMsgId : row.reply_to_message_id, - toUserName : row.to_user_name, - fromUserName : row.from_user_name, - subject : row.subject, - modTimestamp : row.modified_timestamp, - }; -} - function getNewMessageCountInAreaForUser(userId, areaTag, cb) { getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { lastMessageId = lastMessageId || 0; @@ -349,45 +336,33 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { const filter = { areaTag, + resultType : 'messageList', newerThanMessageId : lastMessageId, sort : 'messageId', order : 'ascending', - extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], }; if(Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = userId; } - Message.findMessages(filter, (err, messages) => { - if(err) { - return cb(err); - } - - return cb(null, messages.map(msg => getMessageFromRow(msg))); - }); + return Message.findMessages(filter, cb); }); } function getMessageListForArea(client, areaTag, cb) { const filter = { areaTag, + resultType : 'messageList', sort : 'messageId', order : 'ascending', - extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], }; if(Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = client.user.userId; } - Message.findMessages(filter, (err, messages) => { - if(err) { - return cb(err); - } - - return cb(null, messages.map(msg => getMessageFromRow(msg))); - }); + return Message.findMessages(filter, cb); } function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { diff --git a/core/message_base_search.js b/core/message_base_search.js new file mode 100644 index 00000000..b0259490 --- /dev/null +++ b/core/message_base_search.js @@ -0,0 +1,148 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const { + getSortedAvailMessageConferences, + getAvailableMessageAreasByConfTag, + getSortedAvailMessageAreasByConfTag, +} = require('./message_area.js'); +const Errors = require('./enig_error.js').Errors; +const Message = require('./message.js'); + +// deps +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Message Base Search', + desc : 'Module for quickly searching the message base', + author : 'NuSkooler', +}; + +const MciViewIds = { + search : { + searchTerms : 1, + search : 2, + conf : 3, + area : 4, + to : 5, + from : 6, + advSearch : 7, + } +}; + +exports.getModule = class MessageBaseSearch extends MenuModule { + constructor(options) { + super(options); + + this.menuMethods = { + search : (formData, extraArgs, cb) => { + return this.searchNow(formData, cb); + } + }; + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + this.prepViewController('search', 0, { mciMap : mciData.menu }, (err, vc) => { + if(err) { + return cb(err); + } + + const confView = vc.getView(MciViewIds.search.conf); + const areaView = vc.getView(MciViewIds.search.area); + + if(!confView || !areaView) { + return cb(Errors.DoesNotExist('Missing one or more required views')); + } + + const availConfs = [ { text : '-ALL-', data : '' } ].concat( + getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] + ); + + let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL + + confView.setItems(availConfs); + areaView.setItems(availAreas); + + confView.setFocusItemIndex(0); + areaView.setFocusItemIndex(0); + + confView.on('index update', idx => { + availAreas = [ { text : '-ALL-', data : '' } ].concat( + getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map( + area => Object.assign(area, { text : area.area.name, data : area.areaTag } ) + ) + ); + areaView.setItems(availAreas); + areaView.setFocusItemIndex(0); + }); + + vc.switchFocus(MciViewIds.search.searchTerms); + return cb(null); + }); + }); + } + + searchNow(formData, cb) { + const isAdvanced = formData.submitId === MciViewIds.search.advSearch; + const value = formData.value; + + const filter = { + resultType : 'messageList', + sort : 'modTimestamp', + terms : value.searchTerms, + //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned + }; + + if(isAdvanced) { + filter.toUserName = value.toUserName; + filter.fromUserName = value.fromUserName; + + if(value.confTag && !value.areaTag) { + // areaTag may be a string or array of strings + // getAvailableMessageAreasByConfTag() returns a obj - we only need tags + filter.areaTag = _.map( + getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), + (area, areaTag) => areaTag + ); + } else if(value.areaTag) { + filter.areaTag = value.areaTag; // specific conf + area + } + } + + Message.findMessages(filter, (err, messageList) => { + if(err) { + return cb(err); + } + + if(0 === messageList.length) { + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', + { menuFlags : [ 'popParent' ] }, + cb + ); + } + + const menuOpts = { + extraArgs : { + messageList, + noUpdateLastReadId : true + }, + menuFlags : [ 'popParent' ], + }; + + return this.gotoMenu( + this.menuConfig.config.messageListMenu || 'messageAreaMessageList', + menuOpts, + cb + ); + }); + } +}; diff --git a/core/mod_mixins.js b/core/mod_mixins.js index 48546825..c830813a 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -2,22 +2,25 @@ 'use strict'; const messageArea = require('../core/message_area.js'); +const { get } = require('lodash'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { - tempMessageConfAndAreaSwitch(messageAreaTag) { - messageAreaTag = messageAreaTag || this.messageAreaTag; + tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { + messageAreaTag = messageAreaTag || get(this, 'config.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(recordPrevious) { + 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)) { + if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); } } diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index 0f25c63f..7452d9d2 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -31,6 +31,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { this.messageIndex = this.messageIndex || 0; this.messageTotal = this.messageList.length; + if(this.messageList.length > 0) { + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + } + const self = this; // assign *additional* menuMethods @@ -39,6 +43,9 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { if(self.messageIndex + 1 < self.messageList.length) { self.messageIndex++; + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); } @@ -55,6 +62,9 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { if(self.messageIndex > 0) { self.messageIndex--; + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); } diff --git a/core/msg_list.js b/core/msg_list.js index 72ee20f5..d48c1588 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -35,8 +35,8 @@ exports.moduleInfo = { author : 'NuSkooler', }; -const MCICodesIDs = { - MsgList : 1, // VM1 +const MciViewIds = { + msgList : 1, // VM1 MsgInfo1 : 2, // TL2 }; @@ -44,71 +44,68 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( constructor(options) { super(options); - const self = this; - const config = this.menuConfig.config; + // :TODO: consider this pattern in base MenuModule - clean up code all over + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.messageAreaTag = config.messageAreaTag; + // :TODO: Ugg, this is needed for MessageAreaConfTempSwitcher, which wants |this.messageAreaTag| explicitly + //this.messageAreaTag = this.config.messageAreaTag; this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); - if(options.extraArgs) { - // - // |extraArgs| can override |messageAreaTag| provided by config - // as well as supply a pre-defined message list - // - if(options.extraArgs.messageAreaTag) { - this.messageAreaTag = options.extraArgs.messageAreaTag; - } - - if(options.extraArgs.messageList) { - this.messageList = options.extraArgs.messageList; - } - } - this.menuMethods = { - selectMessage : function(formData, extraArgs, cb) { - if(1 === formData.submitId) { - self.initialFocusIndex = formData.value.message; + selectMessage : (formData, extraArgs, cb) => { + if(MciViewIds.msgList === formData.submitId) { + this.initialFocusIndex = formData.value.message; const modOpts = { extraArgs : { - messageAreaTag : self.messageAreaTag, - messageList : self.messageList, + messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, + messageList : this.config.messageList, messageIndex : formData.value.message, - lastMessageNextExit : true, + lastMessageNextExit : true, } }; + if(_.isBoolean(this.config.noUpdateLastReadId)) { + modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; + } + // // 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 // + const self = this; modOpts.extraArgs.toJSON = function() { - const logMsgList = (this.messageList.length <= 4) ? - this.messageList : - this.messageList.slice(0, 2).concat(this.messageList.slice(-2)); + const logMsgList = (self.config.messageList.length <= 4) ? + self.config.messageList : + self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); return { + // note |this| is scope of toJSON()! messageAreaTag : this.messageAreaTag, apprevMessageList : logMsgList, messageCount : this.messageList.length, - messageIndex : formData.value.message, + messageIndex : this.messageIndex, }; }; - return self.gotoMenu(config.menuViewPost || 'messageAreaViewPost', modOpts, cb); + return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); } else { return cb(null); } }, - fullExit : function(formData, extraArgs, cb) { - self.menuResult = { fullExit : true }; - return self.prevMenu(cb); + fullExit : (formData, extraArgs, cb) => { + this.menuResult = { fullExit : true }; + return this.prevMenu(cb); } }; } + getSelectedAreaTag(listIndex) { + return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag; + } + enter() { if(this.lastMessageReachedExit) { return this.prevMenu(); @@ -118,12 +115,16 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // // Config can specify |messageAreaTag| else it comes from - // the user's current area + // the user's current area. If |messageList| is supplied, + // each item is expected to contain |areaTag|, so we use that + // instead in those cases. // - if(this.messageAreaTag) { - this.tempMessageConfAndAreaSwitch(this.messageAreaTag); - } else { - this.messageAreaTag = this.client.user.properties.message_area_tag; + if(!Array.isArray(this.config.messageList)) { + if(this.config.messageAreaTag) { + this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); + } else { + this.config.messageAreaTag = this.client.user.properties.message_area_tag; + } } } @@ -155,21 +156,27 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // // 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); + if(_.isArray(self.config.messageList)) { + return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); } - messageArea.getMessageListForArea(self.client, self.messageAreaTag, function msgs(err, msgList) { + messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) { if(!msgList || 0 === msgList.length) { return callback(new Error('No messages in area')); } - self.messageList = msgList; + self.config.messageList = msgList; return callback(err); }); }, function getLastReadMesageId(callback) { - messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) { + // messageList entries can contain |isNew| if they want to be considered new + if(Array.isArray(self.config.messageList)) { + self.lastReadId = 0; + return callback(null); + } + + messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { self.lastReadId = lastReadId || 0; return callback(null); // ignore any errors, e.g. missing value }); @@ -180,10 +187,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues let msgNum = 1; - self.messageList.forEach( (listItem, index) => { + self.config.messageList.forEach( (listItem, index) => { listItem.msgNum = msgNum++; listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); - listItem.newIndicator = listItem.messageId > self.lastReadId ? newIndicator : regIndicator; + const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; + listItem.newIndicator = isNew ? newIndicator : regIndicator; if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { self.initialFocusIndex = index; @@ -192,7 +200,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return callback(null); }, function populateList(callback) { - const msgListView = vc.getView(MCICodesIDs.MsgList); + const msgListView = vc.getView(MciViewIds.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}'; @@ -200,19 +208,19 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // :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 => { + msgListView.setItems(_.map(self.config.messageList, listEntry => { return stringFormat(listFormat, listEntry); })); - msgListView.setFocusItems(_.map(self.messageList, listEntry => { + msgListView.setFocusItems(_.map(self.config.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 } )); + MciViewIds.msgInfo1, + stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } )); }); if(self.initialFocusIndex > 0) { @@ -228,8 +236,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.messageList.length } )); + MciViewIds.msgInfo1, + stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } )); return callback(null); }, ], diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 6f083193..423ffc00 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -178,8 +178,7 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) { VerticalMenuView.prototype.getData = function() { const item = this.getItem(this.focusedItemIndex); - return item.data ? item.data : this.focusedItemIndex; - //return this.focusedItemIndex; + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; VerticalMenuView.prototype.setItems = function(items) { From 5c580c1ecd698a46bc85da399dd9934e31fe3fbe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 23:01:42 -0700 Subject: [PATCH 0166/1013] Prevent private mail in message search results --- core/message.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/core/message.js b/core/message.js index f7f980f7..d7204e13 100644 --- a/core/message.js +++ b/core/message.js @@ -228,7 +228,8 @@ module.exports = class Message { filter.extraFields = [] filter.privateTagUserId = - if set, only private messages belonging to are processed - (any other areaTag or confTag filters will be ignored) + - any other areaTag or confTag filters will be ignored + - if NOT present, private areas are skipped *=NYI */ @@ -301,14 +302,21 @@ module.exports = class Message { WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} )`); } else { - if(filter.areaTag && filter.areaTag.length > 0) { + if(filter.areaTag && filter.areaTag.length > 0) { if(Array.isArray(filter.areaTag)) { - const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); - appendWhereClause(`m.area_tag IN(${areaList})`); - } else if(_.isString(filter.areaTag)) { + const areaList = filter.areaTag + .filter(t => t != Message.WellKnownAreaTags.Private) + .map(t => `"${t}"`).join(', '); + if(areaList.length > 0) { + appendWhereClause(`m.area_tag IN(${areaList})`); + } + } else if(_.isString(filter.areaTag) && Message.WellKnownAreaTags.Private !== filter.areaTag) { appendWhereClause(`m.area_tag = "${filter.areaTag}"`); } } + + // explicit exclude of Private + appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); } if(_.isNumber(filter.replyToMessageId)) { From a121d60c1b72e60c9712d959ae30348e3a7ef98a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 1 Feb 2018 19:34:14 -0700 Subject: [PATCH 0167/1013] Fix lastReadId logic --- core/fse.js | 1 - core/msg_list.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/fse.js b/core/fse.js index c0135919..8a409589 100644 --- a/core/fse.js +++ b/core/fse.js @@ -129,7 +129,6 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } this.noUpdateLastReadId = _.get(options, 'extraArgs.noUpdateLastReadId', config.noUpdateLastReadId) || false; - console.log(this.noUpdateLastReadId); this.isReady = false; diff --git a/core/msg_list.js b/core/msg_list.js index d48c1588..ab0ab108 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -141,6 +141,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const self = this; const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + let configProvidedMessageList = false; async.series( [ @@ -157,6 +158,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // Config can supply messages else we'll need to populate the list now // if(_.isArray(self.config.messageList)) { + configProvidedMessageList = true; return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); } @@ -171,7 +173,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }, function getLastReadMesageId(callback) { // messageList entries can contain |isNew| if they want to be considered new - if(Array.isArray(self.config.messageList)) { + if(configProvidedMessageList) { self.lastReadId = 0; return callback(null); } From 548ff41467d60bb649ad2d9b31001f60825eb0e9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 1 Feb 2018 20:29:26 -0700 Subject: [PATCH 0168/1013] Conceptual MenuItem caching - WIP for testing, will impl. in others if it seems good --- core/menu_view.js | 13 +++++++++++++ core/vertical_menu_view.js | 11 ++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/menu_view.js b/core/menu_view.js index f15491cf..dc2f5c81 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -29,6 +29,8 @@ function MenuView(options) { this.items = []; } + this.renderCache = {}; + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); this.setHotKeys(options.hotKeys); @@ -65,6 +67,7 @@ util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { this.sorted = false; + this.renderCache = {}; // // Items can be an array of strings or an array of objects. @@ -98,6 +101,16 @@ MenuView.prototype.setItems = function(items) { } }; +MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; +}; + +MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; +}; + MenuView.prototype.setSort = function(sort) { if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { return; diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 423ffc00..57570e16 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -64,6 +64,11 @@ function VerticalMenuView(options) { return; } + const cached = this.getRenderCacheItem(index, item.focused); + if(cached) { + return self.client.term.write(`${ansi.goto(item.row, self.position.col)}${cached}`); + } + let text; let sgr; if(item.focused && self.hasFocusItems()) { @@ -78,9 +83,9 @@ function VerticalMenuView(options) { sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } - self.client.term.write( - `${ansi.goto(item.row, self.position.col)}${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}` - ); + text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; + self.client.term.write(`${ansi.goto(item.row, self.position.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); }; } From 7e748fc04dd6748c1c869893db75e4115698970b Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 2 Feb 2018 22:02:09 +0000 Subject: [PATCH 0169/1013] * Fix nav when on GH pages --- docs/_includes/nav.md | 74 +++++++++++++++++++------------------- docs/_layouts/default.html | 4 +-- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index f8fbb643..f9787cb3 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -1,67 +1,67 @@ - Installation - - [Installation Methods](/installation/installation-methods) - - [Install script](/installation/install-script) - - [Docker](/installation/docker) - - [Manual installation](/installation/manual) - - [OS / Hardware Specific](/installation/os-hardware) + - [Installation Methods]({% link installation/installation-methods.md %}) + - [Install script]({% link installation/install-script.md %}) + - [Docker]({% link installation/docker.md %}) + - [Manual installation]({% link installation/manual.md %}) + - [OS / Hardware Specific]({% link installation/os-hardware.md %}) - Raspberry Pi - Windows - - [Your Network Setup](/installation/network) - - [Testing Your Installation](/installation/testing) - - [Production Installation](/installation/production) + - [Your Network Setup]({% link installation/network.md %}) + - [Testing Your Installation]({% link installation/testing.md %}) + - [Production Installation]({% link installation/production.md %}) - Configuration - - [Creating Config Files](/configuration/creating-config) - - [SysOp Setup](/configuration/sysop-setup) - - [Editing hjson](/configuration/editing-hjson) - - [config.hjson](/configuration/config-hjson) - - [menu.hjson](/configuration/menu-hjson) + - [Creating Config Files]({% link configuration/creating-config.md %}) + - [SysOp Setup]({% link configuration/sysop-setup.md %}) + - [Editing hjson]({% link configuration/editing-hjson.md %}) + - [config.hjson]({% link configuration/config-hjson.md %}) + - [menu.hjson]({% link configuration/menu-hjson.md %}) - prompt.hjson - - [Directory Structure](/configuration/directory-structure) - - [Archivers](/configuration/archivers) + - [Directory Structure]({% link configuration/directory-structure.md %}) + - [Archivers]({% link configuration/archivers.md %}) - Scheduled jobs - SMTP - File Base - - [About](/filebase/) - - [Configuring a File Area](/filebase/first-file-area) - - [ACS model](/filebase/acs) - - [Uploads](/filebase/uploads) - - [Web Access](/filebase/web-access) - - [TIC Support](/filebase/tic-support) (Importing from FTN networks) + - [About]({% link filebase/index.md %}) + - [Configuring a File Area]({% link filebase/first-file-area.md %}) + - [ACS model]({% link filebase/acs.md %}) + - [Uploads]({% link filebase/uploads.md %}) + - [Web Access]({% link filebase/web-access.md %}) + - [TIC Support]({% link filebase/tic-support.md %}) (Importing from FTN networks) - Tips and tricks - Network mounts and symlinks - Message Areas - - [Configuring a Message Area](/messageareas/configuring-a-message-area) - - [Message networks](/messageareas/message-networks) - - [BSO Import & Export](/messageareas/bso-import-export) - - [Netmail](/messageareas/netmail) + - [Configuring a Message Area]({% link messageareas/configuring-a-message-area.md %}) + - [Message networks]({% link messageareas/message-networks.md %}) + - [BSO Import & Export]({% link messageareas/bso-import-export.md %}) + - [Netmail]({% link messageareas/netmail.md %}) - Art - - [General](/art/general) - - [Themes](/art/themes) - - [MCI Codes](/art/mci) + - [General]({% link art/general.md %}) + - [Themes]({% link art/themes.md %}) + - [MCI Codes]({% link art/mci.md %}) - Servers - Login Servers - - [Telnet](/servers/telnet) - - [SSH](/servers/ssh) - - [WebSocket](/servers/websocket) + - [Telnet]({% link servers/telnet.md %}) + - [SSH]({% link servers/ssh.md %}) + - [WebSocket]({% link servers/websocket.md %}) - Build your own - Content Servers - - [Web](/servers/web-server) + - [Web]({% link servers/web-server.md %}) - Modding - - [Local Doors](/modding/local-doors) - - [Door Servers](/modding/door-servers) + - [Local Doors]({% link modding/local-doors.md %}) + - [Door Servers]({% link modding/door-servers.md %}) - DoorParty - BBSLink - Combatnet - Exodus - - [Existing Mods](/modding/existing-mods) + - [Existing Mods]({% link modding/existing-mods.md %}) - - [Oputil](/oputil/) + - [Oputil]({% link oputil/index.md %}) - Troubleshooting - - [Monitoring Logs](/troubleshooting/monitoring-logs) + - [Monitoring Logs]({% link troubleshooting/monitoring-logs.md %}) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 8b01d854..bb606e15 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -5,16 +5,14 @@ - {% seo %} - Fork me on GitHub