-
Notifications
You must be signed in to change notification settings - Fork 106
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
base: master
Are you sure you want to change the base?
Changes from all commits
19d92b3
d3a0fb4
8a27bb6
e6cc890
e2a9d35
9bb8277
04392e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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.'); | ||
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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small nit here, remove the |
||
'getSysopLastName': this.getSysopLastName(), | ||
'getUserFirstName': this.getUserFirstName(), | ||
'getUserLastName': this.getUserLastName(), | ||
'getUserTotalDownloadK': this.getUserTotalDownloadK(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might be able to do it here as well, something like |
||
'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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,6 +124,12 @@ const PREDEFINED_MCI_GENERATORS = { | |
UN: function userName(client) { | ||
return client.user.username; | ||
}, | ||
UZ: function sanitizedUserName(client) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! We'll need to get these in the MCI docs There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
}, | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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