Merge branch 'msg_network'
This commit is contained in:
commit
6f8f8f7e9d
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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**
|
|
@ -65,7 +65,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
|
|||
## License
|
||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
||||
|
||||
Copyright (c) 2015, Bryan D. Ashby
|
||||
Copyright (c) 2015-2016, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -804,16 +804,13 @@ module.exports = (function() {
|
|||
return !isNaN(value) && user.getAge() >= value;
|
||||
},
|
||||
AS : function accountStatus() {
|
||||
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
return _.findIndex(value, function cmp(accStatus) {
|
||||
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
|
||||
}) > -1;
|
||||
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(userAccountStatus) > -1;
|
||||
},
|
||||
EC : function isEncoding() {
|
||||
switch(value) {
|
||||
|
@ -842,7 +839,7 @@ module.exports = (function() {
|
|||
// :TODO: implement me!!
|
||||
return false;
|
||||
},
|
||||
SC : function isSecerConnection() {
|
||||
SC : function isSecureConnection() {
|
||||
return client.session.isSecure;
|
||||
},
|
||||
ML : function minutesLeft() {
|
||||
|
@ -870,16 +867,20 @@ module.exports = (function() {
|
|||
return !isNaN(value) && client.term.termWidth >= value;
|
||||
},
|
||||
ID : function isUserId(value) {
|
||||
return user.userId === value;
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(user.userId) > -1;
|
||||
},
|
||||
WD : function isOneOfDayOfWeek() {
|
||||
// :TODO: return true if DoW
|
||||
if(_.isNumber(value)) {
|
||||
|
||||
} else if(_.isArray(value)) {
|
||||
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
return false;
|
||||
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(new Date().getDay()) > -1;
|
||||
},
|
||||
MM : function isMinutesPastMidnight() {
|
||||
// :TODO: return true if value is >= minutes past midnight sys time
|
||||
|
|
|
@ -7,8 +7,13 @@ var acsParser = require('./acs_parser.js');
|
|||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
|
||||
exports.checkAcs = checkAcs;
|
||||
exports.getConditionalValue = getConditionalValue;
|
||||
|
||||
function checkAcs(client, acsString) {
|
||||
return acsParser.parse(acsString, { client : client } );
|
||||
}
|
||||
|
||||
function getConditionalValue(client, condArray, memberName) {
|
||||
assert(_.isObject(client));
|
||||
assert(_.isArray(condArray));
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
157
core/art.js
157
core/art.js
|
@ -12,6 +12,7 @@ var events = require('events');
|
|||
var util = require('util');
|
||||
var ansi = require('./ansi_term.js');
|
||||
var aep = require('./ansi_escape_parser.js');
|
||||
var sauce = require('./sauce.js');
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
|
@ -20,10 +21,6 @@ exports.getArtFromPath = getArtFromPath;
|
|||
exports.display = display;
|
||||
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
||||
|
||||
var SAUCE_SIZE = 128;
|
||||
var SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
|
||||
var COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
||||
|
||||
// :TODO: Return MCI code information
|
||||
// :TODO: process SAUCE comments
|
||||
// :TODO: return font + font mapped information from SAUCE
|
||||
|
@ -43,156 +40,6 @@ var SUPPORTED_ART_TYPES = {
|
|||
// :TODO: extension for topaz ansi/ascii.
|
||||
};
|
||||
|
||||
//
|
||||
// See
|
||||
// http://www.acid.org/info/sauce/sauce.htm
|
||||
//
|
||||
// :TODO: Move all SAUCE stuff to sauce.js
|
||||
function readSAUCE(data, cb) {
|
||||
if(data.length < SAUCE_SIZE) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = data.length - SAUCE_SIZE;
|
||||
var sauceRec = data.slice(offset);
|
||||
|
||||
binary.parse(sauceRec)
|
||||
.buffer('id', 5)
|
||||
.buffer('version', 2)
|
||||
.buffer('title', 35)
|
||||
.buffer('author', 20)
|
||||
.buffer('group', 20)
|
||||
.buffer('date', 8)
|
||||
.word32lu('fileSize')
|
||||
.word8('dataType')
|
||||
.word8('fileType')
|
||||
.word16lu('tinfo1')
|
||||
.word16lu('tinfo2')
|
||||
.word16lu('tinfo3')
|
||||
.word16lu('tinfo4')
|
||||
.word8('numComments')
|
||||
.word8('flags')
|
||||
.buffer('tinfos', 22) // SAUCE 00.5
|
||||
.tap(function onVars(vars) {
|
||||
|
||||
if(!SAUCE_ID.equals(vars.id)) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
}
|
||||
|
||||
var ver = iconv.decode(vars.version, 'cp437');
|
||||
|
||||
if('00' !== ver) {
|
||||
cb(new Error('Unsupported SAUCE version: ' + ver));
|
||||
return;
|
||||
}
|
||||
|
||||
var sauce = {
|
||||
id : iconv.decode(vars.id, 'cp437'),
|
||||
version : iconv.decode(vars.version, 'cp437').trim(),
|
||||
title : iconv.decode(vars.title, 'cp437').trim(),
|
||||
author : iconv.decode(vars.author, 'cp437').trim(),
|
||||
group : iconv.decode(vars.group, 'cp437').trim(),
|
||||
date : iconv.decode(vars.date, 'cp437').trim(),
|
||||
fileSize : vars.fileSize,
|
||||
dataType : vars.dataType,
|
||||
fileType : vars.fileType,
|
||||
tinfo1 : vars.tinfo1,
|
||||
tinfo2 : vars.tinfo2,
|
||||
tinfo3 : vars.tinfo3,
|
||||
tinfo4 : vars.tinfo4,
|
||||
numComments : vars.numComments,
|
||||
flags : vars.flags,
|
||||
tinfos : vars.tinfos,
|
||||
};
|
||||
|
||||
var dt = SAUCE_DATA_TYPES[sauce.dataType];
|
||||
if(dt && dt.parser) {
|
||||
sauce[dt.name] = dt.parser(sauce);
|
||||
}
|
||||
|
||||
cb(null, sauce);
|
||||
});
|
||||
}
|
||||
|
||||
// :TODO: These need completed:
|
||||
var SAUCE_DATA_TYPES = {};
|
||||
SAUCE_DATA_TYPES[0] = { name : 'None' };
|
||||
SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE };
|
||||
SAUCE_DATA_TYPES[2] = 'Bitmap';
|
||||
SAUCE_DATA_TYPES[3] = 'Vector';
|
||||
SAUCE_DATA_TYPES[4] = 'Audio';
|
||||
SAUCE_DATA_TYPES[5] = 'BinaryText';
|
||||
SAUCE_DATA_TYPES[6] = 'XBin';
|
||||
SAUCE_DATA_TYPES[7] = 'Archive';
|
||||
SAUCE_DATA_TYPES[8] = 'Executable';
|
||||
|
||||
var SAUCE_CHARACTER_FILE_TYPES = {};
|
||||
SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII';
|
||||
SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi';
|
||||
SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation';
|
||||
SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script';
|
||||
SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard';
|
||||
SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar';
|
||||
SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML';
|
||||
SAUCE_CHARACTER_FILE_TYPES[7] = 'Source';
|
||||
SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw';
|
||||
|
||||
//
|
||||
// Map of SAUCE font -> encoding hint
|
||||
//
|
||||
// Note that this is the same mapping that x84 uses. Be compatible!
|
||||
//
|
||||
var SAUCE_FONT_TO_ENCODING_HINT = {
|
||||
'Amiga MicroKnight' : 'amiga',
|
||||
'Amiga MicroKnight+' : 'amiga',
|
||||
'Amiga mOsOul' : 'amiga',
|
||||
'Amiga P0T-NOoDLE' : 'amiga',
|
||||
'Amiga Topaz 1' : 'amiga',
|
||||
'Amiga Topaz 1+' : 'amiga',
|
||||
'Amiga Topaz 2' : 'amiga',
|
||||
'Amiga Topaz 2+' : 'amiga',
|
||||
'Atari ATASCII' : 'atari',
|
||||
'IBM EGA43' : 'cp437',
|
||||
'IBM EGA' : 'cp437',
|
||||
'IBM VGA25G' : 'cp437',
|
||||
'IBM VGA50' : 'cp437',
|
||||
'IBM VGA' : 'cp437',
|
||||
};
|
||||
|
||||
['437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
|
||||
'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) {
|
||||
var codec = 'cp' + page;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
|
||||
});
|
||||
|
||||
function parseCharacterSAUCE(sauce) {
|
||||
var result = {};
|
||||
|
||||
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
|
||||
|
||||
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
|
||||
// convience: create ansiFlags
|
||||
sauce.ansiFlags = sauce.flags;
|
||||
|
||||
var i = 0;
|
||||
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
|
||||
++i;
|
||||
}
|
||||
var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
|
||||
if(fontName.length > 0) {
|
||||
result.fontName = fontName;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getFontNameFromSAUCE(sauce) {
|
||||
if(sauce.Character) {
|
||||
return sauce.Character.fontName;
|
||||
|
@ -249,7 +96,7 @@ function getArtFromPath(path, options, cb) {
|
|||
}
|
||||
|
||||
if(options.readSauce === true) {
|
||||
readSAUCE(data, function onSauce(err, sauce) {
|
||||
sauce.readSAUCE(data, function onSauce(err, sauce) {
|
||||
if(err) {
|
||||
cb(null, getResult());
|
||||
} else {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
'use strict';
|
||||
|
||||
var Config = require('./config.js').config;
|
||||
var theme = require('./theme.js');
|
||||
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
|
|
87
core/bbs.js
87
core/bbs.js
|
@ -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');
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
102
core/config.js
102
core/config.js
|
@ -8,10 +8,37 @@ var paths = require('path');
|
|||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var hjson = require('hjson');
|
||||
var assert = require('assert');
|
||||
|
||||
exports.init = init;
|
||||
exports.getDefaultPath = getDefaultPath;
|
||||
|
||||
function hasMessageConferenceAndArea(config) {
|
||||
assert(_.isObject(config.messageConferences)); // we create one ourself!
|
||||
|
||||
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
|
||||
return 'system_internal' !== confTag;
|
||||
});
|
||||
|
||||
if(0 === nonInternalConfs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// :TODO: there is likely a better/cleaner way of doing this
|
||||
|
||||
var result = false;
|
||||
_.forEach(nonInternalConfs, confTag => {
|
||||
if(_.has(config.messageConferences[confTag], 'areas') &&
|
||||
Object.keys(config.messageConferences[confTag].areas) > 0)
|
||||
{
|
||||
result = true;
|
||||
return false; // stop iteration
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function init(configPath, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
|
@ -48,19 +75,14 @@ function init(configPath, cb) {
|
|||
//
|
||||
// Various sections must now exist in config
|
||||
//
|
||||
if(!_.has(mergedConfig, 'messages.areas.') ||
|
||||
!_.isArray(mergedConfig.messages.areas) ||
|
||||
0 === mergedConfig.messages.areas.length ||
|
||||
!_.isString(mergedConfig.messages.areas[0].name))
|
||||
{
|
||||
var msgAreasErr = new Error('Please create at least one message area');
|
||||
if(hasMessageConferenceAndArea(mergedConfig)) {
|
||||
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
||||
msgAreasErr.code = 'EBADCONFIG';
|
||||
callback(msgAreasErr);
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
callback(null, mergedConfig);
|
||||
}
|
||||
}
|
||||
],
|
||||
function complete(err, mergedConfig) {
|
||||
exports.config = mergedConfig;
|
||||
|
@ -150,6 +172,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 : {
|
||||
|
|
|
@ -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)
|
||||
);`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
34
core/fse.js
34
core/fse.js
|
@ -7,7 +7,7 @@ var ansi = require('../core/ansi_term.js');
|
|||
var theme = require('../core/theme.js');
|
||||
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||
var Message = require('../core/message.js');
|
||||
var getMessageAreaByName = require('../core/message_area.js').getMessageAreaByName;
|
||||
var getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
|
||||
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
|
||||
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
|
||||
|
||||
|
@ -76,6 +76,8 @@ var MCICodeIds = {
|
|||
MessageID : 10,
|
||||
ReplyToMsgID : 11,
|
||||
|
||||
// :TODO: ConfName
|
||||
|
||||
},
|
||||
|
||||
ViewModeFooter : {
|
||||
|
@ -104,15 +106,15 @@ function FullScreenEditorModule(options) {
|
|||
// editorMode : view | edit | quote
|
||||
//
|
||||
// menuConfig.config or extraArgs
|
||||
// messageAreaName
|
||||
// messageAreaTag
|
||||
// messageIndex / messageTotal
|
||||
// toUserId
|
||||
//
|
||||
this.editorType = config.editorType;
|
||||
this.editorMode = config.editorMode;
|
||||
|
||||
if(config.messageAreaName) {
|
||||
this.messageAreaName = config.messageAreaName;
|
||||
if(config.messageAreaTag) {
|
||||
this.messageAreaTag = config.messageAreaTag;
|
||||
}
|
||||
|
||||
this.messageIndex = config.messageIndex || 0;
|
||||
|
@ -121,8 +123,8 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
// extraArgs can override some config
|
||||
if(_.isObject(options.extraArgs)) {
|
||||
if(options.extraArgs.messageAreaName) {
|
||||
this.messageAreaName = options.extraArgs.messageAreaName;
|
||||
if(options.extraArgs.messageAreaTag) {
|
||||
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||
}
|
||||
if(options.extraArgs.messageIndex) {
|
||||
this.messageIndex = options.extraArgs.messageIndex;
|
||||
|
@ -135,9 +137,6 @@ function FullScreenEditorModule(options) {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(this.toUserId)
|
||||
console.log(this.messageAreaName)
|
||||
|
||||
this.isReady = false;
|
||||
|
||||
this.isEditMode = function() {
|
||||
|
@ -149,7 +148,7 @@ function FullScreenEditorModule(options) {
|
|||
};
|
||||
|
||||
this.isLocalEmail = function() {
|
||||
return Message.WellKnownAreaNames.Private === self.messageAreaName;
|
||||
return Message.WellKnownAreaTags.Private === self.messageAreaTag;
|
||||
};
|
||||
|
||||
this.isReply = function() {
|
||||
|
@ -217,7 +216,7 @@ function FullScreenEditorModule(options) {
|
|||
var headerValues = self.viewControllers.header.getFormData().value;
|
||||
|
||||
var msgOpts = {
|
||||
areaName : self.messageAreaName,
|
||||
areaTag : self.messageAreaTag,
|
||||
toUserName : headerValues.to,
|
||||
fromUserName : headerValues.from,
|
||||
subject : headerValues.subject,
|
||||
|
@ -235,7 +234,7 @@ function FullScreenEditorModule(options) {
|
|||
self.message = message;
|
||||
|
||||
updateMessageAreaLastReadId(
|
||||
self.client.user.userId, self.messageAreaName, self.message.messageId,
|
||||
self.client.user.userId, self.messageAreaTag, self.message.messageId,
|
||||
function lastReadUpdated() {
|
||||
|
||||
if(self.isReady) {
|
||||
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
458
core/ftn_util.js
458
core/ftn_util.js
|
@ -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.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
||||
exports.getMessageSerialNumber = getMessageSerialNumber;
|
||||
exports.createMessageUuid = createMessageUuid;
|
||||
exports.createMessageUuidAlternate = createMessageUuidAlternate;
|
||||
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
||||
exports.getDateTimeString = getDateTimeString;
|
||||
|
||||
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 buffer;
|
||||
}
|
||||
|
||||
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 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() + '> ';
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Specs:
|
||||
// * http://ftsc.org/docs/fts-0009.001
|
||||
// *
|
||||
//
|
||||
function getFtnMsgIdKludgeLine(origAddress, messageId) {
|
||||
if(_.isObject(origAddress)) {
|
||||
origAddress = getFormattedFTNAddress(origAddress, '5D');
|
||||
}
|
||||
|
||||
return '\x01MSGID: ' + origAddress + ' ' + getFtnMessageSerialNumber(messageId);
|
||||
}
|
||||
|
||||
|
||||
function getFTNOriginLine() {
|
||||
//
|
||||
// Specs:
|
||||
// Return a FTS-0004 Origin line
|
||||
// http://ftsc.org/docs/fts-0004.001
|
||||
//
|
||||
return ' * Origin: ' + Config.general.boardName + '(' + getFidoNetAddress() + ')';
|
||||
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})`;
|
||||
}
|
||||
|
||||
//
|
||||
// Return a FRL-1005.001 "Via" line
|
||||
// http://ftsc.org/docs/frl-1005.001
|
||||
//
|
||||
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 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());
|
||||
|
||||
//
|
||||
// For now, we'll just append a new SEEN-BY entry
|
||||
//
|
||||
// :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];
|
||||
}
|
|
@ -25,6 +25,10 @@ function MenuModule(options) {
|
|||
var self = this;
|
||||
this.menuName = options.menuName;
|
||||
this.menuConfig = options.menuConfig;
|
||||
this.client = options.client;
|
||||
|
||||
// :TODO: this and the line below with .config creates empty ({}) objects in the theme --
|
||||
// ...which we really should not do. If they aren't there already, don't use 'em.
|
||||
this.menuConfig.options = options.menuConfig.options || {};
|
||||
this.menuMethods = {}; // methods called from @method's
|
||||
|
||||
|
@ -190,10 +194,7 @@ require('util').inherits(MenuModule, PluginModule);
|
|||
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
|
||||
|
||||
|
||||
MenuModule.prototype.enter = function(client) {
|
||||
this.client = client;
|
||||
assert(_.isObject(client));
|
||||
|
||||
MenuModule.prototype.enter = function() {
|
||||
if(_.isString(this.menuConfig.status)) {
|
||||
this.client.currentStatus = this.menuConfig.status;
|
||||
} else {
|
||||
|
|
|
@ -131,7 +131,7 @@ MenuStack.prototype.goto = function(name, options, cb) {
|
|||
modInst.restoreSavedState(options.savedState);
|
||||
}
|
||||
|
||||
modInst.enter(self.client);
|
||||
modInst.enter();
|
||||
|
||||
self.client.log.trace(
|
||||
{ stack : _.map(self.stack, function(si) { return si.name; } ) },
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
// ENiGMA½
|
||||
var moduleUtil = require('./module_util.js');
|
||||
var Log = require('./logger.js').log;
|
||||
var conf = require('./config.js'); // :TODO: remove me!
|
||||
var Config = require('./config.js').config;
|
||||
var asset = require('./asset.js');
|
||||
var theme = require('./theme.js');
|
||||
var getFullConfig = require('./config_util.js').getFullConfig;
|
||||
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
||||
var acsUtil = require('./acs_util.js');
|
||||
|
@ -68,17 +66,18 @@ function loadMenu(options, cb) {
|
|||
});
|
||||
},
|
||||
function loadMenuModule(menuConfig, callback) {
|
||||
var modAsset = asset.getModuleAsset(menuConfig.module);
|
||||
var modSupplied = null !== modAsset;
|
||||
|
||||
var modLoadOpts = {
|
||||
const modAsset = asset.getModuleAsset(menuConfig.module);
|
||||
const modSupplied = null !== modAsset;
|
||||
|
||||
const modLoadOpts = {
|
||||
name : modSupplied ? modAsset.asset : 'standard_menu',
|
||||
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods,
|
||||
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
|
||||
};
|
||||
|
||||
moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) {
|
||||
var modData = {
|
||||
const modData = {
|
||||
name : modLoadOpts.name,
|
||||
config : menuConfig,
|
||||
mod : mod,
|
||||
|
@ -97,7 +96,8 @@ function loadMenu(options, cb) {
|
|||
{
|
||||
menuName : options.name,
|
||||
menuConfig : modData.config,
|
||||
extraArgs : options.extraArgs
|
||||
extraArgs : options.extraArgs,
|
||||
client : options.client,
|
||||
});
|
||||
callback(null, moduleInstance);
|
||||
} catch(e) {
|
||||
|
@ -174,7 +174,7 @@ function handleAction(client, formData, conf) {
|
|||
assert(_.isObject(conf));
|
||||
assert(_.isString(conf.action));
|
||||
|
||||
var actionAsset = asset.parseAsset(conf.action);
|
||||
const actionAsset = asset.parseAsset(conf.action);
|
||||
assert(_.isObject(actionAsset));
|
||||
|
||||
switch(actionAsset.type) {
|
||||
|
@ -245,84 +245,3 @@ function handleNext(client, nextSpec, conf) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// :TODO: Seems better in theme.js, but that includes ViewController...which would then include theme.js
|
||||
// ...theme.js only brings in VC to create themed pause prompt. Perhaps that should live elsewhere
|
||||
/*
|
||||
function applyGeneralThemeCustomization(options) {
|
||||
//
|
||||
// options.name
|
||||
// options.client
|
||||
// options.type
|
||||
// options.config
|
||||
//
|
||||
assert(_.isString(options.name));
|
||||
assert(_.isObject(options.client));
|
||||
assert("menus" === options.type || "prompts" === options.type);
|
||||
|
||||
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
|
||||
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
|
||||
|
||||
if(themeConfig.config) {
|
||||
Object.keys(themeConfig.config).forEach(function confEntry(conf) {
|
||||
if(options.config[conf]) {
|
||||
_.defaultsDeep(options.config[conf], themeConfig.config[conf]);
|
||||
} else {
|
||||
options.config[conf] = themeConfig.config[conf];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
function applyMciThemeCustomization(options) {
|
||||
//
|
||||
// options.name : menu/prompt name
|
||||
// options.mci : menu/prompt .mci section
|
||||
// options.client : client
|
||||
// options.type : menu|prompt
|
||||
// options.formId : (optional) form ID in cases where multiple forms may exist wanting their own customization
|
||||
//
|
||||
// In the case of formId, the theme must include the ID as well, e.g.:
|
||||
// {
|
||||
// ...
|
||||
// "2" : {
|
||||
// "TL1" : { ... }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
assert(_.isString(options.name));
|
||||
assert("menus" === options.type || "prompts" === options.type);
|
||||
assert(_.isObject(options.client));
|
||||
|
||||
if(_.isUndefined(options.mci)) {
|
||||
options.mci = {};
|
||||
}
|
||||
|
||||
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
|
||||
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
|
||||
|
||||
if(options.formId && _.has(themeConfig, options.formId.toString())) {
|
||||
// form ID found - use exact match
|
||||
themeConfig = themeConfig[options.formId];
|
||||
}
|
||||
|
||||
if(themeConfig.mci) {
|
||||
Object.keys(themeConfig.mci).forEach(function mciEntry(mci) {
|
||||
// :TODO: a better way to do this?
|
||||
if(options.mci[mci]) {
|
||||
_.defaults(options.mci[mci], themeConfig.mci[mci]);
|
||||
} else {
|
||||
options.mci[mci] = themeConfig.mci[mci];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
|
||||
}
|
||||
*/
|
292
core/message.js
292
core/message.js
|
@ -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,36 +347,40 @@ 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);
|
||||
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);
|
||||
});
|
||||
}, function complete(err) {
|
||||
if(!err) {
|
||||
metaStmt.finalize(function finalized() {
|
||||
callback(null);
|
||||
}, err => {
|
||||
nextCat(err);
|
||||
});
|
||||
} else {
|
||||
|
||||
}, err => {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
function storeHashTags(callback) {
|
||||
// :TODO: hash tag support
|
||||
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);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,104 +1,283 @@
|
|||
/* 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,
|
||||
};
|
||||
|
||||
function getAvailableMessageConferences(client, options) {
|
||||
options = options || { includeSystemInternal : false };
|
||||
|
||||
// perform ACS check per conf & omit system_internal if desired
|
||||
return _.omit(Config.messageConferences, (v, k) => {
|
||||
if(!options.includeSystemInternal && 'system_internal' === k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
avail.push(areas[i]);
|
||||
const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
return !checkAcs(client, readAcs);
|
||||
});
|
||||
}
|
||||
|
||||
return avail;
|
||||
}
|
||||
|
||||
function getDefaultMessageArea() {
|
||||
//
|
||||
// Return first non-private/etc. area name. This will be from config.hjson
|
||||
//
|
||||
return getAvailableMessageAreas()[0];
|
||||
/*
|
||||
var avail = getAvailableMessageAreas();
|
||||
for(var i = 0; i < avail.length; ++i) {
|
||||
if(Message.WellKnownAreaNames.Private !== avail[i].name) {
|
||||
return avail[i];
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function getMessageAreaByName(areaName) {
|
||||
areaName = areaName.toLowerCase();
|
||||
|
||||
var availAreas = getAvailableMessageAreas( { includePrivate : true } );
|
||||
var index = _.findIndex(availAreas, function pred(an) {
|
||||
return an.name == areaName;
|
||||
function getSortedAvailMessageConferences(client, options) {
|
||||
var sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
||||
return {
|
||||
confTag : k,
|
||||
conf : v,
|
||||
};
|
||||
});
|
||||
|
||||
if(index > -1) {
|
||||
return availAreas[index];
|
||||
sorted.sort((a, b) => {
|
||||
return a.conf.name.localeCompare(b.conf.name);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// Return an *object* of available areas within |confTag|
|
||||
function getAvailableMessageAreasByConfTag(confTag, options) {
|
||||
options = options || {};
|
||||
|
||||
// :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 changeMessageArea(client, areaName, cb) {
|
||||
function getSortedAvailMessageAreasByConfTag(confTag, options) {
|
||||
const areas = getAvailableMessageAreasByConfTag(confTag, options);
|
||||
|
||||
// :TODO: should probably be using localeCompare / sort
|
||||
return _.sortBy(_.map(areas, (v, k) => {
|
||||
return {
|
||||
areaTag : k,
|
||||
area : v,
|
||||
};
|
||||
}), o => o.area.name); // sort by name
|
||||
}
|
||||
|
||||
function getDefaultMessageConferenceTag(client, disableAcsCheck) {
|
||||
//
|
||||
// Find the first conference marked 'default'. If found,
|
||||
// inspect |client| against *read* ACS using defaults if not
|
||||
// specified.
|
||||
//
|
||||
// If the above fails, just go down the list until we get one
|
||||
// that passes.
|
||||
//
|
||||
// It's possible that we end up with nothing here!
|
||||
//
|
||||
// Note that built in 'system_internal' is always ommited here
|
||||
//
|
||||
let defaultConf = _.findKey(Config.messageConferences, o => o.default);
|
||||
if(defaultConf) {
|
||||
const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(true === disableAcsCheck || checkAcs(client, acs)) {
|
||||
return defaultConf;
|
||||
}
|
||||
}
|
||||
|
||||
// just use anything we can
|
||||
defaultConf = _.findKey(Config.messageConferences, (o, k) => {
|
||||
const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs));
|
||||
});
|
||||
|
||||
return defaultConf;
|
||||
}
|
||||
|
||||
function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
|
||||
//
|
||||
// Similar to finding the default conference:
|
||||
// Find the first entry marked 'default', if any. If found, check | client| against
|
||||
// *read* ACS. If this fails, just find the first one we can that passes checks.
|
||||
//
|
||||
// It's possible that we end up with nothing!
|
||||
//
|
||||
confTag = confTag || getDefaultMessageConferenceTag(client);
|
||||
|
||||
if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) {
|
||||
const areaPool = Config.messageConferences[confTag].areas;
|
||||
let defaultArea = _.findKey(areaPool, o => o.default);
|
||||
if(defaultArea) {
|
||||
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||
if(true === disableAcsCheck || checkAcs(client, readAcs)) {
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
defaultArea = _.findKey(areaPool, (o, k) => {
|
||||
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||
return (true === disableAcsCheck || checkAcs(client, readAcs));
|
||||
});
|
||||
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageConferenceByTag(confTag) {
|
||||
return Config.messageConferences[confTag];
|
||||
}
|
||||
|
||||
function getMessageAreaByTag(areaTag, optionalConfTag) {
|
||||
const confs = Config.messageConferences;
|
||||
|
||||
if(_.isString(optionalConfTag)) {
|
||||
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
|
||||
return confs[optionalConfTag].areas[areaTag];
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// No confTag to work with - we'll have to search through them all
|
||||
//
|
||||
var area;
|
||||
_.forEach(confs, (v, k) => {
|
||||
if(_.has(v, [ 'areas', areaTag ])) {
|
||||
area = v.areas[areaTag];
|
||||
return false; // stop iteration
|
||||
}
|
||||
});
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
|
||||
function changeMessageConference(client, confTag, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function getConf(callback) {
|
||||
const conf = getMessageConferenceByTag(confTag);
|
||||
|
||||
if(conf) {
|
||||
callback(null, conf);
|
||||
} else {
|
||||
callback(new Error('Invalid message conference tag'));
|
||||
}
|
||||
},
|
||||
function getDefaultAreaInConf(conf, callback) {
|
||||
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
||||
const area = getMessageAreaByTag(areaTag, confTag);
|
||||
|
||||
if(area) {
|
||||
callback(null, conf, { areaTag : areaTag, area : area } );
|
||||
} else {
|
||||
callback(new Error('No available areas for this user in conference'));
|
||||
}
|
||||
},
|
||||
function validateAccess(conf, areaInfo, callback) {
|
||||
const confAcs = conf.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
|
||||
if(!checkAcs(client, confAcs)) {
|
||||
callback(new Error('User does not have access to this conference'));
|
||||
} else {
|
||||
const areaAcs = _.has(areaInfo, 'area.acs.read') ? areaInfo.area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(!checkAcs(client, areaAcs)) {
|
||||
callback(new Error('User does not have access to default area in this conference'));
|
||||
} else {
|
||||
callback(null, conf, areaInfo);
|
||||
}
|
||||
}
|
||||
},
|
||||
function changeConferenceAndArea(conf, areaInfo, callback) {
|
||||
const newProps = {
|
||||
message_conf_tag : confTag,
|
||||
message_area_tag : areaInfo.areaTag,
|
||||
};
|
||||
client.user.persistProperties(newProps, err => {
|
||||
callback(err, conf, areaInfo);
|
||||
});
|
||||
},
|
||||
],
|
||||
function complete(err, conf, areaInfo) {
|
||||
if(!err) {
|
||||
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
|
||||
} else {
|
||||
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
|
||||
}
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function changeMessageArea(client, areaTag, cb) {
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getArea(callback) {
|
||||
var area = getMessageAreaByName(areaName);
|
||||
const area = getMessageAreaByTag(areaTag);
|
||||
|
||||
if(area) {
|
||||
callback(null, area);
|
||||
} else {
|
||||
callback(new Error('Invalid message area'));
|
||||
callback(new Error('Invalid message area tag'));
|
||||
}
|
||||
},
|
||||
function validateAccess(area, callback) {
|
||||
if(_.isArray(area.groups) && !
|
||||
client.user.isGroupMember(area.groups))
|
||||
{
|
||||
//
|
||||
// Need at least *read* to access the area
|
||||
//
|
||||
const readAcs = _.has(area, 'acs.read') ? area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(!checkAcs(client, readAcs)) {
|
||||
callback(new Error('User does not have access to this area'));
|
||||
} else {
|
||||
callback(null, area);
|
||||
}
|
||||
},
|
||||
function changeArea(area, callback) {
|
||||
client.user.persistProperty('message_area_name', area.name, function persisted(err) {
|
||||
client.user.persistProperty('message_area_tag', areaTag, function persisted(err) {
|
||||
callback(err, area);
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err, area) {
|
||||
if(!err) {
|
||||
client.log.info( area, 'Current message area changed');
|
||||
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
|
||||
} else {
|
||||
client.log.warn( { area : area, error : err.message }, 'Could not change message area');
|
||||
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
||||
}
|
||||
|
||||
cb(err);
|
||||
|
@ -119,9 +298,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 +310,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 +318,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 +329,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 +337,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 +369,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 +390,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 +415,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 {
|
||||
if(true === didUpdate) {
|
||||
Log.trace(
|
||||
{ userId : userId, areaName : areaName, messageId : messageId },
|
||||
{ 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
|
||||
);
|
||||
}
|
|
@ -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,15 +22,19 @@ 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'));
|
||||
mod = require(paths.join(options.path, options.name + '.js'));
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
|
||||
if(!_.isObject(mod.moduleInfo)) {
|
||||
cb(new Error('Module is missing "moduleInfo" section'));
|
||||
|
@ -39,13 +46,10 @@ function loadModuleEx(options, cb) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Safe configuration, if any, for convience to the module
|
||||
// Ref configuration, if any, for convience to the module
|
||||
mod.runtime = { config : modConfig };
|
||||
|
||||
cb(null, mod);
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadModule(name, category, cb) {
|
||||
|
@ -61,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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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) {
|
||||
};
|
|
@ -267,23 +267,20 @@ function MultiLineEditTextView(options) {
|
|||
return lines;
|
||||
};
|
||||
|
||||
this.getOutputText = function(startIndex, endIndex, includeEol) {
|
||||
var lines = self.getTextLines(startIndex, endIndex);
|
||||
|
||||
//
|
||||
// Convert lines to contiguous string -- all expanded
|
||||
// tabs put back to single '\t' characters.
|
||||
//
|
||||
var text = '';
|
||||
this.getOutputText = function(startIndex, endIndex, eolMarker) {
|
||||
let lines = self.getTextLines(startIndex, endIndex);
|
||||
let 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';
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
103
core/new_scan.js
103
core/new_scan.js
|
@ -7,6 +7,7 @@ var Message = require('./message.js');
|
|||
var MenuModule = require('./menu_module.js').MenuModule;
|
||||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
|
@ -36,10 +37,11 @@ function NewScanModule(options) {
|
|||
var self = this;
|
||||
var config = this.menuConfig.config;
|
||||
|
||||
this.currentStep = 'messageAreas';
|
||||
this.currentScanAux = 0; // e.g. Message.WellKnownAreaNames.Private when currentSteps = messageAreas
|
||||
this.currentStep = 'messageConferences';
|
||||
this.currentScanAux = {};
|
||||
|
||||
this.scanStartFmt = config.scanStartFmt || 'Scanning {desc}...';
|
||||
// :TODO: Make this conf/area specific:
|
||||
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
|
||||
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
|
||||
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
|
||||
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
|
||||
|
@ -58,9 +60,64 @@ function NewScanModule(options) {
|
|||
}
|
||||
};
|
||||
|
||||
this.newScanMessageArea = function(cb) {
|
||||
var availMsgAreas = msgArea.getAvailableMessageAreas( { includePrivate : true } );
|
||||
var currentArea = availMsgAreas[self.currentScanAux];
|
||||
this.newScanMessageConference = function(cb) {
|
||||
// lazy init
|
||||
if(!self.sortedMessageConfs) {
|
||||
const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc.
|
||||
|
||||
self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => {
|
||||
return {
|
||||
confTag : k,
|
||||
conf : v,
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Sort conferences by name, other than 'system_internal' which should
|
||||
// always come first such that we display private mails/etc. before
|
||||
// other conferences & areas
|
||||
//
|
||||
self.sortedMessageConfs.sort((a, b) => {
|
||||
if('system_internal' === a.confTag) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.conf.name.localeCompare(b.conf.name);
|
||||
}
|
||||
});
|
||||
|
||||
self.currentScanAux.conf = self.currentScanAux.conf || 0;
|
||||
self.currentScanAux.area = self.currentScanAux.area || 0;
|
||||
}
|
||||
|
||||
const currentConf = self.sortedMessageConfs[self.currentScanAux.conf];
|
||||
|
||||
async.series(
|
||||
[
|
||||
function scanArea(callback) {
|
||||
//self.currentScanAux.area = self.currentScanAux.area || 0;
|
||||
|
||||
self.newScanMessageArea(currentConf, function areaScanComplete(err) {
|
||||
if(self.sortedMessageConfs.length > self.currentScanAux.conf + 1) {
|
||||
self.currentScanAux.conf += 1;
|
||||
self.currentScanAux.area = 0;
|
||||
|
||||
self.newScanMessageConference(cb); // recursive to next conf
|
||||
//callback(null);
|
||||
} else {
|
||||
self.updateScanStatus(self.scanCompleteMsg);
|
||||
callback(new Error('No more conferences'));
|
||||
}
|
||||
});
|
||||
}
|
||||
],
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
this.newScanMessageArea = function(conf, cb) {
|
||||
// :TODO: it would be nice to cache this - must be done by conf!
|
||||
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : self.client } );
|
||||
const currentArea = sortedAreas[self.currentScanAux.area];
|
||||
|
||||
//
|
||||
// Scan and update index until we find something. If results are found,
|
||||
|
@ -70,8 +127,8 @@ function NewScanModule(options) {
|
|||
[
|
||||
function checkAndUpdateIndex(callback) {
|
||||
// Advance to next area if possible
|
||||
if(availMsgAreas.length >= self.currentScanAux + 1) {
|
||||
self.currentScanAux += 1;
|
||||
if(sortedAreas.length >= self.currentScanAux.area + 1) {
|
||||
self.currentScanAux.area += 1;
|
||||
callback(null);
|
||||
} else {
|
||||
self.updateScanStatus(self.scanCompleteMsg);
|
||||
|
@ -80,21 +137,29 @@ function NewScanModule(options) {
|
|||
},
|
||||
function updateStatusScanStarted(callback) {
|
||||
self.updateScanStatus(self.scanStartFmt.format({
|
||||
desc : currentArea.desc,
|
||||
confName : conf.conf.name,
|
||||
confDesc : conf.conf.desc,
|
||||
areaName : currentArea.area.name,
|
||||
areaDesc : currentArea.area.desc,
|
||||
}));
|
||||
callback(null);
|
||||
},
|
||||
function newScanAreaAndGetMessages(callback) {
|
||||
msgArea.getNewMessagesInAreaForUser(
|
||||
self.client.user.userId, currentArea.name, function msgs(err, msgList) {
|
||||
self.client.user.userId, currentArea.areaTag, function msgs(err, msgList) {
|
||||
if(!err) {
|
||||
if(0 === msgList.length) {
|
||||
self.updateScanStatus(self.scanFinishNoneFmt.format({
|
||||
desc : currentArea.desc,
|
||||
confName : conf.conf.name,
|
||||
confDesc : conf.conf.desc,
|
||||
areaName : currentArea.area.name,
|
||||
areaDesc : currentArea.area.desc,
|
||||
}));
|
||||
} else {
|
||||
self.updateScanStatus(self.scanFinishNewFmt.format({
|
||||
desc : currentArea.desc,
|
||||
confName : conf.conf.name,
|
||||
confDesc : conf.conf.desc,
|
||||
areaName : currentArea.area.name,
|
||||
count : msgList.length,
|
||||
}));
|
||||
}
|
||||
|
@ -107,14 +172,14 @@ function NewScanModule(options) {
|
|||
if(msgList && msgList.length > 0) {
|
||||
var nextModuleOpts = {
|
||||
extraArgs: {
|
||||
messageAreaName : currentArea.name,
|
||||
messageAreaTag : currentArea.areaTag,
|
||||
messageList : msgList,
|
||||
}
|
||||
};
|
||||
|
||||
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
|
||||
} else {
|
||||
self.newScanMessageArea(cb);
|
||||
self.newScanMessageArea(conf, cb);
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -161,8 +226,8 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
|
|||
},
|
||||
function performCurrentStepScan(callback) {
|
||||
switch(self.currentStep) {
|
||||
case 'messageAreas' :
|
||||
self.newScanMessageArea(function scanComplete(err) {
|
||||
case 'messageConferences' :
|
||||
self.newScanMessageConference(function scanComplete(err) {
|
||||
callback(null); // finished
|
||||
});
|
||||
break;
|
||||
|
@ -180,9 +245,3 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
NewScanModule.prototype.finishedLoading = function() {
|
||||
NewScanModule.super_.prototype.finishedLoading.call(this);
|
||||
};
|
||||
*/
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
var Config = require('./config.js').config;
|
||||
var Log = require('./logger.js').log;
|
||||
var getMessageAreaByName = require('./message_area.js').getMessageAreaByName;
|
||||
var getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
|
||||
var clientConnections = require('./client_connections.js');
|
||||
var sysProp = require('./system_property.js');
|
||||
|
||||
|
@ -63,8 +63,13 @@ function getPredefinedMCIValue(client, code) {
|
|||
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
|
||||
},
|
||||
|
||||
MA : function messageAreaDescription() {
|
||||
var area = getMessageAreaByName(client.user.properties.message_area_name);
|
||||
MA : function messageAreaName() {
|
||||
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
|
||||
return area ? area.name : '';
|
||||
},
|
||||
|
||||
ML : function messageAreaDescription() {
|
||||
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
|
||||
return area ? area.desc : '';
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,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
|
@ -235,6 +235,7 @@ 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!
|
||||
debug : function debugSsh(dbgLine) {
|
||||
if(true === Config.servers.ssh.traceConnections) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -27,7 +27,7 @@ function VerticalMenuView(options) {
|
|||
this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row);
|
||||
}
|
||||
|
||||
if(this.autoScale.width) {
|
||||
if(self.autoScale.width) {
|
||||
var l = 0;
|
||||
self.items.forEach(function item(i) {
|
||||
if(i.text.length > l) {
|
||||
|
@ -149,6 +149,17 @@ VerticalMenuView.prototype.setFocus = function(focused) {
|
|||
VerticalMenuView.prototype.setFocusItemIndex = function(index) {
|
||||
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
||||
|
||||
//this.updateViewVisibleItems();
|
||||
|
||||
// :TODO: |viewWindow| must be updated to reflect position change --
|
||||
// if > visibile then += by diff, if < visible
|
||||
|
||||
if(this.focusedItemIndex > this.viewWindow.bottom) {
|
||||
} else if (this.focusedItemIndex < this.viewWindow.top) {
|
||||
// this.viewWindow.top--;
|
||||
// this.viewWindow.bottom--;
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
};
|
||||
|
||||
|
|
|
@ -488,7 +488,6 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) {
|
|||
assert(_.isObject(options.mciMap));
|
||||
|
||||
var self = this;
|
||||
var promptName = _.isString(options.promptName) ? options.promptName : self.client.currentMenuModule.menuConfig.prompt;
|
||||
var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig;
|
||||
var initialFocusId = 1; // default to first
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
||||
|
||||
## 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -16,15 +16,13 @@
|
|||
return !isNaN(value) && user.getAge() >= value;
|
||||
},
|
||||
AS : function accountStatus() {
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
return _.findIndex(value, function cmp(accStatus) {
|
||||
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
|
||||
}) > -1;
|
||||
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(userAccountStatus) > -1;
|
||||
},
|
||||
EC : function isEncoding() {
|
||||
switch(value) {
|
||||
|
@ -53,7 +51,7 @@
|
|||
// :TODO: implement me!!
|
||||
return false;
|
||||
},
|
||||
SC : function isSecerConnection() {
|
||||
SC : function isSecureConnection() {
|
||||
return client.session.isSecure;
|
||||
},
|
||||
ML : function minutesLeft() {
|
||||
|
@ -81,28 +79,20 @@
|
|||
return !isNaN(value) && client.term.termWidth >= value;
|
||||
},
|
||||
ID : function isUserId(value) {
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
return _.findIndex(value, function cmp(uid) {
|
||||
return user.userId === parseInt(uid, 10);
|
||||
}) > -1;
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(user.userId) > -1;
|
||||
},
|
||||
WD : function isOneOfDayOfWeek() {
|
||||
if(_.isNumber(value)) {
|
||||
if(!_.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
assert(_.isArray(value));
|
||||
|
||||
var nowDayOfWeek = new Date().getDay();
|
||||
|
||||
return _.findIndex(value, function cmp(dow) {
|
||||
return nowDayOfWeek === parseInt(dow, 10);
|
||||
}) > -1;
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(new Date().getDay()) > -1;
|
||||
},
|
||||
MM : function isMinutesPastMidnight() {
|
||||
// :TODO: return true if value is >= minutes past midnight sys time
|
||||
|
|
|
@ -177,12 +177,6 @@ function AbracadabraModule(options) {
|
|||
|
||||
require('util').inherits(AbracadabraModule, MenuModule);
|
||||
|
||||
/*
|
||||
AbracadabraModule.prototype.enter = function(client) {
|
||||
AbracadabraModule.super_.prototype.enter.call(this, client);
|
||||
};
|
||||
*/
|
||||
|
||||
AbracadabraModule.prototype.leave = function() {
|
||||
AbracadabraModule.super_.prototype.leave.call(this);
|
||||
|
||||
|
|
|
@ -393,7 +393,7 @@
|
|||
},
|
||||
editorMode: edit
|
||||
editorType: email
|
||||
messageAreaName: private_mail
|
||||
messageAreaTag: private_mail
|
||||
toUserId: 1 /* always to +op */
|
||||
}
|
||||
form: {
|
||||
|
@ -806,7 +806,7 @@
|
|||
},
|
||||
editorMode: edit
|
||||
editorType: email
|
||||
messageAreaName: private_mail
|
||||
messageAreaTag: private_mail
|
||||
toUserId: 1 /* always to +op */
|
||||
}
|
||||
form: {
|
||||
|
@ -1019,6 +1019,10 @@
|
|||
value: { command: "P" }
|
||||
action: @menu:messageAreaNewPost
|
||||
}
|
||||
{
|
||||
value: { command: "J" }
|
||||
action: @menu:messageAreaChangeCurrentConference
|
||||
}
|
||||
{
|
||||
value: { command: "C" }
|
||||
action: @menu:messageAreaChangeCurrentArea
|
||||
|
@ -1041,7 +1045,39 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
messageAreaChangeCurrentConference: {
|
||||
art: CCHANGE
|
||||
module: msg_conf_list
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
focus: true
|
||||
submit: true
|
||||
argName: conf
|
||||
}
|
||||
}
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { conf: null }
|
||||
action: @method:changeConference
|
||||
}
|
||||
]
|
||||
}
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape", "q", "shift + q" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaChangeCurrentArea: {
|
||||
// :TODO: rename this art to ACHANGE
|
||||
art: CHANGE
|
||||
module: msg_area_list
|
||||
form: {
|
||||
|
@ -1070,6 +1106,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaMessageList: {
|
||||
module: msg_list
|
||||
art: MSGLIST
|
||||
|
|
|
@ -5,7 +5,6 @@ var MenuModule = require('../core/menu_module.js').MenuModule;
|
|||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
var messageArea = require('../core/message_area.js');
|
||||
var strUtil = require('../core/string_util.js');
|
||||
//var msgDb = require('./database.js').dbs.message;
|
||||
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
|
@ -43,14 +42,17 @@ function MessageAreaListModule(options) {
|
|||
|
||||
var self = this;
|
||||
|
||||
this.messageAreas = messageArea.getAvailableMessageAreas();
|
||||
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
|
||||
self.client.user.properties.message_conf_tag,
|
||||
{ client : self.client }
|
||||
);
|
||||
|
||||
this.menuMethods = {
|
||||
changeArea : function(formData, extraArgs) {
|
||||
if(1 === formData.submitId) {
|
||||
var areaName = self.messageAreas[formData.value.area].name;
|
||||
const areaTag = self.messageAreas[formData.value.area].areaTag;
|
||||
|
||||
messageArea.changeMessageArea(self.client, areaName, function areaChanged(err) {
|
||||
messageArea.changeMessageArea(self.client, areaTag, function areaChanged(err) {
|
||||
if(err) {
|
||||
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
|
||||
|
||||
|
@ -66,7 +68,7 @@ function MessageAreaListModule(options) {
|
|||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
var v = self.viewControllers.areaList.getView(id);
|
||||
const v = self.viewControllers.areaList.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
|
@ -78,7 +80,7 @@ require('util').inherits(MessageAreaListModule, MenuModule);
|
|||
|
||||
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
||||
var self = this;
|
||||
var vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -99,26 +101,29 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
|||
});
|
||||
},
|
||||
function populateAreaListView(callback) {
|
||||
var listFormat = self.menuConfig.config.listFormat || '{index} ) - {desc}';
|
||||
var focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
var areaListItems = [];
|
||||
var focusListItems = [];
|
||||
const areaListView = vc.getView(1);
|
||||
let i = 1;
|
||||
areaListView.setItems(_.map(self.messageAreas, v => {
|
||||
return listFormat.format({
|
||||
index : i++,
|
||||
areaTag : v.area.areaTag,
|
||||
name : v.area.name,
|
||||
desc : v.area.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
// :TODO: use _.map() here
|
||||
for(var i = 0; i < self.messageAreas.length; ++i) {
|
||||
areaListItems.push(listFormat.format(
|
||||
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
|
||||
);
|
||||
focusListItems.push(focusListFormat.format(
|
||||
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
|
||||
);
|
||||
}
|
||||
|
||||
var areaListView = vc.getView(1);
|
||||
|
||||
areaListView.setItems(areaListItems);
|
||||
areaListView.setFocusItems(focusListItems);
|
||||
i = 1;
|
||||
areaListView.setFocusItems(_.map(self.messageAreas, v => {
|
||||
return focusListFormat.format({
|
||||
index : i++,
|
||||
areaTag : v.area.areaTag,
|
||||
name : v.area.name,
|
||||
desc : v.area.desc,
|
||||
})
|
||||
}));
|
||||
|
||||
areaListView.redraw();
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -72,7 +72,7 @@ function AreaViewFSEModule(options) {
|
|||
if(_.isString(extraArgs.menu)) {
|
||||
var modOpts = {
|
||||
extraArgs : {
|
||||
messageAreaName : self.messageAreaName,
|
||||
messageAreaTag : self.messageAreaTag,
|
||||
replyToMessage : self.message,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
var messageArea = require('../core/message_area.js');
|
||||
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
|
||||
exports.getModule = MessageConfListModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Conference List',
|
||||
desc : 'Module for listing / choosing message conferences',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
var MciCodesIds = {
|
||||
ConfList : 1,
|
||||
CurrentConf : 2,
|
||||
|
||||
// :TODO:
|
||||
// # areas in con
|
||||
//
|
||||
};
|
||||
|
||||
function MessageConfListModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
var self = this;
|
||||
|
||||
this.messageConfs = messageArea.getSortedAvailMessageConferences(self.client);
|
||||
|
||||
this.menuMethods = {
|
||||
changeConference : function(formData, extraArgs) {
|
||||
if(1 === formData.submitId) {
|
||||
const confTag = self.messageConfs[formData.value.conf].confTag;
|
||||
|
||||
messageArea.changeMessageConference(self.client, confTag, err => {
|
||||
if(err) {
|
||||
self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`);
|
||||
|
||||
setTimeout(function timeout() {
|
||||
self.prevMenu();
|
||||
}, 1000);
|
||||
} else {
|
||||
self.prevMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
const v = self.viewControllers.areaList.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(MessageConfListModule, MenuModule);
|
||||
|
||||
MessageConfListModule.prototype.mciReady = function(mciData, cb) {
|
||||
var self = this;
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
MessageConfListModule.super_.prototype.mciReady.call(this, mciData, callback);
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
let loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu,
|
||||
formId : 0,
|
||||
};
|
||||
|
||||
vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function populateConfListView(callback) {
|
||||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
const confListView = vc.getView(1);
|
||||
let i = 1;
|
||||
confListView.setItems(_.map(self.messageConfs, v => {
|
||||
return listFormat.format({
|
||||
index : i++,
|
||||
confTag : v.conf.confTag,
|
||||
name : v.conf.name,
|
||||
desc : v.conf.desc,
|
||||
});
|
||||
}));
|
||||
|
||||
i = 1;
|
||||
confListView.setFocusItems(_.map(self.messageConfs, v => {
|
||||
return focusListFormat.format({
|
||||
index : i++,
|
||||
confTag : v.conf.confTag,
|
||||
name : v.conf.name,
|
||||
desc : v.conf.desc,
|
||||
})
|
||||
}));
|
||||
|
||||
confListView.redraw();
|
||||
|
||||
callback(null);
|
||||
},
|
||||
function populateTextViews(callback) {
|
||||
// :TODO: populate other avail MCI, e.g. current conf name
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
};
|
|
@ -52,15 +52,15 @@ function MessageListModule(options) {
|
|||
var self = this;
|
||||
var config = this.menuConfig.config;
|
||||
|
||||
this.messageAreaName = config.messageAreaName;
|
||||
this.messageAreaTag = config.messageAreaTag;
|
||||
|
||||
if(options.extraArgs) {
|
||||
//
|
||||
// |extraArgs| can override |messageAreaName| provided by config
|
||||
// |extraArgs| can override |messageAreaTag| provided by config
|
||||
// as well as supply a pre-defined message list
|
||||
//
|
||||
if(options.extraArgs.messageAreaName) {
|
||||
this.messageAreaName = options.extraArgs.messageAreaName;
|
||||
if(options.extraArgs.messageAreaTag) {
|
||||
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||
}
|
||||
|
||||
if(options.extraArgs.messageList) {
|
||||
|
@ -73,7 +73,7 @@ function MessageListModule(options) {
|
|||
if(1 === formData.submitId) {
|
||||
var modOpts = {
|
||||
extraArgs : {
|
||||
messageAreaName : self.messageAreaName,
|
||||
messageAreaTag : self.messageAreaTag,
|
||||
messageList : self.messageList,
|
||||
messageIndex : formData.value.message,
|
||||
}
|
||||
|
@ -94,15 +94,15 @@ function MessageListModule(options) {
|
|||
|
||||
require('util').inherits(MessageListModule, MenuModule);
|
||||
|
||||
MessageListModule.prototype.enter = function(client) {
|
||||
MessageListModule.super_.prototype.enter.call(this, client);
|
||||
MessageListModule.prototype.enter = function() {
|
||||
MessageListModule.super_.prototype.enter.call(this);
|
||||
|
||||
//
|
||||
// Config can specify |messageAreaName| else it comes from
|
||||
// Config can specify |messageAreaTag| else it comes from
|
||||
// the user's current area
|
||||
//
|
||||
if(!this.messageAreaName) {
|
||||
this.messageAreaName = client.user.properties.message_area_name;
|
||||
if(!this.messageAreaTag) {
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -110,6 +110,8 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
var self = this;
|
||||
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
var firstNewEntryIndex;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
|
@ -130,7 +132,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
if(_.isArray(self.messageList)) {
|
||||
callback(0 === self.messageList.length ? new Error('No messages in area') : null);
|
||||
} else {
|
||||
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaName, function msgs(err, msgList) {
|
||||
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) {
|
||||
if(msgList && 0 === msgList.length) {
|
||||
callback(new Error('No messages in area'));
|
||||
} else {
|
||||
|
@ -141,7 +143,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
}
|
||||
},
|
||||
function getLastReadMesageId(callback) {
|
||||
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaName, function lastRead(err, lastReadId) {
|
||||
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) {
|
||||
self.lastReadId = lastReadId || 0;
|
||||
callback(null); // ignore any errors, e.g. missing value
|
||||
});
|
||||
|
@ -158,6 +160,13 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
var msgNum = 1;
|
||||
|
||||
function getMsgFmtObj(mle) {
|
||||
|
||||
if(_.isUndefined(firstNewEntryIndex) &&
|
||||
mle.messageId > self.lastReadId)
|
||||
{
|
||||
firstNewEntryIndex = msgNum - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
msgNum : msgNum++,
|
||||
subj : mle.subject,
|
||||
|
@ -183,11 +192,15 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
|
||||
msgListView.redraw();
|
||||
|
||||
if(firstNewEntryIndex > 0) {
|
||||
msgListView.setFocusItemIndex(firstNewEntryIndex);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
},
|
||||
function populateOtherMciViews(callback) {
|
||||
|
||||
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByName(self.messageAreaName).desc);
|
||||
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByTag(self.messageAreaTag).name);
|
||||
self.setViewText(MciCodesIds.MsgSelNum, (vc.getView(MciCodesIds.MsgList).getData() + 1).toString());
|
||||
self.setViewText(MciCodesIds.MsgTotal, self.messageList.length.toString());
|
||||
|
||||
|
|
20
mods/nua.js
20
mods/nua.js
|
@ -5,7 +5,7 @@ var user = require('../core/user.js');
|
|||
var theme = require('../core/theme.js');
|
||||
var login = require('../core/system_menu_method.js').login;
|
||||
var Config = require('../core/config.js').config;
|
||||
var getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea;
|
||||
var messageArea = require('../core/message_area.js');
|
||||
|
||||
var async = require('async');
|
||||
|
||||
|
@ -65,6 +65,16 @@ function NewUserAppModule(options) {
|
|||
|
||||
newUser.username = formData.value.username;
|
||||
|
||||
//
|
||||
// We have to disable ACS checks for initial default areas as the user is not yet ready
|
||||
//
|
||||
var confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
|
||||
var areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
|
||||
|
||||
// can't store undefined!
|
||||
confTag = confTag || '';
|
||||
areaTag = areaTag || '';
|
||||
|
||||
newUser.properties = {
|
||||
real_name : formData.value.realName,
|
||||
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
|
||||
|
@ -75,14 +85,12 @@ function NewUserAppModule(options) {
|
|||
web_address : formData.value.web,
|
||||
account_created : new Date().toISOString(),
|
||||
|
||||
message_area_name : getDefaultMessageArea().name,
|
||||
message_conf_tag : confTag,
|
||||
message_area_tag : areaTag,
|
||||
|
||||
term_height : self.client.term.termHeight,
|
||||
term_width : self.client.term.termWidth,
|
||||
|
||||
// :TODO: This is set in User.create() -- proabbly don't need it here:
|
||||
//account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active,
|
||||
|
||||
// :TODO: Other defaults
|
||||
// :TODO: should probably have a place to create defaults/etc.
|
||||
};
|
||||
|
@ -93,7 +101,7 @@ function NewUserAppModule(options) {
|
|||
newUser.properties.theme_id = Config.defaults.theme;
|
||||
}
|
||||
|
||||
// :TODO: .create() should also validate email uniqueness!
|
||||
// :TODO: User.create() should validate email uniqueness!
|
||||
newUser.create( { password : formData.value.password }, function created(err) {
|
||||
if(err) {
|
||||
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
|
||||
|
|
|
@ -172,8 +172,8 @@
|
|||
|
||||
messageAreaChangeCurrentArea: {
|
||||
config: {
|
||||
listFormat: "|00|15{index} |07- |03{desc}"
|
||||
focusListFormat: "|00|19|15{index} - {desc}"
|
||||
listFormat: "|00|15{index} |07- |03{name}"
|
||||
focusListFormat: "|00|19|15{index} - {name}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
|
@ -310,6 +310,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
newScanMessageList: {
|
||||
config: {
|
||||
listFormat: "|00|15 {msgNum:<5.5}|03{subj:<29.29} |15{from:<20.20} {ts}"
|
||||
focusListFormat: "|00|19> |15{msgNum:<5.5}{subj:<29.29} {from:<20.20} {ts}"
|
||||
dateTimeFormat: ddd MMM Do
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
|
||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
var userDb = require('../core/database.js').dbs.user;
|
||||
//var userDb = require('../core/database.js').dbs.user;
|
||||
var getUserList = require('../core/user.js').getUserList;
|
||||
var ViewController = require('../core/view_controller.js').ViewController;
|
||||
|
||||
|
|
|
@ -84,7 +84,6 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
|
|||
return listFormat.format(oe);
|
||||
}));
|
||||
|
||||
// :TODO: This is a hack until pipe codes are better implemented
|
||||
onlineListView.focusItems = onlineListView.items;
|
||||
|
||||
onlineListView.redraw();
|
||||
|
|
14
oputil.js
14
oputil.js
|
@ -13,7 +13,7 @@ var assert = require('assert');
|
|||
|
||||
var argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
var ExitCodes = {
|
||||
const ExitCodes = {
|
||||
SUCCESS : 0,
|
||||
ERROR : -1,
|
||||
BAD_COMMAND : -2,
|
||||
|
@ -28,9 +28,13 @@ function printUsage(command) {
|
|||
usage =
|
||||
'usage: oputil.js [--version] [--help]\n' +
|
||||
' <command> [<args>]' +
|
||||
'\n' +
|
||||
'\n\n' +
|
||||
'global args:\n' +
|
||||
' --config PATH : specify config path';
|
||||
' --config PATH : specify config path' +
|
||||
'\n\n' +
|
||||
'commands:\n' +
|
||||
' user : User utilities' +
|
||||
'\n';
|
||||
break;
|
||||
|
||||
case 'user' :
|
||||
|
@ -47,7 +51,7 @@ function printUsage(command) {
|
|||
}
|
||||
|
||||
function initConfig(cb) {
|
||||
var configPath = argv.config ? argv.config : config.getDefaultPath();
|
||||
const configPath = argv.config ? argv.config : config.getDefaultPath();
|
||||
|
||||
config.init(configPath, cb);
|
||||
}
|
||||
|
@ -88,7 +92,7 @@ function handleUserCommand() {
|
|||
assert(_.isNumber(userId));
|
||||
assert(userId > 0);
|
||||
|
||||
var u = new user.User();
|
||||
let u = new user.User();
|
||||
u.userId = userId;
|
||||
|
||||
u.setNewAuthCredentials(argv.password, function credsSet(err) {
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
"async": "^1.5.1",
|
||||
"binary": "0.3.x",
|
||||
"buffers": "0.1.x",
|
||||
"bunyan": "1.5.x",
|
||||
"bunyan": "^1.7.1",
|
||||
"gaze": "^0.5.2",
|
||||
"hjson": "1.7.x",
|
||||
"iconv-lite": "^0.4.13",
|
||||
"later": "1.2.0",
|
||||
"lodash": "^3.10.1",
|
||||
"minimist": "1.2.x",
|
||||
"mkdirp": "0.5.x",
|
||||
|
@ -28,7 +29,8 @@
|
|||
"ptyw.js": "^0.3.7",
|
||||
"sqlite3": "^3.1.1",
|
||||
"ssh2": "^0.4.13",
|
||||
"string-format": "davidchambers/string-format#mini-language"
|
||||
"string-format": "davidchambers/string-format#mini-language",
|
||||
"temp": "^0.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.2"
|
||||
|
|
Loading…
Reference in New Issue