Sync up with master
This commit is contained in:
@@ -505,9 +505,9 @@ class Achievements {
|
||||
getFormatObject(info) {
|
||||
return {
|
||||
userName: info.user.username,
|
||||
userRealName: info.user.properties[UserProps.RealName],
|
||||
userLocation: info.user.properties[UserProps.Location],
|
||||
userAffils: info.user.properties[UserProps.Affiliations],
|
||||
userRealName: info.user.realName(false) || 'N/A',
|
||||
userLocation: info.user.properties[UserProps.Location] || 'N/A',
|
||||
userAffils: info.user.properties[UserProps.Affiliations] || 'N/A',
|
||||
nodeId: info.client.node,
|
||||
title: info.details.title,
|
||||
//text : info.global ? info.details.globalText : info.details.text,
|
||||
|
||||
@@ -24,7 +24,7 @@ function ANSIEscapeParser(options) {
|
||||
this.graphicRendition = {};
|
||||
|
||||
this.parseState = {
|
||||
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex
|
||||
};
|
||||
|
||||
options = miscUtil.valueWithDefault(options, {
|
||||
@@ -37,6 +37,12 @@ function ANSIEscapeParser(options) {
|
||||
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
||||
this.breakWidth = this.termWidth;
|
||||
// toNumber takes care of null, undefined etc as well.
|
||||
let artWidth = _.toNumber(options.artWidth);
|
||||
if(!(_.isNaN(artWidth)) && artWidth > 0 && artWidth < this.breakWidth) {
|
||||
this.breakWidth = options.artWidth;
|
||||
}
|
||||
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
||||
|
||||
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
||||
@@ -90,8 +96,8 @@ function ANSIEscapeParser(options) {
|
||||
|
||||
switch (charCode) {
|
||||
case CR:
|
||||
self.emit('literal', text.slice(start, pos));
|
||||
start = pos;
|
||||
self.emit('literal', text.slice(start, pos + 1));
|
||||
start = pos + 1;
|
||||
|
||||
self.column = 1;
|
||||
|
||||
@@ -105,8 +111,8 @@ function ANSIEscapeParser(options) {
|
||||
self.column = 1;
|
||||
}
|
||||
|
||||
self.emit('literal', text.slice(start, pos));
|
||||
start = pos;
|
||||
self.emit('literal', text.slice(start, pos + 1));
|
||||
start = pos + 1;
|
||||
|
||||
self.row += 1;
|
||||
|
||||
@@ -114,13 +120,16 @@ function ANSIEscapeParser(options) {
|
||||
break;
|
||||
|
||||
default:
|
||||
if (self.column === self.termWidth) {
|
||||
if (self.column === self.breakWidth) {
|
||||
self.emit('literal', text.slice(start, pos + 1));
|
||||
start = pos + 1;
|
||||
|
||||
// If we hit breakWidth before termWidth then we need to force the terminal to go to the next line.
|
||||
if(self.column < self.termWidth) {
|
||||
self.emit('literal', '\r\n');
|
||||
}
|
||||
self.column = 1;
|
||||
self.row += 1;
|
||||
|
||||
self.positionUpdated();
|
||||
} else {
|
||||
self.column += 1;
|
||||
@@ -135,7 +144,7 @@ function ANSIEscapeParser(options) {
|
||||
//
|
||||
// Finalize this chunk
|
||||
//
|
||||
if (self.column > self.termWidth) {
|
||||
if (self.column > self.breakWidth) {
|
||||
self.column = 1;
|
||||
self.row += 1;
|
||||
|
||||
@@ -222,7 +231,7 @@ function ANSIEscapeParser(options) {
|
||||
self.parseState = {
|
||||
// ignore anything past EOF marker, if any
|
||||
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
|
||||
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex
|
||||
stop: false,
|
||||
};
|
||||
};
|
||||
|
||||
13
core/art.js
13
core/art.js
@@ -41,11 +41,21 @@ const SUPPORTED_ART_TYPES = {
|
||||
};
|
||||
|
||||
function getFontNameFromSAUCE(sauce) {
|
||||
if (sauce.Character) {
|
||||
if (sauce && sauce.Character) {
|
||||
return sauce.Character.fontName;
|
||||
}
|
||||
}
|
||||
|
||||
function getWidthFromSAUCE(sauce) {
|
||||
if (sauce && sauce.Character) {
|
||||
let sauceWidth = _.toNumber(sauce.Character.characterWidth);
|
||||
if(!(_.isNaN(sauceWidth)) && sauceWidth > 0) {
|
||||
return sauceWidth;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sliceAtEOF(data, eofMarker) {
|
||||
let eof = data.length;
|
||||
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
|
||||
@@ -274,6 +284,7 @@ function display(client, art, options, cb) {
|
||||
mciReplaceChar: options.mciReplaceChar,
|
||||
termHeight: client.term.termHeight,
|
||||
termWidth: client.term.termWidth,
|
||||
artWidth: getWidthFromSAUCE(options.sauce),
|
||||
trailingLF: options.trailingLF,
|
||||
startRow: options.startRow,
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ function getActiveConnectionList(
|
||||
//
|
||||
entry.text = ac.user?.username || 'N/A';
|
||||
entry.userName = ac.user?.username || 'N/A';
|
||||
entry.realName = ac.user?.getProperty(UserProps.RealName) || 'N/A';
|
||||
entry.realName = ac.user?.realName(false) || 'N/A';
|
||||
entry.location = ac.user?.getProperty(UserProps.Location) || 'N/A';
|
||||
entry.affils = entry.affiliation =
|
||||
ac.user?.getProperty(UserProps.Affiliations) || 'N/A';
|
||||
|
||||
@@ -188,22 +188,15 @@ module.exports = () => {
|
||||
//
|
||||
// 1 - Generate a Private Key (PK):
|
||||
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
|
||||
// To generate a secure PK, issue the following command:
|
||||
//
|
||||
// > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
|
||||
// -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \
|
||||
// -out ./config/security/ssh_private_key.pem -aes128
|
||||
//
|
||||
// (The above is a more modern equivalent of the following):
|
||||
// > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048
|
||||
// For information on generating a key, see:
|
||||
// https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html#generate-a-ssh-private-key
|
||||
//
|
||||
// 2 - Set 'privateKeyPass' to the password you used in step #1
|
||||
//
|
||||
// 3 - Finally, set 'enabled' to 'true'
|
||||
//
|
||||
// Additional reading:
|
||||
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
|
||||
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
|
||||
// - https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html
|
||||
//
|
||||
privateKeyPem: paths.join(
|
||||
__dirname,
|
||||
@@ -222,14 +215,18 @@ module.exports = () => {
|
||||
//
|
||||
algorithms: {
|
||||
kex: [
|
||||
'curve25519-sha256',
|
||||
'curve25519-sha256@libssh.org',
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'diffie-hellman-group14-sha1',
|
||||
'diffie-hellman-group1-sha1',
|
||||
// Group exchange not currnetly supported
|
||||
// 'diffie-hellman-group-exchange-sha256',
|
||||
// 'diffie-hellman-group-exchange-sha1',
|
||||
'curve25519-sha256',
|
||||
'curve25519-sha256@libssh.org',
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
],
|
||||
cipher: [
|
||||
'aes128-ctr',
|
||||
@@ -242,12 +239,7 @@ module.exports = () => {
|
||||
'aes256-cbc',
|
||||
'aes192-cbc',
|
||||
'aes128-cbc',
|
||||
'blowfish-cbc',
|
||||
'3des-cbc',
|
||||
'arcfour256',
|
||||
'arcfour128',
|
||||
'cast128-cbc',
|
||||
'arcfour',
|
||||
],
|
||||
hmac: [
|
||||
'hmac-sha2-256',
|
||||
|
||||
@@ -57,8 +57,10 @@ module.exports = class Door {
|
||||
run(exeInfo, cb) {
|
||||
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
||||
|
||||
if ('socket' === this.io && !this.sockServer) {
|
||||
return cb(Errors.UnexpectedState('Socket server is not running'));
|
||||
if ('socket' === this.io) {
|
||||
if(!this.sockServer) {
|
||||
return cb(Errors.UnexpectedState('Socket server is not running'));
|
||||
}
|
||||
} else if ('stdio' !== this.io) {
|
||||
return cb(Errors.Invalid(`"${this.io}" is not a valid io type!`));
|
||||
}
|
||||
|
||||
16
core/fse.js
16
core/fse.js
@@ -167,6 +167,7 @@ exports.FullScreenEditorModule =
|
||||
var newFocusViewId;
|
||||
if (errMsgView) {
|
||||
if (err) {
|
||||
errMsgView.clearText();
|
||||
errMsgView.setText(err.message);
|
||||
|
||||
if (MciViewIds.header.subject === err.view.getId()) {
|
||||
@@ -183,6 +184,13 @@ exports.FullScreenEditorModule =
|
||||
return cb(null);
|
||||
},
|
||||
editModeEscPressed: function (formData, extraArgs, cb) {
|
||||
const errMsgView = self.viewControllers.header.getView(
|
||||
MciViewIds.header.errorMsg
|
||||
);
|
||||
if (errMsgView) {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
|
||||
self.footerMode =
|
||||
'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
||||
|
||||
@@ -982,11 +990,7 @@ exports.FullScreenEditorModule =
|
||||
const area = getMessageAreaByTag(self.messageAreaTag);
|
||||
if (fromView !== undefined) {
|
||||
if (area && area.realNames) {
|
||||
fromView.setText(
|
||||
self.client.user.properties[
|
||||
UserProps.RealName
|
||||
] || self.client.user.username
|
||||
);
|
||||
fromView.setText(self.client.user.realName());
|
||||
} else {
|
||||
fromView.setText(self.client.user.username);
|
||||
}
|
||||
@@ -1054,7 +1058,7 @@ exports.FullScreenEditorModule =
|
||||
posView.setText(
|
||||
_.padStart(String(pos.row + 1), 2, '0') +
|
||||
',' +
|
||||
_.padEnd(String(pos.col + 1), 2, '0')
|
||||
_.padStart(String(pos.col + 1), 2, '0')
|
||||
);
|
||||
this.client.term.rawWrite(ansi.restorePos());
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ module.exports = class Address {
|
||||
static fromString(addrStr) {
|
||||
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
||||
|
||||
if (m) {
|
||||
if (m && m[2] && m[3]) {
|
||||
// start with a 2D
|
||||
let addr = {
|
||||
net: parseInt(m[2]),
|
||||
|
||||
@@ -763,6 +763,11 @@ module.exports = class Message {
|
||||
}
|
||||
|
||||
persist(cb) {
|
||||
const containsNonWhitespaceCharacterRegEx = /\S/;
|
||||
if (!containsNonWhitespaceCharacterRegEx.test(this.message)) {
|
||||
return cb(Errors.Invalid('Empty message'));
|
||||
}
|
||||
|
||||
if (!this.isValid()) {
|
||||
return cb(Errors.Invalid('Cannot persist invalid message!'));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,39 @@ exports.moduleInfo = {
|
||||
author: 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
header: {
|
||||
from: 1,
|
||||
to: 2,
|
||||
subject: 3,
|
||||
errorMsg: 4,
|
||||
modTimestamp: 5,
|
||||
msgNum: 6,
|
||||
msgTotal: 7,
|
||||
|
||||
customRangeStart: 10, // 10+ = customs
|
||||
},
|
||||
|
||||
body: {
|
||||
message: 1,
|
||||
},
|
||||
|
||||
// :TODO: quote builder MCIs - remove all magic #'s
|
||||
|
||||
// :TODO: consolidate all footer MCI's - remove all magic #'s
|
||||
ViewModeFooter: {
|
||||
MsgNum: 6,
|
||||
MsgTotal: 7,
|
||||
// :TODO: Just use custom ranges
|
||||
},
|
||||
|
||||
quoteBuilder: {
|
||||
quotedMsg: 1,
|
||||
// 2 NYI
|
||||
quoteLines: 3,
|
||||
},
|
||||
};
|
||||
|
||||
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
@@ -42,19 +75,25 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||
],
|
||||
function complete(err) {
|
||||
if (err) {
|
||||
// :TODO:... sooooo now what?
|
||||
} else {
|
||||
// note: not logging 'from' here as it's part of client.log.xxxx()
|
||||
self.client.log.info(
|
||||
{
|
||||
to: msg.toUserName,
|
||||
subject: msg.subject,
|
||||
uuid: msg.messageUuid,
|
||||
},
|
||||
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
|
||||
const errMsgView = self.viewControllers.header.getView(
|
||||
MciViewIds.header.errorMsg
|
||||
);
|
||||
if (errMsgView) {
|
||||
errMsgView.setText(err.message);
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// note: not logging 'from' here as it's part of client.log.xxxx()
|
||||
self.client.log.info(
|
||||
{
|
||||
to: msg.toUserName,
|
||||
subject: msg.subject,
|
||||
uuid: msg.messageUuid,
|
||||
},
|
||||
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
|
||||
);
|
||||
|
||||
return self.nextMenu(cb);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -21,10 +21,7 @@ exports.getModule = class MyMessagesModule extends MenuModule {
|
||||
|
||||
initSequence() {
|
||||
const filter = {
|
||||
toUserName: [
|
||||
this.client.user.username,
|
||||
this.client.user.getProperty(UserProps.RealName),
|
||||
],
|
||||
toUserName: [this.client.user.username, this.client.user.realName()],
|
||||
sort: 'modTimestamp',
|
||||
resultType: 'messageList',
|
||||
limit: 1024 * 16, // we want some sort of limit...
|
||||
|
||||
15
core/nua.js
15
core/nua.js
@@ -13,6 +13,7 @@ const UserProps = require('./user_property.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name: 'NUA',
|
||||
@@ -95,15 +96,15 @@ exports.getModule = class NewUserAppModule extends MenuModule {
|
||||
areaTag = areaTag || '';
|
||||
|
||||
newUser.properties = {
|
||||
[UserProps.RealName]: formData.value.realName,
|
||||
[UserProps.RealName]: formData.value.realName || '',
|
||||
[UserProps.Birthdate]: getISOTimestampString(
|
||||
formData.value.birthdate
|
||||
formData.value.birthdate || moment()
|
||||
),
|
||||
[UserProps.Sex]: formData.value.sex,
|
||||
[UserProps.Location]: formData.value.location,
|
||||
[UserProps.Affiliations]: formData.value.affils,
|
||||
[UserProps.EmailAddress]: formData.value.email,
|
||||
[UserProps.WebAddress]: formData.value.web,
|
||||
[UserProps.Sex]: formData.value.sex || '',
|
||||
[UserProps.Location]: formData.value.location || '',
|
||||
[UserProps.Affiliations]: formData.value.affils || '',
|
||||
[UserProps.EmailAddress]: formData.value.email || '',
|
||||
[UserProps.WebAddress]: formData.value.web || '',
|
||||
[UserProps.AccountCreated]: getISOTimestampString(),
|
||||
|
||||
[UserProps.MessageConfTag]: confTag,
|
||||
|
||||
@@ -21,8 +21,10 @@ exports.validateEmailAvail = validateEmailAvail;
|
||||
exports.validateBirthdate = validateBirthdate;
|
||||
exports.validatePasswordSpec = validatePasswordSpec;
|
||||
|
||||
const emptyFieldError = () => new Error('Field cannot be empty');
|
||||
|
||||
function validateNonEmpty(data, cb) {
|
||||
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
|
||||
return cb(data && data.length > 0 ? null : emptyFieldError);
|
||||
}
|
||||
|
||||
function validateMessageSubject(data, cb) {
|
||||
@@ -91,7 +93,11 @@ function validateGeneralMailAddressedTo(data, cb) {
|
||||
// :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
|
||||
const addressedToInfo = getAddressedToInfo(data);
|
||||
|
||||
if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
|
||||
if (addressedToInfo.name.length === 0) {
|
||||
return cb(emptyFieldError());
|
||||
}
|
||||
|
||||
if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,10 @@ TextView.prototype.setText = function (text, redraw) {
|
||||
};
|
||||
|
||||
TextView.prototype.clearText = function () {
|
||||
if (this.text) {
|
||||
this.setText(this.fillChar.repeat(this.text.length));
|
||||
}
|
||||
|
||||
this.setText('');
|
||||
};
|
||||
|
||||
|
||||
21
core/user.js
21
core/user.js
@@ -124,12 +124,29 @@ module.exports = class User {
|
||||
return isMember;
|
||||
}
|
||||
|
||||
realName(withUsernameFallback = true) {
|
||||
const realName = this.getProperty(UserProps.RealName);
|
||||
if (realName) {
|
||||
return realName;
|
||||
}
|
||||
if (withUsernameFallback) {
|
||||
return this.username;
|
||||
}
|
||||
}
|
||||
|
||||
getSanitizedName(type = 'username') {
|
||||
const name =
|
||||
'real' === type ? this.getProperty(UserProps.RealName) : this.username;
|
||||
const name = 'real' === type ? this.realName(true) : this.username;
|
||||
return sanatizeFilename(name) || `user${this.userId.toString()}`;
|
||||
}
|
||||
|
||||
emailAddress() {
|
||||
const email = this.getProperty(UserProps.EmailAddress);
|
||||
if (email) {
|
||||
const realName = this.realName(false);
|
||||
return realName ? `${realName} <${email}>` : email;
|
||||
}
|
||||
}
|
||||
|
||||
isAvailable() {
|
||||
return (this.statusFlags & User.StatusFlags.NotAvailable) == 0;
|
||||
}
|
||||
|
||||
@@ -93,9 +93,7 @@ module.exports = class User2FA_OTPWebRegister {
|
||||
}
|
||||
|
||||
const message = {
|
||||
to: `${
|
||||
user.getProperty(UserProps.RealName) || user.username
|
||||
} <${user.getProperty(UserProps.EmailAddress)}>`,
|
||||
to: user.emailAddress(),
|
||||
// from will be filled in
|
||||
subject: '2-Factor Authentication Registration',
|
||||
text: textTemplate,
|
||||
|
||||
@@ -115,15 +115,15 @@ exports.getModule = class UserConfigModule extends MenuModule {
|
||||
formData = _.clone(formData);
|
||||
|
||||
const newProperties = {
|
||||
[UserProps.RealName]: formData.value.realName,
|
||||
[UserProps.RealName]: formData.value.realName || '',
|
||||
[UserProps.Birthdate]: getISOTimestampString(
|
||||
formData.value.birthdate
|
||||
formData.value.birthdate || moment()
|
||||
),
|
||||
[UserProps.Sex]: formData.value.sex,
|
||||
[UserProps.Location]: formData.value.location,
|
||||
[UserProps.Affiliations]: formData.value.affils,
|
||||
[UserProps.EmailAddress]: formData.value.email,
|
||||
[UserProps.WebAddress]: formData.value.web,
|
||||
[UserProps.Sex]: formData.value.sex || '',
|
||||
[UserProps.Location]: formData.value.location || '',
|
||||
[UserProps.Affiliations]: formData.value.affils || '',
|
||||
[UserProps.EmailAddress]: formData.value.email || '',
|
||||
[UserProps.WebAddress]: formData.value.web || '',
|
||||
[UserProps.TermHeight]: formData.value.termHeight.toString(),
|
||||
[UserProps.ThemeId]:
|
||||
self.availThemeInfo[formData.value.theme].themeId,
|
||||
@@ -233,11 +233,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
|
||||
function populateViews(callback) {
|
||||
const user = self.client.user;
|
||||
|
||||
self.setViewText(
|
||||
'menu',
|
||||
MciCodeIds.RealName,
|
||||
user.properties[UserProps.RealName]
|
||||
);
|
||||
self.setViewText('menu', MciCodeIds.RealName, user.realName(false) || '');
|
||||
self.setViewText(
|
||||
'menu',
|
||||
MciCodeIds.BirthDate,
|
||||
|
||||
@@ -143,9 +143,7 @@ class WebPasswordReset {
|
||||
}
|
||||
|
||||
const message = {
|
||||
to: `${user.properties[UserProps.RealName] || user.username} <${
|
||||
user.properties[UserProps.EmailAddress]
|
||||
}>`,
|
||||
to: user.emailAddress(),
|
||||
// from will be filled in
|
||||
subject: 'Forgot Password',
|
||||
text: textTemplate,
|
||||
|
||||
@@ -506,9 +506,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
||||
|
||||
// Current
|
||||
currentUserName: this.client.user.username,
|
||||
currentUserRealName:
|
||||
this.client.user.getProperty(UserProps.RealName) ||
|
||||
this.client.user.username,
|
||||
currentUserRealName: this.client.user.realName(false) || 'N/A',
|
||||
availIndicator: availIndicator,
|
||||
visIndicator: visIndicator,
|
||||
lastLoginUserName: lastLoginStats.userName,
|
||||
|
||||
Reference in New Issue
Block a user