Major commit for new message network WIP

This commit is contained in:
Bryan Ashby 2016-02-02 21:35:59 -07:00
parent 6750c05f07
commit 317af8419a
40 changed files with 1747 additions and 599 deletions

View File

@ -65,7 +65,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
## License
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
Copyright (c) 2015, Bryan D. Ashby
Copyright (c) 2015-2016, Bryan D. Ashby
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -804,16 +804,13 @@ module.exports = (function() {
return !isNaN(value) && user.getAge() >= value;
},
AS : function accountStatus() {
if(_.isNumber(value)) {
if(!_.isArray(value)) {
value = [ value ];
}
assert(_.isArray(value));
return _.findIndex(value, function cmp(accStatus) {
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
}) > -1;
const userAccountStatus = parseInt(user.properties.account_status, 10);
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(userAccountStatus) > -1;
},
EC : function isEncoding() {
switch(value) {
@ -842,7 +839,7 @@ module.exports = (function() {
// :TODO: implement me!!
return false;
},
SC : function isSecerConnection() {
SC : function isSecureConnection() {
return client.session.isSecure;
},
ML : function minutesLeft() {
@ -870,16 +867,20 @@ module.exports = (function() {
return !isNaN(value) && client.term.termWidth >= value;
},
ID : function isUserId(value) {
return user.userId === value;
if(!_.isArray(value)) {
value = [ value ];
}
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(user.userId) > -1;
},
WD : function isOneOfDayOfWeek() {
// :TODO: return true if DoW
if(_.isNumber(value)) {
} else if(_.isArray(value)) {
if(!_.isArray(value)) {
value = [ value ];
}
return false;
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(new Date().getDay()) > -1;
},
MM : function isMinutesPastMidnight() {
// :TODO: return true if value is >= minutes past midnight sys time

View File

@ -7,8 +7,13 @@ var acsParser = require('./acs_parser.js');
var _ = require('lodash');
var assert = require('assert');
exports.checkAcs = checkAcs;
exports.getConditionalValue = getConditionalValue;
function checkAcs(client, acsString) {
return acsParser.parse(acsString, { client : client } );
}
function getConditionalValue(client, condArray, memberName) {
assert(_.isObject(client));
assert(_.isArray(condArray));

View File

@ -12,6 +12,7 @@ var events = require('events');
var util = require('util');
var ansi = require('./ansi_term.js');
var aep = require('./ansi_escape_parser.js');
var sauce = require('./sauce.js');
var _ = require('lodash');
@ -20,10 +21,6 @@ exports.getArtFromPath = getArtFromPath;
exports.display = display;
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
var SAUCE_SIZE = 128;
var SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
var COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
// :TODO: Return MCI code information
// :TODO: process SAUCE comments
// :TODO: return font + font mapped information from SAUCE
@ -43,156 +40,6 @@ var SUPPORTED_ART_TYPES = {
// :TODO: extension for topaz ansi/ascii.
};
//
// See
// http://www.acid.org/info/sauce/sauce.htm
//
// :TODO: Move all SAUCE stuff to sauce.js
function readSAUCE(data, cb) {
if(data.length < SAUCE_SIZE) {
cb(new Error('No SAUCE record present'));
return;
}
var offset = data.length - SAUCE_SIZE;
var sauceRec = data.slice(offset);
binary.parse(sauceRec)
.buffer('id', 5)
.buffer('version', 2)
.buffer('title', 35)
.buffer('author', 20)
.buffer('group', 20)
.buffer('date', 8)
.word32lu('fileSize')
.word8('dataType')
.word8('fileType')
.word16lu('tinfo1')
.word16lu('tinfo2')
.word16lu('tinfo3')
.word16lu('tinfo4')
.word8('numComments')
.word8('flags')
.buffer('tinfos', 22) // SAUCE 00.5
.tap(function onVars(vars) {
if(!SAUCE_ID.equals(vars.id)) {
cb(new Error('No SAUCE record present'));
return;
}
var ver = iconv.decode(vars.version, 'cp437');
if('00' !== ver) {
cb(new Error('Unsupported SAUCE version: ' + ver));
return;
}
var sauce = {
id : iconv.decode(vars.id, 'cp437'),
version : iconv.decode(vars.version, 'cp437').trim(),
title : iconv.decode(vars.title, 'cp437').trim(),
author : iconv.decode(vars.author, 'cp437').trim(),
group : iconv.decode(vars.group, 'cp437').trim(),
date : iconv.decode(vars.date, 'cp437').trim(),
fileSize : vars.fileSize,
dataType : vars.dataType,
fileType : vars.fileType,
tinfo1 : vars.tinfo1,
tinfo2 : vars.tinfo2,
tinfo3 : vars.tinfo3,
tinfo4 : vars.tinfo4,
numComments : vars.numComments,
flags : vars.flags,
tinfos : vars.tinfos,
};
var dt = SAUCE_DATA_TYPES[sauce.dataType];
if(dt && dt.parser) {
sauce[dt.name] = dt.parser(sauce);
}
cb(null, sauce);
});
}
// :TODO: These need completed:
var SAUCE_DATA_TYPES = {};
SAUCE_DATA_TYPES[0] = { name : 'None' };
SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE };
SAUCE_DATA_TYPES[2] = 'Bitmap';
SAUCE_DATA_TYPES[3] = 'Vector';
SAUCE_DATA_TYPES[4] = 'Audio';
SAUCE_DATA_TYPES[5] = 'BinaryText';
SAUCE_DATA_TYPES[6] = 'XBin';
SAUCE_DATA_TYPES[7] = 'Archive';
SAUCE_DATA_TYPES[8] = 'Executable';
var SAUCE_CHARACTER_FILE_TYPES = {};
SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII';
SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi';
SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation';
SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script';
SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard';
SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar';
SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML';
SAUCE_CHARACTER_FILE_TYPES[7] = 'Source';
SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw';
//
// Map of SAUCE font -> encoding hint
//
// Note that this is the same mapping that x84 uses. Be compatible!
//
var SAUCE_FONT_TO_ENCODING_HINT = {
'Amiga MicroKnight' : 'amiga',
'Amiga MicroKnight+' : 'amiga',
'Amiga mOsOul' : 'amiga',
'Amiga P0T-NOoDLE' : 'amiga',
'Amiga Topaz 1' : 'amiga',
'Amiga Topaz 1+' : 'amiga',
'Amiga Topaz 2' : 'amiga',
'Amiga Topaz 2+' : 'amiga',
'Atari ATASCII' : 'atari',
'IBM EGA43' : 'cp437',
'IBM EGA' : 'cp437',
'IBM VGA25G' : 'cp437',
'IBM VGA50' : 'cp437',
'IBM VGA' : 'cp437',
};
['437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) {
var codec = 'cp' + page;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
});
function parseCharacterSAUCE(sauce) {
var result = {};
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
// convience: create ansiFlags
sauce.ansiFlags = sauce.flags;
var i = 0;
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
++i;
}
var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
if(fontName.length > 0) {
result.fontName = fontName;
}
}
return result;
}
function getFontNameFromSAUCE(sauce) {
if(sauce.Character) {
return sauce.Character.fontName;
@ -249,7 +96,7 @@ function getArtFromPath(path, options, cb) {
}
if(options.readSauce === true) {
readSAUCE(data, function onSauce(err, sauce) {
sauce.readSAUCE(data, function onSauce(err, sauce) {
if(err) {
cb(null, getResult());
} else {

View File

@ -2,7 +2,6 @@
'use strict';
var Config = require('./config.js').config;
var theme = require('./theme.js');
var _ = require('lodash');
var assert = require('assert');

View File

@ -1,6 +1,9 @@
/* jslint node: true */
'use strict';
//var SegfaultHandler = require('segfault-handler');
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
// ENiGMA½
var conf = require('./config.js');
var logger = require('./logger.js');
@ -21,7 +24,7 @@ function bbsMain() {
async.waterfall(
[
function processArgs(callback) {
var args = parseArgs();
const args = parseArgs();
var configPath;
@ -37,8 +40,7 @@ function bbsMain() {
}
}
var configPathSupplied = _.isString(configPath);
callback(null, configPath || conf.getDefaultPath(), configPathSupplied);
callback(null, configPath || conf.getDefaultPath(), _.isString(configPath));
},
function initConfig(configPath, configPathSupplied, callback) {
conf.init(configPath, function configInit(err) {
@ -117,7 +119,7 @@ function initialize(cb) {
process.exit();
});
// Init some extensions
require('string-format').extend(String.prototype, require('./string_util.js').stringFormatExtensions);

View File

@ -8,10 +8,37 @@ var paths = require('path');
var async = require('async');
var _ = require('lodash');
var hjson = require('hjson');
var assert = require('assert');
exports.init = init;
exports.getDefaultPath = getDefaultPath;
function hasMessageConferenceAndArea(config) {
assert(_.isObject(config.messageConferences)); // we create one ourself!
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
return 'system_internal' !== confTag;
});
if(0 === nonInternalConfs.length) {
return false;
}
// :TODO: there is likely a better/cleaner way of doing this
var result = false;
_.forEach(nonInternalConfs, confTag => {
if(_.has(config.messageConferences[confTag], 'areas') &&
Object.keys(config.messageConferences[confTag].areas) > 0)
{
result = true;
return false; // stop iteration
}
});
return result;
}
function init(configPath, cb) {
async.waterfall(
[
@ -48,18 +75,13 @@ function init(configPath, cb) {
//
// Various sections must now exist in config
//
if(!_.has(mergedConfig, 'messages.areas.') ||
!_.isArray(mergedConfig.messages.areas) ||
0 === mergedConfig.messages.areas.length ||
!_.isString(mergedConfig.messages.areas[0].name))
{
var msgAreasErr = new Error('Please create at least one message area');
if(hasMessageConferenceAndArea(mergedConfig)) {
var msgAreasErr = new Error('Please create at least one message conference and area!');
msgAreasErr.code = 'EBADCONFIG';
callback(msgAreasErr);
return;
}
callback(null, mergedConfig);
} else {
callback(null, mergedConfig);
}
}
],
function complete(err, mergedConfig) {
@ -150,6 +172,7 @@ function getDefaultConfig() {
paths : {
mods : paths.join(__dirname, './../mods/'),
servers : paths.join(__dirname, './servers/'),
msgNetworks : paths.join(__dirname, './msg_networks/'),
art : paths.join(__dirname, './../mods/art/'),
themes : paths.join(__dirname, './../mods/themes/'),
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
@ -183,6 +206,25 @@ function getDefaultConfig() {
}
},
messageConferences : {
system_internal : {
name : 'System Internal',
desc : 'Built in conference for private messages, bulletins, etc.',
areas : {
private_mail : {
name : 'Private Mail',
desc : 'Private user to user mail/email',
},
local_bulletin : {
name : 'System Bulletins',
desc : 'Bulletin messages for all users',
}
}
}
},
messages : {
areas : [
{ name : 'private_mail', desc : 'Private Email', groups : [ 'users' ] }

View File

@ -132,7 +132,7 @@ function createMessageBaseTables() {
dbs.message.run(
'CREATE TABLE IF NOT EXISTS message (' +
' message_id INTEGER PRIMARY KEY,' +
' area_name VARCHAR NOT NULL,' +
' area_tag VARCHAR NOT NULL,' +
' message_uuid VARCHAR(36) NOT NULL,' +
' reply_to_message_id INTEGER,' +
' to_user_name VARCHAR NOT NULL,' +
@ -198,9 +198,9 @@ function createMessageBaseTables() {
dbs.message.run(
'CREATE TABLE IF NOT EXISTS user_message_area_last_read (' +
' user_id INTEGER NOT NULL,' +
' area_name VARCHAR NOT NULL,' +
' area_tag VARCHAR NOT NULL,' +
' message_id INTEGER NOT NULL,' +
' UNIQUE(user_id, area_name)' +
' UNIQUE(user_id, area_tag)' +
');'
);

View File

@ -7,7 +7,7 @@ var ansi = require('../core/ansi_term.js');
var theme = require('../core/theme.js');
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
var Message = require('../core/message.js');
var getMessageAreaByName = require('../core/message_area.js').getMessageAreaByName;
var getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
@ -75,6 +75,8 @@ var MCICodeIds = {
HashTags : 9,
MessageID : 10,
ReplyToMsgID : 11,
// :TODO: ConfName
},
@ -104,15 +106,15 @@ function FullScreenEditorModule(options) {
// editorMode : view | edit | quote
//
// menuConfig.config or extraArgs
// messageAreaName
// messageAreaTag
// messageIndex / messageTotal
// toUserId
//
this.editorType = config.editorType;
this.editorMode = config.editorMode;
if(config.messageAreaName) {
this.messageAreaName = config.messageAreaName;
if(config.messageAreaTag) {
this.messageAreaTag = config.messageAreaTag;
}
this.messageIndex = config.messageIndex || 0;
@ -121,8 +123,8 @@ function FullScreenEditorModule(options) {
// extraArgs can override some config
if(_.isObject(options.extraArgs)) {
if(options.extraArgs.messageAreaName) {
this.messageAreaName = options.extraArgs.messageAreaName;
if(options.extraArgs.messageAreaTag) {
this.messageAreaTag = options.extraArgs.messageAreaTag;
}
if(options.extraArgs.messageIndex) {
this.messageIndex = options.extraArgs.messageIndex;
@ -134,9 +136,6 @@ function FullScreenEditorModule(options) {
this.toUserId = options.extraArgs.toUserId;
}
}
console.log(this.toUserId)
console.log(this.messageAreaName)
this.isReady = false;
@ -149,7 +148,7 @@ function FullScreenEditorModule(options) {
};
this.isLocalEmail = function() {
return Message.WellKnownAreaNames.Private === self.messageAreaName;
return Message.WellKnownAreaTags.Private === self.messageAreaTag;
};
this.isReply = function() {
@ -217,7 +216,7 @@ function FullScreenEditorModule(options) {
var headerValues = self.viewControllers.header.getFormData().value;
var msgOpts = {
areaName : self.messageAreaName,
areaTag : self.messageAreaTag,
toUserName : headerValues.to,
fromUserName : headerValues.from,
subject : headerValues.subject,
@ -235,7 +234,7 @@ function FullScreenEditorModule(options) {
self.message = message;
updateMessageAreaLastReadId(
self.client.user.userId, self.messageAreaName, self.message.messageId,
self.client.user.userId, self.messageAreaTag, self.message.messageId,
function lastReadUpdated() {
if(self.isReady) {
@ -631,7 +630,7 @@ function FullScreenEditorModule(options) {
};
this.initHeaderGeneric = function() {
self.setHeaderText(MCICodeIds.ViewModeHeader.AreaName, getMessageAreaByName(self.messageAreaName).desc);
self.setHeaderText(MCICodeIds.ViewModeHeader.AreaName, getMessageAreaByTag(self.messageAreaTag).name);
};
this.initHeaderViewMode = function() {
@ -965,13 +964,10 @@ function FullScreenEditorModule(options) {
require('util').inherits(FullScreenEditorModule, MenuModule);
FullScreenEditorModule.prototype.enter = function(client) {
FullScreenEditorModule.super_.prototype.enter.call(this, client);
FullScreenEditorModule.prototype.enter = function() {
FullScreenEditorModule.super_.prototype.enter.call(this);
};
FullScreenEditorModule.prototype.mciReady = function(mciData, cb) {
this.mciReadyHandler(mciData, cb);
//this['mciReadyHandler' + _.capitalize(this.editorType)](mciData);
};

View File

@ -1,9 +1,10 @@
/* jslint node: true */
'use strict';
var MailPacket = require('./mail_packet.js');
//var MailPacket = require('./mail_packet.js');
var ftn = require('./ftn_util.js');
var Message = require('./message.js');
var sauce = require('./sauce.js');
var _ = require('lodash');
var assert = require('assert');
@ -12,6 +13,8 @@ var fs = require('fs');
var util = require('util');
var async = require('async');
var iconv = require('iconv-lite');
var buffers = require('buffers');
var moment = require('moment');
/*
:TODO: should probably be broken up
@ -20,6 +23,493 @@ var iconv = require('iconv-lite');
FTNPacketExport: message(s) -> packet
*/
/*
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_TYPE = 2;
const FTN_PACKET_MESSAGE_TYPE = 2;
// EOF + SAUCE.id + SAUCE.version ('00')
const FTN_MESSAGE_SAUCE_HEADER =
new Buffer( [ 0x1a, 'S', 'A', 'U', 'C', 'E', '0', '0' ] );
const FTN_MESSAGE_KLUDGE_PREFIX = '\x01';
function FTNPacket() {
var self = this;
this.parsePacketHeader = function(packetBuffer, cb) {
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) {
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 padded 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(packetHeader => {
// Convert password from NULL padded array to string
packetHeader.password = ftn.stringFromFTN(packetHeader.password);
if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) {
cb(new Error('Unsupported header type: ' + packetHeader.packetType));
return;
}
//
// Date/time components into something more reasonable
// Note: The names above match up with object members moment() allows
//
packetHeader.created = moment(packetHeader);
cb(null, packetHeader);
});
};
this.writePacketHeader = function(headerInfo, ws) {
let buffer = new Buffer(FTN_PACKET_HEADER_SIZE);
buffer.writeUInt16LE(headerInfo.origNode, 0);
buffer.writeUInt16LE(headerInfo.destNode, 2);
buffer.writeUInt16LE(headerInfo.created.year(), 4);
buffer.writeUInt16LE(headerInfo.created.month(), 6);
buffer.writeUInt16LE(headerInfo.created.date(), 8);
buffer.writeUInt16LE(headerInfo.created.hour(), 10);
buffer.writeUInt16LE(headerInfo.created.minute(), 12);
buffer.writeUInt16LE(headerInfo.created.second(), 14);
buffer.writeUInt16LE(headerInfo.baud, 16);
buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18);
buffer.writeUInt16LE(headerInfo.origNet, 20);
buffer.writeUInt16LE(headerInfo.destNet, 22);
buffer.writeUInt8(headerInfo.prodCodeLo, 24);
buffer.writeUInt8(headerInfo.revisionMajor, 25);
const pass = ftn.stringToNullPaddedBuffer(headerInfo.password, 8);
pass.copy(buffer, 26);
buffer.writeUInt16LE(headerInfo.origZone, 34);
buffer.writeUInt16LE(headerInfo.destZone, 36);
// FSC-0048.002 additions...
buffer.writeUInt16LE(headerInfo.auxNet, 38);
buffer.writeUInt16LE(headerInfo.capWordA, 40);
buffer.writeUInt8(headerInfo.prodCodeHi, 42);
buffer.writeUInt8(headerInfo.revisionMinor, 43);
buffer.writeUInt16LE(headerInfo.capWordB, 44);
buffer.writeUInt16LE(headerInfo.origZone2, 46);
buffer.writeUInt16LE(headerInfo.destZone2, 48);
buffer.writeUInt16LE(headerInfo.origPoint, 50);
buffer.writeUInt16LE(headerInfo.destPoint, 52);
buffer.writeUInt32LE(headerInfo.prodData, 54);
ws.write(buffer);
};
this.processMessageBody = function(messageBodyBuffer, 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 can be a bit tricky:
// * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that
// * Many kludge lines specify an encoding. If we find one of such lines, we'll
// likely need to re-decode as the specified encoding
// * SAUCE is binary-ish data, so we need to inspect for it before any
// decoding occurs
//
let messageBodyData = {
message : [],
kludgeLines : {}, // KLUDGE:[value1, value2, ...] map
seenBy : [],
};
function addKludgeLine(line) {
const sepIndex = line.indexOf(':');
const key = line.substr(0, sepIndex).toUpperCase();
const value = line.substr(sepIndex + 1).trim();
//
// Allow mapped value to be either a key:value if there is only
// one entry, or key:[value1, value2,...] if there are more
//
if(messageBodyData.kludgeLines[key]) {
if(!_.isArray(messageBodyData.kludgeLines[key])) {
messageBodyData.kludgeLines[key] = [ messageBodyData.kludgeLines[key] ];
}
messageBodyData.kludgeLines[key].push(value);
} else {
messageBodyData.kludgeLines[key] = value;
}
}
async.series(
[
function extractSauce(callback) {
// :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
// has SEEN-BY, PATH, and other kludge information *appended*
const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER);
if(sauceHeaderPosition > -1) {
sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition), (err, theSauce) => {
if(!err) {
// we read some SAUCE - don't re-process that portion into the body
messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition);
messageBodyData.sauce = theSauce;
}
callback(null); // failure to read SAUCE is OK
});
} else {
callback(null);
}
},
function extractMessageData(callback) {
const messageLines =
iconv.decode(messageBodyBuffer, 'CP437').replace(/[\xec\n]/g, '').split(/\r/g);
let preOrigin = true;
messageLines.forEach(line => {
if(0 === line.length) {
messageBodyData.message.push('');
return;
}
if(preOrigin) {
if(line.startsWith('AREA:')) {
messageBodyData.area = line.substring(line.indexOf(':') + 1).trim();
} else if(line.startsWith('--- ')) {
// Tear Lines are tracked allowing for specialized display/etc.
messageBodyData.tearLine = line;
} else if(/[ ]{1,2}(\* )?Origin\: /.test(line)) { // To spec is " * Origin: ..."
messageBodyData.originLine = line;
preOrigin = false;
} else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) {
addKludgeLine(line.slice(1));
} else {
// regular ol' message line
messageBodyData.message.push(line);
}
} else {
if(line.startsWith('SEEN-BY:')) {
messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim());
} else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) {
addKludgeLine(line.slice(1));
}
}
});
callback(null);
}
],
function complete(err) {
messageBodyData.message = messageBodyData.message.join('\n');
cb(messageBodyData);
}
);
};
this.parsePacketMessages = function(messagesBuffer, iterator, cb) {
const NULL_TERM_BUFFER = new Buffer( [ 0 ] );
binary.stream(messagesBuffer).loop(function looper(end, vars) {
//
// Some variable names used here match up directly with well known
// meta data names used with FTN messages.
//
this
.word16lu('messageType')
.word16lu('ftn_orig_node')
.word16lu('ftn_dest_node')
.word16lu('ftn_orig_network')
.word16lu('ftn_dest_network')
.word8('ftn_attr_flags1')
.word8('ftn_attr_flags2')
.word16lu('ftn_cost')
.scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max
.scan('toUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max
.scan('fromUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max
.scan('subject', NULL_TERM_BUFFER) // :TODO: 72 bytes max
.scan('message', NULL_TERM_BUFFER)
.tap(function tapped(msgData) {
if(!msgData.ftn_orig_node) {
// end marker -- no more messages
end();
cb(null);
return;
}
if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) {
end();
cb(new Error('Unsupported message type: ' + msgData.messageType));
return;
}
//
// Convert null terminated arrays to strings
//
let convMsgData = {};
[ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => {
convMsgData[k] = iconv.decode(msgData[k], 'CP437');
});
//
// The message body itself is a special beast as it may
// contain special origin lines, kludges, SAUCE in the case
// of ANSI files, etc.
//
let msg = new Message( {
toUserName : convMsgData.toUserName,
fromUserName : convMsgData.fromUserName,
subject : convMsgData.subject,
modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime),
});
msg.meta.FtnProperty = {};
msg.meta.FtnProperty.ftn_orig_node = msgData.ftn_orig_node;
msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node;
msg.meta.FtnProperty.ftn_orig_network = msgData.ftn_orig_network;
msg.meta.FtnProperty.ftn_dest_network = msgData.ftn_dest_network;
msg.meta.FtnProperty.ftn_attr_flags1 = msgData.ftn_attr_flags1;
msg.meta.FtnProperty.ftn_attr_flags2 = msgData.ftn_attr_flags2;
msg.meta.FtnProperty.ftn_cost = msgData.ftn_cost;
self.processMessageBody(msgData.message, function processed(messageBodyData) {
msg.message = messageBodyData.message;
msg.meta.FtnKludge = messageBodyData.kludgeLines;
if(messageBodyData.tearLine) {
msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine;
}
if(messageBodyData.seenBy.length > 0) {
msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy;
}
if(messageBodyData.area) {
msg.meta.FtnProperty.ftn_area = messageBodyData.area;
}
if(messageBodyData.originLine) {
msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine;
}
iterator('message', msg);
})
});
});
};
this.writeMessage = function(message, ws) {
let basicHeader = new Buffer(34);
basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8);
basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags1, 10);
basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11);
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');
dateTimeBuffer.copy(basicHeader, 14);
ws.write(basicHeader);
// toUserName & fromUserName: up to 36 bytes in length, NULL term'd
// :TODO: DRY...
let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36);
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
ws.write(encBuf);
encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36);
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
ws.write(encBuf);
// subject: up to 72 bytes in length, NULL term'd
encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72);
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
ws.write(encBuf);
//
// message: unbound length, NULL term'd
//
// We need to build in various special lines - kludges, area,
// seen-by, etc.
//
// :TODO: Put this in it's own method
let msgBody = '';
function appendMeta(k, m) {
if(m) {
let a = m;
if(!_.isArray(a)) {
a = [ a ];
}
a.forEach(v => {
msgBody += `${k}: ${v}\n`;
});
}
}
// :TODO: is Area really any differnt (e.g. no space between AREA:the_area)
if(message.meta.FtnProperty.ftn_area) {
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\n`;
}
Object.keys(message.meta.FtnKludge).forEach(k => {
if('PATH' !== k) {
appendMeta(k, message.meta.FtnKludge[k]);
}
});
msgBody += message.message;
appendMeta('', message.meta.FtnProperty.ftn_tear_line);
appendMeta('', message.meta.FtnProperty.ftn_origin);
appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by);
appendMeta('PATH', message.meta.FtnKludge['PATH']);
ws.write(iconv.encode(msgBody + '\0', 'CP437'));
};
this.parsePacketBuffer = function(packetBuffer, iterator, cb) {
async.series(
[
function processHeader(callback) {
self.parsePacketHeader(packetBuffer, (err, header) => {
if(!err) {
iterator('header', header);
}
callback(err);
});
},
function processMessages(callback) {
self.parsePacketMessages(
packetBuffer.slice(FTN_PACKET_HEADER_SIZE),
iterator,
callback);
}
],
cb
);
};
}
FTNPacket.prototype.read = function(pathOrBuffer, iterator, cb) {
var self = this;
async.series(
[
function getBufferIfPath(callback) {
if(_.isString(pathOrBuffer)) {
fs.readFile(pathOrBuffer, (err, data) => {
pathOrBuffer = data;
callback(err);
});
} else {
callback(null);
}
},
function parseBuffer(callback) {
self.parsePacketBuffer(pathOrBuffer, iterator, callback);
}
],
cb // completion callback
);
};
FTNPacket.prototype.write = function(path, headerInfo, messages, cb) {
headerInfo.created = headerInfo.created || moment();
headerInfo.baud = headerInfo.baud || 0;
// :TODO: Other defaults?
if(!_.isArray(messages)) {
messages = [ messages ] ;
}
let ws = fs.createWriteStream(path);
this.writePacketHeader(headerInfo, ws);
messages.forEach(msg => {
this.writeMessage(msg, ws);
});
};
//
// References
// * http://ftsc.org/docs/fts-0001.016
@ -30,7 +520,7 @@ var iconv = require('iconv-lite');
//
function FTNMailPacket(options) {
MailPacket.call(this, options);
//MailPacket.call(this, options);
var self = this;
self.KLUDGE_PREFIX = '\x01';
@ -77,7 +567,7 @@ function FTNMailPacket(options) {
.word16lu('second')
.word16lu('baud')
.word16lu('packetType')
.word16lu('originNet')
.word16lu('origNet')
.word16lu('destNet')
.word8('prodCodeLo')
.word8('revisionMajor') // aka serialNo
@ -100,35 +590,110 @@ function FTNMailPacket(options) {
// :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) {
self.getMessageMeta = function(msgBody, msgData) {
var meta = {
FtnKludge : msgBody.kludgeLines,
FtnProperty : {},
};
if(msgBody.tearLine) {
meta.FtnProperty.ftn_tear_line = [ 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 ];
meta.FtnProperty.ftn_area = msgBody.area;
}
if(msgBody.originLine) {
meta.FtnProperty.ftn_origin = [ 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;
};
@ -172,13 +737,15 @@ function FTNMailPacket(options) {
var preOrigin = true;
function addKludgeLine(kl) {
var kludgeParts = kl.split(':');
const kludgeParts = kl.split(':');
kludgeParts[0] = kludgeParts[0].toUpperCase();
kludgeParts[1] = kludgeParts[1].trim();
(msgBody.kludgeLines[kludgeParts[0]] = msgBody.kludgeLines[kludgeParts[0]] || []).push(kludgeParts[1]);
self.setOrAppend(kludgeParts[1], msgBody.kludgeLines[kludgeParts[0]]);
}
var sauceBuffers;
msgLines.forEach(function nextLine(line) {
if(0 === line.length) {
msgBody.message.push('');
@ -196,10 +763,12 @@ function FTNMailPacket(options) {
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);
}
// :TODO: SAUCE/etc. can be present?
} else {
if(_.startsWith(line, 'SEEN-BY:')) {
msgBody.seenBy.push(line.substring(line.indexOf(':') + 1).trim());
@ -209,29 +778,36 @@ function FTNMailPacket(options) {
}
});
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, cb) {
var nullTermBuf = new Buffer( [ 0 ] );
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('originNode')
.word16lu('origNode')
.word16lu('destNode')
.word16lu('originNet')
.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)
.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.originNode) {
if(!msgData.origNode) {
end();
cb(null);
return;
@ -247,20 +823,25 @@ function FTNMailPacket(options) {
// Now, create a Message object
//
var msg = new Message( {
// :TODO: areaId needs to be looked up via AREA line - may need a 1:n alias -> area ID lookup
// 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),
meta : self.getMessageMeta(msgBody, msgData),
});
self.emit('message', msg); // :TODO: Placeholder
iterator(msg);
//self.emit('message', msg); // :TODO: Placeholder
});
});
});
};
//this.getMessageHeaderBuffer = function(headerInfo)
this.parseFtnMessages = function(buffer, cb) {
var nullTermBuf = new Buffer( [ 0 ] );
@ -269,9 +850,9 @@ function FTNMailPacket(options) {
binary.stream(buffer).loop(function looper(end, vars) {
this
.word16lu('messageType')
.word16lu('originNode')
.word16lu('origNode')
.word16lu('destNode')
.word16lu('originNet')
.word16lu('origNet')
.word16lu('destNet')
.word8('attrFlags1')
.word8('attrFlags2')
@ -282,7 +863,7 @@ function FTNMailPacket(options) {
.scan('subject', nullTermBuf)
.scan('message', nullTermBuf)
.tap(function tapped(msgData) {
if(!msgData.originNode) {
if(!msgData.origNode) {
end();
cb(null, fidoMessages);
return;
@ -302,7 +883,10 @@ function FTNMailPacket(options) {
});
};
this.extractMesssagesFromPacketBuffer = function(packetBuffer, cb) {
this.extractMesssagesFromPacketBuffer = function(packetBuffer, iterator, cb) {
assert(Buffer.isBuffer(packetBuffer));
assert(_.isFunction(iterator));
async.waterfall(
[
function parseHeader(callback) {
@ -318,7 +902,8 @@ function FTNMailPacket(options) {
},
function extractEmbeddedMessages(callback) {
// note: packet header is 58 bytes in length
self.extractMessages(packetBuffer.slice(58), function extracted(err) {
self.extractMessages(
packetBuffer.slice(58), iterator, function extracted(err) {
callback(err);
});
}
@ -361,7 +946,7 @@ function FTNMailPacket(options) {
};
}
require('util').inherits(FTNMailPacket, MailPacket);
//require('util').inherits(FTNMailPacket, MailPacket);
FTNMailPacket.prototype.parse = function(path, cb) {
var self = this;
@ -385,41 +970,67 @@ FTNMailPacket.prototype.parse = function(path, cb) {
);
};
FTNMailPacket.prototype.read = function(options) {
FTNMailPacket.super_.prototype.read.call(this, options);
FTNMailPacket.prototype.read = function(pathOrBuffer, iterator, cb) {
var self = this;
if(_.isString(options.packetPath)) {
if(_.isString(pathOrBuffer)) {
async.waterfall(
[
function readPacketFile(callback) {
fs.readFile(options.packetPath, function packetData(err, data) {
fs.readFile(pathOrBuffer, function packetData(err, data) {
callback(err, data);
});
},
function extractMessages(data, callback) {
self.extractMesssagesFromPacketBuffer(data, function extracted(err) {
callback(err);
});
self.extractMesssagesFromPacketBuffer(data, iterator, callback);
}
],
function complete(err) {
if(err) {
self.emit('error', err);
}
}
cb
);
} else if(Buffer.isBuffer(options.packetBuffer)) {
} else if(Buffer.isBuffer(pathOrBuffer)) {
}
};
FTNMailPacket.prototype.write = function(options) {
FTNMailPacket.super_.prototype.write.call(this, options);
FTNMailPacket.prototype.write = function(messages, fileName, options) {
if(!_.isArray(messages)) {
messages = [ messages ];
}
};
var ftnPacket = new FTNPacket();
var theHeader;
var written = false;
ftnPacket.read(
process.argv[2],
function iterator(dataType, data) {
if('header' === dataType) {
theHeader = data;
console.log(theHeader);
} else if('message' === dataType) {
const msg = data;
console.log(msg);
if(!written) {
written = true;
let messages = [ msg ];
ftnPacket.write('/home/nuskooler/Downloads/ftnout/test1.pkt', theHeader, messages, err => {
});
}
}
},
function completion(err) {
console.log(err);
}
);
/*
var mailPacket = new FTNMailPacket(
{
nodeAddresses : {
@ -434,11 +1045,42 @@ var mailPacket = new FTNMailPacket(
}
);
mailPacket.on('message', function msgParsed(msg) {
console.log(msg);
});
mailPacket.read( { packetPath : '/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/BAD_BNDL.007' } );
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) {

View File

@ -10,11 +10,14 @@ var binary = require('binary');
var fs = require('fs');
var util = require('util');
var iconv = require('iconv-lite');
var moment = require('moment');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringFromFTN = stringFromFTN;
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getFormattedFTNAddress = getFormattedFTNAddress;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString;
exports.getQuotePrefix = getQuotePrefix;
@ -33,6 +36,14 @@ function stringFromFTN(buf, encoding) {
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
}
function stringToNullPaddedBuffer(s, bufLen) {
let buffer = new Buffer(bufLen).fill(0x00);
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
for(let i = 0; i < enc.length; ++i) {
buffer[i] = enc[i];
}
return buffer;
}
//
// Convert a FTN style DateTime string to a Date object
@ -44,9 +55,34 @@ function getDateFromFtnDateTime(dateTime) {
// "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03"
//
// :TODO: Use moment.js here
return (new Date(Date.parse(dateTime))).toISOString();
}
function getDateTimeString(m) {
//
// 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"
//
if(!moment.isMoment(m)) {
m = moment(m);
}
return m.format('DD MMM YY HH:mm:ss');
}
function getFormattedFTNAddress(address, dimensions) {
//var addr = util.format('%d:%d', address.zone, address.net);
var addr = '{0}:{1}'.format(address.zone, address.net);

View File

@ -25,6 +25,10 @@ function MenuModule(options) {
var self = this;
this.menuName = options.menuName;
this.menuConfig = options.menuConfig;
this.client = options.client;
// :TODO: this and the line below with .config creates empty ({}) objects in the theme --
// ...which we really should not do. If they aren't there already, don't use 'em.
this.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's
@ -190,10 +194,7 @@ require('util').inherits(MenuModule, PluginModule);
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
MenuModule.prototype.enter = function(client) {
this.client = client;
assert(_.isObject(client));
MenuModule.prototype.enter = function() {
if(_.isString(this.menuConfig.status)) {
this.client.currentStatus = this.menuConfig.status;
} else {

View File

@ -131,7 +131,7 @@ MenuStack.prototype.goto = function(name, options, cb) {
modInst.restoreSavedState(options.savedState);
}
modInst.enter(self.client);
modInst.enter();
self.client.log.trace(
{ stack : _.map(self.stack, function(si) { return si.name; } ) },

View File

@ -4,10 +4,8 @@
// ENiGMA½
var moduleUtil = require('./module_util.js');
var Log = require('./logger.js').log;
var conf = require('./config.js'); // :TODO: remove me!
var Config = require('./config.js').config;
var asset = require('./asset.js');
var theme = require('./theme.js');
var getFullConfig = require('./config_util.js').getFullConfig;
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
var acsUtil = require('./acs_util.js');
@ -68,17 +66,18 @@ function loadMenu(options, cb) {
});
},
function loadMenuModule(menuConfig, callback) {
var modAsset = asset.getModuleAsset(menuConfig.module);
var modSupplied = null !== modAsset;
var modLoadOpts = {
const modAsset = asset.getModuleAsset(menuConfig.module);
const modSupplied = null !== modAsset;
const modLoadOpts = {
name : modSupplied ? modAsset.asset : 'standard_menu',
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods,
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
};
moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) {
var modData = {
const modData = {
name : modLoadOpts.name,
config : menuConfig,
mod : mod,
@ -97,7 +96,8 @@ function loadMenu(options, cb) {
{
menuName : options.name,
menuConfig : modData.config,
extraArgs : options.extraArgs
extraArgs : options.extraArgs,
client : options.client,
});
callback(null, moduleInstance);
} catch(e) {
@ -174,7 +174,7 @@ function handleAction(client, formData, conf) {
assert(_.isObject(conf));
assert(_.isString(conf.action));
var actionAsset = asset.parseAsset(conf.action);
const actionAsset = asset.parseAsset(conf.action);
assert(_.isObject(actionAsset));
switch(actionAsset.type) {
@ -245,84 +245,3 @@ function handleNext(client, nextSpec, conf) {
break;
}
}
// :TODO: Seems better in theme.js, but that includes ViewController...which would then include theme.js
// ...theme.js only brings in VC to create themed pause prompt. Perhaps that should live elsewhere
/*
function applyGeneralThemeCustomization(options) {
//
// options.name
// options.client
// options.type
// options.config
//
assert(_.isString(options.name));
assert(_.isObject(options.client));
assert("menus" === options.type || "prompts" === options.type);
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
if(themeConfig.config) {
Object.keys(themeConfig.config).forEach(function confEntry(conf) {
if(options.config[conf]) {
_.defaultsDeep(options.config[conf], themeConfig.config[conf]);
} else {
options.config[conf] = themeConfig.config[conf];
}
});
}
}
}
*/
/*
function applyMciThemeCustomization(options) {
//
// options.name : menu/prompt name
// options.mci : menu/prompt .mci section
// options.client : client
// options.type : menu|prompt
// options.formId : (optional) form ID in cases where multiple forms may exist wanting their own customization
//
// In the case of formId, the theme must include the ID as well, e.g.:
// {
// ...
// "2" : {
// "TL1" : { ... }
// }
// }
//
assert(_.isString(options.name));
assert("menus" === options.type || "prompts" === options.type);
assert(_.isObject(options.client));
if(_.isUndefined(options.mci)) {
options.mci = {};
}
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
if(options.formId && _.has(themeConfig, options.formId.toString())) {
// form ID found - use exact match
themeConfig = themeConfig[options.formId];
}
if(themeConfig.mci) {
Object.keys(themeConfig.mci).forEach(function mciEntry(mci) {
// :TODO: a better way to do this?
if(options.mci[mci]) {
_.defaults(options.mci[mci], themeConfig.mci[mci]);
} else {
options.mci[mci] = themeConfig.mci[mci];
}
});
}
}
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
}
*/

View File

@ -16,7 +16,7 @@ function Message(options) {
options = options || {};
this.messageId = options.messageId || 0; // always generated @ persist
this.areaName = options.areaName || Message.WellKnownAreaNames.Invalid;
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
this.uuid = uuid.v1();
this.replyToMsgId = options.replyToMsgId || 0;
this.toUserName = options.toUserName || '';
@ -55,7 +55,7 @@ function Message(options) {
};
this.isPrivate = function() {
return this.areaName === Message.WellKnownAreaNames.Private ? true : false;
return this.areaTag === Message.WellKnownAreaTags.Private ? true : false;
};
this.getMessageTimestampString = function(ts) {
@ -80,7 +80,7 @@ function Message(options) {
*/
}
Message.WellKnownAreaNames = {
Message.WellKnownAreaTags = {
Invalid : '',
Private : 'private_mail',
Bulletin : 'local_bulletin',
@ -104,16 +104,21 @@ Message.SystemMetaNames = {
LocalFromUserID : 'local_from_user_id',
};
Message.FtnPropertyNames = {
FtnCost : 'ftn_cost',
Message.FtnPropertyNames = {
FtnOrigNode : 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node',
FtnOrigNetwork : 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network',
FtnAttrFlags1 : 'ftn_attr_flags1',
FtnAttrFlags2 : 'ftn_attr_flags2',
FtnCost : 'ftn_cost',
FtnOrigZone : 'ftn_orig_zone',
FtnDestZone : 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point',
FtnAttribute : 'ftn_attribute',
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
@ -141,7 +146,7 @@ Message.prototype.load = function(options, cb) {
[
function loadMessage(callback) {
msgDb.get(
'SELECT message_id, area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' +
'SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' +
'message, modified_timestamp, view_count ' +
'FROM message ' +
'WHERE message_uuid=? ' +
@ -149,7 +154,7 @@ Message.prototype.load = function(options, cb) {
[ options.uuid ],
function row(err, msgRow) {
self.messageId = msgRow.message_id;
self.areaName = msgRow.area_name;
self.areaTag = msgRow.area_tag;
self.messageUuid = msgRow.message_uuid;
self.replyToMsgId = msgRow.reply_to_message_id;
self.toUserName = msgRow.to_user_name;
@ -202,8 +207,8 @@ Message.prototype.persist = function(cb) {
},
function storeMessage(callback) {
msgDb.run(
'INSERT INTO message (area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) ' +
'VALUES (?, ?, ?, ?, ?, ?, ?, ?);', [ self.areaName, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
'INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) ' +
'VALUES (?, ?, ?, ?, ?, ?, ?, ?);', [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
function msgInsert(err) {
if(!err) {
self.messageId = this.lastID;

View File

@ -5,100 +5,275 @@ var msgDb = require('./database.js').dbs.message;
var Config = require('./config.js').config;
var Message = require('./message.js');
var Log = require('./logger.js').log;
var checkAcs = require('./acs_util.js').checkAcs;
var async = require('async');
var _ = require('lodash');
var assert = require('assert');
exports.getAvailableMessageAreas = getAvailableMessageAreas;
exports.getDefaultMessageArea = getDefaultMessageArea;
exports.getMessageAreaByName = getMessageAreaByName;
exports.getAvailableMessageConferences = getAvailableMessageConferences;
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
exports.getMessageConferenceByTag = getMessageConferenceByTag;
exports.getMessageAreaByTag = getMessageAreaByTag;
exports.changeMessageConference = changeMessageConference;
exports.changeMessageArea = changeMessageArea;
exports.getMessageListForArea = getMessageListForArea;
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
function getAvailableMessageAreas(options) {
// example: [ { "name" : "local_music", "desc" : "Music Discussion", "groups" : ["somegroup"] }, ... ]
options = options || {};
const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]';
const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]';
var areas = Config.messages.areas;
var avail = [];
for(var i = 0; i < areas.length; ++i) {
if(true !== options.includePrivate &&
Message.WellKnownAreaNames.Private === areas[i].name)
{
continue;
}
const AREA_ACS_DEFAULT = {
read : CONF_AREA_RW_ACS_DEFAULT,
write : CONF_AREA_RW_ACS_DEFAULT,
manage : AREA_MANAGE_ACS_DEFAULT,
};
avail.push(areas[i]);
}
return avail;
function getAvailableMessageConferences(client, options) {
options = options || { includeSystemInternal : false };
// perform ACS check per conf & omit system_internal if desired
return _.omit(Config.messageConferences, (v, k) => {
if(!options.includeSystemInternal && 'system_internal' === k) {
return true;
}
const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT;
return !checkAcs(client, readAcs);
});
}
function getDefaultMessageArea() {
//
// Return first non-private/etc. area name. This will be from config.hjson
//
return getAvailableMessageAreas()[0];
/*
var avail = getAvailableMessageAreas();
for(var i = 0; i < avail.length; ++i) {
if(Message.WellKnownAreaNames.Private !== avail[i].name) {
return avail[i];
}
}
*/
}
function getMessageAreaByName(areaName) {
areaName = areaName.toLowerCase();
var availAreas = getAvailableMessageAreas( { includePrivate : true } );
var index = _.findIndex(availAreas, function pred(an) {
return an.name == areaName;
function getSortedAvailMessageConferences(client, options) {
var sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
return {
confTag : k,
conf : v,
};
});
sorted.sort((a, b) => {
return a.conf.name.localeCompare(b.conf.name);
});
if(index > -1) {
return availAreas[index];
}
return sorted;
}
function changeMessageArea(client, areaName, cb) {
// Return an *object* of available areas within |confTag|
function getAvailableMessageAreasByConfTag(confTag, options) {
options = options || {};
if(_.has(Config.messageConferences, [ confTag, 'areas' ])) {
const areas = Config.messageConferences[confTag].areas;
if(!options.client || true === options.noAcsCheck) {
// everything - no ACS checks
return areas;
} else {
// perform ACS check per area
return _.omit(areas, (v, k) => {
const readAcs = _.has(v, 'acs.read') ? v.acs.read : CONF_AREA_RW_ACS_DEFAULT;
return !checkAcs(options.client, readAcs);
});
}
}
}
function getSortedAvailMessageAreasByConfTag(confTag, options) {
const areas = getAvailableMessageAreasByConfTag(confTag, options);
// :TODO: should probably be using localeCompare / sort
return _.sortBy(_.map(areas, (v, k) => {
return {
areaTag : k,
area : v,
};
}), o => o.area.name); // sort by name
}
function getDefaultMessageConferenceTag(client, disableAcsCheck) {
//
// Find the first conference marked 'default'. If found,
// inspect |client| against *read* ACS using defaults if not
// specified.
//
// If the above fails, just go down the list until we get one
// that passes.
//
// It's possible that we end up with nothing here!
//
// Note that built in 'system_internal' is always ommited here
//
let defaultConf = _.findKey(Config.messageConferences, o => o.default);
if(defaultConf) {
const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT;
if(true === disableAcsCheck || checkAcs(client, acs)) {
return defaultConf;
}
}
// just use anything we can
defaultConf = _.findKey(Config.messageConferences, (o, k) => {
const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT;
return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs));
});
return defaultConf;
}
function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
//
// Similar to finding the default conference:
// Find the first entry marked 'default', if any. If found, check | client| against
// *read* ACS. If this fails, just find the first one we can that passes checks.
//
// It's possible that we end up with nothing!
//
confTag = confTag || getDefaultMessageConferenceTag(client);
if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) {
const areaPool = Config.messageConferences[confTag].areas;
let defaultArea = _.findKey(areaPool, o => o.default);
if(defaultArea) {
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
if(true === disableAcsCheck || checkAcs(client, readAcs)) {
return defaultArea;
}
}
defaultArea = _.findKey(areaPool, (o, k) => {
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
return (true === disableAcsCheck || checkAcs(client, readAcs));
});
return defaultArea;
}
}
function getMessageConferenceByTag(confTag) {
return Config.messageConferences[confTag];
}
function getMessageAreaByTag(areaTag, optionalConfTag) {
const confs = Config.messageConferences;
if(_.isString(optionalConfTag)) {
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
return confs[optionalConfTag].areas[areaTag];
}
} else {
//
// No confTag to work with - we'll have to search through them all
//
var area;
_.forEach(confs, (v, k) => {
if(_.has(v, [ 'areas', areaTag ])) {
area = v.areas[areaTag];
return false; // stop iteration
}
});
return area;
}
}
function changeMessageConference(client, confTag, cb) {
async.waterfall(
[
function getConf(callback) {
const conf = getMessageConferenceByTag(confTag);
if(conf) {
callback(null, conf);
} else {
callback(new Error('Invalid message conference tag'));
}
},
function getDefaultAreaInConf(conf, callback) {
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
const area = getMessageAreaByTag(areaTag, confTag);
if(area) {
callback(null, conf, { areaTag : areaTag, area : area } );
} else {
callback(new Error('No available areas for this user in conference'));
}
},
function validateAccess(conf, areaInfo, callback) {
const confAcs = conf.acs || CONF_AREA_RW_ACS_DEFAULT;
if(!checkAcs(client, confAcs)) {
callback(new Error('User does not have access to this conference'));
} else {
const areaAcs = _.has(areaInfo, 'area.acs.read') ? areaInfo.area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
if(!checkAcs(client, areaAcs)) {
callback(new Error('User does not have access to default area in this conference'));
} else {
callback(null, conf, areaInfo);
}
}
},
function changeConferenceAndArea(conf, areaInfo, callback) {
const newProps = {
message_conf_tag : confTag,
message_area_tag : areaInfo.areaTag,
};
client.user.persistProperties(newProps, err => {
callback(err, conf, areaInfo);
});
},
],
function complete(err, conf, areaInfo) {
if(!err) {
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
} else {
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
}
cb(err);
}
);
}
function changeMessageArea(client, areaTag, cb) {
async.waterfall(
[
function getArea(callback) {
var area = getMessageAreaByName(areaName);
const area = getMessageAreaByTag(areaTag);
if(area) {
callback(null, area);
} else {
callback(new Error('Invalid message area'));
callback(new Error('Invalid message area tag'));
}
},
function validateAccess(area, callback) {
if(_.isArray(area.groups) && !
client.user.isGroupMember(area.groups))
{
//
// Need at least *read* to access the area
//
const readAcs = _.has(area, 'acs.read') ? area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
if(!checkAcs(client, readAcs)) {
callback(new Error('User does not have access to this area'));
} else {
callback(null, area);
}
},
function changeArea(area, callback) {
client.user.persistProperty('message_area_name', area.name, function persisted(err) {
client.user.persistProperty('message_area_tag', areaTag, function persisted(err) {
callback(err, area);
});
}
],
function complete(err, area) {
if(!err) {
client.log.info( area, 'Current message area changed');
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
} else {
client.log.warn( { area : area, error : err.message }, 'Could not change message area');
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
}
cb(err);
@ -119,9 +294,9 @@ function getMessageFromRow(row) {
};
}
function getNewMessagesInAreaForUser(userId, areaName, cb) {
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
//
// If |areaName| is Message.WellKnownAreaNames.Private,
// If |areaTag| is Message.WellKnownAreaTags.Private,
// only messages addressed to |userId| should be returned.
//
// Only messages > lastMessageId should be returned
@ -131,7 +306,7 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
async.waterfall(
[
function getLastMessageId(callback) {
getMessageAreaLastReadId(userId, areaName, function fetched(err, lastMessageId) {
getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) {
callback(null, lastMessageId || 0); // note: willingly ignoring any errors here!
});
},
@ -139,9 +314,9 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
var sql =
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
'FROM message ' +
'WHERE area_name="' + areaName + '" AND message_id > ' + lastMessageId;
'WHERE area_tag ="' + areaTag + '" AND message_id > ' + lastMessageId;
if(Message.WellKnownAreaNames.Private === areaName) {
if(Message.WellKnownAreaTags.Private === areaTag) {
sql +=
' AND message_id in (' +
'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System +
@ -150,8 +325,6 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
sql += ' ORDER BY message_id;';
console.log(sql)
msgDb.each(sql, function msgRow(err, row) {
if(!err) {
msgList.push(getMessageFromRow(row));
@ -160,18 +333,17 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
}
],
function complete(err) {
console.log(msgList)
cb(err, msgList);
}
);
}
function getMessageListForArea(options, areaName, cb) {
function getMessageListForArea(options, areaTag, cb) {
//
// options.client (required)
//
options.client.log.debug( { areaName : areaName }, 'Fetching available messages');
options.client.log.debug( { areaTag : areaTag }, 'Fetching available messages');
assert(_.isObject(options.client));
@ -193,9 +365,9 @@ function getMessageListForArea(options, areaName, cb) {
msgDb.each(
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
'FROM message ' +
'WHERE area_name=? ' +
'WHERE area_tag = ? ' +
'ORDER BY message_id;',
[ areaName.toLowerCase() ],
[ areaTag.toLowerCase() ],
function msgRow(err, row) {
if(!err) {
msgList.push(getMessageFromRow(row));
@ -214,24 +386,24 @@ function getMessageListForArea(options, areaName, cb) {
);
}
function getMessageAreaLastReadId(userId, areaName, cb) {
function getMessageAreaLastReadId(userId, areaTag, cb) {
msgDb.get(
'SELECT message_id ' +
'FROM user_message_area_last_read ' +
'WHERE user_id = ? AND area_name = ?;',
[ userId, areaName ],
'WHERE user_id = ? AND area_tag = ?;',
[ userId, areaTag ],
function complete(err, row) {
cb(err, row ? row.message_id : 0);
}
);
}
function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) {
// :TODO: likely a better way to do this...
async.waterfall(
[
function getCurrent(callback) {
getMessageAreaLastReadId(userId, areaName, function result(err, lastId) {
getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
lastId = lastId || 0;
callback(null, lastId); // ignore errors as we default to 0
});
@ -239,25 +411,29 @@ function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
function update(lastId, callback) {
if(messageId > lastId) {
msgDb.run(
'REPLACE INTO user_message_area_last_read (user_id, area_name, message_id) ' +
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
'VALUES (?, ?, ?);',
[ userId, areaName, messageId ],
callback
[ userId, areaTag, messageId ],
function written(err) {
callback(err, true); // true=didUpdate
}
);
} else {
callback(null);
}
}
],
function complete(err) {
function complete(err, didUpdate) {
if(err) {
Log.debug(
{ error : err.toString(), userId : userId, areaName : areaName, messageId : messageId },
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId },
'Failed updating area last read ID');
} else {
Log.trace(
{ userId : userId, areaName : areaName, messageId : messageId },
'Area last read ID updated');
if(true === didUpdate) {
Log.trace(
{ userId : userId, areaTag : areaTag, messageId : messageId },
'Area last read ID updated');
}
}
cb(err);
}

View File

@ -14,38 +14,40 @@ exports.loadModuleEx = loadModuleEx;
exports.loadModule = loadModule;
exports.loadModulesForCategory = loadModulesForCategory;
function loadModuleEx(options, cb) {
assert(_.isObject(options));
assert(_.isString(options.name));
assert(_.isString(options.path));
var modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
if(_.isObject(modConfig) && false === modConfig.enabled) {
cb(new Error('Module "' + options.name + '" is disabled'));
return;
}
var mod;
try {
var mod = require(paths.join(options.path, options.name + '.js'));
if(!_.isObject(mod.moduleInfo)) {
cb(new Error('Module is missing "moduleInfo" section'));
return;
}
if(!_.isFunction(mod.getModule)) {
cb(new Error('Invalid or missing "getModule" method for module!'));
return;
}
// Safe configuration, if any, for convience to the module
mod.runtime = { config : modConfig };
cb(null, mod);
mod = require(paths.join(options.path, options.name + '.js'));
} catch(e) {
cb(e);
}
if(!_.isObject(mod.moduleInfo)) {
cb(new Error('Module is missing "moduleInfo" section'));
return;
}
if(!_.isFunction(mod.getModule)) {
cb(new Error('Invalid or missing "getModule" method for module!'));
return;
}
// Ref configuration, if any, for convience to the module
mod.runtime = { config : modConfig };
cb(null, mod);
}
function loadModule(name, category, cb) {
@ -61,7 +63,7 @@ function loadModule(name, category, cb) {
});
}
function loadModulesForCategory(category, cb) {
function loadModulesForCategory(category, iterator) {
var path = Config.paths[category];
fs.readdir(path, function onFiles(err, files) {
@ -72,8 +74,7 @@ function loadModulesForCategory(category, cb) {
var filtered = files.filter(function onFilter(file) { return '.js' === paths.extname(file); });
filtered.forEach(function onFile(file) {
var modName = paths.basename(file, '.js');
loadModule(paths.basename(file, '.js'), category, cb);
loadModule(paths.basename(file, '.js'), category, iterator);
});
});
}

View File

@ -0,0 +1,25 @@
/* 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);
};

View File

@ -0,0 +1,27 @@
/* 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
};

View File

@ -7,6 +7,7 @@ var Message = require('./message.js');
var MenuModule = require('./menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController;
var _ = require('lodash');
var async = require('async');
exports.moduleInfo = {
@ -36,10 +37,11 @@ function NewScanModule(options) {
var self = this;
var config = this.menuConfig.config;
this.currentStep = 'messageAreas';
this.currentScanAux = 0; // e.g. Message.WellKnownAreaNames.Private when currentSteps = messageAreas
this.currentStep = 'messageConferences';
this.currentScanAux = {};
this.scanStartFmt = config.scanStartFmt || 'Scanning {desc}...';
// :TODO: Make this conf/area specific:
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
@ -57,10 +59,65 @@ function NewScanModule(options) {
if(view) {
}
};
this.newScanMessageConference = function(cb) {
// lazy init
if(!self.sortedMessageConfs) {
const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc.
this.newScanMessageArea = function(cb) {
var availMsgAreas = msgArea.getAvailableMessageAreas( { includePrivate : true } );
var currentArea = availMsgAreas[self.currentScanAux];
self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => {
return {
confTag : k,
conf : v,
};
});
//
// Sort conferences by name, other than 'system_internal' which should
// always come first such that we display private mails/etc. before
// other conferences & areas
//
self.sortedMessageConfs.sort((a, b) => {
if('system_internal' === a.confTag) {
return -1;
} else {
return a.conf.name.localeCompare(b.conf.name);
}
});
self.currentScanAux.conf = self.currentScanAux.conf || 0;
self.currentScanAux.area = self.currentScanAux.area || 0;
}
const currentConf = self.sortedMessageConfs[self.currentScanAux.conf];
async.series(
[
function scanArea(callback) {
//self.currentScanAux.area = self.currentScanAux.area || 0;
self.newScanMessageArea(currentConf, function areaScanComplete(err) {
if(self.sortedMessageConfs.length > self.currentScanAux.conf + 1) {
self.currentScanAux.conf += 1;
self.currentScanAux.area = 0;
self.newScanMessageConference(cb); // recursive to next conf
//callback(null);
} else {
self.updateScanStatus(self.scanCompleteMsg);
callback(new Error('No more conferences'));
}
});
}
],
cb
);
};
this.newScanMessageArea = function(conf, cb) {
// :TODO: it would be nice to cache this - must be done by conf!
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : self.client } );
const currentArea = sortedAreas[self.currentScanAux.area];
//
// Scan and update index until we find something. If results are found,
@ -70,8 +127,8 @@ function NewScanModule(options) {
[
function checkAndUpdateIndex(callback) {
// Advance to next area if possible
if(availMsgAreas.length >= self.currentScanAux + 1) {
self.currentScanAux += 1;
if(sortedAreas.length >= self.currentScanAux.area + 1) {
self.currentScanAux.area += 1;
callback(null);
} else {
self.updateScanStatus(self.scanCompleteMsg);
@ -80,22 +137,30 @@ function NewScanModule(options) {
},
function updateStatusScanStarted(callback) {
self.updateScanStatus(self.scanStartFmt.format({
desc : currentArea.desc,
confName : conf.conf.name,
confDesc : conf.conf.desc,
areaName : currentArea.area.name,
areaDesc : currentArea.area.desc,
}));
callback(null);
},
function newScanAreaAndGetMessages(callback) {
msgArea.getNewMessagesInAreaForUser(
self.client.user.userId, currentArea.name, function msgs(err, msgList) {
self.client.user.userId, currentArea.areaTag, function msgs(err, msgList) {
if(!err) {
if(0 === msgList.length) {
self.updateScanStatus(self.scanFinishNoneFmt.format({
desc : currentArea.desc,
confName : conf.conf.name,
confDesc : conf.conf.desc,
areaName : currentArea.area.name,
areaDesc : currentArea.area.desc,
}));
} else {
self.updateScanStatus(self.scanFinishNewFmt.format({
desc : currentArea.desc,
count : msgList.length,
confName : conf.conf.name,
confDesc : conf.conf.desc,
areaName : currentArea.area.name,
count : msgList.length,
}));
}
}
@ -107,14 +172,14 @@ function NewScanModule(options) {
if(msgList && msgList.length > 0) {
var nextModuleOpts = {
extraArgs: {
messageAreaName : currentArea.name,
messageAreaTag : currentArea.areaTag,
messageList : msgList,
}
};
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
} else {
self.newScanMessageArea(cb);
self.newScanMessageArea(conf, cb);
}
}
],
@ -161,10 +226,10 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
},
function performCurrentStepScan(callback) {
switch(self.currentStep) {
case 'messageAreas' :
self.newScanMessageArea(function scanComplete(err) {
callback(null); // finished
});
case 'messageConferences' :
self.newScanMessageConference(function scanComplete(err) {
callback(null); // finished
});
break;
default :
@ -180,9 +245,3 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
}
);
};
/*
NewScanModule.prototype.finishedLoading = function() {
NewScanModule.super_.prototype.finishedLoading.call(this);
};
*/

View File

@ -3,7 +3,7 @@
var Config = require('./config.js').config;
var Log = require('./logger.js').log;
var getMessageAreaByName = require('./message_area.js').getMessageAreaByName;
var getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
var clientConnections = require('./client_connections.js');
var sysProp = require('./system_property.js');
@ -63,10 +63,15 @@ function getPredefinedMCIValue(client, code) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
},
MA : function messageAreaDescription() {
var area = getMessageAreaByName(client.user.properties.message_area_name);
return area ? area.desc : '';
MA : function messageAreaName() {
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.name : '';
},
ML : function messageAreaDescription() {
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.desc : '';
},
SH : function termHeight() { return client.term.termHeight.toString(); },
SW : function termWidth() { return client.term.termWidth.toString(); },

165
core/sauce.js Normal file
View File

@ -0,0 +1,165 @@
/* jslint node: true */
'use strict';
var binary = require('binary');
var iconv = require('iconv-lite');
exports.readSAUCE = readSAUCE;
const SAUCE_SIZE = 128;
const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
// :TODO: SAUCE should be a class
// - with getFontName()
// - ...other methods
//
// See
// http://www.acid.org/info/sauce/sauce.htm
//
function readSAUCE(data, cb) {
if(data.length < SAUCE_SIZE) {
cb(new Error('No SAUCE record present'));
return;
}
var offset = data.length - SAUCE_SIZE;
var sauceRec = data.slice(offset);
binary.parse(sauceRec)
.buffer('id', 5)
.buffer('version', 2)
.buffer('title', 35)
.buffer('author', 20)
.buffer('group', 20)
.buffer('date', 8)
.word32lu('fileSize')
.word8('dataType')
.word8('fileType')
.word16lu('tinfo1')
.word16lu('tinfo2')
.word16lu('tinfo3')
.word16lu('tinfo4')
.word8('numComments')
.word8('flags')
.buffer('tinfos', 22) // SAUCE 00.5
.tap(function onVars(vars) {
if(!SAUCE_ID.equals(vars.id)) {
cb(new Error('No SAUCE record present'));
return;
}
var ver = iconv.decode(vars.version, 'cp437');
if('00' !== ver) {
cb(new Error('Unsupported SAUCE version: ' + ver));
return;
}
var sauce = {
id : iconv.decode(vars.id, 'cp437'),
version : iconv.decode(vars.version, 'cp437').trim(),
title : iconv.decode(vars.title, 'cp437').trim(),
author : iconv.decode(vars.author, 'cp437').trim(),
group : iconv.decode(vars.group, 'cp437').trim(),
date : iconv.decode(vars.date, 'cp437').trim(),
fileSize : vars.fileSize,
dataType : vars.dataType,
fileType : vars.fileType,
tinfo1 : vars.tinfo1,
tinfo2 : vars.tinfo2,
tinfo3 : vars.tinfo3,
tinfo4 : vars.tinfo4,
numComments : vars.numComments,
flags : vars.flags,
tinfos : vars.tinfos,
};
var dt = SAUCE_DATA_TYPES[sauce.dataType];
if(dt && dt.parser) {
sauce[dt.name] = dt.parser(sauce);
}
cb(null, sauce);
});
}
// :TODO: These need completed:
var SAUCE_DATA_TYPES = {};
SAUCE_DATA_TYPES[0] = { name : 'None' };
SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE };
SAUCE_DATA_TYPES[2] = 'Bitmap';
SAUCE_DATA_TYPES[3] = 'Vector';
SAUCE_DATA_TYPES[4] = 'Audio';
SAUCE_DATA_TYPES[5] = 'BinaryText';
SAUCE_DATA_TYPES[6] = 'XBin';
SAUCE_DATA_TYPES[7] = 'Archive';
SAUCE_DATA_TYPES[8] = 'Executable';
var SAUCE_CHARACTER_FILE_TYPES = {};
SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII';
SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi';
SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation';
SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script';
SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard';
SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar';
SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML';
SAUCE_CHARACTER_FILE_TYPES[7] = 'Source';
SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw';
//
// Map of SAUCE font -> encoding hint
//
// Note that this is the same mapping that x84 uses. Be compatible!
//
var SAUCE_FONT_TO_ENCODING_HINT = {
'Amiga MicroKnight' : 'amiga',
'Amiga MicroKnight+' : 'amiga',
'Amiga mOsOul' : 'amiga',
'Amiga P0T-NOoDLE' : 'amiga',
'Amiga Topaz 1' : 'amiga',
'Amiga Topaz 1+' : 'amiga',
'Amiga Topaz 2' : 'amiga',
'Amiga Topaz 2+' : 'amiga',
'Atari ATASCII' : 'atari',
'IBM EGA43' : 'cp437',
'IBM EGA' : 'cp437',
'IBM VGA25G' : 'cp437',
'IBM VGA50' : 'cp437',
'IBM VGA' : 'cp437',
};
['437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) {
var codec = 'cp' + page;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
});
function parseCharacterSAUCE(sauce) {
var result = {};
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
// convience: create ansiFlags
sauce.ansiFlags = sauce.flags;
var i = 0;
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
++i;
}
var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
if(fontName.length > 0) {
result.fontName = fontName;
}
}
return result;
}

View File

@ -18,8 +18,8 @@ function StandardMenuModule(menuConfig) {
require('util').inherits(StandardMenuModule, MenuModule);
StandardMenuModule.prototype.enter = function(client) {
StandardMenuModule.super_.prototype.enter.call(this, client);
StandardMenuModule.prototype.enter = function() {
StandardMenuModule.super_.prototype.enter.call(this);
};
StandardMenuModule.prototype.beforeArt = function() {

View File

@ -203,13 +203,13 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
}
}
[ 'menus', 'prompts' ].forEach(function areaEntry(areaName) {
_.keys(mergedTheme[areaName]).forEach(function menuEntry(menuName) {
[ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) {
_.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
var createdFormSection = false;
var mergedThemeMenu = mergedTheme[areaName][menuName];
var mergedThemeMenu = mergedTheme[sectionName][menuName];
if(_.has(theme, [ 'customization', areaName, menuName ])) {
var menuTheme = theme.customization[areaName][menuName];
if(_.has(theme, [ 'customization', sectionName, menuName ])) {
var menuTheme = theme.customization[sectionName][menuName];
// config block is direct assign/overwrite
// :TODO: should probably be _.merge()
@ -217,7 +217,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
}
if('menus' === areaName) {
if('menus' === sectionName) {
if(_.isObject(mergedThemeMenu.form)) {
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
@ -233,7 +233,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
createdFormSection = true;
}
}
} else if('prompts' === areaName) {
} else if('prompts' === sectionName) {
// no 'form' or form keys for prompts -- direct to mci
applyToForm(mergedThemeMenu, menuTheme);
}
@ -247,7 +247,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
// * There is/was no explicit 'form' section
// * There is no 'prompt' specified
//
if('menus' === areaName && !_.isString(mergedThemeMenu.prompt) &&
if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
{
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );

View File

@ -56,7 +56,7 @@ function userLogin(client, username, password, cb) {
// update client logger with addition of username
client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username });
client.log.info('Successful login');
client.log.info('Successful login');
async.parallel(
[

View File

@ -27,7 +27,7 @@ function VerticalMenuView(options) {
this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row);
}
if(this.autoScale.width) {
if(self.autoScale.width) {
var l = 0;
self.items.forEach(function item(i) {
if(i.text.length > l) {
@ -148,6 +148,17 @@ VerticalMenuView.prototype.setFocus = function(focused) {
VerticalMenuView.prototype.setFocusItemIndex = function(index) {
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
//this.updateViewVisibleItems();
// :TODO: |viewWindow| must be updated to reflect position change --
// if > visibile then += by diff, if < visible
if(this.focusedItemIndex > this.viewWindow.bottom) {
} else if (this.focusedItemIndex < this.viewWindow.top) {
// this.viewWindow.top--;
// this.viewWindow.bottom--;
}
this.redraw();
};

View File

@ -488,7 +488,6 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) {
assert(_.isObject(options.mciMap));
var self = this;
var promptName = _.isString(options.promptName) ? options.promptName : self.client.currentMenuModule.menuConfig.prompt;
var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig;
var initialFocusId = 1; // default to first

View File

@ -16,15 +16,13 @@
return !isNaN(value) && user.getAge() >= value;
},
AS : function accountStatus() {
if(_.isNumber(value)) {
if(!_.isArray(value)) {
value = [ value ];
}
assert(_.isArray(value));
return _.findIndex(value, function cmp(accStatus) {
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
}) > -1;
const userAccountStatus = parseInt(user.properties.account_status, 10);
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(userAccountStatus) > -1;
},
EC : function isEncoding() {
switch(value) {
@ -53,7 +51,7 @@
// :TODO: implement me!!
return false;
},
SC : function isSecerConnection() {
SC : function isSecureConnection() {
return client.session.isSecure;
},
ML : function minutesLeft() {
@ -81,28 +79,20 @@
return !isNaN(value) && client.term.termWidth >= value;
},
ID : function isUserId(value) {
if(_.isNumber(value)) {
if(!_.isArray(value)) {
value = [ value ];
}
assert(_.isArray(value));
return _.findIndex(value, function cmp(uid) {
return user.userId === parseInt(uid, 10);
}) > -1;
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(user.userId) > -1;
},
WD : function isOneOfDayOfWeek() {
if(_.isNumber(value)) {
if(!_.isArray(value)) {
value = [ value ];
}
assert(_.isArray(value));
var nowDayOfWeek = new Date().getDay();
return _.findIndex(value, function cmp(dow) {
return nowDayOfWeek === parseInt(dow, 10);
}) > -1;
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(new Date().getDay()) > -1;
},
MM : function isMinutesPastMidnight() {
// :TODO: return true if value is >= minutes past midnight sys time

View File

@ -177,12 +177,6 @@ function AbracadabraModule(options) {
require('util').inherits(AbracadabraModule, MenuModule);
/*
AbracadabraModule.prototype.enter = function(client) {
AbracadabraModule.super_.prototype.enter.call(this, client);
};
*/
AbracadabraModule.prototype.leave = function() {
AbracadabraModule.super_.prototype.leave.call(this);

View File

@ -393,7 +393,7 @@
},
editorMode: edit
editorType: email
messageAreaName: private_mail
messageAreaTag: private_mail
toUserId: 1 /* always to +op */
}
form: {
@ -806,7 +806,7 @@
},
editorMode: edit
editorType: email
messageAreaName: private_mail
messageAreaTag: private_mail
toUserId: 1 /* always to +op */
}
form: {
@ -1019,6 +1019,10 @@
value: { command: "P" }
action: @menu:messageAreaNewPost
}
{
value: { command: "J" }
action: @menu:messageAreaChangeCurrentConference
}
{
value: { command: "C" }
action: @menu:messageAreaChangeCurrentArea
@ -1041,7 +1045,39 @@
}
]
}
messageAreaChangeCurrentConference: {
art: CCHANGE
module: msg_conf_list
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: conf
}
}
submit: {
*: [
{
value: { conf: null }
action: @method:changeConference
}
]
}
actionKeys: [
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
]
}
}
}
messageAreaChangeCurrentArea: {
// :TODO: rename this art to ACHANGE
art: CHANGE
module: msg_area_list
form: {
@ -1070,6 +1106,7 @@
}
}
}
messageAreaMessageList: {
module: msg_list
art: MSGLIST

View File

@ -5,7 +5,6 @@ var MenuModule = require('../core/menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController;
var messageArea = require('../core/message_area.js');
var strUtil = require('../core/string_util.js');
//var msgDb = require('./database.js').dbs.message;
var async = require('async');
var assert = require('assert');
@ -43,30 +42,33 @@ function MessageAreaListModule(options) {
var self = this;
this.messageAreas = messageArea.getAvailableMessageAreas();
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
self.client.user.properties.message_conf_tag,
{ client : self.client }
);
this.menuMethods = {
changeArea : function(formData, extraArgs) {
if(1 === formData.submitId) {
var areaName = self.messageAreas[formData.value.area].name;
const areaTag = self.messageAreas[formData.value.area].areaTag;
messageArea.changeMessageArea(self.client, areaName, function areaChanged(err) {
if(err) {
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
messageArea.changeMessageArea(self.client, areaTag, function areaChanged(err) {
if(err) {
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
setTimeout(function timeout() {
self.prevMenu();
}, 1000);
} else {
self.prevMenu();
}
});
setTimeout(function timeout() {
self.prevMenu();
}, 1000);
} else {
self.prevMenu();
}
});
}
}
};
this.setViewText = function(id, text) {
var v = self.viewControllers.areaList.getView(id);
const v = self.viewControllers.areaList.getView(id);
if(v) {
v.setText(text);
}
@ -78,7 +80,7 @@ require('util').inherits(MessageAreaListModule, MenuModule);
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
var self = this;
var vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series(
[
@ -99,26 +101,29 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
});
},
function populateAreaListView(callback) {
var listFormat = self.menuConfig.config.listFormat || '{index} ) - {desc}';
var focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
var areaListItems = [];
var focusListItems = [];
// :TODO: use _.map() here
for(var i = 0; i < self.messageAreas.length; ++i) {
areaListItems.push(listFormat.format(
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
);
focusListItems.push(focusListFormat.format(
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
);
}
var areaListView = vc.getView(1);
areaListView.setItems(areaListItems);
areaListView.setFocusItems(focusListItems);
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
const areaListView = vc.getView(1);
let i = 1;
areaListView.setItems(_.map(self.messageAreas, v => {
return listFormat.format({
index : i++,
areaTag : v.area.areaTag,
name : v.area.name,
desc : v.area.desc,
});
}));
i = 1;
areaListView.setFocusItems(_.map(self.messageAreas, v => {
return focusListFormat.format({
index : i++,
areaTag : v.area.areaTag,
name : v.area.name,
desc : v.area.desc,
})
}));
areaListView.redraw();

View File

@ -56,11 +56,11 @@ function AreaPostFSEModule(options) {
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
AreaPostFSEModule.prototype.enter = function(client) {
AreaPostFSEModule.prototype.enter = function() {
if(_.isString(client.user.properties.message_area_name) && !_.isString(this.messageAreaName)) {
this.messageAreaName = client.user.properties.message_area_name;
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
this.messageAreaTag = this.client.user.properties.message_area_tag;
}
AreaPostFSEModule.super_.prototype.enter.call(this, client);
AreaPostFSEModule.super_.prototype.enter.call(this);
};

View File

@ -72,7 +72,7 @@ function AreaViewFSEModule(options) {
if(_.isString(extraArgs.menu)) {
var modOpts = {
extraArgs : {
messageAreaName : self.messageAreaName,
messageAreaTag : self.messageAreaTag,
replyToMessage : self.message,
}
};

122
mods/msg_conf_list.js Normal file
View File

@ -0,0 +1,122 @@
/* jslint node: true */
'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController;
var messageArea = require('../core/message_area.js');
var async = require('async');
var assert = require('assert');
var _ = require('lodash');
exports.getModule = MessageConfListModule;
exports.moduleInfo = {
name : 'Message Conference List',
desc : 'Module for listing / choosing message conferences',
author : 'NuSkooler',
};
var MciCodesIds = {
ConfList : 1,
CurrentConf : 2,
// :TODO:
// # areas in con
//
};
function MessageConfListModule(options) {
MenuModule.call(this, options);
var self = this;
this.messageConfs = messageArea.getSortedAvailMessageConferences(self.client);
this.menuMethods = {
changeConference : function(formData, extraArgs) {
if(1 === formData.submitId) {
const confTag = self.messageConfs[formData.value.conf].confTag;
messageArea.changeMessageConference(self.client, confTag, err => {
if(err) {
self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`);
setTimeout(function timeout() {
self.prevMenu();
}, 1000);
} else {
self.prevMenu();
}
});
}
}
};
this.setViewText = function(id, text) {
const v = self.viewControllers.areaList.getView(id);
if(v) {
v.setText(text);
}
};
}
require('util').inherits(MessageConfListModule, MenuModule);
MessageConfListModule.prototype.mciReady = function(mciData, cb) {
var self = this;
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
async.series(
[
function callParentMciReady(callback) {
MessageConfListModule.super_.prototype.mciReady.call(this, mciData, callback);
},
function loadFromConfig(callback) {
let loadOpts = {
callingMenu : self,
mciMap : mciData.menu,
formId : 0,
};
vc.loadFromMenuConfig(loadOpts, callback);
},
function populateConfListView(callback) {
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
const confListView = vc.getView(1);
let i = 1;
confListView.setItems(_.map(self.messageConfs, v => {
return listFormat.format({
index : i++,
confTag : v.conf.confTag,
name : v.conf.name,
desc : v.conf.desc,
});
}));
i = 1;
confListView.setFocusItems(_.map(self.messageConfs, v => {
return focusListFormat.format({
index : i++,
confTag : v.conf.confTag,
name : v.conf.name,
desc : v.conf.desc,
})
}));
confListView.redraw();
callback(null);
},
function populateTextViews(callback) {
// :TODO: populate other avail MCI, e.g. current conf name
callback(null);
}
],
function complete(err) {
cb(err);
}
);
};

View File

@ -52,15 +52,15 @@ function MessageListModule(options) {
var self = this;
var config = this.menuConfig.config;
this.messageAreaName = config.messageAreaName;
this.messageAreaTag = config.messageAreaTag;
if(options.extraArgs) {
//
// |extraArgs| can override |messageAreaName| provided by config
// |extraArgs| can override |messageAreaTag| provided by config
// as well as supply a pre-defined message list
//
if(options.extraArgs.messageAreaName) {
this.messageAreaName = options.extraArgs.messageAreaName;
if(options.extraArgs.messageAreaTag) {
this.messageAreaTag = options.extraArgs.messageAreaTag;
}
if(options.extraArgs.messageList) {
@ -73,7 +73,7 @@ function MessageListModule(options) {
if(1 === formData.submitId) {
var modOpts = {
extraArgs : {
messageAreaName : self.messageAreaName,
messageAreaTag : self.messageAreaTag,
messageList : self.messageList,
messageIndex : formData.value.message,
}
@ -94,15 +94,15 @@ function MessageListModule(options) {
require('util').inherits(MessageListModule, MenuModule);
MessageListModule.prototype.enter = function(client) {
MessageListModule.super_.prototype.enter.call(this, client);
MessageListModule.prototype.enter = function() {
MessageListModule.super_.prototype.enter.call(this);
//
// Config can specify |messageAreaName| else it comes from
// Config can specify |messageAreaTag| else it comes from
// the user's current area
//
if(!this.messageAreaName) {
this.messageAreaName = client.user.properties.message_area_name;
if(!this.messageAreaTag) {
this.messageAreaTag = this.client.user.properties.message_area_tag;
}
};
@ -110,6 +110,8 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
var self = this;
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
var firstNewEntryIndex;
async.series(
[
function callParentMciReady(callback) {
@ -130,7 +132,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
if(_.isArray(self.messageList)) {
callback(0 === self.messageList.length ? new Error('No messages in area') : null);
} else {
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaName, function msgs(err, msgList) {
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) {
if(msgList && 0 === msgList.length) {
callback(new Error('No messages in area'));
} else {
@ -141,7 +143,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
}
},
function getLastReadMesageId(callback) {
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaName, function lastRead(err, lastReadId) {
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) {
self.lastReadId = lastReadId || 0;
callback(null); // ignore any errors, e.g. missing value
});
@ -158,6 +160,13 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
var msgNum = 1;
function getMsgFmtObj(mle) {
if(_.isUndefined(firstNewEntryIndex) &&
mle.messageId > self.lastReadId)
{
firstNewEntryIndex = msgNum - 1;
}
return {
msgNum : msgNum++,
subj : mle.subject,
@ -180,14 +189,18 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
msgListView.on('index update', function indexUpdated(idx) {
self.setViewText(MciCodesIds.MsgSelNum, (idx + 1).toString());
});
msgListView.redraw();
if(firstNewEntryIndex > 0) {
msgListView.setFocusItemIndex(firstNewEntryIndex);
}
callback(null);
},
function populateOtherMciViews(callback) {
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByName(self.messageAreaName).desc);
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByTag(self.messageAreaTag).name);
self.setViewText(MciCodesIds.MsgSelNum, (vc.getView(MciCodesIds.MsgList).getData() + 1).toString());
self.setViewText(MciCodesIds.MsgTotal, self.messageList.length.toString());

View File

@ -5,7 +5,7 @@ var user = require('../core/user.js');
var theme = require('../core/theme.js');
var login = require('../core/system_menu_method.js').login;
var Config = require('../core/config.js').config;
var getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea;
var messageArea = require('../core/message_area.js');
var async = require('async');
@ -65,6 +65,16 @@ function NewUserAppModule(options) {
newUser.username = formData.value.username;
//
// We have to disable ACS checks for initial default areas as the user is not yet ready
//
var confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
var areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
// can't store undefined!
confTag = confTag || '';
areaTag = areaTag || '';
newUser.properties = {
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
@ -74,14 +84,12 @@ function NewUserAppModule(options) {
email_address : formData.value.email,
web_address : formData.value.web,
account_created : new Date().toISOString(),
message_area_name : getDefaultMessageArea().name,
message_conf_tag : confTag,
message_area_tag : areaTag,
term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth,
// :TODO: This is set in User.create() -- proabbly don't need it here:
//account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active,
term_width : self.client.term.termWidth,
// :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc.
@ -92,8 +100,8 @@ function NewUserAppModule(options) {
} else {
newUser.properties.theme_id = Config.defaults.theme;
}
// :TODO: .create() should also validate email uniqueness!
// :TODO: User.create() should validate email uniqueness!
newUser.create( { password : formData.value.password }, function created(err) {
if(err) {
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');

View File

@ -172,8 +172,8 @@
messageAreaChangeCurrentArea: {
config: {
listFormat: "|00|15{index} |07- |03{desc}"
focusListFormat: "|00|19|15{index} - {desc}"
listFormat: "|00|15{index} |07- |03{name}"
focusListFormat: "|00|19|15{index} - {name}"
}
mci: {
VM1: {
@ -310,6 +310,19 @@
}
}
}
newScanMessageList: {
config: {
listFormat: "|00|15 {msgNum:<5.5}|03{subj:<29.29} |15{from:<20.20} {ts}"
focusListFormat: "|00|19> |15{msgNum:<5.5}{subj:<29.29} {from:<20.20} {ts}"
dateTimeFormat: ddd MMM Do
}
mci: {
VM1: {
height: 14
}
}
}
}
}
}

View File

@ -2,7 +2,7 @@
'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule;
var userDb = require('../core/database.js').dbs.user;
//var userDb = require('../core/database.js').dbs.user;
var getUserList = require('../core/user.js').getUserList;
var ViewController = require('../core/view_controller.js').ViewController;

View File

@ -84,7 +84,6 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
return listFormat.format(oe);
}));
// :TODO: This is a hack until pipe codes are better implemented
onlineListView.focusItems = onlineListView.items;
onlineListView.redraw();

View File

@ -13,7 +13,7 @@ var assert = require('assert');
var argv = require('minimist')(process.argv.slice(2));
var ExitCodes = {
const ExitCodes = {
SUCCESS : 0,
ERROR : -1,
BAD_COMMAND : -2,
@ -28,9 +28,13 @@ function printUsage(command) {
usage =
'usage: oputil.js [--version] [--help]\n' +
' <command> [<args>]' +
'\n' +
'\n\n' +
'global args:\n' +
' --config PATH : specify config path';
' --config PATH : specify config path' +
'\n\n' +
'commands:\n' +
' user : User utilities' +
'\n';
break;
case 'user' :
@ -47,7 +51,7 @@ function printUsage(command) {
}
function initConfig(cb) {
var configPath = argv.config ? argv.config : config.getDefaultPath();
const configPath = argv.config ? argv.config : config.getDefaultPath();
config.init(configPath, cb);
}
@ -88,7 +92,7 @@ function handleUserCommand() {
assert(_.isNumber(userId));
assert(userId > 0);
var u = new user.User();
let u = new user.User();
u.userId = userId;
u.setNewAuthCredentials(argv.password, function credsSet(err) {