Merge branch 'msg_network'
This commit is contained in:
commit
6f8f8f7e9d
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
"tab"
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"comma-dangle": 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
For :bug: bug reports, please fill out the information below plus any additional relevant information. If this is a feature request, feel free to clear the form.
|
||||||
|
|
||||||
|
**Short problem description**
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
- [ ] I am using Node.js v4.x or higher
|
||||||
|
- [ ] `npm install` reports success
|
||||||
|
- Actual Node.js version (`node --version`):
|
||||||
|
- Operating system (`uname -a` on *nix systems):
|
||||||
|
- Revision (`git rev-parse --short HEAD`):
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
|
||||||
|
**Actual behavior**
|
||||||
|
|
||||||
|
**Steps to reproduce**
|
|
@ -65,7 +65,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
|
||||||
## License
|
## License
|
||||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
||||||
|
|
||||||
Copyright (c) 2015, Bryan D. Ashby
|
Copyright (c) 2015-2016, Bryan D. Ashby
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
@ -804,16 +804,13 @@ module.exports = (function() {
|
||||||
return !isNaN(value) && user.getAge() >= value;
|
return !isNaN(value) && user.getAge() >= value;
|
||||||
},
|
},
|
||||||
AS : function accountStatus() {
|
AS : function accountStatus() {
|
||||||
|
if(!_.isArray(value)) {
|
||||||
if(_.isNumber(value)) {
|
|
||||||
value = [ value ];
|
value = [ value ];
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_.isArray(value));
|
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||||
|
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||||
return _.findIndex(value, function cmp(accStatus) {
|
return value.indexOf(userAccountStatus) > -1;
|
||||||
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
|
|
||||||
}) > -1;
|
|
||||||
},
|
},
|
||||||
EC : function isEncoding() {
|
EC : function isEncoding() {
|
||||||
switch(value) {
|
switch(value) {
|
||||||
|
@ -842,7 +839,7 @@ module.exports = (function() {
|
||||||
// :TODO: implement me!!
|
// :TODO: implement me!!
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
SC : function isSecerConnection() {
|
SC : function isSecureConnection() {
|
||||||
return client.session.isSecure;
|
return client.session.isSecure;
|
||||||
},
|
},
|
||||||
ML : function minutesLeft() {
|
ML : function minutesLeft() {
|
||||||
|
@ -870,16 +867,20 @@ module.exports = (function() {
|
||||||
return !isNaN(value) && client.term.termWidth >= value;
|
return !isNaN(value) && client.term.termWidth >= value;
|
||||||
},
|
},
|
||||||
ID : function isUserId(value) {
|
ID : function isUserId(value) {
|
||||||
return user.userId === value;
|
if(!_.isArray(value)) {
|
||||||
|
value = [ value ];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||||
|
return value.indexOf(user.userId) > -1;
|
||||||
},
|
},
|
||||||
WD : function isOneOfDayOfWeek() {
|
WD : function isOneOfDayOfWeek() {
|
||||||
// :TODO: return true if DoW
|
if(!_.isArray(value)) {
|
||||||
if(_.isNumber(value)) {
|
value = [ value ];
|
||||||
|
|
||||||
} else if(_.isArray(value)) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||||
|
return value.indexOf(new Date().getDay()) > -1;
|
||||||
},
|
},
|
||||||
MM : function isMinutesPastMidnight() {
|
MM : function isMinutesPastMidnight() {
|
||||||
// :TODO: return true if value is >= minutes past midnight sys time
|
// :TODO: return true if value is >= minutes past midnight sys time
|
||||||
|
|
|
@ -7,8 +7,13 @@ var acsParser = require('./acs_parser.js');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
|
||||||
|
exports.checkAcs = checkAcs;
|
||||||
exports.getConditionalValue = getConditionalValue;
|
exports.getConditionalValue = getConditionalValue;
|
||||||
|
|
||||||
|
function checkAcs(client, acsString) {
|
||||||
|
return acsParser.parse(acsString, { client : client } );
|
||||||
|
}
|
||||||
|
|
||||||
function getConditionalValue(client, condArray, memberName) {
|
function getConditionalValue(client, condArray, memberName) {
|
||||||
assert(_.isObject(client));
|
assert(_.isObject(client));
|
||||||
assert(_.isArray(condArray));
|
assert(_.isArray(condArray));
|
||||||
|
|
|
@ -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 util = require('util');
|
||||||
var ansi = require('./ansi_term.js');
|
var ansi = require('./ansi_term.js');
|
||||||
var aep = require('./ansi_escape_parser.js');
|
var aep = require('./ansi_escape_parser.js');
|
||||||
|
var sauce = require('./sauce.js');
|
||||||
|
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
@ -20,10 +21,6 @@ exports.getArtFromPath = getArtFromPath;
|
||||||
exports.display = display;
|
exports.display = display;
|
||||||
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
||||||
|
|
||||||
var SAUCE_SIZE = 128;
|
|
||||||
var SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
|
|
||||||
var COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
|
||||||
|
|
||||||
// :TODO: Return MCI code information
|
// :TODO: Return MCI code information
|
||||||
// :TODO: process SAUCE comments
|
// :TODO: process SAUCE comments
|
||||||
// :TODO: return font + font mapped information from SAUCE
|
// :TODO: return font + font mapped information from SAUCE
|
||||||
|
@ -43,156 +40,6 @@ var SUPPORTED_ART_TYPES = {
|
||||||
// :TODO: extension for topaz ansi/ascii.
|
// :TODO: extension for topaz ansi/ascii.
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
|
||||||
// See
|
|
||||||
// http://www.acid.org/info/sauce/sauce.htm
|
|
||||||
//
|
|
||||||
// :TODO: Move all SAUCE stuff to sauce.js
|
|
||||||
function readSAUCE(data, cb) {
|
|
||||||
if(data.length < SAUCE_SIZE) {
|
|
||||||
cb(new Error('No SAUCE record present'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset = data.length - SAUCE_SIZE;
|
|
||||||
var sauceRec = data.slice(offset);
|
|
||||||
|
|
||||||
binary.parse(sauceRec)
|
|
||||||
.buffer('id', 5)
|
|
||||||
.buffer('version', 2)
|
|
||||||
.buffer('title', 35)
|
|
||||||
.buffer('author', 20)
|
|
||||||
.buffer('group', 20)
|
|
||||||
.buffer('date', 8)
|
|
||||||
.word32lu('fileSize')
|
|
||||||
.word8('dataType')
|
|
||||||
.word8('fileType')
|
|
||||||
.word16lu('tinfo1')
|
|
||||||
.word16lu('tinfo2')
|
|
||||||
.word16lu('tinfo3')
|
|
||||||
.word16lu('tinfo4')
|
|
||||||
.word8('numComments')
|
|
||||||
.word8('flags')
|
|
||||||
.buffer('tinfos', 22) // SAUCE 00.5
|
|
||||||
.tap(function onVars(vars) {
|
|
||||||
|
|
||||||
if(!SAUCE_ID.equals(vars.id)) {
|
|
||||||
cb(new Error('No SAUCE record present'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ver = iconv.decode(vars.version, 'cp437');
|
|
||||||
|
|
||||||
if('00' !== ver) {
|
|
||||||
cb(new Error('Unsupported SAUCE version: ' + ver));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sauce = {
|
|
||||||
id : iconv.decode(vars.id, 'cp437'),
|
|
||||||
version : iconv.decode(vars.version, 'cp437').trim(),
|
|
||||||
title : iconv.decode(vars.title, 'cp437').trim(),
|
|
||||||
author : iconv.decode(vars.author, 'cp437').trim(),
|
|
||||||
group : iconv.decode(vars.group, 'cp437').trim(),
|
|
||||||
date : iconv.decode(vars.date, 'cp437').trim(),
|
|
||||||
fileSize : vars.fileSize,
|
|
||||||
dataType : vars.dataType,
|
|
||||||
fileType : vars.fileType,
|
|
||||||
tinfo1 : vars.tinfo1,
|
|
||||||
tinfo2 : vars.tinfo2,
|
|
||||||
tinfo3 : vars.tinfo3,
|
|
||||||
tinfo4 : vars.tinfo4,
|
|
||||||
numComments : vars.numComments,
|
|
||||||
flags : vars.flags,
|
|
||||||
tinfos : vars.tinfos,
|
|
||||||
};
|
|
||||||
|
|
||||||
var dt = SAUCE_DATA_TYPES[sauce.dataType];
|
|
||||||
if(dt && dt.parser) {
|
|
||||||
sauce[dt.name] = dt.parser(sauce);
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, sauce);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// :TODO: These need completed:
|
|
||||||
var SAUCE_DATA_TYPES = {};
|
|
||||||
SAUCE_DATA_TYPES[0] = { name : 'None' };
|
|
||||||
SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE };
|
|
||||||
SAUCE_DATA_TYPES[2] = 'Bitmap';
|
|
||||||
SAUCE_DATA_TYPES[3] = 'Vector';
|
|
||||||
SAUCE_DATA_TYPES[4] = 'Audio';
|
|
||||||
SAUCE_DATA_TYPES[5] = 'BinaryText';
|
|
||||||
SAUCE_DATA_TYPES[6] = 'XBin';
|
|
||||||
SAUCE_DATA_TYPES[7] = 'Archive';
|
|
||||||
SAUCE_DATA_TYPES[8] = 'Executable';
|
|
||||||
|
|
||||||
var SAUCE_CHARACTER_FILE_TYPES = {};
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[7] = 'Source';
|
|
||||||
SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Map of SAUCE font -> encoding hint
|
|
||||||
//
|
|
||||||
// Note that this is the same mapping that x84 uses. Be compatible!
|
|
||||||
//
|
|
||||||
var SAUCE_FONT_TO_ENCODING_HINT = {
|
|
||||||
'Amiga MicroKnight' : 'amiga',
|
|
||||||
'Amiga MicroKnight+' : 'amiga',
|
|
||||||
'Amiga mOsOul' : 'amiga',
|
|
||||||
'Amiga P0T-NOoDLE' : 'amiga',
|
|
||||||
'Amiga Topaz 1' : 'amiga',
|
|
||||||
'Amiga Topaz 1+' : 'amiga',
|
|
||||||
'Amiga Topaz 2' : 'amiga',
|
|
||||||
'Amiga Topaz 2+' : 'amiga',
|
|
||||||
'Atari ATASCII' : 'atari',
|
|
||||||
'IBM EGA43' : 'cp437',
|
|
||||||
'IBM EGA' : 'cp437',
|
|
||||||
'IBM VGA25G' : 'cp437',
|
|
||||||
'IBM VGA50' : 'cp437',
|
|
||||||
'IBM VGA' : 'cp437',
|
|
||||||
};
|
|
||||||
|
|
||||||
['437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
|
|
||||||
'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) {
|
|
||||||
var codec = 'cp' + page;
|
|
||||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
|
|
||||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
|
|
||||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
|
|
||||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
|
|
||||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseCharacterSAUCE(sauce) {
|
|
||||||
var result = {};
|
|
||||||
|
|
||||||
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
|
|
||||||
|
|
||||||
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
|
|
||||||
// convience: create ansiFlags
|
|
||||||
sauce.ansiFlags = sauce.flags;
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437');
|
|
||||||
if(fontName.length > 0) {
|
|
||||||
result.fontName = fontName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFontNameFromSAUCE(sauce) {
|
function getFontNameFromSAUCE(sauce) {
|
||||||
if(sauce.Character) {
|
if(sauce.Character) {
|
||||||
return sauce.Character.fontName;
|
return sauce.Character.fontName;
|
||||||
|
@ -249,7 +96,7 @@ function getArtFromPath(path, options, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.readSauce === true) {
|
if(options.readSauce === true) {
|
||||||
readSAUCE(data, function onSauce(err, sauce) {
|
sauce.readSAUCE(data, function onSauce(err, sauce) {
|
||||||
if(err) {
|
if(err) {
|
||||||
cb(null, getResult());
|
cb(null, getResult());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Config = require('./config.js').config;
|
var Config = require('./config.js').config;
|
||||||
var theme = require('./theme.js');
|
|
||||||
|
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
|
87
core/bbs.js
87
core/bbs.js
|
@ -1,44 +1,47 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
//var SegfaultHandler = require('segfault-handler');
|
||||||
|
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
var conf = require('./config.js');
|
let conf = require('./config.js');
|
||||||
var logger = require('./logger.js');
|
let logger = require('./logger.js');
|
||||||
var miscUtil = require('./misc_util.js');
|
let miscUtil = require('./misc_util.js');
|
||||||
var database = require('./database.js');
|
let database = require('./database.js');
|
||||||
var clientConns = require('./client_connections.js');
|
let clientConns = require('./client_connections.js');
|
||||||
|
|
||||||
var paths = require('path');
|
let paths = require('path');
|
||||||
var async = require('async');
|
let async = require('async');
|
||||||
var util = require('util');
|
let util = require('util');
|
||||||
var _ = require('lodash');
|
let _ = require('lodash');
|
||||||
var assert = require('assert');
|
let assert = require('assert');
|
||||||
var mkdirp = require('mkdirp');
|
let mkdirp = require('mkdirp');
|
||||||
|
|
||||||
|
// our main entry point
|
||||||
exports.bbsMain = bbsMain;
|
exports.bbsMain = bbsMain;
|
||||||
|
|
||||||
function bbsMain() {
|
function bbsMain() {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function processArgs(callback) {
|
function processArgs(callback) {
|
||||||
var args = parseArgs();
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
var configPath;
|
var configPath;
|
||||||
|
|
||||||
if(args.indexOf('--help') > 0) {
|
if(args.indexOf('--help') > 0) {
|
||||||
// :TODO: display help
|
// :TODO: display help
|
||||||
} else {
|
} else {
|
||||||
var argCount = args.length;
|
let argCount = args.length;
|
||||||
for(var i = 0; i < argCount; ++i) {
|
for(let i = 0; i < argCount; ++i) {
|
||||||
var arg = args[i];
|
const arg = args[i];
|
||||||
if('--config' == arg) {
|
if('--config' === arg) {
|
||||||
configPath = args[i + 1];
|
configPath = args[i + 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPathSupplied = _.isString(configPath);
|
callback(null, configPath || conf.getDefaultPath(), _.isString(configPath));
|
||||||
callback(null, configPath || conf.getDefaultPath(), configPathSupplied);
|
|
||||||
},
|
},
|
||||||
function initConfig(configPath, configPathSupplied, callback) {
|
function initConfig(configPath, configPathSupplied, callback) {
|
||||||
conf.init(configPath, function configInit(err) {
|
conf.init(configPath, function configInit(err) {
|
||||||
|
@ -68,25 +71,19 @@ function bbsMain() {
|
||||||
}
|
}
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
function listenConnections(callback) {
|
||||||
|
startListening(callback);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(!err) {
|
if(err) {
|
||||||
startListening();
|
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) {
|
function initialize(cb) {
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
|
@ -169,6 +166,9 @@ function initialize(cb) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
function readyMessageNetworkSupport(callback) {
|
||||||
|
require('./msg_network.js').startup(callback);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function onComplete(err) {
|
function onComplete(err) {
|
||||||
|
@ -177,29 +177,36 @@ function initialize(cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListening() {
|
function startListening(cb) {
|
||||||
if(!conf.config.servers) {
|
if(!conf.config.servers) {
|
||||||
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
|
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
|
||||||
logger.log.error('No servers configured');
|
//logger.log.error('No servers configured');
|
||||||
return [];
|
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) {
|
if(err) {
|
||||||
logger.log.info(err);
|
logger.log.info(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var port = parseInt(module.runtime.config.port);
|
const port = parseInt(module.runtime.config.port);
|
||||||
if(isNaN(port)) {
|
if(isNaN(port)) {
|
||||||
logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
|
logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var moduleInst = new module.getModule();
|
const moduleInst = new module.getModule();
|
||||||
var server = moduleInst.createServer();
|
let server;
|
||||||
|
try {
|
||||||
|
server = moduleInst.createServer();
|
||||||
|
} catch(e) {
|
||||||
|
logger.log.warn(e, 'Exception caught creating server!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// :TODO: handle maxConnections, e.g. conf.maxConnections
|
// :TODO: handle maxConnections, e.g. conf.maxConnections
|
||||||
|
|
||||||
|
@ -260,7 +267,11 @@ function startListening() {
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(port);
|
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 async = require('async');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
var hjson = require('hjson');
|
var hjson = require('hjson');
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
exports.init = init;
|
exports.init = init;
|
||||||
exports.getDefaultPath = getDefaultPath;
|
exports.getDefaultPath = getDefaultPath;
|
||||||
|
|
||||||
|
function hasMessageConferenceAndArea(config) {
|
||||||
|
assert(_.isObject(config.messageConferences)); // we create one ourself!
|
||||||
|
|
||||||
|
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
|
||||||
|
return 'system_internal' !== confTag;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(0 === nonInternalConfs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: there is likely a better/cleaner way of doing this
|
||||||
|
|
||||||
|
var result = false;
|
||||||
|
_.forEach(nonInternalConfs, confTag => {
|
||||||
|
if(_.has(config.messageConferences[confTag], 'areas') &&
|
||||||
|
Object.keys(config.messageConferences[confTag].areas) > 0)
|
||||||
|
{
|
||||||
|
result = true;
|
||||||
|
return false; // stop iteration
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function init(configPath, cb) {
|
function init(configPath, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
|
@ -48,19 +75,14 @@ function init(configPath, cb) {
|
||||||
//
|
//
|
||||||
// Various sections must now exist in config
|
// Various sections must now exist in config
|
||||||
//
|
//
|
||||||
if(!_.has(mergedConfig, 'messages.areas.') ||
|
if(hasMessageConferenceAndArea(mergedConfig)) {
|
||||||
!_.isArray(mergedConfig.messages.areas) ||
|
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
||||||
0 === mergedConfig.messages.areas.length ||
|
|
||||||
!_.isString(mergedConfig.messages.areas[0].name))
|
|
||||||
{
|
|
||||||
var msgAreasErr = new Error('Please create at least one message area');
|
|
||||||
msgAreasErr.code = 'EBADCONFIG';
|
msgAreasErr.code = 'EBADCONFIG';
|
||||||
callback(msgAreasErr);
|
callback(msgAreasErr);
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, mergedConfig);
|
callback(null, mergedConfig);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
function complete(err, mergedConfig) {
|
function complete(err, mergedConfig) {
|
||||||
exports.config = mergedConfig;
|
exports.config = mergedConfig;
|
||||||
|
@ -150,6 +172,10 @@ function getDefaultConfig() {
|
||||||
paths : {
|
paths : {
|
||||||
mods : paths.join(__dirname, './../mods/'),
|
mods : paths.join(__dirname, './../mods/'),
|
||||||
servers : paths.join(__dirname, './servers/'),
|
servers : paths.join(__dirname, './servers/'),
|
||||||
|
|
||||||
|
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
|
||||||
|
mailers : paths.join(__dirname, './mailers/') ,
|
||||||
|
|
||||||
art : paths.join(__dirname, './../mods/art/'),
|
art : paths.join(__dirname, './../mods/art/'),
|
||||||
themes : paths.join(__dirname, './../mods/themes/'),
|
themes : paths.join(__dirname, './../mods/themes/'),
|
||||||
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
||||||
|
@ -166,7 +192,7 @@ function getDefaultConfig() {
|
||||||
},
|
},
|
||||||
ssh : {
|
ssh : {
|
||||||
port : 8889,
|
port : 8889,
|
||||||
enabled : true,
|
enabled : false, // defualt to false as PK/pass in config.hjson are required
|
||||||
|
|
||||||
//
|
//
|
||||||
// Private key in PEM format
|
// Private key in PEM format
|
||||||
|
@ -183,24 +209,52 @@ function getDefaultConfig() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
messages : {
|
archivers : {
|
||||||
areas : [
|
zip : {
|
||||||
{ name : 'private_mail', desc : 'Private Email', groups : [ 'users' ] }
|
sig : "504b0304",
|
||||||
]
|
offset : 0,
|
||||||
|
compressCmd : "7z",
|
||||||
|
compressArgs : [ "a", "-tzip", "{archivePath}", "{fileList}" ],
|
||||||
|
decompressCmd : "7z",
|
||||||
|
decompressArgs : [ "e", "-o{extractPath}", "{archivePath}" ]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
networks : {
|
messageConferences : {
|
||||||
/*
|
system_internal : {
|
||||||
networkName : { // e.g. fidoNet
|
name : 'System Internal',
|
||||||
address : {
|
desc : 'Built in conference for private messages, bulletins, etc.',
|
||||||
zone : 0,
|
|
||||||
net : 0,
|
areas : {
|
||||||
node : 0,
|
private_mail : {
|
||||||
point : 0,
|
name : 'Private Mail',
|
||||||
domain : 'l33t.codes'
|
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 : {
|
misc : {
|
||||||
|
|
|
@ -132,7 +132,7 @@ function createMessageBaseTables() {
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
'CREATE TABLE IF NOT EXISTS message (' +
|
'CREATE TABLE IF NOT EXISTS message (' +
|
||||||
' message_id INTEGER PRIMARY KEY,' +
|
' message_id INTEGER PRIMARY KEY,' +
|
||||||
' area_name VARCHAR NOT NULL,' +
|
' area_tag VARCHAR NOT NULL,' +
|
||||||
' message_uuid VARCHAR(36) NOT NULL,' +
|
' message_uuid VARCHAR(36) NOT NULL,' +
|
||||||
' reply_to_message_id INTEGER,' +
|
' reply_to_message_id INTEGER,' +
|
||||||
' to_user_name VARCHAR NOT NULL,' +
|
' to_user_name VARCHAR NOT NULL,' +
|
||||||
|
@ -175,7 +175,7 @@ function createMessageBaseTables() {
|
||||||
' meta_category INTEGER NOT NULL,' +
|
' meta_category INTEGER NOT NULL,' +
|
||||||
' meta_name VARCHAR NOT NULL,' +
|
' meta_name VARCHAR NOT NULL,' +
|
||||||
' meta_value 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)' +
|
' FOREIGN KEY(message_id) REFERENCES message(message_id)' +
|
||||||
');'
|
');'
|
||||||
);
|
);
|
||||||
|
@ -198,20 +198,19 @@ function createMessageBaseTables() {
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
'CREATE TABLE IF NOT EXISTS user_message_area_last_read (' +
|
'CREATE TABLE IF NOT EXISTS user_message_area_last_read (' +
|
||||||
' user_id INTEGER NOT NULL,' +
|
' user_id INTEGER NOT NULL,' +
|
||||||
' area_name VARCHAR NOT NULL,' +
|
' area_tag VARCHAR NOT NULL,' +
|
||||||
' message_id INTEGER NOT NULL,' +
|
' message_id INTEGER NOT NULL,' +
|
||||||
' UNIQUE(user_id, area_name)' +
|
' UNIQUE(user_id, area_tag)' +
|
||||||
');'
|
');'
|
||||||
);
|
);
|
||||||
|
|
||||||
dbs.message.run(
|
dbs.message.run(
|
||||||
'CREATE TABLE IF NOT EXISTS user_message_status (' +
|
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
|
||||||
' user_id INTEGER NOT NULL,' +
|
scan_toss VARCHAR NOT NULL,
|
||||||
' message_id INTEGER NOT NULL,' +
|
area_tag VARCHAR NOT NULL,
|
||||||
' status INTEGER NOT NULL,' +
|
message_id INTEGER NOT NULL,
|
||||||
' UNIQUE(user_id, message_id, status),' +
|
UNIQUE(scan_toss, area_tag)
|
||||||
' FOREIGN KEY(user_id) REFERENCES user(id)' +
|
);`
|
||||||
');'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 theme = require('../core/theme.js');
|
||||||
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||||
var Message = require('../core/message.js');
|
var Message = require('../core/message.js');
|
||||||
var getMessageAreaByName = require('../core/message_area.js').getMessageAreaByName;
|
var getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
|
||||||
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
|
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
|
||||||
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
|
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
|
||||||
|
|
||||||
|
@ -76,6 +76,8 @@ var MCICodeIds = {
|
||||||
MessageID : 10,
|
MessageID : 10,
|
||||||
ReplyToMsgID : 11,
|
ReplyToMsgID : 11,
|
||||||
|
|
||||||
|
// :TODO: ConfName
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
ViewModeFooter : {
|
ViewModeFooter : {
|
||||||
|
@ -104,15 +106,15 @@ function FullScreenEditorModule(options) {
|
||||||
// editorMode : view | edit | quote
|
// editorMode : view | edit | quote
|
||||||
//
|
//
|
||||||
// menuConfig.config or extraArgs
|
// menuConfig.config or extraArgs
|
||||||
// messageAreaName
|
// messageAreaTag
|
||||||
// messageIndex / messageTotal
|
// messageIndex / messageTotal
|
||||||
// toUserId
|
// toUserId
|
||||||
//
|
//
|
||||||
this.editorType = config.editorType;
|
this.editorType = config.editorType;
|
||||||
this.editorMode = config.editorMode;
|
this.editorMode = config.editorMode;
|
||||||
|
|
||||||
if(config.messageAreaName) {
|
if(config.messageAreaTag) {
|
||||||
this.messageAreaName = config.messageAreaName;
|
this.messageAreaTag = config.messageAreaTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageIndex = config.messageIndex || 0;
|
this.messageIndex = config.messageIndex || 0;
|
||||||
|
@ -121,8 +123,8 @@ function FullScreenEditorModule(options) {
|
||||||
|
|
||||||
// extraArgs can override some config
|
// extraArgs can override some config
|
||||||
if(_.isObject(options.extraArgs)) {
|
if(_.isObject(options.extraArgs)) {
|
||||||
if(options.extraArgs.messageAreaName) {
|
if(options.extraArgs.messageAreaTag) {
|
||||||
this.messageAreaName = options.extraArgs.messageAreaName;
|
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||||
}
|
}
|
||||||
if(options.extraArgs.messageIndex) {
|
if(options.extraArgs.messageIndex) {
|
||||||
this.messageIndex = options.extraArgs.messageIndex;
|
this.messageIndex = options.extraArgs.messageIndex;
|
||||||
|
@ -135,9 +137,6 @@ function FullScreenEditorModule(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(this.toUserId)
|
|
||||||
console.log(this.messageAreaName)
|
|
||||||
|
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
|
|
||||||
this.isEditMode = function() {
|
this.isEditMode = function() {
|
||||||
|
@ -149,7 +148,7 @@ function FullScreenEditorModule(options) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isLocalEmail = function() {
|
this.isLocalEmail = function() {
|
||||||
return Message.WellKnownAreaNames.Private === self.messageAreaName;
|
return Message.WellKnownAreaTags.Private === self.messageAreaTag;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isReply = function() {
|
this.isReply = function() {
|
||||||
|
@ -217,7 +216,7 @@ function FullScreenEditorModule(options) {
|
||||||
var headerValues = self.viewControllers.header.getFormData().value;
|
var headerValues = self.viewControllers.header.getFormData().value;
|
||||||
|
|
||||||
var msgOpts = {
|
var msgOpts = {
|
||||||
areaName : self.messageAreaName,
|
areaTag : self.messageAreaTag,
|
||||||
toUserName : headerValues.to,
|
toUserName : headerValues.to,
|
||||||
fromUserName : headerValues.from,
|
fromUserName : headerValues.from,
|
||||||
subject : headerValues.subject,
|
subject : headerValues.subject,
|
||||||
|
@ -235,7 +234,7 @@ function FullScreenEditorModule(options) {
|
||||||
self.message = message;
|
self.message = message;
|
||||||
|
|
||||||
updateMessageAreaLastReadId(
|
updateMessageAreaLastReadId(
|
||||||
self.client.user.userId, self.messageAreaName, self.message.messageId,
|
self.client.user.userId, self.messageAreaTag, self.message.messageId,
|
||||||
function lastReadUpdated() {
|
function lastReadUpdated() {
|
||||||
|
|
||||||
if(self.isReady) {
|
if(self.isReady) {
|
||||||
|
@ -308,7 +307,7 @@ function FullScreenEditorModule(options) {
|
||||||
|
|
||||||
// :TODO: We'd like to delete up to N rows, but this does not work
|
// :TODO: We'd like to delete up to N rows, but this does not work
|
||||||
// in NetRunner:
|
// 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))
|
self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2))
|
||||||
}
|
}
|
||||||
|
@ -631,7 +630,7 @@ function FullScreenEditorModule(options) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.initHeaderGeneric = function() {
|
this.initHeaderGeneric = function() {
|
||||||
self.setHeaderText(MCICodeIds.ViewModeHeader.AreaName, getMessageAreaByName(self.messageAreaName).desc);
|
self.setHeaderText(MCICodeIds.ViewModeHeader.AreaName, getMessageAreaByTag(self.messageAreaTag).name);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.initHeaderViewMode = function() {
|
this.initHeaderViewMode = function() {
|
||||||
|
@ -965,13 +964,10 @@ function FullScreenEditorModule(options) {
|
||||||
|
|
||||||
require('util').inherits(FullScreenEditorModule, MenuModule);
|
require('util').inherits(FullScreenEditorModule, MenuModule);
|
||||||
|
|
||||||
FullScreenEditorModule.prototype.enter = function(client) {
|
FullScreenEditorModule.prototype.enter = function() {
|
||||||
FullScreenEditorModule.super_.prototype.enter.call(this, client);
|
FullScreenEditorModule.super_.prototype.enter.call(this);
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FullScreenEditorModule.prototype.mciReady = function(mciData, cb) {
|
FullScreenEditorModule.prototype.mciReady = function(mciData, cb) {
|
||||||
this.mciReadyHandler(mciData, cb);
|
this.mciReadyHandler(mciData, cb);
|
||||||
//this['mciReadyHandler' + _.capitalize(this.editorType)](mciData);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let _ = require('lodash');
|
||||||
|
|
||||||
|
const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i;
|
||||||
|
const FTN_PATTERN_REGEXP = /^([0-9\*]+:)?([0-9\*]+)(\/[0-9\*]+)?(\.[0-9\*]+)?(@[a-z0-9\-\.\*]+)?$/i;
|
||||||
|
|
||||||
|
module.exports = class Address {
|
||||||
|
constructor(addr) {
|
||||||
|
if(addr) {
|
||||||
|
if(_.isObject(addr)) {
|
||||||
|
Object.assign(this, addr);
|
||||||
|
} else if(_.isString(addr)) {
|
||||||
|
const temp = Address.fromString(addr);
|
||||||
|
if(temp) {
|
||||||
|
Object.assign(this, temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEqual(other) {
|
||||||
|
if(_.isString(other)) {
|
||||||
|
other = Address.fromString(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.net === other.net &&
|
||||||
|
this.node === other.node &&
|
||||||
|
this.zone === other.zone &&
|
||||||
|
this.point === other.point &&
|
||||||
|
this.domain === other.domain
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchAddr(pattern) {
|
||||||
|
const m = FTN_PATTERN_REGEXP.exec(pattern);
|
||||||
|
if(m) {
|
||||||
|
let addr = { };
|
||||||
|
|
||||||
|
if(m[1]) {
|
||||||
|
addr.zone = m[1].slice(0, -1)
|
||||||
|
if('*' !== addr.zone) {
|
||||||
|
addr.zone = parseInt(addr.zone);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addr.zone = '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m[2]) {
|
||||||
|
addr.net = m[2];
|
||||||
|
if('*' !== addr.net) {
|
||||||
|
addr.net = parseInt(addr.net);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addr.net = '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m[3]) {
|
||||||
|
addr.node = m[3].substr(1);
|
||||||
|
if('*' !== addr.node) {
|
||||||
|
addr.node = parseInt(addr.node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addr.node = '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m[4]) {
|
||||||
|
addr.point = m[4].substr(1);
|
||||||
|
if('*' !== addr.point) {
|
||||||
|
addr.point = parseInt(addr.point);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addr.point = '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m[5]) {
|
||||||
|
addr.domain = m[5].substr(1);
|
||||||
|
} else {
|
||||||
|
addr.domain = '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
getMatchScore(pattern) {
|
||||||
|
let score = 0;
|
||||||
|
const addr = this.getMatchAddr(pattern);
|
||||||
|
if(addr) {
|
||||||
|
const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ];
|
||||||
|
for(let i = 0; i < PARTS.length; ++i) {
|
||||||
|
const member = PARTS[i];
|
||||||
|
if(this[member] === addr[member]) {
|
||||||
|
score += 2;
|
||||||
|
} else if('*' === addr[member]) {
|
||||||
|
score += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
isPatternMatch(pattern) {
|
||||||
|
const addr = this.getMatchAddr(pattern);
|
||||||
|
if(addr) {
|
||||||
|
return (
|
||||||
|
('*' === addr.net || this.net === addr.net) &&
|
||||||
|
('*' === addr.node || this.node === addr.node) &&
|
||||||
|
('*' === addr.zone || this.zone === addr.zone) &&
|
||||||
|
('*' === addr.point || this.point === addr.point) &&
|
||||||
|
('*' === addr.domain || this.domain === addr.domain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromString(addrStr) {
|
||||||
|
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
||||||
|
|
||||||
|
if(m) {
|
||||||
|
// start with a 2D
|
||||||
|
let addr = {
|
||||||
|
net : parseInt(m[2]),
|
||||||
|
node : parseInt(m[3].substr(1)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3D: Addition of zone if present
|
||||||
|
if(m[1]) {
|
||||||
|
addr.zone = parseInt(m[1].slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4D if optional point is present
|
||||||
|
if(m[4]) {
|
||||||
|
addr.point = parseInt(m[4].substr(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5D with @domain
|
||||||
|
if(m[5]) {
|
||||||
|
addr.domain = m[5].substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Address(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(dimensions) {
|
||||||
|
dimensions = dimensions || '5D';
|
||||||
|
|
||||||
|
let addrStr = `${this.zone}:${this.net}`;
|
||||||
|
|
||||||
|
// allow for e.g. '4D' or 5
|
||||||
|
const dim = parseInt(dimensions.toString()[0]);
|
||||||
|
|
||||||
|
if(dim >= 3) {
|
||||||
|
addrStr += `/${this.node}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing & .0 are equiv for point
|
||||||
|
if(dim >= 4 && this.point) {
|
||||||
|
addrStr += `.${this.point}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(5 === dim && this.domain) {
|
||||||
|
addrStr += `@${this.domain.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return addrStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getComparator() {
|
||||||
|
return function(left, right) {
|
||||||
|
let c = (left.zone || 0) - (right.zone || 0);
|
||||||
|
if(0 !== c) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = (left.net || 0) - (right.net || 0);
|
||||||
|
if(0 !== c) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = (left.node || 0) - (right.node || 0);
|
||||||
|
if(0 !== c) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (left.domain || '').localeCompare(right.domain || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
458
core/ftn_util.js
458
core/ftn_util.js
|
@ -1,42 +1,65 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'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');
|
let packageJson = require('../package.json');
|
||||||
var assert = require('assert');
|
|
||||||
var binary = require('binary');
|
|
||||||
var fs = require('fs');
|
|
||||||
var util = require('util');
|
|
||||||
var iconv = require('iconv-lite');
|
|
||||||
|
|
||||||
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
|
||||||
exports.stringFromFTN = stringFromFTN;
|
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
|
||||||
exports.getFormattedFTNAddress = getFormattedFTNAddress;
|
exports.getMessageSerialNumber = getMessageSerialNumber;
|
||||||
|
exports.createMessageUuid = createMessageUuid;
|
||||||
|
exports.createMessageUuidAlternate = createMessageUuidAlternate;
|
||||||
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
|
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;
|
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
|
// See list here: https://github.com/Mithgol/node-fidonet-jam
|
||||||
|
|
||||||
// :TODO: proably move this elsewhere as a general method
|
function stringToNullPaddedBuffer(s, bufLen) {
|
||||||
function stringFromFTN(buf, encoding) {
|
let buffer = new Buffer(bufLen).fill(0x00);
|
||||||
var nullPos = buf.length;
|
let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
|
||||||
for(var i = 0; i < buf.length; ++i) {
|
for(let i = 0; i < enc.length; ++i) {
|
||||||
if(0x00 === buf[i]) {
|
buffer[i] = enc[i];
|
||||||
nullPos = i;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Convert a FTN style DateTime string to a Date object
|
// Convert a FTN style DateTime string to a Date object
|
||||||
//
|
//
|
||||||
|
// :TODO: Name the next couple methods better - for FTN *packets*
|
||||||
function getDateFromFtnDateTime(dateTime) {
|
function getDateFromFtnDateTime(dateTime) {
|
||||||
//
|
//
|
||||||
// Examples seen in the wild (Working):
|
// Examples seen in the wild (Working):
|
||||||
|
@ -44,45 +67,146 @@ function getDateFromFtnDateTime(dateTime) {
|
||||||
// "Tue 01 Jan 80 00:00"
|
// "Tue 01 Jan 80 00:00"
|
||||||
// "27 Feb 15 00:00:03"
|
// "27 Feb 15 00:00:03"
|
||||||
//
|
//
|
||||||
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) {
|
function getDateTimeString(m) {
|
||||||
//var addr = util.format('%d:%d', address.zone, address.net);
|
//
|
||||||
var addr = '{0}:{1}'.format(address.zone, address.net);
|
// From http://ftsc.org/docs/fts-0001.016:
|
||||||
switch(dimensions) {
|
// DateTime = (* a character string 20 characters long *)
|
||||||
case 2 :
|
// (* 01 Jan 86 02:34:56 *)
|
||||||
case '2D' :
|
// DayOfMonth " " Month " " Year " "
|
||||||
// above
|
// " " HH ":" MM ":" SS
|
||||||
break;
|
// Null
|
||||||
|
//
|
||||||
case 3 :
|
// DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *)
|
||||||
case '3D' :
|
// Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" |
|
||||||
addr += '/{0}'.format(address.node);
|
// "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
|
||||||
break;
|
// Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00"
|
||||||
|
// HH = "00" | .. | "23"
|
||||||
case 4 :
|
// MM = "00" | .. | "59"
|
||||||
case '4D':
|
// SS = "00" | .. | "59"
|
||||||
addr += '.{0}'.format(address.point || 0); // missing and 0 are equiv for point
|
//
|
||||||
break;
|
if(!moment.isMoment(m)) {
|
||||||
|
m = moment(m);
|
||||||
case 5 :
|
|
||||||
case '5D' :
|
|
||||||
if(address.domain) {
|
|
||||||
addr += '@{0}'.format(address.domain);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function getMessageSerialNumber(messageId) {
|
||||||
return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getFTNMessageSerialNumber(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
|
// Get a FSC-0032 style quote prefixes
|
||||||
|
@ -91,25 +215,221 @@ function getQuotePrefix(name) {
|
||||||
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
|
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Specs:
|
// Return a FTS-0004 Origin line
|
||||||
// * http://ftsc.org/docs/fts-0009.001
|
|
||||||
// *
|
|
||||||
//
|
|
||||||
function getFtnMsgIdKludgeLine(origAddress, messageId) {
|
|
||||||
if(_.isObject(origAddress)) {
|
|
||||||
origAddress = getFormattedFTNAddress(origAddress, '5D');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '\x01MSGID: ' + origAddress + ' ' + getFtnMessageSerialNumber(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getFTNOriginLine() {
|
|
||||||
//
|
|
||||||
// Specs:
|
|
||||||
// http://ftsc.org/docs/fts-0004.001
|
// http://ftsc.org/docs/fts-0004.001
|
||||||
//
|
//
|
||||||
return ' * Origin: ' + Config.general.boardName + '(' + getFidoNetAddress() + ')';
|
function getOrigin(address) {
|
||||||
|
const origin = _.has(Config.messageNetworks.originName) ?
|
||||||
|
Config.messageNetworks.originName :
|
||||||
|
Config.general.boardName;
|
||||||
|
|
||||||
|
const addrStr = new Address(address).toString('5D');
|
||||||
|
return ` * Origin: ${origin} (${addrStr})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTearLine() {
|
||||||
|
const nodeVer = process.version.substr(1); // remove 'v' prefix
|
||||||
|
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return a FRL-1005.001 "Via" line
|
||||||
|
// http://ftsc.org/docs/frl-1005.001
|
||||||
|
//
|
||||||
|
function getVia(address) {
|
||||||
|
/*
|
||||||
|
FRL-1005.001 states teh following format:
|
||||||
|
|
||||||
|
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
|
||||||
|
<Program Name> <Version> [Serial Number]<CR>
|
||||||
|
*/
|
||||||
|
const addrStr = new Address(address).toString('5D');
|
||||||
|
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
|
||||||
|
|
||||||
|
const version = packageJson.version
|
||||||
|
.replace(/\-/g, '.')
|
||||||
|
.replace(/alpha/,'a')
|
||||||
|
.replace(/beta/,'b');
|
||||||
|
|
||||||
|
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbbreviatedNetNodeList(netNodes) {
|
||||||
|
let abbrList = '';
|
||||||
|
let currNet;
|
||||||
|
netNodes.forEach(netNode => {
|
||||||
|
if(_.isString(netNode)) {
|
||||||
|
netNode = Address.fromString(netNode);
|
||||||
|
}
|
||||||
|
if(currNet !== netNode.net) {
|
||||||
|
abbrList += `${netNode.net}/`;
|
||||||
|
currNet = netNode.net;
|
||||||
|
}
|
||||||
|
abbrList += `${netNode.node} `;
|
||||||
|
});
|
||||||
|
|
||||||
|
return abbrList.trim(); // remove trailing space
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH
|
||||||
|
//
|
||||||
|
function parseAbbreviatedNetNodeList(netNodes) {
|
||||||
|
const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g;
|
||||||
|
let net;
|
||||||
|
let m;
|
||||||
|
let results = [];
|
||||||
|
while(null !== (m = re.exec(netNodes))) {
|
||||||
|
if(m[1] && m[2]) {
|
||||||
|
net = parseInt(m[1]);
|
||||||
|
results.push(new Address( { net : net, node : parseInt(m[2]) } ));
|
||||||
|
} else if(net) {
|
||||||
|
results.push(new Address( { net : net, node : parseInt(m[3]) } ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return a FTS-0004.001 SEEN-BY entry(s) that include
|
||||||
|
// all pre-existing SEEN-BY entries with the addition
|
||||||
|
// of |additions|.
|
||||||
|
//
|
||||||
|
// See http://ftsc.org/docs/fts-0004.001
|
||||||
|
// and notes at http://ftsc.org/docs/fsc-0043.002.
|
||||||
|
//
|
||||||
|
// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm
|
||||||
|
//
|
||||||
|
// This method returns an sorted array of values, but
|
||||||
|
// not the "SEEN-BY" prefix itself
|
||||||
|
//
|
||||||
|
function getUpdatedSeenByEntries(existingEntries, additions) {
|
||||||
|
/*
|
||||||
|
From FTS-0004:
|
||||||
|
|
||||||
|
"There can be many seen-by lines at the end of Conference
|
||||||
|
Mail messages, and they are the real "meat" of the control
|
||||||
|
information. They are used to determine the systems to
|
||||||
|
receive the exported messages. The format of the line is:
|
||||||
|
|
||||||
|
SEEN-BY: 132/101 113 136/601 1014/1
|
||||||
|
|
||||||
|
The net/node numbers correspond to the net/node numbers of
|
||||||
|
the systems having already received the message. In this way
|
||||||
|
a message is never sent to a system twice. In a conference
|
||||||
|
with many participants the number of seen-by lines can be
|
||||||
|
very large. This line is added if it is not already a part
|
||||||
|
of the message, or added to if it already exists, each time
|
||||||
|
a message is exported to other systems. This is a REQUIRED
|
||||||
|
field, and Conference Mail will not function correctly if
|
||||||
|
this field is not put in place by other Echomail compatible
|
||||||
|
programs."
|
||||||
|
*/
|
||||||
|
existingEntries = existingEntries || [];
|
||||||
|
if(!_.isArray(existingEntries)) {
|
||||||
|
existingEntries = [ existingEntries ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!_.isString(additions)) {
|
||||||
|
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
|
||||||
|
}
|
||||||
|
|
||||||
|
additions = additions.sort(Address.getComparator());
|
||||||
|
|
||||||
|
//
|
||||||
|
// For now, we'll just append a new SEEN-BY entry
|
||||||
|
//
|
||||||
|
// :TODO: we should at least try and update what is already there in a smart way
|
||||||
|
existingEntries.push(getAbbreviatedNetNodeList(additions));
|
||||||
|
return existingEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdatedPathEntries(existingEntries, localAddress) {
|
||||||
|
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
|
||||||
|
|
||||||
|
existingEntries = existingEntries || [];
|
||||||
|
if(!_.isArray(existingEntries)) {
|
||||||
|
existingEntries = [ existingEntries ];
|
||||||
|
}
|
||||||
|
|
||||||
|
existingEntries.push(getAbbreviatedNetNodeList(
|
||||||
|
parseAbbreviatedNetNodeList(localAddress)));
|
||||||
|
|
||||||
|
return existingEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Return FTS-5000.001 "CHRS" value
|
||||||
|
// http://ftsc.org/docs/fts-5003.001
|
||||||
|
//
|
||||||
|
const ENCODING_TO_FTS_5003_001_CHARS = {
|
||||||
|
// level 1 - generally should not be used
|
||||||
|
ascii : [ 'ASCII', 1 ],
|
||||||
|
'us-ascii' : [ 'ASCII', 1 ],
|
||||||
|
|
||||||
|
// level 2 - 8 bit, ASCII based
|
||||||
|
cp437 : [ 'CP437', 2 ],
|
||||||
|
cp850 : [ 'CP850', 2 ],
|
||||||
|
|
||||||
|
// level 3 - reserved
|
||||||
|
|
||||||
|
// level 4
|
||||||
|
utf8 : [ 'UTF-8', 4 ],
|
||||||
|
'utf-8' : [ 'UTF-8', 4 ],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function getCharacterSetIdentifierByEncoding(encodingName) {
|
||||||
|
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
|
||||||
|
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEncodingFromCharacterSetIdentifier(chrs) {
|
||||||
|
const ident = chrs.split(' ')[0].toUpperCase();
|
||||||
|
|
||||||
|
// :TODO: fill in the rest!!!
|
||||||
|
return {
|
||||||
|
// level 1
|
||||||
|
'ASCII' : 'iso-646-1',
|
||||||
|
'DUTCH' : 'iso-646',
|
||||||
|
'FINNISH' : 'iso-646-10',
|
||||||
|
'FRENCH' : 'iso-646',
|
||||||
|
'CANADIAN' : 'iso-646',
|
||||||
|
'GERMAN' : 'iso-646',
|
||||||
|
'ITALIAN' : 'iso-646',
|
||||||
|
'NORWEIG' : 'iso-646',
|
||||||
|
'PORTU' : 'iso-646',
|
||||||
|
'SPANISH' : 'iso-656',
|
||||||
|
'SWEDISH' : 'iso-646-10',
|
||||||
|
'SWISS' : 'iso-646',
|
||||||
|
'UK' : 'iso-646',
|
||||||
|
'ISO-10' : 'iso-646-10',
|
||||||
|
|
||||||
|
// level 2
|
||||||
|
'CP437' : 'cp437',
|
||||||
|
'CP850' : 'cp850',
|
||||||
|
'CP852' : 'cp852',
|
||||||
|
'CP866' : 'cp866',
|
||||||
|
'CP848' : 'cp848',
|
||||||
|
'CP1250' : 'cp1250',
|
||||||
|
'CP1251' : 'cp1251',
|
||||||
|
'CP1252' : 'cp1252',
|
||||||
|
'CP10000' : 'macroman',
|
||||||
|
'LATIN-1' : 'iso-8859-1',
|
||||||
|
'LATIN-2' : 'iso-8859-2',
|
||||||
|
'LATIN-5' : 'iso-8859-9',
|
||||||
|
'LATIN-9' : 'iso-8859-15',
|
||||||
|
|
||||||
|
// level 4
|
||||||
|
'UTF-8' : 'utf8',
|
||||||
|
|
||||||
|
// deprecated stuff
|
||||||
|
'IBMPC' : 'cp1250', // :TODO: validate
|
||||||
|
'+7_FIDO' : 'cp866',
|
||||||
|
'+7' : 'cp866',
|
||||||
|
'MAC' : 'macroman', // :TODO: validate
|
||||||
|
|
||||||
|
}[ident];
|
||||||
}
|
}
|
|
@ -25,6 +25,10 @@ function MenuModule(options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.menuName = options.menuName;
|
this.menuName = options.menuName;
|
||||||
this.menuConfig = options.menuConfig;
|
this.menuConfig = options.menuConfig;
|
||||||
|
this.client = options.client;
|
||||||
|
|
||||||
|
// :TODO: this and the line below with .config creates empty ({}) objects in the theme --
|
||||||
|
// ...which we really should not do. If they aren't there already, don't use 'em.
|
||||||
this.menuConfig.options = options.menuConfig.options || {};
|
this.menuConfig.options = options.menuConfig.options || {};
|
||||||
this.menuMethods = {}; // methods called from @method's
|
this.menuMethods = {}; // methods called from @method's
|
||||||
|
|
||||||
|
@ -190,10 +194,7 @@ require('util').inherits(MenuModule, PluginModule);
|
||||||
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
|
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
|
||||||
|
|
||||||
|
|
||||||
MenuModule.prototype.enter = function(client) {
|
MenuModule.prototype.enter = function() {
|
||||||
this.client = client;
|
|
||||||
assert(_.isObject(client));
|
|
||||||
|
|
||||||
if(_.isString(this.menuConfig.status)) {
|
if(_.isString(this.menuConfig.status)) {
|
||||||
this.client.currentStatus = this.menuConfig.status;
|
this.client.currentStatus = this.menuConfig.status;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -131,7 +131,7 @@ MenuStack.prototype.goto = function(name, options, cb) {
|
||||||
modInst.restoreSavedState(options.savedState);
|
modInst.restoreSavedState(options.savedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
modInst.enter(self.client);
|
modInst.enter();
|
||||||
|
|
||||||
self.client.log.trace(
|
self.client.log.trace(
|
||||||
{ stack : _.map(self.stack, function(si) { return si.name; } ) },
|
{ stack : _.map(self.stack, function(si) { return si.name; } ) },
|
||||||
|
|
|
@ -4,10 +4,8 @@
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
var moduleUtil = require('./module_util.js');
|
var moduleUtil = require('./module_util.js');
|
||||||
var Log = require('./logger.js').log;
|
var Log = require('./logger.js').log;
|
||||||
var conf = require('./config.js'); // :TODO: remove me!
|
|
||||||
var Config = require('./config.js').config;
|
var Config = require('./config.js').config;
|
||||||
var asset = require('./asset.js');
|
var asset = require('./asset.js');
|
||||||
var theme = require('./theme.js');
|
|
||||||
var getFullConfig = require('./config_util.js').getFullConfig;
|
var getFullConfig = require('./config_util.js').getFullConfig;
|
||||||
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
||||||
var acsUtil = require('./acs_util.js');
|
var acsUtil = require('./acs_util.js');
|
||||||
|
@ -68,17 +66,18 @@ function loadMenu(options, cb) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function loadMenuModule(menuConfig, callback) {
|
function loadMenuModule(menuConfig, callback) {
|
||||||
var modAsset = asset.getModuleAsset(menuConfig.module);
|
|
||||||
var modSupplied = null !== modAsset;
|
|
||||||
|
|
||||||
var modLoadOpts = {
|
const modAsset = asset.getModuleAsset(menuConfig.module);
|
||||||
|
const modSupplied = null !== modAsset;
|
||||||
|
|
||||||
|
const modLoadOpts = {
|
||||||
name : modSupplied ? modAsset.asset : 'standard_menu',
|
name : modSupplied ? modAsset.asset : 'standard_menu',
|
||||||
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods,
|
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods,
|
||||||
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
|
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods',
|
||||||
};
|
};
|
||||||
|
|
||||||
moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) {
|
moduleUtil.loadModuleEx(modLoadOpts, function moduleLoaded(err, mod) {
|
||||||
var modData = {
|
const modData = {
|
||||||
name : modLoadOpts.name,
|
name : modLoadOpts.name,
|
||||||
config : menuConfig,
|
config : menuConfig,
|
||||||
mod : mod,
|
mod : mod,
|
||||||
|
@ -97,7 +96,8 @@ function loadMenu(options, cb) {
|
||||||
{
|
{
|
||||||
menuName : options.name,
|
menuName : options.name,
|
||||||
menuConfig : modData.config,
|
menuConfig : modData.config,
|
||||||
extraArgs : options.extraArgs
|
extraArgs : options.extraArgs,
|
||||||
|
client : options.client,
|
||||||
});
|
});
|
||||||
callback(null, moduleInstance);
|
callback(null, moduleInstance);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
@ -174,7 +174,7 @@ function handleAction(client, formData, conf) {
|
||||||
assert(_.isObject(conf));
|
assert(_.isObject(conf));
|
||||||
assert(_.isString(conf.action));
|
assert(_.isString(conf.action));
|
||||||
|
|
||||||
var actionAsset = asset.parseAsset(conf.action);
|
const actionAsset = asset.parseAsset(conf.action);
|
||||||
assert(_.isObject(actionAsset));
|
assert(_.isObject(actionAsset));
|
||||||
|
|
||||||
switch(actionAsset.type) {
|
switch(actionAsset.type) {
|
||||||
|
@ -245,84 +245,3 @@ function handleNext(client, nextSpec, conf) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// :TODO: Seems better in theme.js, but that includes ViewController...which would then include theme.js
|
|
||||||
// ...theme.js only brings in VC to create themed pause prompt. Perhaps that should live elsewhere
|
|
||||||
/*
|
|
||||||
function applyGeneralThemeCustomization(options) {
|
|
||||||
//
|
|
||||||
// options.name
|
|
||||||
// options.client
|
|
||||||
// options.type
|
|
||||||
// options.config
|
|
||||||
//
|
|
||||||
assert(_.isString(options.name));
|
|
||||||
assert(_.isObject(options.client));
|
|
||||||
assert("menus" === options.type || "prompts" === options.type);
|
|
||||||
|
|
||||||
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
|
|
||||||
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
|
|
||||||
|
|
||||||
if(themeConfig.config) {
|
|
||||||
Object.keys(themeConfig.config).forEach(function confEntry(conf) {
|
|
||||||
if(options.config[conf]) {
|
|
||||||
_.defaultsDeep(options.config[conf], themeConfig.config[conf]);
|
|
||||||
} else {
|
|
||||||
options.config[conf] = themeConfig.config[conf];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
function applyMciThemeCustomization(options) {
|
|
||||||
//
|
|
||||||
// options.name : menu/prompt name
|
|
||||||
// options.mci : menu/prompt .mci section
|
|
||||||
// options.client : client
|
|
||||||
// options.type : menu|prompt
|
|
||||||
// options.formId : (optional) form ID in cases where multiple forms may exist wanting their own customization
|
|
||||||
//
|
|
||||||
// In the case of formId, the theme must include the ID as well, e.g.:
|
|
||||||
// {
|
|
||||||
// ...
|
|
||||||
// "2" : {
|
|
||||||
// "TL1" : { ... }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
assert(_.isString(options.name));
|
|
||||||
assert("menus" === options.type || "prompts" === options.type);
|
|
||||||
assert(_.isObject(options.client));
|
|
||||||
|
|
||||||
if(_.isUndefined(options.mci)) {
|
|
||||||
options.mci = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
|
|
||||||
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
|
|
||||||
|
|
||||||
if(options.formId && _.has(themeConfig, options.formId.toString())) {
|
|
||||||
// form ID found - use exact match
|
|
||||||
themeConfig = themeConfig[options.formId];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(themeConfig.mci) {
|
|
||||||
Object.keys(themeConfig.mci).forEach(function mciEntry(mci) {
|
|
||||||
// :TODO: a better way to do this?
|
|
||||||
if(options.mci[mci]) {
|
|
||||||
_.defaults(options.mci[mci], themeConfig.mci[mci]);
|
|
||||||
} else {
|
|
||||||
options.mci[mci] = themeConfig.mci[mci];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
|
|
||||||
}
|
|
||||||
*/
|
|
292
core/message.js
292
core/message.js
|
@ -1,14 +1,15 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var msgDb = require('./database.js').dbs.message;
|
let msgDb = require('./database.js').dbs.message;
|
||||||
var wordWrapText = require('./word_wrap.js').wordWrapText;
|
let wordWrapText = require('./word_wrap.js').wordWrapText;
|
||||||
var ftnUtil = require('./ftn_util.js');
|
let ftnUtil = require('./ftn_util.js');
|
||||||
|
|
||||||
var uuid = require('node-uuid');
|
let uuid = require('node-uuid');
|
||||||
var async = require('async');
|
let async = require('async');
|
||||||
var _ = require('lodash');
|
let _ = require('lodash');
|
||||||
var assert = require('assert');
|
let assert = require('assert');
|
||||||
|
let moment = require('moment');
|
||||||
|
|
||||||
module.exports = Message;
|
module.exports = Message;
|
||||||
|
|
||||||
|
@ -16,18 +17,18 @@ function Message(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
this.messageId = options.messageId || 0; // always generated @ persist
|
this.messageId = options.messageId || 0; // always generated @ persist
|
||||||
this.areaName = options.areaName || Message.WellKnownAreaNames.Invalid;
|
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
|
||||||
this.uuid = uuid.v1();
|
this.uuid = options.uuid || uuid.v1();
|
||||||
this.replyToMsgId = options.replyToMsgId || 0;
|
this.replyToMsgId = options.replyToMsgId || 0;
|
||||||
this.toUserName = options.toUserName || '';
|
this.toUserName = options.toUserName || '';
|
||||||
this.fromUserName = options.fromUserName || '';
|
this.fromUserName = options.fromUserName || '';
|
||||||
this.subject = options.subject || '';
|
this.subject = options.subject || '';
|
||||||
this.message = options.message || '';
|
this.message = options.message || '';
|
||||||
|
|
||||||
if(_.isDate(options.modTimestamp)) {
|
if(_.isDate(options.modTimestamp) || moment.isMoment(options.modTimestamp)) {
|
||||||
this.modTimestamp = options.modTimestamp;
|
this.modTimestamp = moment(options.modTimestamp);
|
||||||
} else if(_.isString(options.modTimestamp)) {
|
} else if(_.isString(options.modTimestamp)) {
|
||||||
this.modTimestamp = new Date(options.modTimestamp);
|
this.modTimestamp = moment(options.modTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewCount = options.viewCount || 0;
|
this.viewCount = options.viewCount || 0;
|
||||||
|
@ -44,55 +45,30 @@ function Message(options) {
|
||||||
this.meta = options.meta;
|
this.meta = options.meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.meta = options.meta || {};
|
|
||||||
this.hashTags = options.hashTags || [];
|
this.hashTags = options.hashTags || [];
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.isValid = function() {
|
this.isValid = function() {
|
||||||
// :TODO: validate as much as possible
|
// :TODO: validate as much as possible
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isPrivate = function() {
|
this.isPrivate = function() {
|
||||||
return this.areaName === Message.WellKnownAreaNames.Private ? true : false;
|
return this.areaTag === Message.WellKnownAreaTags.Private ? true : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getMessageTimestampString = function(ts) {
|
this.getMessageTimestampString = function(ts) {
|
||||||
ts = ts || new Date();
|
ts = ts || moment();
|
||||||
return ts.toISOString();
|
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 : '',
|
Invalid : '',
|
||||||
Private : 'private_mail',
|
Private : 'private_mail',
|
||||||
Bulletin : 'local_bulletin',
|
Bulletin : 'local_bulletin',
|
||||||
};
|
};
|
||||||
|
|
||||||
// :TODO: This doesn't seem like a good way to go -- perhaps only for local/user2user, or just use
|
// :TODO: FTN stuff really doesn't belong here - move it elsewhere and/or just use the names directly when needed
|
||||||
// a system similar to the "last read" for general areas
|
|
||||||
Message.Status = {
|
|
||||||
New : 0,
|
|
||||||
Read : 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
Message.MetaCategories = {
|
Message.MetaCategories = {
|
||||||
System : 1, // ENiGMA1/2 stuff
|
System : 1, // ENiGMA1/2 stuff
|
||||||
FtnProperty : 2, // Various FTN network properties, ftn_cost, ftn_origin, ...
|
FtnProperty : 2, // Various FTN network properties, ftn_cost, ftn_origin, ...
|
||||||
|
@ -102,18 +78,27 @@ Message.MetaCategories = {
|
||||||
Message.SystemMetaNames = {
|
Message.SystemMetaNames = {
|
||||||
LocalToUserID : 'local_to_user_id',
|
LocalToUserID : 'local_to_user_id',
|
||||||
LocalFromUserID : 'local_from_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 = {
|
Message.FtnPropertyNames = {
|
||||||
FtnCost : 'ftn_cost',
|
|
||||||
FtnOrigNode : 'ftn_orig_node',
|
FtnOrigNode : 'ftn_orig_node',
|
||||||
FtnDestNode : 'ftn_dest_node',
|
FtnDestNode : 'ftn_dest_node',
|
||||||
FtnOrigNetwork : 'ftn_orig_network',
|
FtnOrigNetwork : 'ftn_orig_network',
|
||||||
FtnDestNetwork : 'ftn_dest_network',
|
FtnDestNetwork : 'ftn_dest_network',
|
||||||
|
FtnAttrFlags : 'ftn_attr_flags',
|
||||||
|
FtnCost : 'ftn_cost',
|
||||||
FtnOrigZone : 'ftn_orig_zone',
|
FtnOrigZone : 'ftn_orig_zone',
|
||||||
FtnDestZone : 'ftn_dest_zone',
|
FtnDestZone : 'ftn_dest_zone',
|
||||||
FtnOrigPoint : 'ftn_orig_point',
|
FtnOrigPoint : 'ftn_orig_point',
|
||||||
FtnDestPoint : 'ftn_dest_point',
|
FtnDestPoint : 'ftn_dest_point',
|
||||||
|
|
||||||
FtnAttribute : 'ftn_attribute',
|
FtnAttribute : 'ftn_attribute',
|
||||||
|
|
||||||
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
|
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
|
||||||
|
@ -132,6 +117,124 @@ Message.prototype.setLocalFromUserId = function(userId) {
|
||||||
this.meta.System.local_from_user_id = 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) {
|
Message.prototype.load = function(options, cb) {
|
||||||
assert(_.isString(options.uuid));
|
assert(_.isString(options.uuid));
|
||||||
|
|
||||||
|
@ -141,7 +244,7 @@ Message.prototype.load = function(options, cb) {
|
||||||
[
|
[
|
||||||
function loadMessage(callback) {
|
function loadMessage(callback) {
|
||||||
msgDb.get(
|
msgDb.get(
|
||||||
'SELECT message_id, area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' +
|
'SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' +
|
||||||
'message, modified_timestamp, view_count ' +
|
'message, modified_timestamp, view_count ' +
|
||||||
'FROM message ' +
|
'FROM message ' +
|
||||||
'WHERE message_uuid=? ' +
|
'WHERE message_uuid=? ' +
|
||||||
|
@ -149,14 +252,14 @@ Message.prototype.load = function(options, cb) {
|
||||||
[ options.uuid ],
|
[ options.uuid ],
|
||||||
function row(err, msgRow) {
|
function row(err, msgRow) {
|
||||||
self.messageId = msgRow.message_id;
|
self.messageId = msgRow.message_id;
|
||||||
self.areaName = msgRow.area_name;
|
self.areaTag = msgRow.area_tag;
|
||||||
self.messageUuid = msgRow.message_uuid;
|
self.messageUuid = msgRow.message_uuid;
|
||||||
self.replyToMsgId = msgRow.reply_to_message_id;
|
self.replyToMsgId = msgRow.reply_to_message_id;
|
||||||
self.toUserName = msgRow.to_user_name;
|
self.toUserName = msgRow.to_user_name;
|
||||||
self.fromUserName = msgRow.from_user_name;
|
self.fromUserName = msgRow.from_user_name;
|
||||||
self.subject = msgRow.subject;
|
self.subject = msgRow.subject;
|
||||||
self.message = msgRow.message;
|
self.message = msgRow.message;
|
||||||
self.modTimestamp = msgRow.modified_timestamp;
|
self.modTimestamp = moment(msgRow.modified_timestamp);
|
||||||
self.viewCount = msgRow.view_count;
|
self.viewCount = msgRow.view_count;
|
||||||
|
|
||||||
callback(err);
|
callback(err);
|
||||||
|
@ -164,18 +267,13 @@ Message.prototype.load = function(options, cb) {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function loadMessageMeta(callback) {
|
function loadMessageMeta(callback) {
|
||||||
// :TODO:
|
self.loadMeta(err => {
|
||||||
callback(null);
|
callback(err);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function loadHashTags(callback) {
|
function loadHashTags(callback) {
|
||||||
// :TODO:
|
// :TODO:
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
|
||||||
function loadMessageStatus(callback) {
|
|
||||||
if(options.user) {
|
|
||||||
// :TODO: Load from user_message_status
|
|
||||||
}
|
|
||||||
callback(null);
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
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) {
|
Message.prototype.persist = function(cb) {
|
||||||
|
|
||||||
if(!this.isValid()) {
|
if(!this.isValid()) {
|
||||||
cb(new Error('Cannot persist invalid message!'));
|
return cb(new Error('Cannot persist invalid message!'));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
let self = this;
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function beginTransaction(callback) {
|
function beginTransaction(callback) {
|
||||||
msgDb.run('BEGIN;', function transBegin(err) {
|
Message.startTransaction(err => {
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function storeMessage(callback) {
|
function storeMessage(callback) {
|
||||||
msgDb.run(
|
msgDb.run(
|
||||||
'INSERT INTO message (area_name, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) ' +
|
`INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp)
|
||||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?);', [ self.areaName, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||||
function msgInsert(err) {
|
[ 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) {
|
if(!err) {
|
||||||
self.messageId = this.lastID;
|
self.messageId = this.lastID;
|
||||||
}
|
}
|
||||||
|
@ -217,36 +347,40 @@ Message.prototype.persist = function(cb) {
|
||||||
if(!self.meta) {
|
if(!self.meta) {
|
||||||
callback(null);
|
callback(null);
|
||||||
} else {
|
} else {
|
||||||
// :TODO: this should be it's own method such that meta can be updated
|
/*
|
||||||
var metaStmt = msgDb.prepare(
|
Example of self.meta:
|
||||||
'INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) ' +
|
|
||||||
'VALUES (?, ?, ?, ?);');
|
|
||||||
|
|
||||||
for(var metaCategroy in self.meta) {
|
meta: {
|
||||||
async.each(Object.keys(self.meta[metaCategroy]), function meta(metaName, next) {
|
System: {
|
||||||
metaStmt.run(self.messageId, Message.MetaCategories[metaCategroy], metaName, self.meta[metaCategroy][metaName], function inserted(err) {
|
local_to_user_id: 1234,
|
||||||
next(err);
|
},
|
||||||
|
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) {
|
}, err => {
|
||||||
if(!err) {
|
nextCat(err);
|
||||||
metaStmt.finalize(function finalized() {
|
|
||||||
callback(null);
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
|
}, err => {
|
||||||
callback(err);
|
callback(err);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
function storeHashTags(callback) {
|
function storeHashTags(callback) {
|
||||||
// :TODO: hash tag support
|
// :TODO: hash tag support
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
err => {
|
||||||
msgDb.run(err ? 'ROLLBACK;' : 'COMMIT;', function transEnd(err) {
|
Message.endTransaction(err, transErr => {
|
||||||
cb(err, self.messageId);
|
cb(err ? err : transErr, self.messageId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,104 +1,283 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var msgDb = require('./database.js').dbs.message;
|
let msgDb = require('./database.js').dbs.message;
|
||||||
var Config = require('./config.js').config;
|
let Config = require('./config.js').config;
|
||||||
var Message = require('./message.js');
|
let Message = require('./message.js');
|
||||||
var Log = require('./logger.js').log;
|
let Log = require('./logger.js').log;
|
||||||
|
let checkAcs = require('./acs_util.js').checkAcs;
|
||||||
|
let msgNetRecord = require('./msg_network.js').recordMessage;
|
||||||
|
|
||||||
var async = require('async');
|
let async = require('async');
|
||||||
var _ = require('lodash');
|
let _ = require('lodash');
|
||||||
var assert = require('assert');
|
let assert = require('assert');
|
||||||
|
|
||||||
exports.getAvailableMessageAreas = getAvailableMessageAreas;
|
exports.getAvailableMessageConferences = getAvailableMessageConferences;
|
||||||
exports.getDefaultMessageArea = getDefaultMessageArea;
|
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
|
||||||
exports.getMessageAreaByName = getMessageAreaByName;
|
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
|
||||||
|
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
|
||||||
|
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
|
||||||
|
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
|
||||||
|
exports.getMessageConferenceByTag = getMessageConferenceByTag;
|
||||||
|
exports.getMessageAreaByTag = getMessageAreaByTag;
|
||||||
|
exports.changeMessageConference = changeMessageConference;
|
||||||
exports.changeMessageArea = changeMessageArea;
|
exports.changeMessageArea = changeMessageArea;
|
||||||
exports.getMessageListForArea = getMessageListForArea;
|
exports.getMessageListForArea = getMessageListForArea;
|
||||||
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
||||||
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
||||||
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
|
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
|
||||||
|
exports.persistMessage = persistMessage;
|
||||||
|
|
||||||
function getAvailableMessageAreas(options) {
|
const CONF_AREA_RW_ACS_DEFAULT = 'GM[users]';
|
||||||
// example: [ { "name" : "local_music", "desc" : "Music Discussion", "groups" : ["somegroup"] }, ... ]
|
const AREA_MANAGE_ACS_DEFAULT = 'GM[sysops]';
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
var areas = Config.messages.areas;
|
const AREA_ACS_DEFAULT = {
|
||||||
var avail = [];
|
read : CONF_AREA_RW_ACS_DEFAULT,
|
||||||
for(var i = 0; i < areas.length; ++i) {
|
write : CONF_AREA_RW_ACS_DEFAULT,
|
||||||
if(true !== options.includePrivate &&
|
manage : AREA_MANAGE_ACS_DEFAULT,
|
||||||
Message.WellKnownAreaNames.Private === areas[i].name)
|
};
|
||||||
{
|
|
||||||
continue;
|
function getAvailableMessageConferences(client, options) {
|
||||||
|
options = options || { includeSystemInternal : false };
|
||||||
|
|
||||||
|
// perform ACS check per conf & omit system_internal if desired
|
||||||
|
return _.omit(Config.messageConferences, (v, k) => {
|
||||||
|
if(!options.includeSystemInternal && 'system_internal' === k) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
avail.push(areas[i]);
|
const readAcs = v.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
return !checkAcs(client, readAcs);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return avail;
|
function getSortedAvailMessageConferences(client, options) {
|
||||||
}
|
var sorted = _.map(getAvailableMessageConferences(client, options), (v, k) => {
|
||||||
|
return {
|
||||||
function getDefaultMessageArea() {
|
confTag : k,
|
||||||
//
|
conf : v,
|
||||||
// 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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(index > -1) {
|
sorted.sort((a, b) => {
|
||||||
return availAreas[index];
|
return a.conf.name.localeCompare(b.conf.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an *object* of available areas within |confTag|
|
||||||
|
function getAvailableMessageAreasByConfTag(confTag, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
// :TODO: confTag === "" then find default
|
||||||
|
|
||||||
|
if(_.has(Config.messageConferences, [ confTag, 'areas' ])) {
|
||||||
|
const areas = Config.messageConferences[confTag].areas;
|
||||||
|
|
||||||
|
if(!options.client || true === options.noAcsCheck) {
|
||||||
|
// everything - no ACS checks
|
||||||
|
return areas;
|
||||||
|
} else {
|
||||||
|
// perform ACS check per area
|
||||||
|
return _.omit(areas, (v, k) => {
|
||||||
|
const readAcs = _.has(v, 'acs.read') ? v.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
return !checkAcs(options.client, readAcs);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeMessageArea(client, areaName, cb) {
|
function getSortedAvailMessageAreasByConfTag(confTag, options) {
|
||||||
|
const areas = getAvailableMessageAreasByConfTag(confTag, options);
|
||||||
|
|
||||||
|
// :TODO: should probably be using localeCompare / sort
|
||||||
|
return _.sortBy(_.map(areas, (v, k) => {
|
||||||
|
return {
|
||||||
|
areaTag : k,
|
||||||
|
area : v,
|
||||||
|
};
|
||||||
|
}), o => o.area.name); // sort by name
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultMessageConferenceTag(client, disableAcsCheck) {
|
||||||
|
//
|
||||||
|
// Find the first conference marked 'default'. If found,
|
||||||
|
// inspect |client| against *read* ACS using defaults if not
|
||||||
|
// specified.
|
||||||
|
//
|
||||||
|
// If the above fails, just go down the list until we get one
|
||||||
|
// that passes.
|
||||||
|
//
|
||||||
|
// It's possible that we end up with nothing here!
|
||||||
|
//
|
||||||
|
// Note that built in 'system_internal' is always ommited here
|
||||||
|
//
|
||||||
|
let defaultConf = _.findKey(Config.messageConferences, o => o.default);
|
||||||
|
if(defaultConf) {
|
||||||
|
const acs = Config.messageConferences[defaultConf].acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
if(true === disableAcsCheck || checkAcs(client, acs)) {
|
||||||
|
return defaultConf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// just use anything we can
|
||||||
|
defaultConf = _.findKey(Config.messageConferences, (o, k) => {
|
||||||
|
const acs = o.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
return 'system_internal' !== k && (true === disableAcsCheck || checkAcs(client, acs));
|
||||||
|
});
|
||||||
|
|
||||||
|
return defaultConf;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
|
||||||
|
//
|
||||||
|
// Similar to finding the default conference:
|
||||||
|
// Find the first entry marked 'default', if any. If found, check | client| against
|
||||||
|
// *read* ACS. If this fails, just find the first one we can that passes checks.
|
||||||
|
//
|
||||||
|
// It's possible that we end up with nothing!
|
||||||
|
//
|
||||||
|
confTag = confTag || getDefaultMessageConferenceTag(client);
|
||||||
|
|
||||||
|
if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) {
|
||||||
|
const areaPool = Config.messageConferences[confTag].areas;
|
||||||
|
let defaultArea = _.findKey(areaPool, o => o.default);
|
||||||
|
if(defaultArea) {
|
||||||
|
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||||
|
if(true === disableAcsCheck || checkAcs(client, readAcs)) {
|
||||||
|
return defaultArea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultArea = _.findKey(areaPool, (o, k) => {
|
||||||
|
const readAcs = _.has(areaPool, [ defaultArea, 'acs', 'read' ]) ? areaPool[defaultArea].acs.read : AREA_ACS_DEFAULT.read;
|
||||||
|
return (true === disableAcsCheck || checkAcs(client, readAcs));
|
||||||
|
});
|
||||||
|
|
||||||
|
return defaultArea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageConferenceByTag(confTag) {
|
||||||
|
return Config.messageConferences[confTag];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageAreaByTag(areaTag, optionalConfTag) {
|
||||||
|
const confs = Config.messageConferences;
|
||||||
|
|
||||||
|
if(_.isString(optionalConfTag)) {
|
||||||
|
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) {
|
||||||
|
return confs[optionalConfTag].areas[areaTag];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// No confTag to work with - we'll have to search through them all
|
||||||
|
//
|
||||||
|
var area;
|
||||||
|
_.forEach(confs, (v, k) => {
|
||||||
|
if(_.has(v, [ 'areas', areaTag ])) {
|
||||||
|
area = v.areas[areaTag];
|
||||||
|
return false; // stop iteration
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMessageConference(client, confTag, cb) {
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function getConf(callback) {
|
||||||
|
const conf = getMessageConferenceByTag(confTag);
|
||||||
|
|
||||||
|
if(conf) {
|
||||||
|
callback(null, conf);
|
||||||
|
} else {
|
||||||
|
callback(new Error('Invalid message conference tag'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function getDefaultAreaInConf(conf, callback) {
|
||||||
|
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
|
||||||
|
const area = getMessageAreaByTag(areaTag, confTag);
|
||||||
|
|
||||||
|
if(area) {
|
||||||
|
callback(null, conf, { areaTag : areaTag, area : area } );
|
||||||
|
} else {
|
||||||
|
callback(new Error('No available areas for this user in conference'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function validateAccess(conf, areaInfo, callback) {
|
||||||
|
const confAcs = conf.acs || CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
|
||||||
|
if(!checkAcs(client, confAcs)) {
|
||||||
|
callback(new Error('User does not have access to this conference'));
|
||||||
|
} else {
|
||||||
|
const areaAcs = _.has(areaInfo, 'area.acs.read') ? areaInfo.area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
if(!checkAcs(client, areaAcs)) {
|
||||||
|
callback(new Error('User does not have access to default area in this conference'));
|
||||||
|
} else {
|
||||||
|
callback(null, conf, areaInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function changeConferenceAndArea(conf, areaInfo, callback) {
|
||||||
|
const newProps = {
|
||||||
|
message_conf_tag : confTag,
|
||||||
|
message_area_tag : areaInfo.areaTag,
|
||||||
|
};
|
||||||
|
client.user.persistProperties(newProps, err => {
|
||||||
|
callback(err, conf, areaInfo);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
function complete(err, conf, areaInfo) {
|
||||||
|
if(!err) {
|
||||||
|
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed');
|
||||||
|
} else {
|
||||||
|
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference');
|
||||||
|
}
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMessageArea(client, areaTag, cb) {
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function getArea(callback) {
|
function getArea(callback) {
|
||||||
var area = getMessageAreaByName(areaName);
|
const area = getMessageAreaByTag(areaTag);
|
||||||
|
|
||||||
if(area) {
|
if(area) {
|
||||||
callback(null, area);
|
callback(null, area);
|
||||||
} else {
|
} else {
|
||||||
callback(new Error('Invalid message area'));
|
callback(new Error('Invalid message area tag'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function validateAccess(area, callback) {
|
function validateAccess(area, callback) {
|
||||||
if(_.isArray(area.groups) && !
|
//
|
||||||
client.user.isGroupMember(area.groups))
|
// Need at least *read* to access the area
|
||||||
{
|
//
|
||||||
|
const readAcs = _.has(area, 'acs.read') ? area.acs.read : CONF_AREA_RW_ACS_DEFAULT;
|
||||||
|
if(!checkAcs(client, readAcs)) {
|
||||||
callback(new Error('User does not have access to this area'));
|
callback(new Error('User does not have access to this area'));
|
||||||
} else {
|
} else {
|
||||||
callback(null, area);
|
callback(null, area);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function changeArea(area, callback) {
|
function changeArea(area, callback) {
|
||||||
client.user.persistProperty('message_area_name', area.name, function persisted(err) {
|
client.user.persistProperty('message_area_tag', areaTag, function persisted(err) {
|
||||||
callback(err, area);
|
callback(err, area);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err, area) {
|
function complete(err, area) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
client.log.info( area, 'Current message area changed');
|
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed');
|
||||||
} else {
|
} else {
|
||||||
client.log.warn( { area : area, error : err.message }, 'Could not change message area');
|
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(err);
|
cb(err);
|
||||||
|
@ -119,9 +298,9 @@ function getMessageFromRow(row) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
||||||
//
|
//
|
||||||
// If |areaName| is Message.WellKnownAreaNames.Private,
|
// If |areaTag| is Message.WellKnownAreaTags.Private,
|
||||||
// only messages addressed to |userId| should be returned.
|
// only messages addressed to |userId| should be returned.
|
||||||
//
|
//
|
||||||
// Only messages > lastMessageId should be returned
|
// Only messages > lastMessageId should be returned
|
||||||
|
@ -131,7 +310,7 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function getLastMessageId(callback) {
|
function getLastMessageId(callback) {
|
||||||
getMessageAreaLastReadId(userId, areaName, function fetched(err, lastMessageId) {
|
getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) {
|
||||||
callback(null, lastMessageId || 0); // note: willingly ignoring any errors here!
|
callback(null, lastMessageId || 0); // note: willingly ignoring any errors here!
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -139,9 +318,9 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
||||||
var sql =
|
var sql =
|
||||||
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
||||||
'FROM message ' +
|
'FROM message ' +
|
||||||
'WHERE area_name="' + areaName + '" AND message_id > ' + lastMessageId;
|
'WHERE area_tag ="' + areaTag + '" AND message_id > ' + lastMessageId;
|
||||||
|
|
||||||
if(Message.WellKnownAreaNames.Private === areaName) {
|
if(Message.WellKnownAreaTags.Private === areaTag) {
|
||||||
sql +=
|
sql +=
|
||||||
' AND message_id in (' +
|
' AND message_id in (' +
|
||||||
'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System +
|
'SELECT message_id from message_meta where meta_category=' + Message.MetaCategories.System +
|
||||||
|
@ -150,8 +329,6 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
||||||
|
|
||||||
sql += ' ORDER BY message_id;';
|
sql += ' ORDER BY message_id;';
|
||||||
|
|
||||||
console.log(sql)
|
|
||||||
|
|
||||||
msgDb.each(sql, function msgRow(err, row) {
|
msgDb.each(sql, function msgRow(err, row) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
msgList.push(getMessageFromRow(row));
|
msgList.push(getMessageFromRow(row));
|
||||||
|
@ -160,18 +337,17 @@ function getNewMessagesInAreaForUser(userId, areaName, cb) {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
console.log(msgList)
|
|
||||||
cb(err, msgList);
|
cb(err, msgList);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageListForArea(options, areaName, cb) {
|
function getMessageListForArea(options, areaTag, cb) {
|
||||||
//
|
//
|
||||||
// options.client (required)
|
// options.client (required)
|
||||||
//
|
//
|
||||||
|
|
||||||
options.client.log.debug( { areaName : areaName }, 'Fetching available messages');
|
options.client.log.debug( { areaTag : areaTag }, 'Fetching available messages');
|
||||||
|
|
||||||
assert(_.isObject(options.client));
|
assert(_.isObject(options.client));
|
||||||
|
|
||||||
|
@ -193,9 +369,9 @@ function getMessageListForArea(options, areaName, cb) {
|
||||||
msgDb.each(
|
msgDb.each(
|
||||||
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
'SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count ' +
|
||||||
'FROM message ' +
|
'FROM message ' +
|
||||||
'WHERE area_name=? ' +
|
'WHERE area_tag = ? ' +
|
||||||
'ORDER BY message_id;',
|
'ORDER BY message_id;',
|
||||||
[ areaName.toLowerCase() ],
|
[ areaTag.toLowerCase() ],
|
||||||
function msgRow(err, row) {
|
function msgRow(err, row) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
msgList.push(getMessageFromRow(row));
|
msgList.push(getMessageFromRow(row));
|
||||||
|
@ -214,24 +390,24 @@ function getMessageListForArea(options, areaName, cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageAreaLastReadId(userId, areaName, cb) {
|
function getMessageAreaLastReadId(userId, areaTag, cb) {
|
||||||
msgDb.get(
|
msgDb.get(
|
||||||
'SELECT message_id ' +
|
'SELECT message_id ' +
|
||||||
'FROM user_message_area_last_read ' +
|
'FROM user_message_area_last_read ' +
|
||||||
'WHERE user_id = ? AND area_name = ?;',
|
'WHERE user_id = ? AND area_tag = ?;',
|
||||||
[ userId, areaName ],
|
[ userId, areaTag ],
|
||||||
function complete(err, row) {
|
function complete(err, row) {
|
||||||
cb(err, row ? row.message_id : 0);
|
cb(err, row ? row.message_id : 0);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
|
function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) {
|
||||||
// :TODO: likely a better way to do this...
|
// :TODO: likely a better way to do this...
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function getCurrent(callback) {
|
function getCurrent(callback) {
|
||||||
getMessageAreaLastReadId(userId, areaName, function result(err, lastId) {
|
getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) {
|
||||||
lastId = lastId || 0;
|
lastId = lastId || 0;
|
||||||
callback(null, lastId); // ignore errors as we default to 0
|
callback(null, lastId); // ignore errors as we default to 0
|
||||||
});
|
});
|
||||||
|
@ -239,27 +415,45 @@ function updateMessageAreaLastReadId(userId, areaName, messageId, cb) {
|
||||||
function update(lastId, callback) {
|
function update(lastId, callback) {
|
||||||
if(messageId > lastId) {
|
if(messageId > lastId) {
|
||||||
msgDb.run(
|
msgDb.run(
|
||||||
'REPLACE INTO user_message_area_last_read (user_id, area_name, message_id) ' +
|
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
|
||||||
'VALUES (?, ?, ?);',
|
'VALUES (?, ?, ?);',
|
||||||
[ userId, areaName, messageId ],
|
[ userId, areaTag, messageId ],
|
||||||
callback
|
function written(err) {
|
||||||
|
callback(err, true); // true=didUpdate
|
||||||
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err, didUpdate) {
|
||||||
if(err) {
|
if(err) {
|
||||||
Log.debug(
|
Log.debug(
|
||||||
{ error : err.toString(), userId : userId, areaName : areaName, messageId : messageId },
|
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId },
|
||||||
'Failed updating area last read ID');
|
'Failed updating area last read ID');
|
||||||
} else {
|
} else {
|
||||||
|
if(true === didUpdate) {
|
||||||
Log.trace(
|
Log.trace(
|
||||||
{ userId : userId, areaName : areaName, messageId : messageId },
|
{ userId : userId, areaTag : areaTag, messageId : messageId },
|
||||||
'Area last read ID updated');
|
'Area last read ID updated');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cb(err);
|
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 */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Config = require('./config.js').config;
|
// ENiGMA½
|
||||||
var miscUtil = require('./misc_util.js');
|
let Config = require('./config.js').config;
|
||||||
|
let miscUtil = require('./misc_util.js');
|
||||||
|
|
||||||
var fs = require('fs');
|
// standard/deps
|
||||||
var paths = require('path');
|
let fs = require('fs');
|
||||||
var _ = require('lodash');
|
let paths = require('path');
|
||||||
var assert = require('assert');
|
let _ = require('lodash');
|
||||||
|
let assert = require('assert');
|
||||||
|
let async = require('async');
|
||||||
|
|
||||||
// exports
|
// exports
|
||||||
exports.loadModuleEx = loadModuleEx;
|
exports.loadModuleEx = loadModuleEx;
|
||||||
|
@ -19,15 +22,19 @@ function loadModuleEx(options, cb) {
|
||||||
assert(_.isString(options.name));
|
assert(_.isString(options.name));
|
||||||
assert(_.isString(options.path));
|
assert(_.isString(options.path));
|
||||||
|
|
||||||
var modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
|
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null;
|
||||||
|
|
||||||
if(_.isObject(modConfig) && false === modConfig.enabled) {
|
if(_.isObject(modConfig) && false === modConfig.enabled) {
|
||||||
cb(new Error('Module "' + options.name + '" is disabled'));
|
cb(new Error('Module "' + options.name + '" is disabled'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mod;
|
||||||
try {
|
try {
|
||||||
var mod = require(paths.join(options.path, options.name + '.js'));
|
mod = require(paths.join(options.path, options.name + '.js'));
|
||||||
|
} catch(e) {
|
||||||
|
cb(e);
|
||||||
|
}
|
||||||
|
|
||||||
if(!_.isObject(mod.moduleInfo)) {
|
if(!_.isObject(mod.moduleInfo)) {
|
||||||
cb(new Error('Module is missing "moduleInfo" section'));
|
cb(new Error('Module is missing "moduleInfo" section'));
|
||||||
|
@ -39,13 +46,10 @@ function loadModuleEx(options, cb) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safe configuration, if any, for convience to the module
|
// Ref configuration, if any, for convience to the module
|
||||||
mod.runtime = { config : modConfig };
|
mod.runtime = { config : modConfig };
|
||||||
|
|
||||||
cb(null, mod);
|
cb(null, mod);
|
||||||
} catch(e) {
|
|
||||||
cb(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadModule(name, category, cb) {
|
function loadModule(name, category, cb) {
|
||||||
|
@ -61,19 +65,26 @@ function loadModule(name, category, cb) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadModulesForCategory(category, cb) {
|
function loadModulesForCategory(category, iterator, complete) {
|
||||||
var path = Config.paths[category];
|
|
||||||
|
|
||||||
fs.readdir(path, function onFiles(err, files) {
|
fs.readdir(Config.paths[category], (err, files) => {
|
||||||
if(err) {
|
if(err) {
|
||||||
cb(err);
|
return iterator(err);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filtered = files.filter(function onFilter(file) { return '.js' === paths.extname(file); });
|
const jsModules = files.filter(file => {
|
||||||
filtered.forEach(function onFile(file) {
|
return '.js' === paths.extname(file);
|
||||||
var modName = paths.basename(file, '.js');
|
});
|
||||||
loadModule(paths.basename(file, '.js'), category, cb);
|
|
||||||
|
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;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getOutputText = function(startIndex, endIndex, includeEol) {
|
this.getOutputText = function(startIndex, endIndex, eolMarker) {
|
||||||
var lines = self.getTextLines(startIndex, endIndex);
|
let lines = self.getTextLines(startIndex, endIndex);
|
||||||
|
let text = '';
|
||||||
//
|
|
||||||
// Convert lines to contiguous string -- all expanded
|
|
||||||
// tabs put back to single '\t' characters.
|
|
||||||
//
|
|
||||||
var text = '';
|
|
||||||
var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
|
var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
|
||||||
for(var i = 0; i < lines.length; ++i) {
|
|
||||||
text += lines[i].text.replace(re, '\t');
|
lines.forEach(line => {
|
||||||
if(includeEol && lines[i].eol) {
|
text += line.text.replace(re, '\t');
|
||||||
text += '\n';
|
if(eolMarker && line.eol) {
|
||||||
}
|
text += eolMarker;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
};
|
}
|
||||||
|
|
||||||
this.getContiguousText = function(startIndex, endIndex, includeEol) {
|
this.getContiguousText = function(startIndex, endIndex, includeEol) {
|
||||||
var lines = self.getTextLines(startIndex, endIndex);
|
var lines = self.getTextLines(startIndex, endIndex);
|
||||||
|
@ -1018,7 +1015,7 @@ MultiLineEditTextView.prototype.addText = function(text) {
|
||||||
};
|
};
|
||||||
|
|
||||||
MultiLineEditTextView.prototype.getData = function() {
|
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) {
|
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 MenuModule = require('./menu_module.js').MenuModule;
|
||||||
var ViewController = require('../core/view_controller.js').ViewController;
|
var ViewController = require('../core/view_controller.js').ViewController;
|
||||||
|
|
||||||
|
var _ = require('lodash');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
|
@ -36,10 +37,11 @@ function NewScanModule(options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var config = this.menuConfig.config;
|
var config = this.menuConfig.config;
|
||||||
|
|
||||||
this.currentStep = 'messageAreas';
|
this.currentStep = 'messageConferences';
|
||||||
this.currentScanAux = 0; // e.g. Message.WellKnownAreaNames.Private when currentSteps = messageAreas
|
this.currentScanAux = {};
|
||||||
|
|
||||||
this.scanStartFmt = config.scanStartFmt || 'Scanning {desc}...';
|
// :TODO: Make this conf/area specific:
|
||||||
|
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
|
||||||
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
|
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
|
||||||
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
|
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
|
||||||
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
|
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
|
||||||
|
@ -58,9 +60,64 @@ function NewScanModule(options) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newScanMessageArea = function(cb) {
|
this.newScanMessageConference = function(cb) {
|
||||||
var availMsgAreas = msgArea.getAvailableMessageAreas( { includePrivate : true } );
|
// lazy init
|
||||||
var currentArea = availMsgAreas[self.currentScanAux];
|
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,
|
// Scan and update index until we find something. If results are found,
|
||||||
|
@ -70,8 +127,8 @@ function NewScanModule(options) {
|
||||||
[
|
[
|
||||||
function checkAndUpdateIndex(callback) {
|
function checkAndUpdateIndex(callback) {
|
||||||
// Advance to next area if possible
|
// Advance to next area if possible
|
||||||
if(availMsgAreas.length >= self.currentScanAux + 1) {
|
if(sortedAreas.length >= self.currentScanAux.area + 1) {
|
||||||
self.currentScanAux += 1;
|
self.currentScanAux.area += 1;
|
||||||
callback(null);
|
callback(null);
|
||||||
} else {
|
} else {
|
||||||
self.updateScanStatus(self.scanCompleteMsg);
|
self.updateScanStatus(self.scanCompleteMsg);
|
||||||
|
@ -80,21 +137,29 @@ function NewScanModule(options) {
|
||||||
},
|
},
|
||||||
function updateStatusScanStarted(callback) {
|
function updateStatusScanStarted(callback) {
|
||||||
self.updateScanStatus(self.scanStartFmt.format({
|
self.updateScanStatus(self.scanStartFmt.format({
|
||||||
desc : currentArea.desc,
|
confName : conf.conf.name,
|
||||||
|
confDesc : conf.conf.desc,
|
||||||
|
areaName : currentArea.area.name,
|
||||||
|
areaDesc : currentArea.area.desc,
|
||||||
}));
|
}));
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
function newScanAreaAndGetMessages(callback) {
|
function newScanAreaAndGetMessages(callback) {
|
||||||
msgArea.getNewMessagesInAreaForUser(
|
msgArea.getNewMessagesInAreaForUser(
|
||||||
self.client.user.userId, currentArea.name, function msgs(err, msgList) {
|
self.client.user.userId, currentArea.areaTag, function msgs(err, msgList) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
if(0 === msgList.length) {
|
if(0 === msgList.length) {
|
||||||
self.updateScanStatus(self.scanFinishNoneFmt.format({
|
self.updateScanStatus(self.scanFinishNoneFmt.format({
|
||||||
desc : currentArea.desc,
|
confName : conf.conf.name,
|
||||||
|
confDesc : conf.conf.desc,
|
||||||
|
areaName : currentArea.area.name,
|
||||||
|
areaDesc : currentArea.area.desc,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
self.updateScanStatus(self.scanFinishNewFmt.format({
|
self.updateScanStatus(self.scanFinishNewFmt.format({
|
||||||
desc : currentArea.desc,
|
confName : conf.conf.name,
|
||||||
|
confDesc : conf.conf.desc,
|
||||||
|
areaName : currentArea.area.name,
|
||||||
count : msgList.length,
|
count : msgList.length,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -107,14 +172,14 @@ function NewScanModule(options) {
|
||||||
if(msgList && msgList.length > 0) {
|
if(msgList && msgList.length > 0) {
|
||||||
var nextModuleOpts = {
|
var nextModuleOpts = {
|
||||||
extraArgs: {
|
extraArgs: {
|
||||||
messageAreaName : currentArea.name,
|
messageAreaTag : currentArea.areaTag,
|
||||||
messageList : msgList,
|
messageList : msgList,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
|
self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
|
||||||
} else {
|
} else {
|
||||||
self.newScanMessageArea(cb);
|
self.newScanMessageArea(conf, cb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -161,8 +226,8 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
|
||||||
},
|
},
|
||||||
function performCurrentStepScan(callback) {
|
function performCurrentStepScan(callback) {
|
||||||
switch(self.currentStep) {
|
switch(self.currentStep) {
|
||||||
case 'messageAreas' :
|
case 'messageConferences' :
|
||||||
self.newScanMessageArea(function scanComplete(err) {
|
self.newScanMessageConference(function scanComplete(err) {
|
||||||
callback(null); // finished
|
callback(null); // finished
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -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 Config = require('./config.js').config;
|
||||||
var Log = require('./logger.js').log;
|
var Log = require('./logger.js').log;
|
||||||
var getMessageAreaByName = require('./message_area.js').getMessageAreaByName;
|
var getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
|
||||||
var clientConnections = require('./client_connections.js');
|
var clientConnections = require('./client_connections.js');
|
||||||
var sysProp = require('./system_property.js');
|
var sysProp = require('./system_property.js');
|
||||||
|
|
||||||
|
@ -63,8 +63,13 @@ function getPredefinedMCIValue(client, code) {
|
||||||
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
|
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
|
||||||
},
|
},
|
||||||
|
|
||||||
MA : function messageAreaDescription() {
|
MA : function messageAreaName() {
|
||||||
var area = getMessageAreaByName(client.user.properties.message_area_name);
|
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
|
||||||
|
return area ? area.name : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
ML : function messageAreaDescription() {
|
||||||
|
const area = getMessageAreaByTag(client.user.properties.message_area_tag);
|
||||||
return area ? area.desc : '';
|
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),
|
privateKey : fs.readFileSync(Config.servers.ssh.privateKeyPem),
|
||||||
passphrase : Config.servers.ssh.privateKeyPass,
|
passphrase : Config.servers.ssh.privateKeyPass,
|
||||||
ident : 'enigma-bbs-' + enigVersion + '-srv',
|
ident : 'enigma-bbs-' + enigVersion + '-srv',
|
||||||
|
|
||||||
// Note that sending 'banner' breaks at least EtherTerm!
|
// Note that sending 'banner' breaks at least EtherTerm!
|
||||||
debug : function debugSsh(dbgLine) {
|
debug : function debugSsh(dbgLine) {
|
||||||
if(true === Config.servers.ssh.traceConnections) {
|
if(true === Config.servers.ssh.traceConnections) {
|
||||||
|
|
|
@ -18,8 +18,8 @@ function StandardMenuModule(menuConfig) {
|
||||||
require('util').inherits(StandardMenuModule, MenuModule);
|
require('util').inherits(StandardMenuModule, MenuModule);
|
||||||
|
|
||||||
|
|
||||||
StandardMenuModule.prototype.enter = function(client) {
|
StandardMenuModule.prototype.enter = function() {
|
||||||
StandardMenuModule.super_.prototype.enter.call(this, client);
|
StandardMenuModule.super_.prototype.enter.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
StandardMenuModule.prototype.beforeArt = function() {
|
StandardMenuModule.prototype.beforeArt = function() {
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var miscUtil = require('./misc_util.js');
|
let miscUtil = require('./misc_util.js');
|
||||||
|
|
||||||
|
let iconv = require('iconv-lite');
|
||||||
|
|
||||||
exports.stylizeString = stylizeString;
|
exports.stylizeString = stylizeString;
|
||||||
exports.pad = pad;
|
exports.pad = pad;
|
||||||
exports.replaceAt = replaceAt;
|
exports.replaceAt = replaceAt;
|
||||||
exports.isPrintable = isPrintable;
|
exports.isPrintable = isPrintable;
|
||||||
exports.debugEscapedString = debugEscapedString;
|
exports.debugEscapedString = debugEscapedString;
|
||||||
|
exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
||||||
|
|
||||||
// :TODO: create Unicode verison of this
|
// :TODO: create Unicode verison of this
|
||||||
var VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
|
var VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
|
||||||
|
@ -176,6 +178,23 @@ function debugEscapedString(s) {
|
||||||
return JSON.stringify(s).slice(1, -1);
|
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
|
// Extend String.format's object syntax with some modifiers
|
||||||
// e.g.: '{username!styleL33t}'.format( { username : 'Leet User' } ) -> "L33t U53r"
|
// 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) {
|
[ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) {
|
||||||
_.keys(mergedTheme[areaName]).forEach(function menuEntry(menuName) {
|
_.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
|
||||||
var createdFormSection = false;
|
var createdFormSection = false;
|
||||||
var mergedThemeMenu = mergedTheme[areaName][menuName];
|
var mergedThemeMenu = mergedTheme[sectionName][menuName];
|
||||||
|
|
||||||
if(_.has(theme, [ 'customization', areaName, menuName ])) {
|
if(_.has(theme, [ 'customization', sectionName, menuName ])) {
|
||||||
var menuTheme = theme.customization[areaName][menuName];
|
var menuTheme = theme.customization[sectionName][menuName];
|
||||||
|
|
||||||
// config block is direct assign/overwrite
|
// config block is direct assign/overwrite
|
||||||
// :TODO: should probably be _.merge()
|
// :TODO: should probably be _.merge()
|
||||||
|
@ -217,7 +217,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
||||||
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
|
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if('menus' === areaName) {
|
if('menus' === sectionName) {
|
||||||
if(_.isObject(mergedThemeMenu.form)) {
|
if(_.isObject(mergedThemeMenu.form)) {
|
||||||
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
|
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
|
||||||
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
|
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
|
||||||
|
@ -233,7 +233,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
||||||
createdFormSection = true;
|
createdFormSection = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if('prompts' === areaName) {
|
} else if('prompts' === sectionName) {
|
||||||
// no 'form' or form keys for prompts -- direct to mci
|
// no 'form' or form keys for prompts -- direct to mci
|
||||||
applyToForm(mergedThemeMenu, menuTheme);
|
applyToForm(mergedThemeMenu, menuTheme);
|
||||||
}
|
}
|
||||||
|
@ -247,7 +247,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
||||||
// * There is/was no explicit 'form' section
|
// * There is/was no explicit 'form' section
|
||||||
// * There is no 'prompt' specified
|
// * There is no 'prompt' specified
|
||||||
//
|
//
|
||||||
if('menus' === areaName && !_.isString(mergedThemeMenu.prompt) &&
|
if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
|
||||||
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
|
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
|
||||||
{
|
{
|
||||||
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
|
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
|
||||||
|
@ -523,7 +523,8 @@ function displayThemedPause(options, cb) {
|
||||||
if(options.clearPrompt) {
|
if(options.clearPrompt) {
|
||||||
if(artInfo.startRow && artInfo.height) {
|
if(artInfo.startRow && artInfo.height) {
|
||||||
options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
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));
|
options.client.term.rawWrite(ansi.deleteLine(artInfo.height));
|
||||||
} else {
|
} else {
|
||||||
options.client.term.rawWrite(ansi.eraseLine(1))
|
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);
|
this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.autoScale.width) {
|
if(self.autoScale.width) {
|
||||||
var l = 0;
|
var l = 0;
|
||||||
self.items.forEach(function item(i) {
|
self.items.forEach(function item(i) {
|
||||||
if(i.text.length > l) {
|
if(i.text.length > l) {
|
||||||
|
@ -149,6 +149,17 @@ VerticalMenuView.prototype.setFocus = function(focused) {
|
||||||
VerticalMenuView.prototype.setFocusItemIndex = function(index) {
|
VerticalMenuView.prototype.setFocusItemIndex = function(index) {
|
||||||
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
||||||
|
|
||||||
|
//this.updateViewVisibleItems();
|
||||||
|
|
||||||
|
// :TODO: |viewWindow| must be updated to reflect position change --
|
||||||
|
// if > visibile then += by diff, if < visible
|
||||||
|
|
||||||
|
if(this.focusedItemIndex > this.viewWindow.bottom) {
|
||||||
|
} else if (this.focusedItemIndex < this.viewWindow.top) {
|
||||||
|
// this.viewWindow.top--;
|
||||||
|
// this.viewWindow.bottom--;
|
||||||
|
}
|
||||||
|
|
||||||
this.redraw();
|
this.redraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -488,7 +488,6 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) {
|
||||||
assert(_.isObject(options.mciMap));
|
assert(_.isObject(options.mciMap));
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var promptName = _.isString(options.promptName) ? options.promptName : self.client.currentMenuModule.menuConfig.prompt;
|
|
||||||
var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig;
|
var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig;
|
||||||
var initialFocusId = 1; // default to first
|
var initialFocusId = 1; // default to first
|
||||||
|
|
||||||
|
|
|
@ -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
|
### Menus
|
||||||
TODO: Documentation on menu.hjson, etc.
|
TODO: Documentation on menu.hjson, etc.
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Message Networks
|
||||||
|
Message networks are configured in `messageNetworks` section of `config.hjson`. Each network type has it's own sub section such as `ftn` for FidoNet Technology Network (FTN) style networks.
|
||||||
|
|
||||||
|
## FidoNet Technology Network (FTN)
|
||||||
|
FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`.
|
||||||
|
|
||||||
|
### Networks
|
||||||
|
The `networks` section contains a sub section for network(s) you wish you join your board with. Each entry's key name can be referenced elsewhere in `config.hjson` for FTN oriented configurations.
|
||||||
|
|
||||||
|
**Members**:
|
||||||
|
* `localAddress` (required): FTN address of **your local system**
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```hjson
|
||||||
|
{
|
||||||
|
messageNetworks: {
|
||||||
|
ftn: {
|
||||||
|
networks: {
|
||||||
|
agoranet: {
|
||||||
|
localAddress: "46:3/102"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Areas
|
||||||
|
The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages.
|
||||||
|
|
||||||
|
When importing, messages will be placed in the local area that matches key under `areas`.
|
||||||
|
|
||||||
|
**Members**:
|
||||||
|
* `network` (required): Associated network from the `networks` section
|
||||||
|
* `tag` (required): FTN area tag
|
||||||
|
* `uplinks`: An array of FTN address uplink(s) for this network
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```hjson
|
||||||
|
{
|
||||||
|
messageNetworks: {
|
||||||
|
ftn: {
|
||||||
|
areas: {
|
||||||
|
agoranet_bbs: { /* found within messageConferences */
|
||||||
|
network: agoranet
|
||||||
|
tag: AGN_BBS
|
||||||
|
uplinks: "46:1/100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### BSO Import / Export
|
||||||
|
The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers::ftn_bso`.
|
||||||
|
|
||||||
|
**Members**:
|
||||||
|
* `defaultZone` (required): Sets the default BSO outbound zone
|
||||||
|
* `defaultNetwork` (optional): Sets the default network name from `messageNetworks::ftn::networks`. **Required if more than one network is defined**.
|
||||||
|
* `paths` (optional): Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`.
|
||||||
|
* `packetTargetByteSize` (optional): Overrides the system *target* packet (.pkt) size of 512000 bytes (512k)
|
||||||
|
* `bundleTargetByteSize` (optional): Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M)
|
||||||
|
* `schedule` (required): See Scheduling
|
||||||
|
* `nodes` (required): See Nodes
|
||||||
|
|
||||||
|
#### Nodes
|
||||||
|
The `nodes` section defines how to export messages for one or more uplinks.
|
||||||
|
|
||||||
|
A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain.
|
||||||
|
|
||||||
|
**Members**:
|
||||||
|
* `packetType` (optional): `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability
|
||||||
|
* `packetPassword` (optional): Password for the packet
|
||||||
|
* `encoding` (optional): Encoding to use for message bodies; Defaults to `utf-8`
|
||||||
|
* `archiveType` (optional): Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```hjson
|
||||||
|
{
|
||||||
|
scannerTossers: {
|
||||||
|
ftn_bso: {
|
||||||
|
nodes: {
|
||||||
|
"46:*: {
|
||||||
|
packetType: 2+
|
||||||
|
packetPassword: mypass
|
||||||
|
encoding: cp437
|
||||||
|
archiveType: zip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scheduling
|
||||||
|
Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers.
|
||||||
|
|
||||||
|
* `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`.
|
||||||
|
* `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`.
|
||||||
|
* Free form text can be things like `at 5:00 pm` or `every 2 hours`.
|
||||||
|
|
||||||
|
See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```hjson
|
||||||
|
{
|
||||||
|
scannerTossers: {
|
||||||
|
ftn_bso: {
|
||||||
|
schedule: {
|
||||||
|
import: every 1 hours or @watch:/path/to/watchfile.ext
|
||||||
|
export: every 1 hours or @immediate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -16,15 +16,13 @@
|
||||||
return !isNaN(value) && user.getAge() >= value;
|
return !isNaN(value) && user.getAge() >= value;
|
||||||
},
|
},
|
||||||
AS : function accountStatus() {
|
AS : function accountStatus() {
|
||||||
if(_.isNumber(value)) {
|
if(!_.isArray(value)) {
|
||||||
value = [ value ];
|
value = [ value ];
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_.isArray(value));
|
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||||
|
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||||
return _.findIndex(value, function cmp(accStatus) {
|
return value.indexOf(userAccountStatus) > -1;
|
||||||
return parseInt(accStatus, 10) === parseInt(user.properties.account_status, 10);
|
|
||||||
}) > -1;
|
|
||||||
},
|
},
|
||||||
EC : function isEncoding() {
|
EC : function isEncoding() {
|
||||||
switch(value) {
|
switch(value) {
|
||||||
|
@ -53,7 +51,7 @@
|
||||||
// :TODO: implement me!!
|
// :TODO: implement me!!
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
SC : function isSecerConnection() {
|
SC : function isSecureConnection() {
|
||||||
return client.session.isSecure;
|
return client.session.isSecure;
|
||||||
},
|
},
|
||||||
ML : function minutesLeft() {
|
ML : function minutesLeft() {
|
||||||
|
@ -81,28 +79,20 @@
|
||||||
return !isNaN(value) && client.term.termWidth >= value;
|
return !isNaN(value) && client.term.termWidth >= value;
|
||||||
},
|
},
|
||||||
ID : function isUserId(value) {
|
ID : function isUserId(value) {
|
||||||
if(_.isNumber(value)) {
|
if(!_.isArray(value)) {
|
||||||
value = [ value ];
|
value = [ value ];
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_.isArray(value));
|
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||||
|
return value.indexOf(user.userId) > -1;
|
||||||
return _.findIndex(value, function cmp(uid) {
|
|
||||||
return user.userId === parseInt(uid, 10);
|
|
||||||
}) > -1;
|
|
||||||
},
|
},
|
||||||
WD : function isOneOfDayOfWeek() {
|
WD : function isOneOfDayOfWeek() {
|
||||||
if(_.isNumber(value)) {
|
if(!_.isArray(value)) {
|
||||||
value = [ value ];
|
value = [ value ];
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_.isArray(value));
|
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||||
|
return value.indexOf(new Date().getDay()) > -1;
|
||||||
var nowDayOfWeek = new Date().getDay();
|
|
||||||
|
|
||||||
return _.findIndex(value, function cmp(dow) {
|
|
||||||
return nowDayOfWeek === parseInt(dow, 10);
|
|
||||||
}) > -1;
|
|
||||||
},
|
},
|
||||||
MM : function isMinutesPastMidnight() {
|
MM : function isMinutesPastMidnight() {
|
||||||
// :TODO: return true if value is >= minutes past midnight sys time
|
// :TODO: return true if value is >= minutes past midnight sys time
|
||||||
|
|
|
@ -177,12 +177,6 @@ function AbracadabraModule(options) {
|
||||||
|
|
||||||
require('util').inherits(AbracadabraModule, MenuModule);
|
require('util').inherits(AbracadabraModule, MenuModule);
|
||||||
|
|
||||||
/*
|
|
||||||
AbracadabraModule.prototype.enter = function(client) {
|
|
||||||
AbracadabraModule.super_.prototype.enter.call(this, client);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
AbracadabraModule.prototype.leave = function() {
|
AbracadabraModule.prototype.leave = function() {
|
||||||
AbracadabraModule.super_.prototype.leave.call(this);
|
AbracadabraModule.super_.prototype.leave.call(this);
|
||||||
|
|
||||||
|
|
|
@ -393,7 +393,7 @@
|
||||||
},
|
},
|
||||||
editorMode: edit
|
editorMode: edit
|
||||||
editorType: email
|
editorType: email
|
||||||
messageAreaName: private_mail
|
messageAreaTag: private_mail
|
||||||
toUserId: 1 /* always to +op */
|
toUserId: 1 /* always to +op */
|
||||||
}
|
}
|
||||||
form: {
|
form: {
|
||||||
|
@ -806,7 +806,7 @@
|
||||||
},
|
},
|
||||||
editorMode: edit
|
editorMode: edit
|
||||||
editorType: email
|
editorType: email
|
||||||
messageAreaName: private_mail
|
messageAreaTag: private_mail
|
||||||
toUserId: 1 /* always to +op */
|
toUserId: 1 /* always to +op */
|
||||||
}
|
}
|
||||||
form: {
|
form: {
|
||||||
|
@ -1019,6 +1019,10 @@
|
||||||
value: { command: "P" }
|
value: { command: "P" }
|
||||||
action: @menu:messageAreaNewPost
|
action: @menu:messageAreaNewPost
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
value: { command: "J" }
|
||||||
|
action: @menu:messageAreaChangeCurrentConference
|
||||||
|
}
|
||||||
{
|
{
|
||||||
value: { command: "C" }
|
value: { command: "C" }
|
||||||
action: @menu:messageAreaChangeCurrentArea
|
action: @menu:messageAreaChangeCurrentArea
|
||||||
|
@ -1041,7 +1045,39 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageAreaChangeCurrentConference: {
|
||||||
|
art: CCHANGE
|
||||||
|
module: msg_conf_list
|
||||||
|
form: {
|
||||||
|
0: {
|
||||||
|
mci: {
|
||||||
|
VM1: {
|
||||||
|
focus: true
|
||||||
|
submit: true
|
||||||
|
argName: conf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
submit: {
|
||||||
|
*: [
|
||||||
|
{
|
||||||
|
value: { conf: null }
|
||||||
|
action: @method:changeConference
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
actionKeys: [
|
||||||
|
{
|
||||||
|
keys: [ "escape", "q", "shift + q" ]
|
||||||
|
action: @systemMethod:prevMenu
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
messageAreaChangeCurrentArea: {
|
messageAreaChangeCurrentArea: {
|
||||||
|
// :TODO: rename this art to ACHANGE
|
||||||
art: CHANGE
|
art: CHANGE
|
||||||
module: msg_area_list
|
module: msg_area_list
|
||||||
form: {
|
form: {
|
||||||
|
@ -1070,6 +1106,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageAreaMessageList: {
|
messageAreaMessageList: {
|
||||||
module: msg_list
|
module: msg_list
|
||||||
art: MSGLIST
|
art: MSGLIST
|
||||||
|
|
|
@ -5,7 +5,6 @@ var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
var ViewController = require('../core/view_controller.js').ViewController;
|
var ViewController = require('../core/view_controller.js').ViewController;
|
||||||
var messageArea = require('../core/message_area.js');
|
var messageArea = require('../core/message_area.js');
|
||||||
var strUtil = require('../core/string_util.js');
|
var strUtil = require('../core/string_util.js');
|
||||||
//var msgDb = require('./database.js').dbs.message;
|
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
@ -43,14 +42,17 @@ function MessageAreaListModule(options) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.messageAreas = messageArea.getAvailableMessageAreas();
|
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
|
||||||
|
self.client.user.properties.message_conf_tag,
|
||||||
|
{ client : self.client }
|
||||||
|
);
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
changeArea : function(formData, extraArgs) {
|
changeArea : function(formData, extraArgs) {
|
||||||
if(1 === formData.submitId) {
|
if(1 === formData.submitId) {
|
||||||
var areaName = self.messageAreas[formData.value.area].name;
|
const areaTag = self.messageAreas[formData.value.area].areaTag;
|
||||||
|
|
||||||
messageArea.changeMessageArea(self.client, areaName, function areaChanged(err) {
|
messageArea.changeMessageArea(self.client, areaTag, function areaChanged(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
|
self.client.term.pipeWrite('\n|00Cannot change area: ' + err.message + '\n');
|
||||||
|
|
||||||
|
@ -66,7 +68,7 @@ function MessageAreaListModule(options) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setViewText = function(id, text) {
|
this.setViewText = function(id, text) {
|
||||||
var v = self.viewControllers.areaList.getView(id);
|
const v = self.viewControllers.areaList.getView(id);
|
||||||
if(v) {
|
if(v) {
|
||||||
v.setText(text);
|
v.setText(text);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +80,7 @@ require('util').inherits(MessageAreaListModule, MenuModule);
|
||||||
|
|
||||||
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
|
@ -99,26 +101,29 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function populateAreaListView(callback) {
|
function populateAreaListView(callback) {
|
||||||
var listFormat = self.menuConfig.config.listFormat || '{index} ) - {desc}';
|
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||||
var focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||||
|
|
||||||
var areaListItems = [];
|
const areaListView = vc.getView(1);
|
||||||
var focusListItems = [];
|
let i = 1;
|
||||||
|
areaListView.setItems(_.map(self.messageAreas, v => {
|
||||||
|
return listFormat.format({
|
||||||
|
index : i++,
|
||||||
|
areaTag : v.area.areaTag,
|
||||||
|
name : v.area.name,
|
||||||
|
desc : v.area.desc,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
// :TODO: use _.map() here
|
i = 1;
|
||||||
for(var i = 0; i < self.messageAreas.length; ++i) {
|
areaListView.setFocusItems(_.map(self.messageAreas, v => {
|
||||||
areaListItems.push(listFormat.format(
|
return focusListFormat.format({
|
||||||
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
|
index : i++,
|
||||||
);
|
areaTag : v.area.areaTag,
|
||||||
focusListItems.push(focusListFormat.format(
|
name : v.area.name,
|
||||||
{ index : i, name : self.messageAreas[i].name, desc : self.messageAreas[i].desc } )
|
desc : v.area.desc,
|
||||||
);
|
})
|
||||||
}
|
}));
|
||||||
|
|
||||||
var areaListView = vc.getView(1);
|
|
||||||
|
|
||||||
areaListView.setItems(areaListItems);
|
|
||||||
areaListView.setFocusItems(focusListItems);
|
|
||||||
|
|
||||||
areaListView.redraw();
|
areaListView.redraw();
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
|
let FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
|
||||||
var Message = require('../core/message.js').Message;
|
//var Message = require('../core/message.js').Message;
|
||||||
var user = require('../core/user.js');
|
let persistMessage = require('../core/message_area.js').persistMessage;
|
||||||
|
let user = require('../core/user.js');
|
||||||
|
|
||||||
var _ = require('lodash');
|
let _ = require('lodash');
|
||||||
var async = require('async');
|
let async = require('async');
|
||||||
|
|
||||||
exports.getModule = AreaPostFSEModule;
|
exports.getModule = AreaPostFSEModule;
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ function AreaPostFSEModule(options) {
|
||||||
// we're posting, so always start with 'edit' mode
|
// we're posting, so always start with 'edit' mode
|
||||||
this.editorMode = 'edit';
|
this.editorMode = 'edit';
|
||||||
|
|
||||||
this.menuMethods.editModeMenuSave = function(formData, extraArgs) {
|
this.menuMethods.editModeMenuSave = function() {
|
||||||
|
|
||||||
var msg;
|
var msg;
|
||||||
async.series(
|
async.series(
|
||||||
|
@ -36,16 +37,23 @@ function AreaPostFSEModule(options) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function saveMessage(callback) {
|
function saveMessage(callback) {
|
||||||
|
persistMessage(msg, callback);
|
||||||
|
/*
|
||||||
msg.persist(function persisted(err) {
|
msg.persist(function persisted(err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
// :TODO:... sooooo now what?
|
// :TODO:... sooooo now what?
|
||||||
} else {
|
} 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();
|
self.nextMenu();
|
||||||
|
@ -56,11 +64,11 @@ function AreaPostFSEModule(options) {
|
||||||
|
|
||||||
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
|
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
|
||||||
|
|
||||||
AreaPostFSEModule.prototype.enter = function(client) {
|
AreaPostFSEModule.prototype.enter = function() {
|
||||||
|
|
||||||
if(_.isString(client.user.properties.message_area_name) && !_.isString(this.messageAreaName)) {
|
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
|
||||||
this.messageAreaName = client.user.properties.message_area_name;
|
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
AreaPostFSEModule.super_.prototype.enter.call(this, client);
|
AreaPostFSEModule.super_.prototype.enter.call(this);
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,7 +72,7 @@ function AreaViewFSEModule(options) {
|
||||||
if(_.isString(extraArgs.menu)) {
|
if(_.isString(extraArgs.menu)) {
|
||||||
var modOpts = {
|
var modOpts = {
|
||||||
extraArgs : {
|
extraArgs : {
|
||||||
messageAreaName : self.messageAreaName,
|
messageAreaTag : self.messageAreaTag,
|
||||||
replyToMessage : self.message,
|
replyToMessage : self.message,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 self = this;
|
||||||
var config = this.menuConfig.config;
|
var config = this.menuConfig.config;
|
||||||
|
|
||||||
this.messageAreaName = config.messageAreaName;
|
this.messageAreaTag = config.messageAreaTag;
|
||||||
|
|
||||||
if(options.extraArgs) {
|
if(options.extraArgs) {
|
||||||
//
|
//
|
||||||
// |extraArgs| can override |messageAreaName| provided by config
|
// |extraArgs| can override |messageAreaTag| provided by config
|
||||||
// as well as supply a pre-defined message list
|
// as well as supply a pre-defined message list
|
||||||
//
|
//
|
||||||
if(options.extraArgs.messageAreaName) {
|
if(options.extraArgs.messageAreaTag) {
|
||||||
this.messageAreaName = options.extraArgs.messageAreaName;
|
this.messageAreaTag = options.extraArgs.messageAreaTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.extraArgs.messageList) {
|
if(options.extraArgs.messageList) {
|
||||||
|
@ -73,7 +73,7 @@ function MessageListModule(options) {
|
||||||
if(1 === formData.submitId) {
|
if(1 === formData.submitId) {
|
||||||
var modOpts = {
|
var modOpts = {
|
||||||
extraArgs : {
|
extraArgs : {
|
||||||
messageAreaName : self.messageAreaName,
|
messageAreaTag : self.messageAreaTag,
|
||||||
messageList : self.messageList,
|
messageList : self.messageList,
|
||||||
messageIndex : formData.value.message,
|
messageIndex : formData.value.message,
|
||||||
}
|
}
|
||||||
|
@ -94,15 +94,15 @@ function MessageListModule(options) {
|
||||||
|
|
||||||
require('util').inherits(MessageListModule, MenuModule);
|
require('util').inherits(MessageListModule, MenuModule);
|
||||||
|
|
||||||
MessageListModule.prototype.enter = function(client) {
|
MessageListModule.prototype.enter = function() {
|
||||||
MessageListModule.super_.prototype.enter.call(this, client);
|
MessageListModule.super_.prototype.enter.call(this);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Config can specify |messageAreaName| else it comes from
|
// Config can specify |messageAreaTag| else it comes from
|
||||||
// the user's current area
|
// the user's current area
|
||||||
//
|
//
|
||||||
if(!this.messageAreaName) {
|
if(!this.messageAreaTag) {
|
||||||
this.messageAreaName = client.user.properties.message_area_name;
|
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,6 +110,8 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||||
|
|
||||||
|
var firstNewEntryIndex;
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function callParentMciReady(callback) {
|
function callParentMciReady(callback) {
|
||||||
|
@ -130,7 +132,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
if(_.isArray(self.messageList)) {
|
if(_.isArray(self.messageList)) {
|
||||||
callback(0 === self.messageList.length ? new Error('No messages in area') : null);
|
callback(0 === self.messageList.length ? new Error('No messages in area') : null);
|
||||||
} else {
|
} else {
|
||||||
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaName, function msgs(err, msgList) {
|
messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) {
|
||||||
if(msgList && 0 === msgList.length) {
|
if(msgList && 0 === msgList.length) {
|
||||||
callback(new Error('No messages in area'));
|
callback(new Error('No messages in area'));
|
||||||
} else {
|
} else {
|
||||||
|
@ -141,7 +143,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function getLastReadMesageId(callback) {
|
function getLastReadMesageId(callback) {
|
||||||
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaName, function lastRead(err, lastReadId) {
|
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) {
|
||||||
self.lastReadId = lastReadId || 0;
|
self.lastReadId = lastReadId || 0;
|
||||||
callback(null); // ignore any errors, e.g. missing value
|
callback(null); // ignore any errors, e.g. missing value
|
||||||
});
|
});
|
||||||
|
@ -158,6 +160,13 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
var msgNum = 1;
|
var msgNum = 1;
|
||||||
|
|
||||||
function getMsgFmtObj(mle) {
|
function getMsgFmtObj(mle) {
|
||||||
|
|
||||||
|
if(_.isUndefined(firstNewEntryIndex) &&
|
||||||
|
mle.messageId > self.lastReadId)
|
||||||
|
{
|
||||||
|
firstNewEntryIndex = msgNum - 1;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
msgNum : msgNum++,
|
msgNum : msgNum++,
|
||||||
subj : mle.subject,
|
subj : mle.subject,
|
||||||
|
@ -183,11 +192,15 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
||||||
|
|
||||||
msgListView.redraw();
|
msgListView.redraw();
|
||||||
|
|
||||||
|
if(firstNewEntryIndex > 0) {
|
||||||
|
msgListView.setFocusItemIndex(firstNewEntryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
function populateOtherMciViews(callback) {
|
function populateOtherMciViews(callback) {
|
||||||
|
|
||||||
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByName(self.messageAreaName).desc);
|
self.setViewText(MciCodesIds.MsgAreaDesc, messageArea.getMessageAreaByTag(self.messageAreaTag).name);
|
||||||
self.setViewText(MciCodesIds.MsgSelNum, (vc.getView(MciCodesIds.MsgList).getData() + 1).toString());
|
self.setViewText(MciCodesIds.MsgSelNum, (vc.getView(MciCodesIds.MsgList).getData() + 1).toString());
|
||||||
self.setViewText(MciCodesIds.MsgTotal, self.messageList.length.toString());
|
self.setViewText(MciCodesIds.MsgTotal, self.messageList.length.toString());
|
||||||
|
|
||||||
|
|
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 theme = require('../core/theme.js');
|
||||||
var login = require('../core/system_menu_method.js').login;
|
var login = require('../core/system_menu_method.js').login;
|
||||||
var Config = require('../core/config.js').config;
|
var Config = require('../core/config.js').config;
|
||||||
var getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea;
|
var messageArea = require('../core/message_area.js');
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
|
@ -65,6 +65,16 @@ function NewUserAppModule(options) {
|
||||||
|
|
||||||
newUser.username = formData.value.username;
|
newUser.username = formData.value.username;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We have to disable ACS checks for initial default areas as the user is not yet ready
|
||||||
|
//
|
||||||
|
var confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck
|
||||||
|
var areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck
|
||||||
|
|
||||||
|
// can't store undefined!
|
||||||
|
confTag = confTag || '';
|
||||||
|
areaTag = areaTag || '';
|
||||||
|
|
||||||
newUser.properties = {
|
newUser.properties = {
|
||||||
real_name : formData.value.realName,
|
real_name : formData.value.realName,
|
||||||
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
|
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
|
||||||
|
@ -75,14 +85,12 @@ function NewUserAppModule(options) {
|
||||||
web_address : formData.value.web,
|
web_address : formData.value.web,
|
||||||
account_created : new Date().toISOString(),
|
account_created : new Date().toISOString(),
|
||||||
|
|
||||||
message_area_name : getDefaultMessageArea().name,
|
message_conf_tag : confTag,
|
||||||
|
message_area_tag : areaTag,
|
||||||
|
|
||||||
term_height : self.client.term.termHeight,
|
term_height : self.client.term.termHeight,
|
||||||
term_width : self.client.term.termWidth,
|
term_width : self.client.term.termWidth,
|
||||||
|
|
||||||
// :TODO: This is set in User.create() -- proabbly don't need it here:
|
|
||||||
//account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active,
|
|
||||||
|
|
||||||
// :TODO: Other defaults
|
// :TODO: Other defaults
|
||||||
// :TODO: should probably have a place to create defaults/etc.
|
// :TODO: should probably have a place to create defaults/etc.
|
||||||
};
|
};
|
||||||
|
@ -93,7 +101,7 @@ function NewUserAppModule(options) {
|
||||||
newUser.properties.theme_id = Config.defaults.theme;
|
newUser.properties.theme_id = Config.defaults.theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: .create() should also validate email uniqueness!
|
// :TODO: User.create() should validate email uniqueness!
|
||||||
newUser.create( { password : formData.value.password }, function created(err) {
|
newUser.create( { password : formData.value.password }, function created(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
|
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
|
||||||
|
|
|
@ -172,8 +172,8 @@
|
||||||
|
|
||||||
messageAreaChangeCurrentArea: {
|
messageAreaChangeCurrentArea: {
|
||||||
config: {
|
config: {
|
||||||
listFormat: "|00|15{index} |07- |03{desc}"
|
listFormat: "|00|15{index} |07- |03{name}"
|
||||||
focusListFormat: "|00|19|15{index} - {desc}"
|
focusListFormat: "|00|19|15{index} - {name}"
|
||||||
}
|
}
|
||||||
mci: {
|
mci: {
|
||||||
VM1: {
|
VM1: {
|
||||||
|
@ -310,6 +310,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newScanMessageList: {
|
||||||
|
config: {
|
||||||
|
listFormat: "|00|15 {msgNum:<5.5}|03{subj:<29.29} |15{from:<20.20} {ts}"
|
||||||
|
focusListFormat: "|00|19> |15{msgNum:<5.5}{subj:<29.29} {from:<20.20} {ts}"
|
||||||
|
dateTimeFormat: ddd MMM Do
|
||||||
|
}
|
||||||
|
mci: {
|
||||||
|
VM1: {
|
||||||
|
height: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
var userDb = require('../core/database.js').dbs.user;
|
//var userDb = require('../core/database.js').dbs.user;
|
||||||
var getUserList = require('../core/user.js').getUserList;
|
var getUserList = require('../core/user.js').getUserList;
|
||||||
var ViewController = require('../core/view_controller.js').ViewController;
|
var ViewController = require('../core/view_controller.js').ViewController;
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,6 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
|
||||||
return listFormat.format(oe);
|
return listFormat.format(oe);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// :TODO: This is a hack until pipe codes are better implemented
|
|
||||||
onlineListView.focusItems = onlineListView.items;
|
onlineListView.focusItems = onlineListView.items;
|
||||||
|
|
||||||
onlineListView.redraw();
|
onlineListView.redraw();
|
||||||
|
|
14
oputil.js
14
oputil.js
|
@ -13,7 +13,7 @@ var assert = require('assert');
|
||||||
|
|
||||||
var argv = require('minimist')(process.argv.slice(2));
|
var argv = require('minimist')(process.argv.slice(2));
|
||||||
|
|
||||||
var ExitCodes = {
|
const ExitCodes = {
|
||||||
SUCCESS : 0,
|
SUCCESS : 0,
|
||||||
ERROR : -1,
|
ERROR : -1,
|
||||||
BAD_COMMAND : -2,
|
BAD_COMMAND : -2,
|
||||||
|
@ -28,9 +28,13 @@ function printUsage(command) {
|
||||||
usage =
|
usage =
|
||||||
'usage: oputil.js [--version] [--help]\n' +
|
'usage: oputil.js [--version] [--help]\n' +
|
||||||
' <command> [<args>]' +
|
' <command> [<args>]' +
|
||||||
'\n' +
|
'\n\n' +
|
||||||
'global args:\n' +
|
'global args:\n' +
|
||||||
' --config PATH : specify config path';
|
' --config PATH : specify config path' +
|
||||||
|
'\n\n' +
|
||||||
|
'commands:\n' +
|
||||||
|
' user : User utilities' +
|
||||||
|
'\n';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user' :
|
case 'user' :
|
||||||
|
@ -47,7 +51,7 @@ function printUsage(command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConfig(cb) {
|
function initConfig(cb) {
|
||||||
var configPath = argv.config ? argv.config : config.getDefaultPath();
|
const configPath = argv.config ? argv.config : config.getDefaultPath();
|
||||||
|
|
||||||
config.init(configPath, cb);
|
config.init(configPath, cb);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +92,7 @@ function handleUserCommand() {
|
||||||
assert(_.isNumber(userId));
|
assert(_.isNumber(userId));
|
||||||
assert(userId > 0);
|
assert(userId > 0);
|
||||||
|
|
||||||
var u = new user.User();
|
let u = new user.User();
|
||||||
u.userId = userId;
|
u.userId = userId;
|
||||||
|
|
||||||
u.setNewAuthCredentials(argv.password, function credsSet(err) {
|
u.setNewAuthCredentials(argv.password, function credsSet(err) {
|
||||||
|
|
|
@ -16,10 +16,11 @@
|
||||||
"async": "^1.5.1",
|
"async": "^1.5.1",
|
||||||
"binary": "0.3.x",
|
"binary": "0.3.x",
|
||||||
"buffers": "0.1.x",
|
"buffers": "0.1.x",
|
||||||
"bunyan": "1.5.x",
|
"bunyan": "^1.7.1",
|
||||||
"gaze": "^0.5.2",
|
"gaze": "^0.5.2",
|
||||||
"hjson": "1.7.x",
|
"hjson": "1.7.x",
|
||||||
"iconv-lite": "^0.4.13",
|
"iconv-lite": "^0.4.13",
|
||||||
|
"later": "1.2.0",
|
||||||
"lodash": "^3.10.1",
|
"lodash": "^3.10.1",
|
||||||
"minimist": "1.2.x",
|
"minimist": "1.2.x",
|
||||||
"mkdirp": "0.5.x",
|
"mkdirp": "0.5.x",
|
||||||
|
@ -28,7 +29,8 @@
|
||||||
"ptyw.js": "^0.3.7",
|
"ptyw.js": "^0.3.7",
|
||||||
"sqlite3": "^3.1.1",
|
"sqlite3": "^3.1.1",
|
||||||
"ssh2": "^0.4.13",
|
"ssh2": "^0.4.13",
|
||||||
"string-format": "davidchambers/string-format#mini-language"
|
"string-format": "davidchambers/string-format#mini-language",
|
||||||
|
"temp": "^0.8.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.2"
|
"node": ">=0.12.2"
|
||||||
|
|
Loading…
Reference in New Issue