Compare commits

...

7 Commits

Author SHA1 Message Date
Nathan Byrd 04392e387d Fixed handling of unknown door types 2023-11-05 00:21:49 +00:00
Nathan Byrd 9bb8277583 Various bug fixes 2023-11-04 23:38:43 +00:00
Nathan Byrd e2a9d35bb1 First set of updates for formatting 2023-10-16 00:05:43 +00:00
Nathan Byrd e6cc890338 Merge remote-tracking branch 'origin' into feature/dropfile_improvements 2023-10-15 18:41:54 +00:00
Nathan Byrd 8a27bb6758 Removed unused functions 2023-10-01 21:11:19 +00:00
Nathan Byrd d3a0fb40f9 Started on initial implementation 2023-10-01 21:09:14 +00:00
Nathan Byrd 19d92b34e4 Added dropfile formats 2023-10-01 19:04:41 +00:00
15 changed files with 791 additions and 538 deletions

4
.gitattributes vendored
View File

@ -16,3 +16,7 @@ optutil.js text eol=lf
# The devcontainer is also unix
.devcontainer/Dockerfile text eol=lf
.devcontainer/devcontainer.json text eol=lf
# All dropfiles are DOS
dropfile_formats/* eol=crlf

View File

@ -158,6 +158,11 @@ exports.getModule = class AbracadabraModule extends MenuModule {
fileType: self.config.dropFileType,
});
if(!(self.dropFile.isSupported())) {
// Return error so complete will log and return
return callback(Errors.AccessDenied('Dropfile format not supported'));
}
return self.dropFile.createFile(callback);
},
],

View File

@ -6,15 +6,20 @@ const Config = require('./config.js').get;
const StatLog = require('./stat_log.js');
const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js');
const paths = require('path');
const Log = require('./logger.js').log;
const getPredefinedMCIFormatObject =
require('./predefined_mci').getPredefinedMCIFormatObject;
const stringFormat = require('./string_format');
// deps
const fs = require('graceful-fs');
const paths = require('path');
const _ = require('lodash');
const moment = require('moment');
const iconv = require('iconv-lite');
const { mkdirs } = require('fs-extra');
const parseFullName = require('parse-full-name').parseFullName;
//
// Resources
// * https://github.com/NuSkooler/ansi-bbs/tree/master/docs/dropfile_formats
@ -32,6 +37,13 @@ module.exports = class DropFile {
this.client = client;
this.fileType = fileType.toUpperCase();
this.baseDir = baseDir;
this.dropFileFormatDirectory = paths.join(
__dirname,
'..',
'dropfile_formats'
);
}
static dropFileDirectory(baseDir, client) {
@ -60,6 +72,8 @@ module.exports = class DropFile {
JUMPER: 'JUMPER.DAT', // 2AM BBS
SXDOOR: 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE
INFO: 'INFO.BBS', // Phoenix BBS
SOLARREALMS: 'DOORFILE.SR',
XTRN: 'XTRN.DAT',
}[this.fileType];
}
@ -68,16 +82,118 @@ module.exports = class DropFile {
}
getHandler() {
return {
DOOR: this.getDoorSysBuffer,
DOOR32: this.getDoor32Buffer,
DORINFO: this.getDoorInfoDefBuffer,
}[this.fileType];
// TODO: Replace with a switch statement once we have binary handlers as well
// Read the directory containing the dropfile formats, and return undefined if we don't have the format
const fileName = this.fileName;
if (!fileName) {
Log.info({fileType: this.fileType}, 'Dropfile format not supported.');
return undefined;
}
const filePath = paths.join(this.dropFileFormatDirectory, fileName);
if(!fs.existsSync(filePath)) {
Log.info({filename: fileName}, 'Dropfile format not found or readable.');
return undefined;
}
// Return the handler to get the dropfile, because in the future we may have additional handlers
return this.getDropfile;
}
getContents() {
const handler = this.getHandler().bind(this);
return handler();
const handlerRef = this.getHandler();
if(!handlerRef) {
return undefined;
}
const handler = handlerRef.bind(this);
const contents = handler();
return contents;
}
getDropfile() {
// Get the filename to read
const fileName = paths.join(this.dropFileFormatDirectory, this.fileName);
let text = fs.readFileSync(fileName);
// Format the data with string_format and predefined_mci
let formatObj = getPredefinedMCIFormatObject(this.client, text);
const additionalFormatObj = {
'getSysopFirstName': this.getSysopFirstName(),
'getSysopLastName': this.getSysopLastName(),
'getUserFirstName': this.getUserFirstName(),
'getUserLastName': this.getUserLastName(),
'getUserTotalDownloadK': this.getUserTotalDownloadK(),
'getUserTotalUploadK': this.getUserTotalUploadK(),
'getCurrentDateMMDDYY': this.getCurrentDateMMDDYY(),
'getSystemDailyDownloadK': this.getSystemDailyDownloadK(),
'getUserBirthDateMMDDYY': this.getUserBirthDateMMDDYY(),
};
// Add additional format objects to the format object
formatObj = _.merge(formatObj, additionalFormatObj);
if (formatObj) {
// Expand the text
text = stringFormat(text, formatObj, true);
}
return text;
}
_getFirstName(fullname) {
return parseFullName(fullname).first;
}
_getLastName(fullname) {
return parseFullName(fullname).last;
}
getSysopFirstName() {
return this._getFirstName(StatLog.getSystemStat(SysProps.SysOpRealName));
}
getSysopLastName() {
return this._getLastName(StatLog.getSystemStat(SysProps.SysOpRealName));
}
_userStatAsString(statName, defaultValue) {
return (StatLog.getUserStat(this.client.user, statName) || defaultValue).toLocaleString();
}
_getUserRealName() {
return this._userStatAsString(UserProps.RealName, 'Unknown Unknown');
}
getUserFirstName() {
return this._getFirstName(this._getUserRealName);
}
getUserLastName() {
return this._getLastName(this._getUserRealName);
}
getUserTotalDownloadK() {
return StatLog.getUserStatNum(this.client.user, UserProps.FileDlTotalBytes) / 1024;
}
getSystemDailyDownloadK() {
return StatLog.getSystemStatNum(SysProps.getSystemDailyDownloadK) / 1024;
}
getUserTotalUploadK() {
return StatLog.getUserStatNum(this.client.user, UserProps.FileUlTotalBytes) / 1024;
}
getCurrentDateMMDDYY() {
// Return current date in MM/DD/YY format
return moment().format('MM/DD/YY');
}
getUserBirthDateMMDDYY() {
// Return user's birthdate in MM/DD/YY format
return moment(this.client.user.properties[UserProps.Birthdate]).format('MM/DD/YY');
}
getDoorInfoFileName() {
@ -93,160 +209,14 @@ module.exports = class DropFile {
return 'DORINFO' + x + '.DEF';
}
getDoorSysBuffer() {
const prop = this.client.user.properties;
const now = moment();
const secLevel = this.client.user.getLegacySecurityLevel().toString();
const fullName = this.client.user.getSanitizedName('real');
const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY');
const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024);
const downK = Math.floor(
(parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024
);
const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format(
'hh:mm'
);
// :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol
return iconv.encode(
[
'COM1:', // "Comm Port - COM0: = LOCAL MODE"
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
'8', // "Parity - 7 or 8"
this.client.node.toString(), // "Node Number - 1 to 99"
'57600', // "DTE Rate. Actual BPS rate to use. (kg)"
'Y', // "Screen Display - Y=On N=Off (Default to Y)"
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
'Y', // "Page Bell - Y=On N=Off (Default to Y)"
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
fullName, // "User Full Name"
prop[UserProps.Location] || 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
secLevel, // "Security Level"
prop[UserProps.LoginCount].toString(), // "Total Times On"
now.format('MM/DD/YY'), // "Last Date Called"
'15360', // "Seconds Remaining THIS call (for those that particular)"
'256', // "Minutes Remaining THIS call"
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
this.client.term.termHeight.toString(), // "Page Length"
'N', // "User Mode - Y = Expert, N = Novice"
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
'1', // "Conference Exited To DOOR From (G)"
'01/01/99', // "User Expiration Date (mm/dd/yy)"
this.client.user.userId.toString(), // "User File's Record Number"
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
// :TODO: fix up, down, etc. form user properties
'0', // "Total Uploads"
'0', // "Total Downloads"
'0', // "Daily Download "K" Total"
'999999', // "Daily Download Max. "K" Limit"
bd, // "Caller's Birthdate"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
'X:\\GEN\\', // "Path to the GEN directory"
StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)"
this.client.user.getSanitizedName(), // "Alias name"
'00:05', // "Event time (hh:mm)" (note: wat?)
'Y', // "If its an error correcting connection (Y/N)"
'Y', // "ANSI supported & caller using NG mode (Y/N)"
'Y', // "Use Record Locking (Y/N)"
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
// :TODO: fix minutes here also:
'256', // "Time Credits In Minutes (positive/negative)"
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
timeOfCall, // "Time of This Call"
timeOfCall, // "Time of Last Call (hh:mm)"
'9999', // "Maximum daily files available"
'0', // "Files d/led so far today"
upK.toString(), // "Total "K" Bytes Uploaded"
downK.toString(), // "Total "K" Bytes Downloaded"
prop[UserProps.UserComment] || 'None', // "User Comment"
'0', // "Total Doors Opened"
'0', // "Total Messages Left"
].join('\r\n') + '\r\n',
'cp437'
);
}
getDoor32Buffer() {
//
// Resources:
// * http://wiki.bbses.info/index.php/DOOR32.SYS
// * https://github.com/NuSkooler/ansi-bbs/blob/master/docs/dropfile_formats/door32_sys.txt
//
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
const Door32CommTypes = {
Local: 0,
Serial: 1,
Telnet: 2,
};
const commType = Door32CommTypes.Telnet;
return iconv.encode(
[
commType.toString(),
'-1',
'115200',
Config().general.boardName,
this.client.user.userId.toString(),
this.client.user.getSanitizedName('real'),
this.client.user.getSanitizedName(),
this.client.user.getLegacySecurityLevel().toString(),
'546', // :TODO: Minutes left!
'1', // ANSI
this.client.node.toString(),
].join('\r\n') + '\r\n',
'cp437'
);
}
getDoorInfoDefBuffer() {
// :TODO: fix time remaining
//
// Resources:
// * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm
//
// Note that usernames are just used for first/last names here
//
const opUserName = /[^\s]*/.exec(
StatLog.getSystemStat(SysProps.SysOpUsername)
)[0];
const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0];
const secLevel = this.client.user.getLegacySecurityLevel().toString();
const location = this.client.user.properties[UserProps.Location];
return iconv.encode(
[
Config().general.boardName, // "The name of the system."
opUserName, // "The sysop's name up to the first space."
opUserName, // "The sysop's name following the first space."
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
'57600', // "The current port (DTE) rate."
'0', // "The number "0""
userName, // "The current user's name, up to the first space."
userName, // "The current user's name, following the first space."
location || '', // "Where the user lives, or a blank line if unknown."
'1', // "The number "0" if TTY, or "1" if ANSI."
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
'-1', // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
].join('\r\n') + '\r\n',
'cp437'
);
}
createFile(cb) {
mkdirs(paths.dirname(this.fullPath), err => {
if (err) {
return cb(err);
}
return fs.writeFile(this.fullPath, this.getContents(), cb);
const fullPath = this.fullPath;
const contents = this.getContents();
return fs.writeFile(fullPath, contents, cb);
});
}
};

