Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs

This commit is contained in:
Bryan Ashby 2016-03-25 22:06:05 -06:00
commit 6c108cb6b8
107 changed files with 4800 additions and 1242 deletions

26
.eslintrc.json Normal file
View File

@ -0,0 +1,26 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"comma-dangle": 0
}
}

16
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,16 @@
For :bug: bug reports, please fill out the information below plus any additional relevant information. If this is a feature request, feel free to clear the form.
**Short problem description**
**Environment**
- [ ] I am using Node.js v4.x or higher
- [ ] `npm install` reports success
- Actual Node.js version (`node --version`):
- Operating system (`uname -a` on *nix systems):
- Revision (`git rev-parse --short HEAD`):
**Expected behavior**
**Actual behavior**
**Steps to reproduce**

View File

@ -19,12 +19,12 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
* Door support including common dropfile formats and legacy DOS doors (See [Doors](docs/doors.md))
* [Bunyan](https://github.com/trentm/node-bunyan) logging
* FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export
## In the Works
* Lots of code cleanup, ES6+ usage, and **documentation**!
* FTN import & export
* More ES6+ usage, and **documentation**!
* File areas
* Full access checking framework (ACS)
* ACS support for more areas
* SysOp dashboard (ye ol' WFC)
* Missing functionality such as searching, pipe code support in message areas, etc.
* String localization
@ -65,7 +65,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
## License
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
Copyright (c) 2015, Bryan D. Ashby
Copyright (c) 2015-2016, Bryan D. Ashby
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

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

View File

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

151
core/archive_util.js Normal file
View File

@ -0,0 +1,151 @@
/* jslint node: true */
'use strict';
// ENiGMA½
let Config = require('./config.js').config;
// base/modules
let fs = require('fs');
let _ = require('lodash');
let pty = require('ptyw.js');
module.exports = class ArchiveUtil {
constructor() {
this.archivers = {};
this.longestSignature = 0;
}
init() {
//
// Load configuration
//
if(_.has(Config, 'archivers')) {
Object.keys(Config.archivers).forEach(archKey => {
const arch = Config.archivers[archKey];
if(!_.isString(arch.sig) ||
!_.isString(arch.compressCmd) ||
!_.isString(arch.decompressCmd) ||
!_.isArray(arch.compressArgs) ||
!_.isArray(arch.decompressArgs))
{
// :TODO: log warning
return;
}
const archiver = {
compressCmd : arch.compressCmd,
compressArgs : arch.compressArgs,
decompressCmd : arch.decompressCmd,
decompressArgs : arch.decompressArgs,
sig : new Buffer(arch.sig, 'hex'),
offset : arch.offset || 0,
};
this.archivers[archKey] = archiver;
if(archiver.offset + archiver.sig.length > this.longestSignature) {
this.longestSignature = archiver.offset + archiver.sig.length;
}
});
}
}
getArchiver(archType) {
if(!archType) {
return;
}
archType = archType.toLowerCase();
return this.archivers[archType];
}
haveArchiver(archType) {
return this.getArchiver(archType) ? true : false;
}
detectType(path, cb) {
fs.open(path, 'r', (err, fd) => {
if(err) {
cb(err);
return;
}
let buf = new Buffer(this.longestSignature);
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
if(err) {
cb(err);
return;
}
// return first match
const detected = _.findKey(this.archivers, arch => {
const lenNeeded = arch.offset + arch.sig.length;
if(buf.length < lenNeeded) {
return false;
}
const comp = buf.slice(arch.offset, arch.offset + arch.sig.length);
return (arch.sig.equals(comp));
});
cb(detected ? null : new Error('Unknown type'), detected);
});
});
}
compressTo(archType, archivePath, files, cb) {
const archiver = this.getArchiver(archType);
if(!archiver) {
return cb(new Error(`Unknown archive type: ${archType}`));
}
let args = _.clone(archiver.compressArgs); // don't muck with orig
for(let i = 0; i < args.length; ++i) {
args[i] = args[i].format({
archivePath : archivePath,
fileList : files.join(' '),
});
}
let comp = pty.spawn(archiver.compressCmd, args, this.getPtyOpts());
comp.once('exit', exitCode => {
cb(exitCode ? new Error(`Compression failed with exit code: ${exitCode}`) : null);
});
}
extractTo(archivePath, extractPath, archType, cb) {
const archiver = this.getArchiver(archType);
if(!archiver) {
return cb(new Error(`Unknown archive type: ${archType}`));
}
let args = _.clone(archiver.decompressArgs); // don't muck with orig
for(let i = 0; i < args.length; ++i) {
args[i] = args[i].format({
archivePath : archivePath,
extractPath : extractPath,
});
}
let comp = pty.spawn(archiver.decompressCmd, args, this.getPtyOpts());
comp.once('exit', exitCode => {
cb(exitCode ? new Error(`Decompression failed with exit code: ${exitCode}`) : null);
});
}
getPtyOpts() {
return {
// :TODO: cwd
name : 'enigma-archiver',
cols : 80,
rows : 24,
env : process.env,
};
}
}

View File

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

View File

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

View File

