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 ## License
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) 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. All rights reserved.
Redistribution and use in source and binary forms, with or without 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; return !isNaN(value) && user.getAge() >= value;
}, },
AS : function accountStatus() { AS : function accountStatus() {
if(!_.isArray(value)) {
if(_.isNumber(value)) {
value = [ value ]; value = [ value ];
} }
assert(_.isArray(value)); const userAccountStatus = parseInt(user.properties.account_status, 10);
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return _.findIndex(value, function cmp(accStatus) { return value.indexOf(userAccountStatus) > -1;
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
}) > -1;
}, },
EC : function isEncoding() { EC : function isEncoding() {
switch(value) { switch(value) {
@ -842,7 +839,7 @@ module.exports = (function() {
// :TODO: implement me!! // :TODO: implement me!!
return false; return false;
}, },
SC : function isSecerConnection() { SC : function isSecureConnection() {
return client.session.isSecure; return client.session.isSecure;
}, },
ML : function minutesLeft() { ML : function minutesLeft() {
@ -870,16 +867,20 @@ module.exports = (function() {
return !isNaN(value) && client.term.termWidth >= value; return !isNaN(value) && client.term.termWidth >= value;
}, },
ID : function isUserId(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() { WD : function isOneOfDayOfWeek() {
// :TODO: return true if DoW if(!_.isArray(value)) {
if(_.isNumber(value)) { value = [ value ];
} else if(_.isArray(value)) {
} }
return false;
value = value.map(n => parseInt(n, 10)); // ensure we have integers
return value.indexOf(new Date().getDay()) > -1;
}, },
MM : function isMinutesPastMidnight() { MM : function isMinutesPastMidnight() {
// :TODO: return true if value is >= minutes past midnight sys time // :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 _ = require('lodash');
var assert = require('assert'); var assert = require('assert');
exports.checkAcs = checkAcs;
exports.getConditionalValue = getConditionalValue; exports.getConditionalValue = getConditionalValue;
function checkAcs(client, acsString) {
return acsParser.parse(acsString, { client : client } );
}
function getConditionalValue(client, condArray, memberName) { function getConditionalValue(client, condArray, memberName) {
assert(_.isObject(client)); assert(_.isObject(client));
assert(_.isArray(condArray)); assert(_.isArray(condArray));

View File

@ -12,6 +12,7 @@ var events = require('events');
var util = require('util'); var util = require('util');
var ansi = require('./ansi_term.js'); var ansi = require('./ansi_term.js');
var aep = require('./ansi_escape_parser.js'); var aep = require('./ansi_escape_parser.js');
var sauce = require('./sauce.js');
var _ = require('lodash'); var _ = require('lodash');
@ -20,10 +21,6 @@ exports.getArtFromPath = getArtFromPath;
exports.display = display; exports.display = display;
exports.defaultEncodingFromExtension = defaultEncodingFromExtension; 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: Return MCI code information
// :TODO: process SAUCE comments // :TODO: process SAUCE comments
// :TODO: return font + font mapped information from SAUCE // :TODO: return font + font mapped information from SAUCE
@ -43,156 +40,6 @@ var SUPPORTED_ART_TYPES = {
// :TODO: extension for topaz ansi/ascii. // :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) { function getFontNameFromSAUCE(sauce) {
if(sauce.Character) { if(sauce.Character) {
return sauce.Character.fontName; return sauce.Character.fontName;
@ -249,7 +96,7 @@ function getArtFromPath(path, options, cb) {
} }
if(options.readSauce === true) { if(options.readSauce === true) {
readSAUCE(data, function onSauce(err, sauce) { sauce.readSAUCE(data, function onSauce(err, sauce) {
if(err) { if(err) {
cb(null, getResult()); cb(null, getResult());
} else { } else {

View File

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

View File

@ -1,6 +1,9 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
//var SegfaultHandler = require('segfault-handler');
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
// ENiGMA½ // ENiGMA½
var conf = require('./config.js'); var conf = require('./config.js');
var logger = require('./logger.js'); var logger = require('./logger.js');
@ -21,7 +24,7 @@ function bbsMain() {
async.waterfall( async.waterfall(
[ [
function processArgs(callback) { function processArgs(callback) {
var args = parseArgs(); const args = parseArgs();
var configPath; var configPath;
@ -37,8 +40,7 @@ function bbsMain() {
} }
} }
var configPathSupplied = _.isString(configPath); callback(null, configPath || conf.getDefaultPath(), _.isString(configPath));
callback(null, configPath || conf.getDefaultPath(), configPathSupplied);
}, },
function initConfig(configPath, configPathSupplied, callback) { function initConfig(configPath, configPathSupplied, callback) {
conf.init(configPath, function configInit(err) { conf.init(configPath, function configInit(err) {
@ -117,7 +119,7 @@ function initialize(cb) {
process.exit(); process.exit();
}); });
// Init some extensions // Init some extensions
require('string-format').extend(String.prototype, require('./string_util.js').stringFormatExtensions); 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 async = require('async');
var _ = require('lodash'); var _ = require('lodash');
var hjson = require('hjson'); var hjson = require('hjson');
var assert = require('assert');
exports.init = init; exports.init = init;
exports.getDefaultPath = getDefaultPath; 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) { function init(configPath, cb) {
async.waterfall( async.waterfall(
[ [
@ -48,18 +75,13 @@ function init(configPath, cb) {
// //
// Various sections must now exist in config // Various sections must now exist in config
// //
if(!_.has(mergedConfig, 'messages.areas.') || if(hasMessageConferenceAndArea(mergedConfig)) {
!_.isArray(mergedConfig.messages.areas) || var msgAreasErr = new Error('Please create at least one message conference and area!');
0 === mergedConfig.messages.areas.length ||
!_.isString(mergedConfig.messages.areas[0].name))
{
var msgAreasErr = new Error('Please create at least one message area');
msgAreasErr.code = 'EBADCONFIG'; msgAreasErr.code = 'EBADCONFIG';
callback(msgAreasErr); callback(msgAreasErr);
return; } else {
} callback(null, mergedConfig);
}
callback(null, mergedConfig);
} }
], ],
function complete(err, mergedConfig) { function complete(err, mergedConfig) {
@ -150,6 +172,7 @@ function getDefaultConfig() {
paths : { paths : {
mods : paths.join(__dirname, './../mods/'), mods : paths.join(__dirname, './../mods/'),
servers : paths.join(__dirname, './servers/'), servers : paths.join(__dirname, './servers/'),
msgNetworks : paths.join(__dirname, './msg_networks/'),
art : paths.join(__dirname, './../mods/art/'), art : paths.join(__dirname, './../mods/art/'),
themes : paths.join(__dirname, './../mods/themes/'), themes : paths.join(__dirname, './../mods/themes/'),
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
@ -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 : { messages : {
areas : [ areas : [
{ name : 'private_mail', desc : 'Private Email', groups : [ 'users' ] } { name : 'private_mail', desc : 'Private Email', groups : [ 'users' ] }

View File

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

View File

@ -1,9 +1,10 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var MailPacket = require('./mail_packet.js'); //var MailPacket = require('./mail_packet.js');
var ftn = require('./ftn_util.js'); var ftn = require('./ftn_util.js');
var Message = require('./message.js'); var Message = require('./message.js');
var sauce = require('./sauce.js');
var _ = require('lodash'); var _ = require('lodash');
var assert = require('assert'); var assert = require('assert');
@ -12,6 +13,8 @@ var fs = require('fs');
var util = require('util'); var util = require('util');
var async = require('async'); var async = require('async');
var iconv = require('iconv-lite'); var iconv = require('iconv-lite');
var buffers = require('buffers');
var moment = require('moment');
/* /*
:TODO: should probably be broken up :TODO: should probably be broken up
@ -20,6 +23,493 @@ var iconv = require('iconv-lite');
FTNPacketExport: message(s) -> packet 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 // References
// * http://ftsc.org/docs/fts-0001.016 // * http://ftsc.org/docs/fts-0001.016
@ -30,7 +520,7 @@ var iconv = require('iconv-lite');
// //
function FTNMailPacket(options) { function FTNMailPacket(options) {
MailPacket.call(this, options); //MailPacket.call(this, options);
var self = this; var self = this;
self.KLUDGE_PREFIX = '\x01'; self.KLUDGE_PREFIX = '\x01';
@ -77,7 +567,7 @@ function FTNMailPacket(options) {
.word16lu('second') .word16lu('second')
.word16lu('baud') .word16lu('baud')
.word16lu('packetType') .word16lu('packetType')
.word16lu('originNet') .word16lu('origNet')
.word16lu('destNet') .word16lu('destNet')
.word8('prodCodeLo') .word8('prodCodeLo')
.word8('revisionMajor') // aka serialNo .word8('revisionMajor') // aka serialNo
@ -100,35 +590,110 @@ function FTNMailPacket(options) {
// :TODO: Don't hard code magic # here // :TODO: Don't hard code magic # here
if(2 !== packetHeader.packetType) { if(2 !== packetHeader.packetType) {
console.log(packetHeader.packetType)
cb(new Error('Packet is not Type-2')); cb(new Error('Packet is not Type-2'));
return; 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 // :TODO: validate & pass error if failure
cb(null, packetHeader); 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 = { var meta = {
FtnKludge : msgBody.kludgeLines, FtnKludge : msgBody.kludgeLines,
FtnProperty : {}, FtnProperty : {},
}; };
if(msgBody.tearLine) { if(msgBody.tearLine) {
meta.FtnProperty.ftn_tear_line = [ msgBody.tearLine ]; meta.FtnProperty.ftn_tear_line = msgBody.tearLine;
} }
if(msgBody.seenBy.length > 0) { if(msgBody.seenBy.length > 0) {
meta.FtnProperty.ftn_seen_by = msgBody.seenBy; meta.FtnProperty.ftn_seen_by = msgBody.seenBy;
} }
if(msgBody.area) { if(msgBody.area) {
meta.FtnProperty.ftn_area = [ msgBody.area ]; meta.FtnProperty.ftn_area = msgBody.area;
} }
if(msgBody.originLine) { 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; return meta;
}; };
@ -172,13 +737,15 @@ function FTNMailPacket(options) {
var preOrigin = true; var preOrigin = true;
function addKludgeLine(kl) { function addKludgeLine(kl) {
var kludgeParts = kl.split(':'); const kludgeParts = kl.split(':');
kludgeParts[0] = kludgeParts[0].toUpperCase(); kludgeParts[0] = kludgeParts[0].toUpperCase();
kludgeParts[1] = kludgeParts[1].trim(); 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) { msgLines.forEach(function nextLine(line) {
if(0 === line.length) { if(0 === line.length) {
msgBody.message.push(''); msgBody.message.push('');
@ -196,10 +763,12 @@ function FTNMailPacket(options) {
preOrigin = false; preOrigin = false;
} else if(self.KLUDGE_PREFIX === line.charAt(0)) { } else if(self.KLUDGE_PREFIX === line.charAt(0)) {
addKludgeLine(line.slice(1)); addKludgeLine(line.slice(1));
} else if(!sauceBuffers || _.startsWith(line, '\x1aSAUCE00')) {
sauceBuffers = sauceBuffers || buffers();
sauceBuffers.push(new Buffer(line));
} else { } else {
msgBody.message.push(line); msgBody.message.push(line);
} }
// :TODO: SAUCE/etc. can be present?
} else { } else {
if(_.startsWith(line, 'SEEN-BY:')) { if(_.startsWith(line, 'SEEN-BY:')) {
msgBody.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); 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); cb(null, msgBody);
}; };
this.extractMessages = function(buffer, cb) { this.extractMessages = function(buffer, iterator, cb) {
var nullTermBuf = new Buffer( [ 0 ] ); assert(Buffer.isBuffer(buffer));
assert(_.isFunction(iterator));
const NULL_TERM_BUFFER = new Buffer( [ 0 ] );
binary.stream(buffer).loop(function looper(end, vars) { binary.stream(buffer).loop(function looper(end, vars) {
this this
.word16lu('messageType') .word16lu('messageType')
.word16lu('originNode') .word16lu('origNode')
.word16lu('destNode') .word16lu('destNode')
.word16lu('originNet') .word16lu('origNet')
.word16lu('destNet') .word16lu('destNet')
.word8('attrFlags1') .word8('attrFlags1')
.word8('attrFlags2') .word8('attrFlags2')
.word16lu('cost') .word16lu('cost')
.scan('modDateTime', nullTermBuf) .scan('modDateTime', NULL_TERM_BUFFER)
.scan('toUserName', nullTermBuf) .scan('toUserName', NULL_TERM_BUFFER)
.scan('fromUserName', nullTermBuf) .scan('fromUserName', NULL_TERM_BUFFER)
.scan('subject', nullTermBuf) .scan('subject', NULL_TERM_BUFFER)
.scan('message', nullTermBuf) .scan('message', NULL_TERM_BUFFER)
.tap(function tapped(msgData) { .tap(function tapped(msgData) {
if(!msgData.originNode) { if(!msgData.origNode) {
end(); end();
cb(null); cb(null);
return; return;
@ -247,20 +823,25 @@ function FTNMailPacket(options) {
// Now, create a Message object // Now, create a Message object
// //
var msg = new Message( { 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, toUserName : msgData.toUserName,
fromUserName : msgData.fromUserName, fromUserName : msgData.fromUserName,
subject : msgData.subject, subject : msgData.subject,
message : msgBody.message.join('\n'), // :TODO: \r\n is better? message : msgBody.message.join('\n'), // :TODO: \r\n is better?
modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), 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) { this.parseFtnMessages = function(buffer, cb) {
var nullTermBuf = new Buffer( [ 0 ] ); var nullTermBuf = new Buffer( [ 0 ] );
@ -269,9 +850,9 @@ function FTNMailPacket(options) {
binary.stream(buffer).loop(function looper(end, vars) { binary.stream(buffer).loop(function looper(end, vars) {
this this
.word16lu('messageType') .word16lu('messageType')
.word16lu('originNode') .word16lu('origNode')
.word16lu('destNode') .word16lu('destNode')
.word16lu('originNet') .word16lu('origNet')
.word16lu('destNet') .word16lu('destNet')
.word8('attrFlags1') .word8('attrFlags1')
.word8('attrFlags2') .word8('attrFlags2')
@ -282,7 +863,7 @@ function FTNMailPacket(options) {
.scan('subject', nullTermBuf) .scan('subject', nullTermBuf)
.scan('message', nullTermBuf) .scan('message', nullTermBuf)
.tap(function tapped(msgData) { .tap(function tapped(msgData) {
if(!msgData.originNode) { if(!msgData.origNode) {
end(); end();
cb(null, fidoMessages); cb(null, fidoMessages);
return; 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( async.waterfall(
[ [
function parseHeader(callback) { function parseHeader(callback) {
@ -318,7 +902,8 @@ function FTNMailPacket(options) {
}, },
function extractEmbeddedMessages(callback) { function extractEmbeddedMessages(callback) {
// note: packet header is 58 bytes in length // 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); 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) { FTNMailPacket.prototype.parse = function(path, cb) {
var self = this; var self = this;
@ -385,41 +970,67 @@ FTNMailPacket.prototype.parse = function(path, cb) {
); );
}; };
FTNMailPacket.prototype.read = function(options) { FTNMailPacket.prototype.read = function(pathOrBuffer, iterator, cb) {
FTNMailPacket.super_.prototype.read.call(this, options);
var self = this; var self = this;
if(_.isString(options.packetPath)) { if(_.isString(pathOrBuffer)) {
async.waterfall( async.waterfall(
[ [
function readPacketFile(callback) { function readPacketFile(callback) {
fs.readFile(options.packetPath, function packetData(err, data) { fs.readFile(pathOrBuffer, function packetData(err, data) {
callback(err, data); callback(err, data);
}); });
}, },
function extractMessages(data, callback) { function extractMessages(data, callback) {
self.extractMesssagesFromPacketBuffer(data, function extracted(err) { self.extractMesssagesFromPacketBuffer(data, iterator, callback);
callback(err);
});
} }
], ],
function complete(err) { cb
if(err) {
self.emit('error', err);
}
}
); );
} else if(Buffer.isBuffer(options.packetBuffer)) { } else if(Buffer.isBuffer(pathOrBuffer)) {
} }
}; };
FTNMailPacket.prototype.write = function(options) { FTNMailPacket.prototype.write = function(messages, fileName, options) {
FTNMailPacket.super_.prototype.write.call(this, 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( var mailPacket = new FTNMailPacket(
{ {
nodeAddresses : { 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) { 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 fs = require('fs');
var util = require('util'); var util = require('util');
var iconv = require('iconv-lite'); var iconv = require('iconv-lite');
var moment = require('moment');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module // :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringFromFTN = stringFromFTN; exports.stringFromFTN = stringFromFTN;
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getFormattedFTNAddress = getFormattedFTNAddress; exports.getFormattedFTNAddress = getFormattedFTNAddress;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime; exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString;
exports.getQuotePrefix = getQuotePrefix; exports.getQuotePrefix = getQuotePrefix;
@ -33,6 +36,14 @@ function stringFromFTN(buf, encoding) {
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8'); 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 // Convert a FTN style DateTime string to a Date object
@ -44,9 +55,34 @@ function getDateFromFtnDateTime(dateTime) {
// "Tue 01 Jan 80 00:00" // "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03" // "27 Feb 15 00:00:03"
// //
// :TODO: Use moment.js here
return (new Date(Date.parse(dateTime))).toISOString(); 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) { function getFormattedFTNAddress(address, dimensions) {
//var addr = util.format('%d:%d', address.zone, address.net); //var addr = util.format('%d:%d', address.zone, address.net);
var addr = '{0}:{1}'.format(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; var self = this;
this.menuName = options.menuName; this.menuName = options.menuName;
this.menuConfig = options.menuConfig; 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.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's this.menuMethods = {}; // methods called from @method's
@ -190,10 +194,7 @@ require('util').inherits(MenuModule, PluginModule);
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype); require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
MenuModule.prototype.enter = function(client) { MenuModule.prototype.enter = function() {
this.client = client;
assert(_.isObject(client));
if(_.isString(this.menuConfig.status)) { if(_.isString(this.menuConfig.status)) {
this.client.currentStatus = this.menuConfig.status; this.client.currentStatus = this.menuConfig.status;
} else { } else {

View File

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

View File

@ -4,10 +4,8 @@
// ENiGMA½ // ENiGMA½
var moduleUtil = require('./module_util.js'); var moduleUtil = require('./module_util.js');
var Log = require('./logger.js').log; var Log = require('./logger.js').log;
var conf = require('./config.js'); // :TODO: remove me!
var Config = require('./config.js').config; var Config = require('./config.js').config;
var asset = require('./asset.js'); var asset = require('./asset.js');
var theme = require('./theme.js');
var getFullConfig = require('./config_util.js').getFullConfig; var getFullConfig = require('./config_util.js').getFullConfig;
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
var acsUtil = require('./acs_util.js'); var acsUtil = require('./acs_util.js');
@ -68,17 +66,18 @@ function loadMenu(options, cb) {
}); });
}, },
function loadMenuModule(menuConfig, callback) { 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', name : modSupplied ? modAsset.asset : 'standard_menu',
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods, path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods,
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
}; };
moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) { moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) {
var modData = { const modData = {
name : modLoadOpts.name, name : modLoadOpts.name,
config : menuConfig, config : menuConfig,
mod : mod, mod : mod,
@ -97,7 +96,8 @@ function loadMenu(options, cb) {
{ {
menuName : options.name, menuName : options.name,
menuConfig : modData.config, menuConfig : modData.config,
extraArgs : options.extraArgs extraArgs : options.extraArgs,
client : options.client,
}); });
callback(null, moduleInstance); callback(null, moduleInstance);
} catch(e) { } catch(e) {
@ -174,7 +174,7 @@ function handleAction(client, formData, conf) {
assert(_.isObject(conf)); assert(_.isObject(conf));
assert(_.isString(conf.action)); assert(_.isString(conf.action));
var actionAsset = asset.parseAsset(conf.action); const actionAsset = asset.parseAsset(conf.action);
assert(_.isObject(actionAsset)); assert(_.isObject(actionAsset));
switch(actionAsset.type) { switch(actionAsset.type) {
@ -245,84 +245,3 @@ function handleNext(client, nextSpec, conf) {
break; 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 || {}; options = options || {};
this.messageId = options.messageId || 0; // always generated @ persist 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.uuid = uuid.v1();
this.replyToMsgId = options.replyToMsgId || 0; this.replyToMsgId = options.replyToMsgId || 0;
this.toUserName = options.toUserName || ''; this.toUserName = options.toUserName || '';
@ -55,7 +55,7 @@ function Message(options) {
}; };
this.isPrivate = function() { this.isPrivate = function() {
return this.areaName === Message.WellKnownAreaNames.Private ? true : false; return this.areaTag === Message.WellKnownAreaTags.Private ? true : false;
}; };
this.getMessageTimestampString = function(ts) { this.getMessageTimestampString = function(ts) {
@ -80,7 +80,7 @@ function Message(options) {
*/ */
} }
Message.WellKnownAreaNames = { Message.WellKnownAreaTags = {
Invalid : '', Invalid : '',
Private : 'private_mail', Private : 'private_mail',
Bulletin : 'local_bulletin', Bulletin : 'local_bulletin',
@ -104,16 +104,21 @@ Message.SystemMetaNames = {
LocalFromUserID : 'local_from_user_id', LocalFromUserID : 'local_from_user_id',
}; };
Message.FtnPropertyNames = { Message.FtnPropertyNames = {
FtnCost : 'ftn_cost',
FtnOrigNode : 'ftn_orig_node', FtnOrigNode : 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node', FtnDestNode : 'ftn_dest_node',
FtnOrigNetwork : 'ftn_orig_network', FtnOrigNetwork : 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network', FtnDestNetwork : 'ftn_dest_network',
FtnAttrFlags1 : 'ftn_attr_flags1',
FtnAttrFlags2 : 'ftn_attr_flags2',
FtnCost : 'ftn_cost',
FtnOrigZone : 'ftn_orig_zone', FtnOrigZone : 'ftn_orig_zone',
FtnDestZone : 'ftn_dest_zone', FtnDestZone : 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point', FtnOrigPoint : 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point', FtnDestPoint : 'ftn_dest_point',
FtnAttribute : 'ftn_attribute', FtnAttribute : 'ftn_attribute',
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
@ -141,7 +146,7 @@ Message.prototype.load = function(options, cb) {
[ [
function loadMessage(callback) { function loadMessage(callback) {
msgDb.get( 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 ' + 'message, modified_timestamp, view_count ' +
'FROM message ' + 'FROM message ' +
'WHERE message_uuid=? ' + 'WHERE message_uuid=? ' +
@ -149,7 +154,7 @@ Message.prototype.load = function(options, cb) {
[ options.uuid ], [ options.uuid ],
function row(err, msgRow) { function row(err, msgRow) {
self.messageId = msgRow.message_id; self.messageId = msgRow.message_id;
self.areaName = msgRow.area_name; self.areaTag = msgRow.area_tag;
self.messageUuid = msgRow.message_uuid; self.messageUuid = msgRow.message_uuid;
self.replyToMsgId = msgRow.reply_to_message_id; self.replyToMsgId = msgRow.reply_to_message_id;
self.toUserName = msgRow.to_user_name; self.toUserName = msgRow.to_user_name;
@ -202,8 +207,8 @@ Message.prototype.persist = function(cb) {
}, },
function storeMessage(callback) { function storeMessage(callback) {
msgDb.run( msgDb.run(
'INSERT INTO message (area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) ' + 'INSERT INTO message (area_tag, 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) ], 'VALUES (?, ?, ?, ?, ?, ?, ?, ?);', [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
function msgInsert(err) { function msgInsert(err) {
if(!err) { if(!err) {
self.messageId = this.lastID; self.messageId = this.lastID;

View File

@ -5,100 +5,275 @@ var msgDb = require('./database.js').dbs.message;
var Config = require('./config.js').config; var Config = require('./config.js').config;
var Message = require('./message.js'); var Message = require('./message.js');
var Log = require('./logger.js').log; var Log = require('./logger.js').log;
var checkAcs = require('./acs_util.js').checkAcs;
var async = require('async'); var async = require('async');
var _ = require('lodash'); var _ = require('lodash');
var assert = require('assert'); var assert = require('assert');
exports.getAvailableMessageAreas = getAvailableMessageAreas; exports.getAvailableMessageConferences = getAvailableMessageConferences;
exports.getDefaultMessageArea = getDefaultMessageArea; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
exports.getMessageAreaByName = getMessageAreaByName; 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.changeMessageArea = changeMessageArea;
exports.getMessageListForArea = getMessageListForArea; exports.getMessageListForArea = getMessageListForArea;
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
exports.getMessageAreaLastReadId = getMessageAreaLastReadId; exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
function getAvailableMessageAreas(options) { const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]';
// example: [ { "name" : "local_music", "desc" : "Music Discussion", "groups" : ["somegroup"] }, ... ] const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]';
options = options || {};
var areas = Config.messages.areas; const AREA_ACS_DEFAULT = {
var avail = []; read : CONF_AREA_RW_ACS_DEFAULT,
for(var i = 0; i < areas.length; ++i) { write : CONF_AREA_RW_ACS_DEFAULT,
if(true !== options.includePrivate && manage : AREA_MANAGE_ACS_DEFAULT,
Message.WellKnownAreaNames.Private === areas[i].name) };
{
continue;
}
avail.push(areas[i]); function getAvailableMessageConferences(client, options) {
} options = options || { includeSystemInternal : false };
return avail; // 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() { function getSortedAvailMessageConferences(client, options) {
// var sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
// Return first non-private/etc. area name. This will be from config.hjson return {
// confTag : k,
return getAvailableMessageAreas()[0]; conf : v,
/* };
var avail = getAvailableMessageAreas(); });
for(var i = 0; i < avail.length; ++i) {
if(Message.WellKnownAreaNames.Private !== avail[i].name) { sorted.sort((a, b) => {
return avail[i]; return a.conf.name.localeCompare(b.conf.name);
}
}
*/
}
function getMessageAreaByName(areaName) {
areaName = areaName.toLowerCase();
var availAreas = getAvailableMessageAreas( { includePrivate : true } );
var index = _.findIndex(availAreas, function pred(an) {
return an.name == areaName;
}); });
if(index > -1) { return sorted;
return availAreas[index];
}
} }
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( async.waterfall(
[ [
function getArea(callback) { function getArea(callback) {
var area = getMessageAreaByName(areaName); const area = getMessageAreaByTag(areaTag);
if(area) { if(area) {
callback(null, area); callback(null, area);
} else { } else {
callback(new Error('Invalid message area')); callback(new Error('Invalid message area tag'));
} }
}, },
function validateAccess(area, callback) { 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')); callback(new Error('User does not have access to this area'));
} else { } else {
callback(null, area); callback(null, area);
} }
}, },
function changeArea(area, callback) { 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); callback(err, area);
}); });
} }
], ],
function complete(err, area) { function complete(err, area) {
if(!err) { if(!err) {
client.log.info( area, 'Current message area changed'); client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
} else { } 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); 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 addressed to |userId| should be returned.
// //
// Only messages > lastMessageId should be returned // Only messages > lastMessageId should be returned
@ -131,7 +306,7 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
async.waterfall( async.waterfall(
[ [
function getLastMessageId(callback) { 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! callback(null, lastMessageId || 0); // note: willingly ignoring any errors here!
}); });
}, },
@ -139,9 +314,9 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
var sql = var sql =
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' + 'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
'FROM message ' + '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 += sql +=
' AND message_id in (' + ' AND message_id in (' +
'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System + '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;'; sql += ' ORDER BY message_id;';
console.log(sql)
msgDb.each(sql, function msgRow(err, row) { msgDb.each(sql, function msgRow(err, row) {
if(!err) { if(!err) {
msgList.push(getMessageFromRow(row)); msgList.push(getMessageFromRow(row));
@ -160,18 +333,17 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
} }
], ],
function complete(err) { function complete(err) {
console.log(msgList)
cb(err, msgList); cb(err, msgList);
} }
); );
} }
function getMessageListForArea(options, areaName, cb) { function getMessageListForArea(options, areaTag, cb) {
// //
// options.client (required) // 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)); assert(_.isObject(options.client));
@ -193,9 +365,9 @@ function getMessageListForArea(options, areaName, cb) {
msgDb.each( msgDb.each(
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' + 'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
'FROM message ' + 'FROM message ' +
'WHERE area_name=? ' + 'WHERE area_tag = ? ' +
'ORDER BY message_id;', 'ORDER BY message_id;',
[ areaName.toLowerCase() ], [ areaTag.toLowerCase() ],
function msgRow(err, row) { function msgRow(err, row) {
if(!err) { if(!err) {
msgList.push(getMessageFromRow(row)); 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( msgDb.get(
'SELECT message_id ' + 'SELECT message_id ' +
'FROM user_message_area_last_read ' + 'FROM user_message_area_last_read ' +
'WHERE user_id = ? AND area_name = ?;', 'WHERE user_id = ? AND area_tag = ?;',
[ userId, areaName ], [ userId, areaTag ],
function complete(err, row) { function complete(err, row) {
cb(err, row ? row.message_id : 0); 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... // :TODO: likely a better way to do this...
async.waterfall( async.waterfall(
[ [
function getCurrent(callback) { function getCurrent(callback) {
getMessageAreaLastReadId(userId, areaName, function result(err, lastId) { getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
lastId = lastId || 0; lastId = lastId || 0;
callback(null, lastId); // ignore errors as we default to 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) { function update(lastId, callback) {
if(messageId > lastId) { if(messageId > lastId) {
msgDb.run( 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 (?, ?, ?);', 'VALUES (?, ?, ?);',
[ userId, areaName, messageId ], [ userId, areaTag, messageId ],
callback function written(err) {
callback(err, true); // true=didUpdate
}
); );
} else { } else {
callback(null); callback(null);
} }
} }
], ],
function complete(err) { function complete(err, didUpdate) {
if(err) { if(err) {
Log.debug( 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'); 'Failed updating area last read ID');
} else { } else {
Log.trace( if(true === didUpdate) {
{ userId : userId, areaName : areaName, messageId : messageId }, Log.trace(
'Area last read ID updated'); { userId : userId, areaTag : areaTag, messageId : messageId },
'Area last read ID updated');
}
} }
cb(err); cb(err);
} }

View File

@ -14,38 +14,40 @@ exports.loadModuleEx = loadModuleEx;
exports.loadModule = loadModule; exports.loadModule = loadModule;
exports.loadModulesForCategory = loadModulesForCategory; exports.loadModulesForCategory = loadModulesForCategory;
function loadModuleEx(options, cb) { function loadModuleEx(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
assert(_.isString(options.name)); assert(_.isString(options.name));
assert(_.isString(options.path)); 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) { if(_.isObject(modConfig) && false === modConfig.enabled) {
cb(new Error('Module "' + options.name + '" is disabled')); cb(new Error('Module "' + options.name + '" is disabled'));
return; return;
} }
var mod;
try { try {
var mod = require(paths.join(options.path, options.name + '.js')); 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);
} catch(e) { } catch(e) {
cb(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) { 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]; var path = Config.paths[category];
fs.readdir(path, function onFiles(err, files) { 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); }); var filtered = files.filter(function onFilter(file) { return '.js' === paths.extname(file); });
filtered.forEach(function onFile(file) { filtered.forEach(function onFile(file) {
var modName = paths.basename(file, '.js'); loadModule(paths.basename(file, '.js'), category, iterator);
loadModule(paths.basename(file, '.js'), category, cb);
}); });
}); });
} }

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 MenuModule = require('./menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController; var ViewController = require('../core/view_controller.js').ViewController;
var _ = require('lodash');
var async = require('async'); var async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
@ -36,10 +37,11 @@ function NewScanModule(options) {
var self = this; var self = this;
var config = this.menuConfig.config; var config = this.menuConfig.config;
this.currentStep = 'messageAreas'; this.currentStep = 'messageConferences';
this.currentScanAux = 0; // e.g. Message.WellKnownAreaNames.Private when currentSteps = messageAreas 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.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
@ -57,10 +59,65 @@ function NewScanModule(options) {
if(view) { 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) { self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => {
var availMsgAreas = msgArea.getAvailableMessageAreas( { includePrivate : true } ); return {
var currentArea = availMsgAreas[self.currentScanAux]; 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, // Scan and update index until we find something. If results are found,
@ -70,8 +127,8 @@ function NewScanModule(options) {
[ [
function checkAndUpdateIndex(callback) { function checkAndUpdateIndex(callback) {
// Advance to next area if possible // Advance to next area if possible
if(availMsgAreas.length >= self.currentScanAux + 1) { if(sortedAreas.length >= self.currentScanAux.area + 1) {
self.currentScanAux += 1; self.currentScanAux.area += 1;
callback(null); callback(null);
} else { } else {
self.updateScanStatus(self.scanCompleteMsg); self.updateScanStatus(self.scanCompleteMsg);
@ -80,22 +137,30 @@ function NewScanModule(options) {
}, },
function updateStatusScanStarted(callback) { function updateStatusScanStarted(callback) {
self.updateScanStatus(self.scanStartFmt.format({ 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); callback(null);
}, },
function newScanAreaAndGetMessages(callback) { function newScanAreaAndGetMessages(callback) {
msgArea.getNewMessagesInAreaForUser( 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(!err) {
if(0 === msgList.length) { if(0 === msgList.length) {
self.updateScanStatus(self.scanFinishNoneFmt.format({ 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 { } else {
self.updateScanStatus(self.scanFinishNewFmt.format({ self.updateScanStatus(self.scanFinishNewFmt.format({
desc : currentArea.desc, confName : conf.conf.name,
count : msgList.length, confDesc : conf.conf.desc,
areaName : currentArea.area.name,
count : msgList.length,
})); }));
} }
} }
@ -107,14 +172,14 @@ function NewScanModule(options) {
if(msgList && msgList.length > 0) { if(msgList && msgList.length > 0) {
var nextModuleOpts = { var nextModuleOpts = {
extraArgs: { extraArgs: {
messageAreaName : currentArea.name, messageAreaTag : currentArea.areaTag,
messageList : msgList, messageList : msgList,
} }
}; };
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts); self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
} else { } else {
self.newScanMessageArea(cb); self.newScanMessageArea(conf, cb);
} }
} }
], ],
@ -161,10 +226,10 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
}, },
function performCurrentStepScan(callback) { function performCurrentStepScan(callback) {
switch(self.currentStep) { switch(self.currentStep) {
case 'messageAreas' : case 'messageConferences' :
self.newScanMessageArea(function scanComplete(err) { self.newScanMessageConference(function scanComplete(err) {
callback(null); // finished callback(null); // finished
}); });
break; break;
default : 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 Config = require('./config.js').config;
var Log = require('./logger.js').log; 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 clientConnections = require('./client_connections.js');
var sysProp = require('./system_property.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 : ''; return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
}, },
MA : function messageAreaDescription() { MA : function messageAreaName() {
var area = getMessageAreaByName(client.user.properties.message_area_name); const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.desc : ''; 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(); }, SH : function termHeight() { return client.term.termHeight.toString(); },
SW : function termWidth() { return client.term.termWidth.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); require('util').inherits(StandardMenuModule, MenuModule);
StandardMenuModule.prototype.enter = function(client) { StandardMenuModule.prototype.enter = function() {
StandardMenuModule.super_.prototype.enter.call(this, client); StandardMenuModule.super_.prototype.enter.call(this);
}; };
StandardMenuModule.prototype.beforeArt = function() { StandardMenuModule.prototype.beforeArt = function() {

View File

@ -203,13 +203,13 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
} }
} }
[ 'menus', 'prompts' ].forEach(function areaEntry(areaName) { [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) {
_.keys(mergedTheme[areaName]).forEach(function menuEntry(menuName) { _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
var createdFormSection = false; var createdFormSection = false;
var mergedThemeMenu = mergedTheme[areaName][menuName]; var mergedThemeMenu = mergedTheme[sectionName][menuName];
if(_.has(theme, [ 'customization', areaName, menuName ])) { if(_.has(theme, [ 'customization', sectionName, menuName ])) {
var menuTheme = theme.customization[areaName][menuName]; var menuTheme = theme.customization[sectionName][menuName];
// config block is direct assign/overwrite // config block is direct assign/overwrite
// :TODO: should probably be _.merge() // :TODO: should probably be _.merge()
@ -217,7 +217,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
} }
if('menus' === areaName) { if('menus' === sectionName) {
if(_.isObject(mergedThemeMenu.form)) { if(_.isObject(mergedThemeMenu.form)) {
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
@ -233,7 +233,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
createdFormSection = true; createdFormSection = true;
} }
} }
} else if('prompts' === areaName) { } else if('prompts' === sectionName) {
// no 'form' or form keys for prompts -- direct to mci // no 'form' or form keys for prompts -- direct to mci
applyToForm(mergedThemeMenu, menuTheme); applyToForm(mergedThemeMenu, menuTheme);
} }
@ -247,7 +247,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
// * There is/was no explicit 'form' section // * There is/was no explicit 'form' section
// * There is no 'prompt' specified // * There is no 'prompt' specified
// //
if('menus' === areaName && !_.isString(mergedThemeMenu.prompt) && if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
(createdFormSection || !_.isObject(mergedThemeMenu.form))) (createdFormSection || !_.isObject(mergedThemeMenu.form)))
{ {
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); 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 // update client logger with addition of username
client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.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( 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); 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; var l = 0;
self.items.forEach(function item(i) { self.items.forEach(function item(i) {
if(i.text.length > l) { if(i.text.length > l) {
@ -148,6 +148,17 @@ VerticalMenuView.prototype.setFocus = function(focused) {
VerticalMenuView.prototype.setFocusItemIndex = function(index) { VerticalMenuView.prototype.setFocusItemIndex = function(index) {
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex 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(); this.redraw();
}; };

View File

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

View File

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

View File

@ -393,7 +393,7 @@
}, },
editorMode: edit editorMode: edit
editorType: email editorType: email
messageAreaName: private_mail messageAreaTag: private_mail
toUserId: 1 /* always to +op */ toUserId: 1 /* always to +op */
} }
form: { form: {
@ -806,7 +806,7 @@
}, },
editorMode: edit editorMode: edit
editorType: email editorType: email
messageAreaName: private_mail messageAreaTag: private_mail
toUserId: 1 /* always to +op */ toUserId: 1 /* always to +op */
} }
form: { form: {
@ -1019,6 +1019,10 @@
value: { command: "P" } value: { command: "P" }
action: @menu:messageAreaNewPost action: @menu:messageAreaNewPost
} }
{
value: { command: "J" }
action: @menu:messageAreaChangeCurrentConference
}
{ {
value: { command: "C" } value: { command: "C" }
action: @menu:messageAreaChangeCurrentArea 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: { messageAreaChangeCurrentArea: {
// :TODO: rename this art to ACHANGE
art: CHANGE art: CHANGE
module: msg_area_list module: msg_area_list
form: { form: {
@ -1070,6 +1106,7 @@
} }
} }
} }
messageAreaMessageList: { messageAreaMessageList: {
module: msg_list module: msg_list
art: MSGLIST 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 ViewController = require('../core/view_controller.js').ViewController;
var messageArea = require('../core/message_area.js'); var messageArea = require('../core/message_area.js');
var strUtil = require('../core/string_util.js'); var strUtil = require('../core/string_util.js');
//var msgDb = require('./database.js').dbs.message;
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
@ -43,30 +42,33 @@ function MessageAreaListModule(options) {
var self = this; var self = this;
this.messageAreas = messageArea.getAvailableMessageAreas(); this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
self.client.user.properties.message_conf_tag,
{ client : self.client }
);
this.menuMethods = { this.menuMethods = {
changeArea : function(formData, extraArgs) { changeArea : function(formData, extraArgs) {
if(1 === formData.submitId) { 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) { messageArea.changeMessageArea(self.client, areaTag, function areaChanged(err) {
if(err) { if(err) {
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n'); self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
setTimeout(function timeout() { setTimeout(function timeout() {
self.prevMenu(); self.prevMenu();
}, 1000); }, 1000);
} else { } else {
self.prevMenu(); self.prevMenu();
} }
}); });
} }
} }
}; };
this.setViewText = function(id, text) { this.setViewText = function(id, text) {
var v = self.viewControllers.areaList.getView(id); const v = self.viewControllers.areaList.getView(id);
if(v) { if(v) {
v.setText(text); v.setText(text);
} }
@ -78,7 +80,7 @@ require('util').inherits(MessageAreaListModule, MenuModule);
MessageAreaListModule.prototype.mciReady = function(mciData, cb) { MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
var self = this; 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( async.series(
[ [
@ -99,26 +101,29 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
}); });
}, },
function populateAreaListView(callback) { function populateAreaListView(callback) {
var listFormat = self.menuConfig.config.listFormat || '{index} ) - {desc}'; const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
var areaListItems = []; const areaListView = vc.getView(1);
var focusListItems = []; let i = 1;
areaListView.setItems(_.map(self.messageAreas, v => {
// :TODO: use _.map() here return listFormat.format({
for(var i = 0; i < self.messageAreas.length; ++i) { index : i++,
areaListItems.push(listFormat.format( areaTag : v.area.areaTag,
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } ) name : v.area.name,
); desc : v.area.desc,
focusListItems.push(focusListFormat.format( });
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } ) }));
);
} i = 1;
areaListView.setFocusItems(_.map(self.messageAreas, v => {
var areaListView = vc.getView(1); return focusListFormat.format({
index : i++,
areaListView.setItems(areaListItems); areaTag : v.area.areaTag,
areaListView.setFocusItems(focusListItems); name : v.area.name,
desc : v.area.desc,
})
}));
areaListView.redraw(); areaListView.redraw();

View File

@ -56,11 +56,11 @@ function AreaPostFSEModule(options) {
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule); 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)) { if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
this.messageAreaName = client.user.properties.message_area_name; 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)) { if(_.isString(extraArgs.menu)) {
var modOpts = { var modOpts = {
extraArgs : { extraArgs : {
messageAreaName : self.messageAreaName, messageAreaTag : self.messageAreaTag,
replyToMessage : self.message, 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 self = this;
var config = this.menuConfig.config; var config = this.menuConfig.config;
this.messageAreaName = config.messageAreaName; this.messageAreaTag = config.messageAreaTag;
if(options.extraArgs) { 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 // as well as supply a pre-defined message list
// //
if(options.extraArgs.messageAreaName) { if(options.extraArgs.messageAreaTag) {
this.messageAreaName = options.extraArgs.messageAreaName; this.messageAreaTag = options.extraArgs.messageAreaTag;
} }
if(options.extraArgs.messageList) { if(options.extraArgs.messageList) {
@ -73,7 +73,7 @@ function MessageListModule(options) {
if(1 === formData.submitId) { if(1 === formData.submitId) {
var modOpts = { var modOpts = {
extraArgs : { extraArgs : {
messageAreaName : self.messageAreaName, messageAreaTag : self.messageAreaTag,
messageList : self.messageList, messageList : self.messageList,
messageIndex : formData.value.message, messageIndex : formData.value.message,
} }
@ -94,15 +94,15 @@ function MessageListModule(options) {
require('util').inherits(MessageListModule, MenuModule); require('util').inherits(MessageListModule, MenuModule);
MessageListModule.prototype.enter = function(client) { MessageListModule.prototype.enter = function() {
MessageListModule.super_.prototype.enter.call(this, client); 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 // the user's current area
// //
if(!this.messageAreaName) { if(!this.messageAreaTag) {
this.messageAreaName = client.user.properties.message_area_name; this.messageAreaTag = this.client.user.properties.message_area_tag;
} }
}; };
@ -110,6 +110,8 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
var self = this; var self = this;
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
var firstNewEntryIndex;
async.series( async.series(
[ [
function callParentMciReady(callback) { function callParentMciReady(callback) {
@ -130,7 +132,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
if(_.isArray(self.messageList)) { if(_.isArray(self.messageList)) {
callback(0 === self.messageList.length ? new Error('No messages in area') : null); callback(0 === self.messageList.length ? new Error('No messages in area') : null);
} else { } 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) { if(msgList && 0 === msgList.length) {
callback(new Error('No messages in area')); callback(new Error('No messages in area'));
} else { } else {
@ -141,7 +143,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
} }
}, },
function getLastReadMesageId(callback) { 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; self.lastReadId = lastReadId || 0;
callback(null); // ignore any errors, e.g. missing value callback(null); // ignore any errors, e.g. missing value
}); });
@ -158,6 +160,13 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
var msgNum = 1; var msgNum = 1;
function getMsgFmtObj(mle) { function getMsgFmtObj(mle) {
if(_.isUndefined(firstNewEntryIndex) &&
mle.messageId > self.lastReadId)
{
firstNewEntryIndex = msgNum - 1;
}
return { return {
msgNum : msgNum++, msgNum : msgNum++,
subj : mle.subject, subj : mle.subject,
@ -180,14 +189,18 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
msgListView.on('index update', function indexUpdated(idx) { msgListView.on('index update', function indexUpdated(idx) {
self.setViewText(MciCodesIds.MsgSelNum, (idx + 1).toString()); self.setViewText(MciCodesIds.MsgSelNum, (idx + 1).toString());
}); });
msgListView.redraw(); msgListView.redraw();
if(firstNewEntryIndex > 0) {
msgListView.setFocusItemIndex(firstNewEntryIndex);
}
callback(null); callback(null);
}, },
function populateOtherMciViews(callback) { 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.MsgSelNum, (vc.getView(MciCodesIds.MsgList).getData() + 1).toString());
self.setViewText(MciCodesIds.MsgTotal, self.messageList.length.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 theme = require('../core/theme.js');
var login = require('../core/system_menu_method.js').login; var login = require('../core/system_menu_method.js').login;
var Config = require('../core/config.js').config; 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'); var async = require('async');
@ -65,6 +65,16 @@ function NewUserAppModule(options) {
newUser.username = formData.value.username; 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 = { newUser.properties = {
real_name : formData.value.realName, real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
@ -74,14 +84,12 @@ function NewUserAppModule(options) {
email_address : formData.value.email, email_address : formData.value.email,
web_address : formData.value.web, web_address : formData.value.web,
account_created : new Date().toISOString(), 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_height : self.client.term.termHeight,
term_width : self.client.term.termWidth, 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,
// :TODO: Other defaults // :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc. // :TODO: should probably have a place to create defaults/etc.
@ -92,8 +100,8 @@ function NewUserAppModule(options) {
} else { } else {
newUser.properties.theme_id = Config.defaults.theme; 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) { newUser.create( { password : formData.value.password }, function created(err) {
if(err) { if(err) {
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');

View File

@ -172,8 +172,8 @@
messageAreaChangeCurrentArea: { messageAreaChangeCurrentArea: {
config: { config: {
listFormat: "|00|15{index} |07- |03{desc}" listFormat: "|00|15{index} |07- |03{name}"
focusListFormat: "|00|19|15{index} - {desc}" focusListFormat: "|00|19|15{index} - {name}"
} }
mci: { mci: {
VM1: { 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'; 'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule; 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 getUserList = require('../core/user.js').getUserList;
var ViewController = require('../core/view_controller.js').ViewController; var ViewController = require('../core/view_controller.js').ViewController;

View File

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

View File

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