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!
## Feature Available Now
## Features Available Now
* Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X)
* Multi node support
* **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
* WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**)
* Support board: ☠ BLACK ƒlag ☠ (**telnet://blackflag.acid.org:2425**)
* HappyLand (**telnet://andrew.homeunix.org:2023**)
## 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/)!!)
* Avon of [Agency BBS](http://bbs.geek.nz/) and fsxNet
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
* [Apam](https://github.com/apamment) of HappyLand BBS
## License
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:

View File

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

View File

@ -30,7 +30,7 @@ class ScheduledEvent {
this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].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 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');
@ -18,8 +16,6 @@ let packageJson = require('../package.json');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getMessageSerialNumber = getMessageSerialNumber;
exports.createMessageUuid = createMessageUuid;
exports.createMessageUuidAlternate = createMessageUuidAlternate;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString;
@ -96,45 +92,6 @@ function getDateTimeString(m) {
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) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1));
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) {
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)) {
this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
}
if(this.cls) {
this.client.term.write(ansi.resetScreen());
}
return cb(null);
};

View File

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

View File

@ -4,21 +4,30 @@
let msgDb = require('./database.js').dbs.message;
let wordWrapText = require('./word_wrap.js').wordWrapText;
let ftnUtil = require('./ftn_util.js');
let createNamedUUID = require('./uuid_util.js').createNamedUUID;
let uuid = require('node-uuid');
let async = require('async');
let _ = require('lodash');
let assert = require('assert');
let moment = require('moment');
const iconvEncode = require('iconv-lite').encode;
module.exports = Message;
const ENIGMA_MESSAGE_UUID_NAMESPACE = uuid.parse('154506df-1df8-46b9-98f8-ebb5815baaf8');
function Message(options) {
options = options || {};
this.messageId = options.messageId || 0; // always generated @ persist
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.toUserName = options.toUserName || '';
this.fromUserName = options.fromUserName || '';
@ -110,6 +119,24 @@ Message.prototype.setLocalFromUserId = function(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) {
msgDb.get(
`SELECT message_id
@ -330,10 +357,20 @@ Message.prototype.persist = function(cb) {
});
},
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(
`INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(self.modTimestamp) ],
[ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(msgTimestamp) ],
function inserted(err) { // use for this scope
if(!err) {
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;
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 modPath = paths.join(options.path, `${options.name}.js`); // general case first
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) {
return cb(e);
}
} else {
return cb(e);
}
}
if(!_.isObject(mod.moduleInfo)) {
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
mod.runtime = { config : modConfig };
cb(null, mod);
return cb(null, mod);
}
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) {
cb(err, mod);
return cb(err, mod);
});
}
@ -74,11 +89,11 @@ function loadModulesForCategory(category, iterator, complete) {
async.each(jsModules, (file, next) => {
loadModule(paths.basename(file, '.js'), category, (err, mod) => {
iterator(err, mod);
next();
return next();
});
}, err => {
if(complete) {
complete(err);
return complete(err);
}
});
});

View File

@ -23,6 +23,7 @@ const assert = require('assert');
const gaze = require('gaze');
const fse = require('fs-extra');
const iconv = require('iconv-lite');
const uuid = require('node-uuid');
exports.moduleInfo = {
name : 'FTN BSO',
@ -783,9 +784,13 @@ function FTNMessageScanTossModule() {
}
Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => {
if(msgIds && msgIds.length > 0) {
assert(1 === msgIds.length);
if(msgIds) {
// expect a single match, but dupe checking is not perfect - warn otherwise
if(1 === msgIds.length) {
message.replyToMsgId = msgIds[0];
} else {
Log.warn( { msgIds : msgIds, replyKludge : message.meta.FtnKludge.REPLY }, 'Found 2:n MSGIDs matching REPLY kludge!');
}
}
cb();
});
@ -800,28 +805,35 @@ function FTNMessageScanTossModule() {
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) {
message.areaTag = localAreaTag;
//
// If duplicates are NOT allowed in the area (the default), we need to update
// the message UUID using data available to us. Duplicate UUIDs are internally
// not allowed in our local database.
// If we *allow* dupes (disabled by default), then just generate
// a random UUID. Otherwise, don't assign the UUID just yet. It will be
// generated at persist() time and should be consistent across import/exports
//
if(!Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) {
if(self.messageHasValidMSGID(message)) {
// Update UUID with our preferred generation method
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);
}
if(Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) {
// just generate a UUID & therefor always allow for dupes
message.uuid = uuid.v1();
}
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:
// * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c
@ -855,7 +877,7 @@ function FTNMessageScanTossModule() {
this.importMessagesFromPacketFile = function(packetPath, password, cb) {
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 = {
areaSuccess : {}, // areaTag->count
@ -886,14 +908,23 @@ function FTNMessageScanTossModule() {
//
const localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag);
if(localAreaTag) {
message.uuid = Message.createMessageUUID(
localAreaTag,
message.modTimestamp,
message.subject,
message.message);
self.appendTearAndOrigin(message);
self.importEchoMailToArea(localAreaTag, packetHeader, message, err => {
if(err) {
// bump area fail stats
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(
{ area : localAreaTag, subject : message.subject, uuid : message.uuid },
{ area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId },
'Not importing non-unique message');
return next(null);

View File

@ -384,7 +384,29 @@ function getThemeArt(options, cb) {
//
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);
art.getArt(options.name, options, function artLoaded(err, artInfo) {
@ -563,10 +585,9 @@ function displayThemedAsset(assetSpec, client, options, cb) {
options = {};
}
var artAsset = asset.getArtAsset(assetSpec);
const artAsset = asset.getArtAsset(assetSpec);
if(!artAsset) {
cb(new Error('Asset not found: ' + assetSpec));
return;
return cb(new Error('Asset not found: ' + assetSpec));
}
// :TODO: just use simple merge of options -> displayOptions
@ -580,7 +601,7 @@ function displayThemedAsset(assetSpec, client, options, cb) {
switch(artAsset.type) {
case 'art' :
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;
@ -595,7 +616,6 @@ function displayThemedAsset(assetSpec, client, options, cb) {
break;
default :
cb(new Error('Unsupported art asset type: ' + artAsset.type));
break;
return cb(new Error('Unsupported art asset type: ' + artAsset.type));
}
}

View File

@ -1,15 +1,14 @@
/* jslint node: true */
'use strict';
var MenuView = require('./menu_view.js').MenuView;
var ansi = require('./ansi_term.js');
var strUtil = require('./string_util.js');
var miscUtil = require('./misc_util.js');
var colorCodes = require('./color_codes.js');
// ENiGMA½
const MenuView = require('./menu_view.js').MenuView;
const ansi = require('./ansi_term.js');
const strUtil = require('./string_util.js');
const colorCodes = require('./color_codes.js');
var util = require('util');
var assert = require('assert');
var _ = require('lodash');
// deps
const util = require('util');
exports.VerticalMenuView = VerticalMenuView;
@ -19,7 +18,7 @@ function VerticalMenuView(options) {
MenuView.call(this, options);
var self = this;
const self = this;
this.performAutoScale = function() {
if(this.autoScale.height) {
@ -28,13 +27,13 @@ function VerticalMenuView(options) {
}
if(self.autoScale.width) {
var l = 0;
self.items.forEach(function item(i) {
if(i.text.length > l) {
l = Math.min(i.text.length, self.client.term.termWidth - self.position.col);
let maxLen = 0;
self.items.forEach( item => {
if(item.text.length > maxLen) {
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) {
var item = self.items[index];
const item = self.items[index];
if(!item) {
return;
}
var focusItem;
var text;
let focusItem;
let text;
if(self.hasFocusItems()) {
focusItem = self.focusItems[index];
@ -119,8 +118,24 @@ VerticalMenuView.prototype.redraw = function() {
this.positionCacheExpired = false;
}
var row = this.position.row;
for(var i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) {
// erase old items
// :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;
row += this.itemSpacing + 1;
this.items[i].focused = this.focusedItemIndex === i;
@ -181,6 +196,11 @@ VerticalMenuView.prototype.getData = function() {
};
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);
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*
### oputil.js
Please see `oputil.js config` for configuration generation options.
### Example: System Name
`core/config.js` provides the default system name as follows:
```javascript

View File

@ -43,6 +43,15 @@ openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
## 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.
### 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
{
general: {
@ -79,13 +88,18 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
./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.
### Points of Interest
* 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.
* 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`
* **The first user you create via applying is the SysOp** (aka root)
* You may want to tail the logfile with Bunyan. See Monitoring Logs above.
# Advanced Installation
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 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 = {
name : 'Onelinerz',
desc : 'Standard local onelinerz',
@ -147,6 +156,11 @@ function OnelinerzModule(options) {
entriesView.focusItems = entriesView.items; // :TODO: this is a hack
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);
}
],

Binary file not shown.

View File

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

View File

@ -30,6 +30,7 @@
"ssh2": "^0.4.13",
"string-format": "davidchambers/string-format#mini-language",
"temp": "^0.8.3",
"inquirer" : "^1.1.0",
"fs-extra" : "0.26.x"
},
"engines": {