@ -1,44 +1,47 @@
/* 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');
var miscUtil = require('./misc_util.js');
var database = require('./database.js');
var clientConns = require('./client_connections.js');
let conf = require('./config.js');
let logger = require('./logger.js');
let miscUtil = require('./misc_util.js');
let database = require('./database.js');
let clientConns = require('./client_connections.js');
var paths = require('path');
var async = require('async');
var util = require('util');
var _ = require('lodash');
var assert = require('assert');
var mkdirp = require('mkdirp');
let paths = require('path');
let async = require('async');
let util = require('util');
let _ = require('lodash');
let assert = require('assert');
let mkdirp = require('mkdirp');
exports.bbsMain = bbsMain;
// our main entry point
exports.bbsMain = bbsMain;
function bbsMain() {
async.waterfall(
[
function processArgs(callback) {
var args = parseArgs();
const args = process.argv.slice(2);
var configPath;
if(args.indexOf('--help') > 0) {
// :TODO: display help
} else {
var argCount = args.length;
for(var i = 0; i < argCount; ++i) {
var arg = args[i];
if('--config' == arg) {
let argCount = args.length;
for(let i = 0; i < argCount; ++i) {
const arg = args[i];
if('--config' === arg) {
configPath = args[i + 1];
}
}
}
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) {
@ -68,25 +71,19 @@ function bbsMain() {
}
callback(err);
});
},
function listenConnections(callback) {
startListening(callback);
}
],
function complete(err) {
if(!err) {
startListening();
if(err) {
logger.log.error(err);
}
}
);
}
function parseArgs() {
var args = [];
process.argv.slice(2).forEach(function(val, index, array) {
args.push(val);
});
return args;
}
function initialize(cb) {
async.series(
[
@ -169,6 +166,9 @@ function initialize(cb) {
});
}
});
},
function readyMessageNetworkSupport(callback) {
require('./msg_network.js').startup(callback);
}
],
function onComplete(err) {
@ -177,29 +177,36 @@ function initialize(cb) {
);
}
function startListening() {
function startListening(cb) {
if(!conf.config.servers) {
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
logger.log.error('No servers configured');
return [];
//logger.log.error('No servers configured');
cb(new Error('No servers configured'));
return;
}
var moduleUtil = require('./module_util.js'); // late load so we get Config
let moduleUtil = require('./module_util.js'); // late load so we get Config
moduleUtil.loadModulesForCategory('servers', function onServerModule(err, module) {
moduleUtil.loadModulesForCategory('servers', (err, module) => {
if(err) {
logger.log.info(err);
return;
}
var port = parseInt(module.runtime.config.port);
const port = parseInt(module.runtime.config.port);
if(isNaN(port)) {
logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
return;
}
var moduleInst = new module.getModule();
var server = moduleInst.createServer();
const moduleInst = new module.getModule();
let server;
try {
server = moduleInst.createServer();
} catch(e) {
logger.log.warn(e, 'Exception caught creating server!');
return;
}
// :TODO: handle maxConnections, e.g. conf.maxConnections
@ -260,7 +267,11 @@ function startListening() {
});
server.listen(port);
logger.log.info({ server : module.moduleInfo.name, port : port }, 'Listening for connections');
logger.log.info(
{ server : module.moduleInfo.name, port : port }, 'Listening for connections');
}, err => {
cb(err);
});
}

View File

@ -8,10 +8,37 @@ var paths = require('path');
var async = require('async');
var _ = require('lodash');
var hjson = require('hjson');
var assert = require('assert');
exports.init = init;
exports.getDefaultPath = getDefaultPath;
function hasMessageConferenceAndArea(config) {
assert(_.isObject(config.messageConferences)); // we create one ourself!
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
return 'system_internal' !== confTag;
});
if(0 === nonInternalConfs.length) {
return false;
}
// :TODO: there is likely a better/cleaner way of doing this
var result = false;
_.forEach(nonInternalConfs, confTag => {
if(_.has(config.messageConferences[confTag], 'areas') &&
Object.keys(config.messageConferences[confTag].areas) > 0)
{
result = true;
return false; // stop iteration
}
});
return result;
}
function init(configPath, cb) {
async.waterfall(
[
@ -48,18 +75,13 @@ function init(configPath, cb) {
//
// Various sections must now exist in config
//
if(!_.has(mergedConfig, 'messages.areas.') ||
!_.isArray(mergedConfig.messages.areas) ||
0 === mergedConfig.messages.areas.length ||
!_.isString(mergedConfig.messages.areas[0].name))
{
var msgAreasErr = new Error('Please create at least one message area');
if(hasMessageConferenceAndArea(mergedConfig)) {
var msgAreasErr = new Error('Please create at least one message conference and area!');
msgAreasErr.code = 'EBADCONFIG';
callback(msgAreasErr);
return;
}
callback(null, mergedConfig);
} else {
callback(null, mergedConfig);
}
}
],
function complete(err, mergedConfig) {
@ -150,6 +172,10 @@ function getDefaultConfig() {
paths : {
mods : paths.join(__dirname, './../mods/'),
servers : paths.join(__dirname, './servers/'),
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
mailers : paths.join(__dirname, './mailers/') ,
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
@ -166,7 +192,7 @@ function getDefaultConfig() {
},
ssh : {
port : 8889,
enabled : true,
enabled : false, // defualt to false as PK/pass in config.hjson are required
//
// Private key in PEM format
@ -183,24 +209,52 @@ function getDefaultConfig() {
}
},
messages : {
areas : [
{ name : 'private_mail', desc : 'Private Email', groups : [ 'users' ] }
]
archivers : {
zip : {
sig : "504b0304",
offset : 0,
compressCmd : "7z",
compressArgs : [ "a", "-tzip", "{archivePath}", "{fileList}" ],
decompressCmd : "7z",
decompressArgs : [ "e", "-o{extractPath}", "{archivePath}" ]
}
},
networks : {
/*
networkName : { // e.g. fidoNet
address : {
zone : 0,
net : 0,
node : 0,
point : 0,
domain : 'l33t.codes'
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',
}
}
}
*/
},
scannerTossers : {
ftn_bso : {
paths : {
outbound : paths.join(__dirname, './../mail/ftn_out/'),
inbound : paths.join(__dirname, './../mail/ftn_in/'),
secInbound : paths.join(__dirname, './../mail/ftn_secin/'),
},
//
// Packet and (ArcMail) bundle target sizes are just that: targets.
// Actual sizes may be slightly larger when we must place a full
// PKT contents *somewhere*
//
packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt
bundleTargetByteSize : 2048000, // 2M, before creating another archive
}
},
misc : {

View File

@ -132,7 +132,7 @@ function createMessageBaseTables() {
dbs.message.run(
'CREATE TABLE IF NOT EXISTS message (' +
' message_id INTEGER PRIMARY KEY,' +
' area_name VARCHAR NOT NULL,' +
' area_tag VARCHAR NOT NULL,' +
' message_uuid VARCHAR(36) NOT NULL,' +
' reply_to_message_id INTEGER,' +
' to_user_name VARCHAR NOT NULL,' +
@ -175,7 +175,7 @@ function createMessageBaseTables() {
' meta_category INTEGER NOT NULL,' +
' meta_name VARCHAR NOT NULL,' +
' meta_value VARCHAR NOT NULL,' +
' UNIQUE(message_id, meta_category, meta_name, meta_value),' +
' UNIQUE(message_id, meta_category, meta_name, meta_value),' + // why unique here?
' FOREIGN KEY(message_id) REFERENCES message(message_id)' +
');'
);
@ -198,20 +198,19 @@ 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)' +
');'
);
dbs.message.run(
'CREATE TABLE IF NOT EXISTS user_message_status (' +
' user_id INTEGER NOT NULL,' +
' message_id INTEGER NOT NULL,' +
' status INTEGER NOT NULL,' +
' UNIQUE(user_id, message_id, status),' +
' FOREIGN KEY(user_id) REFERENCES user(id)' +
');'
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
scan_toss VARCHAR NOT NULL,
area_tag VARCHAR NOT NULL,
message_id INTEGER NOT NULL,
UNIQUE(scan_toss, area_tag)
);`
);
}

50
core/fnv1a.js Normal file
View File

@ -0,0 +1,50 @@
/* jslint node: true */
'use strict';
let _ = require('lodash');
// FNV-1a based on work here: https://github.com/wiedi/node-fnv
module.exports = class FNV1a {
constructor(data) {
this.hash = 0x811c9dc5;
if(!_.isUndefined(data)) {
this.update(data);
}
}
update(data) {
if(_.isNumber(data)) {
data = data.toString();
}
if(_.isString(data)) {
data = new Buffer(data);
}
if(!Buffer.isBuffer(data)) {
throw new Error('data must be String or Buffer!');
}
for(let b of data) {
this.hash = this.hash ^ b;
this.hash +=
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) +
(this.hash << 4) + (this.hash << 1);
}
return this;
}
digest(encoding) {
encoding = encoding || 'binary';
let buf = new Buffer(4);
buf.writeInt32BE(this.hash & 0xffffffff, 0);
return buf.toString(encoding);
}
get value() {
return this.hash & 0xffffffff;
}
}

View File

@ -7,7 +7,7 @@ var ansi = require('../core/ansi_term.js');
var theme = require('../core/theme.js');
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
var Message = require('../core/message.js');
var getMessageAreaByName = require('../core/message_area.js').getMessageAreaByName;
var getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
@ -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) {
@ -308,7 +307,7 @@ function FullScreenEditorModule(options) {
// :TODO: We'd like to delete up to N rows, but this does not work
// in NetRunner:
//self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3));
self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3));
self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2))
}
@ -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);
};

198
core/ftn_address.js Normal file
View File

@ -0,0 +1,198 @@
/* jslint node: true */
'use strict';
let _ = require('lodash');
const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i;
const FTN_PATTERN_REGEXP = /^([0-9\*]+:)?([0-9\*]+)(\/[0-9\*]+)?(\.[0-9\*]+)?(@[a-z0-9\-\.\*]+)?$/i;
module.exports = class Address {
constructor(addr) {
if(addr) {
if(_.isObject(addr)) {
Object.assign(this, addr);
} else if(_.isString(addr)) {
const temp = Address.fromString(addr);
if(temp) {
Object.assign(this, temp);
}
}
}
}
isEqual(other) {
if(_.isString(other)) {
other = Address.fromString(other);
}
return (
this.net === other.net &&
this.node === other.node &&
this.zone === other.zone &&
this.point === other.point &&
this.domain === other.domain
);
}
getMatchAddr(pattern) {
const m = FTN_PATTERN_REGEXP.exec(pattern);
if(m) {
let addr = { };
if(m[1]) {
addr.zone = m[1].slice(0, -1)
if('*' !== addr.zone) {
addr.zone = parseInt(addr.zone);
}
} else {
addr.zone = '*';
}
if(m[2]) {
addr.net = m[2];
if('*' !== addr.net) {
addr.net = parseInt(addr.net);
}
} else {
addr.net = '*';
}
if(m[3]) {
addr.node = m[3].substr(1);
if('*' !== addr.node) {
addr.node = parseInt(addr.node);
}
} else {
addr.node = '*';
}
if(m[4]) {
addr.point = m[4].substr(1);
if('*' !== addr.point) {
addr.point = parseInt(addr.point);
}
} else {
addr.point = '*';
}
if(m[5]) {
addr.domain = m[5].substr(1);
} else {
addr.domain = '*';
}
return addr;
}
}
/*
getMatchScore(pattern) {
let score = 0;
const addr = this.getMatchAddr(pattern);
if(addr) {
const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ];
for(let i = 0; i < PARTS.length; ++i) {
const member = PARTS[i];
if(this[member] === addr[member]) {
score += 2;
} else if('*' === addr[member]) {
score += 1;
} else {
break;
}
}
}
return score;
}
*/
isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern);
if(addr) {
return (
('*' === addr.net || this.net === addr.net) &&
('*' === addr.node || this.node === addr.node) &&
('*' === addr.zone || this.zone === addr.zone) &&
('*' === addr.point || this.point === addr.point) &&
('*' === addr.domain || this.domain === addr.domain)
);
}
return false;
}
static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
if(m) {
// start with a 2D
let addr = {
net : parseInt(m[2]),
node : parseInt(m[3].substr(1)),
};
// 3D: Addition of zone if present
if(m[1]) {
addr.zone = parseInt(m[1].slice(0, -1));
}
// 4D if optional point is present
if(m[4]) {
addr.point = parseInt(m[4].substr(1));
}
// 5D with @domain
if(m[5]) {
addr.domain = m[5].substr(1);
}
return new Address(addr);
}
}
toString(dimensions) {
dimensions = dimensions || '5D';
let addrStr = `${this.zone}:${this.net}`;
// allow for e.g. '4D' or 5
const dim = parseInt(dimensions.toString()[0]);
if(dim >= 3) {
addrStr += `/${this.node}`;
}
// missing & .0 are equiv for point
if(dim >= 4 && this.point) {
addrStr += `.${this.point}`;
}
if(5 === dim && this.domain) {
addrStr += `@${this.domain.toLowerCase()}`;
}
return addrStr;
}
static getComparator() {
return function(left, right) {
let c = (left.zone || 0) - (right.zone || 0);
if(0 !== c) {
return c;
}
c = (left.net || 0) - (right.net || 0);
if(0 !== c) {
return c;
}
c = (left.node || 0) - (right.node || 0);
if(0 !== c) {
return c;
}
return (left.domain || '').localeCompare(right.domain || '');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,65 @@
/* jslint node: true */
'use strict';
var Config = require('./config.js').config;
let Config = require('./config.js').config;
let Address = require('./ftn_address.js');
let FNV1a = require('./fnv1a.js');
let createNamedUUID = require('./uuid_util.js').createNamedUUID;
let _ = require('lodash');
let assert = require('assert');
let iconv = require('iconv-lite');
let moment = require('moment');
let uuid = require('node-uuid');
let os = require('os');
var _ = require('lodash');
var assert = require('assert');
var binary = require('binary');
var fs = require('fs');
var util = require('util');
var iconv = require('iconv-lite');
let packageJson = require('../package.json');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringFromFTN = stringFromFTN;
exports.getFormattedFTNAddress = getFormattedFTNAddress;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getMessageSerialNumber = getMessageSerialNumber;
exports.createMessageUuid = createMessageUuid;
exports.createMessageUuidAlternate = createMessageUuidAlternate;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString;
exports.getQuotePrefix = getQuotePrefix;
exports.getMessageIdentifier = getMessageIdentifier;
exports.getProductIdentifier = getProductIdentifier;
exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
exports.getOrigin = getOrigin;
exports.getTearLine = getTearLine;
exports.getVia = getVia;
exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList;
exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList;
exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries;
exports.getUpdatedPathEntries = getUpdatedPathEntries;
exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding;
exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier;
exports.getQuotePrefix = getQuotePrefix;
//
// Namespace for RFC-4122 name based UUIDs generated from
// FTN kludges MSGID + AREA
//
const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654');
// See list here: https://github.com/Mithgol/node-fidonet-jam
// :TODO: proably move this elsewhere as a general method
function stringFromFTN(buf, encoding) {
var nullPos = buf.length;
for(var i = 0; i < buf.length; ++i) {
if(0x00 === buf[i]) {
nullPos = i;
break;
}
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 iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
return buffer;
}
//
// Convert a FTN style DateTime string to a Date object
//
// :TODO: Name the next couple methods better - for FTN *packets*
function getDateFromFtnDateTime(dateTime) {
//
// Examples seen in the wild (Working):
@ -44,45 +67,146 @@ function getDateFromFtnDateTime(dateTime) {
// "Tue 01 Jan 80 00:00"
// "27 Feb 15 00:00:03"
//
return (new Date(Date.parse(dateTime))).toISOString();
// :TODO: Use moment.js here
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
// return (new Date(Date.parse(dateTime))).toISOString();
}
function getFormattedFTNAddress(address, dimensions) {
//var addr = util.format('%d:%d', address.zone, address.net);
var addr = '{0}:{1}'.format(address.zone, address.net);
switch(dimensions) {
case 2 :
case '2D' :
// above
break;
case 3 :
case '3D' :
addr += '/{0}'.format(address.node);
break;
case 4 :
case '4D':
addr += '.{0}'.format(address.point || 0); // missing and 0 are equiv for point
break;
case 5 :
case '5D' :
if(address.domain) {
addr += '@{0}'.format(address.domain);
}
break;
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 addr;
return m.format('DD MMM YY HH:mm:ss');
}
function getFtnMessageSerialNumber(messageId) {
return ((Math.floor((Date.now() - Date.UTC(2015, 1, 1)) / 1000) + messageId)).toString(16);
//
// Create a v5 named UUID given a message ID ("MSGID") and
// FTN area tag ("AREA").
//
// This is similar to CrashMail
// See https://github.com/larsks/crashmail/blob/master/crashmail/dupe.c
//
function createMessageUuid(ftnMsgId, ftnArea) {
assert(_.isString(ftnMsgId));
assert(_.isString(ftnArea));
ftnMsgId = iconv.encode(ftnMsgId, 'CP437');
ftnArea = iconv.encode(ftnArea.toUpperCase(), 'CP437');
return uuid.unparse(createNamedUUID(ENIGMA_FTN_MSGID_NAMESPACE, Buffer.concat( [ ftnMsgId, ftnArea ] )));
};
//
// Create a v5 named UUID given a FTN area tag ("AREA"),
// create/modified date, subject, and message body
//
// This method should be used as a backup for when a MSGID is
// not available in which createMessageUuid() above should be
// used instead.
//
function createMessageUuidAlternate(ftnArea, modTimestamp, subject, msgBody) {
assert(_.isString(ftnArea));
assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp));
assert(_.isString(subject));
assert(_.isString(msgBody));
ftnArea = iconv.encode(ftnArea.toUpperCase(), 'CP437');
modTimestamp = iconv.encode(getDateTimeString(modTimestamp), 'CP437');
subject = iconv.encode(subject.toUpperCase().trim(), 'CP437');
msgBody = iconv.encode(msgBody.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437');
return uuid.unparse(createNamedUUID(ENIGMA_FTN_MSGID_NAMESPACE, Buffer.concat( [ ftnArea, modTimestamp, subject, msgBody ] )));
}
function getFTNMessageID(messageId, areaId) {
return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getFTNMessageSerialNumber(messageId)
function getMessageSerialNumber(messageId) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
return `00000000${hash}`.substr(-8);
}
//
// Return a FTS-0009.001 compliant MSGID value given a message
// See http://ftsc.org/docs/fts-0009.001
//
// "A MSGID line consists of the string "^AMSGID:" (where ^A is a
// control-A (hex 01) and the double-quotes are not part of the
// string), followed by a space, the address of the originating
// system, and a serial number unique to that message on the
// originating system, i.e.:
//
// ^AMSGID: origaddr serialno
//
// The originating address should be specified in a form that
// constitutes a valid return address for the originating network.
// If the originating address is enclosed in double-quotes, the
// entire string between the beginning and ending double-quotes is
// considered to be the orginating address. A double-quote character
// within a quoted address is represented by by two consecutive
// double-quote characters. The serial number may be any eight
// character hexadecimal number, as long as it is unique - no two
// messages from a given system may have the same serial number
// within a three years. The manner in which this serial number is
// generated is left to the implementor."
//
//
// Examples & Implementations
//
// Synchronet: <msgNum>.<conf+area>@<ftnAddr> <serial>
// 2606.agora-agn_tst@46:1/142 19609217
//
// Mystic: <ftnAddress> <serial>
// 46:3/102 46686263
//
// ENiGMA½: <messageId>.<areaTag>@<5dFtnAddress> <serial>
//
function getMessageIdentifier(message, address) {
const addrStr = new Address(address).toString('5D');
return `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`;
}
//
// Return a FSC-0046.005 Product Identifier or "PID"
// http://ftsc.org/docs/fsc-0046.005
//
// Note that we use a variant on the spec for <serial>
// in which (<os>; <arch>; <nodeVer>) is used instead
//
function getProductIdentifier() {
const version = packageJson.version
.replace(/\-/g, '.')
.replace(/alpha/,'a')
.replace(/beta/,'b');
const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
}
//
// Return a FRL-1004 style time zone offset for a
// 'TZUTC' kludge line
//
// http://ftsc.org/docs/frl-1004.002
//
function getUTCTimeZoneOffset() {
return moment().format('ZZ').replace(/\+/, '');
}
// Get a FSC-0032 style quote prefixes
@ -91,25 +215,221 @@ function getQuotePrefix(name) {
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
}
//
// Return a FTS-0004 Origin line
// http://ftsc.org/docs/fts-0004.001
//
function getOrigin(address) {
const origin = _.has(Config.messageNetworks.originName) ?
Config.messageNetworks.originName :
Config.general.boardName;
const addrStr = new Address(address).toString('5D');
return ` * Origin: ${origin} (${addrStr})`;
}
function getTearLine() {
const nodeVer = process.version.substr(1); // remove 'v' prefix
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
}
//
// Specs:
// * http://ftsc.org/docs/fts-0009.001
// *
// Return a FRL-1005.001 "Via" line
// http://ftsc.org/docs/frl-1005.001
//
function getFtnMsgIdKludgeLine(origAddress, messageId) {
if(_.isObject(origAddress)) {
origAddress = getFormattedFTNAddress(origAddress, '5D');
function getVia(address) {
/*
FRL-1005.001 states teh following format:
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
<Program Name> <Version> [Serial Number]<CR>
*/
const addrStr = new Address(address).toString('5D');
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
const version = packageJson.version
.replace(/\-/g, '.')
.replace(/alpha/,'a')
.replace(/beta/,'b');
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
}
function getAbbreviatedNetNodeList(netNodes) {
let abbrList = '';
let currNet;
netNodes.forEach(netNode => {
if(_.isString(netNode)) {
netNode = Address.fromString(netNode);
}
if(currNet !== netNode.net) {
abbrList += `${netNode.net}/`;
currNet = netNode.net;
}
abbrList += `${netNode.node} `;
});
return abbrList.trim(); // remove trailing space
}
//
// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH
//
function parseAbbreviatedNetNodeList(netNodes) {
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
let net;
let m;
let results = [];
while(null !== (m = re.exec(netNodes))) {
if(m[1] && m[2]) {
net = parseInt(m[1]);
results.push(new Address( { net : net, node : parseInt(m[2]) } ));
} else if(net) {
results.push(new Address( { net : net, node : parseInt(m[3]) } ));
}
}
return '\x01MSGID: ' + origAddress + ' ' + getFtnMessageSerialNumber(messageId);
return results;
}
//
// Return a FTS-0004.001 SEEN-BY entry(s) that include
// all pre-existing SEEN-BY entries with the addition
// of |additions|.
//
// See http://ftsc.org/docs/fts-0004.001
// and notes at http://ftsc.org/docs/fsc-0043.002.
//
// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm
//
// This method returns an sorted array of values, but
// not the "SEEN-BY" prefix itself
//
function getUpdatedSeenByEntries(existingEntries, additions) {
/*
From FTS-0004:
"There can be many seen-by lines at the end of Conference
Mail messages, and they are the real "meat" of the control
information. They are used to determine the systems to
receive the exported messages. The format of the line is:
SEEN-BY: 132/101 113 136/601 1014/1
The net/node numbers correspond to the net/node numbers of
the systems having already received the message. In this way
a message is never sent to a system twice. In a conference
with many participants the number of seen-by lines can be
very large. This line is added if it is not already a part
of the message, or added to if it already exists, each time
a message is exported to other systems. This is a REQUIRED
field, and Conference Mail will not function correctly if
this field is not put in place by other Echomail compatible
programs."
*/
existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ];
}
if(!_.isString(additions)) {
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
}
additions = additions.sort(Address.getComparator());
function getFTNOriginLine() {
//
// Specs:
// http://ftsc.org/docs/fts-0004.001
// For now, we'll just append a new SEEN-BY entry
//
return ' * Origin: ' + Config.general.boardName + '(' + getFidoNetAddress() + ')';
// :TODO: we should at least try and update what is already there in a smart way
existingEntries.push(getAbbreviatedNetNodeList(additions));
return existingEntries;
}
function getUpdatedPathEntries(existingEntries, localAddress) {
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ];
}
existingEntries.push(getAbbreviatedNetNodeList(
parseAbbreviatedNetNodeList(localAddress)));
return existingEntries;
}
//
// Return FTS-5000.001 "CHRS" value
// http://ftsc.org/docs/fts-5003.001
//
const ENCODING_TO_FTS_5003_001_CHARS = {
// level 1 - generally should not be used
ascii : [ 'ASCII', 1 ],
'us-ascii' : [ 'ASCII', 1 ],
// level 2 - 8 bit, ASCII based
cp437 : [ 'CP437', 2 ],
cp850 : [ 'CP850', 2 ],
// level 3 - reserved
// level 4
utf8 : [ 'UTF-8', 4 ],
'utf-8' : [ 'UTF-8', 4 ],
};
function getCharacterSetIdentifierByEncoding(encodingName) {
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
}
function getEncodingFromCharacterSetIdentifier(chrs) {
const ident = chrs.split(' ')[0].toUpperCase();
// :TODO: fill in the rest!!!
return {
// level 1
'ASCII' : 'iso-646-1',
'DUTCH' : 'iso-646',
'FINNISH' : 'iso-646-10',
'FRENCH' : 'iso-646',
'CANADIAN' : 'iso-646',
'GERMAN' : 'iso-646',
'ITALIAN' : 'iso-646',
'NORWEIG' : 'iso-646',
'PORTU' : 'iso-646',
'SPANISH' : 'iso-656',
'SWEDISH' : 'iso-646-10',
'SWISS' : 'iso-646',
'UK' : 'iso-646',
'ISO-10' : 'iso-646-10',
// level 2
'CP437' : 'cp437',
'CP850' : 'cp850',
'CP852' : 'cp852',
'CP866' : 'cp866',
'CP848' : 'cp848',
'CP1250' : 'cp1250',
'CP1251' : 'cp1251',
'CP1252' : 'cp1252',
'CP10000' : 'macroman',
'LATIN-1' : 'iso-8859-1',
'LATIN-2' : 'iso-8859-2',
'LATIN-5' : 'iso-8859-9',
'LATIN-9' : 'iso-8859-15',
// level 4
'UTF-8' : 'utf8',
// deprecated stuff
'IBMPC' : 'cp1250', // :TODO: validate
'+7_FIDO' : 'cp866',
'+7' : 'cp866',
'MAC' : 'macroman', // :TODO: validate
}[ident];
}

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
/* jslint node: true */
'use strict';
var msgDb = require('./database.js').dbs.message;
var wordWrapText = require('./word_wrap.js').wordWrapText;
var ftnUtil = require('./ftn_util.js');
let msgDb = require('./database.js').dbs.message;
let wordWrapText = require('./word_wrap.js').wordWrapText;
let ftnUtil = require('./ftn_util.js');
var uuid = require('node-uuid');
var async = require('async');
var _ = require('lodash');
var assert = require('assert');
let uuid = require('node-uuid');
let async = require('async');
let _ = require('lodash');
let assert = require('assert');
let moment = require('moment');
module.exports = Message;
@ -16,18 +17,18 @@ function Message(options) {
options = options || {};
this.messageId = options.messageId || 0; // always generated @ persist
this.areaName = options.areaName || Message.WellKnownAreaNames.Invalid;
this.uuid = uuid.v1();
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
this.uuid = options.uuid || uuid.v1();
this.replyToMsgId = options.replyToMsgId || 0;
this.toUserName = options.toUserName || '';
this.fromUserName = options.fromUserName || '';
this.subject = options.subject || '';
this.message = options.message || '';
if(_.isDate(options.modTimestamp)) {
this.modTimestamp = options.modTimestamp;
if(_.isDate(options.modTimestamp) || moment.isMoment(options.modTimestamp)) {
this.modTimestamp = moment(options.modTimestamp);
} else if(_.isString(options.modTimestamp)) {
this.modTimestamp = new Date(options.modTimestamp);
this.modTimestamp = moment(options.modTimestamp);
}
this.viewCount = options.viewCount || 0;
@ -44,55 +45,30 @@ function Message(options) {
this.meta = options.meta;
}
// this.meta = options.meta || {};
this.hashTags = options.hashTags || [];
var self = this;
this.isValid = function() {
// :TODO: validate as much as possible
return true;
};
this.isPrivate = function() {
return this.areaName === Message.WellKnownAreaNames.Private ? true : false;
return this.areaTag === Message.WellKnownAreaTags.Private ? true : false;
};
this.getMessageTimestampString = function(ts) {
ts = ts || new Date();
return ts.toISOString();
ts = ts || moment();
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
};
/*
Object.defineProperty(this, 'messageId', {
get : function() {
return messageId;
}
});
Object.defineProperty(this, 'areaId', {
get : function() { return areaId; },
set : function(i) {
areaId = i;
}
});
*/
}
Message.WellKnownAreaNames = {
Message.WellKnownAreaTags = {
Invalid : '',
Private : 'private_mail',
Bulletin : 'local_bulletin',
};
// :TODO: This doesn't seem like a good way to go -- perhaps only for local/user2user, or just use
// a system similar to the "last read" for general areas
Message.Status = {
New : 0,
Read : 1,
};
// :TODO: FTN stuff really doesn't belong here - move it elsewhere and/or just use the names directly when needed
Message.MetaCategories = {
System : 1, // ENiGMA1/2 stuff
FtnProperty : 2, // Various FTN network properties, ftn_cost, ftn_origin, ...
@ -102,18 +78,27 @@ Message.MetaCategories = {
Message.SystemMetaNames = {
LocalToUserID : 'local_to_user_id',
LocalFromUserID : 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0
};
Message.StateFlags0 = {
None : 0x00000000,
Imported : 0x00000001, // imported from foreign system
Exported : 0x00000002, // exported to foreign system
};
Message.FtnPropertyNames = {
FtnCost : 'ftn_cost',
FtnOrigNode : 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node',
FtnOrigNetwork : 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network',
FtnAttrFlags : 'ftn_attr_flags',
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
@ -132,6 +117,124 @@ Message.prototype.setLocalFromUserId = function(userId) {
this.meta.System.local_from_user_id = userId;
};
Message.getMessageIdByUuid = function(uuid, cb) {
msgDb.get(
`SELECT message_id
FROM message
WHERE message_uuid = ?
LIMIT 1;`,
[ uuid ],
(err, row) => {
if(err) {
cb(err);
} else {
const success = (row && row.message_id);
cb(success ? null : new Error('No match'), success ? row.message_id : null);
}
}
);
};
Message.getMessageIdsByMetaValue = function(category, name, value, cb) {
msgDb.all(
`SELECT message_id
FROM message_meta
WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`,
[ category, name, value ],
(err, rows) => {
if(err) {
cb(err);
} else {
cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s)
}
}
);
};
Message.getMetaValuesByMessageId = function(messageId, category, name, cb) {
const sql =
`SELECT meta_value
FROM message_meta
WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`;
msgDb.all(sql, [ messageId, category, name ], (err, rows) => {
if(err) {
return cb(err);
}
if(0 === rows.length) {
return cb(new Error('No value for category/name'));
}
// single values are returned without an array
if(1 === rows.length) {
return cb(null, rows[0].meta_value);
}
cb(null, rows.map(r => r.meta_value)); // map to array of values only
});
};
Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) {
async.waterfall(
[
function getMessageId(callback) {
Message.getMessageIdByUuid(uuid, (err, messageId) => {
callback(err, messageId);
});
},
function getMetaValues(messageId, callback) {
Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => {
callback(err, values);
});
}
],
(err, values) => {
cb(err, values);
}
);
};
Message.prototype.loadMeta = function(cb) {
/*
Example of loaded this.meta:
meta: {
System: {
local_to_user_id: 1234,
},
FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
}
}
*/
const sql =
`SELECT meta_category, meta_name, meta_value
FROM message_meta
WHERE message_id = ?;`;
let self = this;
msgDb.each(sql, [ this.messageId ], (err, row) => {
if(!(row.meta_category in self.meta)) {
self.meta[row.meta_category] = { };
self.meta[row.meta_category][row.meta_name] = row.meta_value;
} else {
if(!(row.meta_name in self.meta[row.meta_category])) {
self.meta[row.meta_category][row.meta_name] = row.meta_value;
} else {
if(_.isString(self.meta[row.meta_category][row.meta_name])) {
self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ];
}
self.meta[row.meta_category][row.meta_name].push(row.meta_value);
}
}
}, err => {
cb(err);
});
};
Message.prototype.load = function(options, cb) {
assert(_.isString(options.uuid));
@ -141,7 +244,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,14 +252,14 @@ 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;
self.fromUserName = msgRow.from_user_name;
self.subject = msgRow.subject;
self.message = msgRow.message;
self.modTimestamp = msgRow.modified_timestamp;
self.modTimestamp = moment(msgRow.modified_timestamp);
self.viewCount = msgRow.view_count;
callback(err);
@ -164,18 +267,13 @@ Message.prototype.load = function(options, cb) {
);
},
function loadMessageMeta(callback) {
// :TODO:
callback(null);
self.loadMeta(err => {
callback(err);
});
},
function loadHashTags(callback) {
// :TODO:
callback(null);
},
function loadMessageStatus(callback) {
if(options.user) {
// :TODO: Load from user_message_status
}
callback(null);
}
],
function complete(err) {
@ -184,27 +282,59 @@ Message.prototype.load = function(options, cb) {
);
};
Message.prototype.persistMetaValue = function(category, name, value, cb) {
const metaStmt = msgDb.prepare(
`INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value)
VALUES (?, ?, ?, ?);`);
if(!_.isArray(value)) {
value = [ value ];
}
let self = this;
async.each(value, (v, next) => {
metaStmt.run(self.messageId, category, name, v, err => {
next(err);
});
}, err => {
cb(err);
});
};
Message.startTransaction = function(cb) {
msgDb.run('BEGIN;', err => {
cb(err);
});
};
Message.endTransaction = function(hadError, cb) {
msgDb.run(hadError ? 'ROLLBACK;' : 'COMMIT;', err => {
cb(err);
});
};
Message.prototype.persist = function(cb) {
if(!this.isValid()) {
cb(new Error('Cannot persist invalid message!'));
return;
return cb(new Error('Cannot persist invalid message!'));
}
var self = this;
let self = this;
async.series(
[
function beginTransaction(callback) {
msgDb.run('BEGIN;', function transBegin(err) {
Message.startTransaction(err => {
callback(err);
});
},
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) ],
function msgInsert(err) {
`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 inserted(err) { // use for this scope
if(!err) {
self.messageId = this.lastID;
}
@ -217,26 +347,30 @@ Message.prototype.persist = function(cb) {
if(!self.meta) {
callback(null);
} else {
// :TODO: this should be it's own method such that meta can be updated
var metaStmt = msgDb.prepare(
'INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) ' +
'VALUES (?, ?, ?, ?);');
/*
Example of self.meta:
for(var metaCategroy in self.meta) {
async.each(Object.keys(self.meta[metaCategroy]), function meta(metaName, next) {
metaStmt.run(self.messageId, Message.MetaCategories[metaCategroy], metaName, self.meta[metaCategroy][metaName], function inserted(err) {
next(err);
});
}, function complete(err) {
if(!err) {
metaStmt.finalize(function finalized() {
callback(null);
});
} else {
callback(err);
meta: {
System: {
local_to_user_id: 1234,
},
FtnProperty: {
ftn_seen_by: [ "1/102 103", "2/42 52 65" ]
}
}
*/
async.each(Object.keys(self.meta), (category, nextCat) => {
async.each(Object.keys(self.meta[category]), (name, nextName) => {
self.persistMetaValue(category, name, self.meta[category][name], err => {
nextName(err);
});
}, err => {
nextCat(err);
});
}
}, err => {
callback(err);
});
}
},
function storeHashTags(callback) {
@ -244,9 +378,9 @@ Message.prototype.persist = function(cb) {
callback(null);
}
],
function complete(err) {
msgDb.run(err ? 'ROLLBACK;' : 'COMMIT;', function transEnd(err) {
cb(err, self.messageId);
err => {
Message.endTransaction(err, transErr => {
cb(err ? err : transErr, self.messageId);
});
}
);

View File

@ -1,104 +1,290 @@
/* jslint node: true */
'use strict';
var msgDb = require('./database.js').dbs.message;
var Config = require('./config.js').config;
var Message = require('./message.js');
var Log = require('./logger.js').log;
let msgDb = require('./database.js').dbs.message;
let Config = require('./config.js').config;
let Message = require('./message.js');
let Log = require('./logger.js').log;
let checkAcs = require('./acs_util.js').checkAcs;
let msgNetRecord = require('./msg_network.js').recordMessage;
var async = require('async');
var _ = require('lodash');
var assert = require('assert');
let async = require('async');
let _ = require('lodash');
let 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;
exports.persistMessage = persistMessage;
function getAvailableMessageAreas(options) {
// example: [ { "name" : "local_music", "desc" : "Music Discussion", "groups" : ["somegroup"] }, ... ]
options = options || {};
const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]';
const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]';
var areas = Config.messages.areas;
var avail = [];
for(var i = 0; i < areas.length; ++i) {
if(true !== options.includePrivate &&
Message.WellKnownAreaNames.Private === areas[i].name)
{
continue;
}
const AREA_ACS_DEFAULT = {
read : CONF_AREA_RW_ACS_DEFAULT,
write : CONF_AREA_RW_ACS_DEFAULT,
manage : AREA_MANAGE_ACS_DEFAULT,
};
avail.push(areas[i]);
}
function getAvailableMessageConferences(client, options) {
options = options || { includeSystemInternal : false };
return avail;
// perform ACS check per conf & omit system_internal if desired
return _.omit(Config.messageConferences, (v, k) => {
if(!options.includeSystemInternal && 'system_internal' === k) {
return true;
}
const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT;
return !checkAcs(client, readAcs);
});
}
function getDefaultMessageArea() {
//
// 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) {
const sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
return {
confTag : k,
conf : v,
};
});
if(index > -1) {
return availAreas[index];
}
sorted.sort((a, b) => {
const keyA = a.conf.sort ? a.conf.sort.toString() : a.conf.name;
const keyB = b.conf.sort ? b.conf.sort.toString() : b.conf.name;
return keyA.localeCompare(keyB);
});
return sorted;
}
function changeMessageArea(client, areaName, cb) {
// Return an *object* of available areas within |confTag|
function getAvailableMessageAreasByConfTag(confTag, options) {
options = options || {};
// :TODO: confTag === "" then find default
if(_.has(Config.messageConferences, [ confTag, 'areas' ])) {
const areas = Config.messageConferences[confTag].areas;
if(!options.client || true === options.noAcsCheck) {
// everything - no ACS checks
return areas;
} else {
// perform ACS check per area
return _.omit(areas, (v, k) => {
const readAcs = _.has(v, 'acs.read') ? v.acs.read : CONF_AREA_RW_ACS_DEFAULT;
return !checkAcs(options.client, readAcs);
});
}
}
}
function getSortedAvailMessageAreasByConfTag(confTag, options) {
const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
return {
areaTag : k,
area : v,
}
});
areas.sort((a, b) => {
const keyA = a.area.sort ? a.area.sort.toString() : a.area.name;
const keyB = b.area.sort ? b.area.sort.toString() : b.area.name;
return keyA.localeCompare(keyB);
});
return areas;
}
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 +305,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 +317,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 +325,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 +336,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 +344,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 +376,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 +397,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,27 +422,45 @@ function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
function update(lastId, callback) {
if(messageId > lastId) {
msgDb.run(
'REPLACE INTO user_message_area_last_read (user_id, area_name, message_id) ' +
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
'VALUES (?, ?, ?);',
[ userId, areaName, messageId ],
callback
[ userId, areaTag, messageId ],
function written(err) {
callback(err, true); // true=didUpdate
}
);
} else {
callback(null);
}
}
],
function complete(err) {
function complete(err, didUpdate) {
if(err) {
Log.debug(
{ error : err.toString(), userId : userId, areaName : areaName, messageId : messageId },
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId },
'Failed updating area last read ID');
} else {
Log.trace(
{ userId : userId, areaName : areaName, messageId : messageId },
'Area last read ID updated');
if(true === didUpdate) {
Log.trace(
{ userId : userId, areaTag : areaTag, messageId : messageId },
'Area last read ID updated');
}
}
cb(err);
}
);
}
function persistMessage(message, cb) {
async.series(
[
function persistMessageToDisc(callback) {
message.persist(callback);
},
function recordToMessageNetworks(callback) {
msgNetRecord(message, callback);
}
],
cb
);
}

