* Reworked FTN packet I/O (WIP)
* Detect FTN packet 2, 2.2, and 2+ * Various FTN utils (MSGID, Origin, PID, generation etc) * More work on message network readyness
This commit is contained in:
parent
317af8419a
commit
dec78e942d
|
@ -172,7 +172,10 @@ function getDefaultConfig() {
|
||||||
paths : {
|
paths : {
|
||||||
mods : paths.join(__dirname, './../mods/'),
|
mods : paths.join(__dirname, './../mods/'),
|
||||||
servers : paths.join(__dirname, './servers/'),
|
servers : paths.join(__dirname, './servers/'),
|
||||||
msgNetworks : paths.join(__dirname, './msg_networks/'),
|
|
||||||
|
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
|
||||||
|
mailers : paths.join(__dirname, './mailers/') ,
|
||||||
|
|
||||||
art : paths.join(__dirname, './../mods/art/'),
|
art : paths.join(__dirname, './../mods/art/'),
|
||||||
themes : paths.join(__dirname, './../mods/themes/'),
|
themes : paths.join(__dirname, './../mods/themes/'),
|
||||||
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
||||||
|
|
|
@ -307,9 +307,9 @@ function FullScreenEditorModule(options) {
|
||||||
|
|
||||||
// :TODO: We'd like to delete up to N rows, but this does not work
|
// :TODO: We'd like to delete up to N rows, but this does not work
|
||||||
// in NetRunner:
|
// in NetRunner:
|
||||||
//self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3));
|
self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3));
|
||||||
|
|
||||||
self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2))
|
//self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2))
|
||||||
}
|
}
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,39 +17,37 @@ var buffers = require('buffers');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
:TODO: should probably be broken up
|
:TODO: things
|
||||||
FTNPacket
|
* Read/detect packet types: 2, 2.2, and 2+
|
||||||
FTNPacketImport: packet -> message(s)
|
* Write packet types: 2, 2.2, and 2+
|
||||||
FTNPacketExport: message(s) -> packet
|
* Test SAUCE ignore/extraction
|
||||||
*/
|
* FSP-1010 for netmail (see SBBS)
|
||||||
|
|
||||||
/*
|
|
||||||
Reader: file to ftn data
|
|
||||||
Writer: ftn data to packet
|
|
||||||
|
|
||||||
Data to toMessage
|
|
||||||
Data.fromMessage
|
|
||||||
|
|
||||||
FTNMessage.toMessage() => Message
|
|
||||||
FTNMessage.fromMessage() => Create from Message
|
|
||||||
|
|
||||||
* read: header -> simple {} obj, msg -> Message object
|
|
||||||
* read: read(..., iterator): iterator('header', ...), iterator('message', msg)
|
|
||||||
* write: provide information to go into header
|
|
||||||
|
|
||||||
* Logic of "Is this for us"/etc. elsewhere
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const FTN_PACKET_HEADER_SIZE = 58; // fixed header size
|
const FTN_PACKET_HEADER_SIZE = 58; // fixed header size
|
||||||
const FTN_PACKET_HEADER_TYPE = 2;
|
const FTN_PACKET_HEADER_TYPE = 2;
|
||||||
const FTN_PACKET_MESSAGE_TYPE = 2;
|
const FTN_PACKET_MESSAGE_TYPE = 2;
|
||||||
|
const FTN_PACKET_BAUD_TYPE_2_2 = 2;
|
||||||
|
|
||||||
// EOF + SAUCE.id + SAUCE.version ('00')
|
// SAUCE magic header + version ("00")
|
||||||
const FTN_MESSAGE_SAUCE_HEADER =
|
const FTN_MESSAGE_SAUCE_HEADER = new Buffer('SAUCE00');
|
||||||
new Buffer( [ 0x1a, 'S', 'A', 'U', 'C', 'E', '0', '0' ] );
|
|
||||||
|
|
||||||
const FTN_MESSAGE_KLUDGE_PREFIX = '\x01';
|
const FTN_MESSAGE_KLUDGE_PREFIX = '\x01';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Read/Write FTN packets with support for the following formats:
|
||||||
|
//
|
||||||
|
// * Type 1 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete)
|
||||||
|
// * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001
|
||||||
|
// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004
|
||||||
|
// and http://ftsc.org/docs/fsc-0048.002
|
||||||
|
//
|
||||||
|
// Additional resources:
|
||||||
|
// * Writeup on differences between type 2, 2.2, and 2+:
|
||||||
|
// http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt
|
||||||
|
//
|
||||||
function FTNPacket() {
|
function FTNPacket() {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -57,16 +55,14 @@ function FTNPacket() {
|
||||||
this.parsePacketHeader = function(packetBuffer, cb) {
|
this.parsePacketHeader = function(packetBuffer, cb) {
|
||||||
assert(Buffer.isBuffer(packetBuffer));
|
assert(Buffer.isBuffer(packetBuffer));
|
||||||
|
|
||||||
//
|
|
||||||
// See the following specs:
|
|
||||||
// http://ftsc.org/docs/fts-0001.016
|
|
||||||
// http://ftsc.org/docs/fsc-0048.002
|
|
||||||
//
|
|
||||||
if(packetBuffer.length < FTN_PACKET_HEADER_SIZE) {
|
if(packetBuffer.length < FTN_PACKET_HEADER_SIZE) {
|
||||||
cb(new Error('Buffer too small'));
|
cb(new Error('Buffer too small'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start out reading as if this is a FSC-0048 2+ packet
|
||||||
|
//
|
||||||
binary.parse(packetBuffer)
|
binary.parse(packetBuffer)
|
||||||
.word16lu('origNode')
|
.word16lu('origNode')
|
||||||
.word16lu('destNode')
|
.word16lu('destNode')
|
||||||
|
@ -81,7 +77,7 @@ function FTNPacket() {
|
||||||
.word16lu('origNet')
|
.word16lu('origNet')
|
||||||
.word16lu('destNet')
|
.word16lu('destNet')
|
||||||
.word8('prodCodeLo')
|
.word8('prodCodeLo')
|
||||||
.word8('revisionMajor') // aka serialNo
|
.word8('prodRevLo') // aka serialNo
|
||||||
.buffer('password', 8) // null padded C style string
|
.buffer('password', 8) // null padded C style string
|
||||||
.word16lu('origZone')
|
.word16lu('origZone')
|
||||||
.word16lu('destZone')
|
.word16lu('destZone')
|
||||||
|
@ -89,9 +85,9 @@ function FTNPacket() {
|
||||||
.word16lu('auxNet')
|
.word16lu('auxNet')
|
||||||
.word16lu('capWordA')
|
.word16lu('capWordA')
|
||||||
.word8('prodCodeHi')
|
.word8('prodCodeHi')
|
||||||
.word8('revisionMinor')
|
.word8('prodRevHi')
|
||||||
.word16lu('capWordB')
|
.word16lu('capWordB')
|
||||||
.word16lu('originZone2')
|
.word16lu('origZone2')
|
||||||
.word16lu('destZone2')
|
.word16lu('destZone2')
|
||||||
.word16lu('originPoint')
|
.word16lu('originPoint')
|
||||||
.word16lu('destPoint')
|
.word16lu('destPoint')
|
||||||
|
@ -104,6 +100,30 @@ function FTNPacket() {
|
||||||
cb(new Error('Unsupported header type: ' + packetHeader.packetType));
|
cb(new Error('Unsupported header type: ' + packetHeader.packetType));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// What kind of packet do we really have here?
|
||||||
|
//
|
||||||
|
if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) {
|
||||||
|
packetHeader.packetVersion = '2.2';
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// See heuristics described in FSC-0048, "Receiving Type-2+ bundles"
|
||||||
|
//
|
||||||
|
const capWordASwapped =
|
||||||
|
((packetHeader.capWordA & 0xff) << 8) |
|
||||||
|
((packetHeader.capWordA >> 8) & 0xff);
|
||||||
|
|
||||||
|
if(capWordASwapped === packetHeader.capWordB &&
|
||||||
|
0 != packetHeader.capWordB &&
|
||||||
|
packetHeader.capWordB & 0x0001)
|
||||||
|
{
|
||||||
|
packetHeader.packetVersion = '2+';
|
||||||
|
} else {
|
||||||
|
packetHeader.packetVersion = '2';
|
||||||
|
packetHeader.point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Date/time components into something more reasonable
|
// Date/time components into something more reasonable
|
||||||
|
@ -131,7 +151,7 @@ function FTNPacket() {
|
||||||
buffer.writeUInt16LE(headerInfo.origNet, 20);
|
buffer.writeUInt16LE(headerInfo.origNet, 20);
|
||||||
buffer.writeUInt16LE(headerInfo.destNet, 22);
|
buffer.writeUInt16LE(headerInfo.destNet, 22);
|
||||||
buffer.writeUInt8(headerInfo.prodCodeLo, 24);
|
buffer.writeUInt8(headerInfo.prodCodeLo, 24);
|
||||||
buffer.writeUInt8(headerInfo.revisionMajor, 25);
|
buffer.writeUInt8(headerInfo.prodRevHi, 25);
|
||||||
|
|
||||||
const pass = ftn.stringToNullPaddedBuffer(headerInfo.password, 8);
|
const pass = ftn.stringToNullPaddedBuffer(headerInfo.password, 8);
|
||||||
pass.copy(buffer, 26);
|
pass.copy(buffer, 26);
|
||||||
|
@ -143,7 +163,7 @@ function FTNPacket() {
|
||||||
buffer.writeUInt16LE(headerInfo.auxNet, 38);
|
buffer.writeUInt16LE(headerInfo.auxNet, 38);
|
||||||
buffer.writeUInt16LE(headerInfo.capWordA, 40);
|
buffer.writeUInt16LE(headerInfo.capWordA, 40);
|
||||||
buffer.writeUInt8(headerInfo.prodCodeHi, 42);
|
buffer.writeUInt8(headerInfo.prodCodeHi, 42);
|
||||||
buffer.writeUInt8(headerInfo.revisionMinor, 43);
|
buffer.writeUInt8(headerInfo.prodRevLo, 43);
|
||||||
buffer.writeUInt16LE(headerInfo.capWordB, 44);
|
buffer.writeUInt16LE(headerInfo.capWordB, 44);
|
||||||
buffer.writeUInt16LE(headerInfo.origZone2, 46);
|
buffer.writeUInt16LE(headerInfo.origZone2, 46);
|
||||||
buffer.writeUInt16LE(headerInfo.destZone2, 48);
|
buffer.writeUInt16LE(headerInfo.destZone2, 48);
|
||||||
|
@ -207,13 +227,16 @@ function FTNPacket() {
|
||||||
// :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's
|
// :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's
|
||||||
// present, we need to extract it but keep the rest of hte message intact as it likely
|
// present, we need to extract it but keep the rest of hte message intact as it likely
|
||||||
// has SEEN-BY, PATH, and other kludge information *appended*
|
// has SEEN-BY, PATH, and other kludge information *appended*
|
||||||
const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER);
|
const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER);
|
||||||
if(sauceHeaderPosition > -1) {
|
if(sauceHeaderPosition > -1) {
|
||||||
sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition), (err, theSauce) => {
|
sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
// we read some SAUCE - don't re-process that portion into the body
|
// we read some SAUCE - don't re-process that portion into the body
|
||||||
messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition);
|
messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE);
|
||||||
|
// messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition);
|
||||||
messageBodyData.sauce = theSauce;
|
messageBodyData.sauce = theSauce;
|
||||||
|
} else {
|
||||||
|
console.log(err)
|
||||||
}
|
}
|
||||||
callback(null); // failure to read SAUCE is OK
|
callback(null); // failure to read SAUCE is OK
|
||||||
});
|
});
|
||||||
|
@ -349,6 +372,19 @@ function FTNPacket() {
|
||||||
msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine;
|
msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Update message UUID, if possible, based on MSGID and AREA
|
||||||
|
//
|
||||||
|
if(_.isString(msg.meta.FtnKludge.MSGID) &&
|
||||||
|
_.isString(msg.meta.FtnProperty.ftn_area) &&
|
||||||
|
msg.meta.FtnKludge.MSGID.length > 0 &&
|
||||||
|
msg.meta.FtnProperty.ftn_area.length > 0)
|
||||||
|
{
|
||||||
|
msg.uuid = ftn.createMessageUuid(
|
||||||
|
msg.meta.FtnKludge.MSGID,
|
||||||
|
msg.meta.FtnProperty.area);
|
||||||
|
}
|
||||||
|
|
||||||
iterator('message', msg);
|
iterator('message', msg);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@ -367,21 +403,6 @@ function FTNPacket() {
|
||||||
basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11);
|
basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11);
|
||||||
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12);
|
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12);
|
||||||
|
|
||||||
//
|
|
||||||
// From http://ftsc.org/docs/fts-0001.016:
|
|
||||||
// DateTime = (* a character string 20 characters long *)
|
|
||||||
// (* 01 Jan 86 02:34:56 *)
|
|
||||||
// DayOfMonth " " Month " " Year " "
|
|
||||||
// " " HH ":" MM ":" SS
|
|
||||||
// Null
|
|
||||||
//
|
|
||||||
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
|
||||||
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
|
||||||
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
|
|
||||||
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
|
|
||||||
// HH = "00" | .. | "23"
|
|
||||||
// MM = "00" | .. | "59"
|
|
||||||
// SS = "00" | .. | "59"
|
|
||||||
const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0');
|
const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0');
|
||||||
dateTimeBuffer.copy(basicHeader, 14);
|
dateTimeBuffer.copy(basicHeader, 14);
|
||||||
|
|
||||||
|
@ -423,12 +444,17 @@ function FTNPacket() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: is Area really any differnt (e.g. no space between AREA:the_area)
|
//
|
||||||
|
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
|
||||||
|
// AREA:CONFERENCE
|
||||||
|
// Should be first line in a message
|
||||||
|
//
|
||||||
if(message.meta.FtnProperty.ftn_area) {
|
if(message.meta.FtnProperty.ftn_area) {
|
||||||
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\n`;
|
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(message.meta.FtnKludge).forEach(k => {
|
Object.keys(message.meta.FtnKludge).forEach(k => {
|
||||||
|
// we want PATH to be last
|
||||||
if('PATH' !== k) {
|
if('PATH' !== k) {
|
||||||
appendMeta(k, message.meta.FtnKludge[k]);
|
appendMeta(k, message.meta.FtnKludge[k]);
|
||||||
}
|
}
|
||||||
|
@ -436,9 +462,21 @@ function FTNPacket() {
|
||||||
|
|
||||||
msgBody += message.message;
|
msgBody += message.message;
|
||||||
|
|
||||||
|
//
|
||||||
|
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
|
||||||
|
// Origin line should be near the bottom of a message
|
||||||
|
//
|
||||||
appendMeta('', message.meta.FtnProperty.ftn_tear_line);
|
appendMeta('', message.meta.FtnProperty.ftn_tear_line);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tear line should be near the bottom of a message
|
||||||
|
//
|
||||||
appendMeta('', message.meta.FtnProperty.ftn_origin);
|
appendMeta('', message.meta.FtnProperty.ftn_origin);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
|
||||||
|
// SEEN-BY and PATH should be the last lines of a message
|
||||||
|
//
|
||||||
appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by);
|
appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by);
|
||||||
appendMeta('PATH', message.meta.FtnKludge['PATH']);
|
appendMeta('PATH', message.meta.FtnKludge['PATH']);
|
||||||
|
|
||||||
|
@ -509,498 +547,6 @@ FTNPacket.prototype.write = function(path, headerInfo, messages, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// References
|
|
||||||
// * http://ftsc.org/docs/fts-0001.016
|
|
||||||
// * http://ftsc.org/docs/fsc-0048.002
|
|
||||||
//
|
|
||||||
// Other implementations:
|
|
||||||
// * https://github.com/M-griffin/PyPacketMail/blob/master/PyPacketMail.py
|
|
||||||
//
|
|
||||||
function FTNMailPacket(options) {
|
|
||||||
|
|
||||||
//MailPacket.call(this, options);
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
self.KLUDGE_PREFIX = '\x01';
|
|
||||||
|
|
||||||
this.getPacketHeaderAddress = function() {
|
|
||||||
return {
|
|
||||||
zone : self.packetHeader.destZone,
|
|
||||||
net : self.packetHeader.destNet,
|
|
||||||
node : self.packetHeader.destNode,
|
|
||||||
point : self.packetHeader.destPoint,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getNetworkNameForAddress = function(addr) {
|
|
||||||
var nodeAddr;
|
|
||||||
for(var network in self.nodeAddresses) {
|
|
||||||
nodeAddr = self.nodeAddresses[network];
|
|
||||||
if(nodeAddr.zone === addr.zone &&
|
|
||||||
nodeAddr.net === addr.net &&
|
|
||||||
nodeAddr.node === addr.node &&
|
|
||||||
nodeAddr.point === addr.point)
|
|
||||||
{
|
|
||||||
return network;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.parseFtnPacketHeader = function(packetBuffer, cb) {
|
|
||||||
assert(Buffer.isBuffer(packetBuffer));
|
|
||||||
|
|
||||||
if(packetBuffer.length < 58) {
|
|
||||||
cb(new Error('Buffer too small'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
binary.parse(packetBuffer)
|
|
||||||
.word16lu('origNode')
|
|
||||||
.word16lu('destNode')
|
|
||||||
.word16lu('year')
|
|
||||||
.word16lu('month')
|
|
||||||
.word16lu('day')
|
|
||||||
.word16lu('hour')
|
|
||||||
.word16lu('minute')
|
|
||||||
.word16lu('second')
|
|
||||||
.word16lu('baud')
|
|
||||||
.word16lu('packetType')
|
|
||||||
.word16lu('origNet')
|
|
||||||
.word16lu('destNet')
|
|
||||||
.word8('prodCodeLo')
|
|
||||||
.word8('revisionMajor') // aka serialNo
|
|
||||||
.buffer('password', 8) // null terminated C style string
|
|
||||||
.word16lu('origZone')
|
|
||||||
.word16lu('destZone')
|
|
||||||
// Additions in FSC-0048.002 follow...
|
|
||||||
.word16lu('auxNet')
|
|
||||||
.word16lu('capWordA')
|
|
||||||
.word8('prodCodeHi')
|
|
||||||
.word8('revisionMinor')
|
|
||||||
.word16lu('capWordB')
|
|
||||||
.word16lu('originZone2')
|
|
||||||
.word16lu('destZone2')
|
|
||||||
.word16lu('originPoint')
|
|
||||||
.word16lu('destPoint')
|
|
||||||
.word32lu('prodData')
|
|
||||||
.tap(function tapped(packetHeader) {
|
|
||||||
packetHeader.password = ftn.stringFromFTN(packetHeader.password);
|
|
||||||
|
|
||||||
// :TODO: Don't hard code magic # here
|
|
||||||
if(2 !== packetHeader.packetType) {
|
|
||||||
console.log(packetHeader.packetType)
|
|
||||||
cb(new Error('Packet is not Type-2'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// :TODO: convert date information -> .created
|
|
||||||
|
|
||||||
packetHeader.created = moment(packetHeader);
|
|
||||||
/*
|
|
||||||
packetHeader.year, packetHeader.month, packetHeader.day, packetHeader.hour,
|
|
||||||
packetHeader.minute, packetHeader.second);*/
|
|
||||||
|
|
||||||
// :TODO: validate & pass error if failure
|
|
||||||
cb(null, packetHeader);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getPacketHeaderBuffer = function(packetHeader, options) {
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
if(options.created) {
|
|
||||||
options.created = moment(options.created); // ensure we have a moment obj
|
|
||||||
} else {
|
|
||||||
options.created = moment();
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer = new Buffer(58);
|
|
||||||
|
|
||||||
buffer.writeUInt16LE(packetHeader.origNode, 0);
|
|
||||||
buffer.writeUInt16LE(packetHeader.destNode, 2);
|
|
||||||
buffer.writeUInt16LE(options.created.year(), 4);
|
|
||||||
buffer.writeUInt16LE(options.created.month(), 6);
|
|
||||||
buffer.writeUInt16LE(options.created.date(), 8);
|
|
||||||
buffer.writeUInt16LE(options.created.hour(), 10);
|
|
||||||
buffer.writeUInt16LE(options.created.minute(), 12);
|
|
||||||
buffer.writeUInt16LE(options.created.second(), 14);
|
|
||||||
buffer.writeUInt16LE(0x0000, 16);
|
|
||||||
buffer.writeUInt16LE(0x0002, 18);
|
|
||||||
buffer.writeUInt16LE(packetHeader.origNet, 20);
|
|
||||||
buffer.writeUInt16LE(packetHeader.destNet, 22);
|
|
||||||
buffer.writeUInt8(packetHeader.prodCodeLo, 24);
|
|
||||||
buffer.writeUInt8(packetHeader.revisionMajor, 25);
|
|
||||||
|
|
||||||
const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8);
|
|
||||||
pass.copy(buffer, 26);
|
|
||||||
|
|
||||||
buffer.writeUInt16LE(packetHeader.origZone, 34);
|
|
||||||
buffer.writeUInt16LE(packetHeader.destZone, 36);
|
|
||||||
|
|
||||||
// FSC-0048.002 additions...
|
|
||||||
buffer.writeUInt16LE(packetHeader.auxNet, 38);
|
|
||||||
buffer.writeUInt16LE(packetHeader.capWordA, 40);
|
|
||||||
buffer.writeUInt8(packetHeader.prodCodeHi, 42);
|
|
||||||
buffer.writeUInt8(packetHeader.revisionMinor, 43);
|
|
||||||
buffer.writeUInt16LE(packetHeader.capWordB, 44);
|
|
||||||
buffer.writeUInt16LE(packetHeader.origZone2, 46);
|
|
||||||
buffer.writeUInt16LE(packetHeader.destZone2, 48);
|
|
||||||
buffer.writeUInt16LE(packetHeader.origPoint, 50);
|
|
||||||
buffer.writeUInt16LE(packetHeader.destPoint, 52);
|
|
||||||
buffer.writeUInt32LE(packetHeader.prodData, 54);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.setOrAppend = function(value, dst) {
|
|
||||||
if(dst) {
|
|
||||||
if(!_.isArray(dst)) {
|
|
||||||
dst = [ dst ];
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.push(value);
|
|
||||||
} else {
|
|
||||||
dst = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
self.getMessageMeta = function(msgBody, msgData) {
|
|
||||||
var meta = {
|
|
||||||
FtnKludge : msgBody.kludgeLines,
|
|
||||||
FtnProperty : {},
|
|
||||||
};
|
|
||||||
|
|
||||||
if(msgBody.tearLine) {
|
|
||||||
meta.FtnProperty.ftn_tear_line = msgBody.tearLine;
|
|
||||||
}
|
|
||||||
if(msgBody.seenBy.length > 0) {
|
|
||||||
meta.FtnProperty.ftn_seen_by = msgBody.seenBy;
|
|
||||||
}
|
|
||||||
if(msgBody.area) {
|
|
||||||
meta.FtnProperty.ftn_area = msgBody.area;
|
|
||||||
}
|
|
||||||
if(msgBody.originLine) {
|
|
||||||
meta.FtnProperty.ftn_origin = msgBody.originLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
meta.FtnProperty.ftn_orig_node = msgData.origNode;
|
|
||||||
meta.FtnProperty.ftn_dest_node = msgData.destNode;
|
|
||||||
meta.FtnProperty.ftn_orig_network = msgData.origNet;
|
|
||||||
meta.FtnProperty.ftn_dest_network = msgData.destNet;
|
|
||||||
meta.FtnProperty.ftn_attr_flags1 = msgData.attrFlags1;
|
|
||||||
meta.FtnProperty.ftn_attr_flags2 = msgData.attrFlags2;
|
|
||||||
meta.FtnProperty.ftn_cost = msgData.cost;
|
|
||||||
|
|
||||||
return meta;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.parseFtnMessageBody = function(msgBodyBuffer, cb) {
|
|
||||||
//
|
|
||||||
// From FTS-0001.16:
|
|
||||||
// "Message text is unbounded and null terminated (note exception below).
|
|
||||||
//
|
|
||||||
// A 'hard' carriage return, 0DH, marks the end of a paragraph, and must
|
|
||||||
// be preserved.
|
|
||||||
//
|
|
||||||
// So called 'soft' carriage returns, 8DH, may mark a previous
|
|
||||||
// processor's automatic line wrap, and should be ignored. Beware that
|
|
||||||
// they may be followed by linefeeds, or may not.
|
|
||||||
//
|
|
||||||
// All linefeeds, 0AH, should be ignored. Systems which display message
|
|
||||||
// text should wrap long lines to suit their application."
|
|
||||||
//
|
|
||||||
// This is a bit tricky. Decoding the buffer to CP437 converts all 0x8d -> 0xec, so we'll
|
|
||||||
// have to replace those characters if the buffer is left as CP437.
|
|
||||||
// After decoding, we'll need to peek at the buffer for the various kludge lines
|
|
||||||
// for charsets & possibly re-decode. Uggh!
|
|
||||||
//
|
|
||||||
|
|
||||||
// :TODO: Use the proper encoding here. There appear to be multiple specs and/or
|
|
||||||
// stuff people do with this... some specs kludge lines, which is kinda durpy since
|
|
||||||
// to get to that point, one must read the file (and decode) to find said kludge...
|
|
||||||
|
|
||||||
|
|
||||||
//var msgLines = msgBodyBuffer.toString().split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
|
|
||||||
|
|
||||||
//var msgLines = iconv.decode(msgBodyBuffer, 'CP437').replace(/\xec/g, '').split(/\r\n|[\r\n]/g);
|
|
||||||
var msgLines = iconv.decode(msgBodyBuffer, 'CP437').replace(/[\xec\n]/g, '').split(/\r/g);
|
|
||||||
|
|
||||||
var msgBody = {
|
|
||||||
message : [],
|
|
||||||
kludgeLines : {}, // <KLUDGE> -> [ value1, value2, ... ]
|
|
||||||
seenBy : [],
|
|
||||||
};
|
|
||||||
|
|
||||||
var preOrigin = true;
|
|
||||||
|
|
||||||
function addKludgeLine(kl) {
|
|
||||||
const kludgeParts = kl.split(':');
|
|
||||||
kludgeParts[0] = kludgeParts[0].toUpperCase();
|
|
||||||
kludgeParts[1] = kludgeParts[1].trim();
|
|
||||||
|
|
||||||
self.setOrAppend(kludgeParts[1], msgBody.kludgeLines[kludgeParts[0]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sauceBuffers;
|
|
||||||
|
|
||||||
msgLines.forEach(function nextLine(line) {
|
|
||||||
if(0 === line.length) {
|
|
||||||
msgBody.message.push('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(preOrigin) {
|
|
||||||
if(_.startsWith(line, 'AREA:')) {
|
|
||||||
msgBody.area = line.substring(line.indexOf(':') + 1).trim();
|
|
||||||
} else if(_.startsWith(line, '--- ')) {
|
|
||||||
// Tag lines are tracked allowing for specialized display/etc.
|
|
||||||
msgBody.tearLine = line;
|
|
||||||
} else if(/[ ]{1,2}(\* )?Origin\: /.test(line)) { // To spec is " * Origin: ..."
|
|
||||||
msgBody.originLine = line;
|
|
||||||
preOrigin = false;
|
|
||||||
} else if(self.KLUDGE_PREFIX === line.charAt(0)) {
|
|
||||||
addKludgeLine(line.slice(1));
|
|
||||||
} else if(!sauceBuffers || _.startsWith(line, '\x1aSAUCE00')) {
|
|
||||||
sauceBuffers = sauceBuffers || buffers();
|
|
||||||
sauceBuffers.push(new Buffer(line));
|
|
||||||
} else {
|
|
||||||
msgBody.message.push(line);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(_.startsWith(line, 'SEEN-BY:')) {
|
|
||||||
msgBody.seenBy.push(line.substring(line.indexOf(':') + 1).trim());
|
|
||||||
} else if(self.KLUDGE_PREFIX === line.charAt(0)) {
|
|
||||||
addKludgeLine(line.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(sauceBuffers) {
|
|
||||||
// :TODO: parse sauce -> sauce buffer. This needs changes to this method to return message & optional sauce
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, msgBody);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.extractMessages = function(buffer, iterator, cb) {
|
|
||||||
assert(Buffer.isBuffer(buffer));
|
|
||||||
assert(_.isFunction(iterator));
|
|
||||||
|
|
||||||
const NULL_TERM_BUFFER = new Buffer( [ 0 ] );
|
|
||||||
|
|
||||||
binary.stream(buffer).loop(function looper(end, vars) {
|
|
||||||
this
|
|
||||||
.word16lu('messageType')
|
|
||||||
.word16lu('origNode')
|
|
||||||
.word16lu('destNode')
|
|
||||||
.word16lu('origNet')
|
|
||||||
.word16lu('destNet')
|
|
||||||
.word8('attrFlags1')
|
|
||||||
.word8('attrFlags2')
|
|
||||||
.word16lu('cost')
|
|
||||||
.scan('modDateTime', NULL_TERM_BUFFER)
|
|
||||||
.scan('toUserName', NULL_TERM_BUFFER)
|
|
||||||
.scan('fromUserName', NULL_TERM_BUFFER)
|
|
||||||
.scan('subject', NULL_TERM_BUFFER)
|
|
||||||
.scan('message', NULL_TERM_BUFFER)
|
|
||||||
.tap(function tapped(msgData) {
|
|
||||||
if(!msgData.origNode) {
|
|
||||||
end();
|
|
||||||
cb(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// buffer to string conversion
|
|
||||||
[ 'modDateTime', 'toUserName', 'fromUserName', 'subject', ].forEach(function field(f) {
|
|
||||||
msgData[f] = iconv.decode(msgData[f], 'CP437');
|
|
||||||
});
|
|
||||||
|
|
||||||
self.parseFtnMessageBody(msgData.message, function msgBodyParsed(err, msgBody) {
|
|
||||||
//
|
|
||||||
// Now, create a Message object
|
|
||||||
//
|
|
||||||
var msg = new Message( {
|
|
||||||
// AREA FTN -> local conf/area occurs elsewhere
|
|
||||||
toUserName : msgData.toUserName,
|
|
||||||
fromUserName : msgData.fromUserName,
|
|
||||||
subject : msgData.subject,
|
|
||||||
message : msgBody.message.join('\n'), // :TODO: \r\n is better?
|
|
||||||
modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime),
|
|
||||||
meta : self.getMessageMeta(msgBody, msgData),
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
iterator(msg);
|
|
||||||
//self.emit('message', msg); // :TODO: Placeholder
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//this.getMessageHeaderBuffer = function(headerInfo)
|
|
||||||
|
|
||||||
this.parseFtnMessages = function(buffer, cb) {
|
|
||||||
var nullTermBuf = new Buffer( [ 0 ] );
|
|
||||||
var fidoMessages = [];
|
|
||||||
|
|
||||||
binary.stream(buffer).loop(function looper(end, vars) {
|
|
||||||
this
|
|
||||||
.word16lu('messageType')
|
|
||||||
.word16lu('origNode')
|
|
||||||
.word16lu('destNode')
|
|
||||||
.word16lu('origNet')
|
|
||||||
.word16lu('destNet')
|
|
||||||
.word8('attrFlags1')
|
|
||||||
.word8('attrFlags2')
|
|
||||||
.word16lu('cost')
|
|
||||||
.scan('modDateTime', nullTermBuf)
|
|
||||||
.scan('toUserName', nullTermBuf)
|
|
||||||
.scan('fromUserName', nullTermBuf)
|
|
||||||
.scan('subject', nullTermBuf)
|
|
||||||
.scan('message', nullTermBuf)
|
|
||||||
.tap(function tapped(msgData) {
|
|
||||||
if(!msgData.origNode) {
|
|
||||||
end();
|
|
||||||
cb(null, fidoMessages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// buffer to string conversion
|
|
||||||
// :TODO: What is the real encoding here?
|
|
||||||
[ 'modDateTime', 'toUserName', 'fromUserName', 'subject', ].forEach(function field(f) {
|
|
||||||
msgData[f] = msgData[f].toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.parseFtnMessageBody(msgData.message, function msgBodyParsed(err, msgBody) {
|
|
||||||
msgData.message = msgBody;
|
|
||||||
fidoMessages.push(_.clone(msgData));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.extractMesssagesFromPacketBuffer = function(packetBuffer, iterator, cb) {
|
|
||||||
assert(Buffer.isBuffer(packetBuffer));
|
|
||||||
assert(_.isFunction(iterator));
|
|
||||||
|
|
||||||
async.waterfall(
|
|
||||||
[
|
|
||||||
function parseHeader(callback) {
|
|
||||||
self.parseFtnPacketHeader(packetBuffer, function headerParsed(err, packetHeader) {
|
|
||||||
self.packetHeader = packetHeader;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function validateDesinationAddress(callback) {
|
|
||||||
self.localNetworkName = self.getNetworkNameForAddress(self.getPacketHeaderAddress());
|
|
||||||
self.localNetworkName = 'AllowAnyNetworkForDebugging';
|
|
||||||
callback(self.localNetworkName ? null : new Error('Packet not addressed do this system'));
|
|
||||||
},
|
|
||||||
function extractEmbeddedMessages(callback) {
|
|
||||||
// note: packet header is 58 bytes in length
|
|
||||||
self.extractMessages(
|
|
||||||
packetBuffer.slice(58), iterator, function extracted(err) {
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
],
|
|
||||||
function complete(err) {
|
|
||||||
cb(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.loadMessagesFromPacketBuffer = function(packetBuffer, cb) {
|
|
||||||
async.waterfall(
|
|
||||||
[
|
|
||||||
function parseHeader(callback) {
|
|
||||||
self.parseFtnPacketHeader(packetBuffer, function headerParsed(err, packetHeader) {
|
|
||||||
self.packetHeader = packetHeader;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function validateDesinationAddress(callback) {
|
|
||||||
self.localNetworkName = self.getNetworkNameForAddress(self.getPacketHeaderAddress());
|
|
||||||
self.localNetworkName = 'AllowAnyNetworkForDebugging';
|
|
||||||
callback(self.localNetworkName ? null : new Error('Packet not addressed do this system'));
|
|
||||||
},
|
|
||||||
function parseMessages(callback) {
|
|
||||||
self.parseFtnMessages(packetBuffer.slice(58), function messagesParsed(err, fidoMessages) {
|
|
||||||
callback(err, fidoMessages);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function createMessageObjects(fidoMessages, callback) {
|
|
||||||
fidoMessages.forEach(function msg(fmsg) {
|
|
||||||
console.log(fmsg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
],
|
|
||||||
function complete(err) {
|
|
||||||
cb(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//require('util').inherits(FTNMailPacket, MailPacket);
|
|
||||||
|
|
||||||
FTNMailPacket.prototype.parse = function(path, cb) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
async.waterfall(
|
|
||||||
[
|
|
||||||
function readFromFile(callback) {
|
|
||||||
fs.readFile(path, function packetData(err, data) {
|
|
||||||
callback(err, data);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function extractMessages(data, callback) {
|
|
||||||
self.loadMessagesFromPacketBuffer(data, function extracted(err, messages) {
|
|
||||||
callback(err, messages);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
],
|
|
||||||
function complete(err, messages) {
|
|
||||||
cb(err, messages);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FTNMailPacket.prototype.read = function(pathOrBuffer, iterator, cb) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(_.isString(pathOrBuffer)) {
|
|
||||||
async.waterfall(
|
|
||||||
[
|
|
||||||
function readPacketFile(callback) {
|
|
||||||
fs.readFile(pathOrBuffer, function packetData(err, data) {
|
|
||||||
callback(err, data);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function extractMessages(data, callback) {
|
|
||||||
self.extractMesssagesFromPacketBuffer(data, iterator, callback);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
} else if(Buffer.isBuffer(pathOrBuffer)) {
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
FTNMailPacket.prototype.write = function(messages, fileName, options) {
|
|
||||||
if(!_.isArray(messages)) {
|
|
||||||
messages = [ messages ];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
var ftnPacket = new FTNPacket();
|
var ftnPacket = new FTNPacket();
|
||||||
var theHeader;
|
var theHeader;
|
||||||
var written = false;
|
var written = false;
|
||||||
|
@ -1023,67 +569,23 @@ ftnPacket.read(
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let address = {
|
||||||
|
zone : 46,
|
||||||
|
net : 1,
|
||||||
|
node : 232,
|
||||||
|
domain : 'l33t.codes',
|
||||||
|
};
|
||||||
|
msg.areaTag = 'agn_bbs';
|
||||||
|
msg.messageId = 1234;
|
||||||
|
console.log(ftn.getMessageIdentifier(msg, address));
|
||||||
|
console.log(ftn.getProductIdentifier())
|
||||||
|
//console.log(ftn.getOrigin(address))
|
||||||
|
console.log(ftn.parseAddress('46:1/232.4@l33t.codes'))
|
||||||
|
console.log(ftn.getUTCTimeZoneOffset())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function completion(err) {
|
function completion(err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
|
||||||
var mailPacket = new FTNMailPacket(
|
|
||||||
{
|
|
||||||
nodeAddresses : {
|
|
||||||
fidoNet : {
|
|
||||||
zone : 46,
|
|
||||||
net : 1,
|
|
||||||
node : 140,
|
|
||||||
point : 0,
|
|
||||||
domain : ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
var didWrite = false;
|
|
||||||
mailPacket.read(
|
|
||||||
process.argv[2],
|
|
||||||
//'/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/mf/extracted/27000425.pkt',
|
|
||||||
function packetIter(msg) {
|
|
||||||
console.log(msg);
|
|
||||||
if(_.has(msg, 'meta.FtnProperty.ftn_area')) {
|
|
||||||
console.log('AREA: ' + msg.meta.FtnProperty.ftn_area);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!didWrite) {
|
|
||||||
console.log(mailPacket.packetHeader);
|
|
||||||
console.log('-----------');
|
|
||||||
|
|
||||||
|
|
||||||
didWrite = true;
|
|
||||||
|
|
||||||
let outTest = fs.createWriteStream('/home/nuskooler/Downloads/ftnout/test1.pkt');
|
|
||||||
let buffer = mailPacket.getPacketHeaderBuffer(mailPacket.packetHeader);
|
|
||||||
//mailPacket.write(buffer, msg.packetHeader);
|
|
||||||
outTest.write(buffer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function complete(err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
Area Map
|
|
||||||
networkName: {
|
|
||||||
area_tag: conf_name:area_tag_name
|
|
||||||
...
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
mailPacket.parse('/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/BAD_BNDL.007', function parsed(err, messages) {
|
|
||||||
console.log(err)
|
|
||||||
});
|
|
||||||
*/
|
|
239
core/ftn_util.js
239
core/ftn_util.js
|
@ -11,16 +11,37 @@ var fs = require('fs');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var iconv = require('iconv-lite');
|
var iconv = require('iconv-lite');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
|
var createHash = require('crypto').createHash;
|
||||||
|
var uuid = require('node-uuid');
|
||||||
|
var os = require('os');
|
||||||
|
|
||||||
|
var packageJson = require('../package.json');
|
||||||
|
|
||||||
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
||||||
exports.stringFromFTN = stringFromFTN;
|
exports.stringFromFTN = stringFromFTN;
|
||||||
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
||||||
exports.getFormattedFTNAddress = getFormattedFTNAddress;
|
exports.createMessageUuid = createMessageUuid;
|
||||||
|
exports.parseAddress = parseAddress;
|
||||||
|
exports.formatAddress = formatAddress;
|
||||||
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
||||||
exports.getDateTimeString = getDateTimeString;
|
exports.getDateTimeString = getDateTimeString;
|
||||||
|
|
||||||
|
exports.getMessageIdentifier = getMessageIdentifier;
|
||||||
|
exports.getProductIdentifier = getProductIdentifier;
|
||||||
|
exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
|
||||||
|
exports.getOrigin = getOrigin;
|
||||||
|
|
||||||
exports.getQuotePrefix = getQuotePrefix;
|
exports.getQuotePrefix = getQuotePrefix;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Namespace for RFC-4122 name based UUIDs generated from
|
||||||
|
// FTN kludges MSGID + AREA
|
||||||
|
//
|
||||||
|
const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654');
|
||||||
|
|
||||||
|
// Up to 5D FTN address RegExp
|
||||||
|
const ENIGMA_FTN_ADDRESS_REGEXP = /^([0-9]+):([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i;
|
||||||
|
|
||||||
// See list here: https://github.com/Mithgol/node-fidonet-jam
|
// See list here: https://github.com/Mithgol/node-fidonet-jam
|
||||||
|
|
||||||
// :TODO: proably move this elsewhere as a general method
|
// :TODO: proably move this elsewhere as a general method
|
||||||
|
@ -48,6 +69,7 @@ function stringToNullPaddedBuffer(s, bufLen) {
|
||||||
//
|
//
|
||||||
// Convert a FTN style DateTime string to a Date object
|
// Convert a FTN style DateTime string to a Date object
|
||||||
//
|
//
|
||||||
|
// :TODO: Name the next couple methods better - for FTN *packets*
|
||||||
function getDateFromFtnDateTime(dateTime) {
|
function getDateFromFtnDateTime(dateTime) {
|
||||||
//
|
//
|
||||||
// Examples seen in the wild (Working):
|
// Examples seen in the wild (Working):
|
||||||
|
@ -63,10 +85,10 @@ function getDateTimeString(m) {
|
||||||
//
|
//
|
||||||
// From http://ftsc.org/docs/fts-0001.016:
|
// From http://ftsc.org/docs/fts-0001.016:
|
||||||
// DateTime = (* a character string 20 characters long *)
|
// DateTime = (* a character string 20 characters long *)
|
||||||
// (* 01 Jan 86 02:34:56 *)
|
// (* 01 Jan 86 02:34:56 *)
|
||||||
// DayOfMonth " " Month " " Year " "
|
// DayOfMonth " " Month " " Year " "
|
||||||
// " " HH ":" MM ":" SS
|
// " " HH ":" MM ":" SS
|
||||||
// Null
|
// Null
|
||||||
//
|
//
|
||||||
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
||||||
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
||||||
|
@ -83,42 +105,172 @@ function getDateTimeString(m) {
|
||||||
return m.format('DD MMM YY HH:mm:ss');
|
return m.format('DD MMM YY HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFormattedFTNAddress(address, dimensions) {
|
function createMessageUuid(ftnMsgId, ftnArea) {
|
||||||
//var addr = util.format('%d:%d', address.zone, address.net);
|
//
|
||||||
var addr = '{0}:{1}'.format(address.zone, address.net);
|
// v5 UUID generation code based on the work here:
|
||||||
switch(dimensions) {
|
// https://github.com/download13/uuidv5/blob/master/uuid.js
|
||||||
case 2 :
|
//
|
||||||
case '2D' :
|
// Note: CrashMail uses MSGID + AREA, so we go with that as well:
|
||||||
// above
|
// https://github.com/larsks/crashmail/blob/master/crashmail/dupe.c
|
||||||
break;
|
//
|
||||||
|
if(!Buffer.isBuffer(ftnMsgId)) {
|
||||||
|
ftnMsgId = iconv.encode(ftnMsgId, 'CP437');
|
||||||
|
}
|
||||||
|
|
||||||
case 3 :
|
ftnArea = ftnArea || ''; // AREA is optional
|
||||||
case '3D' :
|
if(!Buffer.isBuffer(ftnArea)) {
|
||||||
addr += '/{0}'.format(address.node);
|
ftnArea = iconv.encode(ftnArea, 'CP437');
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
const ns = new Buffer(ENIGMA_FTN_MSGID_NAMESPACE);
|
||||||
|
|
||||||
case 4 :
|
let digest = createHash('sha1').update(
|
||||||
case '4D':
|
Buffer.concat([ ns, ftnMsgId, ftnArea ])).digest();
|
||||||
addr += '.{0}'.format(address.point || 0); // missing and 0 are equiv for point
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 5 :
|
let u = new Buffer(16);
|
||||||
case '5D' :
|
|
||||||
if(address.domain) {
|
// bbbb - bb - bb - bb - bbbbbb
|
||||||
addr += '@{0}'.format(address.domain);
|
digest.copy(u, 0, 0, 4); // time_low
|
||||||
}
|
digest.copy(u, 4, 4, 6); // time_mid
|
||||||
break;
|
digest.copy(u, 6, 6, 8); // time_hi_and_version
|
||||||
|
|
||||||
|
u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101)
|
||||||
|
u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10
|
||||||
|
u[9] = digest[9];
|
||||||
|
|
||||||
|
digest.copy(u, 10, 10, 16);
|
||||||
|
|
||||||
|
return uuid.unparse(u); // to string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAddress(address) {
|
||||||
|
const m = ENIGMA_FTN_ADDRESS_REGEXP.exec(address);
|
||||||
|
|
||||||
|
if(m) {
|
||||||
|
let addr = {
|
||||||
|
zone : parseInt(m[1]),
|
||||||
|
net : parseInt(m[2]),
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// substr(1) on the following to remove the
|
||||||
|
// captured prefix
|
||||||
|
//
|
||||||
|
if(m[3]) {
|
||||||
|
addr.node = parseInt(m[3].substr(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m[4]) {
|
||||||
|
addr.point = parseInt(m[4].substr(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m[5]) {
|
||||||
|
addr.domain = m[5].substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAddress(address, dimensions) {
|
||||||
|
let addr = `${address.zone}:${address.net}`;
|
||||||
|
|
||||||
|
// allow for e.g. '4D' or 5
|
||||||
|
const dim = parseInt(dimensions.toString()[0]);
|
||||||
|
|
||||||
|
if(dim >= 3) {
|
||||||
|
addr += `/${address.node}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing & .0 are equiv for point
|
||||||
|
if(dim >= 4 && address.point) {
|
||||||
|
addr += `.${addresss.point}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(5 === dim && address.domain) {
|
||||||
|
addr += `@${address.domain.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFtnMessageSerialNumber(messageId) {
|
function getMessageSerialNumber(message) {
|
||||||
return ((Math.floor((Date.now() - Date.UTC(2015, 1, 1)) / 1000) + messageId)).toString(16);
|
return ('00000000' + ((Math.floor((Date.now() - Date.UTC(2016, 1, 1)) / 1000) +
|
||||||
|
message.messageId)).toString(16)).substr(-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return a FTS-0009.001 compliant MSGID value given a message
|
||||||
|
// See http://ftsc.org/docs/fts-0009.001
|
||||||
|
//
|
||||||
|
// "A MSGID line consists of the string "^AMSGID:" (where ^A is a
|
||||||
|
// control-A (hex 01) and the double-quotes are not part of the
|
||||||
|
// string), followed by a space, the address of the originating
|
||||||
|
// system, and a serial number unique to that message on the
|
||||||
|
// originating system, i.e.:
|
||||||
|
//
|
||||||
|
// ^AMSGID: origaddr serialno
|
||||||
|
//
|
||||||
|
// The originating address should be specified in a form that
|
||||||
|
// constitutes a valid return address for the originating network.
|
||||||
|
// If the originating address is enclosed in double-quotes, the
|
||||||
|
// entire string between the beginning and ending double-quotes is
|
||||||
|
// considered to be the orginating address. A double-quote character
|
||||||
|
// within a quoted address is represented by by two consecutive
|
||||||
|
// double-quote characters. The serial number may be any eight
|
||||||
|
// character hexadecimal number, as long as it is unique - no two
|
||||||
|
// messages from a given system may have the same serial number
|
||||||
|
// within a three years. The manner in which this serial number is
|
||||||
|
// generated is left to the implementor."
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Examples & Implementations
|
||||||
|
//
|
||||||
|
// Synchronet: <msgNum>.<conf+area>@<ftnAddr> <serial>
|
||||||
|
// 2606.agora-agn_tst@46:1/142 19609217
|
||||||
|
//
|
||||||
|
// Mystic: <ftnAddress> <serial>
|
||||||
|
// 46:3/102 46686263
|
||||||
|
//
|
||||||
|
// ENiGMA½: <messageId>.<areaTag>@<5dFtnAddress> <serial>
|
||||||
|
//
|
||||||
|
function getMessageIdentifier(message, address) {
|
||||||
|
return `${message.messageId}.${message.areaTag.toLowerCase()}@${formatAddress(address, '5D')} ${getMessageSerialNumber(message)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return a FSC-0046.005 Product Identifier or "PID"
|
||||||
|
// http://ftsc.org/docs/fsc-0046.005
|
||||||
|
//
|
||||||
|
function getProductIdentifier() {
|
||||||
|
const version = packageJson.version
|
||||||
|
.replace(/\-/g, '.')
|
||||||
|
.replace(/alpha/,'a')
|
||||||
|
.replace(/beta/,'b');
|
||||||
|
|
||||||
|
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||||
|
|
||||||
|
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return a FSC-0030.001 compliant (http://ftsc.org/docs/fsc-0030.001) MESSAGE-ID
|
||||||
|
//
|
||||||
|
// <unique-part@domain-name>
|
||||||
|
//
|
||||||
|
// :TODO: not implemented to spec at all yet :)
|
||||||
function getFTNMessageID(messageId, areaId) {
|
function getFTNMessageID(messageId, areaId) {
|
||||||
return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getFTNMessageSerialNumber(messageId)
|
return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getMessageSerialNumber(messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return a FRL-1004 style time zone offset for a
|
||||||
|
// 'TZUTC' kludge line
|
||||||
|
//
|
||||||
|
// http://ftsc.org/docs/frl-1004.002
|
||||||
|
//
|
||||||
|
function getUTCTimeZoneOffset() {
|
||||||
|
return moment().format('ZZ').replace(/\+/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a FSC-0032 style quote prefixes
|
// Get a FSC-0032 style quote prefixes
|
||||||
|
@ -127,25 +279,14 @@ function getQuotePrefix(name) {
|
||||||
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
|
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Specs:
|
// Return a FTS-0004 Origin line
|
||||||
// * http://ftsc.org/docs/fts-0009.001
|
// http://ftsc.org/docs/fts-0004.001
|
||||||
// *
|
//
|
||||||
//
|
function getOrigin(address) {
|
||||||
function getFtnMsgIdKludgeLine(origAddress, messageId) {
|
const origin = _.has(Config.messageNetworks.originName) ?
|
||||||
if(_.isObject(origAddress)) {
|
Config.messageNetworks.originName :
|
||||||
origAddress = getFormattedFTNAddress(origAddress, '5D');
|
Config.general.boardName;
|
||||||
}
|
|
||||||
|
|
||||||
return '\x01MSGID: ' + origAddress + ' ' + getFtnMessageSerialNumber(messageId);
|
return ` * Origin: ${origin} (${formatAddress(address, '5D')})`;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getFTNOriginLine() {
|
|
||||||
//
|
|
||||||
// Specs:
|
|
||||||
// http://ftsc.org/docs/fts-0004.001
|
|
||||||
//
|
|
||||||
return ' * Origin: ' + Config.general.boardName + '(' + getFidoNetAddress() + ')';
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ function Message(options) {
|
||||||
|
|
||||||
this.messageId = options.messageId || 0; // always generated @ persist
|
this.messageId = options.messageId || 0; // always generated @ persist
|
||||||
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
|
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
|
||||||
this.uuid = uuid.v1();
|
this.uuid = options.uuid || uuid.v1();
|
||||||
this.replyToMsgId = options.replyToMsgId || 0;
|
this.replyToMsgId = options.replyToMsgId || 0;
|
||||||
this.toUserName = options.toUserName || '';
|
this.toUserName = options.toUserName || '';
|
||||||
this.fromUserName = options.fromUserName || '';
|
this.fromUserName = options.fromUserName || '';
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
/* jslint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// ENiGMA½
|
|
||||||
var PluginModule = require('./plugin_module.js').PluginModule;
|
|
||||||
|
|
||||||
exports.MessageNetworkModule = MessageNetworkModule;
|
|
||||||
|
|
||||||
function MessageNetworkModule() {
|
|
||||||
PluginModule.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
require('util').inherits(MessageNetworkModule, PluginModule);
|
|
||||||
|
|
||||||
MessageNetworkModule.prototype.startup = function(cb) {
|
|
||||||
cb(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
MessageNetworkModule.prototype.shutdown = function(cb) {
|
|
||||||
cb(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
MessageNetworkModule.prototype.record = function(message, cb) {
|
|
||||||
cb(null);
|
|
||||||
};
|
|
|
@ -1,27 +0,0 @@
|
||||||
/* jslint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// ENiGMA½
|
|
||||||
var MessageNetworkModule = require('./msg_network_module.js').MessageNetworkModule;
|
|
||||||
|
|
||||||
function FTNMessageNetworkModule() {
|
|
||||||
MessageNetworkModule.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
require('util').inherits(FTNMessageNetworkModule, MessageNetworkModule);
|
|
||||||
|
|
||||||
FTNMessageNetworkModule.prototype.startup = function(cb) {
|
|
||||||
cb(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
FTNMessageNetworkModule.prototype.shutdown = function(cb) {
|
|
||||||
cb(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
FTNMessageNetworkModule.prototype.record = function(message, cb) {
|
|
||||||
cb(null);
|
|
||||||
|
|
||||||
// :TODO: should perhaps record in batches - e.g. start an event, record
|
|
||||||
// to temp location until time is hit or N achieved such that if multiple
|
|
||||||
// messages are being created a .FTN file is not made for each one
|
|
||||||
};
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
var PluginModule = require('./plugin_module.js').PluginModule;
|
||||||
|
|
||||||
|
exports.MessageScanTossModule = MessageScanTossModule;
|
||||||
|
|
||||||
|
function MessageScanTossModule() {
|
||||||
|
PluginModule.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
require('util').inherits(MessageScanTossModule, PluginModule);
|
||||||
|
|
||||||
|
MessageScanTossModule.prototype.startup = function(cb) {
|
||||||
|
cb(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageScanTossModule.prototype.shutdown = function(cb) {
|
||||||
|
cb(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageScanTossModule.prototype.record = function(message, cb) {
|
||||||
|
cb(null);
|
||||||
|
};
|
|
@ -6,11 +6,11 @@ var iconv = require('iconv-lite');
|
||||||
|
|
||||||
exports.readSAUCE = readSAUCE;
|
exports.readSAUCE = readSAUCE;
|
||||||
|
|
||||||
|
|
||||||
const SAUCE_SIZE = 128;
|
const SAUCE_SIZE = 128;
|
||||||
const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
|
const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
|
||||||
const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
||||||
|
|
||||||
|
exports.SAUCE_SIZE = SAUCE_SIZE;
|
||||||
// :TODO: SAUCE should be a class
|
// :TODO: SAUCE should be a class
|
||||||
// - with getFontName()
|
// - with getFontName()
|
||||||
// - ...other methods
|
// - ...other methods
|
||||||
|
@ -19,6 +19,8 @@ const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
||||||
// See
|
// See
|
||||||
// http://www.acid.org/info/sauce/sauce.htm
|
// http://www.acid.org/info/sauce/sauce.htm
|
||||||
//
|
//
|
||||||
|
const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ];
|
||||||
|
|
||||||
function readSAUCE(data, cb) {
|
function readSAUCE(data, cb) {
|
||||||
if(data.length < SAUCE_SIZE) {
|
if(data.length < SAUCE_SIZE) {
|
||||||
cb(new Error('No SAUCE record present'));
|
cb(new Error('No SAUCE record present'));
|
||||||
|
@ -59,6 +61,11 @@ function readSAUCE(data, cb) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) {
|
||||||
|
cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var sauce = {
|
var sauce = {
|
||||||
id : iconv.decode(vars.id, 'cp437'),
|
id : iconv.decode(vars.id, 'cp437'),
|
||||||
version : iconv.decode(vars.version, 'cp437').trim(),
|
version : iconv.decode(vars.version, 'cp437').trim(),
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
var MessageScanTossModule = require('../scan_toss_module.js').MessageScanTossModule;
|
||||||
|
var Config = require('../config.js').config;
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'FTN',
|
||||||
|
desc : 'FidoNet Style Message Scanner/Tosser',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getModule = FTNMessageScanTossModule;
|
||||||
|
|
||||||
|
function FTNMessageScanTossModule() {
|
||||||
|
MessageScanTossModule.call(this);
|
||||||
|
|
||||||
|
this.config = Config.scannerTossers.ftn_bso;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule);
|
||||||
|
|
||||||
|
FTNMessageScanTossModule.prototype.startup = function(cb) {
|
||||||
|
cb(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
FTNMessageScanTossModule.prototype.shutdown = function(cb) {
|
||||||
|
cb(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
FTNMessageScanTossModule.prototype.record = function(message, cb) {
|
||||||
|
|
||||||
|
|
||||||
|
cb(null);
|
||||||
|
|
||||||
|
// :TODO: should perhaps record in batches - e.g. start an event, record
|
||||||
|
// to temp location until time is hit or N achieved such that if multiple
|
||||||
|
// messages are being created a .FTN file is not made for each one
|
||||||
|
};
|
|
@ -523,7 +523,8 @@ function displayThemedPause(options, cb) {
|
||||||
if(options.clearPrompt) {
|
if(options.clearPrompt) {
|
||||||
if(artInfo.startRow && artInfo.height) {
|
if(artInfo.startRow && artInfo.height) {
|
||||||
options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
||||||
// :TODO: This will not work with NetRunner:
|
|
||||||
|
// Note: Does not work properly in NetRunner < 2.0b17:
|
||||||
options.client.term.rawWrite(ansi.deleteLine(artInfo.height));
|
options.client.term.rawWrite(ansi.deleteLine(artInfo.height));
|
||||||
} else {
|
} else {
|
||||||
options.client.term.rawWrite(ansi.eraseLine(1))
|
options.client.term.rawWrite(ansi.eraseLine(1))
|
||||||
|
|
Loading…
Reference in New Issue