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

This commit is contained in:
Bryan Ashby 2016-07-05 22:32:48 -06:00
commit c2b9fb9c0c
18 changed files with 468 additions and 329 deletions

View File

@ -5,7 +5,7 @@
ENiGMA½ is a modern BBS software with a nostalgic flair! ENiGMA½ is a modern BBS software with a nostalgic flair!
## Feature Available Now ## Features Available Now
* Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X) * Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X)
* Multi node support * Multi node support
* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods
@ -51,6 +51,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested
## Boards ## Boards
* WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**) * WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**)
* Support board: ☠ BLACK ƒlag ☠ (**telnet://blackflag.acid.org:2425**) * Support board: ☠ BLACK ƒlag ☠ (**telnet://blackflag.acid.org:2425**)
* HappyLand (**telnet://andrew.homeunix.org:2023**)
## Installation ## Installation
@ -65,6 +66,7 @@ Please see the [Quickstart](docs/index.md#quickstart)
* Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!) * Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!)
* Avon of [Agency BBS](http://bbs.geek.nz/) and fsxNet * Avon of [Agency BBS](http://bbs.geek.nz/) and fsxNet
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)! * Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
* [Apam](https://github.com/apamment) of HappyLand BBS
## 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:

View File

@ -1,17 +1,18 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var conf = require('./config.js'); // ENiGMA½
const conf = require('./config.js');
var sqlite3 = require('sqlite3');
var paths = require('path');
var async = require('async');
// deps
const sqlite3 = require('sqlite3');
const paths = require('path');
const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
// database handles // database handles
var dbs = {}; let dbs = {};
exports.getModDatabasePath = getModDatabasePath; exports.getModDatabasePath = getModDatabasePath;
exports.initializeDatabases = initializeDatabases; exports.initializeDatabases = initializeDatabases;
@ -46,51 +47,52 @@ function getModDatabasePath(moduleInfo, suffix) {
} }
function initializeDatabases(cb) { function initializeDatabases(cb) {
// :TODO: this will need to change if more DB's are added ... why?
async.series( async.series(
[ [
function systemDb(callback) { function systemDb(callback) {
dbs.system = new sqlite3.Database(getDatabasePath('system'), function dbCreated(err) { dbs.system = new sqlite3.Database(getDatabasePath('system'), err => {
if(err) { if(err) {
callback(err); return callback(err);
} else { }
dbs.system.serialize(function serialized() {
dbs.system.serialize( () => {
createSystemTables(); createSystemTables();
}); });
callback(null);
} return callback(null);
}); });
}, },
function userDb(callback) { function userDb(callback) {
dbs.user = new sqlite3.Database(getDatabasePath('user'), function dbCreated(err) { dbs.user = new sqlite3.Database(getDatabasePath('user'), err => {
if(err) { if(err) {
callback(err); return callback(err);
} else { }
dbs.user.serialize(function serialized() {
dbs.user.serialize( () => {
createUserTables(); createUserTables();
createInitialUserValues(); createInitialUserValues();
}); });
callback(null);
} return callback(null);
}); });
}, },
function messageDb(callback) { function messageDb(callback) {
dbs.message = new sqlite3.Database(getDatabasePath('message'), function dbCreated(err) { dbs.message = new sqlite3.Database(getDatabasePath('message'), err => {
if(err) { if(err) {
callback(err); return callback(err);
} else { }
dbs.message.serialize(function serialized() { dbs.message.serialize(function serialized() {
createMessageBaseTables(); createMessageBaseTables();
createInitialMessageValues(); createInitialMessageValues();
}); });
callback(null);
} return callback(null);
}); });
} }
], ],
function complete(err) { cb
cb(err);
}
); );
} }
@ -119,119 +121,127 @@ function createSystemTables() {
function createUserTables() { function createUserTables() {
dbs.user.run( dbs.user.run(
'CREATE TABLE IF NOT EXISTS user (' + `CREATE TABLE IF NOT EXISTS user (
' id INTEGER PRIMARY KEY,' + id INTEGER PRIMARY KEY,
' user_name VARCHAR NOT NULL,' + user_name VARCHAR NOT NULL,
' UNIQUE(user_name)' + UNIQUE(user_name)
');' );`
); );
// :TODO: create FK on delete/etc. // :TODO: create FK on delete/etc.
dbs.user.run( dbs.user.run(
'CREATE TABLE IF NOT EXISTS user_property (' + `CREATE TABLE IF NOT EXISTS user_property (
' user_id INTEGER NOT NULL,' + user_id INTEGER NOT NULL,
' prop_name VARCHAR NOT NULL,' + prop_name VARCHAR NOT NULL,
' prop_value VARCHAR,' + prop_value VARCHAR,
' UNIQUE(user_id, prop_name),' + UNIQUE(user_id, prop_name),
' FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE' + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
');' );`
); );
dbs.user.run( dbs.user.run(
'CREATE TABLE IF NOT EXISTS user_group_member (' + `CREATE TABLE IF NOT EXISTS user_group_member (
' group_name VARCHAR NOT NULL,' + group_name VARCHAR NOT NULL,
' user_id INTEGER NOT NULL,' + user_id INTEGER NOT NULL,
' UNIQUE(group_name, user_id)' + UNIQUE(group_name, user_id)
');' );`
); );
dbs.user.run( dbs.user.run(
'CREATE TABLE IF NOT EXISTS user_login_history (' + `CREATE TABLE IF NOT EXISTS user_login_history (
' user_id INTEGER NOT NULL,' + user_id INTEGER NOT NULL,
' user_name VARCHAR NOT NULL,' + user_name VARCHAR NOT NULL,
' timestamp DATETIME NOT NULL' + timestamp DATETIME NOT NULL
');' );`
); );
} }
function createMessageBaseTables() { 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_tag 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,
' from_user_name VARCHAR NOT NULL,' + from_user_name VARCHAR NOT NULL,
' subject,' + // FTS @ message_fts subject, /* FTS @ message_fts */
' message,' + // FTS @ message_fts message, /* FTS @ message_fts */
' modified_timestamp DATETIME NOT NULL,' + modified_timestamp DATETIME NOT NULL,
' view_count INTEGER NOT NULL DEFAULT 0,' + view_count INTEGER NOT NULL DEFAULT 0,
' UNIQUE(message_uuid)' + UNIQUE(message_uuid)
');' );`
); );
dbs.message.run( dbs.message.run(
'CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (' + `CREATE INDEX IF NOT EXISTS message_by_area_tag_index
' content="message",' + ON message (area_tag);`
' subject,' +
' message' +
');'
); );
dbs.message.run( dbs.message.run(
'CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN' + `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
' DELETE FROM message_fts WHERE docid=old.rowid;' + content="message",
'END;' + subject,
'CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN' + message
' DELETE FROM message_fts WHERE docid=old.rowid;' + );`
'END;' + );
'' +
'CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN' + dbs.message.run(
' INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);' + `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
'END;' + DELETE FROM message_fts WHERE docid=old.rowid;
'CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN' + END;
' INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);' +
'END;' CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid;
END;
CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;
CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;`
);
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message_meta (
message_id INTEGER NOT NULL,
meta_category INTEGER NOT NULL,
meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL,
UNIQUE(message_id, meta_category, meta_name, meta_value),
FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE
);`
);
// :TODO: need SQL to ensure cleaned up if delete from message?
/*
dbs.message.run(
`CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY,
hash_tag_name VARCHAR NOT NULL,
UNIQUE(hash_tag_name)
);`
); );
// :TODO: need SQL to ensure cleaned up if delete from message? // :TODO: need SQL to ensure cleaned up if delete from message?
dbs.message.run( dbs.message.run(
'CREATE TABLE IF NOT EXISTS message_meta (' + `CREATE TABLE IF NOT EXISTS message_hash_tag (
' message_id INTEGER NOT NULL,' + hash_tag_id INTEGER NOT NULL,
' meta_category INTEGER NOT NULL,' + message_id INTEGER NOT NULL,
' meta_name VARCHAR NOT NULL,' + );`
' meta_value VARCHAR NOT NULL,' +
' UNIQUE(message_id, meta_category, meta_name, meta_value),' + // :TODO:why unique here?
' FOREIGN KEY(message_id) REFERENCES message(message_id)' +
');'
);
// :TODO: need SQL to ensure cleaned up if delete from message?
dbs.message.run(
'CREATE TABLE IF NOT EXISTS hash_tag (' +
' hash_tag_id INTEGER PRIMARY KEY,' +
' hash_tag_name VARCHAR NOT NULL,' +
' UNIQUE(hash_tag_name)' +
');'
);
// :TODO: need SQL to ensure cleaned up if delete from message?
dbs.message.run(
'CREATE TABLE IF NOT EXISTS message_hash_tag (' +
' hash_tag_id INTEGER NOT NULL,' +
' message_id INTEGER NOT NULL' +
');'
); );
*/
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_tag VARCHAR NOT NULL,' + area_tag VARCHAR NOT NULL,
' message_id INTEGER NOT NULL,' + message_id INTEGER NOT NULL,
' UNIQUE(user_id, area_tag)' + UNIQUE(user_id, area_tag)
');' );`
); );
dbs.message.run( dbs.message.run(

View File

@ -30,7 +30,7 @@ class ScheduledEvent {
this.schedule = this.parseScheduleString(events[name].schedule); this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].action); this.action = this.parseActionSpec(events[name].action);
if(this.action) { if(this.action) {
this.action.args = events[name].args; this.action.args = events[name].args || [];
} }
} }

View File

@ -4,10 +4,8 @@
let Config = require('./config.js').config; let Config = require('./config.js').config;
let Address = require('./ftn_address.js'); let Address = require('./ftn_address.js');
let FNV1a = require('./fnv1a.js'); let FNV1a = require('./fnv1a.js');
let createNamedUUID = require('./uuid_util.js').createNamedUUID;
let _ = require('lodash'); let _ = require('lodash');
let assert = require('assert');
let iconv = require('iconv-lite'); let iconv = require('iconv-lite');
let moment = require('moment'); let moment = require('moment');
let uuid = require('node-uuid'); let uuid = require('node-uuid');
@ -18,8 +16,6 @@ let packageJson = require('../package.json');
// :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.stringToNullPaddedBuffer = stringToNullPaddedBuffer; exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getMessageSerialNumber = getMessageSerialNumber; exports.getMessageSerialNumber = getMessageSerialNumber;
exports.createMessageUuid = createMessageUuid;
exports.createMessageUuidAlternate = createMessageUuidAlternate;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime; exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString; exports.getDateTimeString = getDateTimeString;
@ -96,45 +92,6 @@ function getDateTimeString(m) {
return m.format('DD MMM YY HH:mm:ss'); return m.format('DD MMM YY HH:mm:ss');
} }
//
// 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 getMessageSerialNumber(messageId) { function getMessageSerialNumber(messageId) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);

View File

@ -247,14 +247,19 @@ MenuModule.prototype.leave = function() {
}; };
MenuModule.prototype.beforeArt = function(cb) { MenuModule.prototype.beforeArt = function(cb) {
if(this.cls) { //
this.client.term.write(ansi.resetScreen()); // Set emulated baud rate - note that some terminals will display
} // part of the ESC sequence here (generally a single 'r') if they
// do not support cterm style baud rates
//
if(_.isNumber(this.menuConfig.options.baudRate)) { if(_.isNumber(this.menuConfig.options.baudRate)) {
this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
} }
if(this.cls) {
this.client.term.write(ansi.resetScreen());
}
return cb(null); return cb(null);
}; };

View File

@ -1,13 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var View = require('./view.js').View; // ENiGMA½
var ansi = require('./ansi_term.js'); const View = require('./view.js').View;
var miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
var util = require('util'); // deps
var assert = require('assert'); const util = require('util');
var _ = require('lodash'); const assert = require('assert');
const _ = require('lodash');
exports.MenuView = MenuView; exports.MenuView = MenuView;
@ -17,7 +18,7 @@ function MenuView(options) {
View.call(this, options); View.call(this, options);
var self = this; const self = this;
if(options.items) { if(options.items) {
this.setItems(options.items); this.setItems(options.items);
@ -47,7 +48,7 @@ function MenuView(options) {
this.getHotKeyItemIndex = function(ch) { this.getHotKeyItemIndex = function(ch) {
if(ch && self.hotKeys) { if(ch && self.hotKeys) {
var keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
if(_.isNumber(keyIndex)) { if(_.isNumber(keyIndex)) {
return keyIndex; return keyIndex;
} }
@ -59,11 +60,10 @@ function MenuView(options) {
util.inherits(MenuView, View); util.inherits(MenuView, View);
MenuView.prototype.setItems = function(items) { MenuView.prototype.setItems = function(items) {
var self = this;
if(items) { if(items) {
this.items = []; // :TODO: better way? this.items = [];
items.forEach(function item(itemText) { items.forEach( itemText => {
self.items.push( { text : itemText } ); this.items.push( { text : itemText } );
}); });
} }
}; };
@ -73,8 +73,8 @@ MenuView.prototype.getCount = function() {
}; };
MenuView.prototype.getItems = function() { MenuView.prototype.getItems = function() {
return _.map(this.items, function itemIter(i) { return this.items.map( item => {
return i.text; return item.text;
}); });
}; };
@ -97,7 +97,7 @@ MenuView.prototype.setFocusItemIndex = function(index) {
}; };
MenuView.prototype.onKeyPress = function(ch, key) { MenuView.prototype.onKeyPress = function(ch, key) {
var itemIndex = this.getHotKeyItemIndex(ch); const itemIndex = this.getHotKeyItemIndex(ch);
if(itemIndex >= 0) { if(itemIndex >= 0) {
this.setFocusItemIndex(itemIndex); this.setFocusItemIndex(itemIndex);
@ -110,12 +110,10 @@ MenuView.prototype.onKeyPress = function(ch, key) {
}; };
MenuView.prototype.setFocusItems = function(items) { MenuView.prototype.setFocusItems = function(items) {
var self = this;
if(items) { if(items) {
this.focusItems = []; this.focusItems = [];
items.forEach(function item(itemText) { items.forEach( itemText => {
self.focusItems.push( { text : itemText } ); this.focusItems.push( { text : itemText } );
}); });
} }
}; };

View File

@ -4,21 +4,30 @@
let msgDb = require('./database.js').dbs.message; let msgDb = require('./database.js').dbs.message;
let wordWrapText = require('./word_wrap.js').wordWrapText; let wordWrapText = require('./word_wrap.js').wordWrapText;
let ftnUtil = require('./ftn_util.js'); let ftnUtil = require('./ftn_util.js');
let createNamedUUID = require('./uuid_util.js').createNamedUUID;
let uuid = require('node-uuid'); let uuid = require('node-uuid');
let async = require('async'); let async = require('async');
let _ = require('lodash'); let _ = require('lodash');
let assert = require('assert'); let assert = require('assert');
let moment = require('moment'); let moment = require('moment');
const iconvEncode = require('iconv-lite').encode;
module.exports = Message; module.exports = Message;
const ENIGMA_MESSAGE_UUID_NAMESPACE = uuid.parse('154506df-1df8-46b9-98f8-ebb5815baaf8');
function Message(options) { function Message(options) {
options = options || {}; options = options || {};
this.messageId = options.messageId || 0; // always generated @ persist this.messageId = options.messageId || 0; // always generated @ persist
this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid; this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid;
this.uuid = options.uuid || uuid.v1();
if(options.uuid) {
// note: new messages have UUID generated @ time of persist. See also Message.createMessageUUID()
this.uuid = options.uuid;
}
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 || '';
@ -110,6 +119,24 @@ Message.prototype.setLocalFromUserId = function(userId) {
this.meta.System.local_from_user_id = userId; this.meta.System.local_from_user_id = userId;
}; };
Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) {
assert(_.isString(areaTag));
assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp));
assert(_.isString(subject));
assert(_.isString(body));
if(!moment.isMoment(modTimestamp)) {
modTimestamp = moment(modTimestamp);
}
areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437');
modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437');
subject = iconvEncode(subject.toUpperCase().trim(), 'CP437');
body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437');
return uuid.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] )));
}
Message.getMessageIdByUuid = function(uuid, cb) { Message.getMessageIdByUuid = function(uuid, cb) {
msgDb.get( msgDb.get(
`SELECT message_id `SELECT message_id
@ -330,10 +357,20 @@ Message.prototype.persist = function(cb) {
}); });
}, },
function storeMessage(callback) { function storeMessage(callback) {
// generate a UUID for this message if required (general case)
const msgTimestamp = moment();
if(!self.uuid) {
self.uuid = Message.createMessageUUID(
self.areaTag,
msgTimestamp,
self.subject,
self.message);
}
msgDb.run( msgDb.run(
`INSERT INTO message (area_tag, 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 (?, ?, ?, ?, ?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ], [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(msgTimestamp) ],
function inserted(err) { // use for this scope function inserted(err) { // use for this scope
if(!err) { if(!err) {
self.messageId = this.lastID; self.messageId = this.lastID;

View File

@ -24,15 +24,30 @@ function loadModuleEx(options, cb) {
const 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) {
return cb(new Error('Module "' + options.name + '" is disabled')); return cb(new Error(`Module "${options.name}" is disabled`));
} }
//
// Modules are allowed to live in /path/to/<moduleName>/<moduleName>.js or
// simply in /path/to/<moduleName>.js. This allows for more advanced modules
// to have their own containing folder, package.json & dependencies, etc.
//
let mod; let mod;
let modPath = paths.join(options.path, `${options.name}.js`); // general case first
try { try {
mod = require(paths.join(options.path, options.name + '.js')); mod = require(modPath);
} catch(e) {
if('MODULE_NOT_FOUND' === e.code) {
modPath = paths.join(options.path, options.name, `${options.name}.js`);
try {
mod = require(modPath);
} catch(e) { } catch(e) {
return cb(e); return cb(e);
} }
} else {
return cb(e);
}
}
if(!_.isObject(mod.moduleInfo)) { if(!_.isObject(mod.moduleInfo)) {
return cb(new Error('Module is missing "moduleInfo" section')); return cb(new Error('Module is missing "moduleInfo" section'));
@ -45,7 +60,7 @@ function loadModuleEx(options, cb) {
// Ref 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); return cb(null, mod);
} }
function loadModule(name, category, cb) { function loadModule(name, category, cb) {
@ -56,7 +71,7 @@ function loadModule(name, category, cb) {
} }
loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) {
cb(err, mod); return cb(err, mod);
}); });
} }
@ -74,11 +89,11 @@ function loadModulesForCategory(category, iterator, complete) {
async.each(jsModules, (file, next) => { async.each(jsModules, (file, next) => {
loadModule(paths.basename(file, '.js'), category, (err, mod) => { loadModule(paths.basename(file, '.js'), category, (err, mod) => {
iterator(err, mod); iterator(err, mod);
next(); return next();
}); });
}, err => { }, err => {
if(complete) { if(complete) {
complete(err); return complete(err);
} }
}); });
}); });

View File

@ -23,6 +23,7 @@ const assert = require('assert');
const gaze = require('gaze'); const gaze = require('gaze');
const fse = require('fs-extra'); const fse = require('fs-extra');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const uuid = require('node-uuid');
exports.moduleInfo = { exports.moduleInfo = {
name : 'FTN BSO', name : 'FTN BSO',
@ -783,9 +784,13 @@ function FTNMessageScanTossModule() {
} }
Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => { Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => {
if(msgIds && msgIds.length > 0) { if(msgIds) {
assert(1 === msgIds.length); // expect a single match, but dupe checking is not perfect - warn otherwise
if(1 === msgIds.length) {
message.replyToMsgId = msgIds[0]; message.replyToMsgId = msgIds[0];
} else {
Log.warn( { msgIds : msgIds, replyKludge : message.meta.FtnKludge.REPLY }, 'Found 2:n MSGIDs matching REPLY kludge!');
}
} }
cb(); cb();
}); });
@ -800,28 +805,35 @@ function FTNMessageScanTossModule() {
callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us'));
}, },
function checkForDupeMSGID(callback) {
//
// If we have a MSGID, don't allow a dupe
//
if(!_.has(message.meta, 'FtnKludge.MSGID')) {
return callback(null);
}
Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, (err, msgIds) => {
if(msgIds && msgIds.length > 0) {
const err = new Error('Duplicate MSGID');
err.code = 'DUPE_MSGID';
return callback(err);
}
return callback(null);
});
},
function basicSetup(callback) { function basicSetup(callback) {
message.areaTag = localAreaTag; message.areaTag = localAreaTag;
// //
// If duplicates are NOT allowed in the area (the default), we need to update // If we *allow* dupes (disabled by default), then just generate
// the message UUID using data available to us. Duplicate UUIDs are internally // a random UUID. Otherwise, don't assign the UUID just yet. It will be
// not allowed in our local database. // generated at persist() time and should be consistent across import/exports
// //
if(!Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) { if(Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) {
if(self.messageHasValidMSGID(message)) { // just generate a UUID & therefor always allow for dupes
// Update UUID with our preferred generation method message.uuid = uuid.v1();
message.uuid = ftnUtil.createMessageUuid(
message.meta.FtnKludge.MSGID,
message.meta.FtnProperty.ftn_area);
} else {
// Update UUID with alternate/backup generation method
message.uuid = ftnUtil.createMessageUuidAlternate(
message.meta.FtnProperty.ftn_area,
message.modTimestamp,
message.subject,
message.message);
}
} }
callback(null); callback(null);
@ -847,6 +859,16 @@ function FTNMessageScanTossModule() {
); );
}; };
this.appendTearAndOrigin = function(message) {
if(message.meta.FtnProperty.ftn_tear_line) {
message.message += `\r\n${message.meta.FtnProperty.ftn_tear_line}\r\n`;
}
if(message.meta.FtnProperty.ftn_origin) {
message.message += `${message.meta.FtnProperty.ftn_origin}\r\n`;
}
};
// //
// Ref. implementations on import: // Ref. implementations on import:
// * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c // * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c
@ -855,7 +877,7 @@ function FTNMessageScanTossModule() {
this.importMessagesFromPacketFile = function(packetPath, password, cb) { this.importMessagesFromPacketFile = function(packetPath, password, cb) {
let packetHeader; let packetHeader;
const packetOpts = { keepTearAndOrigin : true }; const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later
let importStats = { let importStats = {
areaSuccess : {}, // areaTag->count areaSuccess : {}, // areaTag->count
@ -886,14 +908,23 @@ function FTNMessageScanTossModule() {
// //
const localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag); const localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag);
if(localAreaTag) { if(localAreaTag) {
message.uuid = Message.createMessageUUID(
localAreaTag,
message.modTimestamp,
message.subject,
message.message);
self.appendTearAndOrigin(message);
self.importEchoMailToArea(localAreaTag, packetHeader, message, err => { self.importEchoMailToArea(localAreaTag, packetHeader, message, err => {
if(err) { if(err) {
// bump area fail stats // bump area fail stats
importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1;
if('SQLITE_CONSTRAINT' === err.code) { if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) {
const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A';
Log.info( Log.info(
{ area : localAreaTag, subject : message.subject, uuid : message.uuid }, { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId },
'Not importing non-unique message'); 'Not importing non-unique message');
return next(null); return next(null);

View File

@ -384,7 +384,29 @@ function getThemeArt(options, cb) {
// //
async.waterfall( async.waterfall(
[ [
function fromSuppliedTheme(callback) { function fromPath(callback) {
//
// We allow relative (to enigma-bbs) or full paths
//
if('/' === options.name[0]) {
// just take the path as-is
options.basePath = paths.dirname(options.name);
} else if(options.name.indexOf('/') > -1) {
// make relative to base BBS dir
options.basePath = paths.join(__dirname, '../', paths.dirname(options.name));
} else {
return callback(null, null);
}
art.getArt(options.name, options, (err, artInfo) => {
return callback(null, artInfo);
});
},
function fromSuppliedTheme(artInfo, callback) {
if(artInfo) {
return callback(null, artInfo);
}
options.basePath = paths.join(Config.paths.themes, options.themeId); options.basePath = paths.join(Config.paths.themes, options.themeId);
art.getArt(options.name, options, function artLoaded(err, artInfo) { art.getArt(options.name, options, function artLoaded(err, artInfo) {
@ -563,10 +585,9 @@ function displayThemedAsset(assetSpec, client, options, cb) {
options = {}; options = {};
} }
var artAsset = asset.getArtAsset(assetSpec); const artAsset = asset.getArtAsset(assetSpec);
if(!artAsset) { if(!artAsset) {
cb(new Error('Asset not found: ' + assetSpec)); return cb(new Error('Asset not found: ' + assetSpec));
return;
} }
// :TODO: just use simple merge of options -> displayOptions // :TODO: just use simple merge of options -> displayOptions
@ -580,7 +601,7 @@ function displayThemedAsset(assetSpec, client, options, cb) {
switch(artAsset.type) { switch(artAsset.type) {
case 'art' : case 'art' :
displayThemeArt(dispOpts, function displayed(err, artData) { displayThemeArt(dispOpts, function displayed(err, artData) {
cb(err, err ? null : { mciMap : artData.mciMap, height : artData.extraInfo.height } ); return cb(err, err ? null : { mciMap : artData.mciMap, height : artData.extraInfo.height } );
}); });
break; break;
@ -595,7 +616,6 @@ function displayThemedAsset(assetSpec, client, options, cb) {
break; break;
default : default :
cb(new Error('Unsupported art asset type: ' + artAsset.type)); return cb(new Error('Unsupported art asset type: ' + artAsset.type));
break;
} }
} }

View File

@ -1,15 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var MenuView = require('./menu_view.js').MenuView; // ENiGMA½
var ansi = require('./ansi_term.js'); const MenuView = require('./menu_view.js').MenuView;
var strUtil = require('./string_util.js'); const ansi = require('./ansi_term.js');
var miscUtil = require('./misc_util.js'); const strUtil = require('./string_util.js');
var colorCodes = require('./color_codes.js'); const colorCodes = require('./color_codes.js');
var util = require('util'); // deps
var assert = require('assert'); const util = require('util');
var _ = require('lodash');
exports.VerticalMenuView = VerticalMenuView; exports.VerticalMenuView = VerticalMenuView;
@ -19,7 +18,7 @@ function VerticalMenuView(options) {
MenuView.call(this, options); MenuView.call(this, options);
var self = this; const self = this;
this.performAutoScale = function() { this.performAutoScale = function() {
if(this.autoScale.height) { if(this.autoScale.height) {
@ -28,13 +27,13 @@ function VerticalMenuView(options) {
} }
if(self.autoScale.width) { if(self.autoScale.width) {
var l = 0; let maxLen = 0;
self.items.forEach(function item(i) { self.items.forEach( item => {
if(i.text.length > l) { if(item.text.length > maxLen) {
l = Math.min(i.text.length, self.client.term.termWidth - self.position.col); maxLen = Math.min(item.text.length, self.client.term.termWidth - self.position.col);
} }
}); });
self.dimens.width = l + 1; self.dimens.width = maxLen + 1;
} }
}; };
@ -66,14 +65,14 @@ function VerticalMenuView(options) {
*/ */
this.drawItem = function(index) { this.drawItem = function(index) {
var item = self.items[index]; const item = self.items[index];
if(!item) { if(!item) {
return; return;
} }
var focusItem; let focusItem;
var text; let text;
if(self.hasFocusItems()) { if(self.hasFocusItems()) {
focusItem = self.focusItems[index]; focusItem = self.focusItems[index];
@ -119,8 +118,24 @@ VerticalMenuView.prototype.redraw = function() {
this.positionCacheExpired = false; this.positionCacheExpired = false;
} }
var row = this.position.row; // erase old items
for(var i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { // :TODO: optimize this: only needed if a item is removed or new max width < old.
if(this.oldDimens) {
const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' ');
let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank;
let row = this.position.row + 1;
const endRow = (row + this.oldDimens.height) - 2;
while(row < endRow) {
seq += ansi.goto(row, this.position.col) + blank;
row += 1;
}
this.client.term.write(seq);
delete this.oldDimens;
}
let row = this.position.row;
for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) {
this.items[i].row = row; this.items[i].row = row;
row += this.itemSpacing + 1; row += this.itemSpacing + 1;
this.items[i].focused = this.focusedItemIndex === i; this.items[i].focused = this.focusedItemIndex === i;
@ -181,6 +196,11 @@ VerticalMenuView.prototype.getData = function() {
}; };
VerticalMenuView.prototype.setItems = function(items) { VerticalMenuView.prototype.setItems = function(items) {
// if we have items already, save off their drawing area so we don't leave fragments at redraw
if(this.items && this.items.length) {
this.oldDimens = this.dimens;
}
VerticalMenuView.super_.prototype.setItems.call(this, items); VerticalMenuView.super_.prototype.setItems.call(this, items);
this.positionCacheExpired = true; this.positionCacheExpired = true;

View File

@ -6,6 +6,9 @@ The main system configuration file, `config.hjson` both overrides defaults and p
**Windows note**: **~** resolves to *C:\Users\YOURLOGINNAME\* on modern installations, e.g. *C:\Users\NuSkooler\\.config\enigma-bbs\config.hjson* **Windows note**: **~** resolves to *C:\Users\YOURLOGINNAME\* on modern installations, e.g. *C:\Users\NuSkooler\\.config\enigma-bbs\config.hjson*
### oputil.js
Please see `oputil.js config` for configuration generation options.
### Example: System Name ### Example: System Name
`core/config.js` provides the default system name as follows: `core/config.js` provides the default system name as follows:
```javascript ```javascript

View File

@ -43,6 +43,15 @@ openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
## Create a Minimal Config ## Create a Minimal Config
The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information. The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information.
### Via oputil.js
`oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**:
optutil.js config --new
You wil be asked a series of basic questions.
### Example Starting Configuration
```hjson ```hjson
{ {
general: { general: {
@ -79,13 +88,18 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
./main.js ./main.js
``` ```
## Monitoring Logs
Logs are produced by Bunyan which outputs each entry as a JSON object. To tail logs in a colorized and pretty pretty format, issue the following command:
tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | /path/to/enigma-bbs/node_modules/bunyan/bin/bunyan
ENiGMA½ does not produce much to standard out. See below for tailing the log file to see what's going on. ENiGMA½ does not produce much to standard out. See below for tailing the log file to see what's going on.
### Points of Interest ### Points of Interest
* Default ports are 8888 (Telnet) and 8889 (SSH) * Default ports are 8888 (Telnet) and 8889 (SSH)
* Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired. * Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired.
* The first user you create via applying is the SysOp (aka root) * **The first user you create via applying is the SysOp** (aka root)
* You may want to tail the logfile with Bunyan: `tail -F ./logs/enigma-bbs.log | ./node_modules/bunyan/bin/bunyan` * You may want to tail the logfile with Bunyan. See Monitoring Logs above.
# Advanced Installation # Advanced Installation
If you've become convinced you would like a "production" BBS running ENiGMA½ a more advanced installation may be in order. If you've become convinced you would like a "production" BBS running ENiGMA½ a more advanced installation may be in order.

View File

@ -14,6 +14,15 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
/*
Module :TODO:
* Add pipe code support
- override max length & monitor *display* len as user types in order to allow for actual display len with color
* Add preview control: Shows preview with pipe codes resolved
* Add ability to at least alternate formatStrings -- every other
*/
exports.moduleInfo = { exports.moduleInfo = {
name : 'Onelinerz', name : 'Onelinerz',
desc : 'Standard local onelinerz', desc : 'Standard local onelinerz',
@ -147,6 +156,11 @@ function OnelinerzModule(options) {
entriesView.focusItems = entriesView.items; // :TODO: this is a hack entriesView.focusItems = entriesView.items; // :TODO: this is a hack
entriesView.redraw(); entriesView.redraw();
return callback(null);
},
function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO
return callback(null); return callback(null);
} }
], ],

Binary file not shown.

View File

@ -27,34 +27,37 @@ const ExitCodes = {
BAD_ARGS : -3, BAD_ARGS : -3,
} }
function printUsage(command) { const USAGE_HELP = {
var usage; General :
`usage: optutil.js [--version] [--help]
<command> [<args>]
switch(command) { global args:
case '' : --config PATH : specify config path (${getDefaultConfigPath()})
usage =
'usage: oputil.js [--version] [--help]\n' +
' <command> [<args>]' +
'\n\n' +
'global args:\n' +
' --config PATH : specify config path' +
'\n\n' +
'commands:\n' +
' user : User utilities' +
'\n';
break;
case 'user' : commands:
usage = user : user utilities
'usage: optutil.js user --user USERNAME <args>\n' + config : config file management
'\n' +
'valid args:\n' + `,
' --user USERNAME : specify username\n' + User :
' --password PASS : reset password to PASS'; `usage: optutil.js user --user USERNAME <args>
break;
valid args:
--user USERNAME : specify username
-- password PASS : specify password (to reset)
`,
Config :
`usage: optutil.js config <args>
valid args:
--new : generate a new/initial configuration
`
} }
console.error(usage); function printUsage(command) {
console.error(USAGE_HELP[command]);
} }
function initConfig(cb) { function initConfig(cb) {
@ -66,7 +69,7 @@ function initConfig(cb) {
function handleUserCommand() { function handleUserCommand() {
if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) { if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) {
process.exitCode = ExitCodes.ERROR; process.exitCode = ExitCodes.ERROR;
return printUsage('user'); return printUsage('User');
} }
if(_.isString(argv.password)) { if(_.isString(argv.password)) {
@ -142,7 +145,7 @@ const QUESTIONS = {
{ {
name : 'configPath', name : 'configPath',
message : 'Configuration path:', message : 'Configuration path:',
default : getDefaultConfigPath(), default : argv.config ? argv.config : getDefaultConfigPath(),
when : answers => answers.createNewConfig when : answers => answers.createNewConfig
}, },
], ],
@ -334,6 +337,12 @@ function askQuestions(cb) {
} }
function handleConfigCommand() { function handleConfigCommand() {
if(true === argv.help) {
process.exitCode = ExitCodes.ERROR;
return printUsage('Config');
}
if(argv.new) {
askQuestions( (err, configPath, config) => { askQuestions( (err, configPath, config) => {
if(err) { if(err) {
return; return;
@ -348,7 +357,10 @@ function handleConfigCommand() {
console.error('Exception attempting to create config: ' + e.toString()); console.error('Exception attempting to create config: ' + e.toString());
} }
}); });
} else {
process.exitCode = ExitCodes.ERROR;
return printUsage('Config');
}
} }
function main() { function main() {
@ -362,7 +374,7 @@ function main() {
if(0 === argv._.length || if(0 === argv._.length ||
'help' === argv._[0]) 'help' === argv._[0])
{ {
printUsage(''); printUsage('General');
process.exit(ExitCodes.SUCCESS); process.exit(ExitCodes.SUCCESS);
} }

View File

@ -30,6 +30,7 @@
"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", "temp": "^0.8.3",
"inquirer" : "^1.1.0",
"fs-extra" : "0.26.x" "fs-extra" : "0.26.x"
}, },
"engines": { "engines": {