View File

@ -124,6 +124,12 @@ const PREDEFINED_MCI_GENERATORS = {
UN: function userName(client) {
return client.user.username;
},
UZ: function sanitizedUserName(client) {
return client.user.getSanitizedName();
},
LL: function legacyUserLevel(client) {
return client.user.getLegacySecurityLevel().toString();
},
UI: function userId(client) {
return client.user.userId.toString();
},

View File

@ -14,6 +14,8 @@ const {
formatCountAbbr,
} = require('./string_util.js');
const colorCodes = require('./color_codes.js');
// deps
const _ = require('lodash');
const moment = require('moment');
@ -210,7 +212,7 @@ function formatNumberHelper(n, precision, type) {
function formatNumber(value, tokens) {
const fill = tokens.fill || (tokens['0'] ? '0' : ' ');
const align = tokens.align || (tokens['0'] ? '=' : '>');
const width = Number(tokens.width);
const width = Number(tokens.width);value.replace(/\x1b\[[0-9;]*m/g, '');
const type = tokens.type || (tokens.precision ? 'g' : '');
if (['c', 'd', 'b', 'o', 'x', 'X'].indexOf(type) > -1) {
@ -299,6 +301,7 @@ const transformers = {
styleSmallI: s => stylizeString(s, 'small i'),
styleMixed: s => stylizeString(s, 'mixed'),
styleL33t: s => stylizeString(s, 'l33t'),
sanitized: s => stylizeString(s, 'sanitized'),
// :TODO:
// toMegs(), toKilobytes(), ...
@ -337,7 +340,7 @@ function getValue(obj, path) {
throw new KeyError(quote(path));
}
module.exports = function format(fmt, obj) {
module.exports = function format(fmt, obj, stripMciColorCodes = false) {
const re = REGEXP_BASIC_FORMAT;
re.lastIndex = 0; // reset from prev
@ -369,6 +372,11 @@ module.exports = function format(fmt, obj) {
value = transformValue(transformer, value);
}
// This is used in cases where the output shouldn't allow color codes
if (stripMciColorCodes) {
value = colorCodes.stripMciColorCodes(value);
}
tokens = tokenizeFormatSpec(formatSpec || '');
if (_.isNumber(value)) {

View File

@ -0,0 +1,36 @@
{UR}
4
{SL}
{LL}
999
COLOR
NOTPROVIDED
{UI}
0
{CT}
{CT} {getCurrentDateMMDDYY}
{MC}
{DN}
999
0
999999
555-555-5555
{getCurrentDateMMDDYY} {CT}
NOVICE
All
{getCurrentDateMMDDYY}
{UC}
{SH}
0
{UP}
{DN}
8
REMOTE
1
{BD}
38400
FALSE
Normal Connection
{getCurrentDateMMDDYY} {CT}
{ND}
0

View File

@ -0,0 +1,31 @@
{UI}
{UN}
{UR}
?
{UA}
M
{AP}
{getCurrentDateMMDDYY}
{SW}
{SH}
{LL}
0
0
1
0
99999
C:\DATA
C:\WWIV\DATA
C:\DATA\logfile.txt
C:\DATA\BBS.LOG
19200
1
{BN}
{SR}
1
0
{getUserTotalUploadK}
{UP}
{getUserTotalDownloadK}
{DN}
8N1

52
dropfile_formats/DOOR.SYS Normal file
View File

@ -0,0 +1,52 @@
COM1:
38400
8
{ND}
19200
N
N
N
N
{UR}
{LO}
555 555-5555
555 555-5555
NOT PROVIDED
{LL}
{UC}
{getCurrentDateMMDDYY}
9999
999
GR
{SH}
N
{MC}
{MC}
12/31/9999
{UI}
Y
{UP}
{DN}
{getSystemDailyDownloadK}
9999999
{getUserBirthDateMMDDYY}
C:\DATA
C:\DATA
{SR}
{UZ}
{CT}
Y
Y
Y
7
32767
{getCurrentDateMMDDYY}
{CT}
{CT}
9999
{DD}
{getUserTotalUploadK}
{getUserTotalDownloadK}
{DR}
32767

View File

@ -0,0 +1,8 @@
{UN}
1
1
{SH}
19200
1
-1
{UR}

View File

@ -0,0 +1,13 @@
{BN}
{getSysopFirstName}
{getSysopLastName}
COM1
19200 BAUD,N,8,1
0
{getUserFirstName}
{getUserLastName}
{LO}
1
{LL}
999
-1

View File

@ -0,0 +1,18 @@
{UI}
{UN}
NOTPROVIDED
{LL}
N
Y
999
555-555-1212
{LO}
{getUserBirthDateMMDDYY}
{ND}
1
19200
0
N
Y
{BN}
{SR}

66
dropfile_formats/XTRN.DAT Normal file
View File

@ -0,0 +1,66 @@
{UR}
{BN}
{SN}
The Guru
C:\CONTROL
C:\DATA
99
{ND}
99999
Yes
{SH}
209087929
{LL}
1
{BD}
M
{UI}
555-555-5555
1
4
3f8
38400
No
No
AT&FS0=0S2=128E0V0X4&C1&D2
ATC0\V1
ATE1V1\V2
ATDT
ATM0H1
ATA
533922058
99
ABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0
0
{UR}
38400

View File

@ -0,0 +1,11 @@
1
1
19200
{BN}
{UI}
{UR}
{UN}
1
999
1
{ND}

View File

@ -53,6 +53,7 @@
"node-pty": "1.0.0",
"nodemailer": "6.7.7",
"otplib": "11.0.1",
"parse-full-name": "^1.2.6",
"qrcode-generator": "^1.4.4",
"rlogin": "^1.0.0",
"sane": "5.0.1",

778
yarn.lock

File diff suppressed because it is too large Load Diff