Merge branch '459-activitypub-integration' of github.com:NuSkooler/enigma-bbs into 459-activitypub-integration

This commit is contained in:
Nathan Byrd 2023-02-04 19:34:09 -06:00
commit 77b0e6dd23
18 changed files with 229 additions and 65 deletions

View File

@ -13,6 +13,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* New users now have randomly generated avatars assigned to them that can be served up via the new System General [Web Handler](/docs/_docs/servers/contentservers/web-handlers.md). * New users now have randomly generated avatars assigned to them that can be served up via the new System General [Web Handler](/docs/_docs/servers/contentservers/web-handlers.md).
* 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.
## 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

@ -400,6 +400,7 @@
TL1: { width: 19, textOverflow: "..." } TL1: { width: 19, textOverflow: "..." }
ET2: { width: 19, textOverflow: "..." } ET2: { width: 19, textOverflow: "..." }
ET3: { width: 19, textOverflow: "..." } ET3: { width: 19, textOverflow: "..." }
ET4: { width: 21, textOverflow: "..." }
} }
} }
1: { 1: {
@ -773,6 +774,7 @@
TL1: { width: 19, textOverflow: "..." } TL1: { width: 19, textOverflow: "..." }
ET2: { width: 19, textOverflow: "..." } ET2: { width: 19, textOverflow: "..." }
ET3: { width: 19, textOverflow: "..." } ET3: { width: 19, textOverflow: "..." }
ET4: { width: 21, textOverflow: "..." }
//TL4: { width: 25 } //TL4: { width: 25 }
} }
} }

View File

@ -94,6 +94,7 @@ module.exports = class Activity extends ActivityPubObject {
return postJson(actorUrl, activityJson, reqOpts, cb); return postJson(actorUrl, activityJson, reqOpts, cb);
} }
// :TODO: we need dp/support a bit more here...
recipientIds() { recipientIds() {
const ids = []; const ids = [];

View File

@ -80,13 +80,13 @@ exports.getModule = class BBSListModule extends MenuModule {
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
if (errMsgView) { if (errMsgView) {
if (err) { if (err) {
errMsgView.setText(err.message); errMsgView.setText(err.friendlyText);
} else { } else {
errMsgView.clearText(); errMsgView.clearText();
} }
} }
return cb(null); return cb(err, null);
}, },
// //

View File

@ -413,6 +413,18 @@ module.exports = () => {
}, },
}, },
// General ActivityPub integration configuration
activityPub: {
// Mimics Mastodon max 500 characters for *outgoing* Notes
// (messages destined for ActivityPub); This is a soft limit;
// Implementations including Mastodon should still display
// longer messages, but this keeps us as a "good citizen"
autoSignatures: false,
// by default, don't include auto-signatures in AP outgoing
maxMessageLength: 500,
},
infoExtractUtils: { infoExtractUtils: {
Exiftool2Desc: { Exiftool2Desc: {
cmd: `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x cmd: `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x

View File

@ -61,6 +61,8 @@ exports.Errors = {
new EnigError('Bad or missing form data', -32016, reason, reasonCode), new EnigError('Bad or missing form data', -32016, reason, reasonCode),
Duplicate: (reason, reasonCode) => Duplicate: (reason, reasonCode) =>
new EnigError('Duplicate', -32017, reason, reasonCode), new EnigError('Duplicate', -32017, reason, reasonCode),
ValidationFailed: (reason, reasonCode) =>
new EnigError('Validation failed', -32018, reason, reasonCode),
}; };
exports.ErrorReasons = { exports.ErrorReasons = {
@ -76,4 +78,11 @@ exports.ErrorReasons = {
Locked: 'LOCKED', Locked: 'LOCKED',
NotAllowed: 'NOTALLOWED', NotAllowed: 'NOTALLOWED',
Invalid2FA: 'INVALID2FA', Invalid2FA: 'INVALID2FA',
ValueTooShort: 'VALUE_TOO_SHORT',
ValueTooLong: 'VALUE_TOO_LONG',
ValueInvalid: 'VALUE_INVALID',
NotAvailable: 'NOT_AVAILABLE',
DoesNotExist: 'EEXIST',
}; };

View File

@ -146,18 +146,17 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
const errorView = this.viewControllers.editor.getView( const errorView = this.viewControllers.editor.getView(
MciViewIds.editor.error MciViewIds.editor.error
); );
let newFocusId;
if (errorView) { if (errorView) {
if (err) { if (err) {
errorView.setText(err.message); errorView.setText(err.friendlyText);
err.view.clearText(); // clear out the invalid data err.view.clearText(); // clear out the invalid data
} else { } else {
errorView.clearText(); errorView.clearText();
} }
} }
return cb(newFocusId); return cb(err, null);
}, },
}; };
} }

