2018-01-01 00:54:11 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
'use strict';
|
|
|
|
|
2018-11-25 03:02:19 +00:00
|
|
|
const {
|
|
|
|
printUsageAndSetExitCode,
|
|
|
|
getConfigPath,
|
|
|
|
ExitCodes,
|
|
|
|
argv,
|
|
|
|
initConfigAndDatabases,
|
|
|
|
getAnswers,
|
|
|
|
writeConfig,
|
2020-05-02 22:48:24 +00:00
|
|
|
} = require('./oputil_common.js');
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
|
|
|
const Address = require('../ftn_address.js');
|
|
|
|
const Errors = require('../enig_error.js').Errors;
|
2018-01-01 00:54:11 +00:00
|
|
|
|
|
|
|
// deps
|
2022-06-05 20:04:25 +00:00
|
|
|
const async = require('async');
|
|
|
|
const paths = require('path');
|
|
|
|
const fs = require('fs');
|
|
|
|
const hjson = require('hjson');
|
|
|
|
const _ = require('lodash');
|
|
|
|
const moment = require('moment');
|
2018-01-01 00:54:11 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
exports.handleMessageBaseCommand = handleMessageBaseCommand;
|
2018-01-01 00:54:11 +00:00
|
|
|
|
|
|
|
function areaFix() {
|
2018-06-22 05:15:04 +00:00
|
|
|
//
|
|
|
|
// oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS]
|
|
|
|
//
|
2022-06-05 20:04:25 +00:00
|
|
|
if (argv._.length < 3) {
|
|
|
|
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function init(callback) {
|
|
|
|
return initConfigAndDatabases(callback);
|
|
|
|
},
|
|
|
|
function validateAddress(callback) {
|
|
|
|
const addrArg = argv._.slice(-1)[0];
|
|
|
|
const ftnAddr = Address.fromString(addrArg);
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!ftnAddr) {
|
|
|
|
return callback(
|
|
|
|
Errors.Invalid(`"${addrArg}" is not a valid FTN address`)
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// 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');
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (argv.from) {
|
2018-06-22 05:15:04 +00:00
|
|
|
User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
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) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
return callback(
|
|
|
|
null,
|
|
|
|
ftnAddr,
|
|
|
|
fromName || 'SysOp',
|
|
|
|
err ? 0 : User.RootUserID
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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'"
|
|
|
|
//
|
2022-06-05 20:04:25 +00:00
|
|
|
const messageBody =
|
|
|
|
argv._.slice(2, -1)
|
|
|
|
.map(arg => {
|
|
|
|
return arg.replace(/["']/g, '');
|
|
|
|
})
|
|
|
|
.join('\r\n') + '\n';
|
2018-06-22 05:15:04 +00:00
|
|
|
|
|
|
|
const Message = require('../message.js');
|
|
|
|
|
|
|
|
const message = new Message({
|
2022-06-05 20:04:25 +00:00
|
|
|
toUserName: argv.to || 'AreaFix',
|
|
|
|
fromUserName: fromName,
|
|
|
|
subject: argv.password || '',
|
|
|
|
message: messageBody,
|
|
|
|
areaTag: Message.WellKnownAreaTags.Private, // mark private
|
|
|
|
meta: {
|
|
|
|
System: {
|
|
|
|
[Message.SystemMetaNames.RemoteToUser]: ftnAddr.toString(), // where to send it
|
|
|
|
[Message.SystemMetaNames.ExternalFlavor]:
|
|
|
|
Message.AddressFlavor.FTN, // on FTN-style network
|
|
|
|
},
|
|
|
|
},
|
2018-06-22 05:15:04 +00:00
|
|
|
});
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (0 !== fromUserId) {
|
2018-06-22 05:15:04 +00:00
|
|
|
message.setLocalFromUserId(fromUserId);
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback(null, message);
|
|
|
|
},
|
|
|
|
function persistMessage(message, callback) {
|
|
|
|
message.persist(err => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!err) {
|
|
|
|
console.log(
|
|
|
|
'AreaFix message persisted and will be exported at next scheduled scan'
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
return callback(err);
|
|
|
|
});
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2018-06-22 05:15:04 +00:00
|
|
|
],
|
|
|
|
err => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
process.exitCode = ExitCodes.ERROR;
|
|
|
|
console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2018-01-01 00:54:11 +00:00
|
|
|
}
|
|
|
|
|
2018-11-25 03:02:19 +00:00
|
|
|
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) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (argv.type) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return argv.type.toLowerCase();
|
|
|
|
}
|
|
|
|
|
|
|
|
return paths.extname(path).substr(1).toLowerCase(); // bbs|na|...
|
|
|
|
}
|
|
|
|
|
|
|
|
function importAreas() {
|
|
|
|
const importPath = argv._[argv._.length - 1];
|
2022-06-05 20:04:25 +00:00
|
|
|
if (argv._.length < 3 || !importPath || 0 === importPath.length) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
|
|
|
}
|
|
|
|
|
|
|
|
const importType = getMsgAreaImportType(importPath);
|
2022-06-05 20:04:25 +00:00
|
|
|
if ('na' !== importType && 'bbs' !== importType) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return console.error(`"${importType}" is not a recognized import file type`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// optional data - we'll prompt if for anything not found
|
2022-06-05 20:04:25 +00:00
|
|
|
let confTag = argv.conf;
|
|
|
|
let networkName = argv.network;
|
|
|
|
let uplinks = argv.uplinks;
|
|
|
|
if (uplinks) {
|
2018-11-25 03:02:19 +00:00
|
|
|
uplinks = uplinks.split(/[\s,]+/);
|
|
|
|
}
|
|
|
|
|
|
|
|
let importEntries;
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function readImportFile(callback) {
|
|
|
|
fs.readFile(importPath, 'utf8', (err, importData) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
importEntries = getImportEntries(importType, importData);
|
2022-06-05 20:04:25 +00:00
|
|
|
if (0 === importEntries.length) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(Errors.Invalid('Invalid or empty import file'));
|
|
|
|
}
|
|
|
|
|
|
|
|
// We should have enough to validate uplinks
|
2022-06-05 20:04:25 +00:00
|
|
|
if ('bbs' === importType) {
|
|
|
|
for (let i = 0; i < importEntries.length; ++i) {
|
|
|
|
if (!validateUplinks(importEntries[i].uplinks)) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(Errors.Invalid('Invalid uplink(s)'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!validateUplinks(uplinks || [])) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(Errors.Invalid('Invalid uplink(s)'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback(null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function init(callback) {
|
|
|
|
return initConfigAndDatabases(callback);
|
|
|
|
},
|
|
|
|
function validateAndCollectInput(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
const msgArea = require('../../core/message_area.js');
|
|
|
|
const sysConfig = require('../../core/config.js').get();
|
2018-11-25 03:02:19 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
let msgConfs = msgArea.getSortedAvailMessageConferences(null, {
|
|
|
|
noClient: true,
|
|
|
|
});
|
|
|
|
if (!msgConfs) {
|
|
|
|
return callback(
|
|
|
|
Errors.DoesNotExist('No conferences exist in your configuration')
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
msgConfs = msgConfs.map(mc => {
|
|
|
|
return {
|
2022-06-05 20:04:25 +00:00
|
|
|
name: mc.conf.name,
|
|
|
|
value: mc.confTag,
|
2018-11-25 03:02:19 +00:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (
|
|
|
|
confTag &&
|
|
|
|
!msgConfs.find(mc => {
|
|
|
|
return confTag === mc.value;
|
|
|
|
})
|
|
|
|
) {
|
|
|
|
return callback(
|
|
|
|
Errors.DoesNotExist(`Conference "${confTag}" does not exist`)
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
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`
|
|
|
|
)
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// can't use --uplinks without a network
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!networkName && 0 === existingNetworkNames.length && uplinks) {
|
|
|
|
return callback(
|
|
|
|
Errors.Invalid(
|
|
|
|
'Cannot use --uplinks without an FTN network to import to'
|
|
|
|
)
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
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();
|
|
|
|
});
|
2018-11-25 03:02:19 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
return callback(null);
|
|
|
|
}
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
},
|
|
|
|
function collectUplinks(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!networkName || uplinks || 'bbs' === importType) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(null);
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
getAnswers(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: 'uplinks',
|
|
|
|
message: 'Uplink(s) (comma separated):',
|
|
|
|
type: 'input',
|
|
|
|
validate: input => {
|
|
|
|
const inputUplinks = input.split(/[\s,]+/);
|
|
|
|
return validateUplinks(inputUplinks)
|
|
|
|
? true
|
|
|
|
: 'Invalid uplink(s)';
|
|
|
|
},
|
2018-11-25 03:02:19 +00:00
|
|
|
},
|
2022-06-05 20:04:25 +00:00
|
|
|
],
|
|
|
|
answers => {
|
|
|
|
uplinks = answers.uplinks;
|
|
|
|
return callback(null);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
2022-06-05 20:04:25 +00:00
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
},
|
|
|
|
function confirmWithUser(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
const sysConfig = require('../../core/config.js').get();
|
2018-11-25 03:02:19 +00:00
|
|
|
|
|
|
|
console.info(`Importing the following for "${confTag}"`);
|
2022-06-05 20:04:25 +00:00
|
|
|
console.info(
|
|
|
|
`(${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
console.info('');
|
|
|
|
importEntries.forEach(ie => {
|
|
|
|
console.info(` ${ie.ftnTag} - ${ie.name}`);
|
|
|
|
});
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (networkName) {
|
2018-11-25 03:02:19 +00:00
|
|
|
console.info('');
|
|
|
|
console.info(`For FTN network: ${networkName}`);
|
|
|
|
console.info(`Uplinks: ${uplinks}`);
|
|
|
|
console.info('');
|
2022-06-05 20:04:25 +00:00
|
|
|
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.'
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
console.info('See Message Networks docs for details.');
|
|
|
|
console.info('');
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
getAnswers(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: 'proceed',
|
|
|
|
message: 'Proceed?',
|
|
|
|
type: 'confirm',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
answers => {
|
|
|
|
return callback(
|
|
|
|
answers.proceed ? null : Errors.General('User canceled')
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
2022-06-05 20:04:25 +00:00
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
},
|
|
|
|
function loadConfigHjson(callback) {
|
|
|
|
const configPath = getConfigPath();
|
|
|
|
fs.readFile(configPath, 'utf8', (err, confData) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
let config;
|
|
|
|
try {
|
2022-06-05 20:04:25 +00:00
|
|
|
config = hjson.parse(confData, { keepWsc: true });
|
|
|
|
} catch (e) {
|
2018-11-25 03:02:19 +00:00
|
|
|
return callback(e);
|
|
|
|
}
|
|
|
|
return callback(null, config);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function performImport(config, callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
const confAreas = { messageConferences: {} };
|
|
|
|
confAreas.messageConferences[confTag] = { areas: {} };
|
2018-11-25 03:02:19 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
const msgNetworks = { messageNetworks: { ftn: { areas: {} } } };
|
2018-11-25 03:02:19 +00:00
|
|
|
|
|
|
|
importEntries.forEach(ie => {
|
2022-06-05 20:04:25 +00:00
|
|
|
const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area
|
2018-11-25 03:02:19 +00:00
|
|
|
|
|
|
|
confAreas.messageConferences[confTag].areas[ie.areaTag] = {
|
2022-06-05 20:04:25 +00:00
|
|
|
name: ie.name,
|
|
|
|
desc: ie.name,
|
2018-11-25 03:02:19 +00:00
|
|
|
};
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (networkName) {
|
2018-11-25 03:02:19 +00:00
|
|
|
msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = {
|
2022-06-05 20:04:25 +00:00
|
|
|
network: networkName,
|
|
|
|
tag: ie.ftnTag,
|
|
|
|
uplinks: specificUplinks,
|
2018-11-25 03:02:19 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const newConfig = _.defaultsDeep(config, confAreas, msgNetworks);
|
|
|
|
const configPath = getConfigPath();
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!writeConfig(newConfig, configPath)) {
|
|
|
|
return callback(
|
|
|
|
Errors.UnexpectedState('Failed writing configuration')
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return callback(null);
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2018-11-25 03:02:19 +00:00
|
|
|
],
|
|
|
|
err => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-11-25 03:02:19 +00:00
|
|
|
console.error(err.reason ? err.reason : err.message);
|
|
|
|
} else {
|
|
|
|
const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"';
|
2018-12-04 06:51:43 +00:00
|
|
|
console.info('Import complete.');
|
2022-06-05 20:04:25 +00:00
|
|
|
console.info(
|
|
|
|
`You may wish to validate changes made to ${getConfigPath()}`
|
|
|
|
);
|
2018-11-25 03:02:19 +00:00
|
|
|
console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`);
|
|
|
|
console.info('');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getImportEntries(importType, importData) {
|
|
|
|
let importEntries = [];
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if ('na' === importType) {
|
2018-11-25 03:02:19 +00:00
|
|
|
//
|
|
|
|
// parse out
|
|
|
|
// TAG DESC
|
|
|
|
//
|
|
|
|
const re = /^([^\s]+)\s+([^\r\n]+)/gm;
|
|
|
|
let m;
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
while ((m = re.exec(importData))) {
|
2018-11-25 03:02:19 +00:00
|
|
|
importEntries.push({
|
2022-06-05 20:04:25 +00:00
|
|
|
ftnTag: m[1].trim(),
|
|
|
|
name: m[2].trim(),
|
2018-11-25 03:02:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} 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;
|
2022-06-05 20:04:25 +00:00
|
|
|
while ((m = re.exec(importData))) {
|
2018-11-25 03:02:19 +00:00
|
|
|
const tag = m[1].trim();
|
|
|
|
|
|
|
|
importEntries.push({
|
2022-06-05 20:04:25 +00:00
|
|
|
ftnTag: tag,
|
|
|
|
name: `Area: ${tag}`,
|
|
|
|
uplinks: m[2].trim().split(/[\s,]+/),
|
2018-11-25 03:02:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return importEntries;
|
|
|
|
}
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
function dumpQWKPacket() {
|
2020-04-22 01:50:04 +00:00
|
|
|
const packetPath = argv._[argv._.length - 1];
|
2022-06-05 20:04:25 +00:00
|
|
|
if (argv._.length < 3 || !packetPath || 0 === packetPath.length) {
|
2020-05-02 22:48:24 +00:00
|
|
|
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
2020-04-22 01:50:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
2022-06-05 20:04:25 +00:00
|
|
|
callback => {
|
2020-04-22 01:50:04 +00:00
|
|
|
return initConfigAndDatabases(callback);
|
|
|
|
},
|
2022-06-05 20:04:25 +00:00
|
|
|
callback => {
|
2020-04-22 01:50:04 +00:00
|
|
|
const { QWKPacketReader } = require('../qwk_mail_packet');
|
2020-05-02 22:48:24 +00:00
|
|
|
const reader = new QWKPacketReader(packetPath);
|
2020-04-22 01:50:04 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
reader.on('error', err => {
|
|
|
|
console.error(`ERROR: ${err.message}`);
|
|
|
|
return callback(err);
|
2020-04-22 01:50:04 +00:00
|
|
|
});
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
reader.on('done', () => {
|
|
|
|
return callback(null);
|
2020-04-22 01:50:04 +00:00
|
|
|
});
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
reader.on('archive type', archiveType => {
|
|
|
|
console.info(`-> Archive type: ${archiveType}`);
|
|
|
|
});
|
2020-05-01 04:07:29 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
reader.on('creator', creator => {
|
|
|
|
console.info(`-> Creator: ${creator}`);
|
|
|
|
});
|
2020-05-01 04:07:29 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
reader.on('message', message => {
|
|
|
|
console.info('--- message ---');
|
|
|
|
console.info(`To: ${message.toUserName}`);
|
|
|
|
console.info(`From: ${message.fromUserName}`);
|
|
|
|
console.info(`Subject: ${message.subject}`);
|
|
|
|
console.info(`Message:\r\n${message.message}`);
|
|
|
|
});
|
2020-05-01 04:07:29 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
reader.read();
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2020-05-01 04:07:29 +00:00
|
|
|
],
|
|
|
|
err => {
|
2020-06-16 01:08:55 +00:00
|
|
|
if (err) {
|
|
|
|
console.error(`QWK dump failed: ${err.message}`);
|
|
|
|
}
|
2020-05-01 04:07:29 +00:00
|
|
|
}
|
2020-06-16 01:08:55 +00:00
|
|
|
);
|
2020-05-01 04:07:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
function exportQWKPacket() {
|
|
|
|
let packetPath = argv._[argv._.length - 1];
|
2022-06-05 20:04:25 +00:00
|
|
|
if (argv._.length < 3 || !packetPath || 0 === packetPath.length) {
|
2020-05-02 22:48:24 +00:00
|
|
|
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
2020-05-01 04:07:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
// oputil mb qwk-export TAGS PATH [--user USER] [--after TIMESTAMP]
|
|
|
|
// [areaTag1,areaTag2,...] PATH --user USER --after TIMESTAMP
|
|
|
|
let bbsID = 'ENIGMA';
|
|
|
|
const filename = paths.basename(packetPath);
|
|
|
|
if (filename) {
|
|
|
|
const ext = paths.extname(filename);
|
|
|
|
bbsID = paths.basename(filename, ext);
|
2020-05-01 04:07:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
packetPath = paths.dirname(packetPath);
|
2020-05-01 04:07:29 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
const posArgLen = argv._.length;
|
2020-05-01 04:07:29 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
let areaTags;
|
|
|
|
if (4 === posArgLen) {
|
|
|
|
areaTags = argv._[posArgLen - 2].split(',');
|
|
|
|
} else {
|
|
|
|
areaTags = [];
|
|
|
|
}
|
2020-05-01 04:07:29 +00:00
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
let newerThanTimestamp = null;
|
|
|
|
if (argv.after) {
|
|
|
|
const ts = moment(argv.after);
|
|
|
|
if (ts.isValid()) {
|
|
|
|
newerThanTimestamp = ts.format();
|
|
|
|
}
|
|
|
|
}
|
2020-05-01 04:07:29 +00:00
|
|
|
|
|
|
|
const userName = argv.user || '-';
|
|
|
|
|
2020-05-06 01:01:47 +00:00
|
|
|
const writerOptions = {
|
2022-06-05 20:04:25 +00:00
|
|
|
enableQWKE: !(false === argv.qwke),
|
|
|
|
enableHeadersExtension: !(false === argv.synchronet),
|
|
|
|
enableAtKludges: !(false === argv.synchronet),
|
|
|
|
archiveFormat: argv.format || 'application/zip',
|
2020-05-06 01:01:47 +00:00
|
|
|
};
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
let totalExported = 0;
|
2020-05-01 04:07:29 +00:00
|
|
|
async.waterfall(
|
|
|
|
[
|
2022-06-05 20:04:25 +00:00
|
|
|
callback => {
|
2020-05-01 04:07:29 +00:00
|
|
|
return initConfigAndDatabases(callback);
|
|
|
|
},
|
2022-06-05 20:04:25 +00:00
|
|
|
callback => {
|
2020-05-01 04:07:29 +00:00
|
|
|
const User = require('../../core/user.js');
|
|
|
|
|
|
|
|
User.getUserIdAndName(userName, (err, userId) => {
|
|
|
|
if (err) {
|
|
|
|
if ('-' === userName) {
|
|
|
|
userId = 1;
|
|
|
|
} else {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return User.getUser(userId, callback);
|
2020-04-22 01:50:04 +00:00
|
|
|
});
|
2020-05-01 04:07:29 +00:00
|
|
|
},
|
2020-05-02 22:48:24 +00:00
|
|
|
(user, callback) => {
|
|
|
|
// populate area tags with all available to user
|
|
|
|
// if they were not explicitly supplied
|
|
|
|
if (!areaTags.length) {
|
|
|
|
const {
|
2022-06-05 20:04:25 +00:00
|
|
|
getAllAvailableMessageAreaTags,
|
2020-05-02 22:48:24 +00:00
|
|
|
} = require('../../core/message_area');
|
|
|
|
|
2020-05-03 16:42:57 +00:00
|
|
|
areaTags = getAllAvailableMessageAreaTags();
|
2020-05-02 22:48:24 +00:00
|
|
|
}
|
|
|
|
return callback(null, user);
|
|
|
|
},
|
2020-05-01 04:07:29 +00:00
|
|
|
(user, callback) => {
|
|
|
|
const Message = require('../message');
|
|
|
|
|
|
|
|
const filter = {
|
2022-06-05 20:04:25 +00:00
|
|
|
resultType: 'id',
|
|
|
|
areaTag: areaTags,
|
2020-05-02 22:48:24 +00:00
|
|
|
newerThanTimestamp,
|
2020-05-01 04:07:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// public
|
|
|
|
Message.findMessages(filter, (err, publicMessageIds) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
delete filter.areaTag;
|
|
|
|
filter.privateTagUserId = user.userId;
|
|
|
|
|
|
|
|
Message.findMessages(filter, (err, privateMessageIds) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
return callback(
|
|
|
|
err,
|
|
|
|
user,
|
|
|
|
Message,
|
|
|
|
privateMessageIds.concat(publicMessageIds)
|
|
|
|
);
|
2020-05-01 04:07:29 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
(user, Message, messageIds, callback) => {
|
|
|
|
const { QWKPacketWriter } = require('../qwk_mail_packet');
|
2022-06-05 20:04:25 +00:00
|
|
|
const writer = new QWKPacketWriter(
|
|
|
|
Object.assign(writerOptions, {
|
|
|
|
bbsID,
|
|
|
|
user,
|
|
|
|
})
|
|
|
|
);
|
2020-04-22 01:50:04 +00:00
|
|
|
|
2020-05-01 04:07:29 +00:00
|
|
|
writer.on('ready', () => {
|
2022-06-05 20:04:25 +00:00
|
|
|
async.eachSeries(
|
|
|
|
messageIds,
|
|
|
|
(messageId, nextMessageId) => {
|
|
|
|
const message = new Message();
|
|
|
|
message.load({ messageId }, err => {
|
|
|
|
if (!err) {
|
|
|
|
writer.appendMessage(message);
|
|
|
|
++totalExported;
|
|
|
|
}
|
|
|
|
return nextMessageId(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
writer.finish(packetPath);
|
|
|
|
if (err) {
|
|
|
|
console.error(
|
|
|
|
`Failed to write one or more messages: ${err.message}`
|
|
|
|
);
|
2020-05-01 04:07:29 +00:00
|
|
|
}
|
|
|
|
}
|
2022-06-05 20:04:25 +00:00
|
|
|
);
|
2020-04-22 01:50:04 +00:00
|
|
|
});
|
|
|
|
|
2020-05-02 22:48:24 +00:00
|
|
|
writer.on('warning', err => {
|
|
|
|
console.warn(`!!! ${err.reason ? err.reason : err.message}`);
|
|
|
|
});
|
|
|
|
|
2020-05-01 04:07:29 +00:00
|
|
|
writer.on('finished', () => {
|
|
|
|
return callback(null);
|
|
|
|
});
|
|
|
|
|
|
|
|
writer.init();
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2020-04-22 01:50:04 +00:00
|
|
|
],
|
|
|
|
err => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2020-05-02 22:48:24 +00:00
|
|
|
return console.error(err.reason ? err.reason : err.message);
|
2020-05-01 04:07:29 +00:00
|
|
|
}
|
2020-05-02 22:48:24 +00:00
|
|
|
|
|
|
|
console.info(`-> Exported ${totalExported} messages`);
|
2020-04-22 01:50:04 +00:00
|
|
|
}
|
2020-05-01 04:07:29 +00:00
|
|
|
);
|
2020-04-22 01:50:04 +00:00
|
|
|
}
|
|
|
|
|
2023-05-11 14:40:53 +00:00
|
|
|
const listConferences = () => {
|
|
|
|
initConfigAndDatabases(err => {
|
|
|
|
if (err) {
|
|
|
|
return console.error(err.reason ? err.reason : err.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { getSortedAvailMessageConferences } = require('../../core/message_area');
|
|
|
|
|
|
|
|
const conferences = getSortedAvailMessageConferences(null, { noClient: true });
|
|
|
|
|
|
|
|
for (let conf of conferences) {
|
|
|
|
console.info(`${conf.confTag} - ${conf.conf.name}`);
|
|
|
|
|
|
|
|
if (!argv.areas) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let areaTag of Object.keys(conf.conf.areas)) {
|
|
|
|
console.info(` ${areaTag} - ${conf.conf.areas[areaTag].name}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-05-11 18:07:29 +00:00
|
|
|
const postMessage = () => {
|
|
|
|
const inputFile = argv._[argv._.length - 1];
|
|
|
|
if (argv._.length < 3 || !inputFile || 0 === inputFile.length) {
|
|
|
|
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
|
|
|
}
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
callback => {
|
|
|
|
return initConfigAndDatabases(callback);
|
|
|
|
},
|
|
|
|
callback => {
|
|
|
|
fs.readFile(inputFile, { encoding: 'utf-8' }, (err, jsonData) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
let messageJson;
|
|
|
|
try {
|
|
|
|
messageJson = JSON.parse(jsonData);
|
|
|
|
} catch (e) {
|
|
|
|
return callback(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let f of ['to', 'from', 'subject', 'body', 'areaTag']) {
|
|
|
|
if (!_.isString(messageJson[f])) {
|
|
|
|
return callback(
|
|
|
|
Errors.MissingConfig(
|
|
|
|
`Missing "${f}" field in message JSON`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
messageJson[f] = messageJson[f].trim();
|
|
|
|
if (messageJson[f].length === 0 && f !== 'subject') {
|
|
|
|
return callback(
|
|
|
|
Errors.Invalid(
|
|
|
|
`"${messageJson[f]}" is not a valid value for the "${f}" field`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const { getMessageAreaByTag } = require('../../core/message_area');
|
|
|
|
|
|
|
|
const area = getMessageAreaByTag(messageJson.areaTag);
|
|
|
|
if (!area) {
|
|
|
|
return callback(
|
|
|
|
Errors.DoesNotExist(
|
|
|
|
`Area "${messageJson.areaTag}" does not exist`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { getAddressedToInfo } = require('../../core/mail_util');
|
|
|
|
const Message = require('../../core/message');
|
|
|
|
|
|
|
|
const toInfo = getAddressedToInfo(messageJson.to);
|
|
|
|
const fromInfo = getAddressedToInfo(messageJson.from);
|
|
|
|
|
|
|
|
if (fromInfo.flavor !== Message.AddressFlavor.Local) {
|
|
|
|
return callback(
|
|
|
|
Errors.Invalid(
|
|
|
|
'Only local "from" users are currently supported'
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let modTimestamp;
|
|
|
|
if (_.isString(messageJson.timestamp)) {
|
|
|
|
modTimestamp = moment(messageJson.timestamp);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!modTimestamp || !modTimestamp.isValid()) {
|
|
|
|
modTimestamp = moment();
|
|
|
|
}
|
|
|
|
|
|
|
|
const message = new Message({
|
|
|
|
toUserName: messageJson.to,
|
|
|
|
fromUserName: messageJson.from,
|
|
|
|
subject: messageJson.subject,
|
|
|
|
message: messageJson.body,
|
|
|
|
areaTag: messageJson.areaTag,
|
|
|
|
modTimestamp,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (toInfo.flavor !== Message.AddressFlavor.Local) {
|
|
|
|
message.setExternalFlavor(toInfo.flavor);
|
|
|
|
message.setRemoteToUser(toInfo.remote);
|
|
|
|
|
|
|
|
return callback(null, area, message);
|
|
|
|
}
|
|
|
|
|
|
|
|
const User = require('../../core/user');
|
|
|
|
User.getUserIdAndNameByLookup(
|
|
|
|
message.toUserName,
|
|
|
|
(err, toUserId, toUserName) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(
|
|
|
|
Errors.DoesNotExist(
|
|
|
|
`User "${message.toUserName}" does not exist.`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
message.to = toUserName; // adjust case/etc.
|
|
|
|
message.setLocalToUserId(toUserId);
|
|
|
|
|
|
|
|
return callback(null, area, message);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
(area, message, callback) => {
|
|
|
|
message.persist(err => {
|
|
|
|
if (!err) {
|
|
|
|
console.info(
|
|
|
|
`Message from ${message.fromUserName} to ${message.toUserName}: "${message.subject}" in ${area.name}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
],
|
|
|
|
err => {
|
|
|
|
if (err) {
|
|
|
|
return console.error(err.reason ? err.reason : err.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2018-01-01 00:54:11 +00:00
|
|
|
function handleMessageBaseCommand() {
|
2018-06-22 05:15:04 +00:00
|
|
|
function errUsage() {
|
2022-06-05 20:04:25 +00:00
|
|
|
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
2018-01-01 00:54:11 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (true === argv.help) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return errUsage();
|
|
|
|
}
|
2018-01-01 00:54:11 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
const action = argv._[1];
|
2018-01-01 00:54:11 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
return (
|
|
|
|
{
|
|
|
|
areafix: areaFix,
|
|
|
|
'import-areas': importAreas,
|
|
|
|
'qwk-dump': dumpQWKPacket,
|
|
|
|
'qwk-export': exportQWKPacket,
|
2023-05-11 14:40:53 +00:00
|
|
|
'list-confs': listConferences,
|
2023-05-11 18:07:29 +00:00
|
|
|
post: postMessage,
|
2022-06-05 20:04:25 +00:00
|
|
|
}[action] || errUsage
|
|
|
|
)();
|
|
|
|
}
|