Major commit for new message network WIP
This commit is contained in:
parent
6750c05f07
commit
317af8419a
|
@ -65,7 +65,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
|
|||
## License
|
||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
||||
|
||||
Copyright (c) 2015, Bryan D. Ashby
|
||||
Copyright (c) 2015-2016, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -804,16 +804,13 @@ module.exports = (function() {
|
|||
return !isNaN(value) && user.getAge() >= value;
|
||||
},
|
||||
AS : function accountStatus() {
|
||||
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
return _.findIndex(value, function cmp(accStatus) {
|
||||
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
|
||||
}) > -1;
|
||||
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(userAccountStatus) > -1;
|
||||
},
|
||||
EC : function isEncoding() {
|
||||
switch(value) {
|
||||
|
@ -842,7 +839,7 @@ module.exports = (function() {
|
|||
// :TODO: implement me!!
|
||||
return false;
|
||||
},
|
||||
SC : function isSecerConnection() {
|
||||
SC : function isSecureConnection() {
|
||||
return client.session.isSecure;
|
||||
},
|
||||
ML : function minutesLeft() {
|
||||
|
@ -870,16 +867,20 @@ module.exports = (function() {
|
|||
return !isNaN(value) && client.term.termWidth >= value;
|
||||
},
|
||||
ID : function isUserId(value) {
|
||||
return user.userId === value;
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(user.userId) > -1;
|
||||
},
|
||||
WD : function isOneOfDayOfWeek() {
|
||||
// :TODO: return true if DoW
|
||||
if(_.isNumber(value)) {
|
||||
|
||||
} else if(_.isArray(value)) {
|
||||
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
return false;
|
||||
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(new Date().getDay()) > -1;
|
||||
},
|
||||
MM : function isMinutesPastMidnight() {
|
||||
// :TODO: return true if value is >= minutes past midnight sys time
|
||||
|
|
|
@ -7,8 +7,13 @@ var acsParser = require('./acs_parser.js');
|
|||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
|
||||
exports.checkAcs = checkAcs;
|
||||
exports.getConditionalValue = getConditionalValue;
|
||||
|
||||
function checkAcs(client, acsString) {
|
||||
return acsParser.parse(acsString, { client : client } );
|
||||
}
|
||||
|
||||
function getConditionalValue(client, condArray, memberName) {
|
||||
assert(_.isObject(client));
|
||||
assert(_.isArray(condArray));
|
||||
|
|
157
core/art.js
157
core/art.js
|
@ -12,6 +12,7 @@ var events = require('events');
|
|||
var util = require('util');
|
||||
var ansi = require('./ansi_term.js');
|
||||
var aep = require('./ansi_escape_parser.js');
|
||||
var sauce = require('./sauce.js');
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
|
@ -20,10 +21,6 @@ exports.getArtFromPath = getArtFromPath;
|
|||
exports.display = display;
|
||||
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
||||
|
||||
var SAUCE_SIZE = 128;
|
||||
var SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
|
||||
var COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
||||
|
||||
// :TODO: Return MCI code information
|
||||
// :TODO: process SAUCE comments
|
||||
// :TODO: return font + font mapped information from SAUCE
|
||||
|
@ -43,156 +40,6 @@ var SUPPORTED_ART_TYPES = {
|
|||
// :TODO: extension for topaz ansi/ascii.
|
||||
};
|
||||
|
||||
//
|
||||
// See
|
||||
// http://www.acid.org/info/sauce/sauce.htm
|
||||
//
|
||||
// :TODO: Move all SAUCE stuff to sauce.js
|
||||
function readSAUCE(data, cb) {
|
||||
if(data.length < SAUCE_SIZE) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = data.length - SAUCE_SIZE;
|
||||
var sauceRec = data.slice(offset);
|
||||
|
||||
binary.parse(sauceRec)
|
||||
.buffer('id', 5)
|
||||
.buffer('version', 2)
|
||||
.buffer('title', 35)
|
||||
.buffer('author', 20)
|
||||
.buffer('group', 20)
|
||||
.buffer('date', 8)
|
||||
.word32lu('fileSize')
|
||||
.word8('dataType')
|
||||
.word8('fileType')
|
||||
.word16lu('tinfo1')
|
||||
.word16lu('tinfo2')
|
||||
.word16lu('tinfo3')
|
||||
.word16lu('tinfo4')
|
||||
.word8('numComments')
|
||||
.word8('flags')
|
||||
.buffer('tinfos', 22) // SAUCE 00.5
|
||||
.tap(function onVars(vars) {
|
||||
|
||||
if(!SAUCE_ID.equals(vars.id)) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
}
|
||||
|
||||
var ver = iconv.decode(vars.version, 'cp437');
|
||||
|
||||
if('00' !== ver) {
|
||||
cb(new Error('Unsupported SAUCE version: ' + ver));
|
||||
return;
|
||||
}
|
||||
|
||||
var sauce = {
|
||||
id : iconv.decode(vars.id, 'cp437'),
|
||||
version : iconv.decode(vars.version, 'cp437').trim(),
|
||||
title : iconv.decode(vars.title, 'cp437').trim(),
|
||||
author : iconv.decode(vars.author, 'cp437').trim(),
|
||||
group : iconv.decode(vars.group, 'cp437').trim(),
|
||||
date : iconv.decode(vars.date, 'cp437').trim(),
|
||||
fileSize : vars.fileSize,
|
||||
dataType : vars.dataType,
|
||||
fileType : vars.fileType,
|
||||
tinfo1 : vars.tinfo1,
|
||||
tinfo2 : vars.tinfo2,
|
||||
tinfo3 : vars.tinfo3,
|
||||
tinfo4 : vars.tinfo4,
|
||||
numComments : vars.numComments,
|
||||
flags : vars.flags,
|
||||
tinfos : vars.tinfos,
|
||||
};
|
||||
|
||||
var dt = SAUCE_DATA_TYPES[sauce.dataType];
|
||||
if(dt && dt.parser) {
|
||||
sauce[dt.name] = dt.parser(sauce);
|
||||
}
|
||||
|
||||
cb(null, sauce);
|
||||
});
|
||||
}
|
||||
|
||||
// :TODO: These need completed:
|
||||
var SAUCE_DATA_TYPES = {};
|
||||
SAUCE_DATA_TYPES[0] = { name : 'None' };
|
||||
SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE };
|
||||
SAUCE_DATA_TYPES[2] = 'Bitmap';
|
||||
SAUCE_DATA_TYPES[3] = 'Vector';
|
||||
SAUCE_DATA_TYPES[4] = 'Audio';
|
||||
SAUCE_DATA_TYPES[5] = 'BinaryText';
|
||||
SAUCE_DATA_TYPES[6] = 'XBin';
|
||||
SAUCE_DATA_TYPES[7] = 'Archive';
|
||||
SAUCE_DATA_TYPES[8] = 'Executable';
|
||||
|
||||
var SAUCE_CHARACTER_FILE_TYPES = {};
|
||||
SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII';
|
||||
SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi';
|
||||
SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation';
|
||||
SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script';
|
||||
SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard';
|
||||
SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar';
|
||||
SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML';
|
||||
SAUCE_CHARACTER_FILE_TYPES[7] = 'Source';
|
||||
SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw';
|
||||
|
||||
//
|
||||
// Map of SAUCE font -> encoding hint
|
||||
//
|
||||
// Note that this is the same mapping that x84 uses. Be compatible!
|
||||
//
|
||||
var SAUCE_FONT_TO_ENCODING_HINT = {
|
||||
'Amiga MicroKnight' : 'amiga',
|
||||
'Amiga MicroKnight+' : 'amiga',
|
||||
'Amiga mOsOul' : 'amiga',
|
||||
'Amiga P0T-NOoDLE' : 'amiga',
|
||||
'Amiga Topaz 1' : 'amiga',
|
||||
'Amiga Topaz 1+' : 'amiga',
|
||||
'Amiga Topaz 2' : 'amiga',
|
||||
'Amiga Topaz 2+' : 'amiga',
|
||||
'Atari ATASCII' : 'atari',
|
||||
'IBM EGA43' : 'cp437',
|
||||
'IBM EGA' : 'cp437',
|
||||
'IBM VGA25G' : 'cp437',
|
||||
'IBM VGA50' : 'cp437',
|
||||
'IBM VGA' : 'cp437',
|
||||
};
|
||||
|
||||
['437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
|
||||
'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) {
|
||||
var codec = 'cp' + page;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
|
||||
});
|
||||
|
||||
function parseCharacterSAUCE(sauce) {
|
||||
var result = {};
|
||||
|
||||
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
|
||||
|
||||
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
|
||||
// convience: create ansiFlags
|
||||
sauce.ansiFlags = sauce.flags;
|
||||
|
||||
var i = 0;
|
||||
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
|
||||
++i;
|
||||
}
|
||||
var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
|
||||
if(fontName.length > 0) {
|
||||
result.fontName = fontName;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getFontNameFromSAUCE(sauce) {
|
||||
if(sauce.Character) {
|
||||
return sauce.Character.fontName;
|
||||
|
@ -249,7 +96,7 @@ function getArtFromPath(path, options, cb) {
|
|||
}
|
||||
|
||||
if(options.readSauce === true) {
|
||||
readSAUCE(data, function onSauce(err, sauce) {
|
||||
sauce.readSAUCE(data, function onSauce(err, sauce) {
|
||||
if(err) {
|
||||
cb(null, getResult());
|
||||
} else {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
'use strict';
|
||||
|
||||
var Config = require('./config.js').config;
|
||||
var theme = require('./theme.js');
|
||||
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
//var SegfaultHandler = require('segfault-handler');
|
||||
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
|
||||
|
||||
// ENiGMA½
|
||||
var conf = require('./config.js');
|
||||
var logger = require('./logger.js');
|
||||
|
@ -21,7 +24,7 @@ function bbsMain() {
|
|||
async.waterfall(
|
||||
[
|
||||
function processArgs(callback) {
|
||||
var args = parseArgs();
|
||||
const args = parseArgs();
|
||||
|
||||
var configPath;
|
||||
|
||||
|
@ -37,8 +40,7 @@ function bbsMain() {
|
|||
}
|
||||
}
|
||||
|
||||
var configPathSupplied = _.isString(configPath);
|
||||
callback(null, configPath || conf.getDefaultPath(), configPathSupplied);
|
||||
callback(null, configPath || conf.getDefaultPath(), _.isString(configPath));
|
||||
},
|
||||
function initConfig(configPath, configPathSupplied, callback) {
|
||||
conf.init(configPath, function configInit(err) {
|
||||
|
|
|
@ -8,10 +8,37 @@ var paths = require('path');
|
|||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var hjson = require('hjson');
|
||||
var assert = require('assert');
|
||||
|
||||
exports.init = init;
|
||||
exports.getDefaultPath = getDefaultPath;
|
||||
|
||||
function hasMessageConferenceAndArea(config) {
|
||||
assert(_.isObject(config.messageConferences)); // we create one ourself!
|
||||
|
||||
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
|
||||
return 'system_internal' !== confTag;
|
||||
});
|
||||
|
||||
if(0 === nonInternalConfs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// :TODO: there is likely a better/cleaner way of doing this
|
||||
|
||||
var result = false;
|
||||
_.forEach(nonInternalConfs, confTag => {
|
||||
if(_.has(config.messageConferences[confTag], 'areas') &&
|
||||
Object.keys(config.messageConferences[confTag].areas) > 0)
|
||||
{
|
||||
result = true;
|
||||
return false; // stop iteration
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function init(configPath, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
|
@ -48,19 +75,14 @@ function init(configPath, cb) {
|
|||
//
|
||||
// Various sections must now exist in config
|
||||
//
|
||||
if(!_.has(mergedConfig, 'messages.areas.') ||
|
||||
!_.isArray(mergedConfig.messages.areas) ||
|
||||
0 === mergedConfig.messages.areas.length ||
|
||||
!_.isString(mergedConfig.messages.areas[0].name))
|
||||
{
|
||||
var msgAreasErr = new Error('Please create at least one message area');
|
||||
if(hasMessageConferenceAndArea(mergedConfig)) {
|
||||
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
||||
msgAreasErr.code = 'EBADCONFIG';
|
||||
callback(msgAreasErr);
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
callback(null, mergedConfig);
|
||||
}
|
||||
}
|
||||
],
|
||||
function complete(err, mergedConfig) {
|
||||
exports.config = mergedConfig;
|
||||
|
@ -150,6 +172,7 @@ function getDefaultConfig() {
|
|||
paths : {
|
||||
mods : paths.join(__dirname, './../mods/'),
|
||||
servers : paths.join(__dirname, './servers/'),
|
||||
msgNetworks : paths.join(__dirname, './msg_networks/'),
|
||||
art : paths.join(__dirname, './../mods/art/'),
|
||||
themes : paths.join(__dirname, './../mods/themes/'),
|
||||
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
||||
|
@ -183,6 +206,25 @@ function getDefaultConfig() {
|
|||
}
|
||||
},
|
||||
|
||||
messageConferences : {
|
||||
system_internal : {
|
||||
name : 'System Internal',
|
||||
desc : 'Built in conference for private messages, bulletins, etc.',
|
||||
|
||||
areas : {
|
||||
private_mail : {
|
||||
name : 'Private Mail',
|
||||
desc : 'Private user to user mail/email',
|
||||
},
|
||||
|
||||
local_bulletin : {
|
||||
name : 'System Bulletins',
|
||||
desc : 'Bulletin messages for all users',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
messages : {
|
||||
areas : [
|
||||
{ name : 'private_mail', desc : 'Private Email', groups : [ 'users' ] }
|
||||
|
|
|
@ -132,7 +132,7 @@ function createMessageBaseTables() {
|
|||
dbs.message.run(
|
||||
'CREATE TABLE IF NOT EXISTS message (' +
|
||||
' message_id INTEGER PRIMARY KEY,' +
|
||||
' area_name VARCHAR NOT NULL,' +
|
||||
' area_tag VARCHAR NOT NULL,' +
|
||||
' message_uuid VARCHAR(36) NOT NULL,' +
|
||||
' reply_to_message_id INTEGER,' +
|
||||
' to_user_name VARCHAR NOT NULL,' +
|
||||
|
@ -198,9 +198,9 @@ function createMessageBaseTables() {
|
|||
dbs.message.run(
|
||||
'CREATE TABLE IF NOT EXISTS user_message_area_last_read (' +
|
||||
' user_id INTEGER NOT NULL,' +
|
||||
' area_name VARCHAR NOT NULL,' +
|
||||
' area_tag VARCHAR NOT NULL,' +
|
||||
' message_id INTEGER NOT NULL,' +
|
||||
' UNIQUE(user_id, area_name)' +
|
||||
' UNIQUE(user_id, area_tag)' +
|
||||
');'
|
||||
);
|
||||
|
||||
|
|
32
core/fse.js
32
core/fse.js
|
@ -7,7 +7,7 @@ var ansi = require('../core/ansi_term.js');
|
|||
var theme = require('../core/theme.js');
|
||||
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||
var Message = require('../core/message.js');
|
||||
var getMessageAreaByName = require('../core/message_area.js').getMessageAreaByName;
|
||||
var getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
|
||||
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
|
||||
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
|
||||
|
||||
|
@ -76,6 +76,8 @@ var MCICodeIds = {
|
|||
MessageID : 10,
|
||||
ReplyToMsgID : 11,
|
||||
|
||||
// :TODO: ConfName
|
||||
|
||||
},
|
||||
|
||||
ViewModeFooter : {
|
||||
|
@ -104,15 +106,15 @@ function FullScreenEditorModule(options) {
|
|||
// editorMode : view | edit | quote
|
||||
//
|
||||
// menuConfig.config or extraArgs
|
||||
// messageAreaName
|
||||
// messageAreaTag
|
||||
// messageIndex / messageTotal
|
||||
// toUserId
|
||||
//
|
||||
this.editorType = config.editorType;
|
||||
this.editorMode = config.editorMode;
|
||||
|
||||
if(config.messageAreaName) {
|
||||
this.messageAreaName = config.messageAreaName;
|
||||
if(config.messageAreaTag) {
|
||||
this.messageAreaTag = config.messageAreaTag;
|
||||
}
|
||||
|
||||
this.messageIndex = config.messageIndex || 0;
|
||||
|
@ -121,8 +123,8 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
// extraArgs can override some config
|
||||
if(_.isObject(options.extraArgs)) {
|
||||
if(options.extraArgs.messageAreaName) {
|
||||
this.messageAreaName = options.extraArgs.messageAreaName;
|
||||
if(options.extraArgs.messageAreaTag) {
|
||||
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||
}
|
||||
if(options.extraArgs.messageIndex) {
|
||||
this.messageIndex = options.extraArgs.messageIndex;
|
||||
|
@ -135,9 +137,6 @@ function FullScreenEditorModule(options) {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(this.toUserId)
|
||||
console.log(this.messageAreaName)
|
||||
|
||||
this.isReady = false;
|
||||
|
||||
this.isEditMode = function() {
|
||||
|
@ -149,7 +148,7 @@ function FullScreenEditorModule(options) {
|
|||
};
|
||||
|
||||
this.isLocalEmail = function() {
|
||||
return Message.WellKnownAreaNames.Private === self.messageAreaName;
|
||||
return Message.WellKnownAreaTags.Private === self.messageAreaTag;
|
||||
};
|
||||
|
||||
this.isReply = function() {
|
||||
|
@ -217,7 +216,7 @@ function FullScreenEditorModule(options) {
|
|||
var headerValues = self.viewControllers.header.getFormData().value;
|
||||
|
||||
var msgOpts = {
|
||||
areaName : self.messageAreaName,
|
||||
areaTag : self.messageAreaTag,
|
||||
toUserName : headerValues.to,
|
||||
fromUserName : headerValues.from,
|
||||
subject : headerValues.subject,
|
||||
|
@ -235,7 +234,7 @@ function FullScreenEditorModule(options) {
|
|||
self.message = message;
|
||||
|
||||
updateMessageAreaLastReadId(
|
||||
self.client.user.userId, self.messageAreaName, self.message.messageId,
|
||||
self.client.user.userId, self.messageAreaTag, self.message.messageId,
|
||||
function lastReadUpdated() {
|
||||
|
||||
if(self.isReady) {
|
||||
|
@ -631,7 +630,7 @@ function FullScreenEditorModule(options) {
|
|||
};
|
||||
|
||||
this.initHeaderGeneric = function() {
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.AreaName, getMessageAreaByName(self.messageAreaName).desc);
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.AreaName, getMessageAreaByTag(self.messageAreaTag).name);
|
||||
};
|
||||
|
||||
this.initHeaderViewMode = function() {
|
||||
|
@ -965,13 +964,10 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
require('util').inherits(FullScreenEditorModule, MenuModule);
|
||||
|
||||
FullScreenEditorModule.prototype.enter = function(client) {
|
||||
FullScreenEditorModule.super_.prototype.enter.call(this, client);
|
||||
|
||||
|
||||
FullScreenEditorModule.prototype.enter = function() {
|
||||
FullScreenEditorModule.super_.prototype.enter.call(this);
|
||||
};
|
||||
|
||||
FullScreenEditorModule.prototype.mciReady = function(mciData, cb) {
|
||||
this.mciReadyHandler(mciData, cb);
|
||||
//this['mciReadyHandler' + _.capitalize(this.editorType)](mciData);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var MailPacket = require('./mail_packet.js');
|
||||
//var MailPacket = require('./mail_packet.js');
|
||||
var ftn = require('./ftn_util.js');
|
||||
var Message = require('./message.js');
|
||||
var sauce = require('./sauce.js');
|
||||
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
|
@ -12,6 +13,8 @@ var fs = require('fs');
|
|||
var util = require('util');
|
||||
var async = require('async');
|
||||
var iconv = require('iconv-lite');
|
||||
var buffers = require('buffers');
|
||||
var moment = require('moment');
|
||||
|
||||
/*
|
||||
:TODO: should probably be broken up
|
||||
|
@ -20,6 +23,493 @@ var iconv = require('iconv-lite');
|
|||
FTNPacketExport: message(s) -> packet
|
||||
*/
|
||||
|
||||
/*
|
||||
Reader: file to ftn data
|
||||
Writer: ftn data to packet
|
||||
|
||||
Data to toMessage
|
||||
Data.fromMessage
|
||||
|
||||
FTNMessage.toMessage() => Message
|
||||
FTNMessage.fromMessage() => Create from Message
|
||||
|
||||
* read: header -> simple {} obj, msg -> Message object
|
||||
* read: read(..., iterator): iterator('header', ...), iterator('message', msg)
|
||||
* write: provide information to go into header
|
||||
|
||||
* Logic of "Is this for us"/etc. elsewhere
|
||||
*/
|
||||
|
||||
const FTN_PACKET_HEADER_SIZE = 58; // fixed header size
|
||||
const FTN_PACKET_HEADER_TYPE = 2;
|
||||
const FTN_PACKET_MESSAGE_TYPE = 2;
|
||||
|
||||
// EOF + SAUCE.id + SAUCE.version ('00')
|
||||
const FTN_MESSAGE_SAUCE_HEADER =
|
||||
new Buffer( [ 0x1a, 'S', 'A', 'U', 'C', 'E', '0', '0' ] );
|
||||
|
||||
const FTN_MESSAGE_KLUDGE_PREFIX = '\x01';
|
||||
|
||||
function FTNPacket() {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.parsePacketHeader = function(packetBuffer, cb) {
|
||||
assert(Buffer.isBuffer(packetBuffer));
|
||||
|
||||
//
|
||||
// See the following specs:
|
||||
// http://ftsc.org/docs/fts-0001.016
|
||||
// http://ftsc.org/docs/fsc-0048.002
|
||||
//
|
||||
if(packetBuffer.length < FTN_PACKET_HEADER_SIZE) {
|
||||
cb(new Error('Buffer too small'));
|
||||
return;
|
||||
}
|
||||
|
||||
binary.parse(packetBuffer)
|
||||
.word16lu('origNode')
|
||||
.word16lu('destNode')
|
||||
.word16lu('year')
|
||||
.word16lu('month')
|
||||
.word16lu('day')
|
||||
.word16lu('hour')
|
||||
.word16lu('minute')
|
||||
.word16lu('second')
|
||||
.word16lu('baud')
|
||||
.word16lu('packetType')
|
||||
.word16lu('origNet')
|
||||
.word16lu('destNet')
|
||||
.word8('prodCodeLo')
|
||||
.word8('revisionMajor') // aka serialNo
|
||||
.buffer('password', 8) // null padded C style string
|
||||
.word16lu('origZone')
|
||||
.word16lu('destZone')
|
||||
// Additions in FSC-0048.002 follow...
|
||||
.word16lu('auxNet')
|
||||
.word16lu('capWordA')
|
||||
.word8('prodCodeHi')
|
||||
.word8('revisionMinor')
|
||||
.word16lu('capWordB')
|
||||
.word16lu('originZone2')
|
||||
.word16lu('destZone2')
|
||||
.word16lu('originPoint')
|
||||
.word16lu('destPoint')
|
||||
.word32lu('prodData')
|
||||
.tap(packetHeader => {
|
||||
// Convert password from NULL padded array to string
|
||||
packetHeader.password = ftn.stringFromFTN(packetHeader.password);
|
||||
|
||||
if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) {
|
||||
cb(new Error('Unsupported header type: ' + packetHeader.packetType));
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Date/time components into something more reasonable
|
||||
// Note: The names above match up with object members moment() allows
|
||||
//
|
||||
packetHeader.created = moment(packetHeader);
|
||||
|
||||
cb(null, packetHeader);
|
||||
});
|
||||
};
|
||||
|
||||
this.writePacketHeader = function(headerInfo, ws) {
|
||||
let buffer = new Buffer(FTN_PACKET_HEADER_SIZE);
|
||||
|
||||
buffer.writeUInt16LE(headerInfo.origNode, 0);
|
||||
buffer.writeUInt16LE(headerInfo.destNode, 2);
|
||||
buffer.writeUInt16LE(headerInfo.created.year(), 4);
|
||||
buffer.writeUInt16LE(headerInfo.created.month(), 6);
|
||||
buffer.writeUInt16LE(headerInfo.created.date(), 8);
|
||||
buffer.writeUInt16LE(headerInfo.created.hour(), 10);
|
||||
buffer.writeUInt16LE(headerInfo.created.minute(), 12);
|
||||
buffer.writeUInt16LE(headerInfo.created.second(), 14);
|
||||
buffer.writeUInt16LE(headerInfo.baud, 16);
|
||||
buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18);
|
||||
buffer.writeUInt16LE(headerInfo.origNet, 20);
|
||||
buffer.writeUInt16LE(headerInfo.destNet, 22);
|
||||
buffer.writeUInt8(headerInfo.prodCodeLo, 24);
|
||||
buffer.writeUInt8(headerInfo.revisionMajor, 25);
|
||||
|
||||
const pass = ftn.stringToNullPaddedBuffer(headerInfo.password, 8);
|
||||
pass.copy(buffer, 26);
|
||||
|
||||
buffer.writeUInt16LE(headerInfo.origZone, 34);
|
||||
buffer.writeUInt16LE(headerInfo.destZone, 36);
|
||||
|
||||
// FSC-0048.002 additions...
|
||||
buffer.writeUInt16LE(headerInfo.auxNet, 38);
|
||||
buffer.writeUInt16LE(headerInfo.capWordA, 40);
|
||||
buffer.writeUInt8(headerInfo.prodCodeHi, 42);
|
||||
buffer.writeUInt8(headerInfo.revisionMinor, 43);
|
||||
buffer.writeUInt16LE(headerInfo.capWordB, 44);
|
||||
buffer.writeUInt16LE(headerInfo.origZone2, 46);
|
||||
buffer.writeUInt16LE(headerInfo.destZone2, 48);
|
||||
buffer.writeUInt16LE(headerInfo.origPoint, 50);
|
||||
buffer.writeUInt16LE(headerInfo.destPoint, 52);
|
||||
buffer.writeUInt32LE(headerInfo.prodData, 54);
|
||||
|
||||
ws.write(buffer);
|
||||
};
|
||||
|
||||
this.processMessageBody = function(messageBodyBuffer, cb) {
|
||||
//
|
||||
// From FTS-0001.16:
|
||||
// "Message text is unbounded and null terminated (note exception below).
|
||||
//
|
||||
// A 'hard' carriage return, 0DH, marks the end of a paragraph, and must
|
||||
// be preserved.
|
||||
//
|
||||
// So called 'soft' carriage returns, 8DH, may mark a previous
|
||||
// processor's automatic line wrap, and should be ignored. Beware that
|
||||
// they may be followed by linefeeds, or may not.
|
||||
//
|
||||
// All linefeeds, 0AH, should be ignored. Systems which display message
|
||||
// text should wrap long lines to suit their application."
|
||||
//
|
||||
// This can be a bit tricky:
|
||||
// * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that
|
||||
// * Many kludge lines specify an encoding. If we find one of such lines, we'll
|
||||
// likely need to re-decode as the specified encoding
|
||||
// * SAUCE is binary-ish data, so we need to inspect for it before any
|
||||
// decoding occurs
|
||||
//
|
||||
let messageBodyData = {
|
||||
message : [],
|
||||
kludgeLines : {}, // KLUDGE:[value1, value2, ...] map
|
||||
seenBy : [],
|
||||
};
|
||||
|
||||
function addKludgeLine(line) {
|
||||
const sepIndex = line.indexOf(':');
|
||||
const key = line.substr(0, sepIndex).toUpperCase();
|
||||
const value = line.substr(sepIndex + 1).trim();
|
||||
|
||||
//
|
||||
// Allow mapped value to be either a key:value if there is only
|
||||
// one entry, or key:[value1, value2,...] if there are more
|
||||
//
|
||||
if(messageBodyData.kludgeLines[key]) {
|
||||
if(!_.isArray(messageBodyData.kludgeLines[key])) {
|
||||
messageBodyData.kludgeLines[key] = [ messageBodyData.kludgeLines[key] ];
|
||||
}
|
||||
messageBodyData.kludgeLines[key].push(value);
|
||||
} else {
|
||||
messageBodyData.kludgeLines[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
async.series(
|
||||
[
|
||||
function extractSauce(callback) {
|
||||
// :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's
|
||||
// present, we need to extract it but keep the rest of hte message intact as it likely
|
||||
// has SEEN-BY, PATH, and other kludge information *appended*
|
||||
const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER);
|
||||
if(sauceHeaderPosition > -1) {
|
||||
sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition), (err, theSauce) => {
|
||||
if(!err) {
|
||||
// we read some SAUCE - don't re-process that portion into the body
|
||||
messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition);
|
||||
messageBodyData.sauce = theSauce;
|
||||
}
|
||||
callback(null); // failure to read SAUCE is OK
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
function extractMessageData(callback) {
|
||||
const messageLines =
|
||||
iconv.decode(messageBodyBuffer, 'CP437').replace(/[\xec\n]/g, '').split(/\r/g);
|
||||
|
||||
let preOrigin = true;
|
||||
|
||||
messageLines.forEach(line => {
|
||||
if(0 === line.length) {
|
||||
messageBodyData.message.push('');
|
||||
return;
|
||||
}
|
||||
|
||||
if(preOrigin) {
|
||||
if(line.startsWith('AREA:')) {
|
||||
messageBodyData.area = line.substring(line.indexOf(':') + 1).trim();
|
||||
} else if(line.startsWith('--- ')) {
|
||||
// Tear Lines are tracked allowing for specialized display/etc.
|
||||
messageBodyData.tearLine = line;
|
||||
} else if(/[ ]{1,2}(\* )?Origin\: /.test(line)) { // To spec is " * Origin: ..."
|
||||
messageBodyData.originLine = line;
|
||||
preOrigin = false;
|
||||
} else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) {
|
||||
addKludgeLine(line.slice(1));
|
||||
} else {
|
||||
// regular ol' message line
|
||||
messageBodyData.message.push(line);
|
||||
}
|
||||
} else {
|
||||
if(line.startsWith('SEEN-BY:')) {
|
||||
messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim());
|
||||
} else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) {
|
||||
addKludgeLine(line.slice(1));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
messageBodyData.message = messageBodyData.message.join('\n');
|
||||
cb(messageBodyData);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.parsePacketMessages = function(messagesBuffer, iterator, cb) {
|
||||
const NULL_TERM_BUFFER = new Buffer( [ 0 ] );
|
||||
|
||||
binary.stream(messagesBuffer).loop(function looper(end, vars) {
|
||||
//
|
||||
// Some variable names used here match up directly with well known
|
||||
// meta data names used with FTN messages.
|
||||
//
|
||||
this
|
||||
.word16lu('messageType')
|
||||
.word16lu('ftn_orig_node')
|
||||
.word16lu('ftn_dest_node')
|
||||
.word16lu('ftn_orig_network')
|
||||
.word16lu('ftn_dest_network')
|
||||
.word8('ftn_attr_flags1')
|
||||
.word8('ftn_attr_flags2')
|
||||
.word16lu('ftn_cost')
|
||||
.scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max
|
||||
.scan('toUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max
|
||||
.scan('fromUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max
|
||||
.scan('subject', NULL_TERM_BUFFER) // :TODO: 72 bytes max
|
||||
.scan('message', NULL_TERM_BUFFER)
|
||||
.tap(function tapped(msgData) {
|
||||
if(!msgData.ftn_orig_node) {
|
||||
// end marker -- no more messages
|
||||
end();
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) {
|
||||
end();
|
||||
cb(new Error('Unsupported message type: ' + msgData.messageType));
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Convert null terminated arrays to strings
|
||||
//
|
||||
let convMsgData = {};
|
||||
[ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => {
|
||||
convMsgData[k] = iconv.decode(msgData[k], 'CP437');
|
||||
});
|
||||
|
||||
//
|
||||
// The message body itself is a special beast as it may
|
||||
// contain special origin lines, kludges, SAUCE in the case
|
||||
// of ANSI files, etc.
|
||||
//
|
||||
let msg = new Message( {
|
||||
toUserName : convMsgData.toUserName,
|
||||
fromUserName : convMsgData.fromUserName,
|
||||
subject : convMsgData.subject,
|
||||
modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime),
|
||||
});
|
||||
|
||||
msg.meta.FtnProperty = {};
|
||||
msg.meta.FtnProperty.ftn_orig_node = msgData.ftn_orig_node;
|
||||
msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node;
|
||||
msg.meta.FtnProperty.ftn_orig_network = msgData.ftn_orig_network;
|
||||
msg.meta.FtnProperty.ftn_dest_network = msgData.ftn_dest_network;
|
||||
msg.meta.FtnProperty.ftn_attr_flags1 = msgData.ftn_attr_flags1;
|
||||
msg.meta.FtnProperty.ftn_attr_flags2 = msgData.ftn_attr_flags2;
|
||||
msg.meta.FtnProperty.ftn_cost = msgData.ftn_cost;
|
||||
|
||||
self.processMessageBody(msgData.message, function processed(messageBodyData) {
|
||||
msg.message = messageBodyData.message;
|
||||
msg.meta.FtnKludge = messageBodyData.kludgeLines;
|
||||
|
||||
if(messageBodyData.tearLine) {
|
||||
msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine;
|
||||
}
|
||||
if(messageBodyData.seenBy.length > 0) {
|
||||
msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy;
|
||||
}
|
||||
if(messageBodyData.area) {
|
||||
msg.meta.FtnProperty.ftn_area = messageBodyData.area;
|
||||
}
|
||||
if(messageBodyData.originLine) {
|
||||
msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine;
|
||||
}
|
||||
|
||||
iterator('message', msg);
|
||||
})
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.writeMessage = function(message, ws) {
|
||||
let basicHeader = new Buffer(34);
|
||||
|
||||
basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0);
|
||||
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2);
|
||||
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4);
|
||||
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6);
|
||||
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8);
|
||||
basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags1, 10);
|
||||
basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11);
|
||||
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12);
|
||||
|
||||
//
|
||||
// From http://ftsc.org/docs/fts-0001.016:
|
||||
// DateTime = (* a character string 20 characters long *)
|
||||
// (* 01 Jan 86 02:34:56 *)
|
||||
// DayOfMonth " " Month " " Year " "
|
||||
// " " HH ":" MM ":" SS
|
||||
// Null
|
||||
//
|
||||
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
||||
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
||||
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
|
||||
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
|
||||
// HH = "00" | .. | "23"
|
||||
// MM = "00" | .. | "59"
|
||||
// SS = "00" | .. | "59"
|
||||
const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0');
|
||||
dateTimeBuffer.copy(basicHeader, 14);
|
||||
|
||||
ws.write(basicHeader);
|
||||
|
||||
// toUserName & fromUserName: up to 36 bytes in length, NULL term'd
|
||||
// :TODO: DRY...
|
||||
let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36);
|
||||
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
|
||||
ws.write(encBuf);
|
||||
|
||||
encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36);
|
||||
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
|
||||
ws.write(encBuf);
|
||||
|
||||
// subject: up to 72 bytes in length, NULL term'd
|
||||
encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72);
|
||||
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
|
||||
ws.write(encBuf);
|
||||
|
||||
//
|
||||
// message: unbound length, NULL term'd
|
||||
//
|
||||
// We need to build in various special lines - kludges, area,
|
||||
// seen-by, etc.
|
||||
//
|
||||
// :TODO: Put this in it's own method
|
||||
let msgBody = '';
|
||||
|
||||
function appendMeta(k, m) {
|
||||
if(m) {
|
||||
let a = m;
|
||||
if(!_.isArray(a)) {
|
||||
a = [ a ];
|
||||
}
|
||||
a.forEach(v => {
|
||||
msgBody += `${k}: ${v}\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO: is Area really any differnt (e.g. no space between AREA:the_area)
|
||||
if(message.meta.FtnProperty.ftn_area) {
|
||||
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\n`;
|
||||
}
|
||||
|
||||
Object.keys(message.meta.FtnKludge).forEach(k => {
|
||||
if('PATH' !== k) {
|
||||
appendMeta(k, message.meta.FtnKludge[k]);
|
||||
}
|
||||
});
|
||||
|
||||
msgBody += message.message;
|
||||
|
||||
appendMeta('', message.meta.FtnProperty.ftn_tear_line);
|
||||
appendMeta('', message.meta.FtnProperty.ftn_origin);
|
||||
|
||||
appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by);
|
||||
appendMeta('PATH', message.meta.FtnKludge['PATH']);
|
||||
|
||||
ws.write(iconv.encode(msgBody + '\0', 'CP437'));
|
||||
};
|
||||
|
||||
this.parsePacketBuffer = function(packetBuffer, iterator, cb) {
|
||||
async.series(
|
||||
[
|
||||
function processHeader(callback) {
|
||||
self.parsePacketHeader(packetBuffer, (err, header) => {
|
||||
if(!err) {
|
||||
iterator('header', header);
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
function processMessages(callback) {
|
||||
self.parsePacketMessages(
|
||||
packetBuffer.slice(FTN_PACKET_HEADER_SIZE),
|
||||
iterator,
|
||||
callback);
|
||||
}
|
||||
],
|
||||
cb
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
FTNPacket.prototype.read = function(pathOrBuffer, iterator, cb) {
|
||||
var self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function getBufferIfPath(callback) {
|
||||
if(_.isString(pathOrBuffer)) {
|
||||
fs.readFile(pathOrBuffer, (err, data) => {
|
||||
pathOrBuffer = data;
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
function parseBuffer(callback) {
|
||||
self.parsePacketBuffer(pathOrBuffer, iterator, callback);
|
||||
}
|
||||
],
|
||||
cb // completion callback
|
||||
);
|
||||
};
|
||||
|
||||
FTNPacket.prototype.write = function(path, headerInfo, messages, cb) {
|
||||
headerInfo.created = headerInfo.created || moment();
|
||||
headerInfo.baud = headerInfo.baud || 0;
|
||||
// :TODO: Other defaults?
|
||||
|
||||
if(!_.isArray(messages)) {
|
||||
messages = [ messages ] ;
|
||||
}
|
||||
|
||||
let ws = fs.createWriteStream(path);
|
||||
this.writePacketHeader(headerInfo, ws);
|
||||
|
||||
messages.forEach(msg => {
|
||||
this.writeMessage(msg, ws);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
//
|
||||
// References
|
||||
// * http://ftsc.org/docs/fts-0001.016
|
||||
|
@ -30,7 +520,7 @@ var iconv = require('iconv-lite');
|
|||
//
|
||||
function FTNMailPacket(options) {
|
||||
|
||||
MailPacket.call(this, options);
|
||||
//MailPacket.call(this, options);
|
||||
|
||||
var self = this;
|
||||
self.KLUDGE_PREFIX = '\x01';
|
||||
|
@ -77,7 +567,7 @@ function FTNMailPacket(options) {
|
|||
.word16lu('second')
|
||||
.word16lu('baud')
|
||||
.word16lu('packetType')
|
||||
.word16lu('originNet')
|
||||
.word16lu('origNet')
|
||||
.word16lu('destNet')
|
||||
.word8('prodCodeLo')
|
||||
.word8('revisionMajor') // aka serialNo
|
||||
|
@ -100,35 +590,110 @@ function FTNMailPacket(options) {
|
|||
|
||||
// :TODO: Don't hard code magic # here
|
||||
if(2 !== packetHeader.packetType) {
|
||||
console.log(packetHeader.packetType)
|
||||
cb(new Error('Packet is not Type-2'));
|
||||
return;
|
||||
}
|
||||
|
||||
// :TODO: convert date information -> .created
|
||||
|
||||
packetHeader.created = moment(packetHeader);
|
||||
/*
|
||||
packetHeader.year, packetHeader.month, packetHeader.day, packetHeader.hour,
|
||||
packetHeader.minute, packetHeader.second);*/
|
||||
|
||||
// :TODO: validate & pass error if failure
|
||||
cb(null, packetHeader);
|
||||
});
|
||||
};
|
||||
|
||||
this.getPacketHeaderBuffer = function(packetHeader, options) {
|
||||
options = options || {};
|
||||
|
||||
self.getMessageMeta = function(msgBody) {
|
||||
if(options.created) {
|
||||
options.created = moment(options.created); // ensure we have a moment obj
|
||||
} else {
|
||||
options.created = moment();
|
||||
}
|
||||
|
||||
let buffer = new Buffer(58);
|
||||
|
||||
buffer.writeUInt16LE(packetHeader.origNode, 0);
|
||||
buffer.writeUInt16LE(packetHeader.destNode, 2);
|
||||
buffer.writeUInt16LE(options.created.year(), 4);
|
||||
buffer.writeUInt16LE(options.created.month(), 6);
|
||||
buffer.writeUInt16LE(options.created.date(), 8);
|
||||
buffer.writeUInt16LE(options.created.hour(), 10);
|
||||
buffer.writeUInt16LE(options.created.minute(), 12);
|
||||
buffer.writeUInt16LE(options.created.second(), 14);
|
||||
buffer.writeUInt16LE(0x0000, 16);
|
||||
buffer.writeUInt16LE(0x0002, 18);
|
||||
buffer.writeUInt16LE(packetHeader.origNet, 20);
|
||||
buffer.writeUInt16LE(packetHeader.destNet, 22);
|
||||
buffer.writeUInt8(packetHeader.prodCodeLo, 24);
|
||||
buffer.writeUInt8(packetHeader.revisionMajor, 25);
|
||||
|
||||
const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8);
|
||||
pass.copy(buffer, 26);
|
||||
|
||||
buffer.writeUInt16LE(packetHeader.origZone, 34);
|
||||
buffer.writeUInt16LE(packetHeader.destZone, 36);
|
||||
|
||||
// FSC-0048.002 additions...
|
||||
buffer.writeUInt16LE(packetHeader.auxNet, 38);
|
||||
buffer.writeUInt16LE(packetHeader.capWordA, 40);
|
||||
buffer.writeUInt8(packetHeader.prodCodeHi, 42);
|
||||
buffer.writeUInt8(packetHeader.revisionMinor, 43);
|
||||
buffer.writeUInt16LE(packetHeader.capWordB, 44);
|
||||
buffer.writeUInt16LE(packetHeader.origZone2, 46);
|
||||
buffer.writeUInt16LE(packetHeader.destZone2, 48);
|
||||
buffer.writeUInt16LE(packetHeader.origPoint, 50);
|
||||
buffer.writeUInt16LE(packetHeader.destPoint, 52);
|
||||
buffer.writeUInt32LE(packetHeader.prodData, 54);
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
self.setOrAppend = function(value, dst) {
|
||||
if(dst) {
|
||||
if(!_.isArray(dst)) {
|
||||
dst = [ dst ];
|
||||
}
|
||||
|
||||
dst.push(value);
|
||||
} else {
|
||||
dst = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.getMessageMeta = function(msgBody, msgData) {
|
||||
var meta = {
|
||||
FtnKludge : msgBody.kludgeLines,
|
||||
FtnProperty : {},
|
||||
};
|
||||
|
||||
if(msgBody.tearLine) {
|
||||
meta.FtnProperty.ftn_tear_line = [ msgBody.tearLine ];
|
||||
meta.FtnProperty.ftn_tear_line = msgBody.tearLine;
|
||||
}
|
||||
if(msgBody.seenBy.length > 0) {
|
||||
meta.FtnProperty.ftn_seen_by = msgBody.seenBy;
|
||||
}
|
||||
if(msgBody.area) {
|
||||
meta.FtnProperty.ftn_area = [ msgBody.area ];
|
||||
meta.FtnProperty.ftn_area = msgBody.area;
|
||||
}
|
||||
if(msgBody.originLine) {
|
||||
meta.FtnProperty.ftn_origin = [ msgBody.originLine ];
|
||||
meta.FtnProperty.ftn_origin = msgBody.originLine;
|
||||
}
|
||||
|
||||
meta.FtnProperty.ftn_orig_node = msgData.origNode;
|
||||
meta.FtnProperty.ftn_dest_node = msgData.destNode;
|
||||
meta.FtnProperty.ftn_orig_network = msgData.origNet;
|
||||
meta.FtnProperty.ftn_dest_network = msgData.destNet;
|
||||
meta.FtnProperty.ftn_attr_flags1 = msgData.attrFlags1;
|
||||
meta.FtnProperty.ftn_attr_flags2 = msgData.attrFlags2;
|
||||
meta.FtnProperty.ftn_cost = msgData.cost;
|
||||
|
||||
return meta;
|
||||
};
|
||||
|
||||
|
@ -172,13 +737,15 @@ function FTNMailPacket(options) {
|
|||
var preOrigin = true;
|
||||
|
||||
function addKludgeLine(kl) {
|
||||
var kludgeParts = kl.split(':');
|
||||
const kludgeParts = kl.split(':');
|
||||
kludgeParts[0] = kludgeParts[0].toUpperCase();
|
||||
kludgeParts[1] = kludgeParts[1].trim();
|
||||
|
||||
(msgBody.kludgeLines[kludgeParts[0]] = msgBody.kludgeLines[kludgeParts[0]] || []).push(kludgeParts[1]);
|
||||
self.setOrAppend(kludgeParts[1], msgBody.kludgeLines[kludgeParts[0]]);
|
||||
}
|
||||
|
||||
var sauceBuffers;
|
||||
|
||||
msgLines.forEach(function nextLine(line) {
|
||||
if(0 === line.length) {
|
||||
msgBody.message.push('');
|
||||
|
@ -196,10 +763,12 @@ function FTNMailPacket(options) {
|
|||
preOrigin = false;
|
||||
} else if(self.KLUDGE_PREFIX === line.charAt(0)) {
|
||||
addKludgeLine(line.slice(1));
|
||||
} else if(!sauceBuffers || _.startsWith(line, '\x1aSAUCE00')) {
|
||||
sauceBuffers = sauceBuffers || buffers();
|
||||
sauceBuffers.push(new Buffer(line));
|
||||
} else {
|
||||
msgBody.message.push(line);
|
||||
}
|
||||
// :TODO: SAUCE/etc. can be present?
|
||||
} else {
|
||||
if(_.startsWith(line, 'SEEN-BY:')) {
|
||||
msgBody.seenBy.push(line.substring(line.indexOf(':') + 1).trim());
|
||||
|
@ -209,29 +778,36 @@ function FTNMailPacket(options) {
|
|||
}
|
||||
});
|
||||
|
||||
if(sauceBuffers) {
|
||||
// :TODO: parse sauce -> sauce buffer. This needs changes to this method to return message & optional sauce
|
||||
}
|
||||
|
||||
cb(null, msgBody);
|
||||
};
|
||||
|
||||
this.extractMessages = function(buffer, cb) {
|
||||
var nullTermBuf = new Buffer( [ 0 ] );
|
||||
this.extractMessages = function(buffer, iterator, cb) {
|
||||
assert(Buffer.isBuffer(buffer));
|
||||
assert(_.isFunction(iterator));
|
||||
|
||||
const NULL_TERM_BUFFER = new Buffer( [ 0 ] );
|
||||
|
||||
binary.stream(buffer).loop(function looper(end, vars) {
|
||||
this
|
||||
.word16lu('messageType')
|
||||
.word16lu('originNode')
|
||||
.word16lu('origNode')
|
||||
.word16lu('destNode')
|
||||
.word16lu('originNet')
|
||||
.word16lu('origNet')
|
||||
.word16lu('destNet')
|
||||
.word8('attrFlags1')
|
||||
.word8('attrFlags2')
|
||||
.word16lu('cost')
|
||||
.scan('modDateTime', nullTermBuf)
|
||||
.scan('toUserName', nullTermBuf)
|
||||
.scan('fromUserName', nullTermBuf)
|
||||
.scan('subject', nullTermBuf)
|
||||
.scan('message', nullTermBuf)
|
||||
.scan('modDateTime', NULL_TERM_BUFFER)
|
||||
.scan('toUserName', NULL_TERM_BUFFER)
|
||||
.scan('fromUserName', NULL_TERM_BUFFER)
|
||||
.scan('subject', NULL_TERM_BUFFER)
|
||||
.scan('message', NULL_TERM_BUFFER)
|
||||
.tap(function tapped(msgData) {
|
||||
if(!msgData.originNode) {
|
||||
if(!msgData.origNode) {
|
||||
end();
|
||||
cb(null);
|
||||
return;
|
||||
|
@ -247,21 +823,26 @@ function FTNMailPacket(options) {
|
|||
// Now, create a Message object
|
||||
//
|
||||
var msg = new Message( {
|
||||
// :TODO: areaId needs to be looked up via AREA line - may need a 1:n alias -> area ID lookup
|
||||
// AREA FTN -> local conf/area occurs elsewhere
|
||||
toUserName : msgData.toUserName,
|
||||
fromUserName : msgData.fromUserName,
|
||||
subject : msgData.subject,
|
||||
message : msgBody.message.join('\n'), // :TODO: \r\n is better?
|
||||
modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime),
|
||||
meta : self.getMessageMeta(msgBody),
|
||||
meta : self.getMessageMeta(msgBody, msgData),
|
||||
|
||||
|
||||
});
|
||||
|
||||
self.emit('message', msg); // :TODO: Placeholder
|
||||
iterator(msg);
|
||||
//self.emit('message', msg); // :TODO: Placeholder
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//this.getMessageHeaderBuffer = function(headerInfo)
|
||||
|
||||
this.parseFtnMessages = function(buffer, cb) {
|
||||
var nullTermBuf = new Buffer( [ 0 ] );
|
||||
var fidoMessages = [];
|
||||
|
@ -269,9 +850,9 @@ function FTNMailPacket(options) {
|
|||
binary.stream(buffer).loop(function looper(end, vars) {
|
||||
this
|
||||
.word16lu('messageType')
|
||||
.word16lu('originNode')
|
||||
.word16lu('origNode')
|
||||
.word16lu('destNode')
|
||||
.word16lu('originNet')
|
||||
.word16lu('origNet')
|
||||
.word16lu('destNet')
|
||||
.word8('attrFlags1')
|
||||
.word8('attrFlags2')
|
||||
|
@ -282,7 +863,7 @@ function FTNMailPacket(options) {
|
|||
.scan('subject', nullTermBuf)
|
||||
.scan('message', nullTermBuf)
|
||||
.tap(function tapped(msgData) {
|
||||
if(!msgData.originNode) {
|
||||
if(!msgData.origNode) {
|
||||
end();
|
||||
cb(null, fidoMessages);
|
||||
return;
|
||||
|
@ -302,7 +883,10 @@ function FTNMailPacket(options) {
|
|||
});
|
||||
};
|
||||
|
||||
this.extractMesssagesFromPacketBuffer = function(packetBuffer, cb) {
|
||||
this.extractMesssagesFromPacketBuffer = function(packetBuffer, iterator, cb) {
|
||||
assert(Buffer.isBuffer(packetBuffer));
|
||||
assert(_.isFunction(iterator));
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function parseHeader(callback) {
|
||||
|
@ -318,7 +902,8 @@ function FTNMailPacket(options) {
|
|||
},
|
||||
function extractEmbeddedMessages(callback) {
|
||||
// note: packet header is 58 bytes in length
|
||||
self.extractMessages(packetBuffer.slice(58), function extracted(err) {
|
||||
self.extractMessages(
|
||||
packetBuffer.slice(58), iterator, function extracted(err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
@ -361,7 +946,7 @@ function FTNMailPacket(options) {
|
|||
};
|
||||
}
|
||||
|
||||
require('util').inherits(FTNMailPacket, MailPacket);
|
||||
//require('util').inherits(FTNMailPacket, MailPacket);
|
||||
|
||||
FTNMailPacket.prototype.parse = function(path, cb) {
|
||||
var self = this;
|
||||
|
@ -385,41 +970,67 @@ FTNMailPacket.prototype.parse = function(path, cb) {
|
|||
);
|
||||
};
|
||||
|
||||
FTNMailPacket.prototype.read = function(options) {
|
||||
FTNMailPacket.super_.prototype.read.call(this, options);
|
||||
|
||||
FTNMailPacket.prototype.read = function(pathOrBuffer, iterator, cb) {
|
||||
var self = this;
|
||||
|
||||
if(_.isString(options.packetPath)) {
|
||||
if(_.isString(pathOrBuffer)) {
|
||||
async.waterfall(
|
||||
[
|
||||
function readPacketFile(callback) {
|
||||
fs.readFile(options.packetPath, function packetData(err, data) {
|
||||
fs.readFile(pathOrBuffer, function packetData(err, data) {
|
||||
callback(err, data);
|
||||
});
|
||||
},
|
||||
function extractMessages(data, callback) {
|
||||
self.extractMesssagesFromPacketBuffer(data, function extracted(err) {
|
||||
callback(err);
|
||||
});
|
||||
self.extractMesssagesFromPacketBuffer(data, iterator, callback);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
self.emit('error', err);
|
||||
cb
|
||||
);
|
||||
} else if(Buffer.isBuffer(pathOrBuffer)) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
FTNMailPacket.prototype.write = function(messages, fileName, options) {
|
||||
if(!_.isArray(messages)) {
|
||||
messages = [ messages ];
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
var ftnPacket = new FTNPacket();
|
||||
var theHeader;
|
||||
var written = false;
|
||||
ftnPacket.read(
|
||||
process.argv[2],
|
||||
function iterator(dataType, data) {
|
||||
if('header' === dataType) {
|
||||
theHeader = data;
|
||||
console.log(theHeader);
|
||||
} else if('message' === dataType) {
|
||||
const msg = data;
|
||||
console.log(msg);
|
||||
|
||||
if(!written) {
|
||||
written = true;
|
||||
|
||||
let messages = [ msg ];
|
||||
ftnPacket.write('/home/nuskooler/Downloads/ftnout/test1.pkt', theHeader, messages, err => {
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
function completion(err) {
|
||||
console.log(err);
|
||||
}
|
||||
);
|
||||
} else if(Buffer.isBuffer(options.packetBuffer)) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
FTNMailPacket.prototype.write = function(options) {
|
||||
FTNMailPacket.super_.prototype.write.call(this, options);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
var mailPacket = new FTNMailPacket(
|
||||
{
|
||||
nodeAddresses : {
|
||||
|
@ -434,11 +1045,42 @@ var mailPacket = new FTNMailPacket(
|
|||
}
|
||||
);
|
||||
|
||||
mailPacket.on('message', function msgParsed(msg) {
|
||||
console.log(msg);
|
||||
});
|
||||
|
||||
mailPacket.read( { packetPath : '/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/BAD_BNDL.007' } );
|
||||
var didWrite = false;
|
||||
mailPacket.read(
|
||||
process.argv[2],
|
||||
//'/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/mf/extracted/27000425.pkt',
|
||||
function packetIter(msg) {
|
||||
console.log(msg);
|
||||
if(_.has(msg, 'meta.FtnProperty.ftn_area')) {
|
||||
console.log('AREA: ' + msg.meta.FtnProperty.ftn_area);
|
||||
}
|
||||
|
||||
if(!didWrite) {
|
||||
console.log(mailPacket.packetHeader);
|
||||
console.log('-----------');
|
||||
|
||||
|
||||
didWrite = true;
|
||||
|
||||
let outTest = fs.createWriteStream('/home/nuskooler/Downloads/ftnout/test1.pkt');
|
||||
let buffer = mailPacket.getPacketHeaderBuffer(mailPacket.packetHeader);
|
||||
//mailPacket.write(buffer, msg.packetHeader);
|
||||
outTest.write(buffer);
|
||||
}
|
||||
},
|
||||
function complete(err) {
|
||||
console.log(err);
|
||||
}
|
||||
);
|
||||
*/
|
||||
/*
|
||||
Area Map
|
||||
networkName: {
|
||||
area_tag: conf_name:area_tag_name
|
||||
...
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
mailPacket.parse('/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/BAD_BNDL.007', function parsed(err, messages) {
|
||||
|
|
|
@ -10,11 +10,14 @@ var binary = require('binary');
|
|||
var fs = require('fs');
|
||||
var util = require('util');
|
||||
var iconv = require('iconv-lite');
|
||||
var moment = require('moment');
|
||||
|
||||
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
||||
exports.stringFromFTN = stringFromFTN;
|
||||
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
||||
exports.getFormattedFTNAddress = getFormattedFTNAddress;
|
||||
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
||||
exports.getDateTimeString = getDateTimeString;
|
||||
|
||||
exports.getQuotePrefix = getQuotePrefix;
|
||||
|
||||
|
@ -33,6 +36,14 @@ function stringFromFTN(buf, encoding) {
|
|||
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
|
||||
}
|
||||
|
||||
function stringToNullPaddedBuffer(s, bufLen) {
|
||||
let buffer = new Buffer(bufLen).fill(0x00);
|
||||
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
|
||||
for(let i = 0; i < enc.length; ++i) {
|
||||
buffer[i] = enc[i];
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
//
|
||||
// Convert a FTN style DateTime string to a Date object
|
||||
|
@ -44,9 +55,34 @@ function getDateFromFtnDateTime(dateTime) {
|
|||
// "Tue 01 Jan 80 00:00"
|
||||
// "27 Feb 15 00:00:03"
|
||||
//
|
||||
// :TODO: Use moment.js here
|
||||
return (new Date(Date.parse(dateTime))).toISOString();
|
||||
}
|
||||
|
||||
function getDateTimeString(m) {
|
||||
//
|
||||
// From http://ftsc.org/docs/fts-0001.016:
|
||||
// DateTime = (* a character string 20 characters long *)
|
||||
// (* 01 Jan 86 02:34:56 *)
|
||||
// DayOfMonth " " Month " " Year " "
|
||||
// " " HH ":" MM ":" SS
|
||||
// Null
|
||||
//
|
||||
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
||||
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
||||
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
|
||||
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
|
||||
// HH = "00" | .. | "23"
|
||||
// MM = "00" | .. | "59"
|
||||
// SS = "00" | .. | "59"
|
||||
//
|
||||
if(!moment.isMoment(m)) {
|
||||
m = moment(m);
|
||||
}
|
||||
|
||||
return m.format('DD MMM YY HH:mm:ss');
|
||||
}
|
||||
|
||||
function getFormattedFTNAddress(address, dimensions) {
|
||||
//var addr = util.format('%d:%d', address.zone, address.net);
|
||||
var addr = '{0}:{1}'.format(address.zone, address.net);
|
||||
|
|
|
@ -25,6 +25,10 @@ function MenuModule(options) {
|
|||
var self = this;
|
||||
this.menuName = options.menuName;
|
||||
this.menuConfig = options.menuConfig;
|
||||
this.client = options.client;
|
||||
|
||||
// :TODO: this and the line below with .config creates empty ({}) objects in the theme --
|
||||
// ...which we really should not do. If they aren't there already, don't use 'em.
|
||||
this.menuConfig.options = options.menuConfig.options || {};
|
||||
this.menuMethods = {}; // methods called from @method's
|
||||
|
||||
|
@ -190,10 +194,7 @@ require('util').inherits(MenuModule, PluginModule);
|
|||
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
|
||||
|
||||
|
||||
MenuModule.prototype.enter = function(client) {
|
||||
this.client = client;
|
||||
assert(_.isObject(client));
|
||||
|
||||
MenuModule.prototype.enter = function() {
|
||||
if(_.isString(this.menuConfig.status)) {
|
||||
this.client.currentStatus = this.menuConfig.status;
|
||||
} else {
|
||||
|
|
|
@ -131,7 +131,7 @@ MenuStack.prototype.goto = function(name, options, cb) {
|
|||
modInst.restoreSavedState(options.savedState);
|
||||
}
|
||||
|
||||
modInst.enter(self.client);
|
||||
modInst.enter();
|
||||
|
||||
self.client.log.trace(
|
||||
{ stack : _.map(self.stack, function(si) { return si.name; } ) },
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
// ENiGMA½
|
||||
var moduleUtil = require('./module_util.js');
|
||||
var Log = require('./logger.js').log;
|
||||
var conf = require('./config.js'); // :TODO: remove me!
|
||||
var Config = require('./config.js').config;
|
||||
var asset = require('./asset.js');
|
||||
var theme = require('./theme.js');
|
||||
var getFullConfig = require('./config_util.js').getFullConfig;
|
||||
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
||||
var acsUtil = require('./acs_util.js');
|
||||
|
@ -68,17 +66,18 @@ function loadMenu(options, cb) {
|
|||
});
|
||||
},
|
||||
function loadMenuModule(menuConfig, callback) {
|
||||
var modAsset = asset.getModuleAsset(menuConfig.module);
|
||||
var modSupplied = null !== modAsset;
|
||||
|
||||
var modLoadOpts = {
|
||||
const modAsset = asset.getModuleAsset(menuConfig.module);
|
||||
const modSupplied = null !== modAsset;
|
||||
|
||||
const modLoadOpts = {
|
||||
name : modSupplied ? modAsset.asset : 'standard_menu',
|
||||
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods,
|
||||
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
|
||||
};
|
||||
|
||||
moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) {
|
||||
var modData = {
|
||||
const modData = {
|
||||
name : modLoadOpts.name,
|
||||
config : menuConfig,
|
||||
mod : mod,
|
||||
|
@ -97,7 +96,8 @@ function loadMenu(options, cb) {
|
|||
{
|
||||
menuName : options.name,
|
||||
menuConfig : modData.config,
|
||||
extraArgs : options.extraArgs
|
||||
extraArgs : options.extraArgs,
|
||||
client : options.client,
|
||||
});
|
||||
callback(null, moduleInstance);
|
||||
} catch(e) {
|
||||
|
@ -174,7 +174,7 @@ function handleAction(client, formData, conf) {
|
|||
assert(_.isObject(conf));
|
||||
assert(_.isString(conf.action));
|
||||
|
||||
var actionAsset = asset.parseAsset(conf.action);
|
||||
const actionAsset = asset.parseAsset(conf.action);
|
||||
assert(_.isObject(actionAsset));
|
||||
|
||||
switch(actionAsset.type) {
|
||||
|
@ -245,84 +245,3 @@ function handleNext(client, nextSpec, conf) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// :TODO: Seems better in theme.js, but that includes ViewController...which would then include theme.js
|
||||
// ...theme.js only brings in VC to create themed pause prompt. Perhaps that should live elsewhere
|
||||
/*
|
||||
function applyGeneralThemeCustomization(options) {
|
||||
//
|
||||
// options.name
|
||||
// options.client
|
||||
// options.type
|
||||
// options.config
|
||||
//
|
||||
assert(_.isString(options.name));
|
||||
assert(_.isObject(options.client));
|
||||
assert("menus" === options.type || "prompts" === options.type);
|
||||
|
||||
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
|
||||
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
|
||||
|
||||
if(themeConfig.config) {
|
||||
Object.keys(themeConfig.config).forEach(function confEntry(conf) {
|
||||
if(options.config[conf]) {
|
||||
_.defaultsDeep(options.config[conf], themeConfig.config[conf]);
|
||||
} else {
|
||||
options.config[conf] = themeConfig.config[conf];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
function applyMciThemeCustomization(options) {
|
||||
//
|
||||
// options.name : menu/prompt name
|
||||
// options.mci : menu/prompt .mci section
|
||||
// options.client : client
|
||||
// options.type : menu|prompt
|
||||
// options.formId : (optional) form ID in cases where multiple forms may exist wanting their own customization
|
||||
//
|
||||
// In the case of formId, the theme must include the ID as well, e.g.:
|
||||
// {
|
||||
// ...
|
||||
// "2" : {
|
||||
// "TL1" : { ... }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
assert(_.isString(options.name));
|
||||
assert("menus" === options.type || "prompts" === options.type);
|
||||
assert(_.isObject(options.client));
|
||||
|
||||
if(_.isUndefined(options.mci)) {
|
||||
options.mci = {};
|
||||
}
|
||||
|
||||
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
|
||||
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
|
||||
|
||||
if(options.formId && _.has(themeConfig, options.formId.toString())) {
|
||||
// form ID found - use exact match
|
||||
themeConfig = themeConfig[options.formId];
|
||||
}
|
||||
|
||||
if(themeConfig.mci) {
|
||||
Object.keys(themeConfig.mci).forEach(function mciEntry(mci) {
|
||||
// :TODO: a better way to do this?
|
||||
if(options.mci[mci]) {
|
||||
_.defaults(options.mci[mci], themeConfig.mci[mci]);
|
||||
} else {
|
||||
options.mci[mci] = themeConfig.mci[mci];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
|
||||
}
|
||||
*/
|
|
@ -16,7 +16,7 @@ function Message(options) {
|
|||
options = options || {};
|
||||
|
||||
this.messageId = options.messageId || 0; // always generated @ persist
|
||||
this.areaName = options.areaName || Message.WellKnownAreaNames.Invalid;
|
||||
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
|
||||
this.uuid = uuid.v1();
|
||||
this.replyToMsgId = options.replyToMsgId || 0;
|
||||
this.toUserName = options.toUserName || '';
|
||||
|
@ -55,7 +55,7 @@ function Message(options) {
|
|||
};
|
||||
|
||||
this.isPrivate = function() {
|
||||
return this.areaName === Message.WellKnownAreaNames.Private ? true : false;
|
||||
return this.areaTag === Message.WellKnownAreaTags.Private ? true : false;
|
||||
};
|
||||
|
||||
this.getMessageTimestampString = function(ts) {
|
||||
|
@ -80,7 +80,7 @@ function Message(options) {
|
|||
*/
|
||||
}
|
||||
|
||||
Message.WellKnownAreaNames = {
|
||||
Message.WellKnownAreaTags = {
|
||||
Invalid : '',
|
||||
Private : 'private_mail',
|
||||
Bulletin : 'local_bulletin',
|
||||
|
@ -105,15 +105,20 @@ Message.SystemMetaNames = {
|
|||
};
|
||||
|
||||
Message.FtnPropertyNames = {
|
||||
FtnCost : 'ftn_cost',
|
||||
FtnOrigNode : 'ftn_orig_node',
|
||||
FtnDestNode : 'ftn_dest_node',
|
||||
FtnOrigNetwork : 'ftn_orig_network',
|
||||
FtnDestNetwork : 'ftn_dest_network',
|
||||
FtnAttrFlags1 : 'ftn_attr_flags1',
|
||||
FtnAttrFlags2 : 'ftn_attr_flags2',
|
||||
FtnCost : 'ftn_cost',
|
||||
FtnOrigZone : 'ftn_orig_zone',
|
||||
FtnDestZone : 'ftn_dest_zone',
|
||||
FtnOrigPoint : 'ftn_orig_point',
|
||||
FtnDestPoint : 'ftn_dest_point',
|
||||
|
||||
|
||||
|
||||
FtnAttribute : 'ftn_attribute',
|
||||
|
||||
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
|
||||
|
@ -141,7 +146,7 @@ Message.prototype.load = function(options, cb) {
|
|||
[
|
||||
function loadMessage(callback) {
|
||||
msgDb.get(
|
||||
'SELECT message_id, area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' +
|
||||
'SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' +
|
||||
'message, modified_timestamp, view_count ' +
|
||||
'FROM message ' +
|
||||
'WHERE message_uuid=? ' +
|
||||
|
@ -149,7 +154,7 @@ Message.prototype.load = function(options, cb) {
|
|||
[ options.uuid ],
|
||||
function row(err, msgRow) {
|
||||
self.messageId = msgRow.message_id;
|
||||
self.areaName = msgRow.area_name;
|
||||
self.areaTag = msgRow.area_tag;
|
||||
self.messageUuid = msgRow.message_uuid;
|
||||
self.replyToMsgId = msgRow.reply_to_message_id;
|
||||
self.toUserName = msgRow.to_user_name;
|
||||
|
@ -202,8 +207,8 @@ Message.prototype.persist = function(cb) {
|
|||
},
|
||||
function storeMessage(callback) {
|
||||
msgDb.run(
|
||||
'INSERT INTO message (area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?);', [ self.areaName, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
|
||||
'INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?);', [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
|
||||
function msgInsert(err) {
|
||||
if(!err) {
|
||||
self.messageId = this.lastID;
|
||||
|
|
|
@ -5,100 +5,275 @@ var msgDb = require('./database.js').dbs.message;
|
|||
var Config = require('./config.js').config;
|
||||
var Message = require('./message.js');
|
||||
var Log = require('./logger.js').log;
|
||||
var checkAcs = require('./acs_util.js').checkAcs;
|
||||
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
|
||||
exports.getAvailableMessageAreas = getAvailableMessageAreas;
|
||||
exports.getDefaultMessageArea = getDefaultMessageArea;
|
||||
exports.getMessageAreaByName = getMessageAreaByName;
|
||||
exports.getAvailableMessageConferences = getAvailableMessageConferences;
|
||||
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
|
||||
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
|
||||
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
|
||||
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
|
||||
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
|
||||
exports.getMessageConferenceByTag = getMessageConferenceByTag;
|
||||
exports.getMessageAreaByTag = getMessageAreaByTag;
|
||||
exports.changeMessageConference = changeMessageConference;
|
||||
exports.changeMessageArea = changeMessageArea;
|
||||
exports.getMessageListForArea = getMessageListForArea;
|
||||
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
||||
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
||||
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
|
||||
|
||||
function getAvailableMessageAreas(options) {
|
||||
// example: [ { "name" : "local_music", "desc" : "Music Discussion", "groups" : ["somegroup"] }, ... ]
|
||||
options = options || {};
|
||||
const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]';
|
||||
const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]';
|
||||
|
||||
var areas = Config.messages.areas;
|
||||
var avail = [];
|
||||
for(var i = 0; i < areas.length; ++i) {
|
||||
if(true !== options.includePrivate &&
|
||||
Message.WellKnownAreaNames.Private === areas[i].name)
|
||||
{
|
||||
continue;
|
||||
const AREA_ACS_DEFAULT = {
|
||||
read : CONF_AREA_RW_ACS_DEFAULT,
|
||||
write : CONF_AREA_RW_ACS_DEFAULT,
|
||||
manage : AREA_MANAGE_ACS_DEFAULT,
|
||||
};
|
||||
|
||||
function getAvailableMessageConferences(client, options) {
|
||||
options = options || { includeSystemInternal : false };
|
||||
|
||||
// perform ACS check per conf & omit system_internal if desired
|
||||
return _.omit(Config.messageConferences, (v, k) => {
|
||||
if(!options.includeSystemInternal && 'system_internal' === k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
avail.push(areas[i]);
|
||||
const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
return !checkAcs(client, readAcs);
|
||||
});
|
||||
}
|
||||
|
||||
return avail;
|
||||
}
|
||||
|
||||
function getDefaultMessageArea() {
|
||||
//
|
||||
// Return first non-private/etc. area name. This will be from config.hjson
|
||||
//
|
||||
return getAvailableMessageAreas()[0];
|
||||
/*
|
||||
var avail = getAvailableMessageAreas();
|
||||
for(var i = 0; i < avail.length; ++i) {
|
||||
if(Message.WellKnownAreaNames.Private !== avail[i].name) {
|
||||
return avail[i];
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function getMessageAreaByName(areaName) {
|
||||
areaName = areaName.toLowerCase();
|
||||
|
||||
var availAreas = getAvailableMessageAreas( { includePrivate : true } );
|
||||
var index = _.findIndex(availAreas, function pred(an) {
|
||||
return an.name == areaName;
|
||||
function getSortedAvailMessageConferences(client, options) {
|
||||
var sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
||||
return {
|
||||
confTag : k,
|
||||
conf : v,
|
||||
};
|
||||
});
|
||||
|
||||
if(index > -1) {
|
||||
return availAreas[index];
|
||||
sorted.sort((a, b) => {
|
||||
return a.conf.name.localeCompare(b.conf.name);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// 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 changeMessageArea(client, areaName, cb) {
|
||||
function getSortedAvailMessageAreasByConfTag(confTag, options) {
|
||||
const areas = getAvailableMessageAreasByConfTag(confTag, options);
|
||||
|
||||
// :TODO: should probably be using localeCompare / sort
|
||||
return _.sortBy(_.map(areas, (v, k) => {
|
||||
return {
|
||||
areaTag : k,
|
||||
area : v,
|
||||
};
|
||||
}), o => o.area.name); // sort by name
|
||||
}
|
||||
|
||||
function getDefaultMessageConferenceTag(client, disableAcsCheck) {
|
||||
//
|
||||
// Find the first conference marked 'default'. If found,
|
||||
// inspect |client| against *read* ACS using defaults if not
|
||||
// specified.
|
||||
//
|
||||
// If the above fails, just go down the list until we get one
|
||||
// that passes.
|
||||
//
|
||||
// It's possible that we end up with nothing here!
|
||||
//
|
||||
// Note that built in 'system_internal' is always ommited here
|
||||
//
|
||||
let defaultConf = _.findKey(Config.messageConferences, o => o.default);
|
||||
if(defaultConf) {
|
||||
const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(true === disableAcsCheck || checkAcs(client, acs)) {
|
||||
return defaultConf;
|
||||
}
|
||||
}
|
||||
|
||||
// just use anything we can
|
||||
defaultConf = _.findKey(Config.messageConferences, (o, k) => {
|
||||
const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs));
|
||||
});
|
||||
|
||||
return defaultConf;
|
||||
}
|
||||
|
||||
function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
|
||||
//
|
||||
// Similar to finding the default conference:
|
||||
// Find the first entry marked 'default', if any. If found, check | client| against
|
||||
// *read* ACS. If this fails, just find the first one we can that passes checks.
|
||||
//
|
||||
// It's possible that we end up with nothing!
|
||||
//
|
||||
confTag = confTag || getDefaultMessageConferenceTag(client);
|
||||
|
||||
if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) {
|
||||
const areaPool = Config.messageConferences[confTag].areas;
|
||||
let defaultArea = _.findKey(areaPool, o => o.default);
|
||||
if(defaultArea) {
|
||||
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||
if(true === disableAcsCheck || checkAcs(client, readAcs)) {
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
defaultArea = _.findKey(areaPool, (o, k) => {
|
||||
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||
return (true === disableAcsCheck || checkAcs(client, readAcs));
|
||||
});
|
||||
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageConferenceByTag(confTag) {
|
||||
return Config.messageConferences[confTag];
|
||||
}
|
||||
|
||||
function getMessageAreaByTag(areaTag, optionalConfTag) {
|
||||
const confs = Config.messageConferences;
|
||||
|
||||
if(_.isString(optionalConfTag)) {
|
||||
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
|
||||
return confs[optionalConfTag].areas[areaTag];
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// No confTag to work with - we'll have to search through them all
|
||||
//
|
||||
var area;
|
||||
_.forEach(confs, (v, k) => {
|
||||
if(_.has(v, [ 'areas', areaTag ])) {
|
||||
area = v.areas[areaTag];
|
||||
return false; // stop iteration
|
||||
}
|
||||
});
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
|
||||
function changeMessageConference(client, confTag, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function getConf(callback) {
|
||||
const conf = getMessageConferenceByTag(confTag);
|
||||
|
||||
if(conf) {
|
||||
callback(null, conf);
|
||||
} else {
|
||||
callback(new Error('Invalid message conference tag'));
|
||||
}
|
||||
},
|
||||
function getDefaultAreaInConf(conf, callback) {
|
||||
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
||||
const area = getMessageAreaByTag(areaTag, confTag);
|
||||
|
||||
if(area) {
|
||||
callback(null, conf, { areaTag : areaTag, area : area } );
|
||||
} else {
|
||||
callback(new Error('No available areas for this user in conference'));
|
||||
}
|
||||
},
|
||||
function validateAccess(conf, areaInfo, callback) {
|
||||
const confAcs = conf.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
|
||||
if(!checkAcs(client, confAcs)) {
|
||||
callback(new Error('User does not have access to this conference'));
|
||||
} else {
|
||||
const areaAcs = _.has(areaInfo, 'area.acs.read') ? areaInfo.area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(!checkAcs(client, areaAcs)) {
|
||||
callback(new Error('User does not have access to default area in this conference'));
|
||||
} else {
|
||||
callback(null, conf, areaInfo);
|
||||
}
|
||||
}
|
||||
},
|
||||
function changeConferenceAndArea(conf, areaInfo, callback) {
|
||||
const newProps = {
|
||||
message_conf_tag : confTag,
|
||||
message_area_tag : areaInfo.areaTag,
|
||||
};
|
||||
client.user.persistProperties(newProps, err => {
|
||||
callback(err, conf, areaInfo);
|
||||
});
|
||||
},
|
||||
],
|
||||
function complete(err, conf, areaInfo) {
|
||||
if(!err) {
|
||||
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
|
||||
} else {
|
||||
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
|
||||
}
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function changeMessageArea(client, areaTag, cb) {
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getArea(callback) {
|
||||
var area = getMessageAreaByName(areaName);
|
||||
const area = getMessageAreaByTag(areaTag);
|
||||
|
||||
if(area) {
|
||||
callback(null, area);
|
||||
} else {
|
||||
callback(new Error('Invalid message area'));
|
||||
callback(new Error('Invalid message area tag'));
|
||||
}
|
||||
},
|
||||
function validateAccess(area, callback) {
|
||||
if(_.isArray(area.groups) && !
|
||||
client.user.isGroupMember(area.groups))
|
||||
{
|
||||
//
|
||||
// Need at least *read* to access the area
|
||||
//
|
||||
const readAcs = _.has(area, 'acs.read') ? area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(!checkAcs(client, readAcs)) {
|
||||
callback(new Error('User does not have access to this area'));
|
||||
} else {
|
||||
callback(null, area);
|
||||
}
|
||||
},
|
||||
function changeArea(area, callback) {
|
||||
client.user.persistProperty('message_area_name', area.name, function persisted(err) {
|
||||
client.user.persistProperty('message_area_tag', areaTag, function persisted(err) {
|
||||
callback(err, area);
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err, area) {
|
||||
if(!err) {
|
||||
client.log.info( area, 'Current message area changed');
|
||||
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
|
||||
} else {
|
||||
client.log.warn( { area : area, error : err.message }, 'Could not change message area');
|
||||
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
||||
}
|
||||
|
||||
cb(err);
|
||||
|
@ -119,9 +294,9 @@ function getMessageFromRow(row) {
|
|||
};
|
||||
}
|
||||
|
||||
function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
||||
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
||||
//
|
||||
// If |areaName| is Message.WellKnownAreaNames.Private,
|
||||
// If |areaTag| is Message.WellKnownAreaTags.Private,
|
||||
// only messages addressed to |userId| should be returned.
|
||||
//
|
||||
// Only messages > lastMessageId should be returned
|
||||
|
@ -131,7 +306,7 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
async.waterfall(
|
||||
[
|
||||
function getLastMessageId(callback) {
|
||||
getMessageAreaLastReadId(userId, areaName, function fetched(err, lastMessageId) {
|
||||
getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) {
|
||||
callback(null, lastMessageId || 0); // note: willingly ignoring any errors here!
|
||||
});
|
||||
},
|
||||
|
@ -139,9 +314,9 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
var sql =
|
||||
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
||||
'FROM message ' +
|
||||
'WHERE area_name="' + areaName + '" AND message_id > ' + lastMessageId;
|
||||
'WHERE area_tag ="' + areaTag + '" AND message_id > ' + lastMessageId;
|
||||
|
||||
if(Message.WellKnownAreaNames.Private === areaName) {
|
||||
if(Message.WellKnownAreaTags.Private === areaTag) {
|
||||
sql +=
|
||||
' AND message_id in (' +
|
||||
'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System +
|
||||
|
@ -150,8 +325,6 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
|
||||
sql += ' ORDER BY message_id;';
|
||||
|
||||
console.log(sql)
|
||||
|
||||
msgDb.each(sql, function msgRow(err, row) {
|
||||
if(!err) {
|
||||
msgList.push(getMessageFromRow(row));
|
||||
|
@ -160,18 +333,17 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
}
|
||||
],
|
||||
function complete(err) {
|
||||
console.log(msgList)
|
||||
cb(err, msgList);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getMessageListForArea(options, areaName, cb) {
|
||||
function getMessageListForArea(options, areaTag, cb) {
|
||||
//
|
||||
// options.client (required)
|
||||
//
|
||||
|
||||
options.client.log.debug( { areaName : areaName }, 'Fetching available messages');
|
||||
options.client.log.debug( { areaTag : areaTag }, 'Fetching available messages');
|
||||
|
||||
assert(_.isObject(options.client));
|
||||
|
||||
|
@ -193,9 +365,9 @@ function getMessageListForArea(options, areaName, cb) {
|
|||
msgDb.each(
|
||||
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
||||
'FROM message ' +
|
||||
'WHERE area_name=? ' +
|
||||
'WHERE area_tag = ? ' +
|
||||
'ORDER BY message_id;',
|
||||
[ areaName.toLowerCase() ],
|
||||
[ areaTag.toLowerCase() ],
|
||||
function msgRow(err, row) {
|
||||
if(!err) {
|
||||
msgList.push(getMessageFromRow(row));
|
||||
|
@ -214,24 +386,24 @@ function getMessageListForArea(options, areaName, cb) {
|
|||
);
|
||||
}
|
||||
|
||||
function getMessageAreaLastReadId(userId, areaName, cb) {
|
||||
function getMessageAreaLastReadId(userId, areaTag, cb) {
|
||||
msgDb.get(
|
||||
'SELECT message_id ' +
|
||||
'FROM user_message_area_last_read ' +
|
||||
'WHERE user_id = ? AND area_name = ?;',
|
||||
[ userId, areaName ],
|
||||
'WHERE user_id = ? AND area_tag = ?;',
|
||||
[ userId, areaTag ],
|
||||
function complete(err, row) {
|
||||
cb(err, row ? row.message_id : 0);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
|
||||
function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) {
|
||||
// :TODO: likely a better way to do this...
|
||||
async.waterfall(
|
||||
[
|
||||
function getCurrent(callback) {
|
||||
getMessageAreaLastReadId(userId, areaName, function result(err, lastId) {
|
||||
getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
|
||||
lastId = lastId || 0;
|
||||
callback(null, lastId); // ignore errors as we default to 0
|
||||
});
|
||||
|
@ -239,25 +411,29 @@ function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
|
|||
function update(lastId, callback) {
|
||||
if(messageId > lastId) {
|
||||
msgDb.run(
|
||||
'REPLACE INTO user_message_area_last_read (user_id, area_name, message_id) ' +
|
||||
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
|
||||
'VALUES (?, ?, ?);',
|
||||
[ userId, areaName, messageId ],
|
||||
callback
|
||||
[ userId, areaTag, messageId ],
|
||||
function written(err) {
|
||||
callback(err, true); // true=didUpdate
|
||||
}
|
||||
);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
function complete(err, didUpdate) {
|
||||
if(err) {
|
||||
Log.debug(
|
||||
{ error : err.toString(), userId : userId, areaName : areaName, messageId : messageId },
|
||||
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId },
|
||||
'Failed updating area last read ID');
|
||||
} else {
|
||||
if(true === didUpdate) {
|
||||
Log.trace(
|
||||
{ userId : userId, areaName : areaName, messageId : messageId },
|
||||
{ userId : userId, areaTag : areaTag, messageId : messageId },
|
||||
'Area last read ID updated');
|
||||
}
|
||||
}
|
||||
cb(err);
|
||||
}
|
||||
|
|
|
@ -14,20 +14,25 @@ exports.loadModuleEx = loadModuleEx;
|
|||
exports.loadModule = loadModule;
|
||||
exports.loadModulesForCategory = loadModulesForCategory;
|
||||
|
||||
|
||||
function loadModuleEx(options, cb) {
|
||||
assert(_.isObject(options));
|
||||
assert(_.isString(options.name));
|
||||
assert(_.isString(options.path));
|
||||
|
||||
var modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
|
||||
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
|
||||
|
||||
if(_.isObject(modConfig) && false === modConfig.enabled) {
|
||||
cb(new Error('Module "' + options.name + '" is disabled'));
|
||||
return;
|
||||
}
|
||||
|
||||
var mod;
|
||||
try {
|
||||
var mod = require(paths.join(options.path, options.name + '.js'));
|
||||
mod = require(paths.join(options.path, options.name + '.js'));
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
|
||||
if(!_.isObject(mod.moduleInfo)) {
|
||||
cb(new Error('Module is missing "moduleInfo" section'));
|
||||
|
@ -39,13 +44,10 @@ function loadModuleEx(options, cb) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Safe configuration, if any, for convience to the module
|
||||
// Ref configuration, if any, for convience to the module
|
||||
mod.runtime = { config : modConfig };
|
||||
|
||||
cb(null, mod);
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadModule(name, category, cb) {
|
||||
|
@ -61,7 +63,7 @@ function loadModule(name, category, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
function loadModulesForCategory(category, cb) {
|
||||
function loadModulesForCategory(category, iterator) {
|
||||
var path = Config.paths[category];
|
||||
|
||||
fs.readdir(path, function onFiles(err, files) {
|
||||
|
@ -72,8 +74,7 @@ function loadModulesForCategory(category, cb) {
|
|||
|
||||
var filtered = files.filter(function onFilter(file) { return '.js' === paths.extname(file); });
|
||||
filtered.forEach(function onFile(file) {
|
||||
var modName = paths.basename(file, '.js');
|
||||
loadModule(paths.basename(file, '.js'), category, cb);
|
||||
loadModule(paths.basename(file, '.js'), category, iterator);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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
|
||||
};
|
103
core/new_scan.js
103
core/new_scan.js
|
@ -7,6 +7,7 @@ var Message = require('./message.js');
|
|||
var MenuModule = require('./menu_module.js').MenuModule;
|
||||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
|
@ -36,10 +37,11 @@ function NewScanModule(options) {
|
|||
var self = this;
|
||||
var config = this.menuConfig.config;
|
||||
|
||||
this.currentStep = 'messageAreas';
|
||||
this.currentScanAux = 0; // e.g. Message.WellKnownAreaNames.Private when currentSteps = messageAreas
|
||||
this.currentStep = 'messageConferences';
|
||||
this.currentScanAux = {};
|
||||
|
||||
this.scanStartFmt = config.scanStartFmt || 'Scanning {desc}...';
|
||||
// :TODO: Make this conf/area specific:
|
||||
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
|
||||
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
|
||||
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
|
||||
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
|
||||
|
@ -58,9 +60,64 @@ function NewScanModule(options) {
|
|||
}
|
||||
};
|
||||
|
||||
this.newScanMessageArea = function(cb) {
|
||||
var availMsgAreas = msgArea.getAvailableMessageAreas( { includePrivate : true } );
|
||||
var currentArea = availMsgAreas[self.currentScanAux];
|
||||
this.newScanMessageConference = function(cb) {
|
||||
// lazy init
|
||||
if(!self.sortedMessageConfs) {
|
||||
const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc.
|
||||
|
||||
self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => {
|
||||
return {
|
||||
confTag : k,
|
||||
conf : v,
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Sort conferences by name, other than 'system_internal' which should
|
||||
// always come first such that we display private mails/etc. before
|
||||
// other conferences & areas
|
||||
//
|
||||
self.sortedMessageConfs.sort((a, b) => {
|
||||
if('system_internal' === a.confTag) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.conf.name.localeCompare(b.conf.name);
|
||||
}
|
||||
});
|
||||
|
||||
self.currentScanAux.conf = self.currentScanAux.conf || 0;
|
||||
self.currentScanAux.area = self.currentScanAux.area || 0;
|
||||
}
|
||||
|
||||
const currentConf = self.sortedMessageConfs[self.currentScanAux.conf];
|
||||
|
||||
async.series(
|
||||
[
|
||||
function scanArea(callback) {
|
||||
//self.currentScanAux.area = self.currentScanAux.area || 0;
|
||||
|
||||
self.newScanMessageArea(currentConf, function areaScanComplete(err) {
|
||||
if(self.sortedMessageConfs.length > self.currentScanAux.conf + 1) {
|
||||
self.currentScanAux.conf += 1;
|
||||
self.currentScanAux.area = 0;
|
||||
|
||||
self.newScanMessageConference(cb); // recursive to next conf
|
||||
//callback(null);
|
||||
} else {
|
||||
self.updateScanStatus(self.scanCompleteMsg);
|
||||
callback(new Error('No more conferences'));
|
||||
}
|
||||
});
|
||||
}
|
||||
],
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
this.newScanMessageArea = function(conf, cb) {
|
||||
// :TODO: it would be nice to cache this - must be done by conf!
|
||||
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : self.client } );
|
||||
const currentArea = sortedAreas[self.currentScanAux.area];
|
||||
|
||||
//
|
||||
// Scan and update index until we find something. If results are found,
|
||||
|
@ -70,8 +127,8 @@ function NewScanModule(options) {
|
|||
[
|
||||
function checkAndUpdateIndex(callback) {
|
||||
// Advance to next area if possible
|
||||
if(availMsgAreas.length >= self.currentScanAux + 1) {
|
||||
self.currentScanAux += 1;
|
||||
if(sortedAreas.length >= self.currentScanAux.area + 1) {
|
||||
self.currentScanAux.area += 1;
|
||||
callback(null);
|
||||
} else {
|
||||
self.updateScanStatus(self.scanCompleteMsg);
|
||||
|
@ -80,21 +137,29 @@ function NewScanModule(options) {
|
|||
},
|
||||
function updateStatusScanStarted(callback) {
|
||||
self.updateScanStatus(self.scanStartFmt.format({
|
||||
desc : currentArea.desc,
|
||||
confName : conf.conf.name,
|
||||
confDesc : conf.conf.desc,
|
||||
areaName : currentArea.area.name,
|
||||
areaDesc : currentArea.area.desc,
|
||||
}));
|
||||
callback(null);
|
||||
},
|
||||
function newScanAreaAndGetMessages(callback) {
|
||||
msgArea.getNewMessagesInAreaForUser(
|
||||
self.client.user.userId, currentArea.name, function msgs(err, msgList) {
|
||||
self.client.user.userId, currentArea.areaTag, function msgs(err, msgList) {
|
||||
if(!err) {
|
||||
if(0 === msgList.length) {
|
||||
self.updateScanStatus(self.scanFinishNoneFmt.format({
|
||||
desc : currentArea.desc,
|
||||
confName : conf.conf.name,
|
||||
confDesc : conf.conf.desc,
|
||||
areaName : currentArea.area.name,
|
||||
areaDesc : currentArea.area.desc,
|
||||
}));
|
||||
} else {
|
||||
self.updateScanStatus(self.scanFinishNewFmt.format({
|
||||
desc : currentArea.desc,
|
||||
confName : conf.conf.name,
|
||||
confDesc : conf.conf.desc,
|
||||
areaName : currentArea.area.name,
|
||||
count : msgList.length,
|
||||
}));
|
||||
}
|
||||
|
@ -107,14 +172,14 @@ function NewScanModule(options) {
|
|||
if(msgList && msgList.length > 0) {
|
||||
var nextModuleOpts = {
|
||||
extraArgs: {
|
||||
messageAreaName : currentArea.name,
|
||||
messageAreaTag : currentArea.areaTag,
|
||||
messageList : msgList,
|
||||
}
|
||||
};
|
||||
|
||||
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
|
||||
} else {
|
||||
self.newScanMessageArea(cb);
|
||||
self.newScanMessageArea(conf, cb);
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -161,8 +226,8 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
|
|||
},
|
||||
function performCurrentStepScan(callback) {
|
||||
switch(self.currentStep) {
|
||||
case 'messageAreas' :
|
||||
self.newScanMessageArea(function scanComplete(err) {
|
||||
case 'messageConferences' :
|
||||
self.newScanMessageConference(function scanComplete(err) {
|
||||
callback(null); // finished
|
||||
});
|
||||
break;
|
||||
|
@ -180,9 +245,3 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
NewScanModule.prototype.finishedLoading = function() {
|
||||
NewScanModule.super_.prototype.finishedLoading.call(this);
|
||||
};
|
||||
*/
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
var Config = require('./config.js').config;
|
||||
var Log = require('./logger.js').log;
|
||||
var getMessageAreaByName = require('./message_area.js').getMessageAreaByName;
|
||||
var getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
|
||||
var clientConnections = require('./client_connections.js');
|
||||
var sysProp = require('./system_property.js');
|
||||
|
||||
|
@ -63,8 +63,13 @@ function getPredefinedMCIValue(client, code) {
|
|||
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
|
||||
},
|
||||
|
||||
MA : function messageAreaDescription() {
|
||||
var area = getMessageAreaByName(client.user.properties.message_area_name);
|
||||
MA : function messageAreaName() {
|
||||
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
|
||||
return area ? area.name : '';
|
||||
},
|
||||
|
||||
ML : function messageAreaDescription() {
|
||||
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
|
||||
return area ? area.desc : '';
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -18,8 +18,8 @@ function StandardMenuModule(menuConfig) {
|
|||
require('util').inherits(StandardMenuModule, MenuModule);
|
||||
|
||||
|
||||
StandardMenuModule.prototype.enter = function(client) {
|
||||
StandardMenuModule.super_.prototype.enter.call(this, client);
|
||||
StandardMenuModule.prototype.enter = function() {
|
||||
StandardMenuModule.super_.prototype.enter.call(this);
|
||||
};
|
||||
|
||||
StandardMenuModule.prototype.beforeArt = function() {
|
||||
|
|
|
@ -203,13 +203,13 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
|||
}
|
||||
}
|
||||
|
||||
[ 'menus', 'prompts' ].forEach(function areaEntry(areaName) {
|
||||
_.keys(mergedTheme[areaName]).forEach(function menuEntry(menuName) {
|
||||
[ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) {
|
||||
_.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
|
||||
var createdFormSection = false;
|
||||
var mergedThemeMenu = mergedTheme[areaName][menuName];
|
||||
var mergedThemeMenu = mergedTheme[sectionName][menuName];
|
||||
|
||||
if(_.has(theme, [ 'customization', areaName, menuName ])) {
|
||||
var menuTheme = theme.customization[areaName][menuName];
|
||||
if(_.has(theme, [ 'customization', sectionName, menuName ])) {
|
||||
var menuTheme = theme.customization[sectionName][menuName];
|
||||
|
||||
// config block is direct assign/overwrite
|
||||
// :TODO: should probably be _.merge()
|
||||
|
@ -217,7 +217,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
|||
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
|
||||
}
|
||||
|
||||
if('menus' === areaName) {
|
||||
if('menus' === sectionName) {
|
||||
if(_.isObject(mergedThemeMenu.form)) {
|
||||
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
|
||||
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
|
||||
|
@ -233,7 +233,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
|||
createdFormSection = true;
|
||||
}
|
||||
}
|
||||
} else if('prompts' === areaName) {
|
||||
} else if('prompts' === sectionName) {
|
||||
// no 'form' or form keys for prompts -- direct to mci
|
||||
applyToForm(mergedThemeMenu, menuTheme);
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
|||
// * There is/was no explicit 'form' section
|
||||
// * There is no 'prompt' specified
|
||||
//
|
||||
if('menus' === areaName && !_.isString(mergedThemeMenu.prompt) &&
|
||||
if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
|
||||
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
|
||||
{
|
||||
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
|
||||
|
|
|
@ -27,7 +27,7 @@ function VerticalMenuView(options) {
|
|||
this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row);
|
||||
}
|
||||
|
||||
if(this.autoScale.width) {
|
||||
if(self.autoScale.width) {
|
||||
var l = 0;
|
||||
self.items.forEach(function item(i) {
|
||||
if(i.text.length > l) {
|
||||
|
@ -149,6 +149,17 @@ VerticalMenuView.prototype.setFocus = function(focused) {
|
|||
VerticalMenuView.prototype.setFocusItemIndex = function(index) {
|
||||
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
||||
|
||||
//this.updateViewVisibleItems();
|
||||
|
||||
// :TODO: |viewWindow| must be updated to reflect position change --
|
||||
// if > visibile then += by diff, if < visible
|
||||
|
||||
if(this.focusedItemIndex > this.viewWindow.bottom) {
|
||||
} else if (this.focusedItemIndex < this.viewWindow.top) {
|
||||
// this.viewWindow.top--;
|
||||
// this.viewWindow.bottom--;
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
};
|
||||
|
||||
|
|
|
@ -488,7 +488,6 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) {
|
|||
assert(_.isObject(options.mciMap));
|
||||
|
||||
var self = this;
|
||||
var promptName = _.isString(options.promptName) ? options.promptName : self.client.currentMenuModule.menuConfig.prompt;
|
||||
var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig;
|
||||
var initialFocusId = 1; // default to first
|
||||
|
||||
|
|
|
@ -16,15 +16,13 @@
|
|||
return !isNaN(value) && user.getAge() >= value;
|
||||
},
|
||||
AS : function accountStatus() {
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
return _.findIndex(value, function cmp(accStatus) {
|
||||
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
|
||||
}) > -1;
|
||||
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(userAccountStatus) > -1;
|
||||
},
|
||||
EC : function isEncoding() {
|
||||
switch(value) {
|
||||
|
@ -53,7 +51,7 @@
|
|||
// :TODO: implement me!!
|
||||
return false;
|
||||
},
|
||||
SC : function isSecerConnection() {
|
||||
SC : function isSecureConnection() {
|
||||
return client.session.isSecure;
|
||||
},
|
||||
ML : function minutesLeft() {
|
||||
|
@ -81,28 +79,20 @@
|
|||
return !isNaN(value) && client.term.termWidth >= value;
|
||||
},
|
||||
ID : function isUserId(value) {
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
return _.findIndex(value, function cmp(uid) {
|
||||
return user.userId === parseInt(uid, 10);
|
||||
}) > -1;
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(user.userId) > -1;
|
||||
},
|
||||
WD : function isOneOfDayOfWeek() {
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
var nowDayOfWeek = new Date().getDay();
|
||||
|
||||
return _.findIndex(value, function cmp(dow) {
|
||||
return nowDayOfWeek === parseInt(dow, 10);
|
||||
}) > -1;
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(new Date().getDay()) > -1;
|
||||
},
|
||||
MM : function isMinutesPastMidnight() {
|
||||
// :TODO: return true if value is >= minutes past midnight sys time
|
||||
|
|
|
@ -177,12 +177,6 @@ function AbracadabraModule(options) {
|
|||
|
||||
require('util').inherits(AbracadabraModule, MenuModule);
|
||||
|
||||
/*
|
||||
AbracadabraModule.prototype.enter = function(client) {
|
||||
AbracadabraModule.super_.prototype.enter.call(this, client);
|
||||
};
|
||||
*/
|
||||
|
||||
AbracadabraModule.prototype.leave = function() {
|
||||
AbracadabraModule.super_.prototype.leave.call(this);
|
||||
|
||||
|
|
|
@ -393,7 +393,7 @@
|
|||
},
|
||||
editorMode: edit
|
||||
editorType: email
|
||||
messageAreaName: private_mail
|
||||
messageAreaTag: private_mail
|
||||
toUserId: 1 /* always to +op */
|
||||
}
|
||||
form: {
|
||||
|
@ -806,7 +806,7 @@
|
|||
},
|
||||
editorMode: edit
|
||||
editorType: email
|
||||
messageAreaName: private_mail
|
||||
messageAreaTag: private_mail
|
||||
toUserId: 1 /* always to +op */
|
||||
}
|
||||
form: {
|
||||
|
@ -1019,6 +1019,10 @@
|
|||
value: { command: "P" }
|
||||
action: @menu:messageAreaNewPost
|
||||
}
|
||||
{
|
||||
value: { command: "J" }
|
||||
action: @menu:messageAreaChangeCurrentConference
|
||||
}
|
||||
{
|
||||
value: { command: "C" }
|
||||
action: @menu:messageAreaChangeCurrentArea
|
||||
|
@ -1041,7 +1045,39 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
messageAreaChangeCurrentConference: {
|
||||
art: CCHANGE
|
||||
module: msg_conf_list
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
focus: true
|
||||
submit: true
|
||||
argName: conf
|
||||
}
|
||||
}
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { conf: null }
|
||||
action: @method:changeConference
|
||||
}
|
||||
]
|
||||
}
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape", "q", "shift + q" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaChangeCurrentArea: {
|
||||
// :TODO: rename this art to ACHANGE
|
||||
art: CHANGE
|
||||
module: msg_area_list
|
||||
form: {
|
||||
|
@ -1070,6 +1106,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaMessageList: {
|
||||
module: msg_list
|
||||
art: MSGLIST
|
||||
|
|
|
@ -5,7 +5,6 @@ var MenuModule = require('../core/menu_module.js').MenuModule;
|
|||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
var messageArea = require('../core/message_area.js');
|
||||
var strUtil = require('../core/string_util.js');
|
||||
//var msgDb = require('./database.js').dbs.message;
|
||||
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
|
@ -43,14 +42,17 @@ function MessageAreaListModule(options) {
|
|||
|
||||
var self = this;
|
||||
|
||||
this.messageAreas = messageArea.getAvailableMessageAreas();
|
||||
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
|
||||
self.client.user.properties.message_conf_tag,
|
||||
{ client : self.client }
|
||||
);
|
||||
|
||||
this.menuMethods = {
|
||||
changeArea : function(formData, extraArgs) {
|
||||
if(1 === formData.submitId) {
|
||||
var areaName = self.messageAreas[formData.value.area].name;
|
||||
const areaTag = self.messageAreas[formData.value.area].areaTag;
|
||||
|
||||
messageArea.changeMessageArea(self.client, areaName, function areaChanged(err) {
|
||||
messageArea.changeMessageArea(self.client, areaTag, function areaChanged(err) {
|
||||
if(err) {
|
||||
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
|
||||
|
||||
|
@ -66,7 +68,7 @@ function MessageAreaListModule(options) {
|
|||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
var v = self.viewControllers.areaList.getView(id);
|
||||
const v = self.viewControllers.areaList.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
|
@ -78,7 +80,7 @@ require('util').inherits(MessageAreaListModule, MenuModule);
|
|||
|
||||
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
||||
var self = this;
|
||||
var vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -99,26 +101,29 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
|||
});
|
||||
},
|
||||
function populateAreaListView(callback) {
|
||||
var listFormat = self.menuConfig.config.listFormat || '{index} ) - {desc}';
|
||||
var focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
var areaListItems = [];
|
||||
var focusListItems = [];
|
||||
const areaListView = vc.getView(1);
|
||||
let i = 1;
|
||||
areaListView.setItems(_.map(self.messageAreas, v => {
|
||||
return listFormat.format({
|
||||
index : i++,
|
||||
areaTag : v.area.areaTag,
|
||||
name : v.area.name,
|
||||
desc : v.area.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
// :TODO: use _.map() here
|
||||
for(var i = 0; i < self.messageAreas.length; ++i) {
|
||||
areaListItems.push(listFormat.format(
|
||||
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
|
||||
);
|
||||
focusListItems.push(focusListFormat.format(
|
||||
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
|
||||
);
|
||||
}
|
||||
|
||||
var areaListView = vc.getView(1);
|
||||
|
||||
areaListView.setItems(areaListItems);
|
||||
areaListView.setFocusItems(focusListItems);
|
||||
i = 1;
|
||||
areaListView.setFocusItems(_.map(self.messageAreas, v => {
|
||||
return focusListFormat.format({
|
||||
index : i++,
|
||||
areaTag : v.area.areaTag,
|
||||
name : v.area.name,
|
||||
desc : v.area.desc,
|
||||
})
|
||||
}));
|
||||
|
||||
areaListView.redraw();
|
||||
|
||||
|
|
|
@ -56,11 +56,11 @@ function AreaPostFSEModule(options) {
|
|||
|
||||
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
|
||||
|
||||
AreaPostFSEModule.prototype.enter = function(client) {
|
||||
AreaPostFSEModule.prototype.enter = function() {
|
||||
|
||||
if(_.isString(client.user.properties.message_area_name) && !_.isString(this.messageAreaName)) {
|
||||
this.messageAreaName = client.user.properties.message_area_name;
|
||||
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
|
||||
AreaPostFSEModule.super_.prototype.enter.call(this, client);
|
||||
AreaPostFSEModule.super_.prototype.enter.call(this);
|
||||
};
|
||||
|
|
|
@ -72,7 +72,7 @@ function AreaViewFSEModule(options) {
|
|||
if(_.isString(extraArgs.menu)) {
|
||||
var modOpts = {
|
||||
extraArgs : {
|
||||
messageAreaName : self.messageAreaName,
|
||||
messageAreaTag : self.messageAreaTag,
|
||||
replyToMessage : self.message,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
};
|
|
@ -52,15 +52,15 @@ function MessageListModule(options) {
|
|||
var self = this;
|
||||
var config = this.menuConfig.config;
|
||||
|
||||
this.messageAreaName = config.messageAreaName;
|
||||
this.messageAreaTag = config.messageAreaTag;
|
||||
|
||||
if(options.extraArgs) {
|
||||
//
|
||||
// |extraArgs| can override |messageAreaName| provided by config
|
||||
// |extraArgs| can override |messageAreaTag| provided by config
|
||||
// as well as supply a pre-defined message list
|
||||
//
|
||||
if(options.extraArgs.messageAreaName) {
|
||||
this.messageAreaName = options.extraArgs.messageAreaName;
|
||||
if(options.extraArgs.messageAreaTag) {
|
||||
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||
}
|
||||
|
||||
if(options.extraArgs.messageList) {
|
||||
|
@ -73,7 +73,7 @@ function MessageListModule(options) {
|
|||
if(1 === formData.submitId) {
|
||||
var modOpts = {
|
||||
extraArgs : {
|
||||
messageAreaName : self.messageAreaName,
|
||||
messageAreaTag : self.messageAreaTag,
|
||||
messageList : self.messageList,
|
||||
messageIndex : formData.value.message,
|
||||
}
|
||||
|
@ -94,15 +94,15 @@ function MessageListModule(options) {
|
|||
|
||||
require('util').inherits(MessageListModule, MenuModule);
|
||||
|
||||
MessageListModule.prototype.enter = function(client) {
|
||||
MessageListModule.super_.prototype.enter.call(this, client);
|
||||
MessageListModule.prototype.enter = function() {
|
||||
MessageListModule.super_.prototype.enter.call(this);
|
||||
|
||||
//
|
||||
// Config can specify |messageAreaName| else it comes from
|
||||
// Config can specify |messageAreaTag| else it comes from
|
||||
// the user's current area
|
||||
//
|
||||
if(!this.messageAreaName) {
|
||||
this.messageAreaName = client.user.properties.message_area_name;
|
||||
if(!this.messageAreaTag) {
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -110,6 +110,8 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
var self = this;
|
||||
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
var firstNewEntryIndex;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
|
@ -130,7 +132,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
if(_.isArray(self.messageList)) {
|
||||
callback(0 === self.messageList.length ? new Error('No messages in area') : null);
|
||||
} else {
|
||||
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaName, function msgs(err, msgList) {
|
||||
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) {
|
||||
if(msgList && 0 === msgList.length) {
|
||||
callback(new Error('No messages in area'));
|
||||
} else {
|
||||
|
@ -141,7 +143,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
}
|
||||
},
|
||||
function getLastReadMesageId(callback) {
|
||||
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaName, function lastRead(err, lastReadId) {
|
||||
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) {
|
||||
self.lastReadId = lastReadId || 0;
|
||||
callback(null); // ignore any errors, e.g. missing value
|
||||
});
|
||||
|
@ -158,6 +160,13 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
var msgNum = 1;
|
||||
|
||||
function getMsgFmtObj(mle) {
|
||||
|
||||
if(_.isUndefined(firstNewEntryIndex) &&
|
||||
mle.messageId > self.lastReadId)
|
||||
{
|
||||
firstNewEntryIndex = msgNum - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
msgNum : msgNum++,
|
||||
subj : mle.subject,
|
||||
|
@ -183,11 +192,15 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
|
||||
msgListView.redraw();
|
||||
|
||||
if(firstNewEntryIndex > 0) {
|
||||
msgListView.setFocusItemIndex(firstNewEntryIndex);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
},
|
||||
function populateOtherMciViews(callback) {
|
||||
|
||||
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByName(self.messageAreaName).desc);
|
||||
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByTag(self.messageAreaTag).name);
|
||||
self.setViewText(MciCodesIds.MsgSelNum, (vc.getView(MciCodesIds.MsgList).getData() + 1).toString());
|
||||
self.setViewText(MciCodesIds.MsgTotal, self.messageList.length.toString());
|
||||
|
||||
|
|
20
mods/nua.js
20
mods/nua.js
|
@ -5,7 +5,7 @@ var user = require('../core/user.js');
|
|||
var theme = require('../core/theme.js');
|
||||
var login = require('../core/system_menu_method.js').login;
|
||||
var Config = require('../core/config.js').config;
|
||||
var getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea;
|
||||
var messageArea = require('../core/message_area.js');
|
||||
|
||||
var async = require('async');
|
||||
|
||||
|
@ -65,6 +65,16 @@ function NewUserAppModule(options) {
|
|||
|
||||
newUser.username = formData.value.username;
|
||||
|
||||
//
|
||||
// We have to disable ACS checks for initial default areas as the user is not yet ready
|
||||
//
|
||||
var confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
|
||||
var areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
|
||||
|
||||
// can't store undefined!
|
||||
confTag = confTag || '';
|
||||
areaTag = areaTag || '';
|
||||
|
||||
newUser.properties = {
|
||||
real_name : formData.value.realName,
|
||||
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
|
||||
|
@ -75,14 +85,12 @@ function NewUserAppModule(options) {
|
|||
web_address : formData.value.web,
|
||||
account_created : new Date().toISOString(),
|
||||
|
||||
message_area_name : getDefaultMessageArea().name,
|
||||
message_conf_tag : confTag,
|
||||
message_area_tag : areaTag,
|
||||
|
||||
term_height : self.client.term.termHeight,
|
||||
term_width : self.client.term.termWidth,
|
||||
|
||||
// :TODO: This is set in User.create() -- proabbly don't need it here:
|
||||
//account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active,
|
||||
|
||||
// :TODO: Other defaults
|
||||
// :TODO: should probably have a place to create defaults/etc.
|
||||
};
|
||||
|
@ -93,7 +101,7 @@ function NewUserAppModule(options) {
|
|||
newUser.properties.theme_id = Config.defaults.theme;
|
||||
}
|
||||
|
||||
// :TODO: .create() should also validate email uniqueness!
|
||||
// :TODO: User.create() should validate email uniqueness!
|
||||
newUser.create( { password : formData.value.password }, function created(err) {
|
||||
if(err) {
|
||||
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
|
||||
|
|
|
@ -172,8 +172,8 @@
|
|||
|
||||
messageAreaChangeCurrentArea: {
|
||||
config: {
|
||||
listFormat: "|00|15{index} |07- |03{desc}"
|
||||
focusListFormat: "|00|19|15{index} - {desc}"
|
||||
listFormat: "|00|15{index} |07- |03{name}"
|
||||
focusListFormat: "|00|19|15{index} - {name}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
|
@ -310,6 +310,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
newScanMessageList: {
|
||||
config: {
|
||||
listFormat: "|00|15 {msgNum:<5.5}|03{subj:<29.29} |15{from:<20.20} {ts}"
|
||||
focusListFormat: "|00|19> |15{msgNum:<5.5}{subj:<29.29} {from:<20.20} {ts}"
|
||||
dateTimeFormat: ddd MMM Do
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
|
||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
var userDb = require('../core/database.js').dbs.user;
|
||||
//var userDb = require('../core/database.js').dbs.user;
|
||||
var getUserList = require('../core/user.js').getUserList;
|
||||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
|
||||
|
|
|
@ -84,7 +84,6 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
|
|||
return listFormat.format(oe);
|
||||
}));
|
||||
|
||||
// :TODO: This is a hack until pipe codes are better implemented
|
||||
onlineListView.focusItems = onlineListView.items;
|
||||
|
||||
onlineListView.redraw();
|
||||
|
|
14
oputil.js
14
oputil.js
|
@ -13,7 +13,7 @@ var assert = require('assert');
|
|||
|
||||
var argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
var ExitCodes = {
|
||||
const ExitCodes = {
|
||||
SUCCESS : 0,
|
||||
ERROR : -1,
|
||||
BAD_COMMAND : -2,
|
||||
|
@ -28,9 +28,13 @@ function printUsage(command) {
|
|||
usage =
|
||||
'usage: oputil.js [--version] [--help]\n' +
|
||||
' <command> [<args>]' +
|
||||
'\n' +
|
||||
'\n\n' +
|
||||
'global args:\n' +
|
||||
' --config PATH : specify config path';
|
||||
' --config PATH : specify config path' +
|
||||
'\n\n' +
|
||||
'commands:\n' +
|
||||
' user : User utilities' +
|
||||
'\n';
|
||||
break;
|
||||
|
||||
case 'user' :
|
||||
|
@ -47,7 +51,7 @@ function printUsage(command) {
|
|||
}
|
||||
|
||||
function initConfig(cb) {
|
||||
var configPath = argv.config ? argv.config : config.getDefaultPath();
|
||||
const configPath = argv.config ? argv.config : config.getDefaultPath();
|
||||
|
||||
config.init(configPath, cb);
|
||||
}
|
||||
|
@ -88,7 +92,7 @@ function handleUserCommand() {
|
|||
assert(_.isNumber(userId));
|
||||
assert(userId > 0);
|
||||
|
||||
var u = new user.User();
|
||||
let u = new user.User();
|
||||
u.userId = userId;
|
||||
|
||||
u.setNewAuthCredentials(argv.password, function credsSet(err) {
|
||||
|
|
Loading…
Reference in New Issue