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!
|
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:
|
||||||
|
|
260
core/database.js
260
core/database.js
|
@ -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() {
|
|
||||||
createSystemTables();
|
|
||||||
});
|
|
||||||
callback(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbs.system.serialize( () => {
|
||||||
|
createSystemTables();
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
|
||||||
createUserTables();
|
|
||||||
createInitialUserValues();
|
|
||||||
});
|
|
||||||
callback(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbs.user.serialize( () => {
|
||||||
|
createUserTables();
|
||||||
|
createInitialUserValues();
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
|
||||||
createMessageBaseTables();
|
|
||||||
createInitialMessageValues();
|
|
||||||
});
|
|
||||||
callback(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dbs.message.serialize(function serialized() {
|
||||||
|
createMessageBaseTables();
|
||||||
|
createInitialMessageValues();
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
cb
|
||||||
cb(err);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,127 +121,135 @@ 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(
|
||||||
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
|
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
|
||||||
scan_toss VARCHAR NOT NULL,
|
scan_toss VARCHAR NOT NULL,
|
||||||
area_tag VARCHAR NOT NULL,
|
area_tag VARCHAR NOT NULL,
|
||||||
message_id INTEGER NOT NULL,
|
message_id INTEGER NOT NULL,
|
||||||
UNIQUE(scan_toss, area_tag)
|
UNIQUE(scan_toss, area_tag)
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -246,15 +246,20 @@ MenuModule.prototype.leave = function() {
|
||||||
this.detachViewControllers();
|
this.detachViewControllers();
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 } );
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -72,9 +72,9 @@ MenuView.prototype.getCount = function() {
|
||||||
return this.items.length;
|
return this.items.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 } );
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -130,11 +128,11 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) {
|
||||||
|
|
||||||
MenuView.prototype.setPropertyValue = function(propName, value) {
|
MenuView.prototype.setPropertyValue = function(propName, value) {
|
||||||
switch(propName) {
|
switch(propName) {
|
||||||
case 'itemSpacing' : this.setItemSpacing(value); break;
|
case 'itemSpacing' : this.setItemSpacing(value); break;
|
||||||
case 'items' : this.setItems(value); break;
|
case 'items' : this.setItems(value); break;
|
||||||
case 'focusItems' : this.setFocusItems(value); break;
|
case 'focusItems' : this.setFocusItems(value); break;
|
||||||
case 'hotKeys' : this.setHotKeys(value); break;
|
case 'hotKeys' : this.setHotKeys(value); break;
|
||||||
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -24,14 +24,29 @@ 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) {
|
} 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)) {
|
if(!_.isObject(mod.moduleInfo)) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
|
@ -192,11 +193,11 @@ function FTNMessageScanTossModule() {
|
||||||
let ext;
|
let ext;
|
||||||
|
|
||||||
switch(flowType) {
|
switch(flowType) {
|
||||||
case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break;
|
case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break;
|
||||||
case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break;
|
case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break;
|
||||||
case 'busy' : ext = 'bsy'; break;
|
case 'busy' : ext = 'bsy'; break;
|
||||||
case 'request' : ext = 'req'; break;
|
case 'request' : ext = 'req'; break;
|
||||||
case 'requests' : ext = 'hrq'; break;
|
case 'requests' : ext = 'hrq'; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ext;
|
return ext;
|
||||||
|
@ -307,8 +308,8 @@ function FTNMessageScanTossModule() {
|
||||||
// Set appropriate attribute flag for export type
|
// Set appropriate attribute flag for export type
|
||||||
//
|
//
|
||||||
switch(this.getExportType(options.nodeConfig)) {
|
switch(this.getExportType(options.nodeConfig)) {
|
||||||
case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break;
|
case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break;
|
||||||
case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break;
|
case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break;
|
||||||
// :TODO: Others?
|
// :TODO: Others?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
message.replyToMsgId = msgIds[0];
|
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();
|
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);
|
||||||
|
@ -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:
|
// Ref. implementations on import:
|
||||||
|
@ -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
|
||||||
|
@ -879,21 +901,30 @@ function FTNMessageScanTossModule() {
|
||||||
} else if('message' === entryType) {
|
} else if('message' === entryType) {
|
||||||
const message = entryData;
|
const message = entryData;
|
||||||
const areaTag = message.meta.FtnProperty.ftn_area;
|
const areaTag = message.meta.FtnProperty.ftn_area;
|
||||||
|
|
||||||
if(areaTag) {
|
if(areaTag) {
|
||||||
//
|
//
|
||||||
// EchoMail
|
// EchoMail
|
||||||
//
|
//
|
||||||
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);
|
||||||
|
|
|
@ -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
|
||||||
|
@ -578,24 +599,23 @@ 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;
|
||||||
|
|
||||||
case 'method' :
|
case 'method' :
|
||||||
// :TODO: fetch & render via method
|
// :TODO: fetch & render via method
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'inline ' :
|
case 'inline ' :
|
||||||
// :TODO: think about this more in relation to themes, etc. How can this come
|
// :TODO: think about this more in relation to themes, etc. How can this come
|
||||||
// from a theme (with override from menu.json) ???
|
// from a theme (with override from menu.json) ???
|
||||||
// look @ client.currentTheme.inlineArt[name] -> menu/prompt[name]
|
// look @ client.currentTheme.inlineArt[name] -> menu/prompt[name]
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default :
|
default :
|
||||||
cb(new Error('Unsupported art asset type: ' + artAsset.type));
|
return cb(new Error('Unsupported art asset type: ' + artAsset.type));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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];
|
||||||
|
@ -109,7 +108,7 @@ function VerticalMenuView(options) {
|
||||||
util.inherits(VerticalMenuView, MenuView);
|
util.inherits(VerticalMenuView, MenuView);
|
||||||
|
|
||||||
VerticalMenuView.prototype.redraw = function() {
|
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
|
// :TODO: rename positionCacheExpired to something that makese sense; combine methods for such
|
||||||
if(this.positionCacheExpired) {
|
if(this.positionCacheExpired) {
|
||||||
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
Binary file not shown.
102
oputil.js
102
oputil.js
|
@ -27,34 +27,37 @@ const ExitCodes = {
|
||||||
BAD_ARGS : -3,
|
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) {
|
function printUsage(command) {
|
||||||
var usage;
|
console.error(USAGE_HELP[command]);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,21 +337,30 @@ function askQuestions(cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfigCommand() {
|
function handleConfigCommand() {
|
||||||
askQuestions( (err, configPath, config) => {
|
if(true === argv.help) {
|
||||||
if(err) {
|
process.exitCode = ExitCodes.ERROR;
|
||||||
return;
|
return printUsage('Config');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t' } );
|
if(argv.new) {
|
||||||
|
askQuestions( (err, configPath, config) => {
|
||||||
try {
|
if(err) {
|
||||||
fs.writeFileSync(configPath, config, 'utf8');
|
return;
|
||||||
console.info('Configuration generated');
|
}
|
||||||
} catch(e) {
|
|
||||||
console.error('Exception attempting to create config: ' + e.toString());
|
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() {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,8 @@
|
||||||
"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",
|
||||||
"fs-extra" : "0.26.x"
|
"inquirer" : "^1.1.0",
|
||||||
|
"fs-extra" : "0.26.x"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.2"
|
"node": ">=0.12.2"
|
||||||
|
|
Loading…
Reference in New Issue