Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dropfile improvements #521

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ optutil.js text eol=lf

# The devcontainer is also unix
.devcontainer/Dockerfile text eol=lf
.devcontainer/devcontainer.json text eol=lf
.devcontainer/devcontainer.json text eol=lf


# All dropfiles are DOS
dropfile_formats/* eol=crlf
5 changes: 5 additions & 0 deletions core/abracadabra.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
],
Expand Down
286 changes: 128 additions & 158 deletions core/dropfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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];
}

Expand All @@ -68,185 +82,141 @@ 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.');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should that be a warn?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed I'll change these to warns

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;
}

getDoorInfoFileName() {
let x;
const node = this.client.node;
if (10 === node) {
x = 0;
} else if (node < 10) {
x = node;
} else {
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
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(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit here, remove the get prefix to match the rest of how the system treats these types of variables

'getSysopLastName': this.getSysopLastName(),
'getUserFirstName': this.getUserFirstName(),
'getUserLastName': this.getUserLastName(),
'getUserTotalDownloadK': this.getUserTotalDownloadK(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think {userTotalDownloadBytes!toKilobytes} (by resolving some TODO's in string_format.js would work here and be more flexible?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this one and the date format I had thought about, but there is a bit of a problem with when these methods are called... it looks like the methods run first and return a string, then the formatting runs on them. So for these it would actually have to parse as a string to format, which is a bit ugly. Maybe a better way would be to have these return an object with a toString instead, then we can format them as we like, with the default behaving as it does now? Just a thought.

Copy link
Owner

@NuSkooler NuSkooler Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at this maybe tomorrow night. I think in string_format.js it can be expanded to allow some Date/Time raw formats of some sort and via transformers:

            try {
                value = getValue(obj, objPath);
                if (transformer) {
// 'value' can be a obj here IIRC
                    value = transformValue(transformer, value);
                }

                tokens = tokenizeFormatSpec(formatSpec || '');
// could allow non-transformers to handle D/T here too... maybe
                if (_.isNumber(value)) {
                    out += formatNumber(value, tokens);
                } else {
                    out += formatString(value, tokens);
                }
            } catch (e) {
                if (e instanceof KeyError) {
                    out += match[0]; //  preserve full thing
                } else if (e instanceof ValueError) {
                    out += value.toString();
                }
            }

'getUserTotalUploadK': this.getUserTotalUploadK(),
'getCurrentDateMMDDYY': this.getCurrentDateMMDDYY(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to do it here as well, something like {currentDate!dateMmDdYy} or something, with some other common formats.

'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 'DORINFO' + x + '.DEF';
return text;
}

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
);
_getFirstName(fullname) {
return parseFullName(fullname).first;
}

const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format(
'hh:mm'
);
_getLastName(fullname) {
return parseFullName(fullname).last;
}

// :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol
return iconv.encode(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I think we're losing here is the self-commenting dropfile formats.

[
'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'
);
getSysopFirstName() {
return this._getFirstName(StatLog.getSystemStat(SysProps.SysOpRealName));
}

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,
};
getSysopLastName() {
return this._getLastName(StatLog.getSystemStat(SysProps.SysOpRealName));
}

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'
);
_userStatAsString(statName, defaultValue) {
return (StatLog.getUserStat(this.client.user, statName) || defaultValue).toLocaleString();
}

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'
);
_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() {
let x;
const node = this.client.node;
if (10 === node) {
x = 0;
} else if (node < 10) {
x = node;
} else {
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
}
return 'DORINFO' + x + '.DEF';
}

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);
});
}
};
6 changes: 6 additions & 0 deletions core/predefined_mci.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ const PREDEFINED_MCI_GENERATORS = {
UN: function userName(client) {
return client.user.username;
},
UZ: function sanitizedUserName(client) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! We'll need to get these in the MCI docs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I actually probably need to do a few more of these too and think about the split - I.e I could also add first name / last name ones etc among others. Probably needs some more thinking on what is appropriate here

return client.user.getSanitizedName();
},
LL: function legacyUserLevel(client) {
return client.user.getLegacySecurityLevel().toString();
},
UI: function userId(client) {
return client.user.userId.toString();
},
Expand Down
Loading