Merge branch '459-activitypub-integration' of ssh://numinibsd/git/base/enigma-bbs into 459-activitypub-integration

This commit is contained in:
Bryan Ashby 2023-08-24 12:49:28 -06:00
commit 7961fa48db
33 changed files with 1064 additions and 708 deletions

View File

@ -22,13 +22,27 @@ Refer to [Upgrading](./docs/_docs/admin/upgrading.md) for details around this pr
## 0.0.13-beta to 0.0.14-beta ## 0.0.13-beta to 0.0.14-beta
* A new ActivityPub menu template has been created. Upgrades will **not** have this file present so you will need to copy the template to your `config/menus` directory and rename it appropriately (it must match the `include` statement in your main `menu.hjson` file). Example: * A new ActivityPub menu template has been created. Upgrades will **not** have this file present so you will need to copy the template to your `config/menus` directory and rename it appropriately (it must match the `include` statement in your main `menu.hjson` file). Example:
```bash ```bash
cp ./misc/menu_templates/activitypub.in.hjson ./config/menus/my_board_name-activitypub.hjson` cp ./misc/menu_templates/activitypub.in.hjson ./config/menus/my_board_name-activitypub.hjson`
``` ```
This will expose the default ActivityPub setup. Enabling ActivityPub functionality requires the web server enabled and ActivityPub itself enabled in your `config.hjson`. This will expose the default ActivityPub setup. Enabling ActivityPub functionality requires the web server enabled and ActivityPub itself enabled in your `config.hjson`. See [Configuration Files Include Statements](./docs/_docs/configuration/config-files.md#includes) for more information on using `include`.
> :information_source: See [Configuration Files Include Statements](./docs/_docs/configuration/config-files.md#includes) for more information on using `include`. * ⚠ The menu flag `noHistory` has been revamped to work as expected. Some menu entires now need this flag. Look for any "NoResults" entries and remove `menuFlags`. For example, here is the (updated) default `fileBaseListEntriesNoResults` menu:
```hjson
fileBaseListEntriesNoResults: {
desc: Browsing Files
art: FBNORES
config: {
pause: true
// no menuFlags here
}
}
```
See also: [Menu Modules](./docs/_docs/modding/menu-module.md).
## 0.0.12-beta to 0.0.13-beta ## 0.0.12-beta to 0.0.13-beta

View File

@ -16,6 +16,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* CombatNet has shut down, so the module (`combatnet.js`) has been removed. * CombatNet has shut down, so the module (`combatnet.js`) has been removed.
* New `NewUserPrePersist` system event available to developers to 'hook' account creation and add their own properties/etc. * New `NewUserPrePersist` system event available to developers to 'hook' account creation and add their own properties/etc.
* The signature for `viewValidationListener`'s callback has changed: It is now `(err, newFocusId)`. To ignore a validation error, implementors can simply call the callback with a `null` error, else they should forward it on. * The signature for `viewValidationListener`'s callback has changed: It is now `(err, newFocusId)`. To ignore a validation error, implementors can simply call the callback with a `null` error, else they should forward it on.
* The Menu Flag `popParent` has been removed and `noHistory` has been updated to work as expected. In general things should "Just Work", but do see [UPGRADE](UPGRADE.md)!
## 0.0.13-beta ## 0.0.13-beta
* **Note for contributors**: ENiGMA has switched to [Prettier](https://prettier.io) for formatting/style. Please see [CONTRIBUTING](CONTRIBUTING.md) and the Prettier website for more information. * **Note for contributors**: ENiGMA has switched to [Prettier](https://prettier.io) for formatting/style. Please see [CONTRIBUTING](CONTRIBUTING.md) and the Prettier website for more information.

View File

@ -597,7 +597,7 @@
} }
} }
messageBaseSearchMessageList: { messageBaseSearchResultsMessageList: {
config: { config: {
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
// Fri Sep 25th // Fri Sep 25th
@ -1543,4 +1543,4 @@
} }
} }
} }
} }

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const { MenuModule, MenuFlags } = require('./menu_module.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
@ -75,6 +75,8 @@ exports.getModule = class FileAreaList extends MenuModule {
this.fileList = _.get(options, 'extraArgs.fileList'); this.fileList = _.get(options, 'extraArgs.fileList');
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
this.setMergedFlag(MenuFlags.NoHistory);
if (this.fileList) { if (this.fileList) {
// we'll need to adjust position as well! // we'll need to adjust position as well!
this.fileListPosition = 0; this.fileListPosition = 0;

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const { MenuModule, MenuFlags } = require('./menu_module.js');
const { getSortedAvailableFileAreas } = require('./file_base_area.js'); const { getSortedAvailableFileAreas } = require('./file_base_area.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
@ -24,6 +24,8 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.menuMethods = { this.menuMethods = {
selectArea: (formData, extraArgs, cb) => { selectArea: (formData, extraArgs, cb) => {
const filterCriteria = { const filterCriteria = {
@ -34,7 +36,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
extraArgs: { extraArgs: {
filterCriteria: filterCriteria, filterCriteria: filterCriteria,
}, },
menuFlags: ['popParent', 'mergeFlags'], menuFlags: [MenuFlags.NoHistory],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -2,10 +2,8 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const { MenuModule, MenuFlags } = require('./menu_module.js');
const ViewController = require('./view_controller.js').ViewController;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
@ -38,6 +36,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
if (_.has(options, 'lastMenuResult.sentFileIds')) { if (_.has(options, 'lastMenuResult.sentFileIds')) {

View File

@ -121,7 +121,6 @@ exports.getModule = class FileBaseSearch extends MenuModule {
extraArgs: { extraArgs: {
filterCriteria: filterCriteria, filterCriteria: filterCriteria,
}, },
menuFlags: ['popParent'],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule, MenuFlags } = require('./menu_module.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const FileArea = require('./file_base_area.js'); const FileArea = require('./file_base_area.js');
const { renderSubstr } = require('./string_util.js'); const { renderSubstr } = require('./string_util.js');
@ -65,6 +65,9 @@ const MciViewIds = {
exports.getModule = class FileBaseListExport extends MenuModule { exports.getModule = class FileBaseListExport extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.config = Object.assign( this.config = Object.assign(
{}, {},
_.get(options, 'menuConfig.config'), _.get(options, 'menuConfig.config'),

View File

@ -2,10 +2,8 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const { MenuModule, MenuFlags } = require('./menu_module.js');
const ViewController = require('./view_controller.js').ViewController;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
@ -14,7 +12,6 @@ const Config = require('./config.js').get;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
@ -40,6 +37,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
this.menuMethods = { this.menuMethods = {

View File

@ -21,6 +21,7 @@ const {
messageInfoFromAddressedToInfo, messageInfoFromAddressedToInfo,
setExternalAddressedToInfo, setExternalAddressedToInfo,
copyExternalAddressedToInfo, copyExternalAddressedToInfo,
getReplyToMessagePrefix,
} = require('./mail_util.js'); } = require('./mail_util.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
@ -28,10 +29,10 @@ const SysProps = require('./system_property.js');
const FileArea = require('./file_base_area.js'); const FileArea = require('./file_base_area.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const EngiAssert = require('./enigma_assert.js');
// deps // deps
const async = require('async'); const async = require('async');
const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const fse = require('fs-extra'); const fse = require('fs-extra');
@ -123,7 +124,7 @@ exports.FullScreenEditorModule =
this.editorMode = config.editorMode; this.editorMode = config.editorMode;
if (config.messageAreaTag) { if (config.messageAreaTag) {
// :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs // :TODO: switch to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs
this.messageAreaTag = config.messageAreaTag; this.messageAreaTag = config.messageAreaTag;
} }
@ -251,7 +252,7 @@ exports.FullScreenEditorModule =
if (self.newQuoteBlock) { if (self.newQuoteBlock) {
self.newQuoteBlock = false; self.newQuoteBlock = false;
// :TODO: If replying to ANSI, add a blank sepration line here // :TODO: If replying to ANSI, add a blank separation line here
quoteMsgView.addText(self.getQuoteByHeader()); quoteMsgView.addText(self.getQuoteByHeader());
} }
@ -480,106 +481,108 @@ exports.FullScreenEditorModule =
this.message = message; this.message = message;
this.updateLastReadId(() => { this.updateLastReadId(() => {
if (this.isReady) { if (!this.isReady) {
this.initHeaderViewMode(); return;
this.initFooterViewMode(); }
const bodyMessageView = this.viewControllers.body.getView( this.initHeaderViewMode();
MciViewIds.body.message this.initFooterViewMode();
);
let msg = this.message.message;
if (bodyMessageView && _.has(this, 'message.message')) { const bodyMessageView = this.viewControllers.body.getView(
MciViewIds.body.message
);
let msg = this.message.message;
if (bodyMessageView && _.has(this, 'message.message')) {
//
// We handle ANSI messages differently than standard messages -- this is required as
// we don't want to do things like word wrap ANSI, but instead, trust that it's formatted
// how the author wanted it
//
if (isAnsi(msg)) {
// //
// We handle ANSI messages differently than standard messages -- this is required as // Find tearline - we want to color it differently.
// we don't want to do things like word wrap ANSI, but instead, trust that it's formatted
// how the author wanted it
// //
if (isAnsi(msg)) { const tearLinePos = Message.getTearLinePosition(msg);
//
// Find tearline - we want to color it differently.
//
const tearLinePos = Message.getTearLinePosition(msg);
if (tearLinePos > -1) { if (tearLinePos > -1) {
msg = insert( msg = insert(
msg, msg,
tearLinePos, tearLinePos,
bodyMessageView.getTextSgrPrefix() bodyMessageView.getTextSgrPrefix()
); );
}
bodyMessageView.setAnsi(
msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF
{
prepped: false,
forceLineTerm: true,
} }
);
} else {
msg = stripAnsiControlCodes(msg); // start clean
bodyMessageView.setAnsi( const styleToArray = (style, len) => {
msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF if (!Array.isArray(style)) {
{ style = [style];
prepped: false, }
forceLineTerm: true, while (style.length < len) {
style.push(style[0]);
}
return style;
};
//
// In *View* mode, if enabled, do a little prep work so we can stylize:
// - Quote indicators
// - Tear lines
// - Origins
//
if (this.menuConfig.config.quoteStyleLevel1) {
// can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT
// Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed
const styleL1 = styleToArray(
this.menuConfig.config.quoteStyleLevel1,
3
);
const QuoteRegex =
/^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm;
msg = msg.replace(
QuoteRegex,
(m, spc1, initials, spc2, text) => {
return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`;
} }
); );
} else {
msg = stripAnsiControlCodes(msg); // start clean
const styleToArray = (style, len) => {
if (!Array.isArray(style)) {
style = [style];
}
while (style.length < len) {
style.push(style[0]);
}
return style;
};
//
// In *View* mode, if enabled, do a little prep work so we can stylize:
// - Quote indicators
// - Tear lines
// - Origins
//
if (this.menuConfig.config.quoteStyleLevel1) {
// can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT
// Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed
const styleL1 = styleToArray(
this.menuConfig.config.quoteStyleLevel1,
3
);
const QuoteRegex =
/^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm;
msg = msg.replace(
QuoteRegex,
(m, spc1, initials, spc2, text) => {
return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`;
}
);
}
if (this.menuConfig.config.tearLineStyle) {
// '---' and TEXT
const style = styleToArray(
this.menuConfig.config.tearLineStyle,
2
);
const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m;
msg = msg.replace(TearLineRegex, (m, text) => {
return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`;
});
}
if (this.menuConfig.config.originStyle) {
const style = styleToArray(
this.menuConfig.config.originStyle,
3
);
const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m;
msg = msg.replace(OriginRegex, (m, spc, text) => {
return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`;
});
}
bodyMessageView.setText(controlCodesToAnsi(msg));
} }
if (this.menuConfig.config.tearLineStyle) {
// '---' and TEXT
const style = styleToArray(
this.menuConfig.config.tearLineStyle,
2
);
const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m;
msg = msg.replace(TearLineRegex, (m, text) => {
return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`;
});
}
if (this.menuConfig.config.originStyle) {
const style = styleToArray(
this.menuConfig.config.originStyle,
3
);
const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m;
msg = msg.replace(OriginRegex, (m, spc, text) => {
return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`;
});
}
bodyMessageView.setText(controlCodesToAnsi(msg));
} }
} }
}); });
@ -849,7 +852,7 @@ exports.FullScreenEditorModule =
const self = this; const self = this;
var art = self.menuConfig.config.art; var art = self.menuConfig.config.art;
assert(_.isObject(art)); EngiAssert(_.isObject(art));
async.waterfall( async.waterfall(
[ [
@ -1161,7 +1164,7 @@ exports.FullScreenEditorModule =
} }
initHeaderReplyEditMode() { initHeaderReplyEditMode() {
assert(_.isObject(this.replyToMessage)); EngiAssert(_.isObject(this.replyToMessage));
this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName);
@ -1177,6 +1180,20 @@ exports.FullScreenEditorModule =
this.setHeaderText(MciViewIds.header.subject, newSubj); this.setHeaderText(MciViewIds.header.subject, newSubj);
} }
initBodyReplyEditMode() {
EngiAssert(_.isObject(this.replyToMessage));
const bodyMessageView = this.viewControllers.body.getView(
MciViewIds.body.message
);
const messagePrefix = getReplyToMessagePrefix(
this.replyToMessage.fromUserName
);
bodyMessageView.setText(messagePrefix);
}
initFooterViewMode() { initFooterViewMode() {
this.setViewText( this.setViewText(
'footerView', 'footerView',
@ -1450,11 +1467,18 @@ exports.FullScreenEditorModule =
switchToBody() { switchToBody() {
const to = this.getView('header', MciViewIds.header.to).getData(); const to = this.getView('header', MciViewIds.header.to).getData();
const msgInfo = messageInfoFromAddressedToInfo(getAddressedToInfo(to)); const msgInfo = messageInfoFromAddressedToInfo(getAddressedToInfo(to));
const bodyView = this.getView('body', MciViewIds.body.message);
if (msgInfo.maxMessageLength > 0) { if (msgInfo.maxMessageLength > 0) {
const bodyView = this.getView('body', MciViewIds.body.message);
bodyView.maxLength = msgInfo.maxMessageLength; bodyView.maxLength = msgInfo.maxMessageLength;
} }
// first pass through, init body (we may need header values set)
const bodyText = bodyView.getData();
if (!bodyText && this.isReply()) {
this.initBodyReplyEditMode();
}
this.viewControllers.header.setFocus(false); this.viewControllers.header.setFocus(false);
this.viewControllers.body.switchFocus(1); this.viewControllers.body.switchFocus(1);

View File

@ -2,7 +2,8 @@ const { Errors } = require('./enig_error.js');
// deps // deps
const { isString, isObject, truncate } = require('lodash'); const { isString, isObject, truncate } = require('lodash');
const { https } = require('follow-redirects'); const httpsNoRedirects = require('node:https');
const { https: httpsWithRedirects } = require('follow-redirects');
const httpSignature = require('http-signature'); const httpSignature = require('http-signature');
const crypto = require('crypto'); const crypto = require('crypto');
@ -78,6 +79,13 @@ function _makeRequest(url, options, cb) {
} }
}; };
let https;
if (options.method === 'POST' || options.sign) {
https = httpsNoRedirects;
} else {
https = httpsWithRedirects;
}
const req = https.request(url, options, res => { const req = https.request(url, options, res => {
let body = []; let body = [];
res.on('data', d => { res.on('data', d => {

View File

@ -15,6 +15,7 @@ exports.setExternalAddressedToInfo = setExternalAddressedToInfo;
exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo; exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo;
exports.messageInfoFromAddressedToInfo = messageInfoFromAddressedToInfo; exports.messageInfoFromAddressedToInfo = messageInfoFromAddressedToInfo;
exports.getQuotePrefixFromName = getQuotePrefixFromName; exports.getQuotePrefixFromName = getQuotePrefixFromName;
exports.getReplyToMessagePrefix = getReplyToMessagePrefix;
const EMAIL_REGEX = const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[?[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}]?)|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[?[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}]?)|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@ -32,6 +33,11 @@ const EMAIL_REGEX =
foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' } foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' }
Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } Bar <baz@foobar.net> { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' }
@JoeUser@some.host.com { name : 'JoeUser', flavor : 'activitypub', remote 'JoeUser@some.host.com' } @JoeUser@some.host.com { name : 'JoeUser', flavor : 'activitypub', remote 'JoeUser@some.host.com' }
Fields:
- name : user/display name
- flavor : remote flavor - FTN/etc.
- remote : Address in remote format, if applicable
*/ */
function getAddressedToInfo(input) { function getAddressedToInfo(input) {
input = input.trim(); input = input.trim();
@ -41,17 +47,26 @@ function getAddressedToInfo(input) {
if (firstAtPos < 0) { if (firstAtPos < 0) {
let addr = Address.fromString(input); let addr = Address.fromString(input);
if (Address.isValidAddress(addr)) { if (Address.isValidAddress(addr)) {
return { flavor: MessageConst.AddressFlavor.FTN, remote: input }; return {
flavor: MessageConst.AddressFlavor.FTN,
remote: input,
};
} }
const lessThanPos = input.indexOf('<'); const lessThanPos = input.indexOf('<');
if (lessThanPos < 0) { if (lessThanPos < 0) {
return { name: input, flavor: MessageConst.AddressFlavor.Local }; return {
name: input,
flavor: MessageConst.AddressFlavor.Local,
};
} }
const greaterThanPos = input.indexOf('>'); const greaterThanPos = input.indexOf('>');
if (greaterThanPos < lessThanPos) { if (greaterThanPos < lessThanPos) {
return { name: input, flavor: MessageConst.AddressFlavor.Local }; return {
name: input,
flavor: MessageConst.AddressFlavor.Local,
};
} }
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
@ -93,7 +108,10 @@ function getAddressedToInfo(input) {
}; };
} }
return { name: input, flavor: MessageConst.AddressFlavor.Local }; return {
name: input,
flavor: MessageConst.AddressFlavor.Local,
};
} }
let m = input.match(EMAIL_REGEX); let m = input.match(EMAIL_REGEX);
@ -107,7 +125,10 @@ function getAddressedToInfo(input) {
let addr = Address.fromString(input); // 5D? let addr = Address.fromString(input); // 5D?
if (Address.isValidAddress(addr)) { if (Address.isValidAddress(addr)) {
return { flavor: MessageConst.AddressFlavor.FTN, remote: addr.toString() }; return {
flavor: MessageConst.AddressFlavor.FTN,
remote: addr.toString(),
};
} }
addr = Address.fromString(input.slice(firstAtPos + 1).trim()); addr = Address.fromString(input.slice(firstAtPos + 1).trim());
@ -172,6 +193,17 @@ function messageInfoFromAddressedToInfo(addressInfo) {
} }
function getQuotePrefixFromName(name) { function getQuotePrefixFromName(name) {
const addrInfo = getAddressedToInfo(name); const addressInfo = getAddressedToInfo(name);
return getQuotePrefix(addrInfo.name || name); return getQuotePrefix(addressInfo.name || name);
}
function getReplyToMessagePrefix(name) {
const addressInfo = getAddressedToInfo(name);
// currently only ActivityPub
if (addressInfo.flavor === MessageConst.AddressFlavor.ActivityPub) {
return `@${addressInfo.name} `;
}
return '';
} }

View File

@ -20,6 +20,22 @@ const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
const iconvDecode = require('iconv-lite').decode; const iconvDecode = require('iconv-lite').decode;
const MenuFlags = {
// When leaving this menu to load/chain to another, remove this
// menu from history. In other words, the fallback from
// the next menu would *not* be this one, but the previous.
NoHistory: 'noHistory',
// Generally used in code only: Request that any flags from menu.hjson
// are merged in to the total set of flags vs overriding the default.
MergeFlags: 'mergeFlags',
// Forward this menu's 'extraArgs' to the next.
ForwardArgs: 'forwardArgs',
};
exports.MenuFlags = MenuFlags;
exports.MenuModule = class MenuModule extends PluginModule { exports.MenuModule = class MenuModule extends PluginModule {
constructor(options) { constructor(options) {
super(options); super(options);
@ -48,6 +64,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
}); });
} }
setMergedFlag(flag) {
this.menuConfig.config.menuFlags.push(flag);
this.menuConfig.config.menuFlags = [
...new Set([...this.menuConfig.config.menuFlags, MenuFlags.MergeFlags]),
];
}
static get InterruptTypes() { static get InterruptTypes() {
return { return {
Never: 'never', Never: 'never',

View File

@ -5,12 +5,12 @@
const loadMenu = require('./menu_util.js').loadMenu; const loadMenu = require('./menu_util.js').loadMenu;
const { Errors, ErrorReasons } = require('./enig_error.js'); const { Errors, ErrorReasons } = require('./enig_error.js');
const { getResolvedSpec } = require('./menu_util.js'); const { getResolvedSpec } = require('./menu_util.js');
const { MenuFlags } = require('./menu_module.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const bunyan = require('bunyan');
// :TODO: Stack is backwards.... top should be most recent! :)
module.exports = class MenuStack { module.exports = class MenuStack {
constructor(client) { constructor(client) {
@ -27,19 +27,11 @@ module.exports = class MenuStack {
} }
peekPrev() { peekPrev() {
if (this.stackSize > 1) { return this.stack[this.stack.length - 2];
return this.stack[this.stack.length - 2];
}
} }
top() { top() {
if (this.stackSize > 0) { return this.stack[this.stack.length - 1];
return this.stack[this.stack.length - 1];
}
}
get stackSize() {
return this.stack.length;
} }
get currentModule() { get currentModule() {
@ -81,19 +73,15 @@ module.exports = class MenuStack {
prev(cb) { prev(cb) {
const menuResult = this.top().instance.getMenuResult(); const menuResult = this.top().instance.getMenuResult();
const currentModuleInfo = this.top();
// :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
const previousModuleInfo = this.pop(); // get previous const previousModuleInfo = this.pop(); // get previous; we'll re-create a instance
if (previousModuleInfo) { if (previousModuleInfo) {
const opts = { const opts = {
extraArgs: previousModuleInfo.extraArgs, extraArgs: previousModuleInfo.extraArgs,
savedState: previousModuleInfo.savedState, savedState: previousModuleInfo.savedState,
lastMenuResult: menuResult, lastMenuResult: menuResult,
currentModuleInfo,
}; };
return this.goto(previousModuleInfo.name, opts, cb); return this.goto(previousModuleInfo.name, opts, cb);
@ -111,8 +99,7 @@ module.exports = class MenuStack {
} }
options = options || {}; options = options || {};
const currentModuleInfo = options.currentModuleInfo || this.top(); const currentModuleInfo = this.top();
const self = this;
if (currentModuleInfo && name === currentModuleInfo.name) { if (currentModuleInfo && name === currentModuleInfo.name) {
if (cb) { if (cb) {
@ -128,10 +115,13 @@ module.exports = class MenuStack {
const loadOpts = { const loadOpts = {
name: name, name: name,
client: self.client, client: this.client,
}; };
if (currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { if (
currentModuleInfo &&
currentModuleInfo.menuFlags.includes(MenuFlags.ForwardArgs)
) {
loadOpts.extraArgs = currentModuleInfo.extraArgs; loadOpts.extraArgs = currentModuleInfo.extraArgs;
} else { } else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
@ -140,11 +130,10 @@ module.exports = class MenuStack {
loadMenu(loadOpts, (err, modInst) => { loadMenu(loadOpts, (err, modInst) => {
if (err) { if (err) {
// :TODO: probably should just require a cb... const errCb = cb || this.client.defaultHandlerMissingMod();
const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err); errCb(err);
} else { } else {
self.client.log.debug({ menuName: name }, 'Goto menu module'); this.client.log.debug({ menuName: name }, 'Goto menu module');
if (!this.client.acs.hasMenuModuleAccess(modInst)) { if (!this.client.acs.hasMenuModuleAccess(modInst)) {
if (cb) { if (cb) {
@ -153,22 +142,6 @@ module.exports = class MenuStack {
return; return;
} }
//
// Handle deprecated 'options' block by merging to config and warning user.
// :TODO: Remove in 0.0.10+
//
if (modInst.menuConfig.options) {
self.client.log.warn(
{ options: modInst.menuConfig.options },
'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions'
);
Object.assign(
modInst.menuConfig.config || {},
modInst.menuConfig.options
);
delete modInst.menuConfig.options;
}
// //
// If menuFlags were supplied in menu.hjson, they should win over // If menuFlags were supplied in menu.hjson, they should win over
// anything supplied in code. // anything supplied in code.
@ -182,9 +155,9 @@ module.exports = class MenuStack {
// in code we can ask to merge in // in code we can ask to merge in
if ( if (
Array.isArray(options.menuFlags) && Array.isArray(options.menuFlags) &&
options.menuFlags.includes('mergeFlags') options.menuFlags.includes(MenuFlags.MergeFlags)
) { ) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); menuFlags = [...new Set(options.menuFlags)]; // make unique
} }
} }
@ -195,16 +168,12 @@ module.exports = class MenuStack {
currentModuleInfo.instance.leave(); currentModuleInfo.instance.leave();
if (currentModuleInfo.menuFlags.includes('noHistory')) { if (currentModuleInfo.menuFlags.includes(MenuFlags.NoHistory)) {
this.pop(); this.pop();
} }
if (menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current
}
} }
self.push({ this.push({
name: name, name: name,
instance: modInst, instance: modInst,
extraArgs: loadOpts.extraArgs, extraArgs: loadOpts.extraArgs,
@ -216,17 +185,19 @@ module.exports = class MenuStack {
modInst.restoreSavedState(options.savedState); modInst.restoreSavedState(options.savedState);
} }
const stackEntries = self.stack.map(stackEntry => { if (this.client.log.level() <= bunyan.TRACE) {
let name = stackEntry.name; const stackEntries = this.stack.map(stackEntry => {
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) { let name = stackEntry.name;
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join( if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
', ' name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
)})`; ', '
} )})`;
return name; }
}); return name;
});
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack'); this.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
}
modInst.enter(); modInst.enter();

View File

@ -113,7 +113,6 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
const returnNoResults = () => { const returnNoResults = () => {
return this.gotoMenu( return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags: ['popParent'] },
cb cb
); );
}; };
@ -160,7 +159,6 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
messageList, messageList,
noUpdateLastReadId: true, noUpdateLastReadId: true,
}, },
menuFlags: ['popParent'],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule, MenuFlags } = require('./menu_module.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
@ -29,6 +29,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
// always include noHistory flag
this.setMergedFlag(MenuFlags.NoHistory);
this.initList(); this.initList();
this.menuMethods = { this.menuMethods = {
@ -49,7 +52,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
extraArgs: { extraArgs: {
areaTag: area.areaTag, areaTag: area.areaTag,
}, },
menuFlags: ['popParent', 'noHistory'], menuFlags: [MenuFlags.NoHistory],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule, MenuFlags } = require('./menu_module.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
@ -26,6 +26,9 @@ exports.getModule = class MessageConfListModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
// always include noHistory flag
this.setMergedFlag(MenuFlags.NoHistory);
this.initList(); this.initList();
this.menuMethods = { this.menuMethods = {
@ -49,7 +52,7 @@ exports.getModule = class MessageConfListModule extends MenuModule {
extraArgs: { extraArgs: {
confTag: conf.confTag, confTag: conf.confTag,
}, },
menuFlags: ['popParent', 'noHistory'], menuFlags: [MenuFlags.NoHistory],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -288,18 +288,20 @@ function MultiLineEditTextView(options) {
this.getOutputText = function (startIndex, endIndex, eolMarker, options) { this.getOutputText = function (startIndex, endIndex, eolMarker, options) {
const lines = self.getTextLines(startIndex, endIndex); const lines = self.getTextLines(startIndex, endIndex);
let text = '';
const re = new RegExp('\\t{1,' + self.tabWidth + '}', 'g'); const re = new RegExp('\\t{1,' + self.tabWidth + '}', 'g');
lines.forEach(line => { return lines
text += line.text.replace(re, '\t'); .map((line, lineIndex) => {
let text = line.text.replace(re, '\t');
if (options.forceLineTerms || (eolMarker && line.eol)) { if (
text += eolMarker; options.forceLineTerms ||
} (eolMarker && line.eol && lineIndex < lines.length - 1)
}); ) {
text += eolMarker;
return text; }
return text;
})
.join();
}; };
this.getContiguousText = function (startIndex, endIndex, includeEol) { this.getContiguousText = function (startIndex, endIndex, includeEol) {

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const { MenuModule, MenuFlags } = require('./menu_module');
const Message = require('./message.js'); const Message = require('./message.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const { filterMessageListByReadACS } = require('./message_area.js'); const { filterMessageListByReadACS } = require('./message_area.js');
@ -16,6 +16,7 @@ exports.moduleInfo = {
exports.getModule = class MyMessagesModule extends MenuModule { exports.getModule = class MyMessagesModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.setMergedFlag(MenuFlags.NoHistory);
} }
initSequence() { initSequence() {
@ -48,8 +49,7 @@ exports.getModule = class MyMessagesModule extends MenuModule {
finishedLoading() { finishedLoading() {
if (!this.messageList || 0 === this.messageList.length) { if (!this.messageList || 0 === this.messageList.length) {
return this.gotoMenu( return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', this.menuConfig.config.noResultsMenu || 'messageSearchNoResults'
{ menuFlags: ['popParent'] }
); );
} }
@ -58,7 +58,6 @@ exports.getModule = class MyMessagesModule extends MenuModule {
messageList: this.messageList, messageList: this.messageList,
noUpdateLastReadId: true, noUpdateLastReadId: true,
}, },
menuFlags: ['popParent'],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -185,6 +185,12 @@ General Information:
Actions: Actions:
list-confs List conferences and areas list-confs List conferences and areas
post PATH Posts a message file specified in PATH.
PATH must point to a UTF-8 encoded JSON file
containing 'to', 'from', 'subject', 'areaTag', and
'body'. If 'timestamp' is present, the system will
attempt to use it.
areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail
NetMail is sent to supplied address with the supplied command(s). Multi-part commands NetMail is sent to supplied address with the supplied command(s). Multi-part commands

View File

@ -716,6 +716,138 @@ const listConferences = () => {
}); });
}; };
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);
}
}
);
};
function handleMessageBaseCommand() { function handleMessageBaseCommand() {
function errUsage() { function errUsage() {
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
@ -734,6 +866,7 @@ function handleMessageBaseCommand() {
'qwk-dump': dumpQWKPacket, 'qwk-dump': dumpQWKPacket,
'qwk-export': exportQWKPacket, 'qwk-export': exportQWKPacket,
'list-confs': listConferences, 'list-confs': listConferences,
post: postMessage,
}[action] || errUsage }[action] || errUsage
)(); )();
} }

View File

@ -172,7 +172,7 @@ exports.getModule = class RumorzModule extends MenuModule {
StatLog.getSystemLogEntries( StatLog.getSystemLogEntries(
SystemLogKeys.UserAddedRumorz, SystemLogKeys.UserAddedRumorz,
StatLog.Order.Timestamp, StatLog.Order.TimestampDesc,
(err, entries) => { (err, entries) => {
return callback(err, entriesView, entries); return callback(err, entriesView, entries);
} }

View File

@ -4,7 +4,6 @@
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const Errors = require('../core/enig_error.js').Errors; const Errors = require('../core/enig_error.js').Errors;
const ANSI = require('./ansi_term.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { getMessageAreaByTag } = require('./message_area.js'); const { getMessageAreaByTag } = require('./message_area.js');
@ -21,6 +20,7 @@ exports.moduleInfo = {
exports.getModule = class ShowArtModule extends MenuModule { exports.getModule = class ShowArtModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
extraArgs: options.extraArgs, extraArgs: options.extraArgs,
}); });

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const { MenuModule, MenuFlags } = require('./menu_module');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const getSortedAvailableFileAreas = const getSortedAvailableFileAreas =
require('./file_base_area.js').getSortedAvailableFileAreas; require('./file_base_area.js').getSortedAvailableFileAreas;
@ -76,6 +76,8 @@ exports.getModule = class UploadModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.interrupt = MenuModule.InterruptTypes.Never; this.interrupt = MenuModule.InterruptTypes.Never;
if (_.has(options, 'lastMenuResult.recvFilePaths')) { if (_.has(options, 'lastMenuResult.recvFilePaths')) {

View File

@ -1,7 +1,7 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activesupport (7.0.4.1) activesupport (7.0.7.2)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -9,7 +9,7 @@ GEM
addressable (2.8.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
colorator (1.1.0) colorator (1.1.0)
concurrent-ruby (1.1.10) concurrent-ruby (1.2.2)
cssminify2 (2.0.1) cssminify2 (2.0.1)
em-websocket (0.5.3) em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
@ -24,7 +24,7 @@ GEM
nokogiri (>= 1.4) nokogiri (>= 1.4)
htmlcompressor (0.4.0) htmlcompressor (0.4.0)
http_parser.rb (0.8.0) http_parser.rb (0.8.0)
i18n (1.12.0) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jekyll (4.2.1) jekyll (4.2.1)
addressable (~> 2.4) addressable (~> 2.4)
@ -76,13 +76,13 @@ GEM
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0) mercenary (0.4.0)
minitest (5.17.0) minitest (5.19.0)
nokogiri (1.13.6-x86_64-linux) nokogiri (1.14.3-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
pathutil (0.16.2) pathutil (0.16.2)
forwardable-extended (~> 2.6) forwardable-extended (~> 2.6)
public_suffix (4.0.6) public_suffix (4.0.6)
racc (1.6.0) racc (1.6.2)
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
@ -93,7 +93,7 @@ GEM
ffi (~> 1.9) ffi (~> 1.9)
terminal-table (2.0.0) terminal-table (2.0.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
tzinfo (2.0.5) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
uglifier (4.2.0) uglifier (4.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)

View File

@ -323,7 +323,7 @@ qwk-export arguments:
| Action | Description | Examples | | 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` | | `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file. Optionally maps areas to FTN networks. | `./oputil.js mb import-areas /some/path/l33tnet.na` |
| `areafix` | Utility for sending AreaFix mails without logging into the system | | | `areafix` | Utility for sending AreaFix mails without logging into the system | |
| `qwk-dump` | Dump a QWK packet to stdout | `./oputil.js mb qwk-dump /path/to/XIBALBA.QWK` | | `qwk-dump` | Dump a QWK packet to stdout | `./oputil.js mb qwk-dump /path/to/XIBALBA.QWK` |
| `qwk-export` | Export messages to a QWK packet | `./oputil.js mb qwk-export /path/to/XIBALBA.QWK` | | `qwk-export` | Export messages to a QWK packet | `./oputil.js mb qwk-export /path/to/XIBALBA.QWK` |

View File

@ -59,13 +59,15 @@ The `config` block for a menu entry can contain common members as well as a per-
| `menuFlags` | An array of menu flag(s) controlling menu behavior. See **Menu Flags** below. | `menuFlags` | An array of menu flag(s) controlling menu behavior. See **Menu Flags** below.
#### Menu Flags #### Menu Flags
The `menuFlags` field of a `config` block can change default behavior of a particular menu. The `menuFlags` field of a `config` block can change default behavior of a particular menu:
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `noHistory` | Prevents the menu from remaining in the menu stack / history. When this flag is set, when the **next** menu falls back, this menu will be skipped and the previous menu again displayed instead. Example: menuA -> menuB(noHistory) -> menuC: Exiting menuC returns the user to menuA. | | `noHistory` | When leaving the current menu to load/chain to another, remove this menu from history. In other words, the fallback from the next menu would *not* be this one, but the previous. |
| `popParent` | When *this* menu is exited, fall back beyond the parent as well. Often used in combination with `noHistory`. | | `mergeFlags` | Generally used in code only: Request that any flags from `menu.hjson` |
| `forwardArgs` | If set, when the next menu is entered, forward any `extraArgs` arguments to *this* menu on to it. | | `forwardArgs` | Forward this menu's `extraArgs` to the next. |
> 💡 In JavaScript code, `MenuFlags` from `menu_module.js` contains constants for these flags.
## Forms ## Forms

View File

@ -58,7 +58,6 @@ showFileBaseAreaArt: {
method: fileBaseArea method: fileBaseArea
cls: true cls: true
pause: true pause: true
menuFlags: [ "popParent", "noHistory" ]
} }
} }
``` ```

View File

@ -101,6 +101,6 @@ webserver, and unpack it to a temporary directory.
otherwise. otherwise.
9. If you navigate to http://your-hostname.here/vtx.html, you should see a splash screen like the following: 9. If you navigate to http://your-hostname.here/vtx.html, you should see a splash screen like the following:
![VTXClient](../assets/images/vtxclient.png "VTXClient") ![VTXClient](../../assets/images/vtxclient.png "VTXClient")

View File

@ -388,7 +388,6 @@
art: FEMPTYQ art: FEMPTYQ
config: { config: {
pause: true pause: true
menuFlags: [ "noHistory", "popParent" ]
} }
} }
@ -779,7 +778,7 @@
art: FBNORES art: FBNORES
config: { config: {
pause: true pause: true
menuFlags: [ "noHistory", "popParent" ] menuFlags: [ "noHistory" ]
} }
} }
@ -807,7 +806,7 @@
art: FBNORES art: FBNORES
config: { config: {
pause: true pause: true
menuFlags: [ "noHistory", "popParent" ] menuFlags: [ "noHistory" ]
} }
} }
@ -852,7 +851,6 @@
art: ULNOAREA art: ULNOAREA
config: { config: {
pause: true pause: true
menuFlags: [ "noHistory", "popParent" ]
} }
} }

View File

@ -776,7 +776,7 @@
key: confTag key: confTag
pause: true pause: true
cls: true cls: true
menuFlags: [ "popParent", "noHistory" ] menuFlags: [ "noHistory" ]
} }
} }
@ -794,7 +794,7 @@
key: areaTag key: areaTag
pause: true pause: true
cls: true cls: true
menuFlags: [ "popParent", "noHistory" ] menuFlags: [ "noHistory" ]
} }
} }
} }

View File

@ -63,7 +63,7 @@
"rlogin": "^1.0.0", "rlogin": "^1.0.0",
"sane": "5.0.1", "sane": "5.0.1",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sqlite3": "5.0.11", "sqlite3": "5.1.6",
"sqlite3-trans": "1.3.0", "sqlite3-trans": "1.3.0",
"ssh2": "1.11.0", "ssh2": "1.11.0",
"string-strip-html": "8.4.0", "string-strip-html": "8.4.0",

1105
yarn.lock

File diff suppressed because it is too large Load Diff