2017-02-16 03:27:16 +00:00
/* jslint node: true */
/* eslint-disable no-console */
'use strict' ;
// ENiGMA½
const resolvePath = require ( '../../core/misc_util.js' ) . resolvePath ;
2018-11-11 06:59:26 +00:00
const {
printUsageAndSetExitCode ,
getConfigPath ,
argv ,
ExitCodes ,
initConfigAndDatabases
} = require ( './oputil_common.js' ) ;
2017-02-16 03:27:16 +00:00
const getHelpFor = require ( './oputil_help.js' ) . getHelpFor ;
2017-02-20 18:31:24 +00:00
const Errors = require ( '../../core/enig_error.js' ) . Errors ;
2017-02-16 03:27:16 +00:00
// deps
2018-11-11 07:19:01 +00:00
const async = require ( 'async' ) ;
const inq = require ( 'inquirer' ) ;
const mkdirsSync = require ( 'fs-extra' ) . mkdirsSync ;
const fs = require ( 'graceful-fs' ) ;
const hjson = require ( 'hjson' ) ;
const paths = require ( 'path' ) ;
const _ = require ( 'lodash' ) ;
const sanatizeFilename = require ( 'sanitize-filename' ) ;
2017-02-16 03:27:16 +00:00
2018-11-11 06:59:26 +00:00
const packageJson = require ( '../../package.json' ) ;
2017-02-16 03:27:16 +00:00
exports . handleConfigCommand = handleConfigCommand ;
function getAnswers ( questions , cb ) {
2018-06-22 05:15:04 +00:00
inq . prompt ( questions ) . then ( answers => {
return cb ( answers ) ;
} ) ;
2017-02-16 03:27:16 +00:00
}
2018-11-08 03:24:05 +00:00
const ConfigIncludeKeys = [
'theme' ,
2018-11-11 08:00:42 +00:00
'users.preAuthIdleLogoutSeconds' , 'users.idleLogoutSeconds' ,
'users.newUserNames' ,
'paths.logs' ,
2018-11-11 03:00:25 +00:00
'loginServers' ,
'contentServers' ,
2018-11-11 03:26:23 +00:00
'fileBase.areaStoragePrefix' ,
2018-11-11 08:00:42 +00:00
'logging.rotatingFile' ,
2018-11-08 03:24:05 +00:00
] ;
2018-11-11 06:59:26 +00:00
const HJSONStringifyComonOpts = {
emitRootBraces : true ,
bracesSameLine : true ,
space : 4 ,
keepWsc : true ,
quotes : 'min' ,
eol : '\n' ,
} ;
2017-02-16 03:27:16 +00:00
const QUESTIONS = {
2018-06-22 05:15:04 +00:00
Intro : [
{
name : 'createNewConfig' ,
message : 'Create a new configuration?' ,
type : 'confirm' ,
default : false ,
} ,
{
name : 'configPath' ,
message : 'Configuration path:' ,
default : getConfigPath ( ) ,
when : answers => answers . createNewConfig
} ,
] ,
OverwriteConfig : [
{
name : 'overwriteConfig' ,
message : 'Config file exists. Overwrite?' ,
type : 'confirm' ,
default : false ,
}
] ,
Basic : [
{
name : 'boardName' ,
message : 'BBS name:' ,
default : 'New ENiGMA½ BBS' ,
} ,
] ,
Misc : [
{
name : 'loggingLevel' ,
message : 'Logging level:' ,
type : 'list' ,
choices : [ 'Error' , 'Warn' , 'Info' , 'Debug' , 'Trace' ] ,
default : 2 ,
filter : s => s . toLowerCase ( ) ,
} ,
] ,
MessageConfAndArea : [
{
name : 'msgConfName' ,
message : 'First message conference:' ,
default : 'Local' ,
} ,
{
name : 'msgConfDesc' ,
message : 'Conference description:' ,
default : 'Local Areas' ,
} ,
{
name : 'msgAreaName' ,
message : 'First area in message conference:' ,
default : 'General' ,
} ,
{
name : 'msgAreaDesc' ,
message : 'Area description:' ,
default : 'General chit-chat' ,
}
]
2017-02-16 03:27:16 +00:00
} ;
function makeMsgConfAreaName ( s ) {
2018-06-22 05:15:04 +00:00
return s . toLowerCase ( ) . replace ( /\s+/g , '_' ) ;
2017-02-16 03:27:16 +00:00
}
function askNewConfigQuestions ( cb ) {
2018-06-22 05:15:04 +00:00
const ui = new inq . ui . BottomBar ( ) ;
let configPath ;
let config ;
async . waterfall (
[
function intro ( callback ) {
getAnswers ( QUESTIONS . Intro , answers => {
if ( ! answers . createNewConfig ) {
return callback ( 'exit' ) ;
}
// adjust for ~ and the like
configPath = resolvePath ( answers . configPath ) ;
const configDir = paths . dirname ( configPath ) ;
mkdirsSync ( configDir ) ;
//
// Check if the file exists and can be written to
//
fs . access ( configPath , fs . F _OK | fs . W _OK , err => {
if ( err ) {
if ( 'EACCES' === err . code ) {
ui . log . write ( ` ${ configPath } cannot be written to ` ) ;
callback ( 'exit' ) ;
} else if ( 'ENOENT' === err . code ) {
callback ( null , false ) ;
}
} else {
callback ( null , true ) ; // exists + writable
}
} ) ;
} ) ;
} ,
function promptOverwrite ( needPrompt , callback ) {
if ( needPrompt ) {
getAnswers ( QUESTIONS . OverwriteConfig , answers => {
2018-11-08 03:24:05 +00:00
return callback ( answers . overwriteConfig ? null : 'exit' ) ;
2018-06-22 05:15:04 +00:00
} ) ;
} else {
2018-11-08 03:24:05 +00:00
return callback ( null ) ;
2018-06-22 05:15:04 +00:00
}
} ,
function basic ( callback ) {
getAnswers ( QUESTIONS . Basic , answers => {
2018-11-08 03:24:05 +00:00
const defaultConfig = require ( '../../core/config.js' ) . getDefaultConfig ( ) ;
// start by plopping in values we want directly from config.js
const template = hjson . rt . parse ( fs . readFileSync ( paths . join ( _ _dirname , '../../misc/config_template.in.hjson' ) , 'utf8' ) ) ;
2018-06-22 05:15:04 +00:00
2018-11-08 03:24:05 +00:00
const direct = { } ;
_ . each ( ConfigIncludeKeys , keyPath => {
_ . set ( direct , keyPath , _ . get ( defaultConfig , keyPath ) ) ;
} ) ;
config = _ . mergeWith ( template , direct ) ;
// we can override/add to it based on user input from this point on...
config . general . boardName = answers . boardName ;
2018-11-10 02:02:07 +00:00
2018-11-08 03:24:05 +00:00
return callback ( null ) ;
2018-06-22 05:15:04 +00:00
} ) ;
} ,
function msgConfAndArea ( callback ) {
getAnswers ( QUESTIONS . MessageConfAndArea , answers => {
const confName = makeMsgConfAreaName ( answers . msgConfName ) ;
const areaName = makeMsgConfAreaName ( answers . msgAreaName ) ;
config . messageConferences [ confName ] = {
name : answers . msgConfName ,
desc : answers . msgConfDesc ,
sort : 1 ,
default : true ,
} ;
config . messageConferences [ confName ] . areas = { } ;
config . messageConferences [ confName ] . areas [ areaName ] = {
name : answers . msgAreaName ,
desc : answers . msgAreaDesc ,
sort : 1 ,
default : true ,
} ;
2018-11-08 03:24:05 +00:00
return callback ( null ) ;
2018-06-22 05:15:04 +00:00
} ) ;
} ,
function misc ( callback ) {
getAnswers ( QUESTIONS . Misc , answers => {
2018-11-08 03:24:05 +00:00
config . logging . rotatingFile . level = answers . loggingLevel ;
2018-06-22 05:15:04 +00:00
2018-11-08 03:24:05 +00:00
return callback ( null ) ;
2018-06-22 05:15:04 +00:00
} ) ;
}
] ,
err => {
2018-11-08 03:24:05 +00:00
return cb ( err , configPath , config ) ;
2018-06-22 05:15:04 +00:00
}
) ;
2017-02-16 03:27:16 +00:00
}
2017-02-20 18:31:24 +00:00
function writeConfig ( config , path ) {
2018-11-11 06:59:26 +00:00
config = hjson . stringify ( config , HJSONStringifyComonOpts )
. replace ( /%ENIG_VERSION%/g , packageJson . version )
. replace ( /%HJSON_VERSION%/g , hjson . version )
;
2018-06-22 05:15:04 +00:00
try {
fs . writeFileSync ( path , config , 'utf8' ) ;
return true ;
} catch ( e ) {
return false ;
}
2017-02-20 18:31:24 +00:00
}
2018-11-11 07:19:01 +00:00
const copyFileSyncSilent = ( to , from , flags ) => {
try {
fs . copyFileSync ( to , from , flags ) ;
} catch ( e ) { }
} ;
2017-02-20 18:31:24 +00:00
function buildNewConfig ( ) {
2018-06-22 05:15:04 +00:00
askNewConfigQuestions ( ( err , configPath , config ) => {
if ( err ) {
return ;
}
2018-11-11 07:19:01 +00:00
const bn = sanatizeFilename ( config . general . boardName ) . replace ( / /g , '_' ) . toLowerCase ( ) ;
const menuFile = ` ${ bn } .hjson ` ;
copyFileSyncSilent (
paths . join ( _ _dirname , '../../config/menu.hjson' ) ,
paths . join ( _ _dirname , '../../config/' , menuFile ) ,
fs . constants . COPYFILE _EXCL
) ;
const promptFile = ` ${ bn } _prompt.hjson ` ;
copyFileSyncSilent (
paths . join ( _ _dirname , '../../config/prompt.hjson' ) ,
paths . join ( _ _dirname , '../../config/' , promptFile ) ,
fs . constants . COPYFILE _EXCL
)
config . general . menuFile = menuFile ;
config . general . promptFile = promptFile ;
2018-06-22 05:15:04 +00:00
if ( writeConfig ( config , configPath ) ) {
console . info ( 'Configuration generated' ) ;
} else {
console . error ( 'Failed writing configuration' ) ;
}
} ) ;
2017-02-20 18:31:24 +00:00
}
function validateUplinks ( uplinks ) {
2018-06-22 05:15:04 +00:00
const ftnAddress = require ( '../../core/ftn_address.js' ) ;
const valid = uplinks . every ( ul => {
const addr = ftnAddress . fromString ( ul ) ;
return addr ;
} ) ;
return valid ;
2017-02-20 18:31:24 +00:00
}
function getMsgAreaImportType ( path ) {
2018-06-22 05:15:04 +00:00
if ( argv . type ) {
return argv . type . toLowerCase ( ) ;
}
2017-02-20 18:31:24 +00:00
2018-06-22 05:15:04 +00:00
const ext = paths . extname ( path ) . toLowerCase ( ) . substr ( 1 ) ;
return ext ; // .bbs|.na|...
2017-02-20 18:31:24 +00:00
}
function importAreas ( ) {
2018-06-22 05:15:04 +00:00
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 ( '' ) ;
}
}
) ;
2017-02-20 18:31:24 +00:00
}
function getImportEntries ( importType , importData ) {
2018-06-22 05:15:04 +00:00
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 ;
2017-02-20 18:31:24 +00:00
}
2018-11-11 06:59:26 +00:00
function catCurrentConfig ( ) {
try {
const config = hjson . rt . parse ( fs . readFileSync ( getConfigPath ( ) , 'utf8' ) ) ;
const hjsonOpts = Object . assign ( { } , HJSONStringifyComonOpts , {
colors : false === argv . colors ? false : true ,
keepWsc : false === argv . comments ? false : true ,
} ) ;
console . log ( hjson . stringify ( config , hjsonOpts ) ) ;
} catch ( e ) {
if ( 'ENOENT' == e . code ) {
console . error ( ` File not found: ${ getConfigPath ( ) } ` ) ;
} else {
console . error ( e ) ;
}
}
}
2017-02-20 18:31:24 +00:00
function handleConfigCommand ( ) {
2018-06-22 05:15:04 +00:00
if ( true === argv . help ) {
return printUsageAndSetExitCode ( getHelpFor ( 'Config' ) , ExitCodes . ERROR ) ;
}
2017-02-20 18:31:24 +00:00
2018-06-22 05:15:04 +00:00
const action = argv . _ [ 1 ] ;
2017-02-20 18:31:24 +00:00
2018-06-22 05:15:04 +00:00
switch ( action ) {
case 'new' : return buildNewConfig ( ) ;
case 'import-areas' : return importAreas ( ) ;
2018-11-11 06:59:26 +00:00
case 'cat' : return catCurrentConfig ( ) ;
2017-02-20 18:31:24 +00:00
2018-06-22 05:15:04 +00:00
default : return printUsageAndSetExitCode ( getHelpFor ( 'Config' ) , ExitCodes . ERROR ) ;
}
2017-02-16 03:27:16 +00:00
}