View File

@ -18,6 +18,7 @@ const { stripMciColorCodes, controlCodesToAnsi } = require('./color_codes.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { const {
getAddressedToInfo, getAddressedToInfo,
messageInfoFromAddressedToInfo,
setExternalAddressedToInfo, setExternalAddressedToInfo,
copyExternalAddressedToInfo, copyExternalAddressedToInfo,
} = require('./mail_util.js'); } = require('./mail_util.js');
@ -37,6 +38,7 @@ const fse = require('fs-extra');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const sanatizeFilename = require('sanitize-filename'); const sanatizeFilename = require('sanitize-filename');
const { ErrorReasons } = require('./enig_error.js');
exports.moduleInfo = { exports.moduleInfo = {
name: 'Full Screen Editor (FSE)', name: 'Full Screen Editor (FSE)',
@ -164,23 +166,35 @@ exports.FullScreenEditorModule =
// //
// Validation stuff // Validation stuff
// //
viewValidationListener: function (err, cb) { viewValidationListener: (err, cb) => {
var errMsgView = self.viewControllers.header.getView( if (
err &&
err.view.getId() === MciViewIds.header.subject &&
err.reasonCode === ErrorReasons.ValueTooShort
) {
// Ignore validation errors if this is the subject field
// and it's optional
const toView = this.getView('header', MciViewIds.header.to);
const msgInfo = messageInfoFromAddressedToInfo(
getAddressedToInfo(toView.getData())
);
if (true === msgInfo.subjectOptional) {
return cb(null, null);
}
}
const errMsgView = this.viewControllers.header.getView(
MciViewIds.header.errorMsg MciViewIds.header.errorMsg
); );
var newFocusViewId;
if (errMsgView) { if (errMsgView) {
if (err) { if (err) {
errMsgView.setText(err.message); errMsgView.setText(err.friendlyText);
if (MciViewIds.header.subject === err.view.getId()) {
// :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel)
}
} else { } else {
errMsgView.clearText(); errMsgView.clearText();
} }
} }
cb(newFocusViewId);
return cb(err, null);
}, },
headerSubmit: function (formData, extraArgs, cb) { headerSubmit: function (formData, extraArgs, cb) {
self.switchToBody(); self.switchToBody();
@ -424,10 +438,15 @@ exports.FullScreenEditorModule =
// //
// Append auto-signature, if enabled for the area & the user has one // Append auto-signature, if enabled for the area & the user has one
// //
if (false != area.autoSignatures) { const msgInfo = messageInfoFromAddressedToInfo(
const sig = this.client.user.getProperty(UserProps.AutoSignature); getAddressedToInfo(headerValues.to)
if (sig) { );
messageBody += `\r\n-- \r\n${sig}`; if (false !== msgInfo.autoSignatures) {
if (false !== area.autoSignatures) {
const sig = this.client.user.getProperty(UserProps.AutoSignature);
if (sig) {
messageBody += `\r\n-- \r\n${sig}`;
}
} }
} }
@ -1391,6 +1410,13 @@ exports.FullScreenEditorModule =
} }
switchToBody() { switchToBody() {
const to = this.getView('header', MciViewIds.header.to).getData();
const msgInfo = messageInfoFromAddressedToInfo(getAddressedToInfo(to));
if (msgInfo.maxMessageLength > 0) {
const bodyView = this.getView('body', MciViewIds.body.message);
bodyView.maxLength = msgInfo.maxMessageLength;
}
this.viewControllers.header.setFocus(false); this.viewControllers.header.setFocus(false);
this.viewControllers.body.switchFocus(1); this.viewControllers.body.switchFocus(1);

View File

@ -5,10 +5,15 @@ const EnigmaAssert = require('./enigma_assert.js');
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const MessageConst = require('./message_const'); const MessageConst = require('./message_const');
const { getQuotePrefix } = require('./ftn_util'); const { getQuotePrefix } = require('./ftn_util');
const Config = require('./config').get;
// deps
const { get } = require('lodash');
exports.getAddressedToInfo = getAddressedToInfo; exports.getAddressedToInfo = getAddressedToInfo;
exports.setExternalAddressedToInfo = setExternalAddressedToInfo; exports.setExternalAddressedToInfo = setExternalAddressedToInfo;
exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo; exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo;
exports.messageInfoFromAddressedToInfo = messageInfoFromAddressedToInfo;
exports.getQuotePrefixFromName = getQuotePrefixFromName; exports.getQuotePrefixFromName = getQuotePrefixFromName;
const EMAIL_REGEX = const EMAIL_REGEX =
@ -148,6 +153,24 @@ function copyExternalAddressedToInfo(fromMessage, toMessage) {
toMessage.setExternalFlavor(fromMessage.meta.System[sm.ExternalFlavor]); toMessage.setExternalFlavor(fromMessage.meta.System[sm.ExternalFlavor]);
} }
function messageInfoFromAddressedToInfo(addressInfo) {
switch (addressInfo.flavor) {
case MessageConst.AddressFlavor.ActivityPub: {
const config = Config();
const maxMessageLength = get(config, 'activityPub.maxMessageLength', 500);
const autoSignatures = get(config, 'activityPub.autoSignatures', false);
// Additionally, it's ot necessary to supply a subject
// (aka summary) with a 'Note' Activity
return { subjectOptional: true, maxMessageLength, autoSignatures };
}
default:
// autoSignatures: null = varies by additional config
return { subjectOptional: false, maxMessageLength: 0, autoSignatures: null };
}
}
function getQuotePrefixFromName(name) { function getQuotePrefixFromName(name) {
const addrInfo = getAddressedToInfo(name); const addrInfo = getAddressedToInfo(name);
return getQuotePrefix(addrInfo.name || name); return getQuotePrefix(addrInfo.name || name);

View File

@ -113,6 +113,7 @@ function MultiLineEditTextView(options) {
this.textLines = []; this.textLines = [];
this.topVisibleIndex = 0; this.topVisibleIndex = 0;
this.mode = options.mode || 'edit'; // edit | preview | read-only this.mode = options.mode || 'edit'; // edit | preview | read-only
this.maxLength = 0; // no max by default
if ('preview' === this.mode) { if ('preview' === this.mode) {
this.autoScroll = options.autoScroll || true; this.autoScroll = options.autoScroll || true;
@ -317,6 +318,15 @@ function MultiLineEditTextView(options) {
return text; return text;
}; };
this.getCharacterLength = function () {
// :TODO: FSE needs re-write anyway, but this should just be known all the time vs calc. Too much of a mess right now...
let len = 0;
this.textLines.forEach(tl => {
len += tl.text.length;
});
return len;
};
this.replaceCharacterInText = function (c, index, col) { this.replaceCharacterInText = function (c, index, col) {
self.textLines[index].text = strUtil.replaceAt( self.textLines[index].text = strUtil.replaceAt(
self.textLines[index].text, self.textLines[index].text,
@ -664,6 +674,10 @@ function MultiLineEditTextView(options) {
}; };
this.keyPressCharacter = function (c) { this.keyPressCharacter = function (c) {
if (this.maxLength > 0 && this.getCharacterLength() + 1 >= this.maxLength) {
return;
}
var index = self.getTextLinesIndex(); var index = self.getTextLinesIndex();
// //
@ -1170,6 +1184,12 @@ MultiLineEditTextView.prototype.setPropertyValue = function (propName, value) {
this.specialKeyMap.next = this.specialKeyMap.next || []; this.specialKeyMap.next = this.specialKeyMap.next || [];
this.specialKeyMap.next.push('tab'); this.specialKeyMap.next.push('tab');
break; break;
case 'maxLength':
if (_.isNumber(value)) {
this.maxLength = value;
}
break;
} }
MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);

View File

@ -49,10 +49,10 @@ exports.getModule = class NewUserAppModule extends MenuModule {
viewValidationListener: function (err, cb) { viewValidationListener: function (err, cb) {
const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg);
let newFocusId;
let newFocusId;
if (err) { if (err) {
errMsgView.setText(err.message); errMsgView.setText(err.friendlyText);
err.view.clearText(); err.view.clearText();
if (err.view.getId() === MciViewIds.confirm) { if (err.view.getId() === MciViewIds.confirm) {
@ -65,7 +65,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
errMsgView.clearText(); errMsgView.clearText();
} }
return cb(newFocusId); return cb(err, newFocusId);
}, },
// //

View File

@ -2,11 +2,12 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const User = require('./user.js'); const User = require('./user');
const Config = require('./config.js').get; const Config = require('./config').get;
const Log = require('./logger.js').log; const Log = require('./logger').log;
const { getAddressedToInfo } = require('./mail_util.js'); const { getAddressedToInfo } = require('./mail_util');
const Message = require('./message.js'); const Message = require('./message');
const { Errors, ErrorReasons } = require('./enig_error'); // note: Only use ValidationFailed in this module!
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
@ -22,36 +23,66 @@ exports.validateBirthdate = validateBirthdate;
exports.validatePasswordSpec = validatePasswordSpec; exports.validatePasswordSpec = validatePasswordSpec;
function validateNonEmpty(data, cb) { function validateNonEmpty(data, cb) {
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); return cb(
data && data.length > 0
? null
: Errors.ValidationFailed('Field cannot be empty', ErrorReasons.ValueTooShort)
);
} }
function validateMessageSubject(data, cb) { function validateMessageSubject(data, cb) {
return cb(data && data.length > 1 ? null : new Error('Subject too short')); return cb(
data && data.length > 1
? null
: Errors.ValidationFailed('Subject too short', ErrorReasons.ValueTooShort)
);
} }
function validateUserNameAvail(data, cb) { function validateUserNameAvail(data, cb) {
const config = Config(); const config = Config();
if (!data || data.length < config.users.usernameMin) { if (!data || data.length < config.users.usernameMin) {
cb(new Error('Username too short')); cb(Errors.ValidationFailed('Username too short', ErrorReasons.ValueTooShort));
} else if (data.length > config.users.usernameMax) { } else if (data.length > config.users.usernameMax) {
// generally should be unreached due to view restraints // generally should be unreached due to view restraints
return cb(new Error('Username too long')); return cb(
Errors.ValidationFailed('Username too long', ErrorReasons.ValueTooLong)
);
} else { } else {
const usernameRegExp = new RegExp(config.users.usernamePattern); const usernameRegExp = new RegExp(config.users.usernamePattern);
const invalidNames = config.users.newUserNames + config.users.badUserNames; const invalidNames = config.users.newUserNames + config.users.badUserNames;
if (!usernameRegExp.test(data)) { if (!usernameRegExp.test(data)) {
return cb(new Error('Username contains invalid characters')); return cb(
Errors.ValidationFailed(
'Username contains invalid characters',
ErrorReasons.ValueInvalid
)
);
} else if (invalidNames.indexOf(data.toLowerCase()) > -1) { } else if (invalidNames.indexOf(data.toLowerCase()) > -1) {
return cb(new Error('Username is blacklisted')); return cb(
Errors.ValidationFailed(
'Username is blacklisted',
ErrorReasons.NotAllowed
)
);
} else if (/^[0-9]+$/.test(data)) { } else if (/^[0-9]+$/.test(data)) {
return cb(new Error('Username cannot be a number')); return cb(
Errors.ValidationFailed(
'Username cannot be a number',
ErrorReasons.ValueInvalid
)
);
} else { } else {
// a new user name cannot be an existing user name or an existing real name // a new user name cannot be an existing user name or an existing real name
User.getUserIdAndNameByLookup(data, function userIdAndName(err) { User.getUserIdAndNameByLookup(data, function userIdAndName(err) {
if (!err) { if (!err) {
// err is null if we succeeded -- meaning this user exists already // err is null if we succeeded -- meaning this user exists already
return cb(new Error('Username unavailable')); return cb(
Errors.ValidationFailed(
'Username unavailable',
ErrorReasons.NotAvailable
)
);
} }
return cb(null); return cb(null);
@ -60,25 +91,41 @@ function validateUserNameAvail(data, cb) {
} }
} }
const invalidUserNameError = () => new Error('Invalid username');
function validateUserNameExists(data, cb) { function validateUserNameExists(data, cb) {
if (0 === data.length) { if (0 === data.length) {
return cb(invalidUserNameError()); return cb(
Errors.ValidationFailed('Invalid username', ErrorReasons.ValueTooShort)
);
} }
User.getUserIdAndName(data, err => { User.getUserIdAndName(data, err => {
return cb(err ? invalidUserNameError() : null); return cb(
err
? Errors.ValidationFailed(
'Failed to find username',
err.reasonCode || ErrorReasons.DoesNotExist
)
: null
);
}); });
} }
function validateUserNameOrRealNameExists(data, cb) { function validateUserNameOrRealNameExists(data, cb) {
if (0 === data.length) { if (0 === data.length) {
return cb(invalidUserNameError()); return cb(
Errors.ValidationFailed('Invalid username', ErrorReasons.ValueTooShort)
);
} }
User.getUserIdAndNameByLookup(data, err => { User.getUserIdAndNameByLookup(data, err => {
return cb(err ? invalidUserNameError() : null); return cb(
err
? Errors.ValidationFailed(
'Failed to find user',
err.reasonCode || ErrorReasons.DoesNotExist
)
: null
);
}); });
} }
@ -112,7 +159,9 @@ function validateEmailAvail(data, cb) {
// //
const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/;
if (!emailRegExp.test(data)) { if (!emailRegExp.test(data)) {
return cb(new Error('Invalid email address')); return cb(
Errors.ValidationFailed('Invalid email address', ErrorReasons.ValueInvalid)
);
} }
User.getUserIdsWithProperty( User.getUserIdsWithProperty(
@ -120,9 +169,19 @@ function validateEmailAvail(data, cb) {
data, data,
function userIdsWithEmail(err, uids) { function userIdsWithEmail(err, uids) {
if (err) { if (err) {
return cb(new Error('Internal system error')); return cb(
Errors.ValidationFailed(
err.message,
err.reasonCode || ErrorReasons.DoesNotExist
)
);
} else if (uids.length > 0) { } else if (uids.length > 0) {
return cb(new Error('Email address not unique')); return cb(
Errors.ValidationFailed(
'Email address not unique',
ErrorReasons.NotAvailable
)
);
} }
return cb(null); return cb(null);
@ -132,25 +191,36 @@ function validateEmailAvail(data, cb) {
function validateBirthdate(data, cb) { function validateBirthdate(data, cb) {
// :TODO: check for dates in the future, or > reasonable values // :TODO: check for dates in the future, or > reasonable values
return cb(isNaN(Date.parse(data)) ? new Error('Invalid birthdate') : null); return cb(
isNaN(Date.parse(data))
? Errors.ValidationFailed('Invalid birthdate', ErrorReasons.ValueInvalid)
: null
);
} }
function validatePasswordSpec(data, cb) { function validatePasswordSpec(data, cb) {
const config = Config(); const config = Config();
if (!data || data.length < config.users.passwordMin) { if (!data || data.length < config.users.passwordMin) {
return cb(new Error('Password too short')); return cb(
Errors.ValidationFailed('Password too short', ErrorReasons.ValueTooShort)
);
} }
// check badpass, if avail // check badpass, if avail
fs.readFile(config.users.badPassFile, 'utf8', (err, passwords) => { fs.readFile(config.users.badPassFile, 'utf8', (err, passwords) => {
if (err) { if (err) {
Log.warn({ error: err.message }, 'Cannot read bad pass file'); Log.warn(
{ error: err.message, path: config.users.badPassFile },
'Cannot read bad pass file'
);
return cb(null); return cb(null);
} }
passwords = passwords.toString().split(/\r\n|\n/g); passwords = passwords.toString().split(/\r\n|\n/g);
if (passwords.includes(data)) { if (passwords.includes(data)) {
return cb(new Error('Password is too common')); return cb(
Errors.ValidationFailed('Password is too common', ErrorReasons.NotAllowed)
);
} }
return cb(null); return cb(null);

View File

@ -120,13 +120,13 @@ exports.getModule = class UploadModule extends MenuModule {
); );
if (errView) { if (errView) {
if (err) { if (err) {
errView.setText(err.message); errView.setText(err.friendlyText);
} else { } else {
errView.clearText(); errView.clearText();
} }
} }
return cb(null); return cb(err, null);
}, },
}; };
} }

View File

@ -90,7 +90,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
var newFocusId; var newFocusId;
if (errMsgView) { if (errMsgView) {
if (err) { if (err) {
errMsgView.setText(err.message); errMsgView.setText(err.friendlyText);
if (err.view.getId() === MciCodeIds.PassConfirm) { if (err.view.getId() === MciCodeIds.PassConfirm) {
newFocusId = MciCodeIds.Password; newFocusId = MciCodeIds.Password;
@ -102,7 +102,8 @@ exports.getModule = class UserConfigModule extends MenuModule {
errMsgView.clearText(); errMsgView.clearText();
} }
} }
cb(newFocusId);
return cb(err, newFocusId);
}, },
// //

View File

@ -150,7 +150,7 @@ View.prototype.setPosition = function (pos) {
this.position.col = parseInt(arguments[1], 10); this.position.col = parseInt(arguments[1], 10);
} }
// sanatize // sanitize
this.position.row = Math.max(this.position.row, 1); this.position.row = Math.max(this.position.row, 1);
this.position.col = Math.max(this.position.col, 1); this.position.col = Math.max(this.position.col, 1);
this.position.row = Math.min(this.position.row, this.client.term.termHeight); this.position.row = Math.min(this.position.row, this.client.term.termHeight);

View File

@ -385,19 +385,18 @@ function ViewController(options) {
this.validateView = function (view, cb) { this.validateView = function (view, cb) {
if (view && _.isFunction(view.validate)) { if (view && _.isFunction(view.validate)) {
view.validate(view.getData(), function validateResult(err) { view.validate(view.getData(), function validateResult(err) {
var viewValidationListener = const viewValidationListener =
self.client.currentMenuModule.menuMethods.viewValidationListener; self.client.currentMenuModule.menuMethods.viewValidationListener;
if (_.isFunction(viewValidationListener)) { if (_.isFunction(viewValidationListener)) {
if (err) { if (err) {
err.view = view; // pass along the view that failed err.view = view; // pass along the view that failed
err.friendlyText = err.reason || err.message;
} }
viewValidationListener( viewValidationListener(err, (err, newFocusedViewId) => {
err, // validator may have updated |err|
function validationComplete(newViewFocusId) { return cb(err, newFocusedViewId);
cb(err, newViewFocusId); });
}
);
} else { } else {
cb(err); cb(err);
} }

View File

@ -226,6 +226,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
enter() { enter() {
this.client.stopIdleMonitor(); this.client.stopIdleMonitor();
this._applyOpVisibility(); this._applyOpVisibility();
Events.on( Events.on(
Events.getSystemEvents().ClientDisconnected, Events.getSystemEvents().ClientDisconnected,
this._clientDisconnected.bind(this) this._clientDisconnected.bind(this)
@ -240,7 +241,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
Events.removeListener( Events.removeListener(
Events.getSystemEvents().ClientDisconnected, Events.getSystemEvents().ClientDisconnected,
this._clientDisconnected this._clientDisconnected.bind(this)
); );
this._restoreOpVisibility(); this._restoreOpVisibility();