New easier to manage Gopher include/exclude areas with wildcard support

* Deprecate but still support older format for now
* Add new format allowing easier management
This commit is contained in:
Bryan Ashby 2022-08-21 13:27:32 -06:00
parent 8be8c21aa8
commit c8df7f3d6b
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
2 changed files with 277 additions and 166 deletions

View File

@ -1603,6 +1603,7 @@ function FTNMessageScanTossModule() {
const packetOpts = { keepTearAndOrigin: false }; // needed so we can calc message UUID without these; we'll add later const packetOpts = { keepTearAndOrigin: false }; // needed so we can calc message UUID without these; we'll add later
let importStats = { let importStats = {
packetPath,
areaSuccess: {}, // areaTag->count areaSuccess: {}, // areaTag->count
areaFail: {}, // areaTag->count areaFail: {}, // areaTag->count
otherFail: 0, otherFail: 0,
@ -1731,19 +1732,21 @@ function FTNMessageScanTossModule() {
: 0; : 0;
}; };
const finalStats = Object.assign(importStats, { packetPath: packetPath }); const totalFail = makeCount(importStats.areaFail) + importStats.otherFail;
const totalFail = makeCount(finalStats.areaFail) + finalStats.otherFail; const packetFileName = paths.basename(packetPath);
if (err || totalFail > 0) { if (err || totalFail > 0) {
if (err) { if (err) {
Object.assign(finalStats, { error: err.message }); Object.assign(importStats, { error: err.message });
} }
Log.warn(finalStats, `Import completed with ${totalFail} error(s)`); Log.warn(
importStats,
`Packet ${packetFileName} import reported ${totalFail} error(s)`
);
} else { } else {
const totalSuccess = makeCount(finalStats.areaSuccess); const totalSuccess = makeCount(importStats.areaSuccess);
Log.info( Log.info(
finalStats, importStats,
`Import completed with ${totalSuccess} new message(s)` `Packet ${packetFileName} imported with ${totalSuccess} new message(s)`
); );
} }

View File

@ -10,11 +10,13 @@ const {
splitTextAtTerms, splitTextAtTerms,
isAnsi, isAnsi,
stripAnsiControlCodes, stripAnsiControlCodes,
wildcardMatch,
} = require('../../string_util.js'); } = require('../../string_util.js');
const { const {
getMessageConferenceByTag, getMessageConferenceByTag,
getMessageAreaByTag, getMessageAreaByTag,
getMessageListForArea, getMessageListForArea,
getAvailableMessageAreasByConfTag,
} = require('../../message_area.js'); } = require('../../message_area.js');
const { sortAreasOrConfs } = require('../../conf_area_util.js'); const { sortAreasOrConfs } = require('../../conf_area_util.js');
const AnsiPrep = require('../../ansi_prep.js'); const AnsiPrep = require('../../ansi_prep.js');
@ -122,7 +124,7 @@ exports.getModule = class GopherModule extends ServerModule {
if (isNaN(port)) { if (isNaN(port)) {
this.log.warn( this.log.warn(
{ port: config.contentServers.gopher.port, server: ModuleInfo.name }, { port: config.contentServers.gopher.port, server: ModuleInfo.name },
'Invalid port' 'Invalid Gopher port'
); );
return cb( return cb(
Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`) Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)
@ -314,70 +316,212 @@ exports.getModule = class GopherModule extends ServerModule {
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers // /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
// //
if (selectorMatch[3] || selectorMatch[4]) { if (selectorMatch[3] || selectorMatch[4]) {
// message selector - display message
// message // message
//const raw = selectorMatch[4] ? true : false; //const raw = selectorMatch[4] ? true : false;
// :TODO: support 'raw' // :TODO: support 'raw'
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
const confTag = selectorMatch[1].substr(1).split('/')[0]; const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const message = new Message(); return this._displayMessage(msgUuid, confTag, areaTag, cb);
return message.load({ uuid: msgUuid }, err => {
if (err) {
this.log.debug(
{ uuid: msgUuid },
'Attempted access to non-existent message UUID!'
);
return this.notFoundGenerator(selectorMatch, cb);
}
if (
message.areaTag !== areaTag ||
!this.isAreaAndConfExposed(confTag, areaTag)
) {
this.log.warn(
{ areaTag },
'Attempted access to non-exposed conference and/or area!'
);
return this.notFoundGenerator(selectorMatch, cb);
}
if (Message.isPrivateAreaTag(areaTag)) {
this.log.warn(
{ areaTag },
'Attempted access to message in private area!'
);
return this.notFoundGenerator(selectorMatch, cb);
}
this.prepareMessageBody(message.message, msgBody => {
const response = `${'-'.repeat(70)}
To : ${message.toUserName}
From : ${message.fromUserName}
When : ${moment(message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')}
Subject: ${message.subject}
ID : ${message.messageUuid} (${message.messageId})
${'-'.repeat(70)}
${msgBody}
`;
return cb(response);
});
});
} else if (selectorMatch[2]) { } else if (selectorMatch[2]) {
// list messages in area // conf/area selector -- list messages in area
const confTag = selectorMatch[1].substr(1).split('/')[0]; const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const area = getMessageAreaByTag(areaTag); const area = getMessageAreaByTag(areaTag);
return this._listMessagesInArea(confTag, areaTag, area, cb);
} else if (selectorMatch[1]) {
// message conference selector -- list areas in this conference
const confTag = selectorMatch[1].replace(/\r\n|\//g, '');
return this._listExposedMessageConferenceAreas(confTag, cb);
} else {
// message area base selector -- list exposed message conferences
return this._listExposedMessageConferences(cb);
}
}
_makeAvailableMessageConferencesResponse(messageConferences, cb) {
sortAreasOrConfs(messageConferences);
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, ''),
...messageConferences.map(conf =>
this.makeItem(
ItemTypes.SubMenu,
`${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`,
`/msgarea/${conf.confTag}`
)
),
].join('');
return cb(response);
}
_exposedMessageConferenceTags(obj) {
return Object.keys(obj || {})
.map(confTag =>
Object.assign({ confTag }, getMessageConferenceByTag(confTag))
)
.filter(conf => conf); // remove any baddies
}
_noExposedMessageConferences(cb) {
return cb(
this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')
);
}
// newer format
_listExposedMessageConferences(cb) {
let exposedConfs = _.get(Config(), 'contentServers.gopher.exposedConfAreas');
if (!_.isObject(exposedConfs)) {
return this._listExposedMessageConferencesLegacy(cb);
}
exposedConfs = this._exposedMessageConferenceTags(exposedConfs);
if (0 === exposedConfs.length) {
return this._noExposedMessageConferences(cb);
}
return this._makeAvailableMessageConferencesResponse(exposedConfs, cb);
}
// older deprecated format
_listExposedMessageConferencesLegacy(cb) {
const exposedConfs = this._exposedMessageConferenceTags(
_.get(Config(), 'contentServers.gopher.messageConferences')
);
if (0 === exposedConfs.length) {
return this._noExposedMessageConferences(cb);
}
return this._makeAvailableMessageConferencesResponse(exposedConfs, cb);
}
_makeAvailableMessageAreasResponse(exposedConf, exposedAreas, cb) {
// ensure nothing private is present
exposedAreas = exposedAreas.filter(
area => area && !Message.isPrivateAreaTag(area.areaTag)
);
if (0 === exposedAreas.length) {
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available'));
}
sortAreasOrConfs(exposedAreas);
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Message areas in ${exposedConf.name}`),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...exposedAreas.map(area =>
this.makeItem(
ItemTypes.SubMenu,
`${area.name} ${area.desc ? '- ' + area.desc : ''}`,
`/msgarea/${exposedConf.confTag}/${area.areaTag}`
)
),
].join('');
return cb(response);
}
_listExposedMessageConferenceAreas(confTag, cb) {
//
// New system -- exposedConfAreas:
// We have a required array |include| of area tags that may
// contain wildcards and a _optional_ |exclude| array that
// overrides any includes
//
// Deprecated -- messageConferences:
// The key should point to an array of area tags
//
const sysConfig = Config();
const getConfConfig = () => {
let config = _.get(sysConfig, [
'contentServers',
'gopher',
'exposedConfAreas',
confTag,
]);
if (config) {
return [config, false]; // new
}
return [
_.get(sysConfig, [
'contentServers',
'gopher',
'messageConferences',
confTag,
]),
true,
];
};
const [confConfig, isLegacy] = getConfConfig();
const messageConference = getMessageConferenceByTag(confTag); // we need the actual conf!
if (!messageConference) {
return this.notFoundGenerator(selectorMatch, cb);
}
let areas;
if (isLegacy) {
areas = (confConfig || {}).map(areaTag =>
Object.assign({ areaTag }, getMessageAreaByTag(areaTag))
);
} else {
// new system is more complex here, but nicer for the +op to manage
areas = getAvailableMessageAreasByConfTag(confTag);
if (!Array.isArray(confConfig.include)) {
return cb(
this.makeItem(ItemTypes.InfoMessage, 'No message areas available')
);
}
// filters |areas| down to what |includes| matches
areas = _.filter(areas, (area, areaTag) => {
for (let needle of confConfig.include) {
if (wildcardMatch(areaTag, needle)) {
area.areaTag = areaTag;
return true;
}
}
return false;
});
// now filter out any excludes, if present
if (Array.isArray(confConfig.exclude)) {
areas = _.filter(areas, area => {
for (let needle of confConfig.exclude) {
if (wildcardMatch(area.areaTag, needle)) {
return false;
}
}
return true;
});
}
}
return this._makeAvailableMessageAreasResponse(messageConference, areas, cb);
}
_listMessagesInArea(confTag, areaTag, area, cb) {
if (Message.isPrivateAreaTag(areaTag)) { if (Message.isPrivateAreaTag(areaTag)) {
this.log.warn({ areaTag }, 'Attempted access to private area!'); this.log.warn({ areaTag }, `Gopher attempted access to private "${areaTag}"`);
return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private')); return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private'));
} }
if (!area || !this.isAreaAndConfExposed(confTag, areaTag)) { if (!area || !this.isAreaAndConfExposed(confTag, areaTag)) {
this.log.warn( this.log.warn(
{ confTag, areaTag }, { confTag, areaTag },
'Attempted access to non-exposed conference and/or area!' `Gopher attempted access to non-exposed "${confTag}"/"${areaTag}"`
); );
return this.notFoundGenerator(selectorMatch, cb); return this.notFoundGenerator(selectorMatch, cb);
} }
@ -409,87 +553,51 @@ ${msgBody}
return cb(response); return cb(response);
}); });
} else if (selectorMatch[1]) { }
// list areas in conf
const sysConfig = Config(); _displayMessage(msgUuid, confTag, areaTag, cb) {
const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); const message = new Message();
const conf =
_.get(sysConfig, [ return message.load({ uuid: msgUuid }, err => {
'contentServers', if (err) {
'gopher', this.log.debug(
'messageConferences', { uuid: msgUuid },
confTag, 'Attempted access to non-existent message UUID!'
]) && getMessageConferenceByTag(confTag); );
if (!conf) {
return this.notFoundGenerator(selectorMatch, cb); return this.notFoundGenerator(selectorMatch, cb);
} }
const areas = _.get( if (
sysConfig, message.areaTag !== areaTag ||
['contentServers', 'gopher', 'messageConferences', confTag], !this.isAreaAndConfExposed(confTag, areaTag)
{} ) {
) this.log.warn(
.map(areaTag => Object.assign({ areaTag }, getMessageAreaByTag(areaTag))) { areaTag },
.filter(area => area && !Message.isPrivateAreaTag(area.areaTag)); `Gopher attempted access to non-exposed "${confTag}"/"${areaTag}"`
if (0 === areas.length) {
return cb(
this.makeItem(ItemTypes.InfoMessage, 'No message areas available')
); );
return this.notFoundGenerator(selectorMatch, cb);
} }
sortAreasOrConfs(areas); if (Message.isPrivateAreaTag(areaTag)) {
this.log.warn(
const response = [ { areaTag },
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), `Gopher attempted access to message in private "${areaTag}"`
this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...areas.map(area =>
this.makeItem(
ItemTypes.SubMenu,
`${area.name} ${area.desc ? '- ' + area.desc : ''}`,
`/msgarea/${confTag}/${area.areaTag}`
)
),
].join('');
return cb(response);
} else {
// message area base (list confs)
const confs = Object.keys(
_.get(Config(), 'contentServers.gopher.messageConferences', {})
)
.map(confTag =>
Object.assign({ confTag }, getMessageConferenceByTag(confTag))
)
.filter(conf => conf); // remove any baddies
if (0 === confs.length) {
return cb(
this.makeItem(
ItemTypes.InfoMessage,
'No message conferences available'
)
); );
return this.notFoundGenerator(selectorMatch, cb);
} }
sortAreasOrConfs(confs); this.prepareMessageBody(message.message, msgBody => {
const response = `${'-'.repeat(70)}
const response = [ To : ${message.toUserName}
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), From : ${message.fromUserName}
this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'), When : ${moment(message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')}
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), Subject: ${message.subject}
this.makeItem(ItemTypes.InfoMessage, ''), ID : ${message.messageUuid} (${message.messageId})
...confs.map(conf => ${'-'.repeat(70)}
this.makeItem( ${msgBody}
ItemTypes.SubMenu, `;
`${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`,
`/msgarea/${conf.confTag}`
)
),
].join('');
return cb(response); return cb(response);
} });
});
} }
}; };