* Moved oputil.js config import-areas to 'oputil.js mb import-areas'
* 'oputil.js mb import-areas' now *optionally* binds areas to FTN networks. Otherwise only areas are imported
This commit is contained in:
parent
a5f3a65faa
commit
2c4fdfdd5f
|
@ -216,7 +216,7 @@ function initialize(cb) {
|
||||||
},
|
},
|
||||||
function loadSysOpInformation(callback) {
|
function loadSysOpInformation(callback) {
|
||||||
//
|
//
|
||||||
// Copy over some +op information from the user DB -> system propertys.
|
// Copy over some +op information from the user DB -> system properties.
|
||||||
// * Makes this accessible for MCI codes, easy non-blocking access, etc.
|
// * Makes this accessible for MCI codes, easy non-blocking access, etc.
|
||||||
// * We do this every time as the op is free to change this information just
|
// * We do this every time as the op is free to change this information just
|
||||||
// like any other user
|
// like any other user
|
||||||
|
|
|
@ -7,6 +7,11 @@ const db = require('../../core/database.js');
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
const inq = require('inquirer');
|
||||||
|
const fs = require('fs');
|
||||||
|
const hjson = require('hjson');
|
||||||
|
|
||||||
|
const packageJson = require('../../package.json');
|
||||||
|
|
||||||
exports.printUsageAndSetExitCode = printUsageAndSetExitCode;
|
exports.printUsageAndSetExitCode = printUsageAndSetExitCode;
|
||||||
exports.getDefaultConfigPath = getDefaultConfigPath;
|
exports.getDefaultConfigPath = getDefaultConfigPath;
|
||||||
|
@ -14,6 +19,17 @@ exports.getConfigPath = getConfigPath;
|
||||||
exports.initConfigAndDatabases = initConfigAndDatabases;
|
exports.initConfigAndDatabases = initConfigAndDatabases;
|
||||||
exports.getAreaAndStorage = getAreaAndStorage;
|
exports.getAreaAndStorage = getAreaAndStorage;
|
||||||
exports.looksLikePattern = looksLikePattern;
|
exports.looksLikePattern = looksLikePattern;
|
||||||
|
exports.getAnswers = getAnswers;
|
||||||
|
exports.writeConfig = writeConfig;
|
||||||
|
|
||||||
|
const HJSONStringifyCommonOpts = exports.HJSONStringifyCommonOpts = {
|
||||||
|
emitRootBraces : true,
|
||||||
|
bracesSameLine : true,
|
||||||
|
space : 4,
|
||||||
|
keepWsc : true,
|
||||||
|
quotes : 'min',
|
||||||
|
eol : '\n',
|
||||||
|
};
|
||||||
|
|
||||||
const exitCodes = exports.ExitCodes = {
|
const exitCodes = exports.ExitCodes = {
|
||||||
SUCCESS : 0,
|
SUCCESS : 0,
|
||||||
|
@ -100,4 +116,23 @@ function looksLikePattern(tag) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return /[*?[\]!()+|^]/.test(tag);
|
return /[*?[\]!()+|^]/.test(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnswers(questions, cb) {
|
||||||
|
inq.prompt(questions).then( answers => {
|
||||||
|
return cb(answers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(config, path) {
|
||||||
|
config = hjson.stringify(config, HJSONStringifyCommonOpts)
|
||||||
|
.replace(/%ENIG_VERSION%/g, packageJson.version)
|
||||||
|
.replace(/%HJSON_VERSION%/g, hjson.version);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path, config, 'utf8');
|
||||||
|
return true;
|
||||||
|
} catch(e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -9,10 +9,11 @@ const {
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
argv,
|
argv,
|
||||||
ExitCodes,
|
ExitCodes,
|
||||||
initConfigAndDatabases
|
getAnswers,
|
||||||
|
writeConfig,
|
||||||
|
HJSONStringifyCommonOpts,
|
||||||
} = require('./oputil_common.js');
|
} = require('./oputil_common.js');
|
||||||
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||||
const Errors = require('../../core/enig_error.js').Errors;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -24,17 +25,8 @@ const paths = require('path');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const sanatizeFilename = require('sanitize-filename');
|
const sanatizeFilename = require('sanitize-filename');
|
||||||
|
|
||||||
const packageJson = require('../../package.json');
|
|
||||||
|
|
||||||
exports.handleConfigCommand = handleConfigCommand;
|
exports.handleConfigCommand = handleConfigCommand;
|
||||||
|
|
||||||
|
|
||||||
function getAnswers(questions, cb) {
|
|
||||||
inq.prompt(questions).then( answers => {
|
|
||||||
return cb(answers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigIncludeKeys = [
|
const ConfigIncludeKeys = [
|
||||||
'theme',
|
'theme',
|
||||||
'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds',
|
'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds',
|
||||||
|
@ -46,15 +38,6 @@ const ConfigIncludeKeys = [
|
||||||
'logging.rotatingFile',
|
'logging.rotatingFile',
|
||||||
];
|
];
|
||||||
|
|
||||||
const HJSONStringifyComonOpts = {
|
|
||||||
emitRootBraces : true,
|
|
||||||
bracesSameLine : true,
|
|
||||||
space : 4,
|
|
||||||
keepWsc : true,
|
|
||||||
quotes : 'min',
|
|
||||||
eol : '\n',
|
|
||||||
};
|
|
||||||
|
|
||||||
const QUESTIONS = {
|
const QUESTIONS = {
|
||||||
Intro : [
|
Intro : [
|
||||||
{
|
{
|
||||||
|
@ -231,34 +214,21 @@ function askNewConfigQuestions(cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeConfig(config, path) {
|
|
||||||
config = hjson.stringify(config, HJSONStringifyComonOpts)
|
|
||||||
.replace(/%ENIG_VERSION%/g, packageJson.version)
|
|
||||||
.replace(/%HJSON_VERSION%/g, hjson.version)
|
|
||||||
;
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(path, config, 'utf8');
|
|
||||||
return true;
|
|
||||||
} catch(e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyFileSyncSilent = (to, from, flags) => {
|
const copyFileSyncSilent = (to, from, flags) => {
|
||||||
try {
|
try {
|
||||||
fs.copyFileSync(to, from, flags);
|
fs.copyFileSync(to, from, flags);
|
||||||
} catch(e) {}
|
} catch(e) {
|
||||||
|
/* absorb! */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildNewConfig() {
|
function buildNewConfig() {
|
||||||
askNewConfigQuestions( (err, configPath, config) => {
|
askNewConfigQuestions( (err, configPath, config) => {
|
||||||
if(err) {
|
if(err) { return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bn = sanatizeFilename(config.general.boardName)
|
const bn = sanatizeFilename(config.general.boardName)
|
||||||
.replace(/[^a-z0-9_\-]/ig, '_')
|
.replace(/[^a-z0-9_-]/ig, '_')
|
||||||
.replace(/_+/g, '_')
|
.replace(/_+/g, '_')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
const menuFile = `${bn}-menu.hjson`;
|
const menuFile = `${bn}-menu.hjson`;
|
||||||
|
@ -273,7 +243,7 @@ function buildNewConfig() {
|
||||||
paths.join(__dirname, '../../misc/prompt_template.in.hjson'),
|
paths.join(__dirname, '../../misc/prompt_template.in.hjson'),
|
||||||
paths.join(__dirname, '../../config/', promptFile),
|
paths.join(__dirname, '../../config/', promptFile),
|
||||||
fs.constants.COPYFILE_EXCL
|
fs.constants.COPYFILE_EXCL
|
||||||
)
|
);
|
||||||
|
|
||||||
config.general.menuFile = menuFile;
|
config.general.menuFile = menuFile;
|
||||||
config.general.promptFile = promptFile;
|
config.general.promptFile = promptFile;
|
||||||
|
@ -286,294 +256,10 @@ function buildNewConfig() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUplinks(uplinks) {
|
|
||||||
const ftnAddress = require('../../core/ftn_address.js');
|
|
||||||
const valid = uplinks.every(ul => {
|
|
||||||
const addr = ftnAddress.fromString(ul);
|
|
||||||
return addr;
|
|
||||||
});
|
|
||||||
return valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMsgAreaImportType(path) {
|
|
||||||
if(argv.type) {
|
|
||||||
return argv.type.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ext = paths.extname(path).toLowerCase().substr(1);
|
|
||||||
return ext; // .bbs|.na|...
|
|
||||||
}
|
|
||||||
|
|
||||||
function importAreas() {
|
|
||||||
const importPath = argv._[argv._.length - 1];
|
|
||||||
if(argv._.length < 3 || !importPath || 0 === importPath.length) {
|
|
||||||
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
const importType = getMsgAreaImportType(importPath);
|
|
||||||
if('na' !== importType && 'bbs' !== importType) {
|
|
||||||
return console.error(`"${importType}" is not a recognized import file type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// optional data - we'll prompt if for anything not found
|
|
||||||
let confTag = argv.conf;
|
|
||||||
let networkName = argv.network;
|
|
||||||
let uplinks = argv.uplinks;
|
|
||||||
if(uplinks) {
|
|
||||||
uplinks = uplinks.split(/[\s,]+/);
|
|
||||||
}
|
|
||||||
|
|
||||||
let importEntries;
|
|
||||||
|
|
||||||
async.waterfall(
|
|
||||||
[
|
|
||||||
function readImportFile(callback) {
|
|
||||||
fs.readFile(importPath, 'utf8', (err, importData) => {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
importEntries = getImportEntries(importType, importData);
|
|
||||||
if(0 === importEntries.length) {
|
|
||||||
return callback(Errors.Invalid('Invalid or empty import file'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should have enough to validate uplinks
|
|
||||||
if('bbs' === importType) {
|
|
||||||
for(let i = 0; i < importEntries.length; ++i) {
|
|
||||||
if(!validateUplinks(importEntries[i].uplinks)) {
|
|
||||||
return callback(Errors.Invalid('Invalid uplink(s)'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(!validateUplinks(uplinks)) {
|
|
||||||
return callback(Errors.Invalid('Invalid uplink(s)'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function init(callback) {
|
|
||||||
return initConfigAndDatabases(callback);
|
|
||||||
},
|
|
||||||
function validateAndCollectInput(callback) {
|
|
||||||
const msgArea = require('../../core/message_area.js');
|
|
||||||
const sysConfig = require('../../core/config.js').get();
|
|
||||||
|
|
||||||
let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } );
|
|
||||||
if(!msgConfs) {
|
|
||||||
return callback(Errors.DoesNotExist('No conferences exist in your configuration'));
|
|
||||||
}
|
|
||||||
|
|
||||||
msgConfs = msgConfs.map(mc => {
|
|
||||||
return {
|
|
||||||
name : mc.conf.name,
|
|
||||||
value : mc.confTag,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if(confTag && !msgConfs.find(mc => {
|
|
||||||
return confTag === mc.value;
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`));
|
|
||||||
}
|
|
||||||
|
|
||||||
let existingNetworkNames = [];
|
|
||||||
if(_.has(sysConfig, 'messageNetworks.ftn.networks')) {
|
|
||||||
existingNetworkNames = Object.keys(sysConfig.messageNetworks.ftn.networks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(0 === existingNetworkNames.length) {
|
|
||||||
return callback(Errors.DoesNotExist('No FTN style networks exist in your configuration'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(networkName && !existingNetworkNames.find(net => networkName === net)) {
|
|
||||||
return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`));
|
|
||||||
}
|
|
||||||
|
|
||||||
getAnswers([
|
|
||||||
{
|
|
||||||
name : 'confTag',
|
|
||||||
message : 'Message conference:',
|
|
||||||
type : 'list',
|
|
||||||
choices : msgConfs,
|
|
||||||
pageSize : 10,
|
|
||||||
when : !confTag,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'networkName',
|
|
||||||
message : 'Network name:',
|
|
||||||
type : 'list',
|
|
||||||
choices : existingNetworkNames,
|
|
||||||
when : !networkName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'uplinks',
|
|
||||||
message : 'Uplink(s) (comma separated):',
|
|
||||||
type : 'input',
|
|
||||||
validate : (input) => {
|
|
||||||
const inputUplinks = input.split(/[\s,]+/);
|
|
||||||
return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)';
|
|
||||||
},
|
|
||||||
when : !uplinks && 'bbs' !== importType,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
answers => {
|
|
||||||
confTag = confTag || answers.confTag;
|
|
||||||
networkName = networkName || answers.networkName;
|
|
||||||
uplinks = uplinks || answers.uplinks;
|
|
||||||
|
|
||||||
importEntries.forEach(ie => {
|
|
||||||
ie.areaTag = ie.ftnTag.toLowerCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function confirmWithUser(callback) {
|
|
||||||
const sysConfig = require('../../core/config.js').get();
|
|
||||||
|
|
||||||
console.info(`Importing the following for "${confTag}" - (${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`);
|
|
||||||
importEntries.forEach(ie => {
|
|
||||||
console.info(` ${ie.ftnTag} - ${ie.name}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info('');
|
|
||||||
console.info('Importing will NOT create required FTN network configurations.');
|
|
||||||
console.info('If you have not yet done this, you will need to complete additional steps after importing.');
|
|
||||||
console.info('See docs/msg_networks.md for details.');
|
|
||||||
console.info('');
|
|
||||||
|
|
||||||
getAnswers([
|
|
||||||
{
|
|
||||||
name : 'proceed',
|
|
||||||
message : 'Proceed?',
|
|
||||||
type : 'confirm',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
answers => {
|
|
||||||
return callback(answers.proceed ? null : Errors.General('User canceled'));
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
function loadConfigHjson(callback) {
|
|
||||||
const configPath = getConfigPath();
|
|
||||||
fs.readFile(configPath, 'utf8', (err, confData) => {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config;
|
|
||||||
try {
|
|
||||||
config = hjson.parse(confData, { keepWsc : true } );
|
|
||||||
} catch(e) {
|
|
||||||
return callback(e);
|
|
||||||
}
|
|
||||||
return callback(null, config);
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function performImport(config, callback) {
|
|
||||||
const confAreas = { messageConferences : {} };
|
|
||||||
confAreas.messageConferences[confTag] = { areas : {} };
|
|
||||||
|
|
||||||
const msgNetworks = { messageNetworks : { ftn : { areas : {} } } };
|
|
||||||
|
|
||||||
importEntries.forEach(ie => {
|
|
||||||
const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area
|
|
||||||
|
|
||||||
confAreas.messageConferences[confTag].areas[ie.areaTag] = {
|
|
||||||
name : ie.name,
|
|
||||||
desc : ie.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = {
|
|
||||||
network : networkName,
|
|
||||||
tag : ie.ftnTag,
|
|
||||||
uplinks : specificUplinks
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const newConfig = _.defaultsDeep(config, confAreas, msgNetworks);
|
|
||||||
const configPath = getConfigPath();
|
|
||||||
|
|
||||||
if(!writeConfig(newConfig, configPath)) {
|
|
||||||
return callback(Errors.UnexpectedState('Failed writing configuration'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
err => {
|
|
||||||
if(err) {
|
|
||||||
console.error(err.reason ? err.reason : err.message);
|
|
||||||
} else {
|
|
||||||
const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"';
|
|
||||||
console.info('Configuration generated.');
|
|
||||||
console.info(`You may wish to validate changes made to ${getConfigPath()}`);
|
|
||||||
console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`);
|
|
||||||
console.info('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImportEntries(importType, importData) {
|
|
||||||
let importEntries = [];
|
|
||||||
|
|
||||||
if('na' === importType) {
|
|
||||||
//
|
|
||||||
// parse out
|
|
||||||
// TAG DESC
|
|
||||||
//
|
|
||||||
const re = /^([^\s]+)\s+([^\r\n]+)/gm;
|
|
||||||
let m;
|
|
||||||
|
|
||||||
while( (m = re.exec(importData) )) {
|
|
||||||
importEntries.push({
|
|
||||||
ftnTag : m[1],
|
|
||||||
name : m[2],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if ('bbs' === importType) {
|
|
||||||
//
|
|
||||||
// Various formats for AREAS.BBS seem to exist. We want to support as much as possible.
|
|
||||||
//
|
|
||||||
// SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS
|
|
||||||
// CODE TAG UPLINKS
|
|
||||||
//
|
|
||||||
// VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS
|
|
||||||
// TAG UPLINKS
|
|
||||||
//
|
|
||||||
// Misc
|
|
||||||
// PATH|OTHER TAG UPLINKS
|
|
||||||
//
|
|
||||||
// Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end)
|
|
||||||
//
|
|
||||||
const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm;
|
|
||||||
let m;
|
|
||||||
while ( (m = re.exec(importData) )) {
|
|
||||||
const tag = m[1];
|
|
||||||
|
|
||||||
importEntries.push({
|
|
||||||
ftnTag : tag,
|
|
||||||
name : `Area: ${tag}`,
|
|
||||||
uplinks : m[2].split(/[\s,]+/),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return importEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function catCurrentConfig() {
|
function catCurrentConfig() {
|
||||||
try {
|
try {
|
||||||
const config = hjson.rt.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
const config = hjson.rt.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
||||||
const hjsonOpts = Object.assign({}, HJSONStringifyComonOpts, {
|
const hjsonOpts = Object.assign({}, HJSONStringifyCommonOpts, {
|
||||||
colors : false === argv.colors ? false : true,
|
colors : false === argv.colors ? false : true,
|
||||||
keepWsc : false === argv.comments ? false : true,
|
keepWsc : false === argv.comments ? false : true,
|
||||||
});
|
});
|
||||||
|
@ -596,9 +282,8 @@ function handleConfigCommand() {
|
||||||
const action = argv._[1];
|
const action = argv._[1];
|
||||||
|
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case 'new' : return buildNewConfig();
|
case 'new' : return buildNewConfig();
|
||||||
case 'import-areas' : return importAreas();
|
case 'cat' : return catCurrentConfig();
|
||||||
case 'cat' : return catCurrentConfig();
|
|
||||||
|
|
||||||
default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,15 +39,8 @@ actions:
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
new generate a new/initial configuration
|
new generate a new/initial configuration
|
||||||
import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH
|
|
||||||
cat cat current configuration to stdout
|
cat cat current configuration to stdout
|
||||||
|
|
||||||
import-areas args:
|
|
||||||
--conf CONF_TAG specify conference tag in which to import areas
|
|
||||||
--network NETWORK specify network name/key to associate FTN areas
|
|
||||||
--uplinks UL1,UL2,... specify one or more comma separated uplinks
|
|
||||||
--type TYPE specifies area import type. valid options are "bbs" and "na"
|
|
||||||
|
|
||||||
cat args:
|
cat args:
|
||||||
--no-color disable color
|
--no-color disable color
|
||||||
--no-comments strip any comments
|
--no-comments strip any comments
|
||||||
|
@ -99,12 +92,19 @@ general information:
|
||||||
FILE_ID a file identifier. see file.sqlite3
|
FILE_ID a file identifier. see file.sqlite3
|
||||||
`,
|
`,
|
||||||
MessageBase :
|
MessageBase :
|
||||||
`usage: oputil.js mb <action> [<args>]
|
`usage: oputil.js mb <action> [<args>]
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s)
|
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
|
one or more commands may be supplied. commands that are multi
|
||||||
part such as "%COMPRESS ZIP" should be quoted.
|
part such as "%COMPRESS ZIP" should be quoted.
|
||||||
|
import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH
|
||||||
|
|
||||||
|
import-areas args:
|
||||||
|
--conf CONF_TAG conference tag in which to import areas
|
||||||
|
--network NETWORK network name/key to associate FTN areas
|
||||||
|
--uplinks UL1,UL2,... one or more comma separated uplinks
|
||||||
|
--type TYPE area import type. valid options are "bbs" and "na"
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,25 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
const {
|
||||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
printUsageAndSetExitCode,
|
||||||
const argv = require('./oputil_common.js').argv;
|
getConfigPath,
|
||||||
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
|
ExitCodes,
|
||||||
|
argv,
|
||||||
|
initConfigAndDatabases,
|
||||||
|
getAnswers,
|
||||||
|
writeConfig,
|
||||||
|
} = require('./oputil_common.js');
|
||||||
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||||
const Address = require('../ftn_address.js');
|
const Address = require('../ftn_address.js');
|
||||||
const Errors = require('../enig_error.js').Errors;
|
const Errors = require('../enig_error.js').Errors;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
const paths = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const hjson = require('hjson');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.handleMessageBaseCommand = handleMessageBaseCommand;
|
exports.handleMessageBaseCommand = handleMessageBaseCommand;
|
||||||
|
|
||||||
|
@ -121,6 +130,310 @@ function areaFix() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateUplinks(uplinks) {
|
||||||
|
const ftnAddress = require('../../core/ftn_address.js');
|
||||||
|
const valid = uplinks.every(ul => {
|
||||||
|
const addr = ftnAddress.fromString(ul);
|
||||||
|
return addr;
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMsgAreaImportType(path) {
|
||||||
|
if(argv.type) {
|
||||||
|
return argv.type.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths.extname(path).substr(1).toLowerCase(); // bbs|na|...
|
||||||
|
}
|
||||||
|
|
||||||
|
function importAreas() {
|
||||||
|
const importPath = argv._[argv._.length - 1];
|
||||||
|
if(argv._.length < 3 || !importPath || 0 === importPath.length) {
|
||||||
|
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importType = getMsgAreaImportType(importPath);
|
||||||
|
if('na' !== importType && 'bbs' !== importType) {
|
||||||
|
return console.error(`"${importType}" is not a recognized import file type`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional data - we'll prompt if for anything not found
|
||||||
|
let confTag = argv.conf;
|
||||||
|
let networkName = argv.network;
|
||||||
|
let uplinks = argv.uplinks;
|
||||||
|
if(uplinks) {
|
||||||
|
uplinks = uplinks.split(/[\s,]+/);
|
||||||
|
}
|
||||||
|
|
||||||
|
let importEntries;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function readImportFile(callback) {
|
||||||
|
fs.readFile(importPath, 'utf8', (err, importData) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
importEntries = getImportEntries(importType, importData);
|
||||||
|
if(0 === importEntries.length) {
|
||||||
|
return callback(Errors.Invalid('Invalid or empty import file'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have enough to validate uplinks
|
||||||
|
if('bbs' === importType) {
|
||||||
|
for(let i = 0; i < importEntries.length; ++i) {
|
||||||
|
if(!validateUplinks(importEntries[i].uplinks)) {
|
||||||
|
return callback(Errors.Invalid('Invalid uplink(s)'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(!validateUplinks(uplinks || [])) {
|
||||||
|
return callback(Errors.Invalid('Invalid uplink(s)'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function init(callback) {
|
||||||
|
return initConfigAndDatabases(callback);
|
||||||
|
},
|
||||||
|
function validateAndCollectInput(callback) {
|
||||||
|
const msgArea = require('../../core/message_area.js');
|
||||||
|
const sysConfig = require('../../core/config.js').get();
|
||||||
|
|
||||||
|
let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } );
|
||||||
|
if(!msgConfs) {
|
||||||
|
return callback(Errors.DoesNotExist('No conferences exist in your configuration'));
|
||||||
|
}
|
||||||
|
|
||||||
|
msgConfs = msgConfs.map(mc => {
|
||||||
|
return {
|
||||||
|
name : mc.conf.name,
|
||||||
|
value : mc.confTag,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if(confTag && !msgConfs.find(mc => {
|
||||||
|
return confTag === mc.value;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingNetworkNames = Object.keys(_.get(sysConfig, 'messageNetworks.ftn.networks', {}));
|
||||||
|
|
||||||
|
if(networkName && !existingNetworkNames.find(net => networkName === net)) {
|
||||||
|
return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't use --uplinks without a network
|
||||||
|
if(!networkName && 0 === existingNetworkNames.length && uplinks) {
|
||||||
|
return callback(Errors.Invalid('Cannot use --uplinks without an FTN network to import to'));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnswers([
|
||||||
|
{
|
||||||
|
name : 'confTag',
|
||||||
|
message : 'Message conference:',
|
||||||
|
type : 'list',
|
||||||
|
choices : msgConfs,
|
||||||
|
pageSize : 10,
|
||||||
|
when : !confTag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'networkName',
|
||||||
|
message : 'FTN network name:',
|
||||||
|
type : 'list',
|
||||||
|
choices : [ '-None-' ].concat(existingNetworkNames),
|
||||||
|
pageSize : 10,
|
||||||
|
when : !networkName && existingNetworkNames.length > 0,
|
||||||
|
filter : (choice) => {
|
||||||
|
return '-None-' === choice ? undefined : choice;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answers => {
|
||||||
|
confTag = confTag || answers.confTag;
|
||||||
|
networkName = networkName || answers.networkName;
|
||||||
|
uplinks = uplinks || answers.uplinks;
|
||||||
|
|
||||||
|
importEntries.forEach(ie => {
|
||||||
|
ie.areaTag = ie.ftnTag.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function collectUplinks(callback) {
|
||||||
|
if(!networkName || uplinks || 'bbs' === importType) {
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnswers([
|
||||||
|
{
|
||||||
|
name : 'uplinks',
|
||||||
|
message : 'Uplink(s) (comma separated):',
|
||||||
|
type : 'input',
|
||||||
|
validate : (input) => {
|
||||||
|
const inputUplinks = input.split(/[\s,]+/);
|
||||||
|
return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
answers => {
|
||||||
|
uplinks = answers.uplinks;
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function confirmWithUser(callback) {
|
||||||
|
const sysConfig = require('../../core/config.js').get();
|
||||||
|
|
||||||
|
console.info(`Importing the following for "${confTag}"`);
|
||||||
|
console.info(`(${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`);
|
||||||
|
console.info('');
|
||||||
|
importEntries.forEach(ie => {
|
||||||
|
console.info(` ${ie.ftnTag} - ${ie.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(networkName) {
|
||||||
|
console.info('');
|
||||||
|
console.info(`For FTN network: ${networkName}`);
|
||||||
|
console.info(`Uplinks: ${uplinks}`);
|
||||||
|
console.info('');
|
||||||
|
console.info('Importing will NOT create required FTN network configurations.');
|
||||||
|
console.info('If you have not yet done this, you will need to complete additional steps after importing.');
|
||||||
|
console.info('See Message Networks docs for details.');
|
||||||
|
console.info('');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnswers([
|
||||||
|
{
|
||||||
|
name : 'proceed',
|
||||||
|
message : 'Proceed?',
|
||||||
|
type : 'confirm',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
answers => {
|
||||||
|
return callback(answers.proceed ? null : Errors.General('User canceled'));
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
function loadConfigHjson(callback) {
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
fs.readFile(configPath, 'utf8', (err, confData) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = hjson.parse(confData, { keepWsc : true } );
|
||||||
|
} catch(e) {
|
||||||
|
return callback(e);
|
||||||
|
}
|
||||||
|
return callback(null, config);
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function performImport(config, callback) {
|
||||||
|
const confAreas = { messageConferences : {} };
|
||||||
|
confAreas.messageConferences[confTag] = { areas : {} };
|
||||||
|
|
||||||
|
const msgNetworks = { messageNetworks : { ftn : { areas : {} } } };
|
||||||
|
|
||||||
|
importEntries.forEach(ie => {
|
||||||
|
const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area
|
||||||
|
|
||||||
|
confAreas.messageConferences[confTag].areas[ie.areaTag] = {
|
||||||
|
name : ie.name,
|
||||||
|
desc : ie.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(networkName) {
|
||||||
|
msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = {
|
||||||
|
network : networkName,
|
||||||
|
tag : ie.ftnTag,
|
||||||
|
uplinks : specificUplinks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const newConfig = _.defaultsDeep(config, confAreas, msgNetworks);
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
|
if(!writeConfig(newConfig, configPath)) {
|
||||||
|
return callback(Errors.UnexpectedState('Failed writing configuration'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
console.error(err.reason ? err.reason : err.message);
|
||||||
|
} else {
|
||||||
|
const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"';
|
||||||
|
console.info('Configuration generated.');
|
||||||
|
console.info(`You may wish to validate changes made to ${getConfigPath()}`);
|
||||||
|
console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`);
|
||||||
|
console.info('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImportEntries(importType, importData) {
|
||||||
|
let importEntries = [];
|
||||||
|
|
||||||
|
if('na' === importType) {
|
||||||
|
//
|
||||||
|
// parse out
|
||||||
|
// TAG DESC
|
||||||
|
//
|
||||||
|
const re = /^([^\s]+)\s+([^\r\n]+)/gm;
|
||||||
|
let m;
|
||||||
|
|
||||||
|
while( (m = re.exec(importData) )) {
|
||||||
|
importEntries.push({
|
||||||
|
ftnTag : m[1].trim(),
|
||||||
|
name : m[2].trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ('bbs' === importType) {
|
||||||
|
//
|
||||||
|
// Various formats for AREAS.BBS seem to exist. We want to support as much as possible.
|
||||||
|
//
|
||||||
|
// SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS
|
||||||
|
// CODE TAG UPLINKS
|
||||||
|
//
|
||||||
|
// VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS
|
||||||
|
// TAG UPLINKS
|
||||||
|
//
|
||||||
|
// Misc
|
||||||
|
// PATH|OTHER TAG UPLINKS
|
||||||
|
//
|
||||||
|
// Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end)
|
||||||
|
//
|
||||||
|
const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm;
|
||||||
|
let m;
|
||||||
|
while ( (m = re.exec(importData) )) {
|
||||||
|
const tag = m[1].trim();
|
||||||
|
|
||||||
|
importEntries.push({
|
||||||
|
ftnTag : tag,
|
||||||
|
name : `Area: ${tag}`,
|
||||||
|
uplinks : m[2].trim().split(/[\s,]+/),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return importEntries;
|
||||||
|
}
|
||||||
|
|
||||||
function handleMessageBaseCommand() {
|
function handleMessageBaseCommand() {
|
||||||
|
|
||||||
function errUsage() {
|
function errUsage() {
|
||||||
|
@ -137,6 +450,7 @@ function handleMessageBaseCommand() {
|
||||||
const action = argv._[1];
|
const action = argv._[1];
|
||||||
|
|
||||||
return({
|
return({
|
||||||
areafix : areaFix,
|
areafix : areaFix,
|
||||||
|
'import-areas' : importAreas,
|
||||||
}[action] || errUsage)();
|
}[action] || errUsage)();
|
||||||
}
|
}
|
|
@ -70,23 +70,18 @@ The `config` command allows sysops to perform various system configuration and m
|
||||||
usage: optutil.js config <action> [<args>]
|
usage: optutil.js config <action> [<args>]
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
new generate a new/initial configuration
|
new generate a new/initial configuration
|
||||||
import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH
|
cat cat current configuration to stdout
|
||||||
|
|
||||||
import-areas args:
|
cat args:
|
||||||
--conf CONF_TAG specify conference tag in which to import areas
|
--no-color disable color
|
||||||
--network NETWORK specify network name/key to associate FTN areas
|
--no-comments strip any comments
|
||||||
--uplinks UL1,UL2,... specify one or more comma separated uplinks
|
|
||||||
--type TYPE specifies area import type. valid options are "bbs" and "na"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
| Action | Description | Examples |
|
| Action | Description | Examples |
|
||||||
|-----------|-------------------|---------------------------------------|
|
|-----------|-------------------|---------------------------------------|
|
||||||
| `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) |
|
| `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) |
|
||||||
| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` |
|
| `cat` | Pretty prints current `config.hjson` configuration to stdout. | `./oputil.js config cat` |
|
||||||
|
|
||||||
When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args".
|
|
||||||
|
|
||||||
## File Base Management
|
## File Base Management
|
||||||
The `fb` command provides a powerful file base management interface.
|
The `fb` command provides a powerful file base management interface.
|
||||||
|
@ -189,3 +184,28 @@ file_crc32: fc6655d
|
||||||
file_md5: 3455f74bbbf9539e69bd38f45e039a4e
|
file_md5: 3455f74bbbf9539e69bd38f45e039a4e
|
||||||
file_sha1: 558fab3b49a8ac302486e023a3c2a86bd4e4b948
|
file_sha1: 558fab3b49a8ac302486e023a3c2a86bd4e4b948
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Message Base Management
|
||||||
|
The `mb` command provides various Message Base related tools:
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: oputil.js mb <action> [<args>]
|
||||||
|
|
||||||
|
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.
|
||||||
|
import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH
|
||||||
|
|
||||||
|
import-areas args:
|
||||||
|
--conf CONF_TAG conference tag in which to import areas
|
||||||
|
--network NETWORK network name/key to associate FTN areas
|
||||||
|
--uplinks UL1,UL2,... one or more comma separated uplinks
|
||||||
|
--type TYPE area import type. valid options are "bbs" and "na"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Action | Description | Examples |
|
||||||
|
|-----------|-------------------|---------------------------------------|
|
||||||
|
| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file. Optionally maps areas to FTN networks. | `./oputil.js config import-areas /some/path/l33tnet.na` |
|
||||||
|
|
||||||
|
When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args".
|
||||||
|
|
Loading…
Reference in New Issue