Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs
This commit is contained in:
commit
6c108cb6b8
|
@ -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**
|
|
@ -19,12 +19,12 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
|
|||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
|
||||
* Door support including common dropfile formats and legacy DOS doors (See [Doors](docs/doors.md))
|
||||
* [Bunyan](https://github.com/trentm/node-bunyan) logging
|
||||
* FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export
|
||||
|
||||
## In the Works
|
||||
* Lots of code cleanup, ES6+ usage, and **documentation**!
|
||||
* FTN import & export
|
||||
* More ES6+ usage, and **documentation**!
|
||||
* File areas
|
||||
* Full access checking framework (ACS)
|
||||
* ACS support for more areas
|
||||
* SysOp dashboard (ye ol' WFC)
|
||||
* Missing functionality such as searching, pipe code support in message areas, etc.
|
||||
* String localization
|
||||
|
@ -65,7 +65,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
|
|||
## License
|
||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
||||
|
||||
Copyright (c) 2015, Bryan D. Ashby
|
||||
Copyright (c) 2015-2016, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -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
444
core/ftn_util.js
444
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 iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Convert a FTN style DateTime string to a Date object
|
||||
//
|
||||
// :TODO: Name the next couple methods better - for FTN *packets*
|
||||
function getDateFromFtnDateTime(dateTime) {
|
||||
//
|
||||
// Examples seen in the wild (Working):
|
||||
|
@ -44,45 +67,146 @@ function getDateFromFtnDateTime(dateTime) {
|
|||
// "Tue 01 Jan 80 00:00"
|
||||
// "27 Feb 15 00:00:03"
|
||||
//
|
||||
return (new Date(Date.parse(dateTime))).toISOString();
|
||||
// :TODO: Use moment.js here
|
||||
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
|
||||
// return (new Date(Date.parse(dateTime))).toISOString();
|
||||
}
|
||||
|
||||
function getFormattedFTNAddress(address, dimensions) {
|
||||
//var addr = util.format('%d:%d', address.zone, address.net);
|
||||
var addr = '{0}:{1}'.format(address.zone, address.net);
|
||||
switch(dimensions) {
|
||||
case 2 :
|
||||
case '2D' :
|
||||
// above
|
||||
break;
|
||||
|
||||
case 3 :
|
||||
case '3D' :
|
||||
addr += '/{0}'.format(address.node);
|
||||
break;
|
||||
|
||||
case 4 :
|
||||
case '4D':
|
||||
addr += '.{0}'.format(address.point || 0); // missing and 0 are equiv for point
|
||||
break;
|
||||
|
||||
case 5 :
|
||||
case '5D' :
|
||||
if(address.domain) {
|
||||
addr += '@{0}'.format(address.domain);
|
||||
}
|
||||
break;
|
||||
function getDateTimeString(m) {
|
||||
//
|
||||
// From http://ftsc.org/docs/fts-0001.016:
|
||||
// DateTime = (* a character string 20 characters long *)
|
||||
// (* 01 Jan 86 02:34:56 *)
|
||||
// DayOfMonth " " Month " " Year " "
|
||||
// " " HH ":" MM ":" SS
|
||||
// Null
|
||||
//
|
||||
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
||||
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
||||
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
|
||||
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
|
||||
// HH = "00" | .. | "23"
|
||||
// MM = "00" | .. | "59"
|
||||
// SS = "00" | .. | "59"
|
||||
//
|
||||
if(!moment.isMoment(m)) {
|
||||
m = moment(m);
|
||||
}
|
||||
|
||||
return addr;
|
||||
return m.format('DD MMM YY HH:mm:ss');
|
||||
}
|
||||
|
||||
function getFtnMessageSerialNumber(messageId) {
|
||||
return ((Math.floor((Date.now() - Date.UTC(2015, 1, 1)) / 1000) + messageId)).toString(16);
|
||||
//
|
||||
// Create a v5 named UUID given a message ID ("MSGID") and
|
||||
// FTN area tag ("AREA").
|
||||
//
|
||||
// This is similar to CrashMail
|
||||
// See https://github.com/larsks/crashmail/blob/master/crashmail/dupe.c
|
||||
//
|
||||
function createMessageUuid(ftnMsgId, ftnArea) {
|
||||
assert(_.isString(ftnMsgId));
|
||||
assert(_.isString(ftnArea));
|
||||
|
||||
ftnMsgId = iconv.encode(ftnMsgId, 'CP437');
|
||||
ftnArea = iconv.encode(ftnArea.toUpperCase(), 'CP437');
|
||||
|
||||
return uuid.unparse(createNamedUUID(ENIGMA_FTN_MSGID_NAMESPACE, Buffer.concat( [ ftnMsgId, ftnArea ] )));
|
||||
};
|
||||
|
||||
//
|
||||
// Create a v5 named UUID given a FTN area tag ("AREA"),
|
||||
// create/modified date, subject, and message body
|
||||
//
|
||||
// This method should be used as a backup for when a MSGID is
|
||||
// not available in which createMessageUuid() above should be
|
||||
// used instead.
|
||||
//
|
||||
function createMessageUuidAlternate(ftnArea, modTimestamp, subject, msgBody) {
|
||||
assert(_.isString(ftnArea));
|
||||
assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp));
|
||||
assert(_.isString(subject));
|
||||
assert(_.isString(msgBody));
|
||||
|
||||
ftnArea = iconv.encode(ftnArea.toUpperCase(), 'CP437');
|
||||
modTimestamp = iconv.encode(getDateTimeString(modTimestamp), 'CP437');
|
||||
subject = iconv.encode(subject.toUpperCase().trim(), 'CP437');
|
||||
msgBody = iconv.encode(msgBody.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437');
|
||||
|
||||
return uuid.unparse(createNamedUUID(ENIGMA_FTN_MSGID_NAMESPACE, Buffer.concat( [ ftnArea, modTimestamp, subject, msgBody ] )));
|
||||
}
|
||||
|
||||
function getFTNMessageID(messageId, areaId) {
|
||||
return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getFTNMessageSerialNumber(messageId)
|
||||
function getMessageSerialNumber(messageId) {
|
||||
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
|
||||
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
|
||||
return `00000000${hash}`.substr(-8);
|
||||
}
|
||||
|
||||
//
|
||||
// Return a FTS-0009.001 compliant MSGID value given a message
|
||||
// See http://ftsc.org/docs/fts-0009.001
|
||||
//
|
||||
// "A MSGID line consists of the string "^AMSGID:" (where ^A is a
|
||||
// control-A (hex 01) and the double-quotes are not part of the
|
||||
// string), followed by a space, the address of the originating
|
||||
// system, and a serial number unique to that message on the
|
||||
// originating system, i.e.:
|
||||
//
|
||||
// ^AMSGID: origaddr serialno
|
||||
//
|
||||
// The originating address should be specified in a form that
|
||||
// constitutes a valid return address for the originating network.
|
||||
// If the originating address is enclosed in double-quotes, the
|
||||
// entire string between the beginning and ending double-quotes is
|
||||
// considered to be the orginating address. A double-quote character
|
||||
// within a quoted address is represented by by two consecutive
|
||||
// double-quote characters. The serial number may be any eight
|
||||
// character hexadecimal number, as long as it is unique - no two
|
||||
// messages from a given system may have the same serial number
|
||||
// within a three years. The manner in which this serial number is
|
||||
// generated is left to the implementor."
|
||||
//
|
||||
//
|
||||
// Examples & Implementations
|
||||
//
|
||||
// Synchronet: <msgNum>.<conf+area>@<ftnAddr> <serial>
|
||||
// 2606.agora-agn_tst@46:1/142 19609217
|
||||
//
|
||||
// Mystic: <ftnAddress> <serial>
|
||||
// 46:3/102 46686263
|
||||
//
|
||||
// ENiGMA½: <messageId>.<areaTag>@<5dFtnAddress> <serial>
|
||||
//
|
||||
function getMessageIdentifier(message, address) {
|
||||
const addrStr = new Address(address).toString('5D');
|
||||
return `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`;
|
||||
}
|
||||
|
||||
//
|
||||
// Return a FSC-0046.005 Product Identifier or "PID"
|
||||
// http://ftsc.org/docs/fsc-0046.005
|
||||
//
|
||||
// Note that we use a variant on the spec for <serial>
|
||||
// in which (<os>; <arch>; <nodeVer>) is used instead
|
||||
//
|
||||
function getProductIdentifier() {
|
||||
const version = packageJson.version
|
||||
.replace(/\-/g, '.')
|
||||
.replace(/alpha/,'a')
|
||||
.replace(/beta/,'b');
|
||||
|
||||
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||
|
||||
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||
}
|
||||
|
||||
//
|
||||
// Return a FRL-1004 style time zone offset for a
|
||||
// 'TZUTC' kludge line
|
||||
//
|
||||
// http://ftsc.org/docs/frl-1004.002
|
||||
//
|
||||
function getUTCTimeZoneOffset() {
|
||||
return moment().format('ZZ').replace(/\+/, '');
|
||||
}
|
||||
|
||||
// Get a FSC-0032 style quote prefixes
|
||||
|
@ -91,25 +215,221 @@ function getQuotePrefix(name) {
|
|||
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
|
||||
}
|
||||
|
||||
//
|
||||
// Return a FTS-0004 Origin line
|
||||
// http://ftsc.org/docs/fts-0004.001
|
||||
//
|
||||
function getOrigin(address) {
|
||||
const origin = _.has(Config.messageNetworks.originName) ?
|
||||
Config.messageNetworks.originName :
|
||||
Config.general.boardName;
|
||||
|
||||
const addrStr = new Address(address).toString('5D');
|
||||
return ` * Origin: ${origin} (${addrStr})`;
|
||||
}
|
||||
|
||||
function getTearLine() {
|
||||
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||
}
|
||||
|
||||
//
|
||||
// Specs:
|
||||
// * http://ftsc.org/docs/fts-0009.001
|
||||
// *
|
||||
// Return a FRL-1005.001 "Via" line
|
||||
// http://ftsc.org/docs/frl-1005.001
|
||||
//
|
||||
function getFtnMsgIdKludgeLine(origAddress, messageId) {
|
||||
if(_.isObject(origAddress)) {
|
||||
origAddress = getFormattedFTNAddress(origAddress, '5D');
|
||||
function getVia(address) {
|
||||
/*
|
||||
FRL-1005.001 states teh following format:
|
||||
|
||||
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
|
||||
<Program Name> <Version> [Serial Number]<CR>
|
||||
*/
|
||||
const addrStr = new Address(address).toString('5D');
|
||||
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
|
||||
|
||||
const version = packageJson.version
|
||||
.replace(/\-/g, '.')
|
||||
.replace(/alpha/,'a')
|
||||
.replace(/beta/,'b');
|
||||
|
||||
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
|
||||
}
|
||||
|
||||
function getAbbreviatedNetNodeList(netNodes) {
|
||||
let abbrList = '';
|
||||
let currNet;
|
||||
netNodes.forEach(netNode => {
|
||||
if(_.isString(netNode)) {
|
||||
netNode = Address.fromString(netNode);
|
||||
}
|
||||
if(currNet !== netNode.net) {
|
||||
abbrList += `${netNode.net}/`;
|
||||
currNet = netNode.net;
|
||||
}
|
||||
abbrList += `${netNode.node} `;
|
||||
});
|
||||
|
||||
return abbrList.trim(); // remove trailing space
|
||||
}
|
||||
|
||||
//
|
||||
// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH
|
||||
//
|
||||
function parseAbbreviatedNetNodeList(netNodes) {
|
||||
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
|
||||
let net;
|
||||
let m;
|
||||
let results = [];
|
||||
while(null !== (m = re.exec(netNodes))) {
|
||||
if(m[1] && m[2]) {
|
||||
net = parseInt(m[1]);
|
||||
results.push(new Address( { net : net, node : parseInt(m[2]) } ));
|
||||
} else if(net) {
|
||||
results.push(new Address( { net : net, node : parseInt(m[3]) } ));
|
||||
}
|
||||
}
|
||||
|
||||
return '\x01MSGID: ' + origAddress + ' ' + getFtnMessageSerialNumber(messageId);
|
||||
return results;
|
||||
}
|
||||
|
||||
//
|
||||
// Return a FTS-0004.001 SEEN-BY entry(s) that include
|
||||
// all pre-existing SEEN-BY entries with the addition
|
||||
// of |additions|.
|
||||
//
|
||||
// See http://ftsc.org/docs/fts-0004.001
|
||||
// and notes at http://ftsc.org/docs/fsc-0043.002.
|
||||
//
|
||||
// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm
|
||||
//
|
||||
// This method returns an sorted array of values, but
|
||||
// not the "SEEN-BY" prefix itself
|
||||
//
|
||||
function getUpdatedSeenByEntries(existingEntries, additions) {
|
||||
/*
|
||||
From FTS-0004:
|
||||
|
||||
"There can be many seen-by lines at the end of Conference
|
||||
Mail messages, and they are the real "meat" of the control
|
||||
information. They are used to determine the systems to
|
||||
receive the exported messages. The format of the line is:
|
||||
|
||||
SEEN-BY: 132/101 113 136/601 1014/1
|
||||
|
||||
The net/node numbers correspond to the net/node numbers of
|
||||
the systems having already received the message. In this way
|
||||
a message is never sent to a system twice. In a conference
|
||||
with many participants the number of seen-by lines can be
|
||||
very large. This line is added if it is not already a part
|
||||
of the message, or added to if it already exists, each time
|
||||
a message is exported to other systems. This is a REQUIRED
|
||||
field, and Conference Mail will not function correctly if
|
||||
this field is not put in place by other Echomail compatible
|
||||
programs."
|
||||
*/
|
||||
existingEntries = existingEntries || [];
|
||||
if(!_.isArray(existingEntries)) {
|
||||
existingEntries = [ existingEntries ];
|
||||
}
|
||||
|
||||
if(!_.isString(additions)) {
|
||||
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
|
||||
}
|
||||
|
||||
additions = additions.sort(Address.getComparator());
|
||||
|
||||
function getFTNOriginLine() {
|
||||
//
|
||||
// Specs:
|
||||
// http://ftsc.org/docs/fts-0004.001
|
||||
// For now, we'll just append a new SEEN-BY entry
|
||||
//
|
||||
return ' * Origin: ' + Config.general.boardName + '(' + getFidoNetAddress() + ')';
|
||||
// :TODO: we should at least try and update what is already there in a smart way
|
||||
existingEntries.push(getAbbreviatedNetNodeList(additions));
|
||||
return existingEntries;
|
||||
}
|
||||
|
||||
function getUpdatedPathEntries(existingEntries, localAddress) {
|
||||
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
|
||||
|
||||
existingEntries = existingEntries || [];
|
||||
if(!_.isArray(existingEntries)) {
|
||||
existingEntries = [ existingEntries ];
|
||||
}
|
||||
|
||||
existingEntries.push(getAbbreviatedNetNodeList(
|
||||
parseAbbreviatedNetNodeList(localAddress)));
|
||||
|
||||
return existingEntries;
|
||||
}
|
||||
|
||||
//
|
||||
// Return FTS-5000.001 "CHRS" value
|
||||
// http://ftsc.org/docs/fts-5003.001
|
||||
//
|
||||
const ENCODING_TO_FTS_5003_001_CHARS = {
|
||||
// level 1 - generally should not be used
|
||||
ascii : [ 'ASCII', 1 ],
|
||||
'us-ascii' : [ 'ASCII', 1 ],
|
||||
|
||||
// level 2 - 8 bit, ASCII based
|
||||
cp437 : [ 'CP437', 2 ],
|
||||
cp850 : [ 'CP850', 2 ],
|
||||
|
||||
// level 3 - reserved
|
||||
|
||||
// level 4
|
||||
utf8 : [ 'UTF-8', 4 ],
|
||||
'utf-8' : [ 'UTF-8', 4 ],
|
||||
};
|
||||
|
||||
|
||||
function getCharacterSetIdentifierByEncoding(encodingName) {
|
||||
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
|
||||
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
|
||||
}
|
||||
|
||||
function getEncodingFromCharacterSetIdentifier(chrs) {
|
||||
const ident = chrs.split(' ')[0].toUpperCase();
|
||||
|
||||
// :TODO: fill in the rest!!!
|
||||
return {
|
||||
// level 1
|
||||
'ASCII' : 'iso-646-1',
|
||||
'DUTCH' : 'iso-646',
|
||||
'FINNISH' : 'iso-646-10',
|
||||
'FRENCH' : 'iso-646',
|
||||
'CANADIAN' : 'iso-646',
|
||||
'GERMAN' : 'iso-646',
|
||||
'ITALIAN' : 'iso-646',
|
||||
'NORWEIG' : 'iso-646',
|
||||
'PORTU' : 'iso-646',
|
||||
'SPANISH' : 'iso-656',
|
||||
'SWEDISH' : 'iso-646-10',
|
||||
'SWISS' : 'iso-646',
|
||||
'UK' : 'iso-646',
|
||||
'ISO-10' : 'iso-646-10',
|
||||
|
||||
// level 2
|
||||
'CP437' : 'cp437',
|
||||
'CP850' : 'cp850',
|
||||
'CP852' : 'cp852',
|
||||
'CP866' : 'cp866',
|
||||
'CP848' : 'cp848',
|
||||
'CP1250' : 'cp1250',
|
||||
'CP1251' : 'cp1251',
|
||||
'CP1252' : 'cp1252',
|
||||
'CP10000' : 'macroman',
|
||||
'LATIN-1' : 'iso-8859-1',
|
||||
'LATIN-2' : 'iso-8859-2',
|
||||
'LATIN-5' : 'iso-8859-9',
|
||||
'LATIN-9' : 'iso-8859-15',
|
||||
|
||||
// level 4
|
||||
'UTF-8' : 'utf8',
|
||||
|
||||
// deprecated stuff
|
||||
'IBMPC' : 'cp1250', // :TODO: validate
|
||||
'+7_FIDO' : 'cp866',
|
||||
'+7' : 'cp866',
|
||||
'MAC' : 'macroman', // :TODO: validate
|
||||
|
||||
}[ident];
|
||||
}
|
|
@ -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,290 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var msgDb = require('./database.js').dbs.message;
|
||||
var Config = require('./config.js').config;
|
||||
var Message = require('./message.js');
|
||||
var Log = require('./logger.js').log;
|
||||
let msgDb = require('./database.js').dbs.message;
|
||||
let Config = require('./config.js').config;
|
||||
let Message = require('./message.js');
|
||||
let Log = require('./logger.js').log;
|
||||
let checkAcs = require('./acs_util.js').checkAcs;
|
||||
let msgNetRecord = require('./msg_network.js').recordMessage;
|
||||
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
let async = require('async');
|
||||
let _ = require('lodash');
|
||||
let assert = require('assert');
|
||||
|
||||
exports.getAvailableMessageAreas = getAvailableMessageAreas;
|
||||
exports.getDefaultMessageArea = getDefaultMessageArea;
|
||||
exports.getMessageAreaByName = getMessageAreaByName;
|
||||
exports.getAvailableMessageConferences = getAvailableMessageConferences;
|
||||
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
|
||||
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
|
||||
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
|
||||
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
|
||||
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
|
||||
exports.getMessageConferenceByTag = getMessageConferenceByTag;
|
||||
exports.getMessageAreaByTag = getMessageAreaByTag;
|
||||
exports.changeMessageConference = changeMessageConference;
|
||||
exports.changeMessageArea = changeMessageArea;
|
||||
exports.getMessageListForArea = getMessageListForArea;
|
||||
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
||||
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
||||
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
|
||||
exports.persistMessage = persistMessage;
|
||||
|
||||
function getAvailableMessageAreas(options) {
|
||||
// example: [ { "name" : "local_music", "desc" : "Music Discussion", "groups" : ["somegroup"] }, ... ]
|
||||
options = options || {};
|
||||
const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]';
|
||||
const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]';
|
||||
|
||||
var areas = Config.messages.areas;
|
||||
var avail = [];
|
||||
for(var i = 0; i < areas.length; ++i) {
|
||||
if(true !== options.includePrivate &&
|
||||
Message.WellKnownAreaNames.Private === areas[i].name)
|
||||
{
|
||||
continue;
|
||||
const AREA_ACS_DEFAULT = {
|
||||
read : CONF_AREA_RW_ACS_DEFAULT,
|
||||
write : CONF_AREA_RW_ACS_DEFAULT,
|
||||
manage : AREA_MANAGE_ACS_DEFAULT,
|
||||
};
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
return avail;
|
||||
const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
return !checkAcs(client, readAcs);
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultMessageArea() {
|
||||
//
|
||||
// Return first non-private/etc. area name. This will be from config.hjson
|
||||
//
|
||||
return getAvailableMessageAreas()[0];
|
||||
/*
|
||||
var avail = getAvailableMessageAreas();
|
||||
for(var i = 0; i < avail.length; ++i) {
|
||||
if(Message.WellKnownAreaNames.Private !== avail[i].name) {
|
||||
return avail[i];
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function getMessageAreaByName(areaName) {
|
||||
areaName = areaName.toLowerCase();
|
||||
|
||||
var availAreas = getAvailableMessageAreas( { includePrivate : true } );
|
||||
var index = _.findIndex(availAreas, function pred(an) {
|
||||
return an.name == areaName;
|
||||
function getSortedAvailMessageConferences(client, options) {
|
||||
const sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
||||
return {
|
||||
confTag : k,
|
||||
conf : v,
|
||||
};
|
||||
});
|
||||
|
||||
if(index > -1) {
|
||||
return availAreas[index];
|
||||
sorted.sort((a, b) => {
|
||||
const keyA = a.conf.sort ? a.conf.sort.toString() : a.conf.name;
|
||||
const keyB = b.conf.sort ? b.conf.sort.toString() : b.conf.name;
|
||||
return keyA.localeCompare(keyB);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// 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 = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
|
||||
return {
|
||||
areaTag : k,
|
||||
area : v,
|
||||
}
|
||||
});
|
||||
|
||||
areas.sort((a, b) => {
|
||||
const keyA = a.area.sort ? a.area.sort.toString() : a.area.name;
|
||||
const keyB = b.area.sort ? b.area.sort.toString() : b.area.name;
|
||||
return keyA.localeCompare(keyB);
|
||||
});
|
||||
|
||||
return areas;
|
||||
}
|
||||
|
||||
function getDefaultMessageConferenceTag(client, disableAcsCheck) {
|
||||
//
|
||||
// Find the first conference marked 'default'. If found,
|
||||
// inspect |client| against *read* ACS using defaults if not
|
||||
// specified.
|
||||
//
|
||||
// If the above fails, just go down the list until we get one
|
||||
// that passes.
|
||||
//
|
||||
// It's possible that we end up with nothing here!
|
||||
//
|
||||
// Note that built in 'system_internal' is always ommited here
|
||||
//
|
||||
let defaultConf = _.findKey(Config.messageConferences, o => o.default);
|
||||
if(defaultConf) {
|
||||
const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(true === disableAcsCheck || checkAcs(client, acs)) {
|
||||
return defaultConf;
|
||||
}
|
||||
}
|
||||
|
||||
// just use anything we can
|
||||
defaultConf = _.findKey(Config.messageConferences, (o, k) => {
|
||||
const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs));
|
||||
});
|
||||
|
||||
return defaultConf;
|
||||
}
|
||||
|
||||
function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
|
||||
//
|
||||
// Similar to finding the default conference:
|
||||
// Find the first entry marked 'default', if any. If found, check | client| against
|
||||
// *read* ACS. If this fails, just find the first one we can that passes checks.
|
||||
//
|
||||
// It's possible that we end up with nothing!
|
||||
//
|
||||
confTag = confTag || getDefaultMessageConferenceTag(client);
|
||||
|
||||
if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) {
|
||||
const areaPool = Config.messageConferences[confTag].areas;
|
||||
let defaultArea = _.findKey(areaPool, o => o.default);
|
||||
if(defaultArea) {
|
||||
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||
if(true === disableAcsCheck || checkAcs(client, readAcs)) {
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
defaultArea = _.findKey(areaPool, (o, k) => {
|
||||
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||
return (true === disableAcsCheck || checkAcs(client, readAcs));
|
||||
});
|
||||
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageConferenceByTag(confTag) {
|
||||
return Config.messageConferences[confTag];
|
||||
}
|
||||
|
||||
function getMessageAreaByTag(areaTag, optionalConfTag) {
|
||||
const confs = Config.messageConferences;
|
||||
|
||||
if(_.isString(optionalConfTag)) {
|
||||
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
|
||||
return confs[optionalConfTag].areas[areaTag];
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// No confTag to work with - we'll have to search through them all
|
||||
//
|
||||
var area;
|
||||
_.forEach(confs, (v, k) => {
|
||||
if(_.has(v, [ 'areas', areaTag ])) {
|
||||
area = v.areas[areaTag];
|
||||
return false; // stop iteration
|
||||
}
|
||||
});
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
|
||||
function changeMessageConference(client, confTag, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function getConf(callback) {
|
||||
const conf = getMessageConferenceByTag(confTag);
|
||||
|
||||
if(conf) {
|
||||
callback(null, conf);
|
||||
} else {
|
||||
callback(new Error('Invalid message conference tag'));
|
||||
}
|
||||
},
|
||||
function getDefaultAreaInConf(conf, callback) {
|
||||
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
||||
const area = getMessageAreaByTag(areaTag, confTag);
|
||||
|
||||
if(area) {
|
||||
callback(null, conf, { areaTag : areaTag, area : area } );
|
||||
} else {
|
||||
callback(new Error('No available areas for this user in conference'));
|
||||
}
|
||||
},
|
||||
function validateAccess(conf, areaInfo, callback) {
|
||||
const confAcs = conf.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||
|
||||
if(!checkAcs(client, confAcs)) {
|
||||
callback(new Error('User does not have access to this conference'));
|
||||
} else {
|
||||
const areaAcs = _.has(areaInfo, 'area.acs.read') ? areaInfo.area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(!checkAcs(client, areaAcs)) {
|
||||
callback(new Error('User does not have access to default area in this conference'));
|
||||
} else {
|
||||
callback(null, conf, areaInfo);
|
||||
}
|
||||
}
|
||||
},
|
||||
function changeConferenceAndArea(conf, areaInfo, callback) {
|
||||
const newProps = {
|
||||
message_conf_tag : confTag,
|
||||
message_area_tag : areaInfo.areaTag,
|
||||
};
|
||||
client.user.persistProperties(newProps, err => {
|
||||
callback(err, conf, areaInfo);
|
||||
});
|
||||
},
|
||||
],
|
||||
function complete(err, conf, areaInfo) {
|
||||
if(!err) {
|
||||
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
|
||||
} else {
|
||||
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
|
||||
}
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function changeMessageArea(client, areaTag, cb) {
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getArea(callback) {
|
||||
var area = getMessageAreaByName(areaName);
|
||||
const area = getMessageAreaByTag(areaTag);
|
||||
|
||||
if(area) {
|
||||
callback(null, area);
|
||||
} else {
|
||||
callback(new Error('Invalid message area'));
|
||||
callback(new Error('Invalid message area tag'));
|
||||
}
|
||||
},
|
||||
function validateAccess(area, callback) {
|
||||
if(_.isArray(area.groups) && !
|
||||
client.user.isGroupMember(area.groups))
|
||||
{
|
||||
//
|
||||
// Need at least *read* to access the area
|
||||
//
|
||||
const readAcs = _.has(area, 'acs.read') ? area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||
if(!checkAcs(client, readAcs)) {
|
||||
callback(new Error('User does not have access to this area'));
|
||||
} else {
|
||||
callback(null, area);
|
||||
}
|
||||
},
|
||||
function changeArea(area, callback) {
|
||||
client.user.persistProperty('message_area_name', area.name, function persisted(err) {
|
||||
client.user.persistProperty('message_area_tag', areaTag, function persisted(err) {
|
||||
callback(err, area);
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err, area) {
|
||||
if(!err) {
|
||||
client.log.info( area, 'Current message area changed');
|
||||
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
|
||||
} else {
|
||||
client.log.warn( { area : area, error : err.message }, 'Could not change message area');
|
||||
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
||||
}
|
||||
|
||||
cb(err);
|
||||
|
@ -119,9 +305,9 @@ function getMessageFromRow(row) {
|
|||
};
|
||||
}
|
||||
|
||||
function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
||||
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
||||
//
|
||||
// If |areaName| is Message.WellKnownAreaNames.Private,
|
||||
// If |areaTag| is Message.WellKnownAreaTags.Private,
|
||||
// only messages addressed to |userId| should be returned.
|
||||
//
|
||||
// Only messages > lastMessageId should be returned
|
||||
|
@ -131,7 +317,7 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
async.waterfall(
|
||||
[
|
||||
function getLastMessageId(callback) {
|
||||
getMessageAreaLastReadId(userId, areaName, function fetched(err, lastMessageId) {
|
||||
getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) {
|
||||
callback(null, lastMessageId || 0); // note: willingly ignoring any errors here!
|
||||
});
|
||||
},
|
||||
|
@ -139,9 +325,9 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
var sql =
|
||||
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
||||
'FROM message ' +
|
||||
'WHERE area_name="' + areaName + '" AND message_id > ' + lastMessageId;
|
||||
'WHERE area_tag ="' + areaTag + '" AND message_id > ' + lastMessageId;
|
||||
|
||||
if(Message.WellKnownAreaNames.Private === areaName) {
|
||||
if(Message.WellKnownAreaTags.Private === areaTag) {
|
||||
sql +=
|
||||
' AND message_id in (' +
|
||||
'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System +
|
||||
|
@ -150,8 +336,6 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
|
||||
sql += ' ORDER BY message_id;';
|
||||
|
||||
console.log(sql)
|
||||
|
||||
msgDb.each(sql, function msgRow(err, row) {
|
||||
if(!err) {
|
||||
msgList.push(getMessageFromRow(row));
|
||||
|
@ -160,18 +344,17 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
|||
}
|
||||
],
|
||||
function complete(err) {
|
||||
console.log(msgList)
|
||||
cb(err, msgList);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getMessageListForArea(options, areaName, cb) {
|
||||
function getMessageListForArea(options, areaTag, cb) {
|
||||
//
|
||||
// options.client (required)
|
||||
//
|
||||
|
||||
options.client.log.debug( { areaName : areaName }, 'Fetching available messages');
|
||||
options.client.log.debug( { areaTag : areaTag }, 'Fetching available messages');
|
||||
|
||||
assert(_.isObject(options.client));
|
||||
|
||||
|
@ -193,9 +376,9 @@ function getMessageListForArea(options, areaName, cb) {
|
|||
msgDb.each(
|
||||
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
||||
'FROM message ' +
|
||||
'WHERE area_name=? ' +
|
||||
'WHERE area_tag = ? ' +
|
||||
'ORDER BY message_id;',
|
||||
[ areaName.toLowerCase() ],
|
||||
[ areaTag.toLowerCase() ],
|
||||
function msgRow(err, row) {
|
||||
if(!err) {
|
||||
msgList.push(getMessageFromRow(row));
|
||||
|
@ -214,24 +397,24 @@ function getMessageListForArea(options, areaName, cb) {
|
|||
);
|
||||
}
|
||||
|
||||
function getMessageAreaLastReadId(userId, areaName, cb) {
|
||||
function getMessageAreaLastReadId(userId, areaTag, cb) {
|
||||
msgDb.get(
|
||||
'SELECT message_id ' +
|
||||
'FROM user_message_area_last_read ' +
|
||||
'WHERE user_id = ? AND area_name = ?;',
|
||||
[ userId, areaName ],
|
||||
'WHERE user_id = ? AND area_tag = ?;',
|
||||
[ userId, areaTag ],
|
||||
function complete(err, row) {
|
||||
cb(err, row ? row.message_id : 0);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
|
||||
function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) {
|
||||
// :TODO: likely a better way to do this...
|
||||
async.waterfall(
|
||||
[
|
||||
function getCurrent(callback) {
|
||||
getMessageAreaLastReadId(userId, areaName, function result(err, lastId) {
|
||||
getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
|
||||
lastId = lastId || 0;
|
||||
callback(null, lastId); // ignore errors as we default to 0
|
||||
});
|
||||
|
@ -239,27 +422,45 @@ function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
|
|||
function update(lastId, callback) {
|
||||
if(messageId > lastId) {
|
||||
msgDb.run(
|
||||
'REPLACE INTO user_message_area_last_read (user_id, area_name, message_id) ' +
|
||||
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
|
||||
'VALUES (?, ?, ?);',
|
||||
[ userId, areaName, messageId ],
|
||||
callback
|
||||
[ userId, areaTag, messageId ],
|
||||
function written(err) {
|
||||
callback(err, true); // true=didUpdate
|
||||
}
|
||||
);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
function complete(err, didUpdate) {
|
||||
if(err) {
|
||||
Log.debug(
|
||||
{ error : err.toString(), userId : userId, areaName : areaName, messageId : messageId },
|
||||
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId },
|
||||
'Failed updating area last read ID');
|
||||
} else {
|
||||
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.
|
|
@ -1,5 +1,5 @@
|
|||
# Doors
|
||||
ENiGMA½ supports a variety of methods for interacting with doors not limited to:
|
||||
ENiGMA½ supports a variety of methods for interacting with doors — not limited to:
|
||||
* `abracadabra` module: Standard in/out (stdio) capture or temporary socket server that can be used with [DOSEMU](http://www.dosemu.org/), [DOSBox](http://www.dosbox.com/), [QEMU](http://wiki.qemu.org/Main_Page), etc.
|
||||
* `bbs_link` module for interaction with [BBSLink](http://www.bbslink.net/)
|
||||
|
||||
|
@ -28,7 +28,7 @@ Variables for use in `args`:
|
|||
|
||||
|
||||
### DOSEMU with abracadabra
|
||||
[DOSEMU](http://www.dosemu.org/) can provide a good solution for running legacy DOS doors when running on Linux systems. For this, we will a virtual serial port (COM1) that communicates with stdio.
|
||||
[DOSEMU](http://www.dosemu.org/) can provide a good solution for running legacy DOS doors when running on Linux systems. For this, we will create a virtual serial port (COM1) that communicates via stdio.
|
||||
|
||||
As an example, here are the steps for setting up Pimp Wars:
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ ENiGMA½ is a modern from scratch BBS package written in Node.js.
|
|||
TL;DR? This should get you started...
|
||||
|
||||
## Prerequisites
|
||||
* [Node.js](https://nodejs.org/) version **v0.12.2 or higher** (v4.2+ is recommended)
|
||||
* [io.js](https://iojs.org/) should also work, though I have not yet tested this.
|
||||
* [Node.js](https://nodejs.org/) version **v4.2.x or higher**
|
||||
* :information_source: It is suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs
|
||||
* **Windows users will need additional dependencies installed** for the `npm install` step in order to compile native binaries:
|
||||
* A recent copy of Visual Studio ([Visual Studio Express](https://www.visualstudio.com/en-us/products/visual-studio-express-vs.aspx) editions OK)
|
||||
|
@ -16,9 +15,9 @@ TL;DR? This should get you started...
|
|||
If you're new to Node.js and/or do not care about Node itself and just want to get ENiGMA½ running these steps should get you going on most \*nix type enviornments:
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.30.1/install.sh | bash
|
||||
nvm install 4.2.4
|
||||
nvm use 4.2.4
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash
|
||||
nvm install 4.4.0
|
||||
nvm use 4.4.0
|
||||
```
|
||||
|
||||
|
||||
|
@ -48,16 +47,27 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
|
|||
general: {
|
||||
boardName: Super Awesome BBS
|
||||
}
|
||||
|
||||
servers: {
|
||||
ssh: {
|
||||
privateKeyPass: YOUR_PK_PASS
|
||||
enabled: true /* set to false to disable the SSH server */
|
||||
}
|
||||
}
|
||||
messages: {
|
||||
areas: [
|
||||
{ name: "local_enigma_discusssion", desc: "ENiGMA Discussion", groups: [ "users" ] }
|
||||
]
|
||||
|
||||
messageConferences: {
|
||||
local_general: {
|
||||
name: Local
|
||||
desc: Local Discussions
|
||||
default: true
|
||||
|
||||
areas: {
|
||||
local_music: {
|
||||
name: Music Discussion
|
||||
desc: Music, bands, etc.
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# Message Conferences & Areas
|
||||
**Message Conferences** and **Areas** allow for grouping of message base topics.
|
||||
|
||||
## Message Conferences
|
||||
Message Conferences are the top level container for 1:n Message Areas via the `messageConferences` section in `config.hjson`. Common message conferences may include a local conference and one or more conferences each dedicated to a particular message network such as FidoNet, AgoraNet, etc.
|
||||
|
||||
Each conference is represented by a entry under `messageConferences`. **The areas key is the conferences tag**.
|
||||
|
||||
**Members**:
|
||||
* `name` (required): Friendly conference name
|
||||
* `desc` (required): Friendly conference description
|
||||
* `sort` (optional): If supplied, provides a key used for sorting
|
||||
* `default` (optional): Specify `true` to make this the default conference (e.g. assigned to new users)
|
||||
* `areas`: Container of 1:n areas described below
|
||||
|
||||
**Example**:
|
||||
```hjson
|
||||
{
|
||||
messageConferences: {
|
||||
local: {
|
||||
name: Local
|
||||
desc: Local discussion
|
||||
sort: 1
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Message Areas
|
||||
Message Areas are topic specific containers for messages that live within a particular conference. **The areas key is it's areas tag**. For example, "General Discussion" may live under a Local conference while an AgoraNet conference may contain "BBS Discussion".
|
||||
|
||||
**Members**:
|
||||
* `name` (required): Friendly area name
|
||||
* `desc` (required): Friendly area discription
|
||||
* `sort` (optional): If supplied, provides a key used for sorting
|
||||
* `default` (optional): Specify `true` to make this the default area (e.g. assigned to new users)
|
||||
|
||||
**Example**:
|
||||
```hjson
|
||||
messageConferences: {
|
||||
local: {
|
||||
// ... see above ...
|
||||
areas: {
|
||||
local_enigma_dev: {
|
||||
name: ENiGMA 1/2 Development
|
||||
desc: Discussion related to features and development of ENiGMA 1/2!
|
||||
sort: 1
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Message Networks
|
||||
ENiGMA½ has the ability to network with other systems via [Message Networks](msg_networks.md). Message **area tags** (described above) are utilized to map foreign areas with locally defined areas.
|
|
@ -0,0 +1,117 @@
|
|||
# Message Networks
|
||||
Message networks are configured in `messageNetworks` section of `config.hjson`. Each network type has it's own sub section such as `ftn` for FidoNet Technology Network (FTN) style networks. Message Networks tie directly with [Message Areas](msg_conf_area.md) that are also defined in `config.hjson`.
|
||||
|
||||
## FidoNet Technology Network (FTN)
|
||||
FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`.
|
||||
|
||||
### Networks
|
||||
The `networks` section contains a sub section for network(s) you wish you join your board with. Each entry's key name can be referenced elsewhere in `config.hjson` for FTN oriented configurations.
|
||||
|
||||
**Members**:
|
||||
* `localAddress` (required): FTN address of **your local system**
|
||||
|
||||
**Example**:
|
||||
```hjson
|
||||
{
|
||||
messageNetworks: {
|
||||
ftn: {
|
||||
networks: {
|
||||
agoranet: {
|
||||
localAddress: "46:3/102"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Areas
|
||||
The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages.
|
||||
|
||||
When importing, messages will be placed in the local area that matches key under `areas`.
|
||||
|
||||
**Members**:
|
||||
* `network` (required): Associated network from the `networks` section
|
||||
* `tag` (required): FTN area tag
|
||||
* `uplinks`: An array of FTN address uplink(s) for this network
|
||||
|
||||
**Example**:
|
||||
```hjson
|
||||
{
|
||||
messageNetworks: {
|
||||
ftn: {
|
||||
areas: {
|
||||
agoranet_bbs: { /* found within messageConferences */
|
||||
network: agoranet
|
||||
tag: AGN_BBS
|
||||
uplinks: "46:1/100"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BSO Import / Export
|
||||
The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers::ftn_bso`.
|
||||
|
||||
**Members**:
|
||||
* `defaultZone` (required): Sets the default BSO outbound zone
|
||||
* `defaultNetwork` (optional): Sets the default network name from `messageNetworks::ftn::networks`. **Required if more than one network is defined**.
|
||||
* `paths` (optional): Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`.
|
||||
* `packetTargetByteSize` (optional): Overrides the system *target* packet (.pkt) size of 512000 bytes (512k)
|
||||
* `bundleTargetByteSize` (optional): Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M)
|
||||
* `schedule` (required): See Scheduling
|
||||
* `nodes` (required): See Nodes
|
||||
|
||||
#### Nodes
|
||||
The `nodes` section defines how to export messages for one or more uplinks.
|
||||
|
||||
A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain.
|
||||
|
||||
**Members**:
|
||||
* `packetType` (optional): `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability
|
||||
* `packetPassword` (optional): Password for the packet
|
||||
* `encoding` (optional): Encoding to use for message bodies; Defaults to `utf-8`
|
||||
* `archiveType` (optional): Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration)
|
||||
|
||||
**Example**:
|
||||
```hjson
|
||||
{
|
||||
scannerTossers: {
|
||||
ftn_bso: {
|
||||
nodes: {
|
||||
"46:*: {
|
||||
packetType: 2+
|
||||
packetPassword: mypass
|
||||
encoding: cp437
|
||||
archiveType: zip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scheduling
|
||||
Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers.
|
||||
|
||||
* `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`.
|
||||
* `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`.
|
||||
* Free form text can be things like `at 5:00 pm` or `every 2 hours`.
|
||||
|
||||
See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information.
|
||||
|
||||
**Example**:
|
||||
```hjson
|
||||
{
|
||||
scannerTossers: {
|
||||
ftn_bso: {
|
||||
schedule: {
|
||||
import: every 1 hours or @watch:/path/to/watchfile.ext
|
||||
export: every 1 hours or @immediate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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);
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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');
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -172,8 +172,8 @@
|
|||
|
||||
messageAreaChangeCurrentArea: {
|
||||
config: {
|
||||
listFormat: "|00|15{index} |07- |03{desc}"
|
||||
focusListFormat: "|00|19|15{index} - {desc}"
|
||||
listFormat: "|00|15{index} |07- |03{name}"
|
||||
focusListFormat: "|00|19|15{index} - {name}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
|
@ -310,6 +310,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
newScanMessageList: {
|
||||
config: {
|
||||
listFormat: "|00|15 {msgNum:<5.5}|03{subj:<29.29} |15{from:<20.20} {ts}"
|
||||
focusListFormat: "|00|19> |15{msgNum:<5.5}{subj:<29.29} {from:<20.20} {ts}"
|
||||
dateTimeFormat: ddd MMM Do
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue