Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs
This commit is contained in:
commit
c2b9fb9c0c
|
@ -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:
|
||||
|
|
260
core/database.js
260
core/database.js
|
@ -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() {
|
||||
createSystemTables();
|
||||
});
|
||||
callback(null);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
dbs.system.serialize( () => {
|
||||
createSystemTables();
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
},
|
||||
function userDb(callback) {
|
||||
dbs.user = new sqlite3.Database(getDatabasePath('user'), function dbCreated(err) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
dbs.user.serialize(function serialized() {
|
||||
createUserTables();
|
||||
createInitialUserValues();
|
||||
});
|
||||
callback(null);
|
||||
dbs.user = new sqlite3.Database(getDatabasePath('user'), err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
dbs.user.serialize( () => {
|
||||
createUserTables();
|
||||
createInitialUserValues();
|
||||
});
|
||||
|
||||
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 {
|
||||
dbs.message.serialize(function serialized() {
|
||||
createMessageBaseTables();
|
||||
createInitialMessageValues();
|
||||
});
|
||||
callback(null);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
||||
dbs.message.serialize(function serialized() {
|
||||
createMessageBaseTables();
|
||||
createInitialMessageValues();
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
cb(err);
|
||||
}
|
||||
cb
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -119,127 +121,135 @@ 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(
|
||||
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
|
||||
scan_toss VARCHAR NOT NULL,
|
||||
area_tag VARCHAR NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
UNIQUE(scan_toss, area_tag)
|
||||
scan_toss VARCHAR NOT NULL,
|
||||
area_tag VARCHAR NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
UNIQUE(scan_toss, area_tag)
|
||||
);`
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -246,15 +246,20 @@ MenuModule.prototype.leave = function() {
|
|||
this.detachViewControllers();
|
||||
};
|
||||
|
||||
MenuModule.prototype.beforeArt = function(cb) {
|
||||
if(this.cls) {
|
||||
this.client.term.write(ansi.resetScreen());
|
||||
}
|
||||
|
||||
MenuModule.prototype.beforeArt = function(cb) {
|
||||
//
|
||||
// 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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 } );
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -72,9 +72,9 @@ MenuView.prototype.getCount = function() {
|
|||
return this.items.length;
|
||||
};
|
||||
|
||||
MenuView.prototype.getItems = function() {
|
||||
return _.map(this.items, function itemIter(i) {
|
||||
return i.text;
|
||||
MenuView.prototype.getItems = function() {
|
||||
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 } );
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -130,11 +128,11 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) {
|
|||
|
||||
MenuView.prototype.setPropertyValue = function(propName, value) {
|
||||
switch(propName) {
|
||||
case 'itemSpacing' : this.setItemSpacing(value); break;
|
||||
case 'items' : this.setItems(value); break;
|
||||
case 'focusItems' : this.setFocusItems(value); break;
|
||||
case 'hotKeys' : this.setHotKeys(value); break;
|
||||
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
||||
case 'itemSpacing' : this.setItemSpacing(value); break;
|
||||
case 'items' : this.setItems(value); break;
|
||||
case 'focusItems' : this.setFocusItems(value); break;
|
||||
case 'hotKeys' : this.setHotKeys(value); break;
|
||||
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
||||
}
|
||||
|
||||
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -24,14 +24,29 @@ 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) {
|
||||
return cb(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)) {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
@ -192,11 +193,11 @@ function FTNMessageScanTossModule() {
|
|||
let ext;
|
||||
|
||||
switch(flowType) {
|
||||
case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break;
|
||||
case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break;
|
||||
case 'busy' : ext = 'bsy'; break;
|
||||
case 'request' : ext = 'req'; break;
|
||||
case 'requests' : ext = 'hrq'; break;
|
||||
case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break;
|
||||
case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break;
|
||||
case 'busy' : ext = 'bsy'; break;
|
||||
case 'request' : ext = 'req'; break;
|
||||
case 'requests' : ext = 'hrq'; break;
|
||||
}
|
||||
|
||||
return ext;
|
||||
|
@ -307,8 +308,8 @@ function FTNMessageScanTossModule() {
|
|||
// Set appropriate attribute flag for export type
|
||||
//
|
||||
switch(this.getExportType(options.nodeConfig)) {
|
||||
case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break;
|
||||
case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break;
|
||||
case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break;
|
||||
case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break;
|
||||
// :TODO: Others?
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
message.replyToMsgId = msgIds[0];
|
||||
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(!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 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) {
|
||||
// just generate a UUID & therefor always allow for dupes
|
||||
message.uuid = uuid.v1();
|
||||
}
|
||||
|
||||
callback(null);
|
||||
|
@ -846,6 +858,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:
|
||||
|
@ -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
|
||||
|
@ -879,21 +901,30 @@ function FTNMessageScanTossModule() {
|
|||
} else if('message' === entryType) {
|
||||
const message = entryData;
|
||||
const areaTag = message.meta.FtnProperty.ftn_area;
|
||||
|
||||
|
||||
if(areaTag) {
|
||||
//
|
||||
// EchoMail
|
||||
//
|
||||
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);
|
||||
|
|
|
@ -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
|
||||
|
@ -578,24 +599,23 @@ 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 } );
|
||||
});
|
||||
break;
|
||||
case 'art' :
|
||||
displayThemeArt(dispOpts, function displayed(err, artData) {
|
||||
return cb(err, err ? null : { mciMap : artData.mciMap, height : artData.extraInfo.height } );
|
||||
});
|
||||
break;
|
||||
|
||||
case 'method' :
|
||||
// :TODO: fetch & render via method
|
||||
break;
|
||||
case 'method' :
|
||||
// :TODO: fetch & render via method
|
||||
break;
|
||||
|
||||
case 'inline ' :
|
||||
// :TODO: think about this more in relation to themes, etc. How can this come
|
||||
// from a theme (with override from menu.json) ???
|
||||
// look @ client.currentTheme.inlineArt[name] -> menu/prompt[name]
|
||||
break;
|
||||
case 'inline ' :
|
||||
// :TODO: think about this more in relation to themes, etc. How can this come
|
||||
// from a theme (with override from menu.json) ???
|
||||
// look @ client.currentTheme.inlineArt[name] -> menu/prompt[name]
|
||||
break;
|
||||
|
||||
default :
|
||||
cb(new Error('Unsupported art asset type: ' + artAsset.type));
|
||||
break;
|
||||
default :
|
||||
return cb(new Error('Unsupported art asset type: ' + artAsset.type));
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
@ -109,7 +108,7 @@ function VerticalMenuView(options) {
|
|||
util.inherits(VerticalMenuView, MenuView);
|
||||
|
||||
VerticalMenuView.prototype.redraw = function() {
|
||||
VerticalMenuView.super_.prototype.redraw.call(this);
|
||||
VerticalMenuView.super_.prototype.redraw.call(this);
|
||||
|
||||
// :TODO: rename positionCacheExpired to something that makese sense; combine methods for such
|
||||
if(this.positionCacheExpired) {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
Binary file not shown.
102
oputil.js
102
oputil.js
|
@ -27,34 +27,37 @@ const ExitCodes = {
|
|||
BAD_ARGS : -3,
|
||||
}
|
||||
|
||||
const USAGE_HELP = {
|
||||
General :
|
||||
`usage: optutil.js [--version] [--help]
|
||||
<command> [<args>]
|
||||
|
||||
global args:
|
||||
--config PATH : specify config path (${getDefaultConfigPath()})
|
||||
|
||||
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
|
||||
`
|
||||
}
|
||||
|
||||
function printUsage(command) {
|
||||
var usage;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
console.error(usage);
|
||||
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,21 +337,30 @@ function askQuestions(cb) {
|
|||
}
|
||||
|
||||
function handleConfigCommand() {
|
||||
askQuestions( (err, configPath, config) => {
|
||||
if(err) {
|
||||
return;
|
||||
}
|
||||
|
||||
config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t' } );
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, config, 'utf8');
|
||||
console.info('Configuration generated');
|
||||
} catch(e) {
|
||||
console.error('Exception attempting to create config: ' + e.toString());
|
||||
}
|
||||
});
|
||||
|
||||
if(true === argv.help) {
|
||||
process.exitCode = ExitCodes.ERROR;
|
||||
return printUsage('Config');
|
||||
}
|
||||
|
||||
if(argv.new) {
|
||||
askQuestions( (err, configPath, config) => {
|
||||
if(err) {
|
||||
return;
|
||||
}
|
||||
|
||||
config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t' } );
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, config, 'utf8');
|
||||
console.info('Configuration generated');
|
||||
} catch(e) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
"ssh2": "^0.4.13",
|
||||
"string-format": "davidchambers/string-format#mini-language",
|
||||
"temp": "^0.8.3",
|
||||
"fs-extra" : "0.26.x"
|
||||
"inquirer" : "^1.1.0",
|
||||
"fs-extra" : "0.26.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.2"
|
||||
|
|
Loading…
Reference in New Issue