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
* 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
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

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.
* 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 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
* **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: {
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
// Fri Sep 25th

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ const {
messageInfoFromAddressedToInfo,
setExternalAddressedToInfo,
copyExternalAddressedToInfo,
getReplyToMessagePrefix,
} = require('./mail_util.js');
const Events = require('./events.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 FileEntry = require('./file_entry.js');
const DownloadQueue = require('./download_queue.js');
const EngiAssert = require('./enigma_assert.js');
// deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const moment = require('moment');
const fse = require('fs-extra');
@ -123,7 +124,7 @@ exports.FullScreenEditorModule =
this.editorMode = config.editorMode;
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;
}
@ -251,7 +252,7 @@ exports.FullScreenEditorModule =
if (self.newQuoteBlock) {
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());
}
@ -480,106 +481,108 @@ exports.FullScreenEditorModule =
this.message = message;
this.updateLastReadId(() => {
if (this.isReady) {
this.initHeaderViewMode();
this.initFooterViewMode();
if (!this.isReady) {
return;
}
const bodyMessageView = this.viewControllers.body.getView(
MciViewIds.body.message
);
let msg = this.message.message;
this.initHeaderViewMode();
this.initFooterViewMode();
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
// we don't want to do things like word wrap ANSI, but instead, trust that it's formatted
// how the author wanted it
// Find tearline - we want to color it differently.
//
if (isAnsi(msg)) {
//
// Find tearline - we want to color it differently.
//
const tearLinePos = Message.getTearLinePosition(msg);
const tearLinePos = Message.getTearLinePosition(msg);
if (tearLinePos > -1) {
msg = insert(
msg,
tearLinePos,
bodyMessageView.getTextSgrPrefix()
);
if (tearLinePos > -1) {
msg = insert(
msg,
tearLinePos,
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(
msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF
{
prepped: false,
forceLineTerm: true,
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}`;
}
);
} 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;
var art = self.menuConfig.config.art;
assert(_.isObject(art));
EngiAssert(_.isObject(art));
async.waterfall(
[
@ -1161,7 +1164,7 @@ exports.FullScreenEditorModule =
}
initHeaderReplyEditMode() {
assert(_.isObject(this.replyToMessage));
EngiAssert(_.isObject(this.replyToMessage));
this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName);
@ -1177,6 +1180,20 @@ exports.FullScreenEditorModule =
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() {
this.setViewText(
'footerView',
@ -1450,11 +1467,18 @@ exports.FullScreenEditorModule =
switchToBody() {
const to = this.getView('header', MciViewIds.header.to).getData();
const msgInfo = messageInfoFromAddressedToInfo(getAddressedToInfo(to));
const bodyView = this.getView('body', MciViewIds.body.message);
if (msgInfo.maxMessageLength > 0) {
const bodyView = this.getView('body', MciViewIds.body.message);
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.body.switchFocus(1);

View File

@ -2,7 +2,8 @@ const { Errors } = require('./enig_error.js');
// deps
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 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 => {
let body = [];
res.on('data', d => {

View File

@ -15,6 +15,7 @@ exports.setExternalAddressedToInfo = setExternalAddressedToInfo;
exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo;
exports.messageInfoFromAddressedToInfo = messageInfoFromAddressedToInfo;
exports.getQuotePrefixFromName = getQuotePrefixFromName;
exports.getReplyToMessagePrefix = getReplyToMessagePrefix;
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,}))$/;
@ -32,6 +33,11 @@ const EMAIL_REGEX =
foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.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' }
Fields:
- name : user/display name
- flavor : remote flavor - FTN/etc.
- remote : Address in remote format, if applicable
*/
function getAddressedToInfo(input) {
input = input.trim();
@ -41,17 +47,26 @@ function getAddressedToInfo(input) {
if (firstAtPos < 0) {
let addr = Address.fromString(input);
if (Address.isValidAddress(addr)) {
return { flavor: MessageConst.AddressFlavor.FTN, remote: input };
return {
flavor: MessageConst.AddressFlavor.FTN,
remote: input,
};
}
const lessThanPos = input.indexOf('<');
if (lessThanPos < 0) {
return { name: input, flavor: MessageConst.AddressFlavor.Local };
return {
name: input,
flavor: MessageConst.AddressFlavor.Local,
};
}
const greaterThanPos = input.indexOf('>');
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));
@ -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);
@ -107,7 +125,10 @@ function getAddressedToInfo(input) {
let addr = Address.fromString(input); // 5D?
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());
@ -172,6 +193,17 @@ function messageInfoFromAddressedToInfo(addressInfo) {
}
function getQuotePrefixFromName(name) {
const addrInfo = getAddressedToInfo(name);
return getQuotePrefix(addrInfo.name || name);
const addressInfo = getAddressedToInfo(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 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 {
constructor(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() {
return {
Never: 'never',

View File

@ -5,12 +5,12 @@
const loadMenu = require('./menu_util.js').loadMenu;
const { Errors, ErrorReasons } = require('./enig_error.js');
const { getResolvedSpec } = require('./menu_util.js');
const { MenuFlags } = require('./menu_module.js');
// deps
const _ = require('lodash');
const assert = require('assert');
// :TODO: Stack is backwards.... top should be most recent! :)
const bunyan = require('bunyan');
module.exports = class MenuStack {
constructor(client) {
@ -27,19 +27,11 @@ module.exports = class MenuStack {
}
peekPrev() {
if (this.stackSize > 1) {
return this.stack[this.stack.length - 2];
}
return this.stack[this.stack.length - 2];
}
top() {
if (this.stackSize > 0) {
return this.stack[this.stack.length - 1];
}
}
get stackSize() {
return this.stack.length;
return this.stack[this.stack.length - 1];
}
get currentModule() {
@ -81,19 +73,15 @@ module.exports = class MenuStack {
prev(cb) {
const menuResult = this.top().instance.getMenuResult();
const currentModuleInfo = this.top();
// :TODO: leave() should really take a cb...
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) {
const opts = {
extraArgs: previousModuleInfo.extraArgs,
savedState: previousModuleInfo.savedState,
lastMenuResult: menuResult,
currentModuleInfo,
};
return this.goto(previousModuleInfo.name, opts, cb);
@ -111,8 +99,7 @@ module.exports = class MenuStack {
}
options = options || {};
const currentModuleInfo = options.currentModuleInfo || this.top();
const self = this;
const currentModuleInfo = this.top();
if (currentModuleInfo && name === currentModuleInfo.name) {
if (cb) {
@ -128,10 +115,13 @@ module.exports = class MenuStack {
const loadOpts = {
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;
} else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
@ -140,11 +130,10 @@ module.exports = class MenuStack {
loadMenu(loadOpts, (err, modInst) => {
if (err) {
// :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod();
const errCb = cb || this.client.defaultHandlerMissingMod();
errCb(err);
} 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 (cb) {
@ -153,22 +142,6 @@ module.exports = class MenuStack {
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
// anything supplied in code.
@ -182,9 +155,9 @@ module.exports = class MenuStack {
// in code we can ask to merge in
if (
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();
if (currentModuleInfo.menuFlags.includes('noHistory')) {
if (currentModuleInfo.menuFlags.includes(MenuFlags.NoHistory)) {
this.pop();
}
if (menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current
}
}
self.push({
this.push({
name: name,
instance: modInst,
extraArgs: loadOpts.extraArgs,
@ -216,17 +185,19 @@ module.exports = class MenuStack {
modInst.restoreSavedState(options.savedState);
}
const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name;
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
', '
)})`;
}
return name;
});
if (this.client.log.level() <= bunyan.TRACE) {
const stackEntries = this.stack.map(stackEntry => {
let name = stackEntry.name;
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
', '
)})`;
}
return name;
});
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
this.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
}
modInst.enter();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -323,7 +323,7 @@ qwk-export arguments:
| 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 | |
| `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` |

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.
#### 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 |
|------|-------------|
| `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. |
| `popParent` | When *this* menu is exited, fall back beyond the parent as well. Often used in combination with `noHistory`. |
| `forwardArgs` | If set, when the next menu is entered, forward any `extraArgs` arguments to *this* menu on to it. |
| `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. |
| `mergeFlags` | Generally used in code only: Request that any flags from `menu.hjson` |
| `forwardArgs` | Forward this menu's `extraArgs` to the next. |
> 💡 In JavaScript code, `MenuFlags` from `menu_module.js` contains constants for these flags.
## Forms

View File

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

View File

@ -101,6 +101,6 @@ webserver, and unpack it to a temporary directory.
otherwise.
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
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
}
}
@ -779,7 +778,7 @@
art: FBNORES
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
menuFlags: [ "noHistory" ]
}
}
@ -807,7 +806,7 @@
art: FBNORES
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
menuFlags: [ "noHistory" ]
}
}
@ -852,7 +851,6 @@
art: ULNOAREA
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
}
}

View File

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

View File

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

1105
yarn.lock

File diff suppressed because it is too large Load Diff