View File

@ -1,13 +1,16 @@
/* jslint node: true */
'use strict';
var Config = require('./config.js').config;
var miscUtil = require('./misc_util.js');
// ENiGMA½
let Config = require('./config.js').config;
let miscUtil = require('./misc_util.js');
var fs = require('fs');
var paths = require('path');
var _ = require('lodash');
var assert = require('assert');
// standard/deps
let fs = require('fs');
let paths = require('path');
let _ = require('lodash');
let assert = require('assert');
let async = require('async');
// exports
exports.loadModuleEx = loadModuleEx;
@ -19,33 +22,34 @@ function loadModuleEx(options, cb) {
assert(_.isString(options.name));
assert(_.isString(options.path));
var modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
if(_.isObject(modConfig) && false === modConfig.enabled) {
cb(new Error('Module "' + options.name + '" is disabled'));
return;
}
var mod;
try {
var mod = require(paths.join(options.path, options.name + '.js'));
if(!_.isObject(mod.moduleInfo)) {
cb(new Error('Module is missing "moduleInfo" section'));
return;
}
if(!_.isFunction(mod.getModule)) {
cb(new Error('Invalid or missing "getModule" method for module!'));
return;
}
// Safe configuration, if any, for convience to the module
mod.runtime = { config : modConfig };
cb(null, mod);
mod = require(paths.join(options.path, options.name + '.js'));
} catch(e) {
cb(e);
}
if(!_.isObject(mod.moduleInfo)) {
cb(new Error('Module is missing "moduleInfo" section'));
return;
}
if(!_.isFunction(mod.getModule)) {
cb(new Error('Invalid or missing "getModule" method for module!'));
return;
}
// Ref configuration, if any, for convience to the module
mod.runtime = { config : modConfig };
cb(null, mod);
}
function loadModule(name, category, cb) {
@ -61,19 +65,26 @@ function loadModule(name, category, cb) {
});
}
function loadModulesForCategory(category, cb) {
var path = Config.paths[category];
function loadModulesForCategory(category, iterator, complete) {
fs.readdir(path, function onFiles(err, files) {
fs.readdir(Config.paths[category], (err, files) => {
if(err) {
cb(err);
return;
return iterator(err);
}
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);
const jsModules = files.filter(file => {
return '.js' === paths.extname(file);
});
async.each(jsModules, (file, next) => {
loadModule(paths.basename(file, '.js'), category, (err, mod) => {
iterator(err, mod);
next();
});
}, err => {
if(complete) {
complete(err);
}
});
});
}

59
core/msg_network.js Normal file
View File

@ -0,0 +1,59 @@
/* jslint node: true */
'use strict';
// ENiGMA½
let loadModulesForCategory = require('./module_util.js').loadModulesForCategory;
// standard/deps
let async = require('async');
exports.startup = startup
exports.shutdown = shutdown;
exports.recordMessage = recordMessage;
let msgNetworkModules = [];
function startup(cb) {
async.series(
[
function loadModules(callback) {
loadModulesForCategory('scannerTossers', (err, module) => {
if(!err) {
const modInst = new module.getModule();
modInst.startup(err => {
if(!err) {
msgNetworkModules.push(modInst);
}
});
}
}, err => {
callback(err);
});
}
],
cb
);
}
function shutdown() {
msgNetworkModules.forEach(mod => {
mod.shutdown();
});
msgNetworkModules = [];
}
function recordMessage(message, cb) {
//
// Give all message network modules (scanner/tossers)
// a chance to do something with |message|. Any or all can
// choose to ignore it.
//
async.each(msgNetworkModules, (modInst, next) => {
modInst.record(message);
next();
}, err => {
cb(err);
});
}

View File

@ -0,0 +1,24 @@
/* jslint node: true */
'use strict';
// ENiGMA½
var PluginModule = require('./plugin_module.js').PluginModule;
exports.MessageScanTossModule = MessageScanTossModule;
function MessageScanTossModule() {
PluginModule.call(this);
}
require('util').inherits(MessageScanTossModule, PluginModule);
MessageScanTossModule.prototype.startup = function(cb) {
cb(null);
};
MessageScanTossModule.prototype.shutdown = function(cb) {
cb(null);
};
MessageScanTossModule.prototype.record = function(message) {
};

View File

@ -267,23 +267,20 @@ function MultiLineEditTextView(options) {
return lines;
};
this.getOutputText = function(startIndex, endIndex, includeEol) {
var lines = self.getTextLines(startIndex, endIndex);
this.getOutputText = function(startIndex, endIndex, eolMarker) {
let lines = self.getTextLines(startIndex, endIndex);
let text = '';
var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
//
// Convert lines to contiguous string -- all expanded
// tabs put back to single '\t' characters.
//
var text = '';
var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
for(var i = 0; i < lines.length; ++i) {
text += lines[i].text.replace(re, '\t');
if(includeEol && lines[i].eol) {
text += '\n';
}
}
return text;
};
lines.forEach(line => {
text += line.text.replace(re, '\t');
if(eolMarker && line.eol) {
text += eolMarker;
}
});
return text;
}
this.getContiguousText = function(startIndex, endIndex, includeEol) {
var lines = self.getTextLines(startIndex, endIndex);
@ -1018,7 +1015,7 @@ MultiLineEditTextView.prototype.addText = function(text) {
};
MultiLineEditTextView.prototype.getData = function() {
return this.getOutputText(0, this.textLines.length, true);
return this.getOutputText(0, this.textLines.length, '\r\n');
};
MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {

View File

@ -7,6 +7,7 @@ var Message = require('./message.js');
var MenuModule = require('./menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController;
var _ = require('lodash');
var async = require('async');
exports.moduleInfo = {
@ -36,10 +37,11 @@ function NewScanModule(options) {
var self = this;
var config = this.menuConfig.config;
this.currentStep = 'messageAreas';
this.currentScanAux = 0; // e.g. Message.WellKnownAreaNames.Private when currentSteps = messageAreas
this.currentStep = 'messageConferences';
this.currentScanAux = {};
this.scanStartFmt = config.scanStartFmt || 'Scanning {desc}...';
// :TODO: Make this conf/area specific:
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
@ -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,22 +137,30 @@ function NewScanModule(options) {
},
function updateStatusScanStarted(callback) {
self.updateScanStatus(self.scanStartFmt.format({
desc : currentArea.desc,
confName : conf.conf.name,
confDesc : conf.conf.desc,
areaName : currentArea.area.name,
areaDesc : currentArea.area.desc,
}));
callback(null);
},
function newScanAreaAndGetMessages(callback) {
msgArea.getNewMessagesInAreaForUser(
self.client.user.userId, currentArea.name, function msgs(err, msgList) {
self.client.user.userId, currentArea.areaTag, function msgs(err, msgList) {
if(!err) {
if(0 === msgList.length) {
self.updateScanStatus(self.scanFinishNoneFmt.format({
desc : currentArea.desc,
confName : conf.conf.name,
confDesc : conf.conf.desc,
areaName : currentArea.area.name,
areaDesc : currentArea.area.desc,
}));
} else {
self.updateScanStatus(self.scanFinishNewFmt.format({
desc : currentArea.desc,
count : msgList.length,
confName : conf.conf.name,
confDesc : conf.conf.desc,
areaName : currentArea.area.name,
count : msgList.length,
}));
}
}
@ -107,14 +172,14 @@ function NewScanModule(options) {
if(msgList && msgList.length > 0) {
var nextModuleOpts = {
extraArgs: {
messageAreaName : currentArea.name,
messageAreaTag : currentArea.areaTag,
messageList : msgList,
}
};
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
} else {
self.newScanMessageArea(cb);
self.newScanMessageArea(conf, cb);
}
}
],
@ -161,10 +226,10 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
},
function performCurrentStepScan(callback) {
switch(self.currentStep) {
case 'messageAreas' :
self.newScanMessageArea(function scanComplete(err) {
callback(null); // finished
});
case 'messageConferences' :
self.newScanMessageConference(function scanComplete(err) {
callback(null); // finished
});
break;
default :
@ -180,9 +245,3 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
}
);
};
/*
NewScanModule.prototype.finishedLoading = function() {
NewScanModule.super_.prototype.finishedLoading.call(this);
};
*/

View File

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

172
core/sauce.js Normal file
View File

@ -0,0 +1,172 @@
/* 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'
exports.SAUCE_SIZE = SAUCE_SIZE;
// :TODO: SAUCE should be a class
// - with getFontName()
// - ...other methods
//
// See
// http://www.acid.org/info/sauce/sauce.htm
//
const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ];
function readSAUCE(data, cb) {
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;
}
if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) {
cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType));
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -235,7 +235,8 @@ SSHServerModule.prototype.createServer = function() {
privateKey : fs.readFileSync(Config.servers.ssh.privateKeyPem),
passphrase : Config.servers.ssh.privateKeyPass,
ident : 'enigma-bbs-' + enigVersion + '-srv',
// Note that sending 'banner' breaks at least EtherTerm!
// Note that sending 'banner' breaks at least EtherTerm!
debug : function debugSsh(dbgLine) {
if(true === Config.servers.ssh.traceConnections) {
Log.trace('SSH: ' + dbgLine);

View File

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

View File

@ -1,14 +1,16 @@
/* jslint node: true */
'use strict';
var miscUtil = require('./misc_util.js');
let miscUtil = require('./misc_util.js');
let iconv = require('iconv-lite');
exports.stylizeString = stylizeString;
exports.pad = pad;
exports.replaceAt = replaceAt;
exports.isPrintable = isPrintable;
exports.debugEscapedString = debugEscapedString;
exports.stylizeString = stylizeString;
exports.pad = pad;
exports.replaceAt = replaceAt;
exports.isPrintable = isPrintable;
exports.debugEscapedString = debugEscapedString;
exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
// :TODO: create Unicode verison of this
var VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
@ -176,6 +178,23 @@ function debugEscapedString(s) {
return JSON.stringify(s).slice(1, -1);
}
function stringFromNullTermBuffer(buf, encoding) {
/*var nullPos = buf.length;
for(var i = 0; i < buf.length; ++i) {
if(0x00 === buf[i]) {
nullPos = i;
break;
}
}
*/
let nullPos = buf.indexOf(new Buffer( [ 0x00 ] ));
if(-1 === nullPos) {
nullPos = buf.length;
}
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
}
//
// Extend String.format's object syntax with some modifiers
// e.g.: '{username!styleL33t}'.format( { username : 'Leet User' } ) -> "L33t U53r"

View File

@ -203,13 +203,13 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
}
}
[ 'menus', 'prompts' ].forEach(function areaEntry(areaName) {
_.keys(mergedTheme[areaName]).forEach(function menuEntry(menuName) {
[ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) {
_.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
var createdFormSection = false;
var mergedThemeMenu = mergedTheme[areaName][menuName];
var mergedThemeMenu = mergedTheme[sectionName][menuName];
if(_.has(theme, [ 'customization', areaName, menuName ])) {
var menuTheme = theme.customization[areaName][menuName];
if(_.has(theme, [ 'customization', sectionName, menuName ])) {
var menuTheme = theme.customization[sectionName][menuName];
// config block is direct assign/overwrite
// :TODO: should probably be _.merge()
@ -217,7 +217,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
}
if('menus' === areaName) {
if('menus' === sectionName) {
if(_.isObject(mergedThemeMenu.form)) {
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
@ -233,7 +233,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
createdFormSection = true;
}
}
} else if('prompts' === areaName) {
} else if('prompts' === sectionName) {
// no 'form' or form keys for prompts -- direct to mci
applyToForm(mergedThemeMenu, menuTheme);
}
@ -247,7 +247,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
// * There is/was no explicit 'form' section
// * There is no 'prompt' specified
//
if('menus' === areaName && !_.isString(mergedThemeMenu.prompt) &&
if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
{
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
@ -523,7 +523,8 @@ function displayThemedPause(options, cb) {
if(options.clearPrompt) {
if(artInfo.startRow && artInfo.height) {
options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
// :TODO: This will not work with NetRunner:
// Note: Does not work properly in NetRunner < 2.0b17:
options.client.term.rawWrite(ansi.deleteLine(artInfo.height));
} else {
options.client.term.rawWrite(ansi.eraseLine(1))

41
core/uuid_util.js Normal file
View File

@ -0,0 +1,41 @@
/* jslint node: true */
'use strict';
let uuid = require('node-uuid');
let assert = require('assert');
let _ = require('lodash');
let createHash = require('crypto').createHash;
exports.createNamedUUID = createNamedUUID;
function createNamedUUID(namespaceUuid, key) {
//
// v5 UUID generation code based on the work here:
// https://github.com/download13/uuidv5/blob/master/uuid.js
//
if(!Buffer.isBuffer(namespaceUuid)) {
namespaceUuid = new Buffer(namespaceUuid);
}
if(!Buffer.isBuffer(key)) {
key = new Buffer(key);
}
let digest = createHash('sha1').update(
Buffer.concat( [ namespaceUuid, key ] )).digest();
let u = new Buffer(16);
// bbbb - bb - bb - bb - bbbbbb
digest.copy(u, 0, 0, 4); // time_low
digest.copy(u, 4, 4, 6); // time_mid
digest.copy(u, 6, 6, 8); // time_hi_and_version
u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101)
u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10
u[9] = digest[9];
digest.copy(u, 10, 10, 16);
return u;
}

View File

@ -27,7 +27,7 @@ function VerticalMenuView(options) {
this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row);
}
if(this.autoScale.width) {
if(self.autoScale.width) {
var l = 0;
self.items.forEach(function item(i) {
if(i.text.length > l) {
@ -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();
};

View File

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

View File

@ -21,5 +21,90 @@ general: {
}
```
#### A Sample Configuration
Below is a **sample** `config.hjson` illustrating various (but not all!) elements that can be configured / tweaked.
```hjson
{
general: {
boardName: A Sample BBS
}
defaults: {
theme: super-fancy-theme
}
preLoginTheme: luciano_blocktronics
messageConferences: {
local_general: {
name: Local
desc: Local Discussions
default: true
areas: {
local_enigma_dev: {
name: ENiGMA 1/2 Development
desc: Discussion related to development and features of ENiGMA 1/2!
default: true
}
}
}
agoranet: {
name: Agoranet
desc: This network is for blatant exploitation of the greatest BBS scene art group ever.. ACiD.
areas: {
agoranet_bbs: {
name: BBS Discussion
desc: Discussion related to BBSs
}
}
}
}
messageNetworks: {
ftn: {
areas: {
agoranet_bbs: { /* hey kids, this matches above! */
// oh oh oh, and this one pairs up with a network below
network: agoranet
tag: AGN_BBS
uplinks: "46:1/100"
}
}
networks: {
agoranet: {
localAddress: "46:3/102"
}
}
}
}
scannerTossers: {
ftn_bso: {
schedule: {
import: every 1 hours or @watch:/home/enigma/bink/watchfile.txt
export: every 1 hours or @immediate
}
defaultZone: 46
defaultNetwork: agoranet
nodes: {
"46:*": {
archiveType: ZIP
encoding: utf8
}
}
}
}
}
```
### Menus
TODO: Documentation on menu.hjson, etc.

View File

@ -1,5 +1,5 @@
# Doors
ENiGMA½ supports a variety of methods for interacting with doors not limited to:
ENiGMA½ supports a variety of methods for interacting with doors not limited to:
* `abracadabra` module: Standard in/out (stdio) capture or temporary socket server that can be used with [DOSEMU](http://www.dosemu.org/), [DOSBox](http://www.dosbox.com/), [QEMU](http://wiki.qemu.org/Main_Page), etc.
* `bbs_link` module for interaction with [BBSLink](http://www.bbslink.net/)
@ -28,7 +28,7 @@ Variables for use in `args`:
### DOSEMU with abracadabra
[DOSEMU](http://www.dosemu.org/) can provide a good solution for running legacy DOS doors when running on Linux systems. For this, we will a virtual serial port (COM1) that communicates with stdio.
[DOSEMU](http://www.dosemu.org/) can provide a good solution for running legacy DOS doors when running on Linux systems. For this, we will create a virtual serial port (COM1) that communicates via stdio.
As an example, here are the steps for setting up Pimp Wars:

View File

@ -5,8 +5,7 @@ ENiGMA½ is a modern from scratch BBS package written in Node.js.
TL;DR? This should get you started...
## Prerequisites
* [Node.js](https://nodejs.org/) version **v0.12.2 or higher** (v4.2+ is recommended)
* [io.js](https://iojs.org/) should also work, though I have not yet tested this.
* [Node.js](https://nodejs.org/) version **v4.2.x or higher**
* :information_source: It is suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs
* **Windows users will need additional dependencies installed** for the `npm install` step in order to compile native binaries:
* A recent copy of Visual Studio ([Visual Studio Express](https://www.visualstudio.com/en-us/products/visual-studio-express-vs.aspx) editions OK)
@ -16,9 +15,9 @@ TL;DR? This should get you started...
If you're new to Node.js and/or do not care about Node itself and just want to get ENiGMA½ running these steps should get you going on most \*nix type enviornments:
```bash
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.30.1/install.sh | bash
nvm install 4.2.4
nvm use 4.2.4
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash
nvm install 4.4.0
nvm use 4.4.0
```
@ -48,16 +47,27 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
general: {
boardName: Super Awesome BBS
}
servers: {
ssh: {
privateKeyPass: YOUR_PK_PASS
enabled: true /* set to false to disable the SSH server */
}
}
messages: {
areas: [
{ name: "local_enigma_discusssion", desc: "ENiGMA Discussion", groups: [ "users" ] }
]
messageConferences: {
local_general: {
name: Local
desc: Local Discussions
default: true
areas: {
local_music: {
name: Music Discussion
desc: Music, bands, etc.
default: true
}
}
}
```

57
docs/msg_conf_area.md Normal file
View File

@ -0,0 +1,57 @@
# Message Conferences & Areas
**Message Conferences** and **Areas** allow for grouping of message base topics.
## Message Conferences
Message Conferences are the top level container for 1:n Message Areas via the `messageConferences` section in `config.hjson`. Common message conferences may include a local conference and one or more conferences each dedicated to a particular message network such as FidoNet, AgoraNet, etc.
Each conference is represented by a entry under `messageConferences`. **The areas key is the conferences tag**.
**Members**:
* `name` (required): Friendly conference name
* `desc` (required): Friendly conference description
* `sort` (optional): If supplied, provides a key used for sorting
* `default` (optional): Specify `true` to make this the default conference (e.g. assigned to new users)
* `areas`: Container of 1:n areas described below
**Example**:
```hjson
{
messageConferences: {
local: {
name: Local
desc: Local discussion
sort: 1
default: true
}
}
}
```
## Message Areas
Message Areas are topic specific containers for messages that live within a particular conference. **The areas key is it's areas tag**. For example, "General Discussion" may live under a Local conference while an AgoraNet conference may contain "BBS Discussion".
**Members**:
* `name` (required): Friendly area name
* `desc` (required): Friendly area discription
* `sort` (optional): If supplied, provides a key used for sorting
* `default` (optional): Specify `true` to make this the default area (e.g. assigned to new users)
**Example**:
```hjson
messageConferences: {
local: {
// ... see above ...
areas: {
local_enigma_dev: {
name: ENiGMA 1/2 Development
desc: Discussion related to features and development of ENiGMA 1/2!
sort: 1
default: true
}
}
}
}
```
## Message Networks
ENiGMA½ has the ability to network with other systems via [Message Networks](msg_networks.md). Message **area tags** (described above) are utilized to map foreign areas with locally defined areas.

117
docs/msg_networks.md Normal file
View File

@ -0,0 +1,117 @@
# Message Networks
Message networks are configured in `messageNetworks` section of `config.hjson`. Each network type has it's own sub section such as `ftn` for FidoNet Technology Network (FTN) style networks. Message Networks tie directly with [Message Areas](msg_conf_area.md) that are also defined in `config.hjson`.
## FidoNet Technology Network (FTN)
FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`.
### Networks
The `networks` section contains a sub section for network(s) you wish you join your board with. Each entry's key name can be referenced elsewhere in `config.hjson` for FTN oriented configurations.
**Members**:
* `localAddress` (required): FTN address of **your local system**
**Example**:
```hjson
{
messageNetworks: {
ftn: {
networks: {
agoranet: {
localAddress: "46:3/102"
}
}
}
}
}
```
### Areas
The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages.
When importing, messages will be placed in the local area that matches key under `areas`.
**Members**:
* `network` (required): Associated network from the `networks` section
* `tag` (required): FTN area tag
* `uplinks`: An array of FTN address uplink(s) for this network
**Example**:
```hjson
{
messageNetworks: {
ftn: {
areas: {
agoranet_bbs: { /* found within messageConferences */
network: agoranet
tag: AGN_BBS
uplinks: "46:1/100"
}
}
}
}
}
```
### BSO Import / Export
The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers::ftn_bso`.
**Members**:
* `defaultZone` (required): Sets the default BSO outbound zone
* `defaultNetwork` (optional): Sets the default network name from `messageNetworks::ftn::networks`. **Required if more than one network is defined**.
* `paths` (optional): Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`.
* `packetTargetByteSize` (optional): Overrides the system *target* packet (.pkt) size of 512000 bytes (512k)
* `bundleTargetByteSize` (optional): Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M)
* `schedule` (required): See Scheduling
* `nodes` (required): See Nodes
#### Nodes
The `nodes` section defines how to export messages for one or more uplinks.
A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain.
**Members**:
* `packetType` (optional): `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability
* `packetPassword` (optional): Password for the packet
* `encoding` (optional): Encoding to use for message bodies; Defaults to `utf-8`
* `archiveType` (optional): Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration)
**Example**:
```hjson
{
scannerTossers: {
ftn_bso: {
nodes: {
"46:*: {
packetType: 2+
packetPassword: mypass
encoding: cp437
archiveType: zip
}
}
}
}
}
```
#### Scheduling
Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers.
* `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`.
* `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`.
* Free form text can be things like `at 5:00 pm` or `every 2 hours`.
See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information.
**Example**:
```hjson
{
scannerTossers: {
ftn_bso: {
schedule: {
import: every 1 hours or @watch:/path/to/watchfile.ext
export: every 1 hours or @immediate
}
}
}
}
```

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@ -1,12 +1,13 @@
/* jslint node: true */
'use strict';
var FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
var Message = require('../core/message.js').Message;
var user = require('../core/user.js');
let FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
//var Message = require('../core/message.js').Message;
let persistMessage = require('../core/message_area.js').persistMessage;
let user = require('../core/user.js');
var _ = require('lodash');
var async = require('async');
let _ = require('lodash');
let async = require('async');
exports.getModule = AreaPostFSEModule;
@ -24,7 +25,7 @@ function AreaPostFSEModule(options) {
// we're posting, so always start with 'edit' mode
this.editorMode = 'edit';
this.menuMethods.editModeMenuSave = function(formData, extraArgs) {
this.menuMethods.editModeMenuSave = function() {
var msg;
async.series(
@ -36,16 +37,23 @@ function AreaPostFSEModule(options) {
});
},
function saveMessage(callback) {
persistMessage(msg, callback);
/*
msg.persist(function persisted(err) {
callback(err);
});
*/
}
],
function complete(err) {
if(err) {
// :TODO:... sooooo now what?
} else {
console.log(msg); // :TODO: remove me -- probably log that one was saved, however.
// note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info(
{ to : msg.toUserName, subject : msg.subject, uuid : msg.uuid },
'Message persisted'
);
}
self.nextMenu();
@ -56,11 +64,11 @@ function AreaPostFSEModule(options) {
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
AreaPostFSEModule.prototype.enter = function(client) {
AreaPostFSEModule.prototype.enter = function() {
if(_.isString(client.user.properties.message_area_name) && !_.isString(this.messageAreaName)) {
this.messageAreaName = client.user.properties.message_area_name;
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
this.messageAreaTag = this.client.user.properties.message_area_tag;
}
AreaPostFSEModule.super_.prototype.enter.call(this, client);
AreaPostFSEModule.super_.prototype.enter.call(this);
};

View File

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

122
mods/msg_conf_list.js Normal file
View File

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

View File

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

View File

@ -5,7 +5,7 @@ var user = require('../core/user.js');
var theme = require('../core/theme.js');
var login = require('../core/system_menu_method.js').login;
var Config = require('../core/config.js').config;
var getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea;
var messageArea = require('../core/message_area.js');
var async = require('async');
@ -65,6 +65,16 @@ function NewUserAppModule(options) {
newUser.username = formData.value.username;
//
// We have to disable ACS checks for initial default areas as the user is not yet ready
//
var confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
var areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
// can't store undefined!
confTag = confTag || '';
areaTag = areaTag || '';
newUser.properties = {
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
@ -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');

View File

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

Some files were not shown because too many files have changed in this diff Show More