Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs
This commit is contained in:
commit
2bf22e722f
|
@ -3,7 +3,7 @@ For :bug: bug reports, please fill out the information below plus any additional
|
|||
**Short problem description**
|
||||
|
||||
**Environment**
|
||||
- [ ] I am using Node.js v4.x or higher
|
||||
- [ ] I am using Node.js v6.x or higher
|
||||
- [ ] `npm install` reports success
|
||||
- Actual Node.js version (`node --version`):
|
||||
- Operating system (`uname -a` on *nix systems):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2015-2016, Bryan D. Ashby
|
||||
Copyright (c) 2015-2017, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
31
README.md
31
README.md
|
@ -6,27 +6,28 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
|
|||
|
||||
|
||||
## Features Available Now
|
||||
* Multi platform: Anywhere Node.js runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows)
|
||||
* Multi node support
|
||||
* Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows)
|
||||
* Unlimited multi node support (for all those BBS "callers"!)
|
||||
* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods
|
||||
* MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles
|
||||
* Telnet & **SSH** access built in. Additional servers are easy to implement & plug in
|
||||
* Telnet & **SSH** access built in. Additional servers are easy to implement
|
||||
* [CP437](http://www.ascii-codes.com/) and UTF-8 output
|
||||
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior
|
||||
* [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support
|
||||
* Pipe codes (ala Renegade)
|
||||
* [SQLite](http://sqlite.org/) storage of users and message areas
|
||||
* Renegade style pipe color codes
|
||||
* [SQLite](http://sqlite.org/) storage of users, message areas, and so on
|
||||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
|
||||
* Door support including common dropfile formats and legacy DOS doors, [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/)! (See [Doors](docs/doors.md))
|
||||
* [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/) support!
|
||||
* [Bunyan](https://github.com/trentm/node-bunyan) logging
|
||||
* FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export
|
||||
* [Message networks](docs/msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export
|
||||
* [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/web_server.md). Legacy X/Y/Z modem also supported!
|
||||
* Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more!
|
||||
|
||||
## In the Works
|
||||
* More ES6+ usage, and **documentation**!
|
||||
* File areas
|
||||
* More ACS support coverage
|
||||
* SysOp dashboard (ye ol' WFC)
|
||||
* Missing functionality such as searching, message area coloring, etc.
|
||||
* Missing functionality such as message FTS, user coloring of messages in the FST, etc.
|
||||
* String localization
|
||||
* A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
||||
|
||||
|
@ -37,11 +38,11 @@ See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more
|
|||
|
||||
## Support
|
||||
* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
||||
* **Discussion on a ENiGMA BBS!**
|
||||
* **Discussion on a ENiGMA BBS!** (see Boards below)
|
||||
* IRC: **#enigma-bbs** on **chat.freenode.net**
|
||||
* Email: bryan -at- l33t.codes
|
||||
* Facebook ENiGMA½ group
|
||||
* ENiGMA discussion on [HappyNet](http://andrew.homeunix.org/doku.php?id=happynet)
|
||||
* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/)
|
||||
* ENiGMA discussion on [fsxNet](http://bbs.geek.nz/#fsxNet)
|
||||
|
||||
## Terminal Clients
|
||||
ENiGMA has been tested with many terminals. However, the following are suggested for BBSing:
|
||||
|
@ -66,17 +67,17 @@ curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/inst
|
|||
* [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!)
|
||||
* [M. Griffin](https://github.com/M-griffin), author of [Enthral BBS](https://github.com/M-griffin/Enthral), [Oblivion/2 XRM](https://github.com/M-griffin/Oblivion2-XRM) and [EtherTerm](https://github.com/M-griffin/EtherTerm)!
|
||||
* [Caphood](http://www.reddit.com/user/Caphood), supreme SysOp of [BLACK ƒlag](http://www.bbsnexus.com/directory/listing/blackflag.html) BBS
|
||||
* Luciano Ayres of [Blocktronics](http://blocktronics.org/), creator of the "Mystery Skulls" default ENiGMA½ theme!
|
||||
* [Luciano Ayres](http://www.lucianoayres.com.br/) of [Blocktronics](http://blocktronics.org/), creator of the "Mystery Skulls" default ENiGMA½ theme!
|
||||
* Sudndeath for Xibalba ANSI work!
|
||||
* 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](http://bbs.geek.nz/#fsxNet)
|
||||
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
|
||||
* [Apam](https://github.com/apamment) of HappyLand BBS and [HappyNet](http://andrew.homeunix.org/doku.php?id=happynet)!
|
||||
|
||||
## License
|
||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
||||
|
||||
Copyright (c) 2015-2016, Bryan D. Ashby
|
||||
Copyright (c) 2015-2017, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
# Introduction
|
||||
This document covers basic upgrade notes for major ENiGMA½ version updates.
|
||||
|
||||
|
||||
# Before Upgrading
|
||||
* Always back up your system!
|
||||
* At least back up the `db` directory and your `menu.hjson` (or renamed equivalent)
|
||||
|
||||
|
||||
# General Notes
|
||||
Upgrades often come with changes to the default `menu.hjson`. It is wise to
|
||||
use a *different* file name for your BBS's version of this file and point to
|
||||
it via `config.hjson`. For example:
|
||||
|
||||
```hjson
|
||||
general: {
|
||||
menuFile: my_bbs.hjson
|
||||
}
|
||||
```
|
||||
|
||||
After updating code, use a program such as DiffMerge to merge in updates to
|
||||
`my_bbs.hjson` from the shipping `menu.hjson`.
|
||||
|
||||
|
||||
# Upgrading the Code
|
||||
Upgrading from GitHub is easy:
|
||||
|
||||
```bash
|
||||
cd /path/to/enigma-bbs
|
||||
git pull
|
||||
rm -rf npm_modules # do this any time you update Node.js itself
|
||||
npm install
|
||||
```
|
||||
|
||||
|
||||
# Problems
|
||||
Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or
|
||||
[file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues).
|
||||
|
||||
|
||||
# 0.0.1-alpha to 0.0.4-alpha
|
||||
## Node.js 6.x+ LTS is now **required**
|
||||
You will need to upgrade Node.js to [6.x+](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V6.md). If using [nvm](https://github.com/creationix/nvm) (you should be!) the process will go something like this:
|
||||
```bash
|
||||
nvm install 6
|
||||
nvm alias default 6
|
||||
```
|
||||
|
||||
### ES6
|
||||
Newly written code will use ES6 and a lot of code has started the migration process. Of note is the `MenuModule` class. If you have created a mod that inherits from `MenuModule`, you will need to upgrade your class to ES6.
|
||||
|
||||
## Manual Database Upgrade
|
||||
A few upgrades need to be made to your SQLite databases:
|
||||
|
||||
```bash
|
||||
rm db/file.sqltie3 # safe to delete this time as it was not used previously
|
||||
sqlite3 db/message.sqlite
|
||||
sqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild');
|
||||
```
|
||||
|
||||
## Archiver Changes
|
||||
If you have overridden or made additions to archivers in your `config.hjson` you will need to update them. See [Archive Configuration](docs/archive.md) and `core/config.js`
|
||||
|
||||
## File Base Configuration
|
||||
As 0.0.4-alpha contains file bases, you'll want to create a suitable configuration if you wish to use the feature. See [File Base Configuration](docs/file_base.md).
|
22
core/acs.js
22
core/acs.js
|
@ -25,6 +25,9 @@ class ACS {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Message Conferences & Areas
|
||||
//
|
||||
hasMessageConfRead(conf) {
|
||||
return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead);
|
||||
}
|
||||
|
@ -33,6 +36,21 @@ class ACS {
|
|||
return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead);
|
||||
}
|
||||
|
||||
//
|
||||
// File Base / Areas
|
||||
//
|
||||
hasFileAreaRead(area) {
|
||||
return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead);
|
||||
}
|
||||
|
||||
hasFileAreaWrite(area) {
|
||||
return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite);
|
||||
}
|
||||
|
||||
hasFileAreaDownload(area) {
|
||||
return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload);
|
||||
}
|
||||
|
||||
getConditionalValue(condArray, memberName) {
|
||||
assert(_.isArray(condArray));
|
||||
assert(_.isString(memberName));
|
||||
|
@ -59,6 +77,10 @@ class ACS {
|
|||
ACS.Defaults = {
|
||||
MessageAreaRead : 'GM[users]',
|
||||
MessageConfRead : 'GM[users]',
|
||||
|
||||
FileAreaRead : 'GM[users]',
|
||||
FileAreaWrite : 'GM[sysops]',
|
||||
FileAreaDownload : 'GM[users]',
|
||||
};
|
||||
|
||||
module.exports = ACS;
|
|
@ -48,8 +48,10 @@ function ANSIEscapeParser(options) {
|
|||
self.row = Math.max(self.row, 1);
|
||||
self.row = Math.min(self.row, self.termHeight);
|
||||
|
||||
self.emit('move cursor', self.column, self.row);
|
||||
self.rowUpdated();
|
||||
// self.emit('move cursor', self.column, self.row);
|
||||
|
||||
self.positionUpdated();
|
||||
//self.rowUpdated();
|
||||
};
|
||||
|
||||
self.saveCursorPosition = function() {
|
||||
|
@ -63,7 +65,9 @@ function ANSIEscapeParser(options) {
|
|||
self.row = self.savedPosition.row;
|
||||
self.column = self.savedPosition.column;
|
||||
delete self.savedPosition;
|
||||
self.rowUpdated();
|
||||
|
||||
self.positionUpdated();
|
||||
// self.rowUpdated();
|
||||
};
|
||||
|
||||
self.clearScreen = function() {
|
||||
|
@ -71,11 +75,76 @@ function ANSIEscapeParser(options) {
|
|||
self.emit('clear screen');
|
||||
};
|
||||
|
||||
/*
|
||||
self.rowUpdated = function() {
|
||||
self.emit('row update', self.row + self.scrollBack);
|
||||
};*/
|
||||
|
||||
self.positionUpdated = function() {
|
||||
self.emit('position update', self.row, self.column);
|
||||
};
|
||||
|
||||
function literal(text) {
|
||||
let charCode;
|
||||
let pos;
|
||||
let start = 0;
|
||||
const len = text.length;
|
||||
|
||||
function emitLiteral() {
|
||||
self.emit('literal', text.slice(start, pos));
|
||||
start = pos;
|
||||
}
|
||||
|
||||
for(pos = 0; pos < len; ++pos) {
|
||||
charCode = text.charCodeAt(pos) & 0xff; // ensure 8bit clean
|
||||
|
||||
switch(charCode) {
|
||||
case CR :
|
||||
emitLiteral();
|
||||
|
||||
self.column = 1;
|
||||
|
||||
self.positionUpdated();
|
||||
break;
|
||||
|
||||
case LF :
|
||||
emitLiteral();
|
||||
|
||||
self.row += 1;
|
||||
|
||||
self.positionUpdated();
|
||||
break;
|
||||
|
||||
default :
|
||||
if(self.column > self.termWidth) {
|
||||
//
|
||||
// Emit data up to this point so it can be drawn before the postion update
|
||||
//
|
||||
emitLiteral();
|
||||
|
||||
self.column = 1;
|
||||
self.row += 1;
|
||||
|
||||
self.positionUpdated();
|
||||
|
||||
|
||||
} else {
|
||||
self.column += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.emit('literal', text.slice(start));
|
||||
|
||||
if(self.column > self.termWidth) {
|
||||
self.column = 1;
|
||||
self.row += 1;
|
||||
self.positionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
function literal2(text) {
|
||||
var charCode;
|
||||
|
||||
var len = text.length;
|
||||
|
@ -88,29 +157,31 @@ function ANSIEscapeParser(options) {
|
|||
|
||||
case LF :
|
||||
self.row++;
|
||||
self.rowUpdated();
|
||||
self.positionUpdated();
|
||||
//self.rowUpdated();
|
||||
break;
|
||||
|
||||
default :
|
||||
// wrap
|
||||
if(self.column === self.termWidth) {
|
||||
if(self.column > self.termWidth) {
|
||||
self.column = 1;
|
||||
self.row++;
|
||||
self.rowUpdated();
|
||||
//self.rowUpdated();
|
||||
self.positionUpdated();
|
||||
} else {
|
||||
self.column++;
|
||||
self.column += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(self.row === 26) { // :TODO: should be termHeight + 1 ?
|
||||
self.scrollBack++;
|
||||
self.row--;
|
||||
self.rowUpdated();
|
||||
if(self.row === self.termHeight) {
|
||||
self.scrollBack += 1;
|
||||
self.row -= 1;
|
||||
|
||||
self.positionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
//self.emit('chunk', text);
|
||||
self.emit('literal', text);
|
||||
}
|
||||
|
||||
|
@ -155,7 +226,7 @@ function ANSIEscapeParser(options) {
|
|||
|
||||
self.lastMciCode = fullMciCode;
|
||||
|
||||
self.graphicRenditionForErase = _.clone(self.graphicRendition, true);
|
||||
self.graphicRenditionForErase = _.clone(self.graphicRendition);
|
||||
}
|
||||
|
||||
|
||||
|
@ -188,10 +259,10 @@ function ANSIEscapeParser(options) {
|
|||
}
|
||||
}
|
||||
|
||||
self.reset = function(buffer) {
|
||||
self.reset = function(input) {
|
||||
self.parseState = {
|
||||
// ignore anything past EOF marker, if any
|
||||
buffer : buffer.split(String.fromCharCode(0x1a), 1)[0],
|
||||
buffer : input.split(String.fromCharCode(0x1a), 1)[0],
|
||||
re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g,
|
||||
stop : false,
|
||||
};
|
||||
|
@ -201,7 +272,11 @@ function ANSIEscapeParser(options) {
|
|||
self.parseState.stop = true;
|
||||
};
|
||||
|
||||
self.parse = function() {
|
||||
self.parse = function(input) {
|
||||
if(input) {
|
||||
self.reset(input);
|
||||
}
|
||||
|
||||
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
|
||||
var pos;
|
||||
var match;
|
||||
|
@ -308,40 +383,45 @@ function ANSIEscapeParser(options) {
|
|||
*/
|
||||
|
||||
function escape(opCode, args) {
|
||||
var arg;
|
||||
var i;
|
||||
var len;
|
||||
let arg;
|
||||
|
||||
switch(opCode) {
|
||||
// cursor up
|
||||
case 'A' :
|
||||
arg = args[0] || 1;
|
||||
//arg = args[0] || 1;
|
||||
arg = isNaN(args[0]) ? 1 : args[0];
|
||||
self.moveCursor(0, -arg);
|
||||
break;
|
||||
|
||||
// cursor down
|
||||
case 'B' :
|
||||
arg = args[0] || 1;
|
||||
//arg = args[0] || 1;
|
||||
arg = isNaN(args[0]) ? 1 : args[0];
|
||||
self.moveCursor(0, arg);
|
||||
break;
|
||||
|
||||
// cursor forward/right
|
||||
case 'C' :
|
||||
arg = args[0] || 1;
|
||||
//arg = args[0] || 1;
|
||||
arg = isNaN(args[0]) ? 1 : args[0];
|
||||
self.moveCursor(arg, 0);
|
||||
break;
|
||||
|
||||
// cursor back/left
|
||||
case 'D' :
|
||||
arg = args[0] || 1;
|
||||
//arg = args[0] || 1;
|
||||
arg = isNaN(args[0]) ? 1 : args[0];
|
||||
self.moveCursor(-arg, 0);
|
||||
break;
|
||||
|
||||
case 'f' : // horiz & vertical
|
||||
case 'H' : // cursor position
|
||||
self.row = args[0] || 1;
|
||||
self.column = args[1] || 1;
|
||||
self.rowUpdated();
|
||||
//self.row = args[0] || 1;
|
||||
//self.column = args[1] || 1;
|
||||
self.row = isNaN(args[0]) ? 1 : args[0];
|
||||
self.column = isNaN(args[1]) ? 1 : args[1];
|
||||
//self.rowUpdated();
|
||||
self.positionUpdated();
|
||||
break;
|
||||
|
||||
// save position
|
||||
|
@ -356,7 +436,7 @@ function ANSIEscapeParser(options) {
|
|||
|
||||
// set graphic rendition
|
||||
case 'm' :
|
||||
for(i = 0, len = args.length; i < len; ++i) {
|
||||
for(let i = 0, len = args.length; i < len; ++i) {
|
||||
arg = args[i];
|
||||
|
||||
if(ANSIEscapeParser.foregroundColors[arg]) {
|
||||
|
@ -410,12 +490,13 @@ function ANSIEscapeParser(options) {
|
|||
}
|
||||
}
|
||||
}
|
||||
break; // m
|
||||
|
||||
break;
|
||||
// :TODO: s, u, K
|
||||
|
||||
// erase display/screen
|
||||
case 'J' :
|
||||
// :TODO: Handle others
|
||||
// :TODO: Handle other 'J' types!
|
||||
if(2 === args[0]) {
|
||||
self.clearScreen();
|
||||
}
|
||||
|
|
|
@ -4,12 +4,44 @@
|
|||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
|
||||
// base/modules
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const pty = require('ptyw.js');
|
||||
|
||||
let archiveUtil;
|
||||
|
||||
class Archiver {
|
||||
constructor(config) {
|
||||
this.compress = config.compress;
|
||||
this.decompress = config.decompress;
|
||||
this.list = config.list;
|
||||
this.extract = config.extract;
|
||||
|
||||
/*this.sig = new Buffer(config.sig, 'hex');
|
||||
this.offset = config.offset || 0;*/
|
||||
}
|
||||
|
||||
ok() {
|
||||
return this.canCompress() && this.canDecompress();
|
||||
}
|
||||
|
||||
can(what) {
|
||||
if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0;
|
||||
}
|
||||
|
||||
canCompress() { return this.can('compress'); }
|
||||
canDecompress() { return this.can('decompress'); }
|
||||
canList() { return this.can('list'); } // :TODO: validate entryMatch
|
||||
canExtract() { return this.can('extract'); }
|
||||
}
|
||||
|
||||
module.exports = class ArchiveUtil {
|
||||
|
||||
constructor() {
|
||||
|
@ -17,95 +49,120 @@ module.exports = class ArchiveUtil {
|
|||
this.longestSignature = 0;
|
||||
}
|
||||
|
||||
// singleton access
|
||||
static getInstance() {
|
||||
if(!archiveUtil) {
|
||||
archiveUtil = new ArchiveUtil();
|
||||
archiveUtil.init();
|
||||
}
|
||||
return archiveUtil;
|
||||
}
|
||||
|
||||
init() {
|
||||
//
|
||||
// Load configuration
|
||||
//
|
||||
if(_.has(Config, 'archivers')) {
|
||||
Object.keys(Config.archivers).forEach(archKey => {
|
||||
const arch = Config.archivers[archKey];
|
||||
if(!_.isString(arch.sig) ||
|
||||
!_.isString(arch.compressCmd) ||
|
||||
!_.isString(arch.decompressCmd) ||
|
||||
!_.isArray(arch.compressArgs) ||
|
||||
!_.isArray(arch.decompressArgs))
|
||||
{
|
||||
// :TODO: log warning
|
||||
return;
|
||||
if(_.has(Config, 'archives.archivers')) {
|
||||
Object.keys(Config.archives.archivers).forEach(archKey => {
|
||||
|
||||
const archConfig = Config.archives.archivers[archKey];
|
||||
const archiver = new Archiver(archConfig);
|
||||
|
||||
if(!archiver.ok()) {
|
||||
// :TODO: Log warning - bad archiver/config
|
||||
}
|
||||
|
||||
const archiver = {
|
||||
compressCmd : arch.compressCmd,
|
||||
compressArgs : arch.compressArgs,
|
||||
decompressCmd : arch.decompressCmd,
|
||||
decompressArgs : arch.decompressArgs,
|
||||
sig : new Buffer(arch.sig, 'hex'),
|
||||
offset : arch.offset || 0,
|
||||
};
|
||||
|
||||
this.archivers[archKey] = archiver;
|
||||
});
|
||||
}
|
||||
|
||||
if(archiver.offset + archiver.sig.length > this.longestSignature) {
|
||||
this.longestSignature = archiver.offset + archiver.sig.length;
|
||||
if(_.has(Config, 'archives.formats')) {
|
||||
Object.keys(Config.archives.formats).forEach(fmtKey => {
|
||||
|
||||
Config.archives.formats[fmtKey].sig = new Buffer(Config.archives.formats[fmtKey].sig, 'hex');
|
||||
Config.archives.formats[fmtKey].offset = Config.archives.formats[fmtKey].offset || 0;
|
||||
|
||||
const sigLen = Config.archives.formats[fmtKey].offset + Config.archives.formats[fmtKey].sig.length;
|
||||
if(sigLen > this.longestSignature) {
|
||||
this.longestSignature = sigLen;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
getArchiver(archType) {
|
||||
if(!archType) {
|
||||
if(!archType || 0 === archType.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
archType = archType.toLowerCase();
|
||||
return this.archivers[archType];
|
||||
}*/
|
||||
|
||||
getArchiver(archType) {
|
||||
if(!archType || 0 === archType.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(_.has(Config, [ 'archives', 'formats', archType, 'handler' ] ) &&
|
||||
_.has(Config, [ 'archives', 'archivers', Config.archives.formats[archType].handler ] ))
|
||||
{
|
||||
return Config.archives.archivers[ Config.archives.formats[archType].handler ];
|
||||
}
|
||||
}
|
||||
|
||||
haveArchiver(archType) {
|
||||
return this.getArchiver(archType) ? true : false;
|
||||
}
|
||||
|
||||
detectType(path, cb) {
|
||||
fs.open(path, 'r', (err, fd) => {
|
||||
if(err) {
|
||||
cb(err);
|
||||
return;
|
||||
detectTypeWithBuf(buf, cb) {
|
||||
// :TODO: implement me!
|
||||
}
|
||||
|
||||
let buf = new Buffer(this.longestSignature);
|
||||
detectType(path, cb) {
|
||||
if(!_.has(Config, 'archives.formats')) {
|
||||
return cb(Errors.DoesNotExist('No formats configured'));
|
||||
}
|
||||
|
||||
fs.open(path, 'r', (err, fd) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const buf = new Buffer(this.longestSignature);
|
||||
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// return first match
|
||||
const detected = _.findKey(this.archivers, arch => {
|
||||
const lenNeeded = arch.offset + arch.sig.length;
|
||||
const archFormat = _.findKey(Config.archives.formats, archFormat => {
|
||||
const lenNeeded = archFormat.offset + archFormat.sig.length;
|
||||
|
||||
if(bytesRead < lenNeeded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const comp = buf.slice(arch.offset, arch.offset + arch.sig.length);
|
||||
return (arch.sig.equals(comp));
|
||||
const comp = buf.slice(archFormat.offset, archFormat.offset + archFormat.sig.length);
|
||||
return (archFormat.sig.equals(comp));
|
||||
});
|
||||
|
||||
cb(detected ? null : new Error('Unknown type'), detected);
|
||||
return cb(archFormat ? null : Errors.General('Unknown type'), archFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
spawnHandler(comp, action, cb) {
|
||||
spawnHandler(proc, action, cb) {
|
||||
// pty.js doesn't currently give us a error when things fail,
|
||||
// so we have this horrible, horrible hack:
|
||||
let err;
|
||||
comp.once('data', d => {
|
||||
proc.once('data', d => {
|
||||
if(_.isString(d) && d.startsWith('execvp(3) failed.: No such file or directory')) {
|
||||
err = new Error(`${action} failed: ${d.trim()}`);
|
||||
}
|
||||
});
|
||||
|
||||
comp.once('exit', exitCode => {
|
||||
proc.once('exit', exitCode => {
|
||||
if(exitCode) {
|
||||
return cb(new Error(`${action} failed with exit code: ${exitCode}`));
|
||||
}
|
||||
|
@ -123,37 +180,97 @@ module.exports = class ArchiveUtil {
|
|||
return cb(new Error(`Unknown archive type: ${archType}`));
|
||||
}
|
||||
|
||||
let args = _.clone(archiver.compressArgs); // don't muck with orig
|
||||
for(let i = 0; i < args.length; ++i) {
|
||||
args[i] = stringFormat(args[i], {
|
||||
const fmtObj = {
|
||||
archivePath : archivePath,
|
||||
fileList : files.join(' '),
|
||||
});
|
||||
fileList : files.join(' '), // :TODO: probably need same hack as extractTo here!
|
||||
};
|
||||
|
||||
const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) );
|
||||
const proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts());
|
||||
|
||||
return this.spawnHandler(proc, 'Compression', cb);
|
||||
}
|
||||
|
||||
let comp = pty.spawn(archiver.compressCmd, args, this.getPtyOpts());
|
||||
extractTo(archivePath, extractPath, archType, fileList, cb) {
|
||||
let haveFileList;
|
||||
|
||||
return this.spawnHandler(comp, 'Compression', cb);
|
||||
if(!cb && _.isFunction(fileList)) {
|
||||
cb = fileList;
|
||||
fileList = [];
|
||||
haveFileList = false;
|
||||
} else {
|
||||
haveFileList = true;
|
||||
}
|
||||
|
||||
extractTo(archivePath, extractPath, archType, cb) {
|
||||
const archiver = this.getArchiver(archType);
|
||||
|
||||
if(!archiver) {
|
||||
return cb(new Error(`Unknown archive type: ${archType}`));
|
||||
}
|
||||
|
||||
let args = _.clone(archiver.decompressArgs); // don't muck with orig
|
||||
for(let i = 0; i < args.length; ++i) {
|
||||
args[i] = stringFormat(args[i], {
|
||||
const fmtObj = {
|
||||
archivePath : archivePath,
|
||||
extractPath : extractPath,
|
||||
};
|
||||
|
||||
const action = haveFileList ? 'extract' : 'decompress';
|
||||
|
||||
// we need to treat {fileList} special in that it should be broken up to 0:n args
|
||||
const args = archiver[action].args.map( arg => {
|
||||
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
|
||||
});
|
||||
|
||||
const fileListPos = args.indexOf('{fileList}');
|
||||
if(fileListPos > -1) {
|
||||
// replace {fileList} with 0:n sep file list arguments
|
||||
args.splice.apply(args, [fileListPos, 1].concat(fileList));
|
||||
}
|
||||
|
||||
const proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts());
|
||||
|
||||
return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb);
|
||||
}
|
||||
|
||||
listEntries(archivePath, archType, cb) {
|
||||
const archiver = this.getArchiver(archType);
|
||||
|
||||
if(!archiver) {
|
||||
return cb(new Error(`Unknown archive type: ${archType}`));
|
||||
}
|
||||
|
||||
const fmtObj = {
|
||||
archivePath : archivePath,
|
||||
};
|
||||
|
||||
const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) );
|
||||
const proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
|
||||
|
||||
let output = '';
|
||||
proc.on('data', data => {
|
||||
// :TODO: hack for: execvp(3) failed.: No such file or directory
|
||||
|
||||
output += data;
|
||||
});
|
||||
|
||||
proc.once('exit', exitCode => {
|
||||
if(exitCode) {
|
||||
return cb(new Error(`List failed with exit code: ${exitCode}`));
|
||||
}
|
||||
|
||||
const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 };
|
||||
|
||||
const entries = [];
|
||||
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
|
||||
let m;
|
||||
while((m = entryMatchRe.exec(output))) {
|
||||
entries.push({
|
||||
byteSize : parseInt(m[entryGroupOrder.byteSize]),
|
||||
fileName : m[entryGroupOrder.fileName],
|
||||
});
|
||||
}
|
||||
|
||||
let comp = pty.spawn(archiver.decompressCmd, args, this.getPtyOpts());
|
||||
|
||||
return this.spawnHandler(comp, 'Decompression', cb);
|
||||
return cb(null, entries);
|
||||
});
|
||||
}
|
||||
|
||||
getPtyOpts() {
|
||||
|
|
16
core/art.js
16
core/art.js
|
@ -7,7 +7,6 @@ const miscUtil = require('./misc_util.js');
|
|||
const ansi = require('./ansi_term.js');
|
||||
const aep = require('./ansi_escape_parser.js');
|
||||
const sauce = require('./sauce.js');
|
||||
const farmhash = require('farmhash');
|
||||
|
||||
// deps
|
||||
const fs = require('fs');
|
||||
|
@ -15,6 +14,7 @@ const paths = require('path');
|
|||
const assert = require('assert');
|
||||
const iconv = require('iconv-lite');
|
||||
const _ = require('lodash');
|
||||
const farmhash = require('farmhash');
|
||||
|
||||
exports.getArt = getArt;
|
||||
exports.getArtFromPath = getArtFromPath;
|
||||
|
@ -78,7 +78,7 @@ function getArtFromPath(path, options, cb) {
|
|||
return iconv.decode(data, encoding);
|
||||
} else {
|
||||
const eofMarker = defaultEofFromExtension(ext);
|
||||
return iconv.decode(sliceAtEOF(data, eofMarker), encoding);
|
||||
return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,11 +213,15 @@ function getArt(name, options, cb) {
|
|||
}
|
||||
|
||||
function defaultEncodingFromExtension(ext) {
|
||||
return SUPPORTED_ART_TYPES[ext.toLowerCase()].defaultEncoding;
|
||||
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
|
||||
return artType ? artType.defaultEncoding : 'utf8';
|
||||
}
|
||||
|
||||
function defaultEofFromExtension(ext) {
|
||||
return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof;
|
||||
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
|
||||
if(artType) {
|
||||
return artType.eof;
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO: Implement the following
|
||||
|
@ -266,7 +270,7 @@ function display(client, art, options, cb) {
|
|||
if(!options.disableMciCache && !mciMapFromCache) {
|
||||
// cache our MCI findings...
|
||||
client.mciCache[artHash] = mciMap;
|
||||
client.log.trace( { artHash : artHash, mciMap : mciMap }, 'Added MCI map to cache');
|
||||
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache');
|
||||
}
|
||||
|
||||
ansiParser.removeAllListeners(); // :TODO: Necessary???
|
||||
|
@ -290,7 +294,7 @@ function display(client, art, options, cb) {
|
|||
|
||||
if(mciMap) {
|
||||
mciMapFromCache = true;
|
||||
client.log.trace( { artHash : artHash, mciMap : mciMap }, 'Loaded MCI map from cache');
|
||||
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache');
|
||||
} else {
|
||||
// no cached MCI info
|
||||
mciMap = {};
|
||||
|
|
195
core/bbs.js
195
core/bbs.js
|
@ -10,12 +10,15 @@ const conf = require('./config.js');
|
|||
const logger = require('./logger.js');
|
||||
const database = require('./database.js');
|
||||
const clientConns = require('./client_connections.js');
|
||||
const resolvePath = require('./misc_util.js').resolvePath;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const util = require('util');
|
||||
const _ = require('lodash');
|
||||
const mkdirs = require('fs-extra').mkdirs;
|
||||
const fs = require('fs');
|
||||
const paths = require('path');
|
||||
|
||||
// our main entry point
|
||||
exports.bbsMain = bbsMain;
|
||||
|
@ -23,30 +26,38 @@ exports.bbsMain = bbsMain;
|
|||
// object with various services we want to de-init/shutdown cleanly if possible
|
||||
const initServices = {};
|
||||
|
||||
const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby';
|
||||
const HELP =
|
||||
`${ENIGMA_COPYRIGHT}
|
||||
usage: main.js <args>
|
||||
|
||||
valid args:
|
||||
--version : display version
|
||||
--help : displays this help
|
||||
--config PATH : override default config.hjson path
|
||||
`;
|
||||
|
||||
function printHelpAndExit() {
|
||||
console.info(HELP);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
function bbsMain() {
|
||||
async.waterfall(
|
||||
[
|
||||
function processArgs(callback) {
|
||||
const args = process.argv.slice(2);
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
var configPath;
|
||||
|
||||
if(args.indexOf('--help') > 0) {
|
||||
// :TODO: display help
|
||||
} else {
|
||||
let argCount = args.length;
|
||||
for(let i = 0; i < argCount; ++i) {
|
||||
const arg = args[i];
|
||||
if('--config' === arg) {
|
||||
configPath = args[i + 1];
|
||||
}
|
||||
}
|
||||
if(argv.help) {
|
||||
printHelpAndExit();
|
||||
}
|
||||
|
||||
callback(null, configPath || conf.getDefaultPath(), _.isString(configPath));
|
||||
const configOverridePath = argv.config;
|
||||
|
||||
return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath));
|
||||
},
|
||||
function initConfig(configPath, configPathSupplied, callback) {
|
||||
conf.init(configPath, function configInit(err) {
|
||||
conf.init(resolvePath(configPath), function configInit(err) {
|
||||
|
||||
//
|
||||
// If the user supplied a path and we can't read/parse it
|
||||
|
@ -71,14 +82,20 @@ function bbsMain() {
|
|||
if(err) {
|
||||
console.error('Error initializing: ' + util.inspect(err));
|
||||
}
|
||||
callback(err);
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function listenConnections(callback) {
|
||||
startListening(callback);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
// note this is escaped:
|
||||
fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => {
|
||||
console.info(ENIGMA_COPYRIGHT);
|
||||
if(!err) {
|
||||
console.info(banner);
|
||||
}
|
||||
console.info('System started!');
|
||||
});
|
||||
|
||||
if(err) {
|
||||
console.error('Error initializing: ' + util.inspect(err));
|
||||
}
|
||||
|
@ -87,7 +104,9 @@ function bbsMain() {
|
|||
}
|
||||
|
||||
function shutdownSystem() {
|
||||
logger.log.info('Process interrupted, shutting down...');
|
||||
const msg = 'Process interrupted. Shutting down...';
|
||||
console.info(msg);
|
||||
logger.log.info(msg);
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -100,21 +119,32 @@ function shutdownSystem() {
|
|||
}
|
||||
callback(null);
|
||||
},
|
||||
function stopListeningServers(callback) {
|
||||
return require('./listening_server.js').shutdown( () => {
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
},
|
||||
function stopEventScheduler(callback) {
|
||||
if(initServices.eventScheduler) {
|
||||
return initServices.eventScheduler.shutdown( () => {
|
||||
callback(null); // ignore err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
} else {
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function stopFileAreaWeb(callback) {
|
||||
require('./file_area_web.js').startup( () => {
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
},
|
||||
function stopMsgNetwork(callback) {
|
||||
require('./msg_network.js').shutdown(callback);
|
||||
}
|
||||
],
|
||||
() => {
|
||||
process.exit();
|
||||
console.info('Goodbye!');
|
||||
return process.exit();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -208,6 +238,12 @@ function initialize(cb) {
|
|||
function readyMessageNetworkSupport(callback) {
|
||||
return require('./msg_network.js').startup(callback);
|
||||
},
|
||||
function listenConnections(callback) {
|
||||
return require('./listening_server.js').startup(callback);
|
||||
},
|
||||
function readyFileAreaWeb(callback) {
|
||||
return require('./file_area_web.js').startup(callback);
|
||||
},
|
||||
function readyEventScheduler(callback) {
|
||||
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
||||
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
||||
|
@ -221,118 +257,3 @@ function initialize(cb) {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
function startListening(cb) {
|
||||
if(!conf.config.loginServers) {
|
||||
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
|
||||
return cb(new Error('No login servers configured'));
|
||||
}
|
||||
|
||||
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
||||
|
||||
moduleUtil.loadModulesForCategory('loginServers', (err, module) => {
|
||||
if(err) {
|
||||
if('EENIGMODDISABLED' === err.code) {
|
||||
logger.log.debug(err.message);
|
||||
} else {
|
||||
logger.log.info( { err : err }, 'Failed loading module');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const port = parseInt(module.runtime.config.port);
|
||||
if(isNaN(port)) {
|
||||
logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleInst = new module.getModule();
|
||||
let server;
|
||||
try {
|
||||
server = moduleInst.createServer();
|
||||
} catch(e) {
|
||||
logger.log.warn(e, 'Exception caught creating server!');
|
||||
return;
|
||||
}
|
||||
|
||||
// :TODO: handle maxConnections, e.g. conf.maxConnections
|
||||
|
||||
server.on('client', function newClient(client, clientSock) {
|
||||
//
|
||||
// Start tracking the client. We'll assign it an ID which is
|
||||
// just the index in our connections array.
|
||||
//
|
||||
if(_.isUndefined(client.session)) {
|
||||
client.session = {};
|
||||
}
|
||||
|
||||
client.session.serverName = module.moduleInfo.name;
|
||||
client.session.isSecure = module.moduleInfo.isSecure || false;
|
||||
|
||||
clientConns.addNewClient(client, clientSock);
|
||||
|
||||
client.on('ready', function clientReady(readyOptions) {
|
||||
|
||||
client.startIdleMonitor();
|
||||
|
||||
// Go to module -- use default error handler
|
||||
prepareClient(client, function clientPrepared() {
|
||||
require('./connect.js').connectEntry(client, readyOptions.firstMenu);
|
||||
});
|
||||
});
|
||||
|
||||
client.on('end', function onClientEnd() {
|
||||
clientConns.removeClient(client);
|
||||
});
|
||||
|
||||
client.on('error', function onClientError(err) {
|
||||
logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message);
|
||||
});
|
||||
|
||||
client.on('close', function onClientClose(hadError) {
|
||||
const logFunc = hadError ? logger.log.info : logger.log.debug;
|
||||
logFunc( { clientId : client.session.id }, 'Connection closed');
|
||||
|
||||
clientConns.removeClient(client);
|
||||
});
|
||||
|
||||
client.on('idle timeout', function idleTimeout() {
|
||||
client.log.info('User idle timeout expired');
|
||||
|
||||
client.menuStack.goto('idleLogoff', function goMenuRes(err) {
|
||||
if(err) {
|
||||
// likely just doesn't exist
|
||||
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.on('error', function serverErr(err) {
|
||||
logger.log.info(err); // 'close' should be handled after
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
|
||||
logger.log.info(
|
||||
{ server : module.moduleInfo.name, port : port }, 'Listening for connections');
|
||||
}, err => {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareClient(client, cb) {
|
||||
const theme = require('./theme.js');
|
||||
|
||||
// :TODO: it feels like this should go somewhere else... and be a bit more elegant.
|
||||
|
||||
if('*' === conf.config.preLoginTheme) {
|
||||
client.user.properties.theme_id = theme.getRandomTheme() || '';
|
||||
} else {
|
||||
client.user.properties.theme_id = conf.config.preLoginTheme;
|
||||
}
|
||||
|
||||
theme.setClientTheme(client, client.user.properties.theme_id);
|
||||
return cb(null); // note: currently useless to use cb here - but this may change...again...
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var TextView = require('./text_view.js').TextView;
|
||||
var miscUtil = require('./misc_util.js');
|
||||
var util = require('util');
|
||||
var assert = require('assert');
|
||||
const TextView = require('./text_view.js').TextView;
|
||||
const miscUtil = require('./misc_util.js');
|
||||
const util = require('util');
|
||||
|
||||
exports.ButtonView = ButtonView;
|
||||
|
||||
|
@ -20,6 +19,7 @@ function ButtonView(options) {
|
|||
util.inherits(ButtonView, TextView);
|
||||
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
// allow space = submit
|
||||
if(' ' === ch) {
|
||||
this.emit('action', 'accept');
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ exports.getActiveConnections = getActiveConnections;
|
|||
exports.getActiveNodeList = getActiveNodeList;
|
||||
exports.addNewClient = addNewClient;
|
||||
exports.removeClient = removeClient;
|
||||
exports.getConnectionByUserId = getConnectionByUserId;
|
||||
|
||||
const clientConnections = [];
|
||||
exports.clientConnections = clientConnections;
|
||||
|
@ -93,3 +94,7 @@ function removeClient(client) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectionByUserId(userId) {
|
||||
return getActiveConnections().find( ac => userId === ac.user.userId );
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.sortAreasOrConfs = sortAreasOrConfs;
|
||||
|
||||
//
|
||||
// Method for sorting message, file, etc. areas and confs
|
||||
// If the sort key is present and is a number, sort in numerical order;
|
||||
// Otherwise, use a locale comparison on the sort key or name as a fallback
|
||||
//
|
||||
function sortAreasOrConfs(areasOrConfs, type) {
|
||||
let entryA;
|
||||
let entryB;
|
||||
|
||||
areasOrConfs.sort((a, b) => {
|
||||
entryA = type ? a[type] : a;
|
||||
entryB = type ? b[type] : b;
|
||||
|
||||
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
|
||||
return entryA.sort - entryB.sort;
|
||||
} else {
|
||||
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
|
||||
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
|
||||
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare
|
||||
}
|
||||
});
|
||||
}
|
310
core/config.js
310
core/config.js
|
@ -1,14 +1,16 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var miscUtil = require('./misc_util.js');
|
||||
// ENiGMA½
|
||||
const miscUtil = require('./misc_util.js');
|
||||
|
||||
var fs = require('fs');
|
||||
var paths = require('path');
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var hjson = require('hjson');
|
||||
var assert = require('assert');
|
||||
// deps
|
||||
const fs = require('fs');
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const hjson = require('hjson');
|
||||
const assert = require('assert');
|
||||
|
||||
exports.init = init;
|
||||
exports.getDefaultPath = getDefaultPath;
|
||||
|
@ -43,38 +45,45 @@ function init(configPath, cb) {
|
|||
async.waterfall(
|
||||
[
|
||||
function loadUserConfig(callback) {
|
||||
if(_.isString(configPath)) {
|
||||
fs.readFile(configPath, { encoding : 'utf8' }, function configData(err, data) {
|
||||
if(!_.isString(configPath)) {
|
||||
return callback(null, { } );
|
||||
}
|
||||
|
||||
fs.readFile(configPath, { encoding : 'utf8' }, (err, configData) => {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let configJson;
|
||||
try {
|
||||
var configJson = hjson.parse(data);
|
||||
callback(null, configJson);
|
||||
configJson = hjson.parse(configData);
|
||||
} catch(e) {
|
||||
callback(e);
|
||||
}
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
return callback(null, configJson);
|
||||
});
|
||||
} else {
|
||||
callback(null, { } );
|
||||
}
|
||||
},
|
||||
function mergeWithDefaultConfig(configJson, callback) {
|
||||
var mergedConfig = _.merge(getDefaultConfig(), configJson, function mergeCustomizer(conf1, conf2) {
|
||||
|
||||
const mergedConfig = _.mergeWith(
|
||||
getDefaultConfig(),
|
||||
configJson, (conf1, conf2) => {
|
||||
// Arrays should always concat
|
||||
if(_.isArray(conf1)) {
|
||||
// :TODO: look for collisions & override dupes
|
||||
return conf1.concat(conf2);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
callback(null, mergedConfig);
|
||||
return callback(null, mergedConfig);
|
||||
},
|
||||
function validate(mergedConfig, callback) {
|
||||
//
|
||||
// Various sections must now exist in config
|
||||
//
|
||||
// :TODO: Logic is broken here:
|
||||
if(hasMessageConferenceAndArea(mergedConfig)) {
|
||||
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
||||
msgAreasErr.code = 'EBADCONFIG';
|
||||
|
@ -92,7 +101,7 @@ function init(configPath, cb) {
|
|||
}
|
||||
|
||||
function getDefaultPath() {
|
||||
var base = miscUtil.resolvePath('~/');
|
||||
const base = miscUtil.resolvePath('~/');
|
||||
if(base) {
|
||||
// e.g. /home/users/joeuser/.config/enigma-bbs/config.hjson
|
||||
return paths.join(base, '.config', 'enigma-bbs', 'config.hjson');
|
||||
|
@ -211,17 +220,212 @@ function getDefaultConfig() {
|
|||
}
|
||||
},
|
||||
|
||||
archivers : {
|
||||
zip : {
|
||||
sig : '504b0304',
|
||||
offset : 0,
|
||||
compressCmd : '7z',
|
||||
compressArgs : [ 'a', '-tzip', '{archivePath}', '{fileList}' ],
|
||||
decompressCmd : '7z',
|
||||
decompressArgs : [ 'e', '-o{extractPath}', '{archivePath}' ]
|
||||
contentServers : {
|
||||
web : {
|
||||
domain : 'another-fine-enigma-bbs.org',
|
||||
|
||||
staticRoot : paths.join(__dirname, './../www'),
|
||||
|
||||
http : {
|
||||
enabled : false,
|
||||
port : 8080,
|
||||
},
|
||||
https : {
|
||||
enabled : false,
|
||||
port : 8443,
|
||||
certPem : paths.join(__dirname, './../misc/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
archives : {
|
||||
archivers : {
|
||||
'7Zip' : {
|
||||
compress : {
|
||||
cmd : '7za',
|
||||
args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ],
|
||||
},
|
||||
decompress : {
|
||||
cmd : '7za',
|
||||
args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
||||
},
|
||||
list : {
|
||||
cmd : '7za',
|
||||
args : [ 'l', '{archivePath}' ],
|
||||
entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$',
|
||||
},
|
||||
extract : {
|
||||
cmd : '7za',
|
||||
args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
||||
},
|
||||
},
|
||||
|
||||
Lha : {
|
||||
//
|
||||
// 'lha' command can be obtained from:
|
||||
// * apt-get: lhasa
|
||||
//
|
||||
// (compress not currently supported)
|
||||
//
|
||||
decompress : {
|
||||
cmd : 'lha',
|
||||
args : [ '-ew={extractPath}', '{archivePath}' ],
|
||||
},
|
||||
list : {
|
||||
cmd : 'lha',
|
||||
args : [ '-l', '{archivePath}' ],
|
||||
entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$',
|
||||
},
|
||||
extract : {
|
||||
cmd : 'lha',
|
||||
args : [ '-ew={extractPath}', '{archivePath}', '{fileList}' ]
|
||||
}
|
||||
},
|
||||
|
||||
Arj : {
|
||||
//
|
||||
// 'arj' command can be obtained from:
|
||||
// * apt-get: arj
|
||||
//
|
||||
decompress : {
|
||||
cmd : 'arj',
|
||||
args : [ 'x', '{archivePath}', '{extractPath}' ],
|
||||
},
|
||||
list : {
|
||||
cmd : 'arj',
|
||||
args : [ 'l', '{archivePath}' ],
|
||||
entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$',
|
||||
entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 }
|
||||
fileName : 1,
|
||||
byteSize : 2,
|
||||
}
|
||||
},
|
||||
extract : {
|
||||
cmd : 'arj',
|
||||
args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ],
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
formats : {
|
||||
//
|
||||
// Resources
|
||||
// * http://www.garykessler.net/library/file_sigs.html
|
||||
//
|
||||
zip : {
|
||||
sig : '504b0304',
|
||||
offset : 0,
|
||||
exts : [ 'zip' ],
|
||||
handler : '7Zip',
|
||||
desc : 'ZIP Archive',
|
||||
},
|
||||
'7z' : {
|
||||
sig : '377abcaf271c',
|
||||
offset : 0,
|
||||
exts : [ '7z' ],
|
||||
handler : '7Zip',
|
||||
desc : '7-Zip Archive',
|
||||
},
|
||||
arj : {
|
||||
sig : '60ea',
|
||||
offset : 0,
|
||||
exts : [ 'arj' ],
|
||||
handler : 'Arj',
|
||||
desc : 'ARJ Archive',
|
||||
},
|
||||
rar : {
|
||||
sig : '526172211a0700',
|
||||
offset : 0,
|
||||
exts : [ 'rar' ],
|
||||
handler : '7Zip',
|
||||
desc : 'RAR Archive',
|
||||
},
|
||||
gzip : {
|
||||
sig : '1f8b',
|
||||
offset : 0,
|
||||
exts : [ 'gz' ],
|
||||
handler : '7Zip',
|
||||
desc : 'Gzip Archive',
|
||||
},
|
||||
bzip : {
|
||||
sig : '425a68',
|
||||
offset : 0,
|
||||
exts : [ 'bz2' ],
|
||||
handler : '7Zip',
|
||||
desc : 'BZip2 Archive',
|
||||
},
|
||||
lzh : {
|
||||
sig : '2d6c68',
|
||||
offset : 2,
|
||||
exts : [ 'lzh', 'ice' ],
|
||||
handler : 'Lha',
|
||||
desc : 'LHArc Archive',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fileTransferProtocols : {
|
||||
//
|
||||
// See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ
|
||||
//
|
||||
zmodem8kSexyz : {
|
||||
name : 'ZModem 8k (SEXYZ)',
|
||||
type : 'external',
|
||||
sort : 1,
|
||||
external : {
|
||||
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
|
||||
sendCmd : 'sexyz',
|
||||
sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ],
|
||||
recvCmd : 'sexyz',
|
||||
recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ],
|
||||
recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ],
|
||||
}
|
||||
},
|
||||
|
||||
xmodemSexyz : {
|
||||
name : 'XModem (SEXYZ)',
|
||||
type : 'external',
|
||||
sort : 3,
|
||||
external : {
|
||||
sendCmd : 'sexyz',
|
||||
sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ],
|
||||
recvCmd : 'sexyz',
|
||||
recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ]
|
||||
}
|
||||
},
|
||||
|
||||
ymodemSexyz : {
|
||||
name : 'YModem (SEXYZ)',
|
||||
type : 'external',
|
||||
sort : 4,
|
||||
external : {
|
||||
sendCmd : 'sexyz',
|
||||
sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ],
|
||||
recvCmd : 'sexyz',
|
||||
recvArgs : [ '-telnet', 'ry', '{uploadDir}' ],
|
||||
}
|
||||
},
|
||||
|
||||
zmodem8kSz : {
|
||||
name : 'ZModem 8k',
|
||||
type : 'external',
|
||||
sort : 2,
|
||||
external : {
|
||||
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
|
||||
sendArgs : [
|
||||
// :TODO: try -q
|
||||
'--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}'
|
||||
],
|
||||
recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
|
||||
recvArgs : [
|
||||
'--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir}
|
||||
],
|
||||
// :TODO: can we not just use --escape ?
|
||||
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
messageAreaDefaults : {
|
||||
//
|
||||
|
@ -272,22 +476,60 @@ function getDefaultConfig() {
|
|||
// areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|:
|
||||
areaStoragePrefix : paths.join(__dirname, './../file_base/'),
|
||||
|
||||
maxDescFileByteSize : 471859, // ~1/4 MB
|
||||
maxDescLongFileByteSize : 524288, // 1/2 MB
|
||||
|
||||
fileNamePatterns: {
|
||||
shortDesc : [ '^FILE_ID\.DIZ$', '^DESC\.SDI$' ],
|
||||
longDesc : [ '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$' ],
|
||||
// These are NOT case sensitive
|
||||
// FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ
|
||||
desc : [
|
||||
'^FILE_ID\.DIZ$', '^DESC\.SDI$', '^DESCRIPT\.ION$', '^FILE\.DES$', '$FILE\.SDI$', '^DISK\.ID$'
|
||||
],
|
||||
|
||||
// common README filename - https://en.wikipedia.org/wiki/README
|
||||
descLong : [
|
||||
'^.*\.NFO$', '^README\.1ST$', '^README\.NOW$', '^README\.TXT$', '^READ\.ME$', '^README$', '^README\.md$'
|
||||
],
|
||||
},
|
||||
|
||||
yearEstPatterns: [
|
||||
//
|
||||
// Patterns should produce the year in the first submatch.
|
||||
// The extracted year may be YY or YYYY
|
||||
//
|
||||
'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc.
|
||||
"\\b('[1789][0-9])\\b", // eslint-disable-line quotes
|
||||
'\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b',
|
||||
'\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997
|
||||
// :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc.
|
||||
// :TODO: "Copyright YYYY someone"
|
||||
],
|
||||
|
||||
web : {
|
||||
path : '/f/',
|
||||
routePath : '/f/[a-zA-Z0-9]+$',
|
||||
expireMinutes : 1440, // 1 day
|
||||
},
|
||||
|
||||
//
|
||||
// File area storage location tag/value pairs.
|
||||
// Non-absolute paths are relative to |areaStoragePrefix|.
|
||||
//
|
||||
storageTags : {
|
||||
sys_msg_attach : 'msg_attach',
|
||||
},
|
||||
|
||||
areas: {
|
||||
message_attachment : {
|
||||
system_message_attachment : {
|
||||
name : 'Message attachments',
|
||||
desc : 'File attachments to messages',
|
||||
storageTags : 'sys_msg_attach', // may be string or array of strings
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
eventScheduler : {
|
||||
|
||||
|
||||
events : {
|
||||
trimMessageAreas : {
|
||||
// may optionally use [or ]@watch:/path/to/file
|
||||
|
|
|
@ -40,11 +40,11 @@ function ConfigCache() {
|
|||
this.gaze.on('changed', function fileChanged(filePath) {
|
||||
assert(filePath in self.cache);
|
||||
|
||||
Log.info( { filePath : filePath }, 'Configuration file changed; recaching');
|
||||
Log.info( { path : filePath }, 'Configuration file changed; re-caching');
|
||||
|
||||
self.reCacheConfigFromFile(filePath, function reCached(err) {
|
||||
if(err) {
|
||||
Log.error( { error : err, filePath : filePath } , 'Error recaching HJSON config');
|
||||
Log.error( { error : err.message, path : filePath } , 'Failed re-caching configuration');
|
||||
} else {
|
||||
self.emit('recached', filePath);
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ function displayBanner(term) {
|
|||
// note: intentional formatting:
|
||||
term.pipeWrite(`
|
||||
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
||||
|06Copyright (c) 2014-2016 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Copyright (c) 2014-2017 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
||||
|00`
|
||||
);
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const CRC32_TABLE = new Int32Array(
|
||||
'00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D'.split(' ').map(s => parseInt(s, 16)));
|
||||
|
||||
exports.CRC32 = class CRC32 {
|
||||
constructor() {
|
||||
this.crc = -1;
|
||||
}
|
||||
|
||||
update(input) {
|
||||
input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary');
|
||||
return input.length > 10240 ? this.update_8(input) : this.update_4(input);
|
||||
}
|
||||
|
||||
update_4(input) {
|
||||
const len = input.length - 3;
|
||||
let i = 0;
|
||||
|
||||
for(i = 0; i < len;) {
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
}
|
||||
while(i < len + 3) {
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ];
|
||||
}
|
||||
}
|
||||
|
||||
update_8(input) {
|
||||
const len = input.length - 7;
|
||||
let i = 0;
|
||||
|
||||
for(i = 0; i < len;) {
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ];
|
||||
}
|
||||
while(i < len + 7) {
|
||||
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ];
|
||||
}
|
||||
}
|
||||
|
||||
finalize() {
|
||||
return (this.crc ^ (-1)) >>> 0;
|
||||
}
|
||||
};
|
|
@ -10,11 +10,13 @@ const paths = require('path');
|
|||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const moment = require('moment');
|
||||
|
||||
// database handles
|
||||
let dbs = {};
|
||||
|
||||
exports.getModDatabasePath = getModDatabasePath;
|
||||
exports.getISOTimestampString = getISOTimestampString;
|
||||
exports.initializeDatabases = initializeDatabases;
|
||||
|
||||
exports.dbs = dbs;
|
||||
|
@ -46,26 +48,31 @@ function getModDatabasePath(moduleInfo, suffix) {
|
|||
return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`);
|
||||
}
|
||||
|
||||
function getISOTimestampString(ts) {
|
||||
ts = ts || moment();
|
||||
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
}
|
||||
|
||||
function initializeDatabases(cb) {
|
||||
async.each( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
|
||||
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
|
||||
dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
dbs[dbName].serialize( () => {
|
||||
DB_INIT_TABLE[dbName]();
|
||||
|
||||
DB_INIT_TABLE[dbName]( () => {
|
||||
return next(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
const DB_INIT_TABLE = {
|
||||
system : () => {
|
||||
system : (cb) => {
|
||||
dbs.system.run('PRAGMA foreign_keys = ON;');
|
||||
|
||||
// Various stat/event logging - see stat_log.js
|
||||
|
@ -98,9 +105,11 @@ const DB_INIT_TABLE = {
|
|||
UNIQUE(timestamp, user_id, log_name)
|
||||
);`
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
user : () => {
|
||||
user : (cb) => {
|
||||
dbs.user.run('PRAGMA foreign_keys = ON;');
|
||||
|
||||
dbs.user.run(
|
||||
|
@ -138,9 +147,11 @@ const DB_INIT_TABLE = {
|
|||
timestamp DATETIME NOT NULL
|
||||
);`
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
message : () => {
|
||||
message : (cb) => {
|
||||
dbs.message.run('PRAGMA foreign_keys = ON;');
|
||||
|
||||
dbs.message.run(
|
||||
|
@ -175,17 +186,23 @@ const DB_INIT_TABLE = {
|
|||
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;
|
||||
END;`
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
|
||||
dbs.message.run(
|
||||
`CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
|
||||
DELETE FROM message_fts WHERE docid=old.rowid;
|
||||
END;
|
||||
END;`
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
|
||||
dbs.message.run(
|
||||
`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;
|
||||
END;`
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
|
||||
dbs.message.run(
|
||||
`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;`
|
||||
);
|
||||
|
@ -201,6 +218,7 @@ const DB_INIT_TABLE = {
|
|||
);`
|
||||
);
|
||||
|
||||
|
||||
// :TODO: need SQL to ensure cleaned up if delete from message?
|
||||
/*
|
||||
dbs.message.run(
|
||||
|
@ -237,9 +255,11 @@ const DB_INIT_TABLE = {
|
|||
UNIQUE(scan_toss, area_tag)
|
||||
);`
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
file : () => {
|
||||
file : (cb) => {
|
||||
dbs.file.run('PRAGMA foreign_keys = ON;');
|
||||
|
||||
dbs.file.run(
|
||||
|
@ -247,11 +267,11 @@ const DB_INIT_TABLE = {
|
|||
`CREATE TABLE IF NOT EXISTS file (
|
||||
file_id INTEGER PRIMARY KEY,
|
||||
area_tag VARCHAR NOT NULL,
|
||||
file_sha1 VARCHAR NOT NULL,
|
||||
file_sha256 VARCHAR NOT NULL,
|
||||
file_name, /* FTS @ file_fts */
|
||||
storage_tag VARCHAR NOT NULL,
|
||||
desc, /* FTS @ file_fts */
|
||||
desc_long, /* FTS @ file_fts */
|
||||
upload_by_username VARCHAR NOT NULL,
|
||||
upload_timestamp DATETIME NOT NULL
|
||||
);`
|
||||
);
|
||||
|
@ -273,18 +293,24 @@ const DB_INIT_TABLE = {
|
|||
dbs.file.run(
|
||||
`CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN
|
||||
DELETE FROM file_fts WHERE docid=old.rowid;
|
||||
END;
|
||||
END;`
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
|
||||
dbs.file.run(
|
||||
`CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
|
||||
DELETE FROM file_fts WHERE docid=old.rowid;
|
||||
END;
|
||||
END;`
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
|
||||
INSERT INTO file_fts(docid, file_name, desc, long_desc) VALUES(new.rowid, new.file_name, new.desc, new.long_desc);
|
||||
END;
|
||||
dbs.file.run(
|
||||
`CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
|
||||
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
|
||||
END;`
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
|
||||
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.long_desc);
|
||||
dbs.file.run(
|
||||
`CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
|
||||
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
|
||||
END;`
|
||||
);
|
||||
|
||||
|
@ -315,5 +341,24 @@ const DB_INIT_TABLE = {
|
|||
UNIQUE(hash_tag_id, file_id)
|
||||
);`
|
||||
);
|
||||
|
||||
dbs.file.run(
|
||||
`CREATE TABLE IF NOT EXISTS file_user_rating (
|
||||
file_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
rating INTEGER NOT NULL,
|
||||
|
||||
UNIQUE(file_id, user_id)
|
||||
);`
|
||||
);
|
||||
|
||||
dbs.file.run(
|
||||
`CREATE TABLE IF NOT EXISTS file_web_serve (
|
||||
hash_id VARCHAR NOT NULL PRIMARY KEY,
|
||||
expire_timestamp DATETIME NOT NULL
|
||||
);`
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
};
|
|
@ -100,6 +100,7 @@ Door.prototype.run = function() {
|
|||
}
|
||||
|
||||
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
|
||||
// :TODO: Use .map() here
|
||||
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
|
||||
|
||||
for(let i = 0; i < args.length; ++i) {
|
||||
|
|
|
@ -10,28 +10,26 @@ const async = require('async');
|
|||
const _ = require('lodash');
|
||||
const SSHClient = require('ssh2').Client;
|
||||
|
||||
exports.getModule = DoorPartyModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'DoorParty',
|
||||
desc : 'DoorParty Access Module',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
|
||||
function DoorPartyModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
const self = this;
|
||||
exports.getModule = class DoorPartyModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
// establish defaults
|
||||
this.config = options.menuConfig.config;
|
||||
this.config.host = this.config.host || 'dp.throwbackbbs.com';
|
||||
this.config.sshPort = this.config.sshPort || 2022;
|
||||
this.config.rloginPort = this.config.rloginPort || 513;
|
||||
}
|
||||
|
||||
this.initSequence = function() {
|
||||
initSequence() {
|
||||
let clientTerminated;
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -116,7 +114,7 @@ function DoorPartyModule(options) {
|
|||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.toString() }, 'DoorParty error');
|
||||
self.client.log.warn( { error : err.message }, 'DoorParty error');
|
||||
}
|
||||
|
||||
// if the client is stil here, go to previous
|
||||
|
@ -125,8 +123,5 @@ function DoorPartyModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
require('util').inherits(DoorPartyModule, MenuModule);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const FileEntry = require('./file_entry.js');
|
||||
|
||||
module.exports = class DownloadQueue {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
if(!Array.isArray(this.client.user.downloadQueue)) {
|
||||
if(this.client.user.properties.dl_queue) {
|
||||
this.loadFromProperty(this.client.user.properties.dl_queue);
|
||||
} else {
|
||||
this.client.user.downloadQueue = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this.client.user.downloadQueue;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.client.user.downloadQueue = [];
|
||||
}
|
||||
|
||||
toggle(fileEntry) {
|
||||
if(this.isQueued(fileEntry)) {
|
||||
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
||||
} else {
|
||||
this.add(fileEntry);
|
||||
}
|
||||
}
|
||||
|
||||
add(fileEntry) {
|
||||
this.client.user.downloadQueue.push({
|
||||
fileId : fileEntry.fileId,
|
||||
areaTag : fileEntry.areaTag,
|
||||
fileName : fileEntry.fileName,
|
||||
path : fileEntry.filePath,
|
||||
byteSize : fileEntry.meta.byte_size || 0,
|
||||
});
|
||||
}
|
||||
|
||||
removeItems(fileIds) {
|
||||
if(!Array.isArray(fileIds)) {
|
||||
fileIds = [ fileIds ];
|
||||
}
|
||||
|
||||
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => ( -1 === fileIds.indexOf(e.fileId) ) );
|
||||
}
|
||||
|
||||
isQueued(entryOrId) {
|
||||
if(entryOrId instanceof FileEntry) {
|
||||
entryOrId = entryOrId.fileId;
|
||||
}
|
||||
|
||||
return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false;
|
||||
}
|
||||
|
||||
toProperty() { return JSON.stringify(this.client.user.downloadQueue); }
|
||||
|
||||
loadFromProperty(prop) {
|
||||
try {
|
||||
this.client.user.downloadQueue = JSON.parse(prop);
|
||||
} catch(e) {
|
||||
this.client.user.downloadQueue = [];
|
||||
|
||||
this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property');
|
||||
}
|
||||
}
|
||||
};
|
|
@ -19,12 +19,22 @@ class EnigError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
class EnigMenuError extends EnigError { }
|
||||
|
||||
exports.EnigError = EnigError;
|
||||
exports.EnigMenuError = EnigMenuError;
|
||||
|
||||
exports.Errors = {
|
||||
General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode),
|
||||
MenuStack : (reason, reasonCode) => new EnigMenuError('Menu stack error', -33001, reason, reasonCode),
|
||||
MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode),
|
||||
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode),
|
||||
AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode),
|
||||
Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
|
||||
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
|
||||
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
|
||||
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode),
|
||||
};
|
||||
|
||||
exports.ErrorReasons = {
|
||||
AlreadyThere : 'ALREADYTHERE',
|
||||
InvalidNextMenu : 'BADNEXT',
|
||||
NoPreviousMenu : 'NOPREV',
|
||||
NoConditionMatch : 'NOCONDMATCH',
|
||||
};
|
|
@ -0,0 +1,273 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const FileDb = require('./database.js').dbs.file;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const getServer = require('./listening_server.js').getServer;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
|
||||
// deps
|
||||
const hashids = require('hashids');
|
||||
const moment = require('moment');
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
const fs = require('fs');
|
||||
const mimeTypes = require('mime-types');
|
||||
|
||||
const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server';
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
* Load temp download URLs @ startup & set expire timers via scheduler.
|
||||
* At creation, set expire timer via scheduler
|
||||
*
|
||||
*/
|
||||
|
||||
class FileAreaWebAccess {
|
||||
constructor() {
|
||||
this.hashids = new hashids(Config.general.boardName);
|
||||
this.expireTimers = {}; // hashId->timer
|
||||
}
|
||||
|
||||
startup(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function initFromDb(callback) {
|
||||
return self.load(callback);
|
||||
},
|
||||
function addWebRoute(callback) {
|
||||
self.webServer = getServer(WEB_SERVER_PACKAGE_NAME);
|
||||
if(!self.webServer) {
|
||||
return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`));
|
||||
}
|
||||
|
||||
const routeAdded = self.webServer.instance.addRoute({
|
||||
method : 'GET',
|
||||
path : Config.fileBase.web.routePath,
|
||||
handler : self.routeWebRequestForFile.bind(self),
|
||||
});
|
||||
|
||||
return callback(routeAdded ? null : Errors.General('Failed adding route'));
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
shutdown(cb) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
load(cb) {
|
||||
//
|
||||
// Load entries, register expiration timers
|
||||
//
|
||||
FileDb.each(
|
||||
`SELECT hash_id, expire_timestamp
|
||||
FROM file_web_serve;`,
|
||||
(err, row) => {
|
||||
if(row) {
|
||||
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
removeEntry(hashId) {
|
||||
//
|
||||
// Delete record from DB, and our timer
|
||||
//
|
||||
FileDb.run(
|
||||
`DELETE FROM file_web_serve
|
||||
WHERE hash_id = ?;`,
|
||||
[ hashId ]
|
||||
);
|
||||
|
||||
delete this.expireTimers[hashId];
|
||||
}
|
||||
|
||||
scheduleExpire(hashId, expireTime) {
|
||||
|
||||
// remove any previous entry for this hashId
|
||||
const previous = this.expireTimers[hashId];
|
||||
if(previous) {
|
||||
clearTimeout(previous);
|
||||
delete this.expireTimers[hashId];
|
||||
}
|
||||
|
||||
const timeoutMs = expireTime.diff(moment());
|
||||
|
||||
if(timeoutMs <= 0) {
|
||||
setImmediate( () => {
|
||||
this.removeEntry(hashId);
|
||||
});
|
||||
} else {
|
||||
this.expireTimers[hashId] = setTimeout( () => {
|
||||
this.removeEntry(hashId);
|
||||
}, timeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
loadServedHashId(hashId, cb) {
|
||||
FileDb.get(
|
||||
`SELECT expire_timestamp FROM
|
||||
file_web_serve
|
||||
WHERE hash_id = ?`,
|
||||
[ hashId ],
|
||||
(err, result) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const decoded = this.hashids.decode(hashId);
|
||||
if(!result || 2 !== decoded.length) {
|
||||
return cb(Errors.Invalid('Invalid or unknown hash ID'));
|
||||
}
|
||||
|
||||
return cb(
|
||||
null,
|
||||
{
|
||||
hashId : hashId,
|
||||
userId : decoded[0],
|
||||
fileId : decoded[1],
|
||||
expireTimestamp : moment(result.expire_timestamp),
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getHashId(client, fileEntry) {
|
||||
//
|
||||
// Hashid is a unique combination of userId & fileId
|
||||
//
|
||||
return this.hashids.encode(client.user.userId, fileEntry.fileId);
|
||||
}
|
||||
|
||||
buildTempDownloadLink(client, fileEntry, hashId) {
|
||||
hashId = hashId || this.getHashId(client, fileEntry);
|
||||
|
||||
//
|
||||
// Create a URL such as
|
||||
// https://l33t.codes:44512/f/qFdxyZr
|
||||
//
|
||||
// Prefer HTTPS over HTTP. Be explicit about the port
|
||||
// only if non-standard.
|
||||
//
|
||||
let schema;
|
||||
let port;
|
||||
if(Config.contentServers.web.https.enabled) {
|
||||
schema = 'https://';
|
||||
port = (443 === Config.contentServers.web.https.port) ?
|
||||
'' :
|
||||
`:${Config.contentServers.web.https.port}`;
|
||||
} else {
|
||||
schema = 'http://';
|
||||
port = (80 === Config.contentServers.web.http.port) ?
|
||||
'' :
|
||||
`:${Config.contentServers.web.http.port}`;
|
||||
}
|
||||
|
||||
return `${schema}${Config.contentServers.web.domain}${port}${Config.fileBase.web.path}${hashId}`;
|
||||
}
|
||||
|
||||
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||
const hashId = this.getHashId(client, fileEntry);
|
||||
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
servedItem.url = this.buildTempDownloadLink(client, fileEntry);
|
||||
|
||||
return cb(null, servedItem);
|
||||
});
|
||||
}
|
||||
|
||||
createAndServeTempDownload(client, fileEntry, options, cb) {
|
||||
const hashId = this.getHashId(client, fileEntry);
|
||||
const url = this.buildTempDownloadLink(client, fileEntry, hashId);
|
||||
options.expireTime = options.expireTime || moment().add(2, 'days');
|
||||
|
||||
// add/update rec with hash id and (latest) timestamp
|
||||
FileDb.run(
|
||||
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
|
||||
VALUES (?, ?);`,
|
||||
[ hashId, getISOTimestampString(options.expireTime) ],
|
||||
err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.scheduleExpire(hashId, options.expireTime);
|
||||
|
||||
return cb(null, url);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fileNotFound(resp) {
|
||||
this.webServer.instance.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
||||
}
|
||||
|
||||
routeWebRequestForFile(req, resp) {
|
||||
const hashId = paths.basename(req.url);
|
||||
|
||||
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||
|
||||
if(err) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
const fileEntry = new FileEntry();
|
||||
fileEntry.load(servedItem.fileId, err => {
|
||||
if(err) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
const filePath = fileEntry.filePath;
|
||||
if(!filePath) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
resp.on('close', () => {
|
||||
// connection closed *before* the response was fully sent
|
||||
// :TODO: Log and such
|
||||
});
|
||||
|
||||
resp.on('finish', () => {
|
||||
// transfer completed fully
|
||||
// :TODO: we need to update the users stats - bytes xferred, credit stuff, etc.
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
|
||||
'Content-Length' : stats.size,
|
||||
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
|
||||
};
|
||||
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
resp.writeHead(200, headers);
|
||||
return readStream.pipe(resp);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FileAreaWebAccess();
|
|
@ -0,0 +1,671 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const FileDb = require('./database.js').dbs.file;
|
||||
const ArchiveUtil = require('./archive_util.js');
|
||||
const CRC32 = require('./crc.js').CRC32;
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const paths = require('path');
|
||||
const temptmp = require('temptmp').createTrackedSession('file_area');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
exports.isInternalArea = isInternalArea;
|
||||
exports.getAvailableFileAreas = getAvailableFileAreas;
|
||||
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
|
||||
exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory;
|
||||
exports.getAreaStorageLocations = getAreaStorageLocations;
|
||||
exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
|
||||
exports.getFileAreaByTag = getFileAreaByTag;
|
||||
exports.getFileEntryPath = getFileEntryPath;
|
||||
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
||||
exports.scanFile = scanFile;
|
||||
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||
|
||||
const WellKnownAreaTags = exports.WellKnownAreaTags = {
|
||||
Invalid : '',
|
||||
MessageAreaAttach : 'system_message_attachment',
|
||||
};
|
||||
|
||||
function isInternalArea(areaTag) {
|
||||
return areaTag === WellKnownAreaTags.MessageAreaAttach;
|
||||
}
|
||||
|
||||
function getAvailableFileAreas(client, options) {
|
||||
options = options || { };
|
||||
|
||||
// perform ACS check per conf & omit internal if desired
|
||||
const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } ));
|
||||
|
||||
return _.omitBy(allAreas, areaInfo => {
|
||||
if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) {
|
||||
return true; // omit
|
||||
}
|
||||
|
||||
return !client.acs.hasFileAreaRead(areaInfo);
|
||||
});
|
||||
}
|
||||
|
||||
function getSortedAvailableFileAreas(client, options) {
|
||||
const areas = _.map(getAvailableFileAreas(client, options), v => v);
|
||||
sortAreasOrConfs(areas);
|
||||
return areas;
|
||||
}
|
||||
|
||||
function getDefaultFileAreaTag(client, disableAcsCheck) {
|
||||
let defaultArea = _.findKey(Config.fileBase, o => o.default);
|
||||
if(defaultArea) {
|
||||
const area = Config.fileBase.areas[defaultArea];
|
||||
if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) {
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
// just use anything we can
|
||||
defaultArea = _.findKey(Config.fileBase.areas, (area, areaTag) => {
|
||||
return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area));
|
||||
});
|
||||
|
||||
return defaultArea;
|
||||
}
|
||||
|
||||
function getFileAreaByTag(areaTag) {
|
||||
const areaInfo = Config.fileBase.areas[areaTag];
|
||||
if(areaInfo) {
|
||||
areaInfo.areaTag = areaTag; // convienence!
|
||||
areaInfo.storage = getAreaStorageLocations(areaInfo);
|
||||
return areaInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function changeFileAreaWithOptions(client, areaTag, options, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function getArea(callback) {
|
||||
const area = getFileAreaByTag(areaTag);
|
||||
return callback(area ? null : Errors.Invalid('Invalid file areaTag'), area);
|
||||
},
|
||||
function validateAccess(area, callback) {
|
||||
if(!client.acs.hasFileAreaRead(area)) {
|
||||
return callback(Errors.AccessDenied('No access to this area'));
|
||||
}
|
||||
},
|
||||
function changeArea(area, callback) {
|
||||
if(true === options.persist) {
|
||||
client.user.persistProperty('file_area_tag', areaTag, err => {
|
||||
return callback(err, area);
|
||||
});
|
||||
} else {
|
||||
client.user.properties['file_area_tag'] = areaTag;
|
||||
return callback(null, area);
|
||||
}
|
||||
}
|
||||
],
|
||||
(err, area) => {
|
||||
if(!err) {
|
||||
client.log.info( { areaTag : areaTag, area : area }, 'Current file area changed');
|
||||
} else {
|
||||
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change file area');
|
||||
}
|
||||
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getAreaStorageDirectoryByTag(storageTag) {
|
||||
const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]);
|
||||
|
||||
return paths.resolve(Config.fileBase.areaStoragePrefix, storageLocation || '');
|
||||
}
|
||||
|
||||
function getAreaDefaultStorageDirectory(areaInfo) {
|
||||
return getAreaStorageDirectoryByTag(areaInfo.storageTags[0]);
|
||||
}
|
||||
|
||||
function getAreaStorageLocations(areaInfo) {
|
||||
|
||||
const storageTags = Array.isArray(areaInfo.storageTags) ?
|
||||
areaInfo.storageTags :
|
||||
[ areaInfo.storageTags || '' ];
|
||||
|
||||
const avail = Config.fileBase.storageTags;
|
||||
|
||||
return _.compact(storageTags.map(storageTag => {
|
||||
if(avail[storageTag]) {
|
||||
return {
|
||||
storageTag : storageTag,
|
||||
dir : getAreaStorageDirectoryByTag(storageTag),
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function getFileEntryPath(fileEntry) {
|
||||
const areaInfo = getFileAreaByTag(fileEntry.areaTag);
|
||||
if(areaInfo) {
|
||||
return paths.join(areaInfo.storageDirectory, fileEntry.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingFileEntriesBySha256(sha256, cb) {
|
||||
const entries = [];
|
||||
|
||||
FileDb.each(
|
||||
`SELECT file_id, area_tag
|
||||
FROM file
|
||||
WHERE file_sha256=?;`,
|
||||
[ sha256 ],
|
||||
(err, fileRow) => {
|
||||
if(fileRow) {
|
||||
entries.push({
|
||||
fileId : fileRow.file_id,
|
||||
areaTag : fileRow.area_tag,
|
||||
});
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return cb(err, entries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// :TODO: This is bascially sliceAtEOF() from art.js .... DRY!
|
||||
function sliceAtSauceMarker(data) {
|
||||
let eof = data.length;
|
||||
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
|
||||
|
||||
for(let i = eof - 1; i > stopPos; i--) {
|
||||
if(0x1a === data[i]) {
|
||||
eof = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data.slice(0, eof);
|
||||
}
|
||||
|
||||
function attemptSetEstimatedReleaseDate(fileEntry) {
|
||||
// :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time
|
||||
const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi'));
|
||||
|
||||
function getMatch(input) {
|
||||
if(input) {
|
||||
let m;
|
||||
for(let i = 0; i < patterns.length; ++i) {
|
||||
m = patterns[i].exec(input);
|
||||
if(m) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// We attempt deteciton in short -> long order
|
||||
//
|
||||
const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong);
|
||||
if(match && match[1]) {
|
||||
let year;
|
||||
if(2 === match[1].length) {
|
||||
year = parseInt(match[1]);
|
||||
if(year) {
|
||||
if(year > 70) {
|
||||
year += 1900;
|
||||
} else {
|
||||
year += 2000;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
year = parseInt(match[1]);
|
||||
}
|
||||
|
||||
if(year) {
|
||||
fileEntry.meta.est_release_year = year;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a simple log proxy for when we call from oputil.js
|
||||
function logDebug(obj, msg) {
|
||||
if(Log) {
|
||||
Log.debug(obj, msg);
|
||||
}
|
||||
}
|
||||
|
||||
function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) {
|
||||
const archiveUtil = ArchiveUtil.getInstance();
|
||||
const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive()
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getArchiveFileList(callback) {
|
||||
stepInfo.step = 'archive_list_start';
|
||||
|
||||
iterator(err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
archiveUtil.listEntries(filePath, archiveType, (err, entries) => {
|
||||
if(err) {
|
||||
stepInfo.step = 'archive_list_failed';
|
||||
} else {
|
||||
stepInfo.step = 'archive_list_finish';
|
||||
stepInfo.archiveEntries = entries || [];
|
||||
}
|
||||
|
||||
iterator(iterErr => {
|
||||
return callback( iterErr, entries || [] ); // ignore original |err| here
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
function processDescFilesStart(entries, callback) {
|
||||
stepInfo.step = 'desc_files_start';
|
||||
iterator(err => {
|
||||
return callback(err, entries);
|
||||
});
|
||||
},
|
||||
function extractDescFiles(entries, callback) {
|
||||
|
||||
// :TODO: would be nice if these RegExp's were cached
|
||||
// :TODO: this is long winded...
|
||||
|
||||
const extractList = [];
|
||||
|
||||
const shortDescFile = entries.find( e => {
|
||||
return Config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) );
|
||||
});
|
||||
|
||||
if(shortDescFile) {
|
||||
extractList.push(shortDescFile.fileName);
|
||||
}
|
||||
|
||||
const longDescFile = entries.find( e => {
|
||||
return Config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) );
|
||||
});
|
||||
|
||||
if(longDescFile) {
|
||||
extractList.push(longDescFile.fileName);
|
||||
}
|
||||
|
||||
if(0 === extractList.length) {
|
||||
return callback(null, [] );
|
||||
}
|
||||
|
||||
temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
archiveUtil.extractTo(filePath, tempDir, archiveType, extractList, err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const descFiles = {
|
||||
desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null,
|
||||
descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null,
|
||||
};
|
||||
|
||||
return callback(null, descFiles);
|
||||
});
|
||||
});
|
||||
},
|
||||
function readDescFiles(descFiles, callback) {
|
||||
async.each(Object.keys(descFiles), (descType, next) => {
|
||||
const path = descFiles[descType];
|
||||
if(!path) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
fs.stat(path, (err, stats) => {
|
||||
if(err) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
// skip entries that are too large
|
||||
const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`;
|
||||
|
||||
if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) {
|
||||
logDebug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` );
|
||||
return next(null);
|
||||
}
|
||||
|
||||
fs.readFile(path, (err, data) => {
|
||||
if(err || !data) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
//
|
||||
// Assume FILE_ID.DIZ, NFO files, etc. are CP437.
|
||||
//
|
||||
// :TODO: This isn't really always the case - how to handle this? We could do a quick detection...
|
||||
fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437');
|
||||
return next(null);
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
// cleanup but don't wait
|
||||
temptmp.cleanup( paths => {
|
||||
// note: don't use client logger here - may not be avail
|
||||
logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' );
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
},
|
||||
function attemptReleaseYearEstimation(callback) {
|
||||
attemptSetEstimatedReleaseDate(fileEntry);
|
||||
return callback(null);
|
||||
},
|
||||
function processDescFilesFinish(callback) {
|
||||
stepInfo.step = 'desc_files_finish';
|
||||
return iterator(callback);
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) {
|
||||
// :TODO: implement me!
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
function addNewFileEntry(fileEntry, filePath, cb) {
|
||||
// :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data
|
||||
|
||||
async.series(
|
||||
[
|
||||
function addNewDbRecord(callback) {
|
||||
return fileEntry.persist(callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateFileEntry(fileEntry, filePath, cb) {
|
||||
|
||||
}
|
||||
|
||||
const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ];
|
||||
|
||||
function scanFile(filePath, options, iterator, cb) {
|
||||
|
||||
if(3 === arguments.length && _.isFunction(iterator)) {
|
||||
cb = iterator;
|
||||
iterator = null;
|
||||
} else if(2 === arguments.length && _.isFunction(options)) {
|
||||
cb = options;
|
||||
iterator = null;
|
||||
options = {};
|
||||
}
|
||||
|
||||
const fileEntry = new FileEntry({
|
||||
areaTag : options.areaTag,
|
||||
meta : options.meta,
|
||||
hashTags : options.hashTags, // Set() or Array
|
||||
fileName : paths.basename(filePath),
|
||||
storageTag : options.storageTag,
|
||||
});
|
||||
|
||||
const stepInfo = {
|
||||
filePath : filePath,
|
||||
fileName : paths.basename(filePath),
|
||||
};
|
||||
|
||||
function callIter(next) {
|
||||
if(iterator) {
|
||||
return iterator(stepInfo, next);
|
||||
} else {
|
||||
return next(null);
|
||||
}
|
||||
}
|
||||
|
||||
function readErrorCallIter(origError, next) {
|
||||
stepInfo.step = 'read_error';
|
||||
stepInfo.error = origError.message;
|
||||
|
||||
callIter( () => {
|
||||
return next(origError);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let lastCalcHashPercent;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function startScan(callback) {
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
return readErrorCallIter(err, callback);
|
||||
}
|
||||
|
||||
stepInfo.step = 'start';
|
||||
stepInfo.byteSize = fileEntry.meta.byte_size = stats.size;
|
||||
|
||||
return callIter(callback);
|
||||
});
|
||||
},
|
||||
function processPhysicalFileGeneric(callback) {
|
||||
stepInfo.bytesProcessed = 0;
|
||||
|
||||
const hashes = {
|
||||
sha1 : crypto.createHash('sha1'),
|
||||
sha256 : crypto.createHash('sha256'),
|
||||
md5 : crypto.createHash('md5'),
|
||||
crc32 : new CRC32(),
|
||||
};
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
function updateHashes(data) {
|
||||
async.each( HASH_NAMES, (hashName, nextHash) => {
|
||||
hashes[hashName].update(data);
|
||||
return nextHash(null);
|
||||
}, () => {
|
||||
return stream.resume();
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('data', data => {
|
||||
stream.pause(); // until iterator compeltes
|
||||
|
||||
stepInfo.bytesProcessed += data.length;
|
||||
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
|
||||
|
||||
//
|
||||
// Only send 'hash_update' step update if we have a noticable percentage change in progress
|
||||
//
|
||||
if(stepInfo.calcHashPercent === lastCalcHashPercent) {
|
||||
updateHashes(data);
|
||||
} else {
|
||||
lastCalcHashPercent = stepInfo.calcHashPercent;
|
||||
stepInfo.step = 'hash_update';
|
||||
|
||||
callIter(err => {
|
||||
if(err) {
|
||||
stream.destroy(); // cancel read
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
updateHashes(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
fileEntry.meta.byte_size = stepInfo.bytesProcessed;
|
||||
|
||||
async.each(HASH_NAMES, (hashName, nextHash) => {
|
||||
if('sha256' === hashName) {
|
||||
stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex');
|
||||
} else if('sha1' === hashName || 'md5' === hashName) {
|
||||
stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex');
|
||||
} else if('crc32' === hashName) {
|
||||
stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16);
|
||||
}
|
||||
|
||||
return nextHash(null);
|
||||
}, () => {
|
||||
stepInfo.step = 'hash_finish';
|
||||
return callIter(callback);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', err => {
|
||||
return readErrorCallIter(err, callback);
|
||||
});
|
||||
},
|
||||
function processPhysicalFileByType(callback) {
|
||||
const archiveUtil = ArchiveUtil.getInstance();
|
||||
|
||||
archiveUtil.detectType(filePath, (err, archiveType) => {
|
||||
if(archiveType) {
|
||||
// save this off
|
||||
fileEntry.meta.archive_type = archiveType;
|
||||
|
||||
populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => {
|
||||
if(err) {
|
||||
populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => {
|
||||
// :TODO: log err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
} else {
|
||||
return callback(null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => {
|
||||
// :TODO: log err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
function fetchExistingEntry(callback) {
|
||||
getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => {
|
||||
return callback(err, dupeEntries);
|
||||
});
|
||||
},
|
||||
function finished(dupeEntries, callback) {
|
||||
stepInfo.step = 'finished';
|
||||
callIter( () => {
|
||||
return callback(null, dupeEntries);
|
||||
});
|
||||
}
|
||||
],
|
||||
(err, dupeEntries) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return cb(null, fileEntry, dupeEntries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
|
||||
if(3 === arguments.length && _.isFunction(iterator)) {
|
||||
cb = iterator;
|
||||
iterator = null;
|
||||
} else if(2 === arguments.length && _.isFunction(options)) {
|
||||
cb = options;
|
||||
iterator = null;
|
||||
options = {};
|
||||
}
|
||||
|
||||
const storageLocations = getAreaStorageLocations(areaInfo);
|
||||
|
||||
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
|
||||
async.series(
|
||||
[
|
||||
function scanPhysFiles(callback) {
|
||||
const physDir = storageLoc.dir;
|
||||
|
||||
fs.readdir(physDir, (err, files) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.eachSeries(files, (fileName, nextFile) => {
|
||||
const fullPath = paths.join(physDir, fileName);
|
||||
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!
|
||||
return nextFile(null); // always try next file
|
||||
}
|
||||
|
||||
if(!stats.isFile()) {
|
||||
return nextFile(null);
|
||||
}
|
||||
|
||||
scanFile(
|
||||
fullPath,
|
||||
{
|
||||
areaTag : areaInfo.areaTag,
|
||||
storageTag : storageLoc.storageTag
|
||||
},
|
||||
iterator,
|
||||
(err, fileEntry, dupeEntries) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!!!
|
||||
return nextFile(null); // try next anyway
|
||||
}
|
||||
|
||||
if(dupeEntries.length > 0) {
|
||||
// :TODO: Handle duplidates -- what to do here???
|
||||
} else {
|
||||
if(Array.isArray(options.tags)) {
|
||||
options.tags.forEach(tag => {
|
||||
fileEntry.hashTags.add(tag);
|
||||
});
|
||||
}
|
||||
addNewFileEntry(fileEntry, fullPath, err => {
|
||||
// pass along error; we failed to insert a record in our DB or something else bad
|
||||
return nextFile(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
function scanDbEntries(callback) {
|
||||
// :TODO: Look @ db entries for area that were *not* processed above
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return nextLocation(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const uuidV4 = require('uuid/v4');
|
||||
|
||||
module.exports = class FileBaseFilters {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
static get OrderByValues() {
|
||||
return [ 'descending', 'ascending' ];
|
||||
}
|
||||
|
||||
static get SortByValues() {
|
||||
return [
|
||||
'upload_timestamp',
|
||||
'upload_by_username',
|
||||
'dl_count',
|
||||
'user_rating',
|
||||
'est_release_year',
|
||||
'byte_size',
|
||||
];
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return _.map(this.filters, (filter, uuid) => {
|
||||
return Object.assign( { uuid : uuid }, filter );
|
||||
});
|
||||
}
|
||||
|
||||
get(filterUuid) {
|
||||
return this.filters[filterUuid];
|
||||
}
|
||||
|
||||
add(filterInfo) {
|
||||
const filterUuid = uuidV4();
|
||||
|
||||
filterInfo.tags = this.cleanTags(filterInfo.tags);
|
||||
|
||||
this.filters[filterUuid] = filterInfo;
|
||||
|
||||
return filterUuid;
|
||||
}
|
||||
|
||||
replace(filterUuid, filterInfo) {
|
||||
const filter = this.get(filterUuid);
|
||||
if(!filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
filterInfo.tags = this.cleanTags(filterInfo.tags);
|
||||
this.filters[filterUuid] = filterInfo;
|
||||
return true;
|
||||
}
|
||||
|
||||
remove(filterUuid) {
|
||||
delete this.filters[filterUuid];
|
||||
}
|
||||
|
||||
load() {
|
||||
let filtersProperty = this.client.user.properties.file_base_filters;
|
||||
let defaulted;
|
||||
if(!filtersProperty) {
|
||||
filtersProperty = JSON.stringify(FileBaseFilters.getDefaultFilters());
|
||||
defaulted = true;
|
||||
}
|
||||
|
||||
try {
|
||||
this.filters = JSON.parse(filtersProperty);
|
||||
} catch(e) {
|
||||
this.filters = FileBaseFilters.getDefaultFilters(); // something bad happened; reset everything back to defaults :(
|
||||
defaulted = true;
|
||||
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
|
||||
}
|
||||
|
||||
if(defaulted) {
|
||||
this.persist( err => {
|
||||
if(!err) {
|
||||
const defaultActiveUuid = this.toArray()[0].uuid;
|
||||
this.setActive(defaultActiveUuid);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
persist(cb) {
|
||||
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb);
|
||||
}
|
||||
|
||||
cleanTags(tags) {
|
||||
return tags.toLowerCase().replace(/,?\s+|\,/g, ' ').trim();
|
||||
}
|
||||
|
||||
setActive(filterUuid) {
|
||||
const activeFilter = this.get(filterUuid);
|
||||
|
||||
if(activeFilter) {
|
||||
this.activeFilter = activeFilter;
|
||||
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static getDefaultFilters() {
|
||||
const filters = {};
|
||||
|
||||
const uuid = uuidV4();
|
||||
filters[uuid] = {
|
||||
name : 'Default',
|
||||
areaTag : '', // all
|
||||
terms : '', // *
|
||||
tags : '', // *
|
||||
order : 'descending',
|
||||
sort : 'upload_timestamp',
|
||||
uuid : uuid,
|
||||
};
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
static getActiveFilter(client) {
|
||||
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,399 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const fileDb = require('./database.js').dbs.file;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
const Config = require('./config.js').config;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const paths = require('path');
|
||||
|
||||
const FILE_TABLE_MEMBERS = [
|
||||
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag',
|
||||
'desc', 'desc_long', 'upload_timestamp'
|
||||
];
|
||||
|
||||
const FILE_WELL_KNOWN_META = {
|
||||
// name -> *read* converter, if any
|
||||
upload_by_username : null,
|
||||
upload_by_user_id : (u) => parseInt(u) || 0,
|
||||
file_md5 : null,
|
||||
file_sha1 : null,
|
||||
file_crc32 : null,
|
||||
est_release_year : (y) => parseInt(y) || new Date().getFullYear(),
|
||||
dl_count : (d) => parseInt(d) || 0,
|
||||
byte_size : (b) => parseInt(b) || 0,
|
||||
archive_type : null,
|
||||
};
|
||||
|
||||
module.exports = class FileEntry {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
this.fileId = options.fileId || 0;
|
||||
this.areaTag = options.areaTag || '';
|
||||
this.meta = options.meta || {
|
||||
// values we always want
|
||||
dl_count : 0,
|
||||
};
|
||||
|
||||
this.hashTags = options.hashTags || new Set();
|
||||
this.fileName = options.fileName;
|
||||
this.storageTag = options.storageTag;
|
||||
}
|
||||
|
||||
static loadBasicEntry(fileId, dest, cb) {
|
||||
if(!cb && _.isFunction(dest)) {
|
||||
cb = dest;
|
||||
dest = this;
|
||||
}
|
||||
|
||||
fileDb.get(
|
||||
`SELECT ${FILE_TABLE_MEMBERS.join(', ')}
|
||||
FROM file
|
||||
WHERE file_id=?
|
||||
LIMIT 1;`,
|
||||
[ fileId ],
|
||||
(err, file) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if(!file) {
|
||||
return cb(Errors.DoesNotExist('No file is available by that ID'));
|
||||
}
|
||||
|
||||
// assign props from |file|
|
||||
FILE_TABLE_MEMBERS.forEach(prop => {
|
||||
dest[_.camelCase(prop)] = file[prop];
|
||||
});
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
load(fileId, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadBasicEntry(callback) {
|
||||
FileEntry.loadBasicEntry(fileId, self, callback);
|
||||
},
|
||||
function loadMeta(callback) {
|
||||
return self.loadMeta(callback);
|
||||
},
|
||||
function loadHashTags(callback) {
|
||||
return self.loadHashTags(callback);
|
||||
},
|
||||
function loadUserRating(callback) {
|
||||
return self.loadRating(callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
persist(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function startTrans(callback) {
|
||||
return fileDb.run('BEGIN;', callback);
|
||||
},
|
||||
function storeEntry(callback) {
|
||||
fileDb.run(
|
||||
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?);`,
|
||||
[ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
|
||||
function inserted(err) { // use non-arrow func for 'this' scope / lastID
|
||||
if(!err) {
|
||||
self.fileId = this.lastID;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function storeMeta(callback) {
|
||||
async.each(Object.keys(self.meta), (n, next) => {
|
||||
const v = self.meta[n];
|
||||
return FileEntry.persistMetaValue(self.fileId, n, v, next);
|
||||
},
|
||||
err => {
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function storeHashTags(callback) {
|
||||
const hashTagsArray = Array.from(self.hashTags);
|
||||
async.each(hashTagsArray, (hashTag, next) => {
|
||||
return FileEntry.persistHashTag(self.fileId, hashTag, next);
|
||||
},
|
||||
err => {
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
// :TODO: Log orig err
|
||||
fileDb.run(err ? 'ROLLBACK;' : 'COMMIT;', err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static getAreaStorageDirectoryByTag(storageTag) {
|
||||
const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]);
|
||||
|
||||
// absolute paths as-is
|
||||
if(storageLocation && '/' === storageLocation.charAt(0)) {
|
||||
return storageLocation;
|
||||
}
|
||||
|
||||
// relative to |areaStoragePrefix|
|
||||
return paths.join(Config.fileBase.areaStoragePrefix, storageLocation || '');
|
||||
}
|
||||
|
||||
get filePath() {
|
||||
const storageDir = FileEntry.getAreaStorageDirectoryByTag(this.storageTag);
|
||||
return paths.join(storageDir, this.fileName);
|
||||
}
|
||||
|
||||
static persistUserRating(fileId, userId, rating, cb) {
|
||||
return fileDb.run(
|
||||
`REPLACE INTO file_user_rating (file_id, user_id, rating)
|
||||
VALUES (?, ?, ?);`,
|
||||
[ fileId, userId, rating ],
|
||||
cb
|
||||
);
|
||||
}
|
||||
|
||||
static persistMetaValue(fileId, name, value, cb) {
|
||||
return fileDb.run(
|
||||
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
|
||||
VALUES (?, ?, ?);`,
|
||||
[ fileId, name, value ],
|
||||
cb
|
||||
);
|
||||
}
|
||||
|
||||
static incrementAndPersistMetaValue(fileId, name, incrementBy, cb) {
|
||||
incrementBy = incrementBy || 1;
|
||||
fileDb.run(
|
||||
`UPDATE file_meta
|
||||
SET meta_value = meta_value + ?
|
||||
WHERE file_id = ? AND meta_name = ?;`,
|
||||
[ incrementBy, fileId, name ],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadMeta(cb) {
|
||||
fileDb.each(
|
||||
`SELECT meta_name, meta_value
|
||||
FROM file_meta
|
||||
WHERE file_id=?;`,
|
||||
[ this.fileId ],
|
||||
(err, meta) => {
|
||||
if(meta) {
|
||||
const conv = FILE_WELL_KNOWN_META[meta.meta_name];
|
||||
this.meta[meta.meta_name] = conv ? conv(meta.meta_value) : meta.meta_value;
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static persistHashTag(fileId, hashTag, cb) {
|
||||
fileDb.serialize( () => {
|
||||
fileDb.run(
|
||||
`INSERT OR IGNORE INTO hash_tag (hash_tag)
|
||||
VALUES (?);`,
|
||||
[ hashTag ]
|
||||
);
|
||||
|
||||
fileDb.run(
|
||||
`REPLACE INTO file_hash_tag (hash_tag_id, file_id)
|
||||
VALUES (
|
||||
(SELECT hash_tag_id
|
||||
FROM hash_tag
|
||||
WHERE hash_tag = ?),
|
||||
?
|
||||
);`,
|
||||
[ hashTag, fileId ],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadHashTags(cb) {
|
||||
fileDb.each(
|
||||
`SELECT ht.hash_tag_id, ht.hash_tag
|
||||
FROM hash_tag ht
|
||||
WHERE ht.hash_tag_id IN (
|
||||
SELECT hash_tag_id
|
||||
FROM file_hash_tag
|
||||
WHERE file_id=?
|
||||
);`,
|
||||
[ this.fileId ],
|
||||
(err, hashTag) => {
|
||||
if(hashTag) {
|
||||
this.hashTags.add(hashTag.hash_tag);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadRating(cb) {
|
||||
fileDb.get(
|
||||
`SELECT AVG(fur.rating) AS avg_rating
|
||||
FROM file_user_rating fur
|
||||
INNER JOIN file f
|
||||
ON f.file_id = fur.file_id
|
||||
AND f.file_id = ?`,
|
||||
[ this.fileId ],
|
||||
(err, result) => {
|
||||
if(result) {
|
||||
this.userRating = result.avg_rating;
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setHashTags(hashTags) {
|
||||
if(_.isString(hashTags)) {
|
||||
this.hashTags = new Set(hashTags.split(/[\s,]+/));
|
||||
} else if(Array.isArray(hashTags)) {
|
||||
this.hashTags = new Set(hashTags);
|
||||
} else if(hashTags instanceof Set) {
|
||||
this.hashTags = hashTags;
|
||||
}
|
||||
}
|
||||
|
||||
static getWellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); }
|
||||
|
||||
static findFiles(filter, cb) {
|
||||
filter = filter || {};
|
||||
|
||||
let sql;
|
||||
let sqlWhere = '';
|
||||
let sqlOrderBy;
|
||||
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
|
||||
|
||||
function getOrderByWithCast(ob) {
|
||||
if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) {
|
||||
return `ORDER BY CAST(${ob} AS INTEGER)`;
|
||||
}
|
||||
|
||||
return `ORDER BY ${ob}`;
|
||||
}
|
||||
|
||||
function appendWhereClause(clause) {
|
||||
if(sqlWhere) {
|
||||
sqlWhere += ' AND ';
|
||||
} else {
|
||||
sqlWhere += ' WHERE ';
|
||||
}
|
||||
sqlWhere += clause;
|
||||
}
|
||||
|
||||
if(filter.sort && filter.sort.length > 0) {
|
||||
if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value?
|
||||
sql =
|
||||
`SELECT f.file_id
|
||||
FROM file f, file_meta m`;
|
||||
|
||||
appendWhereClause(`f.file_id = m.file_id AND m.meta_name="${filter.sort}"`);
|
||||
|
||||
sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`;
|
||||
} else {
|
||||
// additional special treatment for user ratings: we need to average them
|
||||
if('user_rating' === filter.sort) {
|
||||
sql =
|
||||
`SELECT f.file_id,
|
||||
(SELECT IFNULL(AVG(rating), 0) rating
|
||||
FROM file_user_rating
|
||||
WHERE file_id = f.file_id)
|
||||
AS avg_rating
|
||||
FROM file f`;
|
||||
|
||||
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
|
||||
} else {
|
||||
sql =
|
||||
`SELECT f.file_id, f.${filter.sort}
|
||||
FROM file f`;
|
||||
|
||||
sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sql =
|
||||
`SELECT f.file_id
|
||||
FROM file`;
|
||||
|
||||
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
|
||||
}
|
||||
|
||||
|
||||
if(filter.areaTag && filter.areaTag.length > 0) {
|
||||
appendWhereClause(`f.area_tag="${filter.areaTag}"`);
|
||||
}
|
||||
|
||||
if(filter.terms && filter.terms.length > 0) {
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT rowid
|
||||
FROM file_fts
|
||||
WHERE file_fts MATCH "${filter.terms.replace(/"/g,'""')}"
|
||||
)`
|
||||
);
|
||||
}
|
||||
|
||||
if(filter.tags && filter.tags.length > 0) {
|
||||
// build list of quoted tags; filter.tags comes in as a space separated values
|
||||
const tags = filter.tags.split(' ').map( tag => `"${tag}"` ).join(',');
|
||||
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT file_id
|
||||
FROM file_hash_tag
|
||||
WHERE hash_tag_id IN (
|
||||
SELECT hash_tag_id
|
||||
FROM hash_tag
|
||||
WHERE hash_tag IN (${tags})
|
||||
)
|
||||
)`
|
||||
);
|
||||
}
|
||||
|
||||
sql += `${sqlWhere} ${sqlOrderBy};`;
|
||||
|
||||
const matchingFileIds = [];
|
||||
fileDb.each(sql, (err, fileId) => {
|
||||
if(fileId) {
|
||||
matchingFileIds.push(fileId.file_id);
|
||||
}
|
||||
}, err => {
|
||||
return cb(err, matchingFileIds);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,582 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const Config = require('./config.js').config;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const StatLog = require('./stat_log.js');
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const pty = require('ptyw.js');
|
||||
const temptmp = require('temptmp').createTrackedSession('transfer_file');
|
||||
const paths = require('path');
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
|
||||
// some consts
|
||||
const SYSTEM_EOL = require('os').EOL;
|
||||
const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
|
||||
|
||||
/*
|
||||
Notes
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
See core/config.js for external protocol configuration
|
||||
|
||||
|
||||
Resources
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
ZModem
|
||||
* http://gallium.inria.fr/~doligez/zmodem/zmodem.txt
|
||||
* https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c
|
||||
|
||||
*/
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Transfer file',
|
||||
desc : 'Sends or receives a file(s)',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = class TransferFileModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = this.menuConfig.config || {};
|
||||
|
||||
//
|
||||
// Most options can be set via extraArgs or config block
|
||||
//
|
||||
if(options.extraArgs) {
|
||||
if(options.extraArgs.protocol) {
|
||||
this.protocolConfig = Config.fileTransferProtocols[options.extraArgs.protocol];
|
||||
}
|
||||
|
||||
if(options.extraArgs.direction) {
|
||||
this.direction = options.extraArgs.direction;
|
||||
}
|
||||
|
||||
if(options.extraArgs.sendQueue) {
|
||||
this.sendQueue = options.extraArgs.sendQueue;
|
||||
}
|
||||
|
||||
if(options.extraArgs.recvFileName) {
|
||||
this.recvFileName = options.extraArgs.recvFileName;
|
||||
}
|
||||
|
||||
if(options.extraArgs.recvDirectory) {
|
||||
this.recvDirectory = options.extraArgs.recvDirectory;
|
||||
}
|
||||
} else {
|
||||
if(this.config.protocol) {
|
||||
this.protocolConfig = Config.fileTransferProtocols[this.config.protocol];
|
||||
}
|
||||
|
||||
if(this.config.direction) {
|
||||
this.direction = this.config.direction;
|
||||
}
|
||||
|
||||
if(this.config.sendQueue) {
|
||||
this.sendQueue = this.config.sendQueue;
|
||||
}
|
||||
|
||||
if(this.config.recvFileName) {
|
||||
this.recvFileName = this.config.recvFileName;
|
||||
}
|
||||
|
||||
if(this.config.recvDirectory) {
|
||||
this.recvDirectory = this.config.recvDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
this.protocolConfig = this.protocolConfig || Config.fileTransferProtocols.zmodem8kSz; // try for *something*
|
||||
this.direction = this.direction || 'send';
|
||||
this.sendQueue = this.sendQueue || [];
|
||||
|
||||
// Ensure sendQueue is an array of objects that contain at least a 'path' member
|
||||
this.sendQueue = this.sendQueue.map(item => {
|
||||
if(_.isString(item)) {
|
||||
return { path : item };
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
|
||||
this.sentFileIds = [];
|
||||
}
|
||||
|
||||
isSending() {
|
||||
return ('send' === this.direction);
|
||||
}
|
||||
|
||||
restorePipeAfterExternalProc() {
|
||||
if(!this.pipeRestored) {
|
||||
this.pipeRestored = true;
|
||||
|
||||
this.client.restoreDataHandler();
|
||||
}
|
||||
}
|
||||
|
||||
sendFiles(cb) {
|
||||
// assume *sending* can always batch
|
||||
// :TODO: Look into this further
|
||||
const allFiles = this.sendQueue.map(f => f.path);
|
||||
this.executeExternalProtocolHandlerForSend(allFiles, err => {
|
||||
if(err) {
|
||||
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
|
||||
} else {
|
||||
const sentFiles = [];
|
||||
this.sendQueue.forEach(f => {
|
||||
f.sent = true;
|
||||
sentFiles.push(f.path);
|
||||
|
||||
});
|
||||
|
||||
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
|
||||
}
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
sendFiles(cb) {
|
||||
// :TODO: built in/native protocol support
|
||||
|
||||
if(this.protocolConfig.external.supportsBatch) {
|
||||
const allFiles = this.sendQueue.map(f => f.path);
|
||||
this.executeExternalProtocolHandlerForSend(allFiles, err => {
|
||||
if(err) {
|
||||
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
|
||||
} else {
|
||||
const sentFiles = [];
|
||||
this.sendQueue.forEach(f => {
|
||||
f.sent = true;
|
||||
sentFiles.push(f.path);
|
||||
|
||||
});
|
||||
|
||||
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
|
||||
}
|
||||
return cb(err);
|
||||
});
|
||||
} else {
|
||||
// :TODO: we need to prompt between entries such that users can prepare their clients
|
||||
async.eachSeries(this.sendQueue, (queueItem, next) => {
|
||||
this.executeExternalProtocolHandlerForSend(queueItem.path, err => {
|
||||
if(err) {
|
||||
this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' );
|
||||
} else {
|
||||
queueItem.sent = true;
|
||||
|
||||
this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' );
|
||||
}
|
||||
return next(err);
|
||||
});
|
||||
}, err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
moveFileWithCollisionHandling(src, dst, cb) {
|
||||
//
|
||||
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
|
||||
// in the case of collisions.
|
||||
//
|
||||
const dstPath = paths.dirname(dst);
|
||||
const dstFileExt = paths.extname(dst);
|
||||
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
||||
|
||||
let renameIndex = 0;
|
||||
let movedOk = false;
|
||||
let tryDstPath;
|
||||
|
||||
async.until(
|
||||
() => movedOk, // until moved OK
|
||||
(cb) => {
|
||||
if(0 === renameIndex) {
|
||||
// try originally supplied path first
|
||||
tryDstPath = dst;
|
||||
} else {
|
||||
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
|
||||
}
|
||||
|
||||
fse.move(src, tryDstPath, err => {
|
||||
if(err) {
|
||||
if('EEXIST' === err.code) {
|
||||
renameIndex += 1;
|
||||
return cb(null); // keep trying
|
||||
}
|
||||
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
movedOk = true;
|
||||
return cb(null, tryDstPath);
|
||||
});
|
||||
},
|
||||
(err, finalPath) => {
|
||||
return cb(err, finalPath);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
recvFiles(cb) {
|
||||
this.executeExternalProtocolHandlerForRecv(err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.recvFilePaths = [];
|
||||
|
||||
if(this.recvFileName) {
|
||||
//
|
||||
// file name specified - we expect a single file in |this.recvDirectory|
|
||||
// by the name of |this.recvFileName|
|
||||
//
|
||||
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
|
||||
fs.stat(recvFullPath, (err, stats) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if(!stats.isFile()) {
|
||||
return cb(Errors.Invalid('Expected file entry in recv directory'));
|
||||
}
|
||||
|
||||
this.recvFilePaths.push(recvFullPath);
|
||||
return cb(null);
|
||||
});
|
||||
} else {
|
||||
//
|
||||
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
|
||||
//
|
||||
fs.readdir(this.recvDirectory, (err, files) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// stat each to grab files only
|
||||
async.each(files, (fileName, nextFile) => {
|
||||
const recvFullPath = paths.join(this.recvDirectory, fileName);
|
||||
|
||||
fs.stat(recvFullPath, (err, stats) => {
|
||||
if(err) {
|
||||
this.client.log.warn('Failed to stat file', { path : recvFullPath } );
|
||||
return nextFile(null); // just try the next one
|
||||
}
|
||||
|
||||
if(stats.isFile()) {
|
||||
this.recvFilePaths.push(recvFullPath);
|
||||
}
|
||||
|
||||
return nextFile(null);
|
||||
});
|
||||
}, () => {
|
||||
return cb(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pathWithTerminatingSeparator(path) {
|
||||
if(path && paths.sep !== path.charAt(path.length - 1)) {
|
||||
path = path + paths.sep;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
prepAndBuildSendArgs(filePaths, cb) {
|
||||
const externalArgs = this.protocolConfig.external['sendArgs'];
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getTempFileListPath(callback) {
|
||||
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) );
|
||||
if(!hasFileList) {
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => {
|
||||
if(err) {
|
||||
return callback(err); // failed to create it
|
||||
}
|
||||
|
||||
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL));
|
||||
fs.close(tempFileInfo.fd, err => {
|
||||
return callback(err, tempFileInfo.path);
|
||||
});
|
||||
});
|
||||
},
|
||||
function createArgs(tempFileListPath, callback) {
|
||||
// initial args: ignore {filePaths} as we must break that into it's own sep array items
|
||||
const args = externalArgs.map(arg => {
|
||||
return '{filePaths}' === arg ? arg : stringFormat(arg, {
|
||||
fileListPath : tempFileListPath || '',
|
||||
});
|
||||
});
|
||||
|
||||
const filePathsPos = args.indexOf('{filePaths}');
|
||||
if(filePathsPos > -1) {
|
||||
// replace {filePaths} with 0:n individual entries in |args|
|
||||
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
|
||||
}
|
||||
|
||||
return callback(null, args);
|
||||
}
|
||||
],
|
||||
(err, args) => {
|
||||
return cb(err, args);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
prepAndBuildRecvArgs(cb) {
|
||||
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
|
||||
const externalArgs = this.protocolConfig.external[argsKey];
|
||||
const args = externalArgs.map(arg => stringFormat(arg, {
|
||||
uploadDir : this.recvDirectory,
|
||||
fileName : this.recvFileName || '',
|
||||
}));
|
||||
|
||||
return cb(null, args);
|
||||
}
|
||||
|
||||
executeExternalProtocolHandler(args, cb) {
|
||||
const external = this.protocolConfig.external;
|
||||
const cmd = external[`${this.direction}Cmd`];
|
||||
|
||||
this.client.log.debug(
|
||||
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
|
||||
'Executing external protocol'
|
||||
);
|
||||
|
||||
const externalProc = pty.spawn(cmd, args, {
|
||||
cols : this.client.term.termWidth,
|
||||
rows : this.client.term.termHeight,
|
||||
cwd : this.recvDirectory,
|
||||
});
|
||||
|
||||
this.client.setTemporaryDirectDataHandler(data => {
|
||||
// needed for things like sz/rz
|
||||
if(external.escapeTelnet) {
|
||||
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
|
||||
externalProc.write(new Buffer(tmp, 'binary'));
|
||||
} else {
|
||||
externalProc.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
externalProc.on('data', data => {
|
||||
// needed for things like sz/rz
|
||||
if(external.escapeTelnet) {
|
||||
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
|
||||
this.client.term.rawWrite(new Buffer(tmp, 'binary'));
|
||||
} else {
|
||||
this.client.term.rawWrite(data);
|
||||
}
|
||||
});
|
||||
|
||||
externalProc.once('close', () => {
|
||||
return this.restorePipeAfterExternalProc();
|
||||
});
|
||||
|
||||
externalProc.once('exit', (exitCode) => {
|
||||
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
|
||||
|
||||
this.restorePipeAfterExternalProc();
|
||||
externalProc.removeAllListeners();
|
||||
|
||||
return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null);
|
||||
});
|
||||
}
|
||||
|
||||
executeExternalProtocolHandlerForSend(filePaths, cb) {
|
||||
if(!Array.isArray(filePaths)) {
|
||||
filePaths = [ filePaths ];
|
||||
}
|
||||
|
||||
this.prepAndBuildSendArgs(filePaths, (err, args) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.executeExternalProtocolHandler(args, err => {
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
executeExternalProtocolHandlerForRecv(cb) {
|
||||
this.prepAndBuildRecvArgs( (err, args) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.executeExternalProtocolHandler(args, err => {
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getMenuResult() {
|
||||
if(this.isSending()) {
|
||||
return { sentFileIds : this.sentFileIds };
|
||||
} else {
|
||||
return { recvFilePaths : this.recvFilePaths };
|
||||
}
|
||||
}
|
||||
|
||||
updateSendStats(cb) {
|
||||
let downloadBytes = 0;
|
||||
let downloadCount = 0;
|
||||
let fileIds = [];
|
||||
|
||||
async.each(this.sendQueue, (queueItem, next) => {
|
||||
if(!queueItem.sent) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
if(queueItem.fileId) {
|
||||
fileIds.push(queueItem.fileId);
|
||||
}
|
||||
|
||||
if(_.isNumber(queueItem.byteSize)) {
|
||||
downloadCount += 1;
|
||||
downloadBytes += queueItem.byteSize;
|
||||
return next(null);
|
||||
}
|
||||
|
||||
// we just have a path - figure it out
|
||||
fs.stat(queueItem.path, (err, stats) => {
|
||||
if(err) {
|
||||
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
|
||||
} else {
|
||||
downloadCount += 1;
|
||||
downloadBytes += stats.size;
|
||||
}
|
||||
|
||||
return next(null);
|
||||
});
|
||||
}, () => {
|
||||
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
|
||||
StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount);
|
||||
StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes);
|
||||
StatLog.incrementSystemStat('dl_total_count', downloadCount);
|
||||
StatLog.incrementSystemStat('dl_total_bytes', downloadBytes);
|
||||
|
||||
fileIds.forEach(fileId => {
|
||||
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
|
||||
});
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
updateRecvStats(cb) {
|
||||
let uploadBytes = 0;
|
||||
let uploadCount = 0;
|
||||
|
||||
async.each(this.recvFilePaths, (filePath, next) => {
|
||||
// we just have a path - figure it out
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
|
||||
} else {
|
||||
uploadCount += 1;
|
||||
uploadBytes += stats.size;
|
||||
}
|
||||
|
||||
return next(null);
|
||||
});
|
||||
}, () => {
|
||||
StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount);
|
||||
StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes);
|
||||
StatLog.incrementSystemStat('ul_total_count', uploadCount);
|
||||
StatLog.incrementSystemStat('ul_total_bytes', uploadBytes);
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
// :TODO: break this up to send|recv
|
||||
|
||||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(self.isSending()) {
|
||||
if(!Array.isArray(self.sendQueue)) {
|
||||
self.sendQueue = [ self.sendQueue ];
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function transferFiles(callback) {
|
||||
if(self.isSending()) {
|
||||
self.sendFiles( err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const sentFileIds = [];
|
||||
self.sendQueue.forEach(queueItem => {
|
||||
if(queueItem.sent && queueItem.fileId) {
|
||||
sentFileIds.push(queueItem.fileId);
|
||||
}
|
||||
});
|
||||
|
||||
if(sentFileIds.length > 0) {
|
||||
// remove items we sent from the D/L queue
|
||||
const dlQueue = new DownloadQueue(self.client);
|
||||
dlQueue.removeItems(sentFileIds);
|
||||
|
||||
self.sentFileIds = sentFileIds;
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
} else {
|
||||
self.recvFiles( err => {
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
function cleanupTempFiles(callback) {
|
||||
temptmp.cleanup( paths => {
|
||||
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' );
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function updateUserAndSystemStats(callback) {
|
||||
if(self.isSending()) {
|
||||
return self.updateSendStats(callback);
|
||||
} else {
|
||||
return self.updateRecvStats(callback);
|
||||
}
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.message }, 'File transfer error');
|
||||
}
|
||||
|
||||
return self.prevMenu();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
|
||||
// deps
|
||||
const fse = require('fs-extra');
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
|
||||
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
|
||||
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
|
||||
|
||||
//
|
||||
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
|
||||
// in the case of collisions.
|
||||
//
|
||||
function moveFileWithCollisionHandling(src, dst, cb) {
|
||||
const dstPath = paths.dirname(dst);
|
||||
const dstFileExt = paths.extname(dst);
|
||||
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
||||
|
||||
let renameIndex = 0;
|
||||
let movedOk = false;
|
||||
let tryDstPath;
|
||||
|
||||
async.until(
|
||||
() => movedOk, // until moved OK
|
||||
(cb) => {
|
||||
if(0 === renameIndex) {
|
||||
// try originally supplied path first
|
||||
tryDstPath = dst;
|
||||
} else {
|
||||
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
|
||||
}
|
||||
|
||||
fse.move(src, tryDstPath, err => {
|
||||
if(err) {
|
||||
if('EEXIST' === err.code) {
|
||||
renameIndex += 1;
|
||||
return cb(null); // keep trying
|
||||
}
|
||||
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
movedOk = true;
|
||||
return cb(null, tryDstPath);
|
||||
});
|
||||
},
|
||||
(err, finalPath) => {
|
||||
return cb(err, finalPath);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function pathWithTerminatingSeparator(path) {
|
||||
if(path && paths.sep !== path.charAt(path.length - 1)) {
|
||||
path = path + paths.sep;
|
||||
}
|
||||
return path;
|
||||
}
|
657
core/fse.js
657
core/fse.js
|
@ -13,6 +13,7 @@ const getUserIdAndName = require('./user.js').getUserIdAndName;
|
|||
const cleanControlCodes = require('./string_util.js').cleanControlCodes;
|
||||
const StatLog = require('./stat_log.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -20,12 +21,6 @@ const assert = require('assert');
|
|||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.FullScreenEditorModule = FullScreenEditorModule;
|
||||
|
||||
// :TODO: clean this up:
|
||||
|
||||
exports.getModule = FullScreenEditorModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Full Screen Editor (FSE)',
|
||||
desc : 'A full screen editor/viewer',
|
||||
|
@ -66,7 +61,7 @@ exports.moduleInfo = {
|
|||
|
||||
|
||||
*/
|
||||
var MCICodeIds = {
|
||||
const MciCodeIds = {
|
||||
ViewModeHeader : {
|
||||
From : 1,
|
||||
To : 2,
|
||||
|
@ -98,11 +93,15 @@ var MCICodeIds = {
|
|||
},
|
||||
};
|
||||
|
||||
function FullScreenEditorModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
// :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives
|
||||
|
||||
var self = this;
|
||||
var config = this.menuConfig.config;
|
||||
exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
//
|
||||
// menuConfig.config:
|
||||
|
@ -143,27 +142,143 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
this.isReady = false;
|
||||
|
||||
this.isEditMode = function() {
|
||||
return 'edit' === self.editorMode;
|
||||
};
|
||||
if(_.has(options, 'extraArgs.message')) {
|
||||
this.setMessage(options.extraArgs.message);
|
||||
} else if(_.has(options, 'extraArgs.replyToMessage')) {
|
||||
this.replyToMessage = options.extraArgs.replyToMessage;
|
||||
}
|
||||
|
||||
this.isViewMode = function() {
|
||||
return 'view' === self.editorMode;
|
||||
};
|
||||
this.menuMethods = {
|
||||
//
|
||||
// Validation stuff
|
||||
//
|
||||
viewValidationListener : function(err, cb) {
|
||||
var errMsgView = self.viewControllers.header.getView(MciCodeIds.ReplyEditModeHeader.ErrorMsg);
|
||||
var newFocusViewId;
|
||||
if(errMsgView) {
|
||||
if(err) {
|
||||
errMsgView.setText(err.message);
|
||||
|
||||
this.isLocalEmail = function() {
|
||||
return Message.WellKnownAreaTags.Private === self.messageAreaTag;
|
||||
};
|
||||
if(MciCodeIds.ViewModeHeader.Subject === err.view.getId()) {
|
||||
// :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel)
|
||||
}
|
||||
} else {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
}
|
||||
cb(newFocusViewId);
|
||||
},
|
||||
|
||||
this.isReply = function() {
|
||||
return !_.isUndefined(self.replyToMessage);
|
||||
};
|
||||
headerSubmit : function(formData, extraArgs, cb) {
|
||||
self.switchToBody();
|
||||
return cb(null);
|
||||
},
|
||||
editModeEscPressed : function(formData, extraArgs, cb) {
|
||||
self.footerMode = 'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
||||
|
||||
this.getFooterName = function() {
|
||||
return 'footer' + _.capitalize(self.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ...
|
||||
};
|
||||
self.switchFooter(function next(err) {
|
||||
if(err) {
|
||||
// :TODO:... what now?
|
||||
console.log(err)
|
||||
} else {
|
||||
switch(self.footerMode) {
|
||||
case 'editor' :
|
||||
if(!_.isUndefined(self.viewControllers.footerEditorMenu)) {
|
||||
//self.viewControllers.footerEditorMenu.setFocus(false);
|
||||
self.viewControllers.footerEditorMenu.detachClientEvents();
|
||||
}
|
||||
self.viewControllers.body.switchFocus(1);
|
||||
self.observeEditorEvents();
|
||||
break;
|
||||
|
||||
this.getFormId = function(name) {
|
||||
case 'editorMenu' :
|
||||
self.viewControllers.body.setFocus(false);
|
||||
self.viewControllers.footerEditorMenu.switchFocus(1);
|
||||
break;
|
||||
|
||||
default : throw new Error('Unexpected mode');
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
},
|
||||
editModeMenuQuote : function(formData, extraArgs, cb) {
|
||||
self.viewControllers.footerEditorMenu.setFocus(false);
|
||||
self.displayQuoteBuilder();
|
||||
return cb(null);
|
||||
},
|
||||
appendQuoteEntry: function(formData, extraArgs, cb) {
|
||||
// :TODO: Dont' use magic # ID's here
|
||||
var quoteMsgView = self.viewControllers.quoteBuilder.getView(1);
|
||||
|
||||
if(self.newQuoteBlock) {
|
||||
self.newQuoteBlock = false;
|
||||
quoteMsgView.addText(self.getQuoteByHeader());
|
||||
}
|
||||
|
||||
var quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote);
|
||||
quoteMsgView.addText(quoteText);
|
||||
|
||||
//
|
||||
// If this is *not* the last item, advance. Otherwise, do nothing as we
|
||||
// don't want to jump back to the top and repeat already quoted lines
|
||||
//
|
||||
var quoteListView = self.viewControllers.quoteBuilder.getView(3);
|
||||
if(quoteListView.getData() !== quoteListView.getCount() - 1) {
|
||||
quoteListView.focusNext();
|
||||
} else {
|
||||
self.quoteBuilderFinalize();
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
quoteBuilderEscPressed : function(formData, extraArgs, cb) {
|
||||
self.quoteBuilderFinalize();
|
||||
return cb(null);
|
||||
},
|
||||
/*
|
||||
replyDiscard : function(formData, extraArgs) {
|
||||
// :TODO: need to prompt yes/no
|
||||
// :TODO: @method for fallback would be better
|
||||
self.prevMenu();
|
||||
},
|
||||
*/
|
||||
editModeMenuHelp : function(formData, extraArgs, cb) {
|
||||
self.viewControllers.footerEditorMenu.setFocus(false);
|
||||
return self.displayHelp(cb);
|
||||
},
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// View Mode
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
viewModeMenuHelp : function(formData, extraArgs, cb) {
|
||||
self.viewControllers.footerView.setFocus(false);
|
||||
return self.displayHelp(cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
isEditMode() {
|
||||
return 'edit' === this.editorMode;
|
||||
}
|
||||
|
||||
isViewMode() {
|
||||
return 'view' === this.editorMode;
|
||||
}
|
||||
|
||||
isLocalEmail() {
|
||||
return Message.WellKnownAreaTags.Private === this.messageAreaTag;
|
||||
}
|
||||
|
||||
isReply() {
|
||||
return !_.isUndefined(this.replyToMessage);
|
||||
}
|
||||
|
||||
getFooterName() {
|
||||
return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ...
|
||||
}
|
||||
|
||||
getFormId(name) {
|
||||
return {
|
||||
header : 0,
|
||||
body : 1,
|
||||
|
@ -174,27 +289,13 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
help : 50,
|
||||
}[name];
|
||||
};
|
||||
|
||||
/*ViewModeHeader : {
|
||||
From : 1,
|
||||
To : 2,
|
||||
Subject : 3,
|
||||
|
||||
DateTime : 5,
|
||||
MsgNum : 6,
|
||||
MsgTotal : 7,
|
||||
ViewCount : 8,
|
||||
HashTags : 9,
|
||||
MessageID : 10,
|
||||
ReplyToMsgID : 11
|
||||
},*/
|
||||
}
|
||||
|
||||
// :TODO: convert to something like this for all view acces:
|
||||
this.getHeaderViews = function() {
|
||||
var vc = self.viewControllers.header;
|
||||
getHeaderViews() {
|
||||
var vc = this.viewControllers.header;
|
||||
|
||||
if(self.isViewMode()) {
|
||||
if(this.isViewMode()) {
|
||||
return {
|
||||
from : vc.getView(1),
|
||||
to : vc.getView(2),
|
||||
|
@ -206,61 +307,55 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.setInitialFooterMode = function() {
|
||||
switch(self.editorMode) {
|
||||
case 'edit' : self.footerMode = 'editor'; break;
|
||||
case 'view' : self.footerMode = 'view'; break;
|
||||
}
|
||||
};
|
||||
|
||||
this.buildMessage = function() {
|
||||
var headerValues = self.viewControllers.header.getFormData().value;
|
||||
setInitialFooterMode() {
|
||||
switch(this.editorMode) {
|
||||
case 'edit' : this.footerMode = 'editor'; break;
|
||||
case 'view' : this.footerMode = 'view'; break;
|
||||
}
|
||||
}
|
||||
|
||||
buildMessage() {
|
||||
const headerValues = this.viewControllers.header.getFormData().value;
|
||||
|
||||
var msgOpts = {
|
||||
areaTag : self.messageAreaTag,
|
||||
areaTag : this.messageAreaTag,
|
||||
toUserName : headerValues.to,
|
||||
fromUserName : headerValues.from,
|
||||
fromUserName : this.client.user.username,
|
||||
subject : headerValues.subject,
|
||||
message : self.viewControllers.body.getFormData().value.message,
|
||||
message : this.viewControllers.body.getFormData().value.message,
|
||||
};
|
||||
|
||||
if(self.isReply()) {
|
||||
msgOpts.replyToMsgId = self.replyToMessage.messageId;
|
||||
if(this.isReply()) {
|
||||
msgOpts.replyToMsgId = this.replyToMessage.messageId;
|
||||
}
|
||||
|
||||
self.message = new Message(msgOpts);
|
||||
};
|
||||
this.message = new Message(msgOpts);
|
||||
}
|
||||
|
||||
/*
|
||||
this.setBodyMessageViewText = function() {
|
||||
self.bodyMessageView.setText(cleanControlCodes(self.message.message));
|
||||
};
|
||||
*/
|
||||
|
||||
this.setMessage = function(message) {
|
||||
self.message = message;
|
||||
setMessage(message) {
|
||||
this.message = message;
|
||||
|
||||
updateMessageAreaLastReadId(
|
||||
self.client.user.userId, self.messageAreaTag, self.message.messageId, () => {
|
||||
this.client.user.userId, this.messageAreaTag, this.message.messageId, () => {
|
||||
|
||||
if(self.isReady) {
|
||||
self.initHeaderViewMode();
|
||||
self.initFooterViewMode();
|
||||
if(this.isReady) {
|
||||
this.initHeaderViewMode();
|
||||
this.initFooterViewMode();
|
||||
|
||||
var bodyMessageView = self.viewControllers.body.getView(1);
|
||||
if(bodyMessageView && _.has(self, 'message.message')) {
|
||||
//self.setBodyMessageViewText();
|
||||
bodyMessageView.setText(cleanControlCodes(self.message.message));
|
||||
//bodyMessageView.redraw();
|
||||
var bodyMessageView = this.viewControllers.body.getView(1);
|
||||
if(bodyMessageView && _.has(this, 'message.message')) {
|
||||
bodyMessageView.setText(cleanControlCodes(this.message.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
getMessage(cb) {
|
||||
const self = this;
|
||||
|
||||
this.getMessage = function(cb) {
|
||||
async.series(
|
||||
[
|
||||
function buildIfNecessary(callback) {
|
||||
|
@ -296,24 +391,22 @@ function FullScreenEditorModule(options) {
|
|||
cb(err, self.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.updateUserStats = function(cb) {
|
||||
updateUserStats(cb) {
|
||||
if(Message.isPrivateAreaTag(this.message.areaTag)) {
|
||||
if(cb) {
|
||||
return cb(null);
|
||||
cb(null);
|
||||
}
|
||||
return; // don't inc stats for private messages
|
||||
}
|
||||
|
||||
StatLog.incrementUserStat(
|
||||
self.client.user,
|
||||
'post_count',
|
||||
1,
|
||||
cb
|
||||
);
|
||||
};
|
||||
return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb);
|
||||
}
|
||||
|
||||
redrawFooter(options, cb) {
|
||||
const self = this;
|
||||
|
||||
this.redrawFooter = function(options, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function moveToFooterPosition(callback) {
|
||||
|
@ -339,7 +432,7 @@ function FullScreenEditorModule(options) {
|
|||
callback(null);
|
||||
},
|
||||
function displayFooterArt(callback) {
|
||||
var footerArt = self.menuConfig.config.art[options.footerName];
|
||||
const footerArt = self.menuConfig.config.art[options.footerName];
|
||||
|
||||
theme.displayThemedAsset(
|
||||
footerArt,
|
||||
|
@ -355,10 +448,11 @@ function FullScreenEditorModule(options) {
|
|||
cb(err, artData);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.redrawScreen = function(cb) {
|
||||
redrawScreen(cb) {
|
||||
var comps = [ 'header', 'body' ];
|
||||
const self = this;
|
||||
var art = self.menuConfig.config.art;
|
||||
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
|
@ -399,43 +493,44 @@ function FullScreenEditorModule(options) {
|
|||
cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
switchFooter(cb) {
|
||||
var footerName = this.getFooterName();
|
||||
|
||||
this.switchFooter = function(cb) {
|
||||
var footerName = self.getFooterName();
|
||||
|
||||
self.redrawFooter( { footerName : footerName, clear : true }, function artDisplayed(err, artData) {
|
||||
this.redrawFooter( { footerName : footerName, clear : true }, (err, artData) => {
|
||||
if(err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var formId = self.getFormId(footerName);
|
||||
var formId = this.getFormId(footerName);
|
||||
|
||||
if(_.isUndefined(self.viewControllers[footerName])) {
|
||||
if(_.isUndefined(this.viewControllers[footerName])) {
|
||||
var menuLoadOpts = {
|
||||
callingMenu : self,
|
||||
callingMenu : this,
|
||||
formId : formId,
|
||||
mciMap : artData.mciMap
|
||||
};
|
||||
|
||||
self.addViewController(
|
||||
this.addViewController(
|
||||
footerName,
|
||||
new ViewController( { client : self.client, formId : formId } )
|
||||
).loadFromMenuConfig(menuLoadOpts, function footerReady(err) {
|
||||
new ViewController( { client : this.client, formId : formId } )
|
||||
).loadFromMenuConfig(menuLoadOpts, err => {
|
||||
cb(err);
|
||||
});
|
||||
} else {
|
||||
self.viewControllers[footerName].redrawAll();
|
||||
this.viewControllers[footerName].redrawAll();
|
||||
cb(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.initSequence = function() {
|
||||
initSequence() {
|
||||
var mciData = { };
|
||||
const self = this;
|
||||
var art = self.menuConfig.config.art;
|
||||
|
||||
assert(_.isObject(art));
|
||||
|
||||
async.series(
|
||||
|
@ -489,10 +584,10 @@ function FullScreenEditorModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.createInitialViews = function(mciData, cb) {
|
||||
}
|
||||
|
||||
createInitialViews(mciData, cb) {
|
||||
const self = this;
|
||||
var menuLoadOpts = { callingMenu : self };
|
||||
|
||||
async.series(
|
||||
|
@ -603,11 +698,11 @@ function FullScreenEditorModule(options) {
|
|||
cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.mciReadyHandler = function(mciData, cb) {
|
||||
mciReadyHandler(mciData, cb) {
|
||||
|
||||
self.createInitialViews(mciData, function viewsCreated(err) {
|
||||
this.createInitialViews(mciData, err => {
|
||||
// :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in
|
||||
// place - if this is for existing usernames else validate spec
|
||||
|
||||
|
@ -627,103 +722,94 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.updateEditModePosition = function(pos) {
|
||||
if(self.isEditMode()) {
|
||||
var posView = self.viewControllers.footerEditor.getView(1);
|
||||
updateEditModePosition(pos) {
|
||||
if(this.isEditMode()) {
|
||||
var posView = this.viewControllers.footerEditor.getView(1);
|
||||
if(posView) {
|
||||
self.client.term.rawWrite(ansi.savePos());
|
||||
posView.setText(_.padLeft(String(pos.row + 1), 2, '0') + ',' + _.padLeft(String(pos.col + 1), 2, '0'));
|
||||
self.client.term.rawWrite(ansi.restorePos());
|
||||
this.client.term.rawWrite(ansi.savePos());
|
||||
// :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat
|
||||
posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0'));
|
||||
this.client.term.rawWrite(ansi.restorePos());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.updateTextEditMode = function(mode) {
|
||||
if(self.isEditMode()) {
|
||||
var modeView = self.viewControllers.footerEditor.getView(2);
|
||||
updateTextEditMode(mode) {
|
||||
if(this.isEditMode()) {
|
||||
var modeView = this.viewControllers.footerEditor.getView(2);
|
||||
if(modeView) {
|
||||
self.client.term.rawWrite(ansi.savePos());
|
||||
this.client.term.rawWrite(ansi.savePos());
|
||||
modeView.setText('insert' === mode ? 'INS' : 'OVR');
|
||||
self.client.term.rawWrite(ansi.restorePos());
|
||||
this.client.term.rawWrite(ansi.restorePos());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setHeaderText = function(id, text) {
|
||||
var v = self.viewControllers.header.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
|
||||
this.initHeaderViewMode = function() {
|
||||
assert(_.isObject(self.message));
|
||||
setHeaderText(id, text) {
|
||||
this.setViewText('header', id, text);
|
||||
}
|
||||
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.From, self.message.fromUserName);
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.To, self.message.toUserName);
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.Subject, self.message.subject);
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.DateTime, moment(self.message.modTimestamp).format(self.client.currentTheme.helpers.getDateTimeFormat()));
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.MsgNum, (self.messageIndex + 1).toString());
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.MsgTotal, self.messageTotal.toString());
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.ViewCount, self.message.viewCount);
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.HashTags, 'TODO hash tags');
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.MessageID, self.message.messageId);
|
||||
self.setHeaderText(MCICodeIds.ViewModeHeader.ReplyToMsgID, self.message.replyToMessageId);
|
||||
};
|
||||
initHeaderViewMode() {
|
||||
assert(_.isObject(this.message));
|
||||
|
||||
this.initHeaderReplyEditMode = function() {
|
||||
assert(_.isObject(self.replyToMessage));
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.From, this.message.fromUserName);
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.To, this.message.toUserName);
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.Subject, this.message.subject);
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.DateTime, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat()));
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.MsgNum, (this.messageIndex + 1).toString());
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.MsgTotal, this.messageTotal.toString());
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.ViewCount, this.message.viewCount);
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.HashTags, 'TODO hash tags');
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.MessageID, this.message.messageId);
|
||||
this.setHeaderText(MciCodeIds.ViewModeHeader.ReplyToMsgID, this.message.replyToMessageId);
|
||||
}
|
||||
|
||||
self.setHeaderText(MCICodeIds.ReplyEditModeHeader.To, self.replyToMessage.fromUserName);
|
||||
initHeaderReplyEditMode() {
|
||||
assert(_.isObject(this.replyToMessage));
|
||||
|
||||
this.setHeaderText(MciCodeIds.ReplyEditModeHeader.To, this.replyToMessage.fromUserName);
|
||||
|
||||
//
|
||||
// We want to prefix the subject with "RE: " only if it's not already
|
||||
// that way -- avoid RE: RE: RE: RE: ...
|
||||
//
|
||||
let newSubj = self.replyToMessage.subject;
|
||||
let newSubj = this.replyToMessage.subject;
|
||||
if(false === /^RE:\s+/i.test(newSubj)) {
|
||||
newSubj = `RE: ${newSubj}`;
|
||||
}
|
||||
|
||||
self.setHeaderText(MCICodeIds.ReplyEditModeHeader.Subject, newSubj);
|
||||
};
|
||||
|
||||
this.initFooterViewMode = function() {
|
||||
|
||||
function setFooterText(id, text) {
|
||||
var v = self.viewControllers.footerView.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
this.setHeaderText(MciCodeIds.ReplyEditModeHeader.Subject, newSubj);
|
||||
}
|
||||
|
||||
setFooterText(MCICodeIds.ViewModeFooter.MsgNum, (self.messageIndex + 1).toString());
|
||||
setFooterText(MCICodeIds.ViewModeFooter.MsgTotal, self.messageTotal.toString());
|
||||
};
|
||||
initFooterViewMode() {
|
||||
this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgNum, (this.messageIndex + 1).toString() );
|
||||
this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgTotal, this.messageTotal.toString() );
|
||||
}
|
||||
|
||||
this.displayHelp = function(cb) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
displayHelp(cb) {
|
||||
this.client.term.rawWrite(ansi.resetScreen());
|
||||
|
||||
theme.displayThemeArt(
|
||||
{ name : self.menuConfig.config.art.help, client : self.client },
|
||||
{ name : this.menuConfig.config.art.help, client : this.client },
|
||||
() => {
|
||||
self.client.waitForKeyPress( () => {
|
||||
self.redrawScreen( () => {
|
||||
self.viewControllers[self.getFooterName()].setFocus(true);
|
||||
this.client.waitForKeyPress( () => {
|
||||
this.redrawScreen( () => {
|
||||
this.viewControllers[this.getFooterName()].setFocus(true);
|
||||
return cb(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.displayQuoteBuilder = function() {
|
||||
displayQuoteBuilder() {
|
||||
//
|
||||
// Clear body area
|
||||
//
|
||||
self.newQuoteBlock = true;
|
||||
this.newQuoteBlock = true;
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
|
@ -779,19 +865,19 @@ function FullScreenEditorModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.observeEditorEvents = function() {
|
||||
var bodyView = self.viewControllers.body.getView(1);
|
||||
observeEditorEvents() {
|
||||
const bodyView = this.viewControllers.body.getView(1);
|
||||
|
||||
bodyView.on('edit position', function cursorPosUpdate(pos) {
|
||||
self.updateEditModePosition(pos);
|
||||
bodyView.on('edit position', pos => {
|
||||
this.updateEditModePosition(pos);
|
||||
});
|
||||
|
||||
bodyView.on('text edit mode', function textEditMode(mode) {
|
||||
self.updateTextEditMode(mode);
|
||||
bodyView.on('text edit mode', mode => {
|
||||
this.updateTextEditMode(mode);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
this.observeViewPosition = function() {
|
||||
|
@ -801,43 +887,43 @@ function FullScreenEditorModule(options) {
|
|||
};
|
||||
*/
|
||||
|
||||
this.switchToHeader = function() {
|
||||
self.viewControllers.body.setFocus(false);
|
||||
self.viewControllers.header.switchFocus(2); // to
|
||||
switchToHeader() {
|
||||
this.viewControllers.body.setFocus(false);
|
||||
this.viewControllers.header.switchFocus(2); // to
|
||||
}
|
||||
|
||||
switchToBody() {
|
||||
this.viewControllers.header.setFocus(false);
|
||||
this.viewControllers.body.switchFocus(1);
|
||||
|
||||
this.observeEditorEvents();
|
||||
};
|
||||
|
||||
this.switchToBody = function() {
|
||||
self.viewControllers.header.setFocus(false);
|
||||
self.viewControllers.body.switchFocus(1);
|
||||
switchToFooter() {
|
||||
this.viewControllers.header.setFocus(false);
|
||||
this.viewControllers.body.setFocus(false);
|
||||
|
||||
self.observeEditorEvents();
|
||||
};
|
||||
this.viewControllers[this.getFooterName()].switchFocus(1); // HM1
|
||||
}
|
||||
|
||||
this.switchToFooter = function() {
|
||||
self.viewControllers.header.setFocus(false);
|
||||
self.viewControllers.body.setFocus(false);
|
||||
|
||||
self.viewControllers[self.getFooterName()].switchFocus(1); // HM1
|
||||
};
|
||||
|
||||
this.switchFromQuoteBuilderToBody = function() {
|
||||
self.viewControllers.quoteBuilder.setFocus(false);
|
||||
var body = self.viewControllers.body.getView(1);
|
||||
switchFromQuoteBuilderToBody() {
|
||||
this.viewControllers.quoteBuilder.setFocus(false);
|
||||
var body = this.viewControllers.body.getView(1);
|
||||
body.redraw();
|
||||
self.viewControllers.body.switchFocus(1);
|
||||
this.viewControllers.body.switchFocus(1);
|
||||
|
||||
// :TODO: create method (DRY)
|
||||
|
||||
self.updateTextEditMode(body.getTextEditMode());
|
||||
self.updateEditModePosition(body.getEditPosition());
|
||||
this.updateTextEditMode(body.getTextEditMode());
|
||||
this.updateEditModePosition(body.getEditPosition());
|
||||
|
||||
self.observeEditorEvents();
|
||||
};
|
||||
this.observeEditorEvents();
|
||||
}
|
||||
|
||||
this.quoteBuilderFinalize = function() {
|
||||
quoteBuilderFinalize() {
|
||||
// :TODO: fix magic #'s
|
||||
var quoteMsgView = self.viewControllers.quoteBuilder.getView(1);
|
||||
var msgView = self.viewControllers.body.getView(1);
|
||||
var quoteMsgView = this.viewControllers.quoteBuilder.getView(1);
|
||||
var msgView = this.viewControllers.body.getView(1);
|
||||
|
||||
var quoteLines = quoteMsgView.getData();
|
||||
|
||||
|
@ -848,164 +934,43 @@ function FullScreenEditorModule(options) {
|
|||
|
||||
quoteMsgView.setText('');
|
||||
|
||||
var footerName = self.getFooterName();
|
||||
this.footerMode = 'editor';
|
||||
|
||||
self.footerMode = 'editor';
|
||||
|
||||
self.switchFooter(function switched(err) {
|
||||
self.switchFromQuoteBuilderToBody();
|
||||
this.switchFooter( () => {
|
||||
this.switchFromQuoteBuilderToBody();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.getQuoteByHeader = function() {
|
||||
getQuoteByHeader() {
|
||||
let quoteFormat = this.menuConfig.config.quoteFormats;
|
||||
|
||||
if(Array.isArray(quoteFormat)) {
|
||||
quoteFormat = quoteFormat[ Math.floor(Math.random() * quoteFormat.length) ];
|
||||
} else if(!_.isString(quoteFormat)) {
|
||||
quoteFormat = 'On {dateTime} {userName} said...';
|
||||
}
|
||||
|
||||
const dtFormat = this.menuConfig.config.quoteDateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat();
|
||||
const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat();
|
||||
return stringFormat(quoteFormat, {
|
||||
dateTime : moment(self.replyToMessage.modTimestamp).format(dtFormat),
|
||||
userName : self.replyToMessage.fromUserName,
|
||||
dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat),
|
||||
userName : this.replyToMessage.fromUserName,
|
||||
});
|
||||
};
|
||||
|
||||
this.menuMethods = {
|
||||
//
|
||||
// Validation stuff
|
||||
//
|
||||
viewValidationListener : function(err, cb) {
|
||||
var errMsgView = self.viewControllers.header.getView(MCICodeIds.ReplyEditModeHeader.ErrorMsg);
|
||||
var newFocusViewId;
|
||||
if(errMsgView) {
|
||||
if(err) {
|
||||
errMsgView.setText(err.message);
|
||||
|
||||
if(MCICodeIds.ViewModeHeader.Subject === err.view.getId()) {
|
||||
// :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel)
|
||||
}
|
||||
} else {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
}
|
||||
cb(newFocusViewId);
|
||||
},
|
||||
|
||||
headerSubmit : function(formData, extraArgs, cb) {
|
||||
self.switchToBody();
|
||||
return cb(null);
|
||||
},
|
||||
editModeEscPressed : function(formData, extraArgs, cb) {
|
||||
self.footerMode = 'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
||||
|
||||
self.switchFooter(function next(err) {
|
||||
if(err) {
|
||||
// :TODO:... what now?
|
||||
console.log(err)
|
||||
} else {
|
||||
switch(self.footerMode) {
|
||||
case 'editor' :
|
||||
if(!_.isUndefined(self.viewControllers.footerEditorMenu)) {
|
||||
//self.viewControllers.footerEditorMenu.setFocus(false);
|
||||
self.viewControllers.footerEditorMenu.detachClientEvents();
|
||||
}
|
||||
self.viewControllers.body.switchFocus(1);
|
||||
self.observeEditorEvents();
|
||||
break;
|
||||
|
||||
case 'editorMenu' :
|
||||
self.viewControllers.body.setFocus(false);
|
||||
self.viewControllers.footerEditorMenu.switchFocus(1);
|
||||
break;
|
||||
|
||||
default : throw new Error('Unexpected mode');
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
},
|
||||
editModeMenuQuote : function(formData, extraArgs, cb) {
|
||||
self.viewControllers.footerEditorMenu.setFocus(false);
|
||||
self.displayQuoteBuilder();
|
||||
return cb(null);
|
||||
},
|
||||
appendQuoteEntry: function(formData, extraArgs, cb) {
|
||||
// :TODO: Dont' use magic # ID's here
|
||||
var quoteMsgView = self.viewControllers.quoteBuilder.getView(1);
|
||||
|
||||
if(self.newQuoteBlock) {
|
||||
self.newQuoteBlock = false;
|
||||
quoteMsgView.addText(self.getQuoteByHeader());
|
||||
}
|
||||
|
||||
var quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote);
|
||||
quoteMsgView.addText(quoteText);
|
||||
|
||||
//
|
||||
// If this is *not* the last item, advance. Otherwise, do nothing as we
|
||||
// don't want to jump back to the top and repeat already quoted lines
|
||||
//
|
||||
var quoteListView = self.viewControllers.quoteBuilder.getView(3);
|
||||
if(quoteListView.getData() !== quoteListView.getCount() - 1) {
|
||||
quoteListView.focusNext();
|
||||
} else {
|
||||
self.quoteBuilderFinalize();
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
quoteBuilderEscPressed : function(formData, extraArgs, cb) {
|
||||
self.quoteBuilderFinalize();
|
||||
return cb(null);
|
||||
},
|
||||
/*
|
||||
replyDiscard : function(formData, extraArgs) {
|
||||
// :TODO: need to prompt yes/no
|
||||
// :TODO: @method for fallback would be better
|
||||
self.prevMenu();
|
||||
},
|
||||
*/
|
||||
editModeMenuHelp : function(formData, extraArgs, cb) {
|
||||
self.viewControllers.footerEditorMenu.setFocus(false);
|
||||
return self.displayHelp(cb);
|
||||
},
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// View Mode
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
viewModeMenuHelp : function(formData, extraArgs, cb) {
|
||||
self.viewControllers.footerView.setFocus(false);
|
||||
return self.displayHelp(cb);
|
||||
}
|
||||
};
|
||||
|
||||
if(_.has(options, 'extraArgs.message')) {
|
||||
this.setMessage(options.extraArgs.message);
|
||||
} else if(_.has(options, 'extraArgs.replyToMessage')) {
|
||||
this.replyToMessage = options.extraArgs.replyToMessage;
|
||||
}
|
||||
}
|
||||
|
||||
require('util').inherits(FullScreenEditorModule, MenuModule);
|
||||
|
||||
require('./mod_mixins.js').MessageAreaConfTempSwitcher.call(FullScreenEditorModule.prototype);
|
||||
|
||||
FullScreenEditorModule.prototype.enter = function() {
|
||||
|
||||
enter() {
|
||||
if(this.messageAreaTag) {
|
||||
this.tempMessageConfAndAreaSwitch(this.messageAreaTag);
|
||||
}
|
||||
|
||||
FullScreenEditorModule.super_.prototype.enter.call(this);
|
||||
};
|
||||
super.enter();
|
||||
}
|
||||
|
||||
FullScreenEditorModule.prototype.leave = function() {
|
||||
leave() {
|
||||
this.tempMessageConfAndAreaRestore();
|
||||
FullScreenEditorModule.super_.prototype.leave.call(this);
|
||||
};
|
||||
super.leave();
|
||||
}
|
||||
|
||||
FullScreenEditorModule.prototype.mciReady = function(mciData, cb) {
|
||||
this.mciReadyHandler(mciData, cb);
|
||||
mciReady(mciData, cb) {
|
||||
return this.mciReadyHandler(mciData, cb);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ let FNV1a = require('./fnv1a.js');
|
|||
let _ = require('lodash');
|
||||
let iconv = require('iconv-lite');
|
||||
let moment = require('moment');
|
||||
let uuid = require('node-uuid');
|
||||
//let uuid = require('node-uuid');
|
||||
let os = require('os');
|
||||
|
||||
let packageJson = require('../package.json');
|
||||
|
@ -39,7 +39,7 @@ exports.getQuotePrefix = getQuotePrefix;
|
|||
// Namespace for RFC-4122 name based UUIDs generated from
|
||||
// FTN kludges MSGID + AREA
|
||||
//
|
||||
const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654');
|
||||
//const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654');
|
||||
|
||||
// See list here: https://github.com/Mithgol/node-fidonet-jam
|
||||
|
||||
|
|
|
@ -113,30 +113,39 @@ HorizontalMenuView.prototype.setItems = function(items) {
|
|||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
|
||||
if(key) {
|
||||
var prevFocusedItemIndex = this.focusedItemIndex;
|
||||
HorizontalMenuView.prototype.focusNext = function() {
|
||||
if(this.items.length - 1 === this.focusedItemIndex) {
|
||||
this.focusedItemIndex = 0;
|
||||
} else {
|
||||
this.focusedItemIndex++;
|
||||
}
|
||||
|
||||
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
|
||||
this.redraw();
|
||||
|
||||
HorizontalMenuView.super_.prototype.focusNext.call(this);
|
||||
};
|
||||
|
||||
HorizontalMenuView.prototype.focusPrevious = function() {
|
||||
|
||||
if(this.isKeyMapped('left', key.name)) {
|
||||
if(0 === this.focusedItemIndex) {
|
||||
this.focusedItemIndex = this.items.length - 1;
|
||||
} else {
|
||||
this.focusedItemIndex--;
|
||||
}
|
||||
|
||||
} else if(this.isKeyMapped('right', key.name)) {
|
||||
if(this.items.length - 1 === this.focusedItemIndex) {
|
||||
this.focusedItemIndex = 0;
|
||||
} else {
|
||||
this.focusedItemIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if(prevFocusedItemIndex !== this.focusedItemIndex) {
|
||||
// :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes
|
||||
// if this is changed to allow scrolling
|
||||
this.redraw();
|
||||
return;
|
||||
|
||||
HorizontalMenuView.super_.prototype.focusPrevious.call(this);
|
||||
};
|
||||
|
||||
HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
|
||||
if(key) {
|
||||
if(this.isKeyMapped('left', key.name)) {
|
||||
this.focusPrevious();
|
||||
} else if(this.isKeyMapped('right', key.name)) {
|
||||
this.focusNext();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,19 +18,27 @@ module.exports = class KeyEntryView extends View {
|
|||
this.eatTabKey = options.eatTabKey || true;
|
||||
this.caseInsensitive = options.caseInsensitive || true;
|
||||
|
||||
// :TODO: allow (by default) only supplied keys[] to even draw
|
||||
if(Array.isArray(options.keys)) {
|
||||
if(this.caseInsensitive) {
|
||||
this.keys = options.keys.map( k => k.toUpperCase() );
|
||||
} else {
|
||||
this.keys = options.keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress(ch, key) {
|
||||
if(ch && isPrintable(ch)) {
|
||||
this.redraw(); // sets position
|
||||
this.client.term.write(stylizeString(ch, this.textStyle));
|
||||
}
|
||||
const drawKey = ch;
|
||||
|
||||
if(ch && this.caseInsensitive) {
|
||||
ch = ch.toUpperCase();
|
||||
}
|
||||
|
||||
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
|
||||
this.redraw(); // sets position
|
||||
this.client.term.write(stylizeString(ch, this.textStyle));
|
||||
}
|
||||
|
||||
this.keyEntered = ch || key.name;
|
||||
|
||||
if(key && 'tab' === key.name && !this.eatTabKey) {
|
||||
|
@ -54,6 +62,12 @@ module.exports = class KeyEntryView extends View {
|
|||
this.caseInsensitive = propValue;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'keys' :
|
||||
if(Array.isArray(propValue)) {
|
||||
this.keys = propValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
super.setPropertyValue(propName, propValue);
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const logger = require('./logger.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
||||
const listeningServers = {}; // packageName -> info
|
||||
|
||||
exports.startup = startup;
|
||||
exports.shutdown = shutdown;
|
||||
exports.getServer = getServer;
|
||||
|
||||
function startup(cb) {
|
||||
return startListening(cb);
|
||||
}
|
||||
|
||||
function shutdown(cb) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
function getServer(packageName) {
|
||||
return listeningServers[packageName];
|
||||
}
|
||||
|
||||
function startListening(cb) {
|
||||
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
||||
|
||||
async.each( [ 'login', 'content' ], (category, next) => {
|
||||
moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => {
|
||||
// :TODO: use enig error here!
|
||||
if(err) {
|
||||
if('EENIGMODDISABLED' === err.code) {
|
||||
logger.log.debug(err.message);
|
||||
} else {
|
||||
logger.log.info( { err : err }, 'Failed loading module');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleInst = new module.getModule();
|
||||
try {
|
||||
moduleInst.createServer();
|
||||
if(!moduleInst.listen()) {
|
||||
throw new Error('Failed listening');
|
||||
}
|
||||
|
||||
listeningServers[module.moduleInfo.packageName] = {
|
||||
instance : moduleInst,
|
||||
info : module.moduleInfo,
|
||||
};
|
||||
|
||||
} catch(e) {
|
||||
logger.log.error(e, 'Exception caught creating server!');
|
||||
}
|
||||
}, err => {
|
||||
return next(err);
|
||||
});
|
||||
}, err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const conf = require('./config.js');
|
||||
const logger = require('./logger.js');
|
||||
const ServerModule = require('./server_module.js').ServerModule;
|
||||
const clientConns = require('./client_connections.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = class LoginServerModule extends ServerModule {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
// :TODO: we need to max connections -- e.g. from config 'maxConnections'
|
||||
|
||||
prepareClient(client, cb) {
|
||||
const theme = require('./theme.js');
|
||||
|
||||
//
|
||||
// Choose initial theme before we have user context
|
||||
//
|
||||
if('*' === conf.config.preLoginTheme) {
|
||||
client.user.properties.theme_id = theme.getRandomTheme() || '';
|
||||
} else {
|
||||
client.user.properties.theme_id = conf.config.preLoginTheme;
|
||||
}
|
||||
|
||||
theme.setClientTheme(client, client.user.properties.theme_id);
|
||||
return cb(null); // note: currently useless to use cb here - but this may change...again...
|
||||
}
|
||||
|
||||
handleNewClient(client, clientSock, modInfo) {
|
||||
//
|
||||
// Start tracking the client. We'll assign it an ID which is
|
||||
// just the index in our connections array.
|
||||
//
|
||||
if(_.isUndefined(client.session)) {
|
||||
client.session = {};
|
||||
}
|
||||
|
||||
client.session.serverName = modInfo.name;
|
||||
client.session.isSecure = modInfo.isSecure || false;
|
||||
|
||||
clientConns.addNewClient(client, clientSock);
|
||||
|
||||
client.on('ready', readyOptions => {
|
||||
|
||||
client.startIdleMonitor();
|
||||
|
||||
// Go to module -- use default error handler
|
||||
this.prepareClient(client, () => {
|
||||
require('./connect.js').connectEntry(client, readyOptions.firstMenu);
|
||||
});
|
||||
});
|
||||
|
||||
client.on('end', () => {
|
||||
clientConns.removeClient(client);
|
||||
});
|
||||
|
||||
client.on('error', err => {
|
||||
logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message);
|
||||
});
|
||||
|
||||
client.on('close', err => {
|
||||
const logFunc = err ? logger.log.info : logger.log.debug;
|
||||
logFunc( { clientId : client.session.id }, 'Connection closed');
|
||||
|
||||
clientConns.removeClient(client);
|
||||
});
|
||||
|
||||
client.on('idle timeout', () => {
|
||||
client.log.info('User idle timeout expired');
|
||||
|
||||
client.menuStack.goto('idleLogoff', err => {
|
||||
if(err) {
|
||||
// likely just doesn't exist
|
||||
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,112 +1,50 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var PluginModule = require('./plugin_module.js').PluginModule;
|
||||
var theme = require('./theme.js');
|
||||
var art = require('./art.js');
|
||||
var Log = require('./logger.js').log;
|
||||
var ansi = require('./ansi_term.js');
|
||||
var asset = require('./asset.js');
|
||||
var ViewController = require('./view_controller.js').ViewController;
|
||||
var menuUtil = require('./menu_util.js');
|
||||
var Config = require('./config.js').config;
|
||||
const PluginModule = require('./plugin_module.js').PluginModule;
|
||||
const theme = require('./theme.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const menuUtil = require('./menu_util.js');
|
||||
const Config = require('./config.js').config;
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||
const Errors = require('../core/enig_error.js').Errors;
|
||||
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
// deps
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.MenuModule = MenuModule;
|
||||
exports.MenuModule = class MenuModule extends PluginModule {
|
||||
|
||||
// :TODO: some of this is a bit off... should pause after finishedLoading()
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
function MenuModule(options) {
|
||||
PluginModule.call(this, options);
|
||||
|
||||
var self = this;
|
||||
this.menuName = options.menuName;
|
||||
this.menuConfig = options.menuConfig;
|
||||
this.client = options.client;
|
||||
|
||||
// :TODO: this and the line below with .config creates empty ({}) objects in the theme --
|
||||
// ...which we really should not do. If they aren't there already, don't use 'em.
|
||||
this.menuConfig.options = options.menuConfig.options || {};
|
||||
this.menuMethods = {}; // methods called from @method's
|
||||
|
||||
this.cls = _.isBoolean(this.menuConfig.options.cls) ?
|
||||
this.menuConfig.options.cls :
|
||||
Config.menus.cls;
|
||||
|
||||
this.menuConfig.config = this.menuConfig.config || {};
|
||||
|
||||
this.initViewControllers();
|
||||
this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config.menus.cls;
|
||||
|
||||
this.shouldPause = function() {
|
||||
return 'end' === self.menuConfig.options.pause || true === self.menuConfig.options.pause;
|
||||
};
|
||||
|
||||
this.hasNextTimeout = function() {
|
||||
return _.isNumber(self.menuConfig.options.nextTimeout);
|
||||
};
|
||||
|
||||
this.autoNextMenu = function(cb) {
|
||||
function goNext() {
|
||||
if(_.isString(self.menuConfig.next) || _.isArray(self.menuConfig.next)) {
|
||||
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
|
||||
} else {
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
}
|
||||
|
||||
if(_.has(self.menuConfig, 'runtime.autoNext') && true === self.menuConfig.runtime.autoNext) {
|
||||
/*
|
||||
If 'next' is supplied, we'll use it. Otherwise, utlize fallback which
|
||||
may be explicit (supplied) or non-explicit (previous menu)
|
||||
|
||||
'next' may be a simple asset, or a object with next.asset and
|
||||
extrArgs
|
||||
|
||||
next: assetSpec
|
||||
|
||||
-or-
|
||||
|
||||
next: {
|
||||
asset: assetSpec
|
||||
extraArgs: ...
|
||||
}
|
||||
*/
|
||||
if(self.hasNextTimeout()) {
|
||||
setTimeout( () => {
|
||||
return goNext();
|
||||
}, this.menuConfig.options.nextTimeout);
|
||||
} else {
|
||||
goNext();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.haveNext = function() {
|
||||
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(MenuModule, PluginModule);
|
||||
|
||||
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
|
||||
|
||||
|
||||
MenuModule.prototype.enter = function() {
|
||||
if(_.isString(this.menuConfig.desc)) {
|
||||
this.client.currentStatus = this.menuConfig.desc;
|
||||
} else {
|
||||
this.client.currentStatus = 'Browsing menus';
|
||||
this.viewControllers = {};
|
||||
}
|
||||
|
||||
enter() {
|
||||
this.initSequence();
|
||||
};
|
||||
}
|
||||
|
||||
MenuModule.prototype.initSequence = function() {
|
||||
var mciData = { };
|
||||
leave() {
|
||||
this.detachViewControllers();
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
const mciData = {};
|
||||
let pausePosition;
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -114,211 +52,372 @@ MenuModule.prototype.initSequence = function() {
|
|||
self.beforeArt(callback);
|
||||
},
|
||||
function displayMenuArt(callback) {
|
||||
if(_.isString(self.menuConfig.art)) {
|
||||
theme.displayThemedAsset(
|
||||
if(!_.isString(self.menuConfig.art)) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
self.displayAsset(
|
||||
self.menuConfig.art,
|
||||
self.client,
|
||||
self.menuConfig.options, // can include .font, .trailingLF, etc.
|
||||
function displayed(err, artData) {
|
||||
self.menuConfig.options,
|
||||
(err, artData) => {
|
||||
if(err) {
|
||||
self.client.log.trace( { art : self.menuConfig.art, error : err.message }, 'Could not display art');
|
||||
self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } );
|
||||
} else {
|
||||
mciData.menu = artData.mciMap;
|
||||
}
|
||||
callback(null); // non-fatal
|
||||
|
||||
return callback(null); // any errors are non-fatal
|
||||
}
|
||||
);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
function moveToPromptLocation(callback) {
|
||||
if(self.menuConfig.prompt) {
|
||||
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
|
||||
}
|
||||
|
||||
callback(null);
|
||||
return callback(null);
|
||||
},
|
||||
function displayPromptArt(callback) {
|
||||
if(_.isString(self.menuConfig.prompt)) {
|
||||
// If a prompt is specified, we need the configuration
|
||||
if(!_.isObject(self.menuConfig.promptConfig)) {
|
||||
callback(new Error('Prompt specified but configuraiton not found!'));
|
||||
return;
|
||||
if(!_.isString(self.menuConfig.prompt)) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
// Prompts *must* have art. If it's missing it's an error
|
||||
// :TODO: allow inline prompts in the future, e.g. @inline:memberName -> { "memberName" : { "text" : "stuff", ... } }
|
||||
var promptConfig = self.menuConfig.promptConfig;
|
||||
theme.displayThemedAsset(
|
||||
promptConfig.art,
|
||||
self.client,
|
||||
self.menuConfig.options, // can include .font, .trailingLF, etc.
|
||||
function displayed(err, artData) {
|
||||
if(!err) {
|
||||
if(!_.isObject(self.menuConfig.promptConfig)) {
|
||||
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
|
||||
}
|
||||
|
||||
self.displayAsset(
|
||||
self.menuConfig.promptConfig.art,
|
||||
self.menuConfig.options,
|
||||
(err, artData) => {
|
||||
if(artData) {
|
||||
mciData.prompt = artData.mciMap;
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
return callback(err); // pass err here; prompts *must* have art
|
||||
}
|
||||
);
|
||||
},
|
||||
function recordCursorPosition(callback) {
|
||||
if(self.shouldPause()) {
|
||||
self.client.once('cursor position report', function cpr(pos) {
|
||||
self.afterArtPos = pos;
|
||||
self.client.log.trace( { position : pos }, 'After art position recorded');
|
||||
callback(null);
|
||||
});
|
||||
self.client.term.write(ansi.queryPos());
|
||||
} else {
|
||||
callback(null);
|
||||
if(!self.shouldPause()) {
|
||||
return callback(null); // cursor position not needed
|
||||
}
|
||||
|
||||
self.client.once('cursor position report', pos => {
|
||||
pausePosition = { row : pos[0], col : 1 };
|
||||
self.client.log.trace('After art position recorded', pausePosition );
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
self.client.term.rawWrite(ansi.queryPos());
|
||||
},
|
||||
function afterArtDisplayed(callback) {
|
||||
self.mciReady(mciData, callback);
|
||||
return self.mciReady(mciData, callback);
|
||||
},
|
||||
function displayPauseIfRequested(callback) {
|
||||
if(self.shouldPause()) {
|
||||
self.client.term.write(ansi.goto(self.afterArtPos[0], 1));
|
||||
|
||||
// :TODO: really need a client.term.pause() that uses the correct art/etc.
|
||||
theme.displayThemedPause( { client : self.client }, function keyPressed() {
|
||||
callback(null);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
if(!self.shouldPause()) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
return self.pausePrompt(pausePosition, callback);
|
||||
},
|
||||
function finishAndNext(callback) {
|
||||
self.finishedLoading();
|
||||
|
||||
self.autoNextMenu(callback);
|
||||
return self.autoNextMenu(callback);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
err => {
|
||||
if(err) {
|
||||
console.log(err)
|
||||
// :TODO: what to do exactly?????
|
||||
return self.prevMenu( () => {
|
||||
// dummy
|
||||
});
|
||||
self.client.log.warn('Error during init sequence', { error : err.message } );
|
||||
|
||||
return self.prevMenu( () => { /* dummy */ } );
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
MenuModule.prototype.getSaveState = function() {
|
||||
// nothing in base
|
||||
};
|
||||
|
||||
MenuModule.prototype.restoreSavedState = function(/*savedState*/) {
|
||||
// nothing in base
|
||||
};
|
||||
|
||||
MenuModule.prototype.nextMenu = function(cb) {
|
||||
//
|
||||
// If we don't actually have |next|, we'll go previous
|
||||
//
|
||||
if(!this.haveNext()) {
|
||||
return this.prevMenu(cb);
|
||||
}
|
||||
|
||||
this.client.menuStack.next(cb);
|
||||
};
|
||||
|
||||
MenuModule.prototype.prevMenu = function(cb) {
|
||||
this.client.menuStack.prev(cb);
|
||||
};
|
||||
|
||||
MenuModule.prototype.gotoMenu = function(name, options, cb) {
|
||||
this.client.menuStack.goto(name, options, cb);
|
||||
};
|
||||
|
||||
MenuModule.prototype.leave = function() {
|
||||
this.detachViewControllers();
|
||||
};
|
||||
|
||||
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
|
||||
//
|
||||
beforeArt(cb) {
|
||||
if(_.isNumber(this.menuConfig.options.baudRate)) {
|
||||
this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
|
||||
// :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here
|
||||
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
|
||||
}
|
||||
|
||||
if(this.cls) {
|
||||
this.client.term.write(ansi.resetScreen());
|
||||
this.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
};
|
||||
}
|
||||
|
||||
MenuModule.prototype.mciReady = function(mciData, cb) {
|
||||
// Reserved for sub classes
|
||||
cb(null);
|
||||
};
|
||||
mciReady(mciData, cb) {
|
||||
// available for sub-classes
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
MenuModule.prototype.standardMCIReadyHandler = function(mciData, cb) {
|
||||
finishedLoading() {
|
||||
// nothing in base
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
// nothing in base
|
||||
}
|
||||
|
||||
restoreSavedState(/*savedState*/) {
|
||||
// nothing in base
|
||||
}
|
||||
|
||||
getMenuResult() {
|
||||
// default to the formData that was provided @ a submit, if any
|
||||
return this.submitFormData;
|
||||
}
|
||||
|
||||
nextMenu(cb) {
|
||||
if(!this.haveNext()) {
|
||||
return this.prevMenu(cb); // no next, go to prev
|
||||
}
|
||||
|
||||
return this.client.menuStack.next(cb);
|
||||
}
|
||||
|
||||
prevMenu(cb) {
|
||||
return this.client.menuStack.prev(cb);
|
||||
}
|
||||
|
||||
gotoMenu(name, options, cb) {
|
||||
return this.client.menuStack.goto(name, options, cb);
|
||||
}
|
||||
|
||||
addViewController(name, vc) {
|
||||
assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`);
|
||||
|
||||
this.viewControllers[name] = vc;
|
||||
return vc;
|
||||
}
|
||||
|
||||
detachViewControllers() {
|
||||
Object.keys(this.viewControllers).forEach( name => {
|
||||
this.viewControllers[name].detachClientEvents();
|
||||
});
|
||||
}
|
||||
|
||||
shouldPause() {
|
||||
return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause);
|
||||
}
|
||||
|
||||
hasNextTimeout() {
|
||||
return _.isNumber(this.menuConfig.options.nextTimeout);
|
||||
}
|
||||
|
||||
haveNext() {
|
||||
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
|
||||
}
|
||||
|
||||
autoNextMenu(cb) {
|
||||
const self = this;
|
||||
|
||||
function gotoNextMenu() {
|
||||
if(self.haveNext()) {
|
||||
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
|
||||
} else {
|
||||
return self.prevMenu(cb);
|
||||
}
|
||||
}
|
||||
|
||||
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
|
||||
if(this.hasNextTimeout()) {
|
||||
setTimeout( () => {
|
||||
return gotoNextMenu();
|
||||
}, this.menuConfig.options.nextTimeout);
|
||||
} else {
|
||||
return gotoNextMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
standardMCIReadyHandler(mciData, cb) {
|
||||
//
|
||||
// A quick rundown:
|
||||
// * We may have mciData.menu, mciData.prompt, or both.
|
||||
// * Prompt form is favored over menu form if both are present.
|
||||
// * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve)
|
||||
//
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function addViewControllers(callback) {
|
||||
_.forEach(mciData, function entry(mciMap, name) {
|
||||
_.forEach(mciData, (mciMap, name) => {
|
||||
assert('menu' === name || 'prompt' === name);
|
||||
self.addViewController(name, new ViewController( { client : self.client } ) );
|
||||
});
|
||||
callback(null);
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function createMenu(callback) {
|
||||
if(self.viewControllers.menu) {
|
||||
var menuLoadOpts = {
|
||||
if(!self.viewControllers.menu) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
const menuLoadOpts = {
|
||||
mciMap : mciData.menu,
|
||||
callingMenu : self,
|
||||
withoutForm : _.isObject(mciData.prompt),
|
||||
};
|
||||
|
||||
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, function menuLoaded(err) {
|
||||
callback(err);
|
||||
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
|
||||
return callback(err);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
function createPrompt(callback) {
|
||||
if(self.viewControllers.prompt) {
|
||||
var promptLoadOpts = {
|
||||
if(!self.viewControllers.prompt) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
const promptLoadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.prompt,
|
||||
};
|
||||
|
||||
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, function promptLoaded(err) {
|
||||
callback(err);
|
||||
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => {
|
||||
return callback(err);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
cb(err);
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayAsset(name, options, cb) {
|
||||
if(_.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if(options.clearScreen) {
|
||||
this.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
return theme.displayThemedAsset(
|
||||
name,
|
||||
this.client,
|
||||
Object.assign( { font : this.menuConfig.config.font }, options ),
|
||||
(err, artData) => {
|
||||
if(cb) {
|
||||
return cb(err, artData);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
prepViewController(name, formId, artData, cb) {
|
||||
if(_.isUndefined(this.viewControllers[name])) {
|
||||
const vcOpts = {
|
||||
client : this.client,
|
||||
formId : formId,
|
||||
};
|
||||
|
||||
MenuModule.prototype.finishedLoading = function() {
|
||||
const vc = this.addViewController(name, new ViewController(vcOpts));
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : this,
|
||||
mciMap : artData.mciMap,
|
||||
formId : formId,
|
||||
};
|
||||
|
||||
MenuModule.prototype.getMenuResult = function() {
|
||||
// nothing in base
|
||||
return vc.loadFromMenuConfig(loadOpts, cb);
|
||||
}
|
||||
|
||||
this.viewControllers[name].setFocus(true);
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
prepViewControllerWithArt(name, formId, options, cb) {
|
||||
this.displayAsset(
|
||||
this.menuConfig.config.art[name],
|
||||
options,
|
||||
(err, artData) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return this.prepViewController(name, formId, artData, cb);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
optionalMoveToPosition(position) {
|
||||
if(position) {
|
||||
position.x = position.row || position.x || 1;
|
||||
position.y = position.col || position.y || 1;
|
||||
|
||||
this.client.term.rawWrite(ansi.goto(position.x, position.y));
|
||||
}
|
||||
}
|
||||
|
||||
pausePrompt(position, cb) {
|
||||
if(!cb && _.isFunction(position)) {
|
||||
cb = position;
|
||||
position = null;
|
||||
}
|
||||
|
||||
this.optionalMoveToPosition(position);
|
||||
|
||||
return theme.displayThemedPause(this.client, cb);
|
||||
}
|
||||
|
||||
/*
|
||||
:TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... )
|
||||
promptForInput(formName, name, options, cb) {
|
||||
if(!cb && _.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
options.viewController = this.viewControllers[formName];
|
||||
|
||||
this.optionalMoveToPosition(options.position);
|
||||
|
||||
return theme.displayThemedPrompt(name, this.client, options, cb);
|
||||
}
|
||||
*/
|
||||
|
||||
setViewText(formName, mciId, text, appendMultiLine) {
|
||||
const view = this.viewControllers[formName].getView(mciId);
|
||||
if(!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(appendMultiLine && (view instanceof MultiLineEditTextView)) {
|
||||
view.addText(text);
|
||||
} else {
|
||||
view.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
|
||||
options = options || {};
|
||||
|
||||
let textView;
|
||||
let customMciId = startId;
|
||||
const config = this.menuConfig.config;
|
||||
const endId = options.endId || 99; // we'll fail to get a view before 99
|
||||
|
||||
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) {
|
||||
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
|
||||
const format = config[key];
|
||||
|
||||
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) {
|
||||
const text = stringFormat(format, fmtObj);
|
||||
|
||||
if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) {
|
||||
textView.addText(text);
|
||||
} else {
|
||||
textView.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
++customMciId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -99,6 +99,7 @@ module.exports = class MenuStack {
|
|||
|
||||
if(!cb && _.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
@ -133,6 +134,12 @@ module.exports = class MenuStack {
|
|||
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
|
||||
|
||||
currentModuleInfo.instance.leave();
|
||||
|
||||
const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags;
|
||||
|
||||
if(menuFlags.includes('noHistory')) {
|
||||
this.pop().instance.leave(); // leave & remove current
|
||||
}
|
||||
}
|
||||
|
||||
self.push({
|
||||
|
@ -146,11 +153,17 @@ module.exports = class MenuStack {
|
|||
modInst.restoreSavedState(options.savedState);
|
||||
}
|
||||
|
||||
modInst.enter();
|
||||
const stackEntries = self.stack.map(stackEntry => {
|
||||
let name = stackEntry.name;
|
||||
if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) {
|
||||
name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
|
||||
self.client.log.trace(
|
||||
{ stack : _.map(self.stack, stackEntry => stackEntry.name) },
|
||||
'Updated menu stack');
|
||||
self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' );
|
||||
|
||||
modInst.enter();
|
||||
|
||||
if(cb) {
|
||||
cb(null);
|
||||
|
|
|
@ -64,6 +64,12 @@ function loadMenu(options, cb) {
|
|||
},
|
||||
function loadMenuModule(menuConfig, callback) {
|
||||
|
||||
menuConfig.options = menuConfig.options || {};
|
||||
menuConfig.options.menuFlags = menuConfig.options.menuFlags || [];
|
||||
if(!Array.isArray(menuConfig.options.menuFlags)) {
|
||||
menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ];
|
||||
}
|
||||
|
||||
const modAsset = asset.getModuleAsset(menuConfig.module);
|
||||
const modSupplied = null !== modAsset;
|
||||
|
||||
|
@ -88,18 +94,20 @@ function loadMenu(options, cb) {
|
|||
{ moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo },
|
||||
'Creating menu module instance');
|
||||
|
||||
let moduleInstance;
|
||||
try {
|
||||
const moduleInstance = new modData.mod.getModule({
|
||||
moduleInstance = new modData.mod.getModule({
|
||||
menuName : options.name,
|
||||
menuConfig : modData.config,
|
||||
extraArgs : options.extraArgs,
|
||||
client : options.client,
|
||||
lastMenuResult : options.lastMenuResult,
|
||||
});
|
||||
return callback(null, moduleInstance);
|
||||
} catch(e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
return callback(null, moduleInstance);
|
||||
}
|
||||
],
|
||||
(err, modInst) => {
|
||||
|
@ -121,8 +129,8 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
|
|||
return;
|
||||
}
|
||||
|
||||
var formForId = menuConfig.form[formId];
|
||||
var mciReqKey = _.filter(_.pluck(_.sortBy(mciMap, 'code'), 'code'), function(mci) {
|
||||
const formForId = menuConfig.form[formId];
|
||||
const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => {
|
||||
return MCIViewFactory.UserViewCodes.indexOf(mci) > -1;
|
||||
}).join('');
|
||||
|
||||
|
@ -142,8 +150,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
|
|||
//
|
||||
if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
|
||||
Log.trace('Using generic configuration');
|
||||
cb(null, formForId);
|
||||
return;
|
||||
return cb(null, formForId);
|
||||
}
|
||||
|
||||
cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\''));
|
||||
|
|
|
@ -77,6 +77,20 @@ MenuView.prototype.setItems = function(items) {
|
|||
}
|
||||
};
|
||||
|
||||
MenuView.prototype.removeItem = function(index) {
|
||||
this.items.splice(index, 1);
|
||||
|
||||
if(this.focusItems) {
|
||||
this.focusItems.splice(index, 1);
|
||||
}
|
||||
|
||||
if(this.focusedItemIndex >= index) {
|
||||
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
|
||||
}
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
MenuView.prototype.getCount = function() {
|
||||
return this.items.length;
|
||||
};
|
||||
|
@ -92,12 +106,10 @@ MenuView.prototype.getItem = function(index) {
|
|||
};
|
||||
|
||||
MenuView.prototype.focusNext = function() {
|
||||
// nothing @ base currently
|
||||
this.emit('index update', this.focusedItemIndex);
|
||||
};
|
||||
|
||||
MenuView.prototype.focusPrevious = function() {
|
||||
// nothign @ base currently
|
||||
this.emit('index update', this.focusedItemIndex);
|
||||
};
|
||||
|
||||
|
@ -149,6 +161,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) {
|
|||
case 'hotKeys' : this.setHotKeys(value); break;
|
||||
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
||||
case 'justify' : this.justify = value; break;
|
||||
case 'focusItemIndex' : this.focusedItemIndex = value; break;
|
||||
}
|
||||
|
||||
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
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;
|
||||
const msgDb = require('./database.js').dbs.message;
|
||||
const wordWrapText = require('./word_wrap.js').wordWrapText;
|
||||
const ftnUtil = require('./ftn_util.js');
|
||||
const createNamedUUID = require('./uuid_util.js').createNamedUUID;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
|
||||
let uuid = require('node-uuid');
|
||||
let async = require('async');
|
||||
let _ = require('lodash');
|
||||
let assert = require('assert');
|
||||
let moment = require('moment');
|
||||
// deps
|
||||
const uuidParse = require('uuid-parse');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const moment = require('moment');
|
||||
const iconvEncode = require('iconv-lite').encode;
|
||||
|
||||
module.exports = Message;
|
||||
|
||||
const ENIGMA_MESSAGE_UUID_NAMESPACE = uuid.parse('154506df-1df8-46b9-98f8-ebb5815baaf8');
|
||||
const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8');
|
||||
|
||||
function Message(options) {
|
||||
options = options || {};
|
||||
|
@ -64,11 +66,6 @@ function Message(options) {
|
|||
this.isPrivate = function() {
|
||||
return Message.isPrivateAreaTag(this.areaTag);
|
||||
};
|
||||
|
||||
this.getMessageTimestampString = function(ts) {
|
||||
ts = ts || moment();
|
||||
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
};
|
||||
}
|
||||
|
||||
Message.WellKnownAreaTags = {
|
||||
|
@ -138,7 +135,7 @@ Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) {
|
|||
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 ] )));
|
||||
return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] )));
|
||||
};
|
||||
|
||||
Message.getMessageIdByUuid = function(uuid, cb) {
|
||||
|
@ -374,7 +371,7 @@ Message.prototype.persist = function(cb) {
|
|||
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(msgTimestamp) ],
|
||||
[ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ],
|
||||
function inserted(err) { // use non-arrow function for 'this' scope
|
||||
if(!err) {
|
||||
self.messageId = this.lastID;
|
||||
|
|
|
@ -7,6 +7,7 @@ const Config = require('./config.js').config;
|
|||
const Message = require('./message.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const msgNetRecord = require('./msg_network.js').recordMessage;
|
||||
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -32,34 +33,11 @@ exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
|
|||
exports.persistMessage = persistMessage;
|
||||
exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent;
|
||||
|
||||
//
|
||||
// Method for sorting Message areas and conferences
|
||||
// If the sort key is present and is a number, sort in numerical order;
|
||||
// Otherwise, use a locale comparison on the sort key or name as a fallback
|
||||
//
|
||||
function sortAreasOrConfs(areasOrConfs, type) {
|
||||
let entryA;
|
||||
let entryB;
|
||||
|
||||
areasOrConfs.sort((a, b) => {
|
||||
entryA = a[type];
|
||||
entryB = b[type];
|
||||
|
||||
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
|
||||
return entryA.sort - entryB.sort;
|
||||
} else {
|
||||
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
|
||||
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
|
||||
return keyA.localeCompare(keyB);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAvailableMessageConferences(client, options) {
|
||||
options = options || { includeSystemInternal : false };
|
||||
|
||||
// perform ACS check per conf & omit system_internal if desired
|
||||
return _.omit(Config.messageConferences, (conf, confTag) => {
|
||||
return _.omitBy(Config.messageConferences, (conf, confTag) => {
|
||||
if(!options.includeSystemInternal && 'system_internal' === confTag) {
|
||||
return true;
|
||||
}
|
||||
|
@ -95,7 +73,7 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
|
|||
return areas;
|
||||
} else {
|
||||
// perform ACS check per area
|
||||
return _.omit(areas, area => {
|
||||
return _.omitBy(areas, area => {
|
||||
return !options.client.acs.hasMessageAreaRead(area);
|
||||
});
|
||||
}
|
||||
|
@ -269,7 +247,7 @@ function changeMessageConference(client, confTag, cb) {
|
|||
}
|
||||
|
||||
function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
||||
options = options || {};
|
||||
options = options || {}; // :TODO: this is currently pointless... cb is required...
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
|
@ -305,7 +283,7 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
|||
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area');
|
||||
}
|
||||
|
||||
cb(err);
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,54 +3,29 @@
|
|||
|
||||
const messageArea = require('../core/message_area.js');
|
||||
|
||||
// deps
|
||||
const assert = require('assert');
|
||||
|
||||
//
|
||||
// A simple mixin for View Controller management
|
||||
//
|
||||
exports.ViewControllerManagement = function() {
|
||||
this.initViewControllers = function() {
|
||||
this.viewControllers = {};
|
||||
};
|
||||
exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
|
||||
|
||||
this.detachViewControllers = function() {
|
||||
var self = this;
|
||||
Object.keys(this.viewControllers).forEach(function vc(name) {
|
||||
self.viewControllers[name].detachClientEvents();
|
||||
});
|
||||
};
|
||||
|
||||
this.addViewController = function(name, vc) {
|
||||
assert(this.viewControllers, 'initViewControllers() has not been called!');
|
||||
assert(!this.viewControllers[name], 'ViewController by the name of \'' + name + '\' already exists!');
|
||||
|
||||
this.viewControllers[name] = vc;
|
||||
return vc;
|
||||
};
|
||||
};
|
||||
|
||||
exports.MessageAreaConfTempSwitcher = function() {
|
||||
|
||||
this.tempMessageConfAndAreaSwitch = function(messageAreaTag) {
|
||||
tempMessageConfAndAreaSwitch(messageAreaTag) {
|
||||
messageAreaTag = messageAreaTag || this.messageAreaTag;
|
||||
if(!messageAreaTag) {
|
||||
return; // nothing to do!
|
||||
}
|
||||
|
||||
this.prevMessageConfAndArea = {
|
||||
confTag : this.client.user.properties.message_conf_tag,
|
||||
areaTag : this.client.user.properties.message_area_tag,
|
||||
};
|
||||
|
||||
if(!messageArea.tempChangeMessageConfAndArea(this.client, this.messageAreaTag)) {
|
||||
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.tempMessageConfAndAreaRestore = function() {
|
||||
tempMessageConfAndAreaRestore() {
|
||||
if(this.prevMessageConfAndArea) {
|
||||
this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag;
|
||||
this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -59,9 +59,6 @@ function loadModuleEx(options, cb) {
|
|||
return cb(new Error('Invalid or missing "getModule" method for module!'));
|
||||
}
|
||||
|
||||
// Ref configuration, if any, for convience to the module
|
||||
mod.runtime = { config : modConfig };
|
||||
|
||||
return cb(null, mod);
|
||||
}
|
||||
|
||||
|
|
|
@ -115,8 +115,10 @@ function MultiLineEditTextView(options) {
|
|||
|
||||
if ('preview' === this.mode) {
|
||||
this.autoScroll = options.autoScroll || true;
|
||||
this.tabSwitchesView = true;
|
||||
} else {
|
||||
this.autoScroll = options.autoScroll || false;
|
||||
this.tabSwitchesView = options.tabSwitchesView || false;
|
||||
}
|
||||
//
|
||||
// cursorPos represents zero-based row, col positions
|
||||
|
@ -284,7 +286,7 @@ function MultiLineEditTextView(options) {
|
|||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
this.getContiguousText = function(startIndex, endIndex, includeEol) {
|
||||
var lines = self.getTextLines(startIndex, endIndex);
|
||||
|
@ -532,7 +534,7 @@ function MultiLineEditTextView(options) {
|
|||
// before and and after column
|
||||
//
|
||||
// :TODO: Need to clean this string (e.g. collapse tabs)
|
||||
text = self.textLines
|
||||
text = self.textLines;
|
||||
|
||||
// :TODO: Remove original line @ index
|
||||
}
|
||||
|
@ -544,15 +546,15 @@ function MultiLineEditTextView(options) {
|
|||
.replace(/\b/g, '')
|
||||
.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
|
||||
|
||||
var wrapped;
|
||||
let wrapped;
|
||||
|
||||
for(var i = 0; i < text.length; ++i) {
|
||||
for(let i = 0; i < text.length; ++i) {
|
||||
wrapped = self.wordWrapSingleLine(
|
||||
text[i], // input
|
||||
'expand', // tabHandling
|
||||
self.dimens.width).wrapped;
|
||||
|
||||
for(var j = 0; j < wrapped.length - 1; ++j) {
|
||||
for(let j = 0; j < wrapped.length - 1; ++j) {
|
||||
self.textLines.splice(index++, 0, { text : wrapped[j] } );
|
||||
}
|
||||
self.textLines.splice(index++, 0, { text : wrapped[wrapped.length - 1], eol : true } );
|
||||
|
@ -1023,14 +1025,26 @@ MultiLineEditTextView.prototype.getData = function() {
|
|||
|
||||
MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
|
||||
switch(propName) {
|
||||
case 'mode' : this.mode = value; break;
|
||||
case 'mode' :
|
||||
this.mode = value;
|
||||
if('preview' === value && !this.specialKeyMap.next) {
|
||||
this.specialKeyMap.next = [ 'tab' ];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'autoScroll' : this.autoScroll = value; break;
|
||||
|
||||
case 'tabSwitchesView' :
|
||||
this.tabSwitchesView = value;
|
||||
this.specialKeyMap.next = this.specialKeyMap.next || [];
|
||||
this.specialKeyMap.next.push('tab');
|
||||
break;
|
||||
}
|
||||
|
||||
MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||
};
|
||||
|
||||
var HANDLED_SPECIAL_KEYS = [
|
||||
const HANDLED_SPECIAL_KEYS = [
|
||||
'up', 'down', 'left', 'right',
|
||||
'home', 'end',
|
||||
'page up', 'page down',
|
||||
|
@ -1041,13 +1055,13 @@ var HANDLED_SPECIAL_KEYS = [
|
|||
'delete line',
|
||||
];
|
||||
|
||||
var PREVIEW_MODE_KEYS = [
|
||||
const PREVIEW_MODE_KEYS = [
|
||||
'up', 'down', 'page up', 'page down'
|
||||
];
|
||||
|
||||
MultiLineEditTextView.prototype.onKeyPress = function(ch, key) {
|
||||
var self = this;
|
||||
var handled;
|
||||
const self = this;
|
||||
let handled;
|
||||
|
||||
if(key) {
|
||||
HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) {
|
||||
|
@ -1057,9 +1071,11 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) {
|
|||
return;
|
||||
}
|
||||
|
||||
if('tab' !== key.name || !self.tabSwitchesView) {
|
||||
self[_.camelCase('keyPress ' + specialKey)]();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@ exports.moduleInfo = {
|
|||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = NewScanModule;
|
||||
|
||||
/*
|
||||
* :TODO:
|
||||
* * User configurable new scan: Area selection (avail from messages area) (sep module)
|
||||
|
@ -27,16 +25,14 @@ exports.getModule = NewScanModule;
|
|||
|
||||
*/
|
||||
|
||||
var MciCodeIds = {
|
||||
const MciCodeIds = {
|
||||
ScanStatusLabel : 1, // TL1
|
||||
ScanStatusList : 2, // VM2 (appends)
|
||||
};
|
||||
|
||||
function NewScanModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
var self = this;
|
||||
var config = this.menuConfig.config;
|
||||
exports.getModule = class NewScanModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.newScanFullExit = _.has(options, 'lastMenuResult.fullExit') ? options.lastMenuResult.fullExit : false;
|
||||
|
||||
|
@ -44,31 +40,30 @@ function NewScanModule(options) {
|
|||
this.currentScanAux = {};
|
||||
|
||||
// :TODO: Make this conf/area specific:
|
||||
const config = this.menuConfig.config;
|
||||
this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...';
|
||||
this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new';
|
||||
this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found';
|
||||
this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan';
|
||||
|
||||
this.updateScanStatus = function(statusText) {
|
||||
var vc = self.viewControllers.allViews;
|
||||
|
||||
var view = vc.getView(MciCodeIds.ScanStatusLabel);
|
||||
if(view) {
|
||||
view.setText(statusText);
|
||||
}
|
||||
|
||||
updateScanStatus(statusText) {
|
||||
this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText);
|
||||
|
||||
/*
|
||||
view = vc.getView(MciCodeIds.ScanStatusList);
|
||||
// :TODO: MenuView needs appendItem()
|
||||
if(view) {
|
||||
}
|
||||
};
|
||||
*/
|
||||
}
|
||||
|
||||
this.newScanMessageConference = function(cb) {
|
||||
newScanMessageConference(cb) {
|
||||
// lazy init
|
||||
if(!self.sortedMessageConfs) {
|
||||
if(!this.sortedMessageConfs) {
|
||||
const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc.
|
||||
|
||||
self.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(self.client, getAvailOpts), (v, k) => {
|
||||
this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => {
|
||||
return {
|
||||
confTag : k,
|
||||
conf : v,
|
||||
|
@ -80,19 +75,20 @@ function NewScanModule(options) {
|
|||
// always come first such that we display private mails/etc. before
|
||||
// other conferences & areas
|
||||
//
|
||||
self.sortedMessageConfs.sort((a, b) => {
|
||||
this.sortedMessageConfs.sort((a, b) => {
|
||||
if('system_internal' === a.confTag) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.conf.name.localeCompare(b.conf.name);
|
||||
return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } );
|
||||
}
|
||||
});
|
||||
|
||||
self.currentScanAux.conf = self.currentScanAux.conf || 0;
|
||||
self.currentScanAux.area = self.currentScanAux.area || 0;
|
||||
this.currentScanAux.conf = this.currentScanAux.conf || 0;
|
||||
this.currentScanAux.area = this.currentScanAux.area || 0;
|
||||
}
|
||||
|
||||
const currentConf = self.sortedMessageConfs[self.currentScanAux.conf];
|
||||
const currentConf = this.sortedMessageConfs[this.currentScanAux.conf];
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -113,19 +109,22 @@ function NewScanModule(options) {
|
|||
});
|
||||
}
|
||||
],
|
||||
cb
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.newScanMessageArea = function(conf, cb) {
|
||||
newScanMessageArea(conf, cb) {
|
||||
// :TODO: it would be nice to cache this - must be done by conf!
|
||||
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : self.client } );
|
||||
const currentArea = sortedAreas[self.currentScanAux.area];
|
||||
const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } );
|
||||
const currentArea = sortedAreas[this.currentScanAux.area];
|
||||
|
||||
//
|
||||
// Scan and update index until we find something. If results are found,
|
||||
// we'll goto the list module & show them.
|
||||
//
|
||||
const self = this;
|
||||
async.waterfall(
|
||||
[
|
||||
function checkAndUpdateIndex(callback) {
|
||||
|
@ -165,47 +164,45 @@ function NewScanModule(options) {
|
|||
}
|
||||
};
|
||||
|
||||
return self.gotoMenu(config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
|
||||
return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts);
|
||||
}
|
||||
],
|
||||
cb // no more areas
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
require('util').inherits(NewScanModule, MenuModule);
|
||||
|
||||
NewScanModule.prototype.getSaveState = function() {
|
||||
getSaveState() {
|
||||
return {
|
||||
currentStep : this.currentStep,
|
||||
currentScanAux : this.currentScanAux,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
NewScanModule.prototype.restoreSavedState = function(savedState) {
|
||||
restoreSavedState(savedState) {
|
||||
this.currentStep = savedState.currentStep;
|
||||
this.currentScanAux = savedState.currentScanAux;
|
||||
};
|
||||
|
||||
NewScanModule.prototype.mciReady = function(mciData, cb) {
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
if(this.newScanFullExit) {
|
||||
// user has canceled the entire scan @ message list view
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
// :TODO: display scan step/etc.
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
NewScanModule.super_.prototype.mciReady.call(self, mciData, callback);
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
|
@ -227,11 +224,13 @@ NewScanModule.prototype.mciReady = function(mciData, cb) {
|
|||
}
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.error( { error : err.toString() }, 'Error during new scan');
|
||||
}
|
||||
cb(err);
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/* jslint node: true */
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const resolvePath = require('../misc_util.js').resolvePath;
|
||||
|
||||
const config = require('../../core/config.js');
|
||||
const db = require('../../core/database.js');
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
|
||||
exports.printUsageAndSetExitCode = printUsageAndSetExitCode;
|
||||
exports.getDefaultConfigPath = getDefaultConfigPath;
|
||||
exports.initConfigAndDatabases = initConfigAndDatabases;
|
||||
exports.getAreaAndStorage = getAreaAndStorage;
|
||||
|
||||
const exitCodes = exports.ExitCodes = {
|
||||
SUCCESS : 0,
|
||||
ERROR : -1,
|
||||
BAD_COMMAND : -2,
|
||||
BAD_ARGS : -3,
|
||||
};
|
||||
|
||||
const argv = exports.argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
function printUsageAndSetExitCode(errMsg, exitCode) {
|
||||
if(_.isUndefined(exitCode)) {
|
||||
exitCode = exitCodes.ERROR;
|
||||
}
|
||||
|
||||
process.exitCode = exitCode;
|
||||
|
||||
if(errMsg) {
|
||||
console.error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultConfigPath() {
|
||||
return resolvePath('~/.config/enigma-bbs/config.hjson');
|
||||
}
|
||||
|
||||
function initConfig(cb) {
|
||||
const configPath = argv.config ? argv.config : config.getDefaultPath();
|
||||
|
||||
config.init(configPath, cb);
|
||||
}
|
||||
|
||||
function initConfigAndDatabases(cb) {
|
||||
async.series(
|
||||
[
|
||||
function init(callback) {
|
||||
initConfig(callback);
|
||||
},
|
||||
function initDb(callback) {
|
||||
db.initializeDatabases(callback);
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getAreaAndStorage(tags) {
|
||||
return tags.map(tag => {
|
||||
const parts = tag.split('@');
|
||||
const entry = {
|
||||
areaTag : parts[0],
|
||||
};
|
||||
if(parts[1]) {
|
||||
entry.storageTag = parts[1];
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
/* jslint node: true */
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const resolvePath = require('../../core/misc_util.js').resolvePath;
|
||||
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
||||
const argv = require('./oputil_common.js').argv;
|
||||
const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath;
|
||||
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const inq = require('inquirer');
|
||||
const mkdirsSync = require('fs-extra').mkdirsSync;
|
||||
const fs = require('fs');
|
||||
const hjson = require('hjson');
|
||||
const paths = require('path');
|
||||
|
||||
exports.handleConfigCommand = handleConfigCommand;
|
||||
|
||||
|
||||
function getAnswers(questions, cb) {
|
||||
inq.prompt(questions).then( answers => {
|
||||
return cb(answers);
|
||||
});
|
||||
}
|
||||
|
||||
const QUESTIONS = {
|
||||
Intro : [
|
||||
{
|
||||
name : 'createNewConfig',
|
||||
message : 'Create a new configuration?',
|
||||
type : 'confirm',
|
||||
default : false,
|
||||
},
|
||||
{
|
||||
name : 'configPath',
|
||||
message : 'Configuration path:',
|
||||
default : argv.config ? argv.config : getDefaultConfigPath(),
|
||||
when : answers => answers.createNewConfig
|
||||
},
|
||||
],
|
||||
|
||||
OverwriteConfig : [
|
||||
{
|
||||
name : 'overwriteConfig',
|
||||
message : 'Config file exists. Overwrite?',
|
||||
type : 'confirm',
|
||||
default : false,
|
||||
}
|
||||
],
|
||||
|
||||
Basic : [
|
||||
{
|
||||
name : 'boardName',
|
||||
message : 'BBS name:',
|
||||
default : 'New ENiGMA½ BBS',
|
||||
},
|
||||
],
|
||||
|
||||
Misc : [
|
||||
{
|
||||
name : 'loggingLevel',
|
||||
message : 'Logging level:',
|
||||
type : 'list',
|
||||
choices : [ 'Error', 'Warn', 'Info', 'Debug', 'Trace' ],
|
||||
default : 2,
|
||||
filter : s => s.toLowerCase(),
|
||||
},
|
||||
{
|
||||
name : 'sevenZipExe',
|
||||
message : '7-Zip executable:',
|
||||
type : 'list',
|
||||
choices : [ '7z', '7za', 'None' ]
|
||||
}
|
||||
],
|
||||
|
||||
MessageConfAndArea : [
|
||||
{
|
||||
name : 'msgConfName',
|
||||
message : 'First message conference:',
|
||||
default : 'Local',
|
||||
},
|
||||
{
|
||||
name : 'msgConfDesc',
|
||||
message : 'Conference description:',
|
||||
default : 'Local Areas',
|
||||
},
|
||||
{
|
||||
name : 'msgAreaName',
|
||||
message : 'First area in message conference:',
|
||||
default : 'General',
|
||||
},
|
||||
{
|
||||
name : 'msgAreaDesc',
|
||||
message : 'Area description:',
|
||||
default : 'General chit-chat',
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function makeMsgConfAreaName(s) {
|
||||
return s.toLowerCase().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
function askNewConfigQuestions(cb) {
|
||||
|
||||
const ui = new inq.ui.BottomBar();
|
||||
|
||||
let configPath;
|
||||
let config;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function intro(callback) {
|
||||
getAnswers(QUESTIONS.Intro, answers => {
|
||||
if(!answers.createNewConfig) {
|
||||
return callback('exit');
|
||||
}
|
||||
|
||||
// adjust for ~ and the like
|
||||
configPath = resolvePath(answers.configPath);
|
||||
|
||||
const configDir = paths.dirname(configPath);
|
||||
mkdirsSync(configDir);
|
||||
|
||||
//
|
||||
// Check if the file exists and can be written to
|
||||
//
|
||||
fs.access(configPath, fs.F_OK | fs.W_OK, err => {
|
||||
if(err) {
|
||||
if('EACCES' === err.code) {
|
||||
ui.log.write(`${configPath} cannot be written to`);
|
||||
callback('exit');
|
||||
} else if('ENOENT' === err.code) {
|
||||
callback(null, false);
|
||||
}
|
||||
} else {
|
||||
callback(null, true); // exists + writable
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
function promptOverwrite(needPrompt, callback) {
|
||||
if(needPrompt) {
|
||||
getAnswers(QUESTIONS.OverwriteConfig, answers => {
|
||||
callback(answers.overwriteConfig ? null : 'exit');
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
function basic(callback) {
|
||||
getAnswers(QUESTIONS.Basic, answers => {
|
||||
config = {
|
||||
general : {
|
||||
boardName : answers.boardName,
|
||||
},
|
||||
};
|
||||
|
||||
callback(null);
|
||||
});
|
||||
},
|
||||
function msgConfAndArea(callback) {
|
||||
getAnswers(QUESTIONS.MessageConfAndArea, answers => {
|
||||
config.messageConferences = {};
|
||||
|
||||
const confName = makeMsgConfAreaName(answers.msgConfName);
|
||||
const areaName = makeMsgConfAreaName(answers.msgAreaName);
|
||||
|
||||
config.messageConferences[confName] = {
|
||||
name : answers.msgConfName,
|
||||
desc : answers.msgConfDesc,
|
||||
sort : 1,
|
||||
default : true,
|
||||
};
|
||||
|
||||
config.messageConferences.another_sample_conf = {
|
||||
name : 'Another Sample Conference',
|
||||
desc : 'Another conference example. Change me!',
|
||||
sort : 2,
|
||||
};
|
||||
|
||||
config.messageConferences[confName].areas = {};
|
||||
config.messageConferences[confName].areas[areaName] = {
|
||||
name : answers.msgAreaName,
|
||||
desc : answers.msgAreaDesc,
|
||||
sort : 1,
|
||||
default : true,
|
||||
};
|
||||
|
||||
config.messageConferences.another_sample_conf = {
|
||||
name : 'Another Sample Conference',
|
||||
desc : 'Another conf sample. Change me!',
|
||||
|
||||
areas : {
|
||||
another_sample_area : {
|
||||
name : 'Another Sample Area',
|
||||
desc : 'Another area example. Change me!',
|
||||
sort : 2
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
callback(null);
|
||||
});
|
||||
},
|
||||
function misc(callback) {
|
||||
getAnswers(QUESTIONS.Misc, answers => {
|
||||
if('None' !== answers.sevenZipExe) {
|
||||
config.archivers = {
|
||||
zip : {
|
||||
compressCmd : answers.sevenZipExe,
|
||||
decompressCmd : answers.sevenZipExe,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
config.logging = {
|
||||
level : answers.loggingLevel,
|
||||
};
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
cb(err, configPath, config);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleConfigCommand() {
|
||||
if(true === argv.help) {
|
||||
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||
}
|
||||
|
||||
if(argv.new) {
|
||||
askNewConfigQuestions( (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 {
|
||||
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
/* jslint node: true */
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
||||
const argv = require('./oputil_common.js').argv;
|
||||
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
|
||||
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||
const getAreaAndStorage = require('./oputil_common.js').getAreaAndStorage;
|
||||
|
||||
|
||||
const async = require('async');
|
||||
const fs = require('fs');
|
||||
const paths = require('path');
|
||||
|
||||
exports.handleFileBaseCommand = handleFileBaseCommand;
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
|
||||
Global options:
|
||||
--yes: assume yes
|
||||
--no-prompt: try to avoid user input
|
||||
|
||||
Prompt for import and description before scan
|
||||
* Only after finding duplicate-by-path
|
||||
* Default to filename -> desc if auto import
|
||||
|
||||
*/
|
||||
|
||||
let fileArea; // required during init
|
||||
|
||||
function scanFileAreaForChanges(areaInfo, options, cb) {
|
||||
|
||||
const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => {
|
||||
return options.areaAndStorageInfo.find(asi => {
|
||||
return !asi.storageTag || sl.storageTag === asi.storageTag;
|
||||
});
|
||||
});
|
||||
|
||||
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
|
||||
async.series(
|
||||
[
|
||||
function scanPhysFiles(callback) {
|
||||
const physDir = storageLoc.dir;
|
||||
|
||||
fs.readdir(physDir, (err, files) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.eachSeries(files, (fileName, nextFile) => {
|
||||
const fullPath = paths.join(physDir, fileName);
|
||||
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!
|
||||
return nextFile(null); // always try next file
|
||||
}
|
||||
|
||||
if(!stats.isFile()) {
|
||||
return nextFile(null);
|
||||
}
|
||||
|
||||
process.stdout.write(`* Scanning ${fullPath}... `);
|
||||
|
||||
fileArea.scanFile(
|
||||
fullPath,
|
||||
{
|
||||
areaTag : areaInfo.areaTag,
|
||||
storageTag : storageLoc.storageTag
|
||||
},
|
||||
(err, fileEntry, dupeEntries) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!!!
|
||||
console.info(`Error: ${err.message}`);
|
||||
return nextFile(null); // try next anyway
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(dupeEntries.length > 0) {
|
||||
// :TODO: Handle duplidates -- what to do here???
|
||||
console.info('Dupe');
|
||||
return nextFile(null);
|
||||
} else {
|
||||
console.info('Done!');
|
||||
if(Array.isArray(options.tags)) {
|
||||
options.tags.forEach(tag => {
|
||||
fileEntry.hashTags.add(tag);
|
||||
});
|
||||
}
|
||||
|
||||
fileEntry.persist( err => {
|
||||
return nextFile(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
function scanDbEntries(callback) {
|
||||
// :TODO: Look @ db entries for area that were *not* processed above
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return nextLocation(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function scanFileAreas() {
|
||||
const options = {};
|
||||
|
||||
const tags = argv.tags;
|
||||
if(tags) {
|
||||
options.tags = tags.split(',');
|
||||
}
|
||||
|
||||
options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2));
|
||||
|
||||
async.series(
|
||||
[
|
||||
function init(callback) {
|
||||
return initConfigAndDatabases(callback);
|
||||
},
|
||||
function scanAreas(callback) {
|
||||
fileArea = require('../../core/file_base_area.js');
|
||||
|
||||
async.eachSeries(options.areaAndStorageInfo, (areaAndStorage, nextAreaTag) => {
|
||||
const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
|
||||
if(!areaInfo) {
|
||||
return nextAreaTag(new Error(`Invalid file base area tag: ${areaAndStorage.areaTag}`));
|
||||
}
|
||||
|
||||
console.info(`Processing area "${areaInfo.name}":`);
|
||||
|
||||
scanFileAreaForChanges(areaInfo, options, err => {
|
||||
return callback(err);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
process.exitCode = ExitCodes.ERROR;
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleFileBaseCommand() {
|
||||
if(true === argv.help) {
|
||||
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
|
||||
}
|
||||
|
||||
const action = argv._[1];
|
||||
|
||||
switch(action) {
|
||||
case 'scan' : return scanFileAreas();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/* jslint node: true */
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath;
|
||||
|
||||
exports.getHelpFor = getHelpFor;
|
||||
|
||||
const usageHelp = exports.USAGE_HELP = {
|
||||
General :
|
||||
`usage: optutil.js [--version] [--help]
|
||||
<command> [<args>]
|
||||
|
||||
global args:
|
||||
--config PATH : specify config path (${getDefaultConfigPath()})
|
||||
|
||||
where <command> is one of:
|
||||
user : user utilities
|
||||
config : config file management
|
||||
file-base
|
||||
fb : file base management
|
||||
|
||||
`,
|
||||
User :
|
||||
`usage: optutil.js user --user USERNAME <args>
|
||||
|
||||
valid args:
|
||||
--user USERNAME : specify username for further actions
|
||||
--password PASS : set new password
|
||||
--delete : delete user
|
||||
--activate : activate user
|
||||
--deactivate : deactivate user
|
||||
`,
|
||||
|
||||
Config :
|
||||
`usage: optutil.js config <args>
|
||||
|
||||
valid args:
|
||||
--new : generate a new/initial configuration
|
||||
`,
|
||||
FileBase :
|
||||
`usage: oputil.js file-base <action> [<args>] [<action_specific>]
|
||||
|
||||
where <action> is one of:
|
||||
scan <args> AREA_TAG : (re)scan area specified by AREA_TAG for new files
|
||||
multiple area tags can be specified in form of AREA_TAG1 AREA_TAG2 ...
|
||||
|
||||
valid scan <args>:
|
||||
--tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries
|
||||
`
|
||||
};
|
||||
|
||||
function getHelpFor(command) {
|
||||
return usageHelp[command];
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* jslint node: true */
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
||||
const argv = require('./oputil_common.js').argv;
|
||||
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
||||
const handleUserCommand = require('./oputil_user.js').handleUserCommand;
|
||||
const handleFileBaseCommand = require('./oputil_file_base.js').handleFileBaseCommand;
|
||||
const handleConfigCommand = require('./oputil_config.js').handleConfigCommand;
|
||||
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||
|
||||
|
||||
module.exports = function() {
|
||||
|
||||
process.exitCode = ExitCodes.SUCCESS;
|
||||
|
||||
if(true === argv.version) {
|
||||
return console.info(require('../package.json').version);
|
||||
}
|
||||
|
||||
if(0 === argv._.length ||
|
||||
'help' === argv._[0])
|
||||
{
|
||||
printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS);
|
||||
}
|
||||
|
||||
switch(argv._[0]) {
|
||||
case 'user' :
|
||||
handleUserCommand();
|
||||
break;
|
||||
|
||||
case 'config' :
|
||||
handleConfigCommand();
|
||||
break;
|
||||
|
||||
case 'file-base' :
|
||||
case 'fb' :
|
||||
handleFileBaseCommand();
|
||||
break;
|
||||
|
||||
default:
|
||||
return printUsageAndSetExitCode('', ExitCodes.BAD_COMMAND);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
/* jslint node: true */
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
||||
const argv = require('./oputil_common.js').argv;
|
||||
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
|
||||
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.handleUserCommand = handleUserCommand;
|
||||
|
||||
function handleUserCommand() {
|
||||
if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) {
|
||||
return printUsageAndSetExitCode('User', ExitCodes.ERROR);
|
||||
}
|
||||
|
||||
if(_.isString(argv.password)) {
|
||||
if(0 === argv.password.length) {
|
||||
process.exitCode = ExitCodes.BAD_ARGS;
|
||||
return console.error('Invalid password');
|
||||
}
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function init(callback) {
|
||||
initAndGetUser(argv.user, callback);
|
||||
},
|
||||
function setNewPass(user, callback) {
|
||||
user.setNewAuthCredentials(argv.password, function credsSet(err) {
|
||||
if(err) {
|
||||
process.exitCode = ExitCodes.ERROR;
|
||||
callback(new Error('Failed setting password'));
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
console.error(err.message);
|
||||
} else {
|
||||
console.info('Password set');
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if(argv.activate) {
|
||||
setAccountStatus(argv.user, true);
|
||||
} else if(argv.deactivate) {
|
||||
setAccountStatus(argv.user, false);
|
||||
}
|
||||
}
|
||||
|
||||
function getUser(userName, cb) {
|
||||
const user = require('./core/user.js');
|
||||
user.getUserIdAndName(argv.user, function userNameAndId(err, userId) {
|
||||
if(err) {
|
||||
process.exitCode = ExitCodes.BAD_ARGS;
|
||||
return cb(new Error('Failed to retrieve user'));
|
||||
} else {
|
||||
let u = new user.User();
|
||||
u.userId = userId;
|
||||
return cb(null, u);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initAndGetUser(userName, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function init(callback) {
|
||||
initConfigAndDatabases(callback);
|
||||
},
|
||||
function getUserObject(callback) {
|
||||
getUser(argv.user, (err, user) => {
|
||||
if(err) {
|
||||
process.exitCode = ExitCodes.BAD_ARGS;
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, user);
|
||||
});
|
||||
}
|
||||
],
|
||||
(err, user) => {
|
||||
return cb(err, user);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function setAccountStatus(userName, active) {
|
||||
async.waterfall(
|
||||
[
|
||||
function init(callback) {
|
||||
initAndGetUser(argv.user, callback);
|
||||
},
|
||||
function activateUser(user, callback) {
|
||||
const AccountStatus = require('./core/user.js').User.AccountStatus;
|
||||
user.persistProperty('account_status', active ? AccountStatus.active : AccountStatus.inactive, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
console.error(err.message);
|
||||
} else {
|
||||
console.info('User ' + ((true === active) ? 'activated' : 'deactivated'));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -8,6 +8,8 @@ const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
|
|||
const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag;
|
||||
const clientConnections = require('./client_connections.js');
|
||||
const StatLog = require('./stat_log.js');
|
||||
const FileBaseFilters = require('./file_base_filter.js');
|
||||
const formatByteSize = require('./string_util.js').formatByteSize;
|
||||
|
||||
// deps
|
||||
const packageJson = require('../package.json');
|
||||
|
@ -35,6 +37,17 @@ function setNextRandomRumor(cb) {
|
|||
});
|
||||
}
|
||||
|
||||
function getRatio(client, propA, propB) {
|
||||
const a = StatLog.getUserStatNum(client.user, propA);
|
||||
const b = StatLog.getUserStatNum(client.user, propB);
|
||||
const ratio = ~~((a / b) * 100);
|
||||
return `${ratio}%`;
|
||||
}
|
||||
|
||||
function userStatAsString(client, statName, defaultValue) {
|
||||
return (StatLog.getUserStat(client.user, statName) || defaultValue).toString();
|
||||
}
|
||||
|
||||
function getPredefinedMCIValue(client, code) {
|
||||
|
||||
if(!client || !code) {
|
||||
|
@ -67,32 +80,43 @@ function getPredefinedMCIValue(client, code) {
|
|||
UN : function userName() { return client.user.username; },
|
||||
UI : function userId() { return client.user.userId.toString(); },
|
||||
UG : function groups() { return _.values(client.user.groups).join(', '); },
|
||||
UR : function realName() { return client.user.properties.real_name; },
|
||||
LO : function location() { return client.user.properties.location; },
|
||||
UR : function realName() { return userStatAsString(client, 'real_name', ''); },
|
||||
LO : function location() { return userStatAsString(client, 'location', ''); },
|
||||
UA : function age() { return client.user.getAge().toString(); },
|
||||
UB : function birthdate() { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); },
|
||||
US : function sex() { return client.user.properties.sex; },
|
||||
UE : function emailAddres() { return client.user.properties.email_address; },
|
||||
UW : function webAddress() { return client.user.properties.web_address; },
|
||||
UF : function affils() { return client.user.properties.affiliation; },
|
||||
UT : function themeId() { return client.user.properties.theme_id; },
|
||||
UC : function loginCount() { return client.user.properties.login_count.toString(); },
|
||||
BD : function birthdate() { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY
|
||||
US : function sex() { return userStatAsString(client, 'sex', ''); },
|
||||
UE : function emailAddres() { return userStatAsString(client, 'email_address', ''); },
|
||||
UW : function webAddress() { return userStatAsString(client, 'web_address', ''); },
|
||||
UF : function affils() { return userStatAsString(client, 'affiliation', ''); },
|
||||
UT : function themeId() { return userStatAsString(client, 'theme_id', ''); },
|
||||
UC : function loginCount() { return userStatAsString(client, 'login_count', 0); },
|
||||
ND : function connectedNode() { return client.node.toString(); },
|
||||
IP : function clientIpAddress() { return client.address().address; },
|
||||
ST : function serverName() { return client.session.serverName; },
|
||||
FN : function activeFileBaseFilterName() {
|
||||
const activeFilter = FileBaseFilters.getActiveFilter(client);
|
||||
return activeFilter ? activeFilter.name : '';
|
||||
},
|
||||
DN : function userNumDownloads() { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2
|
||||
DK : function userByteDownload() { // Obv/2 uses DK=downloaded Kbytes
|
||||
const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes');
|
||||
return formatByteSize(byteSize, true); // true=withAbbr
|
||||
},
|
||||
UP : function userNumUploads() { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2
|
||||
UK : function userByteUpload() { // Obv/2 uses UK=uploaded Kbytes
|
||||
const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes');
|
||||
return formatByteSize(byteSize, true); // true=withAbbr
|
||||
},
|
||||
NR : function userUpDownRatio() { // Obv/2
|
||||
return getRatio(client, 'ul_total_count', 'dl_total_count');
|
||||
},
|
||||
KR : function userUpDownByteRatio() { // Obv/2 uses KR=upload/download Kbyte ratio
|
||||
return getRatio(client, 'ul_total_bytes', 'dl_total_bytes');
|
||||
},
|
||||
|
||||
MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
|
||||
CS : function currentStatus() { return client.currentStatus; },
|
||||
PS : function userPostCount() {
|
||||
const postCount = client.user.properties.post_count || 0;
|
||||
return postCount.toString();
|
||||
},
|
||||
PC : function userPostCallRatio() {
|
||||
const postCount = client.user.properties.post_count || 0;
|
||||
const callCount = client.user.properties.login_count;
|
||||
const ratio = ~~((postCount / callCount) * 100);
|
||||
return `${ratio}%`;
|
||||
},
|
||||
PS : function userPostCount() { return userStatAsString(client, 'post_count', 0); },
|
||||
PC : function userPostCallRatio() { return getRatio(client, 'post_count', 'login_count'); },
|
||||
|
||||
MD : function currentMenuDescription() {
|
||||
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
|
||||
|
@ -163,6 +187,26 @@ function getPredefinedMCIValue(client, code) {
|
|||
return StatLog.getSystemStat('random_rumor');
|
||||
},
|
||||
|
||||
//
|
||||
// System File Base, Up/Download Info
|
||||
//
|
||||
// :TODO: DD - Today's # of downloads (iNiQUiTY)
|
||||
//
|
||||
// :TODO: System stat log for total ul/dl, total ul/dl bytes
|
||||
|
||||
// :TODO: PT - Messages posted *today* (Obv/2)
|
||||
// -> Include FTN/etc.
|
||||
// :TODO: NT - New users today (Obv/2)
|
||||
// :TODO: CT - Calls *today* (Obv/2)
|
||||
// :TODO: TF - Total files on the system (Obv/2)
|
||||
// :TODO: FT - Files uploaded/added *today* (Obv/2)
|
||||
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
|
||||
// :TODO: TP - total message/posts on the system (Obv/2)
|
||||
// -> Include FTN/etc.
|
||||
// :TODO: LC - name of last caller to system (Obv/2)
|
||||
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
|
||||
|
||||
|
||||
//
|
||||
// Special handling for XY
|
||||
//
|
||||
|
|
|
@ -50,20 +50,17 @@ function readSAUCE(data, cb) {
|
|||
.tap(function onVars(vars) {
|
||||
|
||||
if(!SAUCE_ID.equals(vars.id)) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
return cb(new Error('No SAUCE record present'));
|
||||
}
|
||||
|
||||
var ver = iconv.decode(vars.version, 'cp437');
|
||||
|
||||
if('00' !== ver) {
|
||||
cb(new Error('Unsupported SAUCE version: ' + ver));
|
||||
return;
|
||||
return cb(new Error('Unsupported SAUCE version: ' + ver));
|
||||
}
|
||||
|
||||
if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) {
|
||||
cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType));
|
||||
return;
|
||||
return cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType));
|
||||
}
|
||||
|
||||
var sauce = {
|
||||
|
|
|
@ -18,12 +18,12 @@ const paths = require('path');
|
|||
const async = require('async');
|
||||
const fs = require('fs');
|
||||
const later = require('later');
|
||||
const temp = require('temp').track(); // track() cleans up temp dir/files for us
|
||||
const temptmp = require('temptmp').createTrackedSession('ftn_bso');
|
||||
const assert = require('assert');
|
||||
const gaze = require('gaze');
|
||||
const fse = require('fs-extra');
|
||||
const iconv = require('iconv-lite');
|
||||
const uuid = require('node-uuid');
|
||||
const uuidV4 = require('uuid/v4');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'FTN BSO',
|
||||
|
@ -37,6 +37,8 @@ exports.moduleInfo = {
|
|||
* Support NetMail
|
||||
* NetMail needs explicit isNetMail() check
|
||||
* NetMail filename / location / etc. is still unknown - need to post on groups & get real answers
|
||||
* Validate packet passwords!!!!
|
||||
=> secure vs insecure landing areas
|
||||
|
||||
*/
|
||||
|
||||
|
@ -49,9 +51,7 @@ function FTNMessageScanTossModule() {
|
|||
|
||||
let self = this;
|
||||
|
||||
this.archUtil = new ArchiveUtil();
|
||||
this.archUtil.init();
|
||||
|
||||
this.archUtil = ArchiveUtil.getInstance();
|
||||
|
||||
if(_.has(Config, 'scannerTossers.ftn_bso')) {
|
||||
this.moduleConfig = Config.scannerTossers.ftn_bso;
|
||||
|
@ -871,7 +871,7 @@ function FTNMessageScanTossModule() {
|
|||
//
|
||||
if(Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) {
|
||||
// just generate a UUID & therefor always allow for dupes
|
||||
message.uuid = uuid.v1();
|
||||
message.uuid = uuidV4();
|
||||
}
|
||||
|
||||
callback(null);
|
||||
|
@ -1136,6 +1136,8 @@ function FTNMessageScanTossModule() {
|
|||
return nextFile(); // unknown archive type
|
||||
}
|
||||
|
||||
Log.debug( { bundleFile : bundleFile }, 'Processing bundle' );
|
||||
|
||||
self.archUtil.extractTo(
|
||||
bundleFile.path,
|
||||
self.importTempDir,
|
||||
|
@ -1188,14 +1190,14 @@ function FTNMessageScanTossModule() {
|
|||
};
|
||||
|
||||
this.createTempDirectories = function(cb) {
|
||||
temp.mkdir('enigftnexport-', (err, tempDir) => {
|
||||
temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
self.exportTempDir = tempDir;
|
||||
|
||||
temp.mkdir('enigftnimport-', (err, tempDir) => {
|
||||
temptmp.mkdir( { prefix : 'enigftnimport-' }, (err, tempDir) => {
|
||||
self.importTempDir = tempDir;
|
||||
|
||||
cb(err);
|
||||
|
@ -1325,17 +1327,20 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) {
|
|||
//
|
||||
// Clean up temp dir/files we created
|
||||
//
|
||||
temp.cleanup((err, stats) => {
|
||||
const fullStats = Object.assign(stats, { exportTemp : this.exportTempDir, importTemp : this.importTempDir } );
|
||||
temptmp.cleanup( paths => {
|
||||
const fullStats = {
|
||||
exportDir : this.exportTempDir,
|
||||
importTemp : this.importTempDir,
|
||||
paths : paths,
|
||||
sessionId : temptmp.sessionId,
|
||||
};
|
||||
|
||||
if(err) {
|
||||
Log.warn(fullStats, 'Failed cleaning up temporary directories!');
|
||||
} else {
|
||||
Log.trace(fullStats, 'Temporary directories cleaned up');
|
||||
}
|
||||
|
||||
FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb);
|
||||
});
|
||||
|
||||
FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb);
|
||||
};
|
||||
|
||||
FTNMessageScanTossModule.prototype.performImport = function(cb) {
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Log = require('../../logger.js').log;
|
||||
const ServerModule = require('../../server_module.js').ServerModule;
|
||||
const Config = require('../../config.js').config;
|
||||
|
||||
// deps
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const paths = require('path');
|
||||
const mimeTypes = require('mime-types');
|
||||
|
||||
const ModuleInfo = exports.moduleInfo = {
|
||||
name : 'Web',
|
||||
desc : 'Web Server',
|
||||
author : 'NuSkooler',
|
||||
packageName : 'codes.l33t.enigma.web.server',
|
||||
};
|
||||
|
||||
class Route {
|
||||
constructor(route) {
|
||||
Object.assign(this, route);
|
||||
|
||||
if(this.method) {
|
||||
this.method = this.method.toUpperCase();
|
||||
}
|
||||
|
||||
try {
|
||||
this.pathRegExp = new RegExp(this.path);
|
||||
} catch(e) {
|
||||
Log.debug( { route : route }, 'Invalid regular expression for route path' );
|
||||
}
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (
|
||||
this.pathRegExp instanceof RegExp &&
|
||||
( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) ||
|
||||
!_.isFunction(this.handler)
|
||||
);
|
||||
}
|
||||
|
||||
matchesRequest(req) { return req.method === this.method && this.pathRegExp.test(req.url); }
|
||||
|
||||
getRouteKey() { return `${this.method}:${this.path}`; }
|
||||
}
|
||||
|
||||
exports.getModule = class WebServerModule extends ServerModule {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.enableHttp = Config.contentServers.web.http.enabled || true;
|
||||
this.enableHttps = Config.contentServers.web.https.enabled || false;
|
||||
|
||||
this.routes = {};
|
||||
|
||||
if(Config.contentServers.web.staticRoot) {
|
||||
this.addRoute({
|
||||
method : 'GET',
|
||||
path : '/static/.*$',
|
||||
handler : this.routeStaticFile.bind(this),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createServer() {
|
||||
if(this.enableHttp) {
|
||||
this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) );
|
||||
}
|
||||
|
||||
if(this.enableHttps) {
|
||||
const options = {
|
||||
cert : fs.readFileSync(Config.contentServers.web.https.certPem),
|
||||
key : fs.readFileSync(Config.contentServers.web.https.keyPem),
|
||||
};
|
||||
|
||||
// additional options
|
||||
Object.assign(options, Config.contentServers.web.https.options || {} );
|
||||
|
||||
this.httpsServer = https.createServer(options, this.routeRequest);
|
||||
}
|
||||
}
|
||||
|
||||
listen() {
|
||||
let ok = true;
|
||||
|
||||
[ 'http', 'https' ].forEach(service => {
|
||||
const name = `${service}Server`;
|
||||
if(this[name]) {
|
||||
const port = parseInt(Config.contentServers.web[service].port);
|
||||
if(isNaN(port)) {
|
||||
ok = false;
|
||||
return Log.warn( { port : Config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` );
|
||||
}
|
||||
return this[name].listen(port);
|
||||
}
|
||||
});
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
addRoute(route) {
|
||||
route = new Route(route);
|
||||
|
||||
if(!route.isValid()) {
|
||||
Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' );
|
||||
return false;
|
||||
}
|
||||
|
||||
const routeKey = route.getRouteKey();
|
||||
if(routeKey in this.routes) {
|
||||
Log.warn( { route : route }, 'Cannot add route: duplicate method/path combination exists' );
|
||||
return false;
|
||||
}
|
||||
|
||||
this.routes[routeKey] = route;
|
||||
return true;
|
||||
}
|
||||
|
||||
routeRequest(req, resp) {
|
||||
const route = _.find(this.routes, r => r.matchesRequest(req) );
|
||||
return route ? route.handler(req, resp) : this.accessDenied(resp);
|
||||
}
|
||||
|
||||
respondWithError(resp, code, bodyText, title) {
|
||||
const customErrorPage = paths.join(Config.contentServers.web.staticRoot, `${code}.html`);
|
||||
|
||||
fs.readFile(customErrorPage, 'utf8', (err, data) => {
|
||||
resp.writeHead(code, { 'Content-Type' : 'text/html' } );
|
||||
|
||||
if(err) {
|
||||
return resp.end(`<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<h2>${bodyText}</h2>
|
||||
</article>
|
||||
</body>
|
||||
</html>`
|
||||
);
|
||||
}
|
||||
|
||||
return resp.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
accessDenied(resp) {
|
||||
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
|
||||
}
|
||||
|
||||
routeStaticFile(req, resp) {
|
||||
const fileName = req.url.substr(req.url.indexOf('/', 1));
|
||||
const filePath = paths.join(Config.contentServers.web.staticRoot, fileName);
|
||||
const self = this;
|
||||
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
return self.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
|
||||
'Content-Length' : stats.size,
|
||||
};
|
||||
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
resp.writeHead(200, headers);
|
||||
return readStream.pipe(resp);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
const Config = require('../../config.js').config;
|
||||
const baseClient = require('../../client.js');
|
||||
const Log = require('../../logger.js').log;
|
||||
const ServerModule = require('../../server_module.js').ServerModule;
|
||||
const LoginServerModule = require('../../login_server_module.js');
|
||||
const userLogin = require('../../user_login.js').userLogin;
|
||||
const enigVersion = require('../../../package.json').version;
|
||||
const theme = require('../../theme.js');
|
||||
|
@ -18,15 +18,14 @@ const util = require('util');
|
|||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
|
||||
exports.moduleInfo = {
|
||||
const ModuleInfo = exports.moduleInfo = {
|
||||
name : 'SSH',
|
||||
desc : 'SSH Server',
|
||||
author : 'NuSkooler',
|
||||
isSecure : true,
|
||||
packageName : 'codes.l33t.enigma.ssh.server',
|
||||
};
|
||||
|
||||
exports.getModule = SSHServerModule;
|
||||
|
||||
function SSHClient(clientConn) {
|
||||
baseClient.Client.apply(this, arguments);
|
||||
|
||||
|
@ -226,15 +225,12 @@ util.inherits(SSHClient, baseClient.Client);
|
|||
|
||||
SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ];
|
||||
|
||||
function SSHServerModule() {
|
||||
ServerModule.call(this);
|
||||
exports.getModule = class SSHServerModule extends LoginServerModule {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
util.inherits(SSHServerModule, ServerModule);
|
||||
|
||||
SSHServerModule.prototype.createServer = function() {
|
||||
SSHServerModule.super_.prototype.createServer.call(this);
|
||||
|
||||
createServer() {
|
||||
const serverConf = {
|
||||
hostKeys : [
|
||||
{
|
||||
|
@ -252,14 +248,22 @@ SSHServerModule.prototype.createServer = function() {
|
|||
},
|
||||
};
|
||||
|
||||
const server = ssh2.Server(serverConf);
|
||||
server.on('connection', function onConnection(conn, info) {
|
||||
this.server = ssh2.Server(serverConf);
|
||||
this.server.on('connection', (conn, info) => {
|
||||
Log.info(info, 'New SSH connection');
|
||||
|
||||
const client = new SSHClient(conn);
|
||||
|
||||
this.emit('client', client, conn._sock);
|
||||
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);
|
||||
});
|
||||
}
|
||||
|
||||
return server;
|
||||
listen() {
|
||||
const port = parseInt(Config.loginServers.ssh.port);
|
||||
if(isNaN(port)) {
|
||||
Log.error( { server : ModuleInfo.name, port : Config.loginServers.ssh.port }, 'Cannot load server (invalid port)' );
|
||||
return false;
|
||||
}
|
||||
|
||||
this.server.listen(port);
|
||||
Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' );
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
// ENiGMA½
|
||||
const baseClient = require('../../client.js');
|
||||
const Log = require('../../logger.js').log;
|
||||
const ServerModule = require('../../server_module.js').ServerModule;
|
||||
const LoginServerModule = require('../../login_server_module.js');
|
||||
const Config = require('../../config.js').config;
|
||||
|
||||
// deps
|
||||
|
@ -16,16 +16,14 @@ const util = require('util');
|
|||
|
||||
//var debug = require('debug')('telnet');
|
||||
|
||||
exports.moduleInfo = {
|
||||
const ModuleInfo = exports.moduleInfo = {
|
||||
name : 'Telnet',
|
||||
desc : 'Telnet Server',
|
||||
author : 'NuSkooler',
|
||||
isSecure : false,
|
||||
packageName : 'codes.l33t.enigma.telnet.server',
|
||||
};
|
||||
|
||||
exports.getModule = TelnetServerModule;
|
||||
|
||||
|
||||
//
|
||||
// Telnet Protocol Resources
|
||||
// * http://pcmicro.com/netfoss/telnet.html
|
||||
|
@ -440,6 +438,65 @@ function TelnetClient(input, output) {
|
|||
newEnvironRequested : false,
|
||||
};
|
||||
|
||||
this.setTemporaryDirectDataHandler = function(handler) {
|
||||
this.input.removeAllListeners('data');
|
||||
this.input.on('data', handler);
|
||||
};
|
||||
|
||||
this.restoreDataHandler = function() {
|
||||
this.input.removeAllListeners('data');
|
||||
this.input.on('data', this.dataHandler);
|
||||
};
|
||||
|
||||
this.dataHandler = function(b) {
|
||||
bufs.push(b);
|
||||
|
||||
let i;
|
||||
while((i = bufs.indexOf(IAC_BUF)) >= 0) {
|
||||
|
||||
//
|
||||
// Some clients will send even IAC separate from data
|
||||
//
|
||||
if(bufs.length <= (i + 1)) {
|
||||
i = MORE_DATA_REQUIRED;
|
||||
break;
|
||||
}
|
||||
|
||||
assert(bufs.length > (i + 1));
|
||||
|
||||
if(i > 0) {
|
||||
self.emit('data', bufs.splice(0, i).toBuffer());
|
||||
}
|
||||
|
||||
i = parseBufs(bufs);
|
||||
|
||||
if(MORE_DATA_REQUIRED === i) {
|
||||
break;
|
||||
} else {
|
||||
if(i.option) {
|
||||
self.emit(i.option, i); // "transmit binary", "echo", ...
|
||||
}
|
||||
|
||||
self.handleTelnetEvent(i);
|
||||
|
||||
if(i.data) {
|
||||
self.emit('data', i.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(MORE_DATA_REQUIRED !== i && bufs.length > 0) {
|
||||
//
|
||||
// Standard data payload. This can still be "non-user" data
|
||||
// such as ANSI control, but we don't handle that here.
|
||||
//
|
||||
self.emit('data', bufs.splice(0).toBuffer());
|
||||
}
|
||||
};
|
||||
|
||||
this.input.on('data', this.dataHandler);
|
||||
|
||||
/*
|
||||
this.input.on('data', b => {
|
||||
bufs.push(b);
|
||||
|
||||
|
@ -484,8 +541,8 @@ function TelnetClient(input, output) {
|
|||
//
|
||||
self.emit('data', bufs.splice(0).toBuffer());
|
||||
}
|
||||
|
||||
});
|
||||
*/
|
||||
|
||||
this.input.on('end', () => {
|
||||
self.emit('end');
|
||||
|
@ -767,22 +824,34 @@ Object.keys(OPTIONS).forEach(function(name) {
|
|||
});
|
||||
});
|
||||
|
||||
function TelnetServerModule() {
|
||||
ServerModule.call(this);
|
||||
exports.getModule = class TelnetServerModule extends LoginServerModule {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
util.inherits(TelnetServerModule, ServerModule);
|
||||
|
||||
TelnetServerModule.prototype.createServer = function() {
|
||||
TelnetServerModule.super_.prototype.createServer.call(this);
|
||||
|
||||
const server = net.createServer( (sock) => {
|
||||
createServer() {
|
||||
this.server = net.createServer( sock => {
|
||||
const client = new TelnetClient(sock, sock);
|
||||
|
||||
client.banner();
|
||||
|
||||
server.emit('client', client, sock);
|
||||
this.handleNewClient(client, sock, ModuleInfo);
|
||||
});
|
||||
|
||||
return server;
|
||||
this.server.on('error', err => {
|
||||
Log.info( { error : err.message }, 'Telnet server error');
|
||||
});
|
||||
}
|
||||
|
||||
listen() {
|
||||
const port = parseInt(Config.loginServers.telnet.port);
|
||||
if(isNaN(port)) {
|
||||
Log.error( { server : ModuleInfo.name, port : Config.loginServers.telnet.port }, 'Cannot load server (invalid port)' );
|
||||
return false;
|
||||
}
|
||||
|
||||
this.server.listen(port);
|
||||
Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' );
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var MenuModule = require('./menu_module.js').MenuModule;
|
||||
|
||||
exports.getModule = StandardMenuModule;
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Standard Menu Module',
|
||||
|
@ -11,30 +9,19 @@ exports.moduleInfo = {
|
|||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
function StandardMenuModule(menuConfig) {
|
||||
MenuModule.call(this, menuConfig);
|
||||
exports.getModule = class StandardMenuModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
require('util').inherits(StandardMenuModule, MenuModule);
|
||||
|
||||
|
||||
StandardMenuModule.prototype.enter = function() {
|
||||
StandardMenuModule.super_.prototype.enter.call(this);
|
||||
};
|
||||
|
||||
StandardMenuModule.prototype.beforeArt = function(cb) {
|
||||
StandardMenuModule.super_.prototype.beforeArt.call(this, cb);
|
||||
};
|
||||
|
||||
StandardMenuModule.prototype.mciReady = function(mciData, cb) {
|
||||
var self = this;
|
||||
|
||||
StandardMenuModule.super_.prototype.mciReady.call(this, mciData, function mciReadyComplete(err) {
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
cb(err);
|
||||
} else {
|
||||
// we do this so other modules can be both customized and still perform standard tasks
|
||||
StandardMenuModule.super_.prototype.standardMCIReadyHandler.call(self, mciData, cb);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// we do this so other modules can be both customized and still perform standard tasks
|
||||
return this.standardMCIReadyHandler(mciData, cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -118,6 +118,14 @@ class StatLog {
|
|||
return user.persistProperty(statName, statValue, cb);
|
||||
}
|
||||
|
||||
getUserStat(user, statName) {
|
||||
return user.properties[statName];
|
||||
}
|
||||
|
||||
getUserStatNum(user, statName) {
|
||||
return parseInt(this.getUserStat(user, statName)) || 0;
|
||||
}
|
||||
|
||||
incrementUserStat(user, statName, incrementBy, cb) {
|
||||
incrementBy = incrementBy || 1;
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ const pad = require('./string_util.js').pad;
|
|||
const stylizeString = require('./string_util.js').stylizeString;
|
||||
const renderStringLength = require('./string_util.js').renderStringLength;
|
||||
const renderSubstr = require('./string_util.js').renderSubstr;
|
||||
const formatByteSize = require('./string_util.js').formatByteSize;
|
||||
const formatByteSizeAbbr = require('./string_util.js').formatByteSizeAbbr;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
@ -265,6 +267,12 @@ const transformers = {
|
|||
styleSmallI : (s) => stylizeString(s, 'small i'),
|
||||
styleMixed : (s) => stylizeString(s, 'mixed'),
|
||||
styleL33t : (s) => stylizeString(s, 'l33t'),
|
||||
|
||||
// toMegs(), toKilobytes(), ...
|
||||
// toList(), toCommaList(),
|
||||
sizeWithAbbr : (n) => formatByteSize(n, true, 2),
|
||||
sizeWithoutAbbr : (n) => formatByteSize(n, false, 2),
|
||||
sizeAbbr : (n) => formatByteSizeAbbr(n),
|
||||
};
|
||||
|
||||
function transformValue(transformerName, value) {
|
||||
|
|
|
@ -3,17 +3,25 @@
|
|||
|
||||
// ENiGMA½
|
||||
const miscUtil = require('./misc_util.js');
|
||||
const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser;
|
||||
const ANSI = require('./ansi_term.js');
|
||||
|
||||
// deps
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
exports.stylizeString = stylizeString;
|
||||
exports.pad = pad;
|
||||
exports.replaceAt = replaceAt;
|
||||
exports.isPrintable = isPrintable;
|
||||
exports.stripAllLineFeeds = stripAllLineFeeds;
|
||||
exports.debugEscapedString = debugEscapedString;
|
||||
exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
||||
exports.renderSubstr = renderSubstr;
|
||||
exports.renderStringLength = renderStringLength;
|
||||
exports.formatByteSizeAbbr = formatByteSizeAbbr;
|
||||
exports.formatByteSize = formatByteSize;
|
||||
exports.cleanControlCodes = cleanControlCodes;
|
||||
exports.createCleanAnsi = createCleanAnsi;
|
||||
|
||||
// :TODO: create Unicode verison of this
|
||||
const VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
|
||||
|
@ -182,6 +190,10 @@ function stringLength(s) {
|
|||
return s.length;
|
||||
}
|
||||
|
||||
function stripAllLineFeeds(s) {
|
||||
return s.replace(/\r?\n|[\r\u2028\u2029]/g, '');
|
||||
}
|
||||
|
||||
function debugEscapedString(s) {
|
||||
return JSON.stringify(s).slice(1, -1);
|
||||
}
|
||||
|
@ -286,20 +298,49 @@ function renderStringLength(s) {
|
|||
return len;
|
||||
}
|
||||
|
||||
const SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :)
|
||||
|
||||
function formatByteSizeAbbr(byteSize) {
|
||||
if(0 === byteSize) {
|
||||
return SIZE_ABBRS[0]; // B
|
||||
}
|
||||
|
||||
return SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))];
|
||||
}
|
||||
|
||||
function formatByteSize(byteSize, withAbbr, decimals) {
|
||||
withAbbr = withAbbr || false;
|
||||
decimals = decimals || 3;
|
||||
const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024));
|
||||
let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals));
|
||||
if(withAbbr) {
|
||||
result += ` ${SIZE_ABBRS[i]}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's
|
||||
//const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g;
|
||||
const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([\?=;0-9]*?)([A-ORZcf-npsu=><])/g;
|
||||
const ANSI_OPCODES_ALLOWED_CLEAN = [
|
||||
'C', 'm' ,
|
||||
'A', 'B', 'D'
|
||||
'A', 'B', // up, down
|
||||
'C', 'D', // right, left
|
||||
'm', // color
|
||||
];
|
||||
|
||||
function cleanControlCodes(input) {
|
||||
const AnsiSpecialOpCodes = {
|
||||
positioning : [ 'A', 'B', 'C', 'D' ], // up, down, right, left
|
||||
style : [ 'm' ] // color
|
||||
};
|
||||
|
||||
function cleanControlCodes(input, options) {
|
||||
let m;
|
||||
let pos;
|
||||
let cleaned = '';
|
||||
|
||||
options = options || {};
|
||||
|
||||
//
|
||||
// Loop through |input| adding only allowed ESC
|
||||
// sequences and literals to |cleaned|
|
||||
|
@ -313,6 +354,10 @@ function cleanControlCodes(input) {
|
|||
cleaned += input.slice(pos, m.index);
|
||||
}
|
||||
|
||||
if(options.all) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(ANSI_OPCODES_ALLOWED_CLEAN.indexOf(m[2].charAt(0)) > -1) {
|
||||
cleaned += m[0];
|
||||
}
|
||||
|
@ -328,61 +373,147 @@ function cleanControlCodes(input) {
|
|||
return cleaned;
|
||||
}
|
||||
|
||||
function getCleanAnsi(input) {
|
||||
//
|
||||
// Process |input| and produce |cleaned|, an array
|
||||
// of lines with "clean" ANSI.
|
||||
//
|
||||
// Clean ANSI:
|
||||
// * Contains only color/SGR sequences
|
||||
// * All movement (up/down/left/right) removed but positioning
|
||||
// left intact via spaces/etc.
|
||||
//
|
||||
// Temporary processing will occur in a grid. Each cell
|
||||
// containing a character (defaulting to space) possibly a SGR
|
||||
//
|
||||
function createCleanAnsi(input, options, cb) {
|
||||
|
||||
let m;
|
||||
let pos;
|
||||
let grid = [];
|
||||
let gridPos = { row : 0, col : 0 };
|
||||
|
||||
function updateGrid(data, dataType) {
|
||||
//
|
||||
// Start at to grid[row][col] and populate val[0]...val[N]
|
||||
// creating cells as necessary
|
||||
//
|
||||
if(!grid[gridPos.row]) {
|
||||
grid[gridPos.row] = [];
|
||||
if(!input) {
|
||||
return cb('');
|
||||
}
|
||||
|
||||
if('literal' === dataType) {
|
||||
data.forEach(c => {
|
||||
grid[gridPos.row][gridPos.col] = (grid[gridPos.row][gridPos.col] || '') + c; // append to existing SGR
|
||||
gridPos.col++;
|
||||
options.width = options.width || 80;
|
||||
options.height = options.height || 25;
|
||||
|
||||
const canvas = new Array(options.height);
|
||||
for(let i = 0; i < options.height; ++i) {
|
||||
canvas[i] = new Array(options.width);
|
||||
for(let j = 0; j < options.width; ++j) {
|
||||
canvas[i][j] = {};
|
||||
}
|
||||
}
|
||||
|
||||
const parserOpts = {
|
||||
termHeight : options.height,
|
||||
termWidth : options.width,
|
||||
};
|
||||
|
||||
const parser = new ANSIEscapeParser(parserOpts);
|
||||
|
||||
const canvasPos = {
|
||||
col : 0,
|
||||
row : 0,
|
||||
};
|
||||
|
||||
let sgr;
|
||||
|
||||
function ensureCell() {
|
||||
// we've pre-allocated a matrix, but allow for > supplied dimens up front. They will be trimmed @ finalize
|
||||
if(!canvas[canvasPos.row]) {
|
||||
canvas[canvasPos.row] = new Array(options.width);
|
||||
for(let j = 0; j < options.width; ++j) {
|
||||
canvas[canvasPos.row][j] = {};
|
||||
}
|
||||
}
|
||||
canvas[canvasPos.row][canvasPos.col] = canvas[canvasPos.row][canvasPos.col] || {};
|
||||
//canvas[canvasPos.row][0].width = Math.max(canvas[canvasPos.row][0].width || 0, canvasPos.col);
|
||||
}
|
||||
|
||||
parser.on('literal', literal => {
|
||||
//
|
||||
// CR/LF are handled for 'position update'; we don't need the chars themselves
|
||||
//
|
||||
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
|
||||
|
||||
for(let i = 0; i < literal.length; ++i) {
|
||||
const c = literal.charAt(i);
|
||||
|
||||
ensureCell();
|
||||
|
||||
canvas[canvasPos.row][canvasPos.col].char = c;
|
||||
|
||||
if(sgr) {
|
||||
canvas[canvasPos.row][canvasPos.col].sgr = sgr;
|
||||
sgr = null;
|
||||
}
|
||||
|
||||
canvasPos.col += 1;
|
||||
}
|
||||
});
|
||||
} else if('sgr' === dataType) {
|
||||
grid[gridPos.row][gridPos.col] = (grid[gridPos.row][gridPos.col] || '') + data;
|
||||
|
||||
parser.on('control', (match, opCode) => {
|
||||
if('m' !== opCode) {
|
||||
return; // don't care'
|
||||
}
|
||||
sgr = match;
|
||||
});
|
||||
|
||||
parser.on('position update', (row, col) => {
|
||||
canvasPos.row = row - 1;
|
||||
canvasPos.col = Math.min(col - 1, options.width);
|
||||
});
|
||||
|
||||
parser.on('complete', () => {
|
||||
for(let row = 0; row < options.height; ++row) {
|
||||
let col = 0;
|
||||
|
||||
//while(col <= canvas[row][0].width) {
|
||||
while(col < options.width) {
|
||||
if(!canvas[row][col].char) {
|
||||
canvas[row][col].char = ' ';
|
||||
if(!canvas[row][col].sgr) {
|
||||
// :TODO: fix duplicate SGR's in a row here - we just need one per sequence
|
||||
canvas[row][col].sgr = ANSI.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function literal(s) {
|
||||
let charCode;
|
||||
const len = s.length;
|
||||
for(let i = 0; i < len; ++i) {
|
||||
charCode = s.charCodeAt(i) & 0xff;
|
||||
|
||||
}
|
||||
col += 1;
|
||||
}
|
||||
|
||||
do {
|
||||
pos = REGEXP_ANSI_CONTROL_CODES.lastIndex;
|
||||
m = REGEXP_ANSI_CONTROL_CODES.exec(input);
|
||||
// :TODO: end *all* with CRLF - general usage will be width : 79 - prob update defaults
|
||||
|
||||
if(null !== m) {
|
||||
if(m.index > pos) {
|
||||
updateGrid(input.slice(pos, m.index), 'literal');
|
||||
if(col <= options.width) {
|
||||
canvas[row][col] = canvas[row][col] || {};
|
||||
|
||||
canvas[row][col].char = '\r\n';
|
||||
canvas[row][col].sgr = ANSI.reset();
|
||||
|
||||
// :TODO: don't splice, just reset + fill with ' ' till end
|
||||
for(let fillCol = col; fillCol <= options.width; ++fillCol) {
|
||||
canvas[row][fillCol].char = ' ';
|
||||
}
|
||||
|
||||
//canvas[row] = canvas[row].splice(0, col + 1);
|
||||
//canvas[row][options.width - 1].char = '\r\n';
|
||||
|
||||
|
||||
} else {
|
||||
canvas[row] = canvas[row].splice(0, options.width + 1);
|
||||
}
|
||||
} while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex);
|
||||
|
||||
}
|
||||
|
||||
let out = '';
|
||||
for(let row = 0; row < options.height; ++row) {
|
||||
out += canvas[row].map( col => {
|
||||
let c = col.sgr || '';
|
||||
c += col.char;
|
||||
return c;
|
||||
}).join('');
|
||||
|
||||
}
|
||||
|
||||
// :TODO: finalize: @ any non-char cell, reset sgr & set to ' '
|
||||
// :TODO: finalize: after sgr established, omit anything > supplied dimens
|
||||
return cb(out);
|
||||
});
|
||||
|
||||
parser.parse(input);
|
||||
}
|
||||
|
||||
/*
|
||||
const fs = require('fs');
|
||||
let data = fs.readFileSync('/home/nuskooler/Downloads/art3.ans');
|
||||
data = iconv.decode(data, 'cp437');
|
||||
createCleanAnsi(data, { width : 79, height : 25 }, (out) => {
|
||||
out = iconv.encode(out, 'cp437');
|
||||
fs.writeFileSync('/home/nuskooler/Downloads/art4.ans', out);
|
||||
});
|
||||
*/
|
|
@ -63,9 +63,15 @@ function logoff(callingMenu, formData, extraArgs, cb) {
|
|||
}
|
||||
|
||||
function prevMenu(callingMenu, formData, extraArgs, cb) {
|
||||
|
||||
// :TODO: this is a pretty big hack -- need the whole key map concep there like other places
|
||||
if(formData.key && 'return' === formData.key.name) {
|
||||
callingMenu.submitFormData = formData;
|
||||
}
|
||||
|
||||
callingMenu.prevMenu( err => {
|
||||
if(err) {
|
||||
callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to fallback!');
|
||||
callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!');
|
||||
}
|
||||
return cb(err);
|
||||
});
|
||||
|
@ -74,7 +80,7 @@ function prevMenu(callingMenu, formData, extraArgs, cb) {
|
|||
function nextMenu(callingMenu, formData, extraArgs, cb) {
|
||||
callingMenu.nextMenu( err => {
|
||||
if(err) {
|
||||
callingMenu.client.log.error( { error : err.toString() }, 'Error attempting to go to next menu!');
|
||||
callingMenu.client.log.error( { error : err.message}, 'Error attempting to go to next menu!');
|
||||
}
|
||||
return cb(err);
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ const stylizeString = require('./string_util.js').stylizeString;
|
|||
const renderSubstr = require('./string_util.js').renderSubstr;
|
||||
const renderStringLength = require('./string_util.js').renderStringLength;
|
||||
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
|
||||
const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds;
|
||||
|
||||
// deps
|
||||
const util = require('util');
|
||||
|
@ -102,7 +103,7 @@ function TextView(options) {
|
|||
|
||||
renderLength = renderStringLength(textToDraw);
|
||||
|
||||
if(renderLength > this.dimens.width) {
|
||||
if(renderLength >= this.dimens.width) {
|
||||
if(this.hasFocus) {
|
||||
if(this.horizScroll) {
|
||||
textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength);
|
||||
|
@ -150,7 +151,7 @@ TextView.prototype.redraw = function() {
|
|||
// and there is no actual text (e.g. save SGR's and processing)
|
||||
//
|
||||
if(!this.hasDrawnOnce) {
|
||||
if(!this.text) {
|
||||
if(_.isUndefined(this.text)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +184,7 @@ TextView.prototype.setText = function(text, redraw) {
|
|||
text = text.toString();
|
||||
}
|
||||
|
||||
text = pipeToAnsi(text, this.client); // expand MCI/etc.
|
||||
text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc.
|
||||
|
||||
var widthDelta = 0;
|
||||
if(this.text && this.text !== text) {
|
||||
|
|
316
core/theme.js
316
core/theme.js
|
@ -1,21 +1,21 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var Config = require('./config.js').config;
|
||||
var art = require('./art.js');
|
||||
var ansi = require('./ansi_term.js');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
var Log = require('./logger.js').log;
|
||||
var configCache = require('./config_cache.js');
|
||||
var getFullConfig = require('./config_util.js').getFullConfig;
|
||||
var asset = require('./asset.js');
|
||||
var ViewController = require('./view_controller.js').ViewController;
|
||||
const Config = require('./config.js').config;
|
||||
const art = require('./art.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const configCache = require('./config_cache.js');
|
||||
const getFullConfig = require('./config_util.js').getFullConfig;
|
||||
const asset = require('./asset.js');
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
|
||||
var fs = require('fs');
|
||||
var paths = require('path');
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
|
||||
exports.getThemeArt = getThemeArt;
|
||||
exports.getAvailableThemes = getAvailableThemes;
|
||||
|
@ -24,6 +24,7 @@ exports.setClientTheme = setClientTheme;
|
|||
exports.initAvailableThemes = initAvailableThemes;
|
||||
exports.displayThemeArt = displayThemeArt;
|
||||
exports.displayThemedPause = displayThemedPause;
|
||||
exports.displayThemedPrompt = displayThemedPrompt;
|
||||
exports.displayThemedAsset = displayThemedAsset;
|
||||
|
||||
function refreshThemeHelpers(theme) {
|
||||
|
@ -100,9 +101,9 @@ function loadTheme(themeID, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
var availableThemes = {};
|
||||
const availableThemes = {};
|
||||
|
||||
var IMMUTABLE_MCI_PROPERTIES = [
|
||||
const IMMUTABLE_MCI_PROPERTIES = [
|
||||
'maxLength', 'argName', 'submit', 'validate'
|
||||
];
|
||||
|
||||
|
@ -144,7 +145,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
|||
|
||||
function mergeMciProperties(dest, src) {
|
||||
Object.keys(src).forEach(function mciEntry(mci) {
|
||||
_.merge(dest[mci], src[mci], mciCustomizer);
|
||||
_.mergeWith(dest[mci], src[mci], mciCustomizer);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -356,15 +357,15 @@ function getThemeArt(options, cb) {
|
|||
//
|
||||
// options - required:
|
||||
// name
|
||||
// client
|
||||
//
|
||||
// options - optional
|
||||
// client - needed for user's theme/etc.
|
||||
// themeId
|
||||
// asAnsi
|
||||
// readSauce
|
||||
// random
|
||||
//
|
||||
if(!options.themeId && _.has(options.client, 'user.properties.theme_id')) {
|
||||
if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) {
|
||||
options.themeId = options.client.user.properties.theme_id;
|
||||
} else {
|
||||
options.themeId = Config.defaults.theme;
|
||||
|
@ -438,9 +439,13 @@ function getThemeArt(options, cb) {
|
|||
],
|
||||
function complete(err, artInfo) {
|
||||
if(err) {
|
||||
options.client.log.debug( { error : err }, 'Cannot find art');
|
||||
if(options.client) {
|
||||
options.client.log.debug( { error : err.message }, 'Cannot find theme art' );
|
||||
} else {
|
||||
Log.debug( { error : err.message }, 'Cannot find theme art' );
|
||||
}
|
||||
cb(err, artInfo);
|
||||
}
|
||||
return cb(err, artInfo);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -481,110 +486,187 @@ function displayThemeArt(options, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
/*
|
||||
function displayThemedPrompt(name, client, options, cb) {
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function loadConfig(callback) {
|
||||
configCache.getModConfig('prompt.hjson', (err, promptJson) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(_.has(promptJson, [ 'prompts', name ] )) {
|
||||
return callback(Errors.DoesNotExist(`Prompt "${name}" does not exist`));
|
||||
}
|
||||
|
||||
const promptConfig = promptJson.prompts[name];
|
||||
if(!_.isObject(promptConfig)) {
|
||||
return callback(Errors.Invalid(`Prompt "${name} is invalid`));
|
||||
}
|
||||
|
||||
return callback(null, promptConfig);
|
||||
});
|
||||
},
|
||||
function display(promptConfig, callback) {
|
||||
if(options.clearScreen) {
|
||||
client.term.rawWrite(ansi.clearScreen());
|
||||
}
|
||||
|
||||
//
|
||||
// If we did not clear the screen, don't let the font change
|
||||
//
|
||||
const dispOptions = Object.assign( {}, promptConfig.options );
|
||||
if(!options.clearScreen) {
|
||||
dispOptions.font = 'not_really_a_font!';
|
||||
}
|
||||
|
||||
displayThemedAsset(
|
||||
promptConfig.art,
|
||||
client,
|
||||
dispOptions,
|
||||
(err, artData) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, promptConfig, artData.mciMap);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepViews(promptConfig, mciMap, callback) {
|
||||
vc = new ViewController( { client : client } );
|
||||
|
||||
const loadOpts = {
|
||||
promptName : name,
|
||||
mciMap : mciMap,
|
||||
config : promptConfig,
|
||||
};
|
||||
|
||||
vc.loadFromPromptConfig(loadOpts, err => {
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
function displayThemedPrompt(name, client, options, cb) {
|
||||
|
||||
const useTempViewController = _.isUndefined(options.viewController);
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function display(callback) {
|
||||
const promptConfig = client.currentTheme.prompts[name];
|
||||
if(!promptConfig) {
|
||||
return callback(Errors.DoesNotExist(`Missing "${name}" prompt configuration!`));
|
||||
}
|
||||
|
||||
if(options.clearScreen) {
|
||||
client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
//
|
||||
// If we did *not* clear the screen, don't let the font change
|
||||
// as it will mess with the output of the existing art displayed in a terminal
|
||||
//
|
||||
const dispOptions = Object.assign( {}, promptConfig.options );
|
||||
if(!options.clearScreen) {
|
||||
dispOptions.font = 'not_really_a_font!'; // kludge :)
|
||||
}
|
||||
|
||||
displayThemedAsset(
|
||||
promptConfig.art,
|
||||
client,
|
||||
dispOptions,
|
||||
(err, artInfo) => {
|
||||
return callback(err, promptConfig, artInfo);
|
||||
}
|
||||
);
|
||||
},
|
||||
function discoverCursorPosition(promptConfig, artInfo, callback) {
|
||||
if(!options.clearPrompt) {
|
||||
// no need to query cursor - we're not gonna use it
|
||||
return callback(null, promptConfig, artInfo);
|
||||
}
|
||||
|
||||
client.once('cursor position report', pos => {
|
||||
artInfo.startRow = pos[0] - artInfo.height;
|
||||
return callback(null, promptConfig, artInfo);
|
||||
});
|
||||
|
||||
client.term.rawWrite(ansi.queryPos());
|
||||
},
|
||||
function createMCIViews(promptConfig, artInfo, callback) {
|
||||
const tempViewController = useTempViewController ? new ViewController( { client : client } ) : options.viewController;
|
||||
|
||||
const loadOpts = {
|
||||
promptName : name,
|
||||
mciMap : artInfo.mciMap,
|
||||
config : promptConfig,
|
||||
};
|
||||
|
||||
tempViewController.loadFromPromptConfig(loadOpts, () => {
|
||||
return callback(null, artInfo, tempViewController);
|
||||
});
|
||||
},
|
||||
function pauseForUserInput(artInfo, tempViewController, callback) {
|
||||
if(!options.pause) {
|
||||
return callback(null, artInfo, tempViewController);
|
||||
}
|
||||
|
||||
client.waitForKeyPress( () => {
|
||||
return callback(null, artInfo, tempViewController);
|
||||
});
|
||||
},
|
||||
function clearPauseArt(artInfo, tempViewController, callback) {
|
||||
if(options.clearPrompt) {
|
||||
if(artInfo.startRow && artInfo.height) {
|
||||
client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
||||
|
||||
// Note: Does not work properly in NetRunner < 2.0b17:
|
||||
client.term.rawWrite(ansi.deleteLine(artInfo.height));
|
||||
} else {
|
||||
client.term.rawWrite(ansi.eraseLine(1));
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, tempViewController);
|
||||
}
|
||||
],
|
||||
(err, tempViewController) => {
|
||||
if(err) {
|
||||
client.log.warn( { error : err.message }, `Failed displaying "${name}" prompt` );
|
||||
}
|
||||
|
||||
if(tempViewController && useTempViewController) {
|
||||
tempViewController.detachClientEvents();
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Pause prompts are a special prompt by the name 'pause'.
|
||||
//
|
||||
function displayThemedPause(options, cb) {
|
||||
//
|
||||
// options.client
|
||||
// options clearPrompt
|
||||
//
|
||||
assert(_.isObject(options.client));
|
||||
function displayThemedPause(client, options, cb) {
|
||||
|
||||
if(!cb && _.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if(!_.isBoolean(options.clearPrompt)) {
|
||||
options.clearPrompt = true;
|
||||
}
|
||||
|
||||
// :TODO: Support animated pause prompts. Probably via MCI with AnimatedView
|
||||
|
||||
var artInfo;
|
||||
var vc;
|
||||
var promptConfig;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadPromptJSON(callback) {
|
||||
configCache.getModConfig('prompt.hjson', function loaded(err, promptJson) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
if(_.has(promptJson, [ 'prompts', 'pause' ] )) {
|
||||
promptConfig = promptJson.prompts.pause;
|
||||
callback(_.isObject(promptConfig) ? null : new Error('Invalid prompt config block!'));
|
||||
} else {
|
||||
callback(new Error('Missing standard \'pause\' prompt'));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
function displayPausePrompt(callback) {
|
||||
//
|
||||
// Override .font so it doesn't change from current setting
|
||||
//
|
||||
var dispOptions = promptConfig.options;
|
||||
dispOptions.font = 'not_really_a_font!';
|
||||
|
||||
displayThemedAsset(
|
||||
promptConfig.art,
|
||||
options.client,
|
||||
dispOptions,
|
||||
function displayed(err, artData) {
|
||||
artInfo = artData;
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function discoverCursorPosition(callback) {
|
||||
options.client.once('cursor position report', function cpr(pos) {
|
||||
artInfo.startRow = pos[0] - artInfo.height;
|
||||
callback(null);
|
||||
});
|
||||
options.client.term.rawWrite(ansi.queryPos());
|
||||
},
|
||||
function createMCIViews(callback) {
|
||||
vc = new ViewController( { client : options.client, noInput : true } );
|
||||
vc.loadFromPromptConfig( { promptName : 'pause', mciMap : artInfo.mciMap, config : promptConfig }, function loaded(err) {
|
||||
callback(null);
|
||||
});
|
||||
},
|
||||
function pauseForUserInput(callback) {
|
||||
options.client.waitForKeyPress(function keyPressed() {
|
||||
callback(null);
|
||||
});
|
||||
},
|
||||
function clearPauseArt(callback) {
|
||||
if(options.clearPrompt) {
|
||||
if(artInfo.startRow && artInfo.height) {
|
||||
options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
||||
|
||||
// Note: Does not work properly in NetRunner < 2.0b17:
|
||||
options.client.term.rawWrite(ansi.deleteLine(artInfo.height));
|
||||
} else {
|
||||
options.client.term.rawWrite(ansi.eraseLine(1))
|
||||
}
|
||||
}
|
||||
callback(null);
|
||||
}
|
||||
/*
|
||||
, function debugPause(callback) {
|
||||
setTimeout(function to() {
|
||||
callback(null);
|
||||
}, 4000);
|
||||
}
|
||||
*/
|
||||
],
|
||||
function complete(err) {
|
||||
if(err) {
|
||||
Log.error(err);
|
||||
}
|
||||
|
||||
if(vc) {
|
||||
vc.detachClientEvents();
|
||||
}
|
||||
|
||||
cb();
|
||||
}
|
||||
);
|
||||
const promptOptions = Object.assign( {}, options, { pause : true } );
|
||||
return displayThemedPrompt('pause', client, promptOptions, cb);
|
||||
}
|
||||
|
||||
function displayThemedAsset(assetSpec, client, options, cb) {
|
||||
|
|
|
@ -25,9 +25,7 @@ function ToggleMenuView (options) {
|
|||
*/
|
||||
|
||||
this.updateSelection = function() {
|
||||
//assert(!self.positionCacheExpired);
|
||||
assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length);
|
||||
|
||||
self.redraw();
|
||||
};
|
||||
}
|
||||
|
@ -74,28 +72,38 @@ ToggleMenuView.prototype.setFocus = function(focused) {
|
|||
this.redraw();
|
||||
};
|
||||
|
||||
ToggleMenuView.prototype.onKeyPress = function(ch, key) {
|
||||
if(key) {
|
||||
var needsUpdate;
|
||||
if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) {
|
||||
ToggleMenuView.prototype.focusNext = function() {
|
||||
if(this.items.length - 1 === this.focusedItemIndex) {
|
||||
this.focusedItemIndex = 0;
|
||||
} else {
|
||||
this.focusedItemIndex++;
|
||||
}
|
||||
needsUpdate = true;
|
||||
} else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.name)) {
|
||||
|
||||
this.updateSelection();
|
||||
|
||||
ToggleMenuView.super_.prototype.focusNext.call(this);
|
||||
};
|
||||
|
||||
ToggleMenuView.prototype.focusPrevious = function() {
|
||||
|
||||
if(0 === this.focusedItemIndex) {
|
||||
this.focusedItemIndex = this.items.length - 1;
|
||||
} else {
|
||||
this.focusedItemIndex--;
|
||||
}
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if(needsUpdate) {
|
||||
this.updateSelection();
|
||||
return;
|
||||
|
||||
ToggleMenuView.super_.prototype.focusPrevious.call(this);
|
||||
};
|
||||
|
||||
ToggleMenuView.prototype.onKeyPress = function(ch, key) {
|
||||
|
||||
if(key) {
|
||||
if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) {
|
||||
this.focusNext();
|
||||
} else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.nam4e)) {
|
||||
this.focusPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
18
core/user.js
18
core/user.js
|
@ -325,6 +325,22 @@ User.prototype.persistProperty = function(propName, propValue, cb) {
|
|||
);
|
||||
};
|
||||
|
||||
User.prototype.removeProperty = function(propName, cb) {
|
||||
// update live
|
||||
delete this.properties[propName];
|
||||
|
||||
userDb.run(
|
||||
`DELETE FROM user_property
|
||||
WHERE user_id = ? AND prop_name = ?;`,
|
||||
[ this.userId, propName ],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
User.prototype.persistProperties = function(properties, cb) {
|
||||
var self = this;
|
||||
|
||||
|
@ -458,7 +474,7 @@ function generatePasswordDerivedKeySalt(cb) {
|
|||
|
||||
function generatePasswordDerivedKey(password, salt, cb) {
|
||||
password = new Buffer(password).toString('hex');
|
||||
crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, function onDerivedKey(err, dk) {
|
||||
crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', function onDerivedKey(err, dk) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
} else {
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var MenuModule = require('./menu_module.js').MenuModule;
|
||||
var ViewController = require('./view_controller.js').ViewController;
|
||||
var theme = require('./theme.js');
|
||||
var sysValidate = require('./system_view_validate.js');
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
const theme = require('./theme.js');
|
||||
const sysValidate = require('./system_view_validate.js');
|
||||
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
|
||||
exports.getModule = UserConfigModule;
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'User Configuration',
|
||||
|
@ -19,7 +17,7 @@ exports.moduleInfo = {
|
|||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
var MciCodeIds = {
|
||||
const MciCodeIds = {
|
||||
RealName : 1,
|
||||
BirthDate : 2,
|
||||
Sex : 3,
|
||||
|
@ -37,21 +35,11 @@ var MciCodeIds = {
|
|||
SaveCancel : 25,
|
||||
};
|
||||
|
||||
function UserConfigModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class UserConfigModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
var self = this;
|
||||
|
||||
self.getView = function(viewId) {
|
||||
return self.viewControllers.menu.getView(viewId);
|
||||
};
|
||||
|
||||
self.setViewText = function(viewId, text) {
|
||||
var v = self.getView(viewId);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
const self = this;
|
||||
|
||||
this.menuMethods = {
|
||||
//
|
||||
|
@ -156,19 +144,22 @@ function UserConfigModule(options) {
|
|||
};
|
||||
}
|
||||
|
||||
require('util').inherits(UserConfigModule, MenuModule);
|
||||
getView(viewId) {
|
||||
return this.viewControllers.menu.getView(viewId);
|
||||
}
|
||||
|
||||
UserConfigModule.prototype.mciReady = function(mciData, cb) {
|
||||
var self = this;
|
||||
var vc = self.viewControllers.menu = new ViewController( { client : self.client} );
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var currentThemeIdIndex = 0;
|
||||
const self = this;
|
||||
const vc = self.viewControllers.menu = new ViewController( { client : self.client} );
|
||||
let currentThemeIdIndex = 0;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
UserConfigModule.super_.prototype.mciReady.call(self, mciData, callback);
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
||||
},
|
||||
|
@ -192,14 +183,14 @@ UserConfigModule.prototype.mciReady = function(mciData, cb) {
|
|||
function populateViews(callback) {
|
||||
var user = self.client.user;
|
||||
|
||||
self.setViewText(MciCodeIds.RealName, user.properties.real_name);
|
||||
self.setViewText(MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD'));
|
||||
self.setViewText(MciCodeIds.Sex, user.properties.sex);
|
||||
self.setViewText(MciCodeIds.Loc, user.properties.location);
|
||||
self.setViewText(MciCodeIds.Affils, user.properties.affiliation);
|
||||
self.setViewText(MciCodeIds.Email, user.properties.email_address);
|
||||
self.setViewText(MciCodeIds.Web, user.properties.web_address);
|
||||
self.setViewText(MciCodeIds.TermHeight, user.properties.term_height.toString());
|
||||
self.setViewText('menu', MciCodeIds.RealName, user.properties.real_name);
|
||||
self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD'));
|
||||
self.setViewText('menu', MciCodeIds.Sex, user.properties.sex);
|
||||
self.setViewText('menu', MciCodeIds.Loc, user.properties.location);
|
||||
self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation);
|
||||
self.setViewText('menu', MciCodeIds.Email, user.properties.email_address);
|
||||
self.setViewText('menu', MciCodeIds.Web, user.properties.web_address);
|
||||
self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString());
|
||||
|
||||
|
||||
var themeView = self.getView(MciCodeIds.Theme);
|
||||
|
@ -225,4 +216,6 @@ UserConfigModule.prototype.mciReady = function(mciData, cb) {
|
|||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
// ENiGMA½
|
||||
const setClientTheme = require('./theme.js').setClientTheme;
|
||||
const clientConnections = require('./client_connections.js').clientConnections;
|
||||
const userDb = require('./database.js').dbs.user;
|
||||
const StatLog = require('./stat_log.js');
|
||||
const logger = require('./logger.js');
|
||||
|
||||
|
@ -21,9 +20,8 @@ function userLogin(client, username, password, cb) {
|
|||
// :TODO: if username exists, record failed login attempt to properties
|
||||
// :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true
|
||||
|
||||
cb(err);
|
||||
} else {
|
||||
const now = new Date();
|
||||
return cb(err);
|
||||
}
|
||||
const user = client.user;
|
||||
|
||||
//
|
||||
|
@ -31,7 +29,7 @@ function userLogin(client, username, password, cb) {
|
|||
// Loop through active connections -- which includes the current --
|
||||
// and check for matching user ID. If the count is > 1, disallow.
|
||||
//
|
||||
var existingClientConnection;
|
||||
let existingClientConnection;
|
||||
clientConnections.forEach(function connEntry(cc) {
|
||||
if(cc.user !== user && cc.user.userId === user.userId) {
|
||||
existingClientConnection = cc;
|
||||
|
@ -49,7 +47,9 @@ function userLogin(client, username, password, cb) {
|
|||
var existingConnError = new Error('Already logged in as supplied user');
|
||||
existingConnError.existingConn = true;
|
||||
|
||||
return cb(existingClientConnection);
|
||||
// :TODO: We should use EnigError & pass existing connection as second param
|
||||
|
||||
return cb(existingConnError);
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,6 +81,5 @@ function userLogin(client, username, password, cb) {
|
|||
cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
let uuid = require('node-uuid');
|
||||
let assert = require('assert');
|
||||
let _ = require('lodash');
|
||||
let createHash = require('crypto').createHash;
|
||||
const createHash = require('crypto').createHash;
|
||||
|
||||
exports.createNamedUUID = createNamedUUID;
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ function VerticalMenuView(options) {
|
|||
|
||||
self.viewWindow = {
|
||||
top : self.focusedItemIndex,
|
||||
bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1
|
||||
bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -107,6 +107,7 @@ VerticalMenuView.prototype.redraw = function() {
|
|||
delete this.oldDimens;
|
||||
}
|
||||
|
||||
if(this.items.length) {
|
||||
let row = this.position.row;
|
||||
for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) {
|
||||
this.items[i].row = row;
|
||||
|
@ -114,6 +115,7 @@ VerticalMenuView.prototype.redraw = function() {
|
|||
this.items[i].focused = this.focusedItemIndex === i;
|
||||
this.drawItem(i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VerticalMenuView.prototype.setHeight = function(height) {
|
||||
|
@ -171,7 +173,7 @@ 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;
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
}
|
||||
|
||||
VerticalMenuView.super_.prototype.setItems.call(this, items);
|
||||
|
@ -179,6 +181,14 @@ VerticalMenuView.prototype.setItems = function(items) {
|
|||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
VerticalMenuView.prototype.removeItem = function(index) {
|
||||
if(this.items && this.items.length) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
}
|
||||
|
||||
VerticalMenuView.super_.prototype.removeItem.call(this, index);
|
||||
};
|
||||
|
||||
// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view!
|
||||
|
||||
VerticalMenuView.prototype.focusNext = function() {
|
||||
|
|
|
@ -6,7 +6,6 @@ var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
|||
var menuUtil = require('./menu_util.js');
|
||||
var asset = require('./asset.js');
|
||||
var ansi = require('./ansi_term.js');
|
||||
const Log = require('./logger.js');
|
||||
|
||||
// deps
|
||||
var events = require('events');
|
||||
|
@ -74,8 +73,9 @@ function ViewController(options) {
|
|||
self.switchFocus(actionForKey.viewId);
|
||||
self.submitForm(key);
|
||||
} else if(_.isString(actionForKey.action)) {
|
||||
const formData = self.getFocusedView() ? self.getFormData() : { };
|
||||
self.handleActionWrapper(
|
||||
{ ch : ch, key : key }, // formData
|
||||
Object.assign( { ch : ch, key : key }, formData ), // formData + key info
|
||||
actionForKey); // actionBlock
|
||||
}
|
||||
} else {
|
||||
|
@ -116,6 +116,7 @@ function ViewController(options) {
|
|||
self.emit('submit', this.getFormData(key));
|
||||
};
|
||||
|
||||
// :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them
|
||||
this.getLogFriendlyFormData = function(formData) {
|
||||
// :TODO: these fields should be part of menu.json sensitiveMembers[]
|
||||
var safeFormData = _.cloneDeep(formData);
|
||||
|
@ -143,8 +144,10 @@ function ViewController(options) {
|
|||
var mci = mciMap[name];
|
||||
var view = self.mciViewFactory.createFromMCI(mci);
|
||||
|
||||
if(view && false === self.noInput) {
|
||||
if(view) {
|
||||
if(false === self.noInput) {
|
||||
view.on('action', self.viewActionListener);
|
||||
}
|
||||
|
||||
self.addView(view);
|
||||
}
|
||||
|
@ -447,6 +450,12 @@ ViewController.prototype.setFocus = function(focused) {
|
|||
this.setViewFocusWithEvents(this.focusedView, focused);
|
||||
};
|
||||
|
||||
ViewController.prototype.resetInitialFocus = function() {
|
||||
if(this.formInitialFocusId) {
|
||||
return this.switchFocus(this.formInitialFocusId);
|
||||
}
|
||||
};
|
||||
|
||||
ViewController.prototype.switchFocus = function(id) {
|
||||
//
|
||||
// Perform focus switching validation now
|
||||
|
@ -471,15 +480,19 @@ ViewController.prototype.switchFocus = function(id) {
|
|||
};
|
||||
|
||||
ViewController.prototype.nextFocus = function() {
|
||||
var nextId;
|
||||
let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId];
|
||||
|
||||
if(!this.focusedView) {
|
||||
nextId = this.views[this.firstId].id;
|
||||
} else {
|
||||
nextId = this.views[this.focusedView.id].nextId;
|
||||
// find the next view that accepts focus
|
||||
while(nextFocusView && nextFocusView.nextId) {
|
||||
nextFocusView = this.getView(nextFocusView.nextId);
|
||||
if(!nextFocusView || nextFocusView.acceptsFocus) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.switchFocus(nextId);
|
||||
if(nextFocusView && this.focusedView !== nextFocusView) {
|
||||
this.switchFocus(nextFocusView.id);
|
||||
}
|
||||
};
|
||||
|
||||
ViewController.prototype.setViewOrder = function(order) {
|
||||
|
@ -498,7 +511,6 @@ ViewController.prototype.setViewOrder = function(order) {
|
|||
}
|
||||
|
||||
if(viewIdOrder.length > 0) {
|
||||
var view;
|
||||
var count = viewIdOrder.length - 1;
|
||||
for(var i = 0; i < count; ++i) {
|
||||
this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1];
|
||||
|
@ -578,7 +590,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) {
|
|||
for(var c = 0; c < menuSubmit.length; ++c) {
|
||||
var actionBlock = menuSubmit[c];
|
||||
|
||||
if(_.isEqual(formData.value, actionBlock.value, self.actionBlockValueComparator)) {
|
||||
if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) {
|
||||
self.handleActionWrapper(formData, actionBlock);
|
||||
break; // there an only be one...
|
||||
}
|
||||
|
@ -589,6 +601,33 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) {
|
|||
|
||||
callback(null);
|
||||
},
|
||||
function loadActionKeys(callback) {
|
||||
if(!_.isObject(promptConfig) || !_.isArray(promptConfig.actionKeys)) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
promptConfig.actionKeys.forEach(ak => {
|
||||
//
|
||||
// * 'keys' must be present and be an array of key names
|
||||
// * If 'viewId' is present, key(s) will focus & submit on behalf
|
||||
// of the specified view.
|
||||
// * If 'action' is present, that action will be procesed when
|
||||
// triggered by key(s)
|
||||
//
|
||||
// Ultimately, create a map of key -> { action block }
|
||||
//
|
||||
if(!_.isArray(ak.keys)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ak.keys.forEach(kn => {
|
||||
self.actionKeyMap[kn] = ak;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function drawAllViews(callback) {
|
||||
self.redrawAll(initialFocusId);
|
||||
callback(null);
|
||||
|
@ -616,7 +655,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
|||
|
||||
var self = this;
|
||||
var formIdKey = options.formId ? options.formId.toString() : '0';
|
||||
var initialFocusId = 1; // default to first
|
||||
this.formInitialFocusId = 1; // default to first
|
||||
var formConfig;
|
||||
|
||||
// :TODO: honor options.withoutForm
|
||||
|
@ -669,7 +708,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
|||
function applyViewConfiguration(callback) {
|
||||
if(_.isObject(formConfig)) {
|
||||
self.applyViewConfig(formConfig, function configApplied(err, info) {
|
||||
initialFocusId = info.initialFocusId;
|
||||
self.formInitialFocusId = info.initialFocusId;
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
|
@ -706,7 +745,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
|||
for(var c = 0; c < confForFormId.length; ++c) {
|
||||
var actionBlock = confForFormId[c];
|
||||
|
||||
if(_.isEqual(formData.value, actionBlock.value, self.actionBlockValueComparator)) {
|
||||
if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) {
|
||||
self.handleActionWrapper(formData, actionBlock);
|
||||
break; // there an only be one...
|
||||
}
|
||||
|
@ -744,12 +783,12 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
|||
callback(null);
|
||||
},
|
||||
function drawAllViews(callback) {
|
||||
self.redrawAll(initialFocusId);
|
||||
self.redrawAll(self.formInitialFocusId);
|
||||
callback(null);
|
||||
},
|
||||
function setInitialViewFocus(callback) {
|
||||
if(initialFocusId) {
|
||||
self.switchFocus(initialFocusId);
|
||||
if(self.formInitialFocusId) {
|
||||
self.switchFocus(self.formInitialFocusId);
|
||||
}
|
||||
callback(null);
|
||||
}
|
||||
|
@ -792,7 +831,7 @@ ViewController.prototype.getFormData = function(key) {
|
|||
|
||||
}
|
||||
*/
|
||||
var formData = {
|
||||
const formData = {
|
||||
id : this.formId,
|
||||
submitId : this.focusedView.id,
|
||||
value : {},
|
||||
|
@ -802,36 +841,24 @@ ViewController.prototype.getFormData = function(key) {
|
|||
formData.key = key;
|
||||
}
|
||||
|
||||
var viewData;
|
||||
var view;
|
||||
for(var id in this.views) {
|
||||
let viewData;
|
||||
_.each(this.views, view => {
|
||||
try {
|
||||
view = this.views[id];
|
||||
// don't fill forms with static, non user-editable data data
|
||||
if(!view.acceptsInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewData = view.getData();
|
||||
if(!_.isUndefined(viewData)) {
|
||||
if(_.isString(view.submitArgName)) {
|
||||
formData.value[view.submitArgName] = viewData;
|
||||
} else {
|
||||
formData.value[id] = viewData;
|
||||
}
|
||||
if(_.isUndefined(viewData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
formData.value[ view.submitArgName ? view.submitArgName : view.id ] = viewData;
|
||||
} catch(e) {
|
||||
this.client.log.error(e); // :TODO: Log better ;)
|
||||
}
|
||||
this.client.log.error( { error : e.message }, 'Exception caught gathering form data' );
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/*
|
||||
ViewController.prototype.formatMenuArgs = function(args) {
|
||||
var self = this;
|
||||
|
||||
return _.mapValues(args, function val(value) {
|
||||
if('string' === typeof value) {
|
||||
return self.formatMCIString(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
*/
|
|
@ -1,16 +1,19 @@
|
|||
# About ENiGMA½
|
||||
|
||||
## High Level Feature Overview
|
||||
* 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 JS based mods
|
||||
* Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows)
|
||||
* Unlimited multi node support (for all those BBS "callers"!)
|
||||
* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods
|
||||
* MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles
|
||||
* Telnet & SSH access built in. Additional servers are easy to implement & plug in
|
||||
* Telnet & **SSH** access built in. Additional servers are easy to implement
|
||||
* [CP437](http://www.ascii-codes.com/) and UTF-8 output
|
||||
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior.
|
||||
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior
|
||||
* [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support
|
||||
* Renegade style pipe codes
|
||||
* [SQLite](http://sqlite.org/) storage of users and message areas
|
||||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password storage
|
||||
* Door support including common dropfile formats and [DOSEMU](http://www.dosemu.org/)
|
||||
* Renegade style pipe color codes
|
||||
* [SQLite](http://sqlite.org/) storage of users, message areas, and so on
|
||||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
|
||||
* [Door support](doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/) support!
|
||||
* [Bunyan](https://github.com/trentm/node-bunyan) logging
|
||||
* [Message networks](msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export
|
||||
* [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](web_server.md). Legacy X/Y/Z modem also supported!
|
||||
* Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more!
|
|
@ -0,0 +1,69 @@
|
|||
# File Archives & Archivers
|
||||
ENiGMA½ can detect and process various archive formats such as zip and arj for a variety of tasks from file upload processing to EchoMail bundle compress/decompression. The `archives` section of `config.hjson` is used to override defaults, add new handlers, and so on.
|
||||
|
||||
## Archivers
|
||||
Archivers are manged via the `archives:archivers` configuration block of `config.hjson`. Each entry in this section defines an **external archiver** that can be referenced in other sections of `config.hjson` as and in code. Entries define how to `compress`, `decompress` (a full archive), `list`, and `extract` (specific files from an archive).
|
||||
|
||||
### Predefined Archivers
|
||||
The following archivers are pre-configured in ENiGMA½ as of this writing. Remember that you can override settings or add new handlers!
|
||||
|
||||
#### ZZip
|
||||
* Formats: .7z, .bzip2, .zip, .gzip/.gz, and more
|
||||
* Key: `7Zip`
|
||||
* Homepage/package: [7-zip.org](http://www.7-zip.org/). Generally obtained from a `p7zip` package in *nix environments. See http://p7zip.sourceforge.net/ for details.
|
||||
|
||||
#### Lha
|
||||
* Formats: <a href="https://en.wikipedia.org/wiki/LHA_(file_format)">LHA</a> files such as .lzh.
|
||||
* Key: `Lha`
|
||||
* Homepage/package: `lhasa` on most *nix environments. See also https://fragglet.github.io/lhasa/ and http://www2m.biglobe.ne.jp/~dolphin/lha/lha-unix.htm
|
||||
|
||||
#### Arj
|
||||
* Formats: .arj
|
||||
* Key: `Arj`
|
||||
* Homepage/package: `arj` on most *nix environments.
|
||||
|
||||
### Archiver Configuration
|
||||
Archiver entries in `config.hjson` are mostly self explanatory with the exception of `list` commands that require some additional information. The `args` member for an entry is an array of arguments to pass to `cmd`. Some variables are available to `args` that will be expanded by the system:
|
||||
|
||||
* `{archivePath}` (all): Path to the archive
|
||||
* `{fileList}` (compress, extract): List of file(s) to compress or extract
|
||||
* `{extractPath}` (decompress, extract): Path to extract *to*
|
||||
|
||||
For `list` commands, the `entryMatch` key must be provided. This key should provide a regular expression that matches two sub groups: One for uncompressed file byte sizes (sub group 1) and the other for file names (sub group 2). An optional `entryGroupOrder` can be supplied to change the default sub group order.
|
||||
|
||||
#### Example Archiver Configuration
|
||||
```
|
||||
7Zip: {
|
||||
compress: {
|
||||
cmd: 7za,
|
||||
args: [ "a", "-tzip", "{archivePath}", "{fileList}" ]
|
||||
}
|
||||
decompress: {
|
||||
cmd: 7za,
|
||||
args: [ "e", "-o{extractPath}", "{archivePath}" ]
|
||||
}
|
||||
list: {
|
||||
cmd: 7za,
|
||||
args: [ "l", "{archivePath}" ]
|
||||
entryMatch: "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$",
|
||||
}
|
||||
extract: {
|
||||
cmd: 7za,
|
||||
args [ "e", "-o{extractPath}", "{archivePath}", "{fileList}" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Archive Formats
|
||||
Archive formats can be defined such that ENiGMA½ can detect them by signature or extension, then utilize the correct *archiver* to process them. Formats are defined in the `archives:formats` key in `config.hjson`. Many differnet types come pre-configured (see `core/config.js`).
|
||||
|
||||
### Example Archive Format Configuration
|
||||
```
|
||||
zip: {
|
||||
sig: "504b0304" /* byte signature in HEX */
|
||||
offset: 0
|
||||
exts: [ "zip" ]
|
||||
handler: 7Zip /* points to a defined archiver */
|
||||
desc: "ZIP Archive"
|
||||
}
|
||||
```
|
|
@ -1,10 +1,10 @@
|
|||
# Configuration
|
||||
Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.org/) files. HJSON is just like JSON but simplified and much more resilient to human error.
|
||||
|
||||
## System Configuraiton
|
||||
## System Configuration
|
||||
The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `~/.config/enigma-bbs/config.hjson` though you can override this with the `--config` parameter when invoking `main.js`. Values found in core/config.js may be overridden by simply providing the object members you wish replace.
|
||||
|
||||
**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 Windows installations, e.g. `C:\Users\NuSkooler\.config\enigma-bbs\config.hjson`
|
||||
|
||||
### oputil.js
|
||||
Please see `oputil.js config` for configuration generation options.
|
||||
|
@ -24,29 +24,19 @@ general: {
|
|||
}
|
||||
```
|
||||
|
||||
(Note the very slightly different syntax. **You can use standard JSON if you wish**)
|
||||
|
||||
### Specific Areas of Interest
|
||||
* [Menu System](menu_system.md)
|
||||
* [Message Conferences](msg_conf_area.md)
|
||||
* [Message Networks](msg_networks.md)
|
||||
* [File Base](file_base.md)
|
||||
* [File Archives & Archivers](archives.md)
|
||||
* [Doors](doors.md)
|
||||
* [MCI Codes](mci.md)
|
||||
* [Web Server](web_server.md)
|
||||
...and other stuff [in the /docs directory](./)
|
||||
|
||||
#### Archivers
|
||||
External archivers can be configured for various tasks such as EchoMail bundle handling.
|
||||
|
||||
TODO: Document further inc. Members & defaults
|
||||
|
||||
**Example**:
|
||||
|
||||
```hjson
|
||||
archivers: {'
|
||||
zip: {
|
||||
// byte signature in HEX of ZIP archives
|
||||
sig: "504b0304"
|
||||
// offset of sig
|
||||
offset: 0
|
||||
compressCmd: "7za"
|
||||
compressArgs: [ "a", "-tzip", "{archivePath}", "{fileList}" ]
|
||||
decompressCmd: "7za"
|
||||
decompressArgs: [ "e", "-o{extractPath}", "{archivePath}" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### A Sample Configuration
|
||||
Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked.
|
||||
|
@ -136,4 +126,4 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all
|
|||
```
|
||||
|
||||
## Menus
|
||||
TODO: Documentation on menu.hjson, etc.
|
||||
See [the menu system docs](menu_system.md)
|
|
@ -0,0 +1,89 @@
|
|||
# File Bases
|
||||
Starting with version 0.0.4-alpha, ENiGMA½ has support for File Bases! Documentation below covers setup of file area(s), but first some information on what to expect:
|
||||
|
||||
## A Different Appoach
|
||||
ENiGMA½ has strayed away from the old familure setup here and instead takes a more modern approach:
|
||||
* [Gazelle](https://whatcd.github.io/Gazelle/) inspired system for searching & browsing files
|
||||
* No File Conferences (just areas!)
|
||||
* File Areas are still around but should generally be used less. Instead, files can have one or more tags. Think things like `dos.retro`, `pc.warez`, `games`, etc.
|
||||
* Temporary web (http:// or https://) download links in additional to standard X/Y/Z protocol support
|
||||
* Users can star rate files & search/filter by ratings
|
||||
* Concept of user defined filters
|
||||
|
||||
## Other bells and whistles
|
||||
* A given area can span one to many physical storage locations
|
||||
* Upload processor can extract and use `FILE_ID.DIZ`/`DESC.SDI`, for standard descriptions as well as `README.TXT`, `*.NFO`, and so on for longer descriptions
|
||||
* Upload processor also attempts release year estimation by scanning prementioned description file(s)
|
||||
* Fast indexed Full Text Search (FTS)
|
||||
* Duplicates validated by SHA-256
|
||||
|
||||
## Configuration
|
||||
Like many things in ENiGMA½, configuration of file base(s) is handled via `config.hjson` -- specifically in the `fileBase` section.
|
||||
|
||||
```hjson
|
||||
fileBase: {
|
||||
areaStoragePrefix: /path/to/somewhere/
|
||||
|
||||
storageTags: {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
areas: {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(Take a look at `core/config.js` for additional keys that may be overridden)
|
||||
|
||||
### Storage tags
|
||||
**Storage Tags** define paths to a physical (file) storage location that can later be referenced in a file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths are relative to the value set by the `areaStoragePrefix` key. Below is an example defining a both a relative and fully qualified path each attached to a storage tag:
|
||||
|
||||
```hjson
|
||||
storageTags: {
|
||||
retro_pc: "retro_pc" // relative
|
||||
retro_pc_bbs: "retro_pc/bbs" // still relative!
|
||||
bbs_stuff: "/path/to/bbs_stuff_storage" // fully qualified
|
||||
}
|
||||
```
|
||||
|
||||
### Areas
|
||||
File base **Areas** are configured using the `fileBase::areas` configuration block in `config.hjson`. Each entry within `areas` must contain a `name`, `desc`, and `storageTags`. Remember that in ENiGMA½ while areas are important, they should generally be used less than in tradditional BBS software. It is recommended to favor the use of more **tags** over more areas.
|
||||
|
||||
Example areas section:
|
||||
```hjson
|
||||
areas: {
|
||||
retro_pc: {
|
||||
name: Retro PC
|
||||
desc: Oldschool PC/DOS
|
||||
storageTags: [ "retro_pc", "retro_pc_bbs" ]
|
||||
acs: {
|
||||
write: GM[users] /* optional, see ACS below */
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ACS
|
||||
If no `acs` block is supplied, the following defaults apply to an area:
|
||||
* `read` (list, download, etc.): `GM[users]`
|
||||
* `write` (upload): `GM[sysops]`
|
||||
|
||||
To override read and/or write ACS, supply a valid `acs` member.
|
||||
|
||||
#### Uploads
|
||||
Note that `storageTags` may contain *1:n* storage tag references. **Uploads in a particular area are stored in the first storage tag path**.
|
||||
|
||||
## Web Access
|
||||
Temporary web HTTP(S) URLs can be used to download files using the built in web server. Temporary links expire after `fileBase::web::expireMinutes`. The full URL given to users is built using `contentServers::web::domain` and will default to HTTPS (http://) if enabled with a fallback to HTTP. The end result is users are given a temporary web link that may look something like this: `https://xibalba.l33t.codes:44512/f/h7JK`
|
||||
|
||||
See [Web Server](web_server.md) for more information.
|
||||
|
||||
## oputil
|
||||
The `oputil.js` +op utilty `fb` command has tools for managing file bases. For example, to import existing files found within **all** storage locations tied to an area and set tags `tag1` and `tag2` to each import:
|
||||
|
||||
```bash
|
||||
oputil.js fb scan some_area --tags tag1,tag2
|
||||
```
|
||||
|
||||
See `oputil.js fb --help` for additional information.
|
|
@ -11,12 +11,14 @@ Under most Linux/UNIX like environments (Linux, BSD, OS X, ...) new users can s
|
|||
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash
|
||||
```
|
||||
|
||||
For other environments such as Windows, see **The Manual Way** below.
|
||||
|
||||
## The Manual Way
|
||||
For Windows environments or if you simply like to do things manually, read on...
|
||||
|
||||
### Prerequisites
|
||||
* [Node.js](https://nodejs.org/) version **v4.2.x or higher**
|
||||
* :information_source: It is suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs
|
||||
* [Node.js](https://nodejs.org/) version **v6.x or higher**
|
||||
* :information_source: It is **highly** suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs
|
||||
* [Python](https://www.python.org/downloads/) 2.7.x
|
||||
* A compiler such as Clang or GCC for Linux/UNIX systems or a recent copy of Visual Studio ([Visual Studio Express](https://www.visualstudio.com/en-us/products/visual-studio-express-vs.aspx) editions OK) for Windows users. Note that you **should only need the Visual C++ component**.
|
||||
|
||||
|
@ -25,14 +27,16 @@ For Windows environments or if you simply like to do things manually, read on...
|
|||
If you're new to Node.js and/or do not care about Node itself and just want to get ENiGMA½ running these steps should get you going on most \*nix type enviornments (Please consider the `install.sh` approach unless you really want to manually install!):
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash
|
||||
nvm install 4.4.0
|
||||
nvm use 4.4.0
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
|
||||
nvm install 6
|
||||
nvm use 6
|
||||
```
|
||||
|
||||
|
||||
If the above completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment.
|
||||
|
||||
For Windows nvm-like systems exist ([nvm-windows](https://github.com/coreybutler/nvm-windows), ...) or [just download the installer](https://nodejs.org/en/download/).
|
||||
|
||||
|
||||
### Clone
|
||||
```bash
|
||||
git clone https://github.com/NuSkooler/enigma-bbs.git
|
||||
|
@ -56,9 +60,11 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
|
|||
#### Via oputil.js
|
||||
`oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**:
|
||||
|
||||
```bash
|
||||
./oputil.js config --new
|
||||
```
|
||||
|
||||
You wil be asked a series of basic questions.
|
||||
(You wil be asked a series of basic questions)
|
||||
|
||||
#### Example Starting Configuration
|
||||
Below is an _example_ configuration. It is recommended that you at least **start with a generated configuration using oputil.js described above**.
|
||||
|
@ -69,7 +75,7 @@ Below is an _example_ configuration. It is recommended that you at least **start
|
|||
boardName: Super Awesome BBS
|
||||
}
|
||||
|
||||
servers: {
|
||||
loginServers: {
|
||||
ssh: {
|
||||
privateKeyPass: YOUR_PK_PASS
|
||||
enabled: true /* set to false to disable the SSH server */
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
# MCI Codes
|
||||
|
||||
## Introduction
|
||||
ENiGMA½ supports a variety of MCI codes. Some **predefined** codes produce information about the current user, system, or other statistics while others are used to instanciate a **View**. MCI codes are two characters in length and are prefixed with a percent (%) symbol. Some MCI codes have additional options that may be set directly from the code itself while others -- and more advanced options -- are controlled via the current theme. Standard (non-focus) and focus colors are set by placing duplicate codes back to back in art files.
|
||||
|
||||
## Views
|
||||
A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is a Vertical Menu (`%VM`): Oldschool BBSers may recognize this as a lightbar menu.
|
||||
|
||||
### Available Views
|
||||
* Text Label (`TL`): Displays text
|
||||
* Edit Text (`ET`): Collect user input
|
||||
* Masked Edit Text (`ME`): Collect user input using a *mask*
|
||||
* Multi Line Text Edit (`MT`): Multi line edit control
|
||||
* Button (`BT`): A button
|
||||
* Vertical Menu (`VM`): A vertical menu aka a vertical lightbar
|
||||
* Horizontal Menu (`HM`): A horizontal menu aka a horizontal lightbar
|
||||
* Spinner Menu (`SM`): A spinner input control
|
||||
* Toggle Menu (`TM`): A toggle menu commonly used for Yes/No style input
|
||||
* Key Entry (`KE`): A *single* key input control
|
||||
|
||||
(Peek at `core/mci_view_factory.js` to see additional information on these)
|
||||
|
||||
## Predefined
|
||||
There are many predefined MCI codes that can be used anywhere on the system (placed in any art file). More are added all the time so also check out `core/predefined_mci.js` for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc.
|
||||
|
||||
* `BN`: Board Name
|
||||
* `VL`: Version *label*, e.g. "ENiGMA½ v0.0.3-alpha"
|
||||
* `VN`: Version *number*, eg.. "0.0.3-alpha"
|
||||
* `SN`: SysOp username
|
||||
* `SR`: SysOp real name
|
||||
* `SL`: SysOp location
|
||||
* `SA`: SysOp affiliations
|
||||
* `SS`: SysOp sex
|
||||
* `SE`: SysOp email address
|
||||
* `UN`: Current user's username
|
||||
* `UI`: Current user's user ID
|
||||
* `UG`: Current user's group membership(s)
|
||||
* `UR`: Current user's real name
|
||||
* `LO`: Current user's location
|
||||
* `UA`: Current user's age
|
||||
* `BD`: Current user's birthdate (using theme date format)
|
||||
* `US`: Current user's sex
|
||||
* `UE`: Current user's email address
|
||||
* `UW`: Current user's web address
|
||||
* `UF`: Current user's affiliations
|
||||
* `UT`: Current user's *theme ID* (e.g. "luciano_blocktronics")
|
||||
* `UC`: Current user's login/call count
|
||||
* `ND`: Current user's connected node number
|
||||
* `IP`: Current user's IP address
|
||||
* `ST`: Current user's connected server name (e.g. "Telnet" or "SSH")
|
||||
* `FN`: Current user's active file base filter name
|
||||
* `DN`: Current user's number of downloads
|
||||
* `DK`: Current user's download amount (formatted to appropriate bytes/megs/etc.)
|
||||
* `UP`: Current user's number of uploads
|
||||
* `UK`: Current user's upload amount (formatted to appropriate bytes/megs/etc.)
|
||||
* `NR`: Current user's upload/download ratio
|
||||
* `KR`: Current user's upload/download *bytes* ratio
|
||||
* `MS`: Current user's account creation date (using theme date format)
|
||||
* `PS`: Current user's post count
|
||||
* `PC`: Current user's post/call ratio
|
||||
* `MD`: Current user's status/viewing menu/activity
|
||||
* `MA`: Current user's active message area name
|
||||
* `MC`: Current user's active message conference name
|
||||
* `ML`: Current user's active message area description
|
||||
* `CM`: Current user's active message conference description
|
||||
* `SH`: Current user's term height
|
||||
* `SW`: Current user's term width
|
||||
* `DT`: Current date (using theme date format)
|
||||
* `CT`: Current time (using theme time format)
|
||||
* `OS`: System OS (Linux, Windows, etc.)
|
||||
* `OA`: System architecture (x86, x86_64, arm, etc.)
|
||||
* `SC`: System CPU model
|
||||
* `NV`: System underlying Node.js version
|
||||
* `AN`: Current active node count
|
||||
* `TC`: Total login/calls to system
|
||||
* `RR`: Displays a random rumor
|
||||
|
||||
A special `XY` MCI code may also be utilized for placement identification when creating menus.
|
||||
|
||||
## Properties & Theming
|
||||
Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`.
|
||||
|
||||
### Common Properties
|
||||
* `textStyle`: Sets the standard (non-focus) text style. See **Text Styles** below
|
||||
* `focusTextStyle`: Sets focus text style. See **Text Styles** below.
|
||||
* `itemSpacing`: Used to separate items in menus such as Vertical Menu and Horizontal Menu Views.
|
||||
* `height`: Sets the height of views such as menus that may be > 1 character in height
|
||||
* `width`: Sets the width of a view
|
||||
* `focus`: If set to `true`, establishes initial focus
|
||||
* `text`: (initial) text of a view
|
||||
* `submit`: If set to `true` any `accept` action upon this view will submit the encompassing **form**
|
||||
|
||||
These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files!
|
||||
|
||||
|
||||
#### Text Styles
|
||||
Standard style types available for `textStyle` and `focusTextStyle`:
|
||||
|
||||
* `normal`: Leaves text as-is. This is the default.
|
||||
* `upper`: ENIGMA BULLETIN BOARD SOFTWARE
|
||||
* `lower`: enigma bulletin board software
|
||||
* `title`: Enigma Bulletin Board Software
|
||||
* `first lower`: eNIGMA bULLETIN bOARD sOFTWARE
|
||||
* `small vowels`: eNiGMa BuLLeTiN BoaRD SoFTWaRe
|
||||
* `big vowels`: EniGMa bUllEtIn bOArd sOftwArE
|
||||
* `small i`: ENiGMA BULLETiN BOARD SOFTWARE
|
||||
* `mixed`: EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned)
|
||||
* `l33t`: 3n1gm4 bull371n b04rd 50f7w4r3
|
|
@ -1,13 +1,14 @@
|
|||
# Menu System
|
||||
ENiGMA½'s menu system is highly flexible and moddable. The possibilities are almost endless! By modifying your `menu.hjson` you will be able to create a custom look and feel unique to your board.
|
||||
|
||||
The default `menu.hjson` file lives within the `mods` directory. To specify another file, set the `menuFile` property in your `config.hjson` file:
|
||||
The default `menu.hjson` file lives within the `mods` directory. It is **highly recommended** to specify another file by setting the `menuFile` property in your `config.hjson` file:
|
||||
```hjson
|
||||
general: {
|
||||
/* Can also specify a full path */
|
||||
menuFile: mybbs.hjson
|
||||
}
|
||||
```
|
||||
(You can start by copying the default `menu.hjson` to `mybbs.hjson`)
|
||||
|
||||
## The Basics
|
||||
Like all configuration within ENiGMA½, menu configuration is done via a HJSON file. This file is located in the `mods` directory: `mods/menu.hjson`.
|
||||
|
@ -37,6 +38,7 @@ Now let's look at `matrix`, the `next` entry from `telnetConnected`:
|
|||
```hjson
|
||||
matrix: {
|
||||
art: matrix
|
||||
desc: Login Matrix
|
||||
form: {
|
||||
0: {
|
||||
VM: {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# Web Server
|
||||
ENiGMA½ comes with a built in *content server* for supporting both HTTP and HTTPS. Currently the [File Bases](file_base.md) registers routes for file downloads, and static files can also be served for your BBS. Other features will likely come in the future or you can easily write your own!
|
||||
|
||||
## Configuration
|
||||
By default the web server is not enabled. To enable it, you will need to at a minimum configure two keys in the `contentServers::web` section of `config.hjson`:
|
||||
|
||||
```hjson
|
||||
contentServers: {
|
||||
web: {
|
||||
domain: bbs.yourdomain.com
|
||||
|
||||
http: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will configure HTTP for port 8080 (override with `port`). To additionally enable HTTPS, you will need a PEM encoded SSL certificate and private key. Once obtained, simply enable the HTTPS server:
|
||||
```hjson
|
||||
contentServers: {
|
||||
web: {
|
||||
domain: bbs.yourdomain.com
|
||||
|
||||
https: {
|
||||
enabled: true
|
||||
port: 8443
|
||||
certPem: /path/to/your/cert.pem
|
||||
keyPem: /path/to/your/cert_private_key.pem
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Static Routes
|
||||
Static files live relative to the `contentServers::web::staticRoot` path which defaults to `enigma-bbs/www`.
|
||||
|
||||
### Custom Error Pages
|
||||
Customized error pages can be created for [HTTP error codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error) by providing a `<error_code>.html` file in the *static routes* area. For example: `404.html`.
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{ # this ensures the entire script is downloaded before execution
|
||||
|
||||
ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=4.4}
|
||||
ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=6}
|
||||
ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs}
|
||||
ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git}
|
||||
TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"`
|
||||
|
@ -22,7 +22,7 @@ _____________________ _____ ____________________ __________\\_ /
|
|||
|
||||
ENiGMA½ will be installed to ${ENIGMA_INSTALL_DIR}, from source ${ENIGMA_SOURCE}.
|
||||
|
||||
ENiGMA½ requires Node, v${ENIGMA_NODE_VERSION} will be installed via nvm. If you already have nvm installed, this install script will update it to the latest version.
|
||||
ENiGMA½ requires Node.js. Version ${ENIGMA_NODE_VERSION}.x current will be installed via nvm. If you already have nvm installed, this install script will update it to the latest version.
|
||||
|
||||
If this isn't what you were expecting, hit ctrl-c now. Installation will continue in ${WAIT_BEFORE_INSTALL} seconds...
|
||||
|
||||
|
@ -106,6 +106,21 @@ If this is the first time you've installed ENiGMA½, you now need to generate a
|
|||
cd ${ENIGMA_INSTALL_DIR}
|
||||
./oputil.js config --new
|
||||
|
||||
Additionally, the following support binaires are recommended:
|
||||
7zip: Archive support
|
||||
Debian/Ubuntu : apt-get install p7zip
|
||||
CentOS : yum install p7zip
|
||||
|
||||
Lha: Archive support
|
||||
Debian/Ubuntu : apt-get install lhasa
|
||||
|
||||
Arj: Archive support
|
||||
Debian/Ubuntu : apt-get install arj
|
||||
|
||||
sz/rz: Various X/Y/Z modem support
|
||||
Debian/Ubuntu : apt-get install lrzsz
|
||||
CentOS : yum install lrzsz
|
||||
|
||||
EndOfMessage
|
||||
echo -e "\e[39m"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
_____________________ _____ ____________________ __________\_ /
|
||||
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
|
||||
// __|___// | \// |// | \// | | \// \ /___ /_____
|
||||
/____ _____| __________ ___|__| ____| \ / _____ \
|
||||
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
|
||||
/__ _\
|
||||
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
|
||||
|
||||
-------------------------------------------------------------------------------
|
|
@ -1,23 +1,21 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
let MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
let DropFile = require('../core/dropfile.js').DropFile;
|
||||
let door = require('../core/door.js');
|
||||
let theme = require('../core/theme.js');
|
||||
let ansi = require('../core/ansi_term.js');
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const DropFile = require('../core/dropfile.js').DropFile;
|
||||
const door = require('../core/door.js');
|
||||
const theme = require('../core/theme.js');
|
||||
const ansi = require('../core/ansi_term.js');
|
||||
|
||||
let async = require('async');
|
||||
let assert = require('assert');
|
||||
let paths = require('path');
|
||||
let _ = require('lodash');
|
||||
let mkdirs = require('fs-extra').mkdirs;
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const paths = require('path');
|
||||
const _ = require('lodash');
|
||||
const mkdirs = require('fs-extra').mkdirs;
|
||||
|
||||
// :TODO: This should really be a system module... needs a little work to allow for such
|
||||
|
||||
exports.getModule = AbracadabraModule;
|
||||
|
||||
let activeDoorNodeInstances = {};
|
||||
const activeDoorNodeInstances = {};
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Abracadabra',
|
||||
|
@ -60,20 +58,20 @@ exports.moduleInfo = {
|
|||
|
||||
:TODO: See Mystic & others for other arg options that we may need to support
|
||||
*/
|
||||
function AbracadabraModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
let self = this;
|
||||
exports.getModule = class AbracadabraModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = options.menuConfig.config;
|
||||
|
||||
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts!
|
||||
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
||||
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
|
||||
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
||||
|
||||
this.config.nodeMax = this.config.nodeMax || 0;
|
||||
this.config.args = this.config.args || [];
|
||||
}
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
|
@ -82,7 +80,9 @@ function AbracadabraModule(options) {
|
|||
* Font support ala all other menus... or does this just work?
|
||||
*/
|
||||
|
||||
this.initSequence = function() {
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function validateNodeCount(callback) {
|
||||
|
@ -99,14 +99,15 @@ function AbracadabraModule(options) {
|
|||
|
||||
if(_.isString(self.config.tooManyArt)) {
|
||||
theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() {
|
||||
theme.displayThemedPause( { client : self.client }, function keyPressed() {
|
||||
self.pausePrompt( () => {
|
||||
callback(new Error('Too many active instances'));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.client.term.write('\nToo many active instances. Try again later.\n');
|
||||
|
||||
theme.displayThemedPause( { client : self.client }, function keyPressed() {
|
||||
// :TODO: Use MenuModule.pausePrompt()
|
||||
self.pausePrompt( () => {
|
||||
callback(new Error('Too many active instances'));
|
||||
});
|
||||
}
|
||||
|
@ -146,54 +147,51 @@ function AbracadabraModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.runDoor = function() {
|
||||
runDoor() {
|
||||
|
||||
const exeInfo = {
|
||||
cmd : self.config.cmd,
|
||||
args : self.config.args,
|
||||
io : self.config.io || 'stdio',
|
||||
encoding : self.config.encoding || self.client.term.outputEncoding,
|
||||
dropFile : self.dropFile.fileName,
|
||||
node : self.client.node,
|
||||
//inhSocket : self.client.output._handle.fd,
|
||||
cmd : this.config.cmd,
|
||||
args : this.config.args,
|
||||
io : this.config.io || 'stdio',
|
||||
encoding : this.config.encoding || this.client.term.outputEncoding,
|
||||
dropFile : this.dropFile.fileName,
|
||||
node : this.client.node,
|
||||
//inhSocket : this.client.output._handle.fd,
|
||||
};
|
||||
|
||||
const doorInstance = new door.Door(self.client, exeInfo);
|
||||
const doorInstance = new door.Door(this.client, exeInfo);
|
||||
|
||||
doorInstance.once('finished', () => {
|
||||
//
|
||||
// Try to clean up various settings such as scroll regions that may
|
||||
// have been set within the door
|
||||
//
|
||||
self.client.term.rawWrite(
|
||||
this.client.term.rawWrite(
|
||||
ansi.normal() +
|
||||
ansi.goto(self.client.term.termHeight, self.client.term.termWidth) +
|
||||
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
|
||||
ansi.setScrollRegion() +
|
||||
ansi.goto(self.client.term.termHeight, 0) +
|
||||
ansi.goto(this.client.term.termHeight, 0) +
|
||||
'\r\n\r\n'
|
||||
);
|
||||
|
||||
self.prevMenu();
|
||||
this.prevMenu();
|
||||
});
|
||||
|
||||
self.client.term.write(ansi.resetScreen());
|
||||
this.client.term.write(ansi.resetScreen());
|
||||
|
||||
doorInstance.run();
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(AbracadabraModule, MenuModule);
|
||||
|
||||
AbracadabraModule.prototype.leave = function() {
|
||||
AbracadabraModule.super_.prototype.leave.call(this);
|
||||
|
||||
leave() {
|
||||
super.leave();
|
||||
if(!this.lastError) {
|
||||
activeDoorNodeInstances[this.config.name] -= 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AbracadabraModule.prototype.finishedLoading = function() {
|
||||
finishedLoading() {
|
||||
this.runDoor();
|
||||
}
|
||||
};
|
Binary file not shown.
|
@ -1,33 +0,0 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
|
||||
|
||||
exports.getModule = ArtPoolModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Art Pool',
|
||||
desc : 'Display art from a pool of options',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
function ArtPoolModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
var config = this.menuConfig.config;
|
||||
|
||||
//
|
||||
// :TODO: General idea
|
||||
// * Break up some of MenuModule initSequence's calls into methods
|
||||
// * initSequence here basically has general "clear", "next", etc. as per normal
|
||||
// * Display art -> ooptinal pause -> display more if requested, etc.
|
||||
// * Finally exit & move on as per normal
|
||||
|
||||
}
|
||||
|
||||
require('util').inherits(ArtPoolModule, MenuModule);
|
||||
|
||||
MessageAreaModule.prototype.mciReady = function(mciData, cb) {
|
||||
this.standardMCIReadyHandler(mciData, cb);
|
||||
};
|
|
@ -36,28 +36,26 @@ const packageJson = require('../package.json');
|
|||
// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors
|
||||
// :TODO: ENH: Support nodeMax and tooManyArt
|
||||
|
||||
exports.getModule = BBSLinkModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'BBSLink',
|
||||
desc : 'BBSLink Access Module',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = class BBSLinkModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
function BBSLinkModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
var self = this;
|
||||
this.config = options.menuConfig.config;
|
||||
|
||||
this.config.host = this.config.host || 'games.bbslink.net';
|
||||
this.config.port = this.config.port || 23;
|
||||
}
|
||||
|
||||
this.initSequence = function() {
|
||||
var token;
|
||||
var randomKey;
|
||||
var clientTerminated;
|
||||
initSequence() {
|
||||
let token;
|
||||
let randomKey;
|
||||
let clientTerminated;
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
|
@ -180,17 +178,17 @@ function BBSLinkModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.simpleHttpRequest = function(path, headers, cb) {
|
||||
var getOpts = {
|
||||
simpleHttpRequest(path, headers, cb) {
|
||||
const getOpts = {
|
||||
host : this.config.host,
|
||||
path : path,
|
||||
headers : headers,
|
||||
};
|
||||
|
||||
var req = http.get(getOpts, function response(resp) {
|
||||
var data = '';
|
||||
const req = http.get(getOpts, function response(resp) {
|
||||
let data = '';
|
||||
|
||||
resp.on('data', function chunk(c) {
|
||||
data += c;
|
||||
|
@ -205,7 +203,5 @@ function BBSLinkModule(options) {
|
|||
req.on('error', function reqErr(err) {
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(BBSLinkModule, MenuModule);
|
||||
};
|
||||
|
|
268
mods/bbs_list.js
268
mods/bbs_list.js
|
@ -17,17 +17,13 @@ const _ = require('lodash');
|
|||
|
||||
// :TODO: add notes field
|
||||
|
||||
exports.getModule = BBSListModule;
|
||||
|
||||
const moduleInfo = {
|
||||
const moduleInfo = exports.moduleInfo = {
|
||||
name : 'BBS List',
|
||||
desc : 'List of other BBSes',
|
||||
author : 'Andrew Pamment',
|
||||
packageName : 'com.magickabbs.enigma.bbslist'
|
||||
};
|
||||
|
||||
exports.moduleInfo = moduleInfo;
|
||||
|
||||
const MciViewIds = {
|
||||
view : {
|
||||
BBSList : 1,
|
||||
|
@ -69,13 +65,106 @@ const SELECTED_MCI_NAME_TO_ENTRY = {
|
|||
SelectedBBSNotes : 'notes',
|
||||
};
|
||||
|
||||
function BBSListModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class BBSListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
this.menuMethods = {
|
||||
//
|
||||
// Validators
|
||||
//
|
||||
viewValidationListener : function(err, cb) {
|
||||
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
|
||||
if(errMsgView) {
|
||||
if(err) {
|
||||
errMsgView.setText(err.message);
|
||||
} else {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
}
|
||||
|
||||
this.initSequence = function() {
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
//
|
||||
// Key & submit handlers
|
||||
//
|
||||
addBBS : function(formData, extraArgs, cb) {
|
||||
self.displayAddScreen(cb);
|
||||
},
|
||||
deleteBBS : function(formData, extraArgs, cb) {
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
||||
|
||||
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
|
||||
// must be owner or +op
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
const entry = self.entries[self.selectedBBS];
|
||||
if(!entry) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
self.database.run(
|
||||
`DELETE FROM bbs_list
|
||||
WHERE id=?;`,
|
||||
[ entry.id ],
|
||||
err => {
|
||||
if (err) {
|
||||
self.client.log.error( { err : err }, 'Error deleting from BBS list');
|
||||
} else {
|
||||
self.entries.splice(self.selectedBBS, 1);
|
||||
|
||||
self.setEntries(entriesView);
|
||||
|
||||
if(self.entries.length > 0) {
|
||||
entriesView.focusPrevious();
|
||||
}
|
||||
|
||||
self.viewControllers.view.redrawAll();
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
},
|
||||
submitBBS : function(formData, extraArgs, cb) {
|
||||
|
||||
let ok = true;
|
||||
[ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => {
|
||||
if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) {
|
||||
ok = false;
|
||||
}
|
||||
});
|
||||
if(!ok) {
|
||||
// validators should prevent this!
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
self.database.run(
|
||||
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||
[ formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes ],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.error( { err : err }, 'Error adding to BBS list');
|
||||
}
|
||||
|
||||
self.clearAddForm();
|
||||
self.displayBBSList(true, cb);
|
||||
}
|
||||
);
|
||||
},
|
||||
cancelSubmit : function(formData, extraArgs, cb) {
|
||||
self.clearAddForm();
|
||||
self.displayBBSList(true, cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
async.series(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
|
@ -92,39 +181,42 @@ function BBSListModule(options) {
|
|||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.drawSelectedEntry = function(entry) {
|
||||
drawSelectedEntry(entry) {
|
||||
if(!entry) {
|
||||
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
||||
self.setViewText(MciViewIds.view[mciName], '');
|
||||
this.setViewText('view', MciViewIds.view[mciName], '');
|
||||
});
|
||||
} else {
|
||||
const youSubmittedFormat = config.youSubmittedFormat || '{submitter} (You!)';
|
||||
const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
|
||||
|
||||
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
|
||||
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
|
||||
if(MciViewIds.view[mciName]) {
|
||||
|
||||
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == self.client.user.userId) {
|
||||
self.setViewText(MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
|
||||
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) {
|
||||
this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry));
|
||||
} else {
|
||||
self.setViewText(MciViewIds.view[mciName], t);
|
||||
this.setViewText('view',MciViewIds.view[mciName], t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.setEntries = function(entriesView) {
|
||||
setEntries(entriesView) {
|
||||
const config = this.menuConfig.config;
|
||||
const listFormat = config.listFormat || '{bbsName}';
|
||||
const focusListFormat = config.focusListFormat || '{bbsName}';
|
||||
|
||||
entriesView.setItems(self.entries.map( e => stringFormat(listFormat, e) ) );
|
||||
entriesView.setFocusItems(self.entries.map( e => stringFormat(focusListFormat, e) ) );
|
||||
};
|
||||
entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) );
|
||||
entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) );
|
||||
}
|
||||
|
||||
displayBBSList(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
this.displayBBSList = function(clearScreen, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
|
@ -135,7 +227,7 @@ function BBSListModule(options) {
|
|||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
theme.displayThemedAsset(
|
||||
config.art.entries,
|
||||
self.menuConfig.config.art.entries,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
|
@ -238,9 +330,11 @@ function BBSListModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
displayAddScreen(cb) {
|
||||
const self = this;
|
||||
|
||||
this.displayAddScreen = function(cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
|
@ -248,7 +342,7 @@ function BBSListModule(options) {
|
|||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art.add,
|
||||
self.menuConfig.config.art.add,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
(err, artData) => {
|
||||
|
@ -284,117 +378,17 @@ function BBSListModule(options) {
|
|||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.clearAddForm = function() {
|
||||
clearAddForm() {
|
||||
[ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => {
|
||||
const v = self.viewControllers.add.getView(MciViewIds.add[mciName]);
|
||||
if(v) {
|
||||
v.setText('');
|
||||
}
|
||||
this.setViewText('add', MciViewIds.add[mciName], '');
|
||||
});
|
||||
};
|
||||
|
||||
this.menuMethods = {
|
||||
//
|
||||
// Validators
|
||||
//
|
||||
viewValidationListener : function(err, cb) {
|
||||
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
|
||||
if(errMsgView) {
|
||||
if(err) {
|
||||
errMsgView.setText(err.message);
|
||||
} else {
|
||||
errMsgView.clearText();
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
initDatabase(cb) {
|
||||
const self = this;
|
||||
|
||||
//
|
||||
// Key & submit handlers
|
||||
//
|
||||
addBBS : function(formData, extraArgs, cb) {
|
||||
self.displayAddScreen(cb);
|
||||
},
|
||||
deleteBBS : function(formData, extraArgs, cb) {
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
||||
|
||||
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
|
||||
// must be owner or +op
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
const entry = self.entries[self.selectedBBS];
|
||||
if(!entry) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
self.database.run(
|
||||
`DELETE FROM bbs_list
|
||||
WHERE id=?;`,
|
||||
[ entry.id ],
|
||||
err => {
|
||||
if (err) {
|
||||
self.client.log.error( { err : err }, 'Error deleting from BBS list');
|
||||
} else {
|
||||
self.entries.splice(self.selectedBBS, 1);
|
||||
|
||||
self.setEntries(entriesView);
|
||||
|
||||
if(self.entries.length > 0) {
|
||||
entriesView.focusPrevious();
|
||||
}
|
||||
|
||||
self.viewControllers.view.redrawAll();
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
},
|
||||
submitBBS : function(formData, extraArgs, cb) {
|
||||
|
||||
let ok = true;
|
||||
[ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => {
|
||||
if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) {
|
||||
ok = false;
|
||||
}
|
||||
});
|
||||
if(!ok) {
|
||||
// validators should prevent this!
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
self.database.run(
|
||||
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||
[ formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes ],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.error( { err : err }, 'Error adding to BBS list');
|
||||
}
|
||||
|
||||
self.clearAddForm();
|
||||
self.displayBBSList(true, cb);
|
||||
}
|
||||
);
|
||||
},
|
||||
cancelSubmit : function(formData, extraArgs, cb) {
|
||||
self.clearAddForm();
|
||||
self.displayBBSList(true, cb);
|
||||
}
|
||||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
var v = self.viewControllers.view.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
|
||||
this.initDatabase = function(cb) {
|
||||
async.series(
|
||||
[
|
||||
function openDatabase(callback) {
|
||||
|
@ -422,15 +416,15 @@ function BBSListModule(options) {
|
|||
callback(null);
|
||||
}
|
||||
],
|
||||
cb
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(BBSListModule, MenuModule);
|
||||
|
||||
BBSListModule.prototype.beforeArt = function(cb) {
|
||||
BBSListModule.super_.prototype.beforeArt.call(this, err => {
|
||||
beforeArt(cb) {
|
||||
super.beforeArt(err => {
|
||||
return err ? cb(err) : this.initDatabase(cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
|
||||
// deps
|
||||
|
@ -33,8 +33,9 @@ var MciViewIds = {
|
|||
InputArea : 3,
|
||||
};
|
||||
|
||||
// :TODO: needs converted to ES6 MenuModule subclass
|
||||
function ErcClientModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
MenuModule.prototype.ctorShim.call(this, options);
|
||||
|
||||
const self = this;
|
||||
this.config = options.menuConfig.config;
|
||||
|
|
|
@ -0,0 +1,321 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
|
||||
const FileBaseFilters = require('../core/file_base_filter.js');
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Area Filter Editor',
|
||||
desc : 'Module for adding, deleting, and modifying file base filters',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
editor : {
|
||||
searchTerms : 1,
|
||||
tags : 2,
|
||||
area : 3,
|
||||
sort : 4,
|
||||
order : 5,
|
||||
filterName : 6,
|
||||
navMenu : 7,
|
||||
|
||||
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
|
||||
selectedFilterInfo : 10, // { ...filter object ... }
|
||||
activeFilterInfo : 11, // { ...filter object ... }
|
||||
error : 12, // validation errors
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
|
||||
this.currentFilterIndex = 0; // into |filtersArray|
|
||||
|
||||
//
|
||||
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
|
||||
//
|
||||
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||
this.filtersArray.sort( (filterA, filterB) => {
|
||||
if(activeFilter) {
|
||||
if(filterA.uuid === activeFilter.uuid) {
|
||||
return -1;
|
||||
}
|
||||
if(filterB.uuid === activeFilter.uuid) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } );
|
||||
});
|
||||
|
||||
this.menuMethods = {
|
||||
saveFilter : (formData, extraArgs, cb) => {
|
||||
return this.saveCurrentFilter(formData, cb);
|
||||
|
||||
},
|
||||
prevFilter : (formData, extraArgs, cb) => {
|
||||
this.currentFilterIndex -= 1;
|
||||
if(this.currentFilterIndex < 0) {
|
||||
this.currentFilterIndex = this.filtersArray.length - 1;
|
||||
}
|
||||
this.loadDataForFilter(this.currentFilterIndex);
|
||||
return cb(null);
|
||||
},
|
||||
nextFilter : (formData, extraArgs, cb) => {
|
||||
this.currentFilterIndex += 1;
|
||||
if(this.currentFilterIndex >= this.filtersArray.length) {
|
||||
this.currentFilterIndex = 0;
|
||||
}
|
||||
this.loadDataForFilter(this.currentFilterIndex);
|
||||
return cb(null);
|
||||
},
|
||||
makeFilterActive : (formData, extraArgs, cb) => {
|
||||
const filters = new FileBaseFilters(this.client);
|
||||
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
|
||||
|
||||
this.updateActiveLabel();
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
newFilter : (formData, extraArgs, cb) => {
|
||||
this.currentFilterIndex = this.filtersArray.length; // next avail slot
|
||||
this.clearForm(MciViewIds.editor.searchTerms);
|
||||
return cb(null);
|
||||
},
|
||||
deleteFilter : (formData, extraArgs, cb) => {
|
||||
const filterUuid = this.filtersArray[this.currentFilterIndex].uuid;
|
||||
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
|
||||
|
||||
// remove from stored properties
|
||||
const filters = new FileBaseFilters(this.client);
|
||||
filters.remove(filterUuid);
|
||||
filters.persist( () => {
|
||||
|
||||
//
|
||||
// If the item was also the active filter, we need to make a new one active
|
||||
//
|
||||
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
|
||||
const newActive = this.filtersArray[this.currentFilterIndex];
|
||||
if(newActive) {
|
||||
filters.setActive(newActive.uuid);
|
||||
} else {
|
||||
// nothing to set active to
|
||||
this.client.user.removeProperty('file_base_filter_active_uuid');
|
||||
}
|
||||
}
|
||||
|
||||
// update UI
|
||||
this.updateActiveLabel();
|
||||
|
||||
if(this.filtersArray.length > 0) {
|
||||
this.loadDataForFilter(this.currentFilterIndex);
|
||||
} else {
|
||||
this.clearForm();
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
},
|
||||
|
||||
viewValidationListener : (err, cb) => {
|
||||
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
|
||||
let newFocusId;
|
||||
|
||||
if(errorView) {
|
||||
if(err) {
|
||||
errorView.setText(err.message);
|
||||
err.view.clearText(); // clear out the invalid data
|
||||
} else {
|
||||
errorView.clearText();
|
||||
}
|
||||
}
|
||||
|
||||
return cb(newFocusId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
||||
},
|
||||
function populateAreas(callback) {
|
||||
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
||||
|
||||
const areasView = vc.getView(MciViewIds.editor.area);
|
||||
if(areasView) {
|
||||
areasView.setItems( self.availAreas.map( a => a.name ) );
|
||||
}
|
||||
|
||||
self.updateActiveLabel();
|
||||
self.loadDataForFilter(self.currentFilterIndex);
|
||||
self.viewControllers.editor.resetInitialFocus();
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentFilter() {
|
||||
return this.filtersArray[this.currentFilterIndex];
|
||||
}
|
||||
|
||||
setText(mciId, text) {
|
||||
const view = this.viewControllers.editor.getView(mciId);
|
||||
if(view) {
|
||||
view.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveLabel() {
|
||||
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||
if(activeFilter) {
|
||||
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
|
||||
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
|
||||
}
|
||||
}
|
||||
|
||||
setFocusItemIndex(mciId, index) {
|
||||
const view = this.viewControllers.editor.getView(mciId);
|
||||
if(view) {
|
||||
view.setFocusItemIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
clearForm(newFocusId) {
|
||||
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
|
||||
this.setText(mciId, '');
|
||||
});
|
||||
|
||||
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
|
||||
this.setFocusItemIndex(mciId, 0);
|
||||
});
|
||||
|
||||
if(newFocusId) {
|
||||
this.viewControllers.editor.switchFocus(newFocusId);
|
||||
} else {
|
||||
this.viewControllers.editor.resetInitialFocus();
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedAreaTag(index) {
|
||||
if(0 === index) {
|
||||
return ''; // -ALL-
|
||||
}
|
||||
const area = this.availAreas[index];
|
||||
if(!area) {
|
||||
return '';
|
||||
}
|
||||
return area.areaTag;
|
||||
}
|
||||
|
||||
getOrderBy(index) {
|
||||
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
|
||||
}
|
||||
|
||||
setAreaIndexFromCurrentFilter() {
|
||||
let index;
|
||||
const filter = this.getCurrentFilter();
|
||||
if(filter) {
|
||||
// special treatment: areaTag saved as blank ("") if -ALL-
|
||||
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
this.setFocusItemIndex(MciViewIds.editor.area, index);
|
||||
}
|
||||
|
||||
setOrderByFromCurrentFilter() {
|
||||
let index;
|
||||
const filter = this.getCurrentFilter();
|
||||
if(filter) {
|
||||
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
this.setFocusItemIndex(MciViewIds.editor.order, index);
|
||||
}
|
||||
|
||||
setSortByFromCurrentFilter() {
|
||||
let index;
|
||||
const filter = this.getCurrentFilter();
|
||||
if(filter) {
|
||||
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
this.setFocusItemIndex(MciViewIds.editor.sort, index);
|
||||
}
|
||||
|
||||
getSortBy(index) {
|
||||
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
|
||||
}
|
||||
|
||||
setFilterValuesFromFormData(filter, formData) {
|
||||
filter.name = formData.value.name;
|
||||
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
|
||||
filter.terms = formData.value.searchTerms;
|
||||
filter.tags = formData.value.tags;
|
||||
filter.order = this.getOrderBy(formData.value.orderByIndex);
|
||||
filter.sort = this.getSortBy(formData.value.sortByIndex);
|
||||
}
|
||||
|
||||
saveCurrentFilter(formData, cb) {
|
||||
const filters = new FileBaseFilters(this.client);
|
||||
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
||||
|
||||
if(selectedFilter) {
|
||||
// *update* currently selected filter
|
||||
this.setFilterValuesFromFormData(selectedFilter, formData);
|
||||
filters.replace(selectedFilter.uuid, selectedFilter);
|
||||
} else {
|
||||
// add a new entry; note that UUID will be generated
|
||||
const newFilter = {};
|
||||
this.setFilterValuesFromFormData(newFilter, formData);
|
||||
|
||||
// set current to what we just saved
|
||||
newFilter.uuid = filters.add(newFilter);
|
||||
|
||||
// add to our array (at current index position)
|
||||
this.filtersArray[this.currentFilterIndex] = newFilter;
|
||||
}
|
||||
|
||||
return filters.persist(cb);
|
||||
}
|
||||
|
||||
loadDataForFilter(filterIndex) {
|
||||
const filter = this.filtersArray[filterIndex];
|
||||
if(filter) {
|
||||
this.setText(MciViewIds.editor.searchTerms, filter.terms);
|
||||
this.setText(MciViewIds.editor.tags, filter.tags);
|
||||
this.setText(MciViewIds.editor.filterName, filter.name);
|
||||
|
||||
this.setAreaIndexFromCurrentFilter();
|
||||
this.setSortByFromCurrentFilter();
|
||||
this.setOrderByFromCurrentFilter();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,648 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const ansi = require('../core/ansi_term.js');
|
||||
const theme = require('../core/theme.js');
|
||||
const FileEntry = require('../core/file_entry.js');
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
const createCleanAnsi = require('../core/string_util.js').createCleanAnsi;
|
||||
const FileArea = require('../core/file_base_area.js');
|
||||
const Errors = require('../core/enig_error.js').Errors;
|
||||
const ArchiveUtil = require('../core/archive_util.js');
|
||||
const Config = require('../core/config.js').config;
|
||||
const DownloadQueue = require('../core/download_queue.js');
|
||||
const FileAreaWeb = require('../core/file_area_web.js');
|
||||
const FileBaseFilters = require('../core/file_base_filter.js');
|
||||
|
||||
const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Area List',
|
||||
desc : 'Lists contents of file an file area',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
browse : 0,
|
||||
details : 1,
|
||||
detailsGeneral : 2,
|
||||
detailsNfo : 3,
|
||||
detailsFileList : 4,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
browse : {
|
||||
desc : 1,
|
||||
navMenu : 2,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
details : {
|
||||
navMenu : 1,
|
||||
infoXyTop : 2, // %XY starting position for info area
|
||||
infoXyBottom : 3,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
detailsGeneral : {
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
detailsNfo : {
|
||||
nfo : 1,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
detailsFileList : {
|
||||
fileList : 1,
|
||||
|
||||
customRangeStart : 10, // 10+ = customs
|
||||
},
|
||||
};
|
||||
|
||||
exports.getModule = class FileAreaList extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
if(options.extraArgs) {
|
||||
this.filterCriteria = options.extraArgs.filterCriteria;
|
||||
}
|
||||
|
||||
this.dlQueue = new DownloadQueue(this.client);
|
||||
|
||||
if(!this.filterCriteria) {
|
||||
this.filterCriteria = FileBaseFilters.getActiveFilter(this.client);
|
||||
}
|
||||
|
||||
if(_.isString(this.filterCriteria)) {
|
||||
this.filterCriteria = JSON.parse(this.filterCriteria);
|
||||
}
|
||||
|
||||
if(_.has(options, 'lastMenuResult.value')) {
|
||||
this.lastMenuResultValue = options.lastMenuResult.value;
|
||||
}
|
||||
|
||||
this.menuMethods = {
|
||||
nextFile : (formData, extraArgs, cb) => {
|
||||
if(this.fileListPosition + 1 < this.fileList.length) {
|
||||
this.fileListPosition += 1;
|
||||
|
||||
return this.displayBrowsePage(true, cb); // true=clerarScreen
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
prevFile : (formData, extraArgs, cb) => {
|
||||
if(this.fileListPosition > 0) {
|
||||
--this.fileListPosition;
|
||||
|
||||
return this.displayBrowsePage(true, cb); // true=clearScreen
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
viewDetails : (formData, extraArgs, cb) => {
|
||||
this.viewControllers.browse.setFocus(false);
|
||||
return this.displayDetailsPage(cb);
|
||||
},
|
||||
detailsQuit : (formData, extraArgs, cb) => {
|
||||
this.viewControllers.details.setFocus(false);
|
||||
return this.displayBrowsePage(true, cb); // true=clearScreen
|
||||
},
|
||||
toggleQueue : (formData, extraArgs, cb) => {
|
||||
this.dlQueue.toggle(this.currentFileEntry);
|
||||
this.updateQueueIndicator();
|
||||
return cb(null);
|
||||
},
|
||||
showWebDownloadLink : (formData, extraArgs, cb) => {
|
||||
return this.fetchAndDisplayWebDownloadLink(cb);
|
||||
},
|
||||
displayHelp : (formData, extraArgs, cb) => {
|
||||
return this.displayHelpPage(cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
enter() {
|
||||
super.enter();
|
||||
}
|
||||
|
||||
leave() {
|
||||
super.leave();
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
return {
|
||||
fileList : this.fileList,
|
||||
fileListPosition : this.fileListPosition,
|
||||
};
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
if(savedState) {
|
||||
this.fileList = savedState.fileList;
|
||||
this.fileListPosition = savedState.fileListPosition;
|
||||
}
|
||||
}
|
||||
|
||||
updateFileEntryWithMenuResult(cb) {
|
||||
if(!this.lastMenuResultValue) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
if(_.isNumber(this.lastMenuResultValue.rating)) {
|
||||
const fileId = this.fileList[this.fileListPosition];
|
||||
FileEntry.persistUserRating(fileId, this.client.user.userId, this.lastMenuResultValue.rating, err => {
|
||||
if(err) {
|
||||
this.client.log.warn( { error : err.message, fileId : fileId }, 'Failed to persist file rating' );
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
} else {
|
||||
return cb(null);
|
||||
}
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function preInit(callback) {
|
||||
return self.updateFileEntryWithMenuResult(callback);
|
||||
},
|
||||
function beforeArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayBrowsePage(false, err => {
|
||||
if(err && 'NORESULTS' === err.reasonCode) {
|
||||
self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults');
|
||||
}
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
() => {
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
populateCurrentEntryInfo(cb) {
|
||||
const config = this.menuConfig.config;
|
||||
const currEntry = this.currentFileEntry;
|
||||
|
||||
const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD';
|
||||
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
|
||||
const hashTagsSep = config.hashTagsSep || ', ';
|
||||
const isQueuedIndicator = config.isQueuedIndicator || 'Y';
|
||||
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
|
||||
|
||||
const entryInfo = currEntry.entryInfo = {
|
||||
fileId : currEntry.fileId,
|
||||
areaTag : currEntry.areaTag,
|
||||
areaName : area.name || 'N/A',
|
||||
areaDesc : area.desc || 'N/A',
|
||||
fileSha256 : currEntry.fileSha256,
|
||||
fileName : currEntry.fileName,
|
||||
desc : currEntry.desc || '',
|
||||
descLong : currEntry.descLong || '',
|
||||
userRating : currEntry.userRating,
|
||||
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
|
||||
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
|
||||
isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator,
|
||||
webDlLink : '', // :TODO: fetch web any existing web d/l link
|
||||
webDlExpire : '', // :TODO: fetch web d/l link expire time
|
||||
};
|
||||
|
||||
//
|
||||
// We need the entry object to contain meta keys even if they are empty as
|
||||
// consumers may very likely attempt to use them
|
||||
//
|
||||
const metaValues = FileEntry.getWellKnownMetaValues();
|
||||
metaValues.forEach(name => {
|
||||
const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A';
|
||||
entryInfo[_.camelCase(name)] = value;
|
||||
});
|
||||
|
||||
if(entryInfo.archiveType) {
|
||||
entryInfo.archiveTypeDesc = _.has(Config, [ 'archives', 'formats', entryInfo.archiveType, 'desc' ]) ?
|
||||
Config.archives.formats[entryInfo.archiveType].desc :
|
||||
entryInfo.archiveType;
|
||||
} else {
|
||||
entryInfo.archiveTypeDesc = 'N/A';
|
||||
}
|
||||
|
||||
entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported
|
||||
entryInfo.hashTags = entryInfo.hashTags || '(none)';
|
||||
|
||||
// create a rating string, e.g. "**---"
|
||||
const userRatingTicked = config.userRatingTicked || '*';
|
||||
const userRatingUnticked = config.userRatingUnticked || '';
|
||||
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
|
||||
entryInfo.userRatingString = new Array(entryInfo.userRating + 1).join(userRatingTicked);
|
||||
if(entryInfo.userRating < 5) {
|
||||
entryInfo.userRatingString += new Array( (5 - entryInfo.userRating) + 1).join(userRatingUnticked);
|
||||
}
|
||||
|
||||
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
|
||||
if(err) {
|
||||
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
|
||||
entryInfo.webDlExpire = '';
|
||||
} else {
|
||||
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
entryInfo.webDlLink = serveItem.url;
|
||||
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
populateCustomLabels(category, startId) {
|
||||
return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo);
|
||||
}
|
||||
|
||||
displayArtAndPrepViewController(name, options, cb) {
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function readyAndDisplayArt(callback) {
|
||||
if(options.clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art[name],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepeareViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers[name])) {
|
||||
const vcOpts = {
|
||||
client : self.client,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
if(!_.isUndefined(options.noInput)) {
|
||||
vcOpts.noInput = options.noInput;
|
||||
}
|
||||
|
||||
const vc = self.addViewController(name, new ViewController(vcOpts));
|
||||
|
||||
if('details' === name) {
|
||||
try {
|
||||
self.detailsInfoArea = {
|
||||
top : artData.mciMap.XY2.position,
|
||||
bottom : artData.mciMap.XY3.position,
|
||||
};
|
||||
} catch(e) {
|
||||
return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!'));
|
||||
}
|
||||
}
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
}
|
||||
|
||||
self.viewControllers[name].setFocus(true);
|
||||
return callback(null);
|
||||
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayBrowsePage(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function fetchEntryData(callback) {
|
||||
if(self.fileList) {
|
||||
return callback(null);
|
||||
}
|
||||
return self.loadFileIds(false, callback); // false=do not force
|
||||
},
|
||||
function checkEmptyResults(callback) {
|
||||
if(0 === self.fileList.length) {
|
||||
return callback(Errors.General('No results for criteria', 'NORESULTS'));
|
||||
}
|
||||
return callback(null);
|
||||
},
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback);
|
||||
},
|
||||
function loadCurrentFileInfo(callback) {
|
||||
self.currentFileEntry = new FileEntry();
|
||||
|
||||
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return self.populateCurrentEntryInfo(callback);
|
||||
});
|
||||
},
|
||||
function populateViews(callback) {
|
||||
if(_.isString(self.currentFileEntry.desc)) {
|
||||
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
|
||||
if(descView) {
|
||||
createCleanAnsi(
|
||||
self.currentFileEntry.desc,
|
||||
{ height : self.client.termHeight, width : descView.dimens.width },
|
||||
cleanDesc => {
|
||||
// :TODO: use cleanDesc -- need to finish createCleanAnsi() !!
|
||||
//descView.setText(cleanDesc);
|
||||
descView.setText( self.currentFileEntry.desc );
|
||||
|
||||
self.updateQueueIndicator();
|
||||
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.updateQueueIndicator();
|
||||
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayDetailsPage(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
self.populateCustomLabels('details', MciViewIds.details.customRangeStart);
|
||||
return callback(null);
|
||||
},
|
||||
function prepSection(callback) {
|
||||
return self.displayDetailsSection('general', false, callback);
|
||||
},
|
||||
function listenNavChanges(callback) {
|
||||
const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu);
|
||||
navMenu.setFocusItemIndex(0);
|
||||
|
||||
navMenu.on('index update', index => {
|
||||
const sectionName = {
|
||||
0 : 'general',
|
||||
1 : 'nfo',
|
||||
2 : 'fileList',
|
||||
}[index];
|
||||
|
||||
if(sectionName) {
|
||||
self.displayDetailsSection(sectionName, true);
|
||||
}
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayHelpPage(cb) {
|
||||
this.displayAsset(
|
||||
this.menuConfig.config.art.help,
|
||||
{ clearScreen : true },
|
||||
() => {
|
||||
this.client.waitForKeyPress( () => {
|
||||
return this.displayBrowsePage(true, cb);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fetchAndDisplayWebDownloadLink(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function generateLinkIfNeeded(callback) {
|
||||
|
||||
if(self.currentFileEntry.webDlExpireTime < moment()) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempDownload(
|
||||
self.client,
|
||||
self.currentFileEntry,
|
||||
{ expireTime : expireTime },
|
||||
(err, url) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.currentFileEntry.webDlExpireTime = expireTime;
|
||||
|
||||
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
self.currentFileEntry.entryInfo.webDlLink = url;
|
||||
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
},
|
||||
function updateActiveViews(callback) {
|
||||
self.updateCustomViewTextsWithFilter(
|
||||
'browse',
|
||||
MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo,
|
||||
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
|
||||
);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateQueueIndicator() {
|
||||
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
|
||||
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
|
||||
|
||||
this.currentFileEntry.entryInfo.isQueued = stringFormat(
|
||||
this.dlQueue.isQueued(this.currentFileEntry) ?
|
||||
isQueuedIndicator :
|
||||
isNotQueuedIndicator
|
||||
);
|
||||
|
||||
this.updateCustomViewTextsWithFilter(
|
||||
'browse',
|
||||
MciViewIds.browse.customRangeStart,
|
||||
this.currentFileEntry.entryInfo,
|
||||
{ filter : [ '{isQueued}' ] }
|
||||
);
|
||||
}
|
||||
|
||||
cacheArchiveEntries(cb) {
|
||||
// check cache
|
||||
if(this.currentFileEntry.archiveEntries) {
|
||||
return cb(null, 'cache');
|
||||
}
|
||||
|
||||
const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag);
|
||||
if(!areaInfo) {
|
||||
return cb(Errors.Invalid('Invalid area tag'));
|
||||
}
|
||||
|
||||
const filePath = this.currentFileEntry.filePath;
|
||||
const archiveUtil = ArchiveUtil.getInstance();
|
||||
|
||||
archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.currentFileEntry.archiveEntries = entries;
|
||||
return cb(null, 're-cached');
|
||||
});
|
||||
}
|
||||
|
||||
populateFileListing() {
|
||||
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
|
||||
|
||||
if(this.currentFileEntry.entryInfo.archiveType) {
|
||||
this.cacheArchiveEntries( (err, cacheStatus) => {
|
||||
if(err) {
|
||||
// :TODO: Handle me!!!
|
||||
fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck
|
||||
return;
|
||||
}
|
||||
|
||||
if('re-cached' === cacheStatus) {
|
||||
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here?
|
||||
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
|
||||
|
||||
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
|
||||
fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) );
|
||||
|
||||
fileListView.redraw();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] );
|
||||
}
|
||||
}
|
||||
|
||||
displayDetailsSection(sectionName, clearArea, cb) {
|
||||
const self = this;
|
||||
const name = `details${_.upperFirst(sectionName)}`;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function detachPrevious(callback) {
|
||||
if(self.lastDetailsViewController) {
|
||||
self.lastDetailsViewController.detachClientEvents();
|
||||
}
|
||||
return callback(null);
|
||||
},
|
||||
function prepArtAndViewController(callback) {
|
||||
|
||||
function gotoTopPos() {
|
||||
self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1));
|
||||
}
|
||||
|
||||
gotoTopPos();
|
||||
|
||||
if(clearArea) {
|
||||
self.client.term.rawWrite(ansi.reset());
|
||||
|
||||
let pos = self.detailsInfoArea.top[0];
|
||||
const bottom = self.detailsInfoArea.bottom[0];
|
||||
|
||||
while(pos++ <= bottom) {
|
||||
self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
|
||||
}
|
||||
|
||||
gotoTopPos();
|
||||
}
|
||||
|
||||
return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
self.lastDetailsViewController = self.viewControllers[name];
|
||||
|
||||
switch(sectionName) {
|
||||
case 'nfo' :
|
||||
{
|
||||
const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo);
|
||||
if(nfoView) {
|
||||
nfoView.setText(self.currentFileEntry.entryInfo.descLong);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fileList' :
|
||||
self.populateFileListing();
|
||||
break;
|
||||
}
|
||||
|
||||
self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadFileIds(force, cb) {
|
||||
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) {
|
||||
this.fileListPosition = 0;
|
||||
FileEntry.findFiles(this.filterCriteria, (err, fileIds) => {
|
||||
this.fileList = fileIds;
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,218 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const DownloadQueue = require('../core/download_queue.js');
|
||||
const theme = require('../core/theme.js');
|
||||
const ansi = require('../core/ansi_term.js');
|
||||
const Errors = require('../core/enig_error.js').Errors;
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Base Download Queue Manager',
|
||||
desc : 'Module for interacting with download queue/batch',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
queueManager : 0,
|
||||
details : 1,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
queueManager : {
|
||||
queue : 1,
|
||||
navMenu : 2,
|
||||
},
|
||||
details : {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.dlQueue = new DownloadQueue(this.client);
|
||||
|
||||
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||
}
|
||||
|
||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||
|
||||
this.menuMethods = {
|
||||
downloadAll : (formData, extraArgs, cb) => {
|
||||
const modOpts = {
|
||||
extraArgs : {
|
||||
sendQueue : this.dlQueue.items,
|
||||
direction : 'send',
|
||||
}
|
||||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
|
||||
},
|
||||
viewItemInfo : (formData, extraArgs, cb) => {
|
||||
},
|
||||
removeItem : (formData, extraArgs, cb) => {
|
||||
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
||||
if(!selectedItem) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
this.dlQueue.removeItems(selectedItem.fileId);
|
||||
|
||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
|
||||
},
|
||||
clearQueue : (formData, extraArgs, cb) => {
|
||||
this.dlQueue.clear();
|
||||
|
||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||
return this.removeItemsFromDownloadQueueView('all', cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
if(0 === this.dlQueue.items.length) {
|
||||
if(this.sendFileIds) {
|
||||
// we've finished everything up - just fall back
|
||||
return this.prevMenu();
|
||||
}
|
||||
|
||||
// Simply an empty D/L queue: Present a specialized "empty queue" page
|
||||
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function beforeArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayQueueManagerPage(false, callback);
|
||||
}
|
||||
],
|
||||
() => {
|
||||
return self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||
if(!queueView) {
|
||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
if('all' === itemIndex) {
|
||||
queueView.setItems([]);
|
||||
queueView.setFocusItems([]);
|
||||
} else {
|
||||
queueView.removeItem(itemIndex);
|
||||
}
|
||||
|
||||
queueView.redraw();
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
updateDownloadQueueView(cb) {
|
||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||
if(!queueView) {
|
||||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}';
|
||||
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
|
||||
|
||||
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
|
||||
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
|
||||
|
||||
queueView.redraw();
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
displayQueueManagerPage(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
return self.updateDownloadQueueView(callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayArtAndPrepViewController(name, options, cb) {
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function readyAndDisplayArt(callback) {
|
||||
if(options.clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art[name],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function prepeareViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers[name])) {
|
||||
const vcOpts = {
|
||||
client : self.client,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
if(!_.isUndefined(options.noInput)) {
|
||||
vcOpts.noInput = options.noInput;
|
||||
}
|
||||
|
||||
const vc = self.addViewController(name, new ViewController(vcOpts));
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds[name],
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
}
|
||||
|
||||
self.viewControllers[name].setFocus(true);
|
||||
return callback(null);
|
||||
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
|
||||
const FileBaseFilters = require('../core/file_base_filter.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Base Search',
|
||||
desc : 'Module for quickly searching the file base',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
search : {
|
||||
searchTerms : 1,
|
||||
search : 2,
|
||||
tags : 3,
|
||||
area : 4,
|
||||
orderBy : 5,
|
||||
sort : 6,
|
||||
advSearch : 7,
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileBaseSearch extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.menuMethods = {
|
||||
search : (formData, extraArgs, cb) => {
|
||||
const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
|
||||
return this.searchNow(formData, isAdvanced, cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
||||
},
|
||||
function populateAreas(callback) {
|
||||
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
||||
|
||||
const areasView = vc.getView(MciViewIds.search.area);
|
||||
areasView.setItems( self.availAreas.map( a => a.name ) );
|
||||
areasView.redraw();
|
||||
vc.switchFocus(MciViewIds.search.searchTerms);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedAreaTag(index) {
|
||||
if(0 === index) {
|
||||
return ''; // -ALL-
|
||||
}
|
||||
const area = this.availAreas[index];
|
||||
if(!area) {
|
||||
return '';
|
||||
}
|
||||
return area.areaTag;
|
||||
}
|
||||
|
||||
getOrderBy(index) {
|
||||
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
|
||||
}
|
||||
|
||||
getSortBy(index) {
|
||||
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
|
||||
}
|
||||
|
||||
getFilterValuesFromFormData(formData, isAdvanced) {
|
||||
const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
|
||||
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
|
||||
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
|
||||
|
||||
return {
|
||||
areaTag : this.getSelectedAreaTag(areaIndex),
|
||||
terms : formData.value.searchTerms,
|
||||
tags : isAdvanced ? formData.value.tags : '',
|
||||
order : this.getOrderBy(orderByIndex),
|
||||
sort : this.getSortBy(sortByIndex),
|
||||
};
|
||||
}
|
||||
|
||||
searchNow(formData, isAdvanced, cb) {
|
||||
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
|
||||
|
||||
const menuOpts = {
|
||||
extraArgs : {
|
||||
filterCriteria : filterCriteria,
|
||||
},
|
||||
menuFlags : [ 'noHistory' ],
|
||||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,158 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const Config = require('../core/config.js').config;
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File transfer protocol selection',
|
||||
desc : 'Select protocol / method for file transfer',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
protList : 1,
|
||||
};
|
||||
|
||||
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.config = this.menuConfig.config || {};
|
||||
|
||||
if(options.extraArgs) {
|
||||
if(options.extraArgs.direction) {
|
||||
this.config.direction = options.extraArgs.direction;
|
||||
}
|
||||
}
|
||||
|
||||
this.config.direction = this.config.direction || 'send';
|
||||
|
||||
this.extraArgs = options.extraArgs;
|
||||
|
||||
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||
}
|
||||
|
||||
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
|
||||
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
||||
}
|
||||
|
||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||
|
||||
this.loadAvailProtocols();
|
||||
|
||||
this.menuMethods = {
|
||||
selectProtocol : (formData, extraArgs, cb) => {
|
||||
const protocol = this.protocols[formData.value.protocol];
|
||||
const finalExtraArgs = this.extraArgs || {};
|
||||
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
|
||||
|
||||
const modOpts = {
|
||||
extraArgs : finalExtraArgs,
|
||||
};
|
||||
|
||||
if('send' === this.config.direction) {
|
||||
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb);
|
||||
} else {
|
||||
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getMenuResult() {
|
||||
if(this.sentFileIds) {
|
||||
return { sentFileIds : this.sentFileIds };
|
||||
}
|
||||
|
||||
if(this.recvFilePaths) {
|
||||
return { recvFilePaths : this.recvFilePaths };
|
||||
}
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
if(this.sentFileIds || this.recvFilePaths) {
|
||||
// nothing to do here; move along (we're just falling through)
|
||||
this.prevMenu();
|
||||
} else {
|
||||
super.initSequence();
|
||||
}
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : mciData.menu
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
},
|
||||
function populateList(callback) {
|
||||
const protListView = vc.getView(MciViewIds.protList);
|
||||
|
||||
const protListFormat = self.config.protListFormat || '{name}';
|
||||
const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
|
||||
|
||||
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
|
||||
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
|
||||
|
||||
protListView.redraw();
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadAvailProtocols() {
|
||||
this.protocols = _.map(Config.fileTransferProtocols, (protInfo, protocol) => {
|
||||
return {
|
||||
protocol : protocol,
|
||||
name : protInfo.name,
|
||||
hasBatch : _.has(protInfo, 'external.recvArgs'),
|
||||
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
|
||||
sort : protInfo.sort,
|
||||
};
|
||||
});
|
||||
|
||||
// Filter out batch vs non-batch only protocols
|
||||
if(this.extraArgs.recvFileName) { // non-batch aka non-blind
|
||||
this.protocols = this.protocols.filter( prot => prot.hasNonBatch );
|
||||
} else {
|
||||
this.protocols = this.protocols.filter( prot => prot.hasBatch );
|
||||
}
|
||||
|
||||
// natural sort taking explicit orders into consideration
|
||||
this.protocols.sort( (a, b) => {
|
||||
if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
|
||||
return a.sort - b.sort;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -32,19 +32,21 @@ exports.moduleInfo = {
|
|||
packageName : 'codes.l33t.enigma.lastcallers' // :TODO: concept idea for mods
|
||||
};
|
||||
|
||||
exports.getModule = LastCallersModule;
|
||||
|
||||
var MciCodeIds = {
|
||||
const MciCodeIds = {
|
||||
CallerList : 1,
|
||||
};
|
||||
|
||||
function LastCallersModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class LastCallersModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
require('util').inherits(LastCallersModule, MenuModule);
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
LastCallersModule.prototype.mciReady = function(mciData, cb) {
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
|
@ -53,9 +55,6 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) {
|
|||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
LastCallersModule.super_.prototype.mciReady.call(self, mciData, callback);
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
|
@ -139,4 +138,6 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) {
|
|||
cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
702
mods/menu.hjson
702
mods/menu.hjson
|
@ -1,11 +1,29 @@
|
|||
{
|
||||
/*
|
||||
ENiGMA½ Menu Configuration
|
||||
./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- -
|
||||
|
||||
This configuration is in HJSON format. Strict to-spec JSON is also
|
||||
perfectly valid. The hjson npm can be used to convert to/from JSON.
|
||||
_____________________ _____ ____________________ __________\_ /
|
||||
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
|
||||
// __|___// | \// |// | \// | | \// \ /___ /_____
|
||||
/____ _____| __________ ___|__| ____| \ / _____ \
|
||||
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
|
||||
/__ _\
|
||||
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
This configuration is in HJSON (http://hjson.org/) format. Strict to-spec
|
||||
JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON.
|
||||
|
||||
See http://hjson.org/ for more information and syntax.
|
||||
|
||||
|
||||
If you haven't yet, copy the conents of this file to something like
|
||||
sick_board.hjson. Point to it via config.hjson using the
|
||||
'general.menuFile' key:
|
||||
|
||||
general: { menuFile: "sick_board.hjson" }
|
||||
|
||||
*/
|
||||
menus: {
|
||||
//
|
||||
|
@ -34,7 +52,7 @@
|
|||
//
|
||||
sshConnectedNewUser: {
|
||||
art: CONNECT
|
||||
next: newUserApplicationSsh
|
||||
next: newUserApplicationPreSsh
|
||||
options: { nextTimeout: 1500 }
|
||||
}
|
||||
|
||||
|
@ -60,7 +78,7 @@
|
|||
}
|
||||
{
|
||||
value: { 1: 1 },
|
||||
action: @menu:newUserApplication
|
||||
action: @menu:newUserApplicationPre
|
||||
}
|
||||
{
|
||||
value: { 1: 2 },
|
||||
|
@ -162,12 +180,24 @@
|
|||
desc: Logging Off
|
||||
next: @systemMethod:logoff
|
||||
}
|
||||
/*
|
||||
TODO: display PRINT before this (Obv/2) or NEWUSER1 (Mystic)
|
||||
*/
|
||||
|
||||
// A quick preamble - defaults to warning about broken terminals
|
||||
newUserApplicationPre: {
|
||||
art: NEWUSER1
|
||||
next: newUserApplication
|
||||
desc: Applying
|
||||
options: {
|
||||
pause: true
|
||||
cls: true
|
||||
}
|
||||
}
|
||||
|
||||
newUserApplication: {
|
||||
module: nua
|
||||
art: NUA
|
||||
options: {
|
||||
menuFlags: [ "noHistory" ]
|
||||
}
|
||||
next: [
|
||||
{
|
||||
// Initial SysOp does not send feedback to themselves
|
||||
|
@ -268,6 +298,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// A quick preamble - defaults to warning about broken terminals (SSH version)
|
||||
newUserApplicationPreSsh: {
|
||||
art: NEWUSER1
|
||||
next: newUserApplicationSsh
|
||||
desc: Applying
|
||||
options: {
|
||||
pause: true
|
||||
cls: true
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// SSH specialization of NUA
|
||||
// Canceling this form logs off vs falling back to matrix
|
||||
|
@ -275,6 +316,9 @@
|
|||
newUserApplicationSsh: {
|
||||
art: NUA
|
||||
fallback: logoff
|
||||
options: {
|
||||
menuFlags: [ "noHistory" ]
|
||||
}
|
||||
next: newUserFeedbackToSysOpPreamble
|
||||
form: {
|
||||
0: {
|
||||
|
@ -350,7 +394,7 @@
|
|||
}
|
||||
{
|
||||
value: { "submission" : 1 }
|
||||
action: @systemMethod:prevMenu
|
||||
action: @systemMethod:logoff
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -358,7 +402,7 @@
|
|||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape" ]
|
||||
action: @systemMethod:prevMenu
|
||||
action: @systemMethod:logoff
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -708,6 +752,10 @@
|
|||
value: { command: "D" }
|
||||
action: @menu:doorMenu
|
||||
}
|
||||
{
|
||||
value: { command: "F" }
|
||||
action: @menu:fileBase
|
||||
}
|
||||
{
|
||||
value: { command: "U" }
|
||||
action: @menu:mainMenuUserList
|
||||
|
@ -1696,6 +1744,7 @@
|
|||
HM1: {
|
||||
// :TODO: (#)Jump/(L)Index (msg list)/Last
|
||||
items: [ "prev", "next", "reply", "quit", "help" ]
|
||||
focusItemIndex: 1
|
||||
}
|
||||
}
|
||||
submit: {
|
||||
|
@ -2226,6 +2275,639 @@
|
|||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// File Area
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
fileBase: {
|
||||
desc: File Base
|
||||
art: FMENU
|
||||
prompt: fileMenuCommand
|
||||
submit: [
|
||||
{
|
||||
value: { menuOption: "B" }
|
||||
action: @menu:fileBaseListEntries
|
||||
}
|
||||
{
|
||||
value: { menuOption: "F" }
|
||||
action: @menu:fileAreaFilterEditor
|
||||
}
|
||||
{
|
||||
value: { menuOption: "Q" }
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
{
|
||||
value: { menuOption: "G" }
|
||||
action: @menu:fullLogoffSequence
|
||||
}
|
||||
{
|
||||
value: { menuOption: "D" }
|
||||
action: @menu:fileBaseDownloadManager
|
||||
}
|
||||
{
|
||||
value: { menuOption: "U" }
|
||||
action: @menu:fileBaseUploadFiles
|
||||
}
|
||||
{
|
||||
value: { menuOption: "S" }
|
||||
action: @menu:fileBaseSearch
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
fileBaseListEntries: {
|
||||
module: file_area_list
|
||||
desc: Browsing Files
|
||||
config: {
|
||||
art: {
|
||||
browse: FBRWSE
|
||||
details: FDETAIL
|
||||
detailsGeneral: FDETGEN
|
||||
detailsNfo: FDETNFO
|
||||
detailsFileList: FDETLST
|
||||
help: FBHELP
|
||||
}
|
||||
}
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
MT1: {
|
||||
mode: preview
|
||||
}
|
||||
|
||||
HM2: {
|
||||
focus: true
|
||||
submit: true
|
||||
argName: navSelect
|
||||
items: [
|
||||
"prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit"
|
||||
]
|
||||
focusItemIndex: 1
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { navSelect: 0 }
|
||||
action: @method:prevFile
|
||||
}
|
||||
{
|
||||
value: { navSelect: 1 }
|
||||
action: @method:nextFile
|
||||
}
|
||||
{
|
||||
value: { navSelect: 2 }
|
||||
action: @method:viewDetails
|
||||
}
|
||||
{
|
||||
value: { navSelect: 3 }
|
||||
action: @method:toggleQueue
|
||||
}
|
||||
{
|
||||
value: { navSelect: 4 }
|
||||
action: @menu:fileBaseGetRatingForSelectedEntry
|
||||
}
|
||||
{
|
||||
value: { navSelect: 5 }
|
||||
action: @menu:fileAreaFilterEditor
|
||||
}
|
||||
{
|
||||
value: { navSelect: 6 }
|
||||
action: @method:displayHelp
|
||||
}
|
||||
{
|
||||
value: { navSelect: 7 }
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "w", "shift + w" ]
|
||||
action: @method:showWebDownloadLink
|
||||
}
|
||||
{
|
||||
keys: [ "escape", "q", "shift + q" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
{
|
||||
keys: [ "t", "shift + t" ]
|
||||
action: @method:toggleQueue
|
||||
}
|
||||
{
|
||||
keys: [ "f", "shift + f" ]
|
||||
action: @menu:fileAreaFilterEditor
|
||||
}
|
||||
{
|
||||
keys: [ "v", "shift + v" ]
|
||||
action: @method:viewDetails
|
||||
}
|
||||
{
|
||||
keys: [ "r", "shift + r" ]
|
||||
action: @menu:fileBaseGetRatingForSelectedEntry
|
||||
}
|
||||
{
|
||||
keys: [ "?" ]
|
||||
action: @method:displayHelp
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1: {
|
||||
mci: {
|
||||
HM1: {
|
||||
focus: true
|
||||
submit: true
|
||||
argName: navSelect
|
||||
items: [
|
||||
"general", "nfo/readme", "file listing"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape", "q", "shift + q" ]
|
||||
action: @method:detailsQuit
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2: {
|
||||
// details - general
|
||||
mci: {}
|
||||
}
|
||||
|
||||
3: {
|
||||
// details - nfo/readme
|
||||
mci: {
|
||||
MT1: {
|
||||
mode: preview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4: {
|
||||
// details - file listing
|
||||
mci: {
|
||||
VM1: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseGetRatingForSelectedEntry: {
|
||||
desc: Rating a File
|
||||
prompt: fileBaseRateEntryPrompt
|
||||
options: {
|
||||
cls: true
|
||||
}
|
||||
submit: [
|
||||
// :TODO: handle esc/q
|
||||
{
|
||||
// pass data back to caller
|
||||
value: { rating: null }
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
fileBaseListEntriesNoResults: {
|
||||
desc: Browsing Files
|
||||
art: FBNORES
|
||||
options: {
|
||||
pause: true
|
||||
menuFlags: [ "noHistory" ]
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseSearch: {
|
||||
module: file_base_search
|
||||
desc: Searching Files
|
||||
art: FSEARCH
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
ET1: {
|
||||
focus: true
|
||||
argName: searchTerms
|
||||
}
|
||||
BT2: {
|
||||
argName: search
|
||||
text: search
|
||||
submit: true
|
||||
}
|
||||
ET3: {
|
||||
maxLength: 64
|
||||
argName: tags
|
||||
}
|
||||
SM4: {
|
||||
maxLength: 64
|
||||
argName: areaIndex
|
||||
}
|
||||
SM5: {
|
||||
items: [
|
||||
"upload date",
|
||||
"uploaded by",
|
||||
"downloads",
|
||||
"rating",
|
||||
"estimated year",
|
||||
"size",
|
||||
]
|
||||
argName: sortByIndex
|
||||
}
|
||||
SM6: {
|
||||
items: [
|
||||
"decending",
|
||||
"ascending"
|
||||
]
|
||||
argName: orderByIndex
|
||||
}
|
||||
BT7: {
|
||||
argName: advancedSearch
|
||||
text: advanced search
|
||||
submit: true
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { search: null }
|
||||
action: @method:search
|
||||
}
|
||||
{
|
||||
value: { advancedSearch: null }
|
||||
action: @method:search
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileAreaFilterEditor: {
|
||||
desc: File Filter Editor
|
||||
module: file_area_filter_edit
|
||||
art: FFILEDT
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
ET1: {
|
||||
argName: searchTerms
|
||||
}
|
||||
ET2: {
|
||||
maxLength: 64
|
||||
argName: tags
|
||||
}
|
||||
SM3: {
|
||||
maxLength: 64
|
||||
argName: areaIndex
|
||||
}
|
||||
SM4: {
|
||||
items: [
|
||||
"upload date",
|
||||
"uploaded by",
|
||||
"downloads",
|
||||
"rating",
|
||||
"estimated year",
|
||||
"size",
|
||||
]
|
||||
argName: sortByIndex
|
||||
}
|
||||
SM5: {
|
||||
items: [
|
||||
"decending",
|
||||
"ascending"
|
||||
]
|
||||
argName: orderByIndex
|
||||
}
|
||||
ET6: {
|
||||
maxLength: 64
|
||||
argName: name
|
||||
validate: @systemMethod:validateNonEmpty
|
||||
}
|
||||
HM7: {
|
||||
focus: true
|
||||
items: [
|
||||
"prev", "next", "make active", "save", "new", "delete"
|
||||
]
|
||||
argName: navSelect
|
||||
focusItemIndex: 1
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { navSelect: 0 }
|
||||
action: @method:prevFilter
|
||||
}
|
||||
{
|
||||
value: { navSelect: 1 }
|
||||
action: @method:nextFilter
|
||||
}
|
||||
{
|
||||
value: { navSelect: 2 }
|
||||
action: @method:makeFilterActive
|
||||
}
|
||||
{
|
||||
value: { navSelect: 3 }
|
||||
action: @method:saveFilter
|
||||
}
|
||||
{
|
||||
value: { navSelect: 4 }
|
||||
action: @method:newFilter
|
||||
}
|
||||
{
|
||||
value: { navSelect: 5 }
|
||||
action: @method:deleteFilter
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseDownloadManager: {
|
||||
desc: Download Manager
|
||||
module: file_base_download_manager
|
||||
config: {
|
||||
art: {
|
||||
queueManager: FDLMGR
|
||||
/*
|
||||
NYI
|
||||
details: FDLDET
|
||||
*/
|
||||
}
|
||||
emptyQueueMenu: fileBaseDownloadManagerEmptyQueue
|
||||
}
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
argName: queueItem
|
||||
}
|
||||
HM2: {
|
||||
focus: true
|
||||
items: [ "download all", "quit" ]
|
||||
argName: navSelect
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { navSelect: 0 }
|
||||
action: @method:downloadAll
|
||||
}
|
||||
{
|
||||
value: { navSelect: 1 }
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "a", "shift + a" ]
|
||||
action: @method:downloadAll
|
||||
}
|
||||
{
|
||||
keys: [ "delete", "r", "shift + r" ]
|
||||
action: @method:removeItem
|
||||
}
|
||||
{
|
||||
keys: [ "c", "shift + c" ]
|
||||
action: @method:clearQueue
|
||||
}
|
||||
{
|
||||
keys: [ "escape", "q", "shift + q" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseDownloadManagerEmptyQueue: {
|
||||
desc: Empty Download Queue
|
||||
art: FEMPTYQ
|
||||
options: {
|
||||
pause: true
|
||||
menuFlags: [ "noHistory" ]
|
||||
}
|
||||
}
|
||||
|
||||
fileTransferProtocolSelection: {
|
||||
desc: Protocol selection
|
||||
module: file_transfer_protocol_select
|
||||
art: FPROSEL
|
||||
form: {
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
focus: true
|
||||
argName: protocol
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { protocol: null }
|
||||
action: @method:selectProtocol
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseUploadFiles: {
|
||||
desc: Uploading
|
||||
module: upload
|
||||
config: {
|
||||
art: {
|
||||
options: ULOPTS
|
||||
fileDetails: ULDETAIL
|
||||
processing: ULCHECK
|
||||
dupes: ULDUPES
|
||||
}
|
||||
}
|
||||
|
||||
form: {
|
||||
// options
|
||||
0: {
|
||||
mci: {
|
||||
SM1: {
|
||||
argName: areaSelect
|
||||
focus: true
|
||||
}
|
||||
TM2: {
|
||||
argName: uploadType
|
||||
items: [ "blind", "supply filename" ]
|
||||
}
|
||||
ET3: {
|
||||
argName: fileName
|
||||
maxLength: 255
|
||||
validate: @method:validateNonBlindFileName
|
||||
}
|
||||
HM4: {
|
||||
argName: navSelect
|
||||
items: [ "continue", "cancel" ]
|
||||
submit: true
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { navSelect: 0 }
|
||||
action: @method:optionsNavContinue
|
||||
}
|
||||
{
|
||||
value: { navSelect: 1 }
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"actionKeys" : [
|
||||
{
|
||||
"keys" : [ "escape" ],
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1: {
|
||||
mci: {
|
||||
TL1: {}
|
||||
TL2: {}
|
||||
TL3: {}
|
||||
MT4: {}
|
||||
TL10: {}
|
||||
}
|
||||
}
|
||||
|
||||
// file details entry
|
||||
2: {
|
||||
mci: {
|
||||
MT1: {
|
||||
argName: shortDesc
|
||||
tabSwitchesView: true
|
||||
focus: true
|
||||
}
|
||||
|
||||
ET2: {
|
||||
argName: tags
|
||||
}
|
||||
|
||||
ME3: {
|
||||
argName: estYear
|
||||
maskPattern: "####"
|
||||
}
|
||||
|
||||
BT4: {
|
||||
argName: continue
|
||||
text: continue
|
||||
submit: true
|
||||
}
|
||||
}
|
||||
|
||||
submit: {
|
||||
*: [
|
||||
{
|
||||
value: { continue: null }
|
||||
action: @method:fileDetailsContinue
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// dupes
|
||||
3: {
|
||||
mci: {
|
||||
VM1: {
|
||||
/*
|
||||
Use 'dupeInfoFormat' to custom format:
|
||||
|
||||
areaDesc
|
||||
areaName
|
||||
areaTag
|
||||
desc
|
||||
descLong
|
||||
fileId
|
||||
fileName
|
||||
fileSha256
|
||||
storageTag
|
||||
uploadTimestamp
|
||||
|
||||
*/
|
||||
|
||||
mode: preview
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseNoUploadAreasAvail: {
|
||||
desc: File Base
|
||||
art: ULNOAREA
|
||||
options: {
|
||||
pause: true
|
||||
menuFlags: [ "noHistory" ]
|
||||
}
|
||||
}
|
||||
|
||||
sendFilesToUser: {
|
||||
desc: Downloading
|
||||
module: @systemModule:file_transfer
|
||||
config: {
|
||||
// defaults - generally use extraArgs
|
||||
protocol: zmodem8kSexyz
|
||||
direction: send
|
||||
}
|
||||
}
|
||||
|
||||
recvFilesFromUser: {
|
||||
desc: Uploading
|
||||
module: @systemModule:file_transfer
|
||||
config: {
|
||||
// defaults - generally use extraArgs
|
||||
protocol: zmodem8kSexyz
|
||||
direction: recv
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Required entries
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -6,7 +6,6 @@ const MenuModule = require('../core/menu_module.js').MenuModule;
|
|||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const messageArea = require('../core/message_area.js');
|
||||
const displayThemeArt = require('../core/theme.js').displayThemeArt;
|
||||
const displayThemedPause = require('../core/theme.js').displayThemedPause;
|
||||
const resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
|
||||
|
@ -14,8 +13,6 @@ const stringFormat = require('../core/string_format.js');
|
|||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.getModule = MessageAreaListModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area List',
|
||||
desc : 'Module for listing / choosing message areas',
|
||||
|
@ -36,28 +33,22 @@ exports.moduleInfo = {
|
|||
|TI Current time
|
||||
*/
|
||||
|
||||
const MCICodesIDs = {
|
||||
const MciViewIds = {
|
||||
AreaList : 1,
|
||||
SelAreaInfo1 : 2,
|
||||
SelAreaInfo2 : 3,
|
||||
};
|
||||
|
||||
function MessageAreaListModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
|
||||
var self = this;
|
||||
exports.getModule = class MessageAreaListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
|
||||
self.client.user.properties.message_conf_tag,
|
||||
{ client : self.client }
|
||||
this.client.user.properties.message_conf_tag,
|
||||
{ client : this.client }
|
||||
);
|
||||
|
||||
this.prevMenuOnTimeout = function(timeout, cb) {
|
||||
setTimeout( () => {
|
||||
self.prevMenu(cb);
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
const self = this;
|
||||
this.menuMethods = {
|
||||
changeArea : function(formData, extraArgs, cb) {
|
||||
if(1 === formData.submitId) {
|
||||
|
@ -84,7 +75,7 @@ function MessageAreaListModule(options) {
|
|||
if(_.has(area, 'options.pause') && false === area.options.pause) {
|
||||
return self.prevMenuOnTimeout(1000, cb);
|
||||
} else {
|
||||
displayThemedPause( { client : self.client }, () => {
|
||||
self.pausePrompt( () => {
|
||||
return self.prevMenu(cb);
|
||||
});
|
||||
}
|
||||
|
@ -99,42 +90,39 @@ function MessageAreaListModule(options) {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
const v = self.viewControllers.areaList.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
|
||||
this.updateGeneralAreaInfoViews = function(areaIndex) {
|
||||
prevMenuOnTimeout(timeout, cb) {
|
||||
setTimeout( () => {
|
||||
return this.prevMenu(cb);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
updateGeneralAreaInfoViews(areaIndex) {
|
||||
// :TODO: these concepts have been replaced with the {someKey} style formatting - update me!
|
||||
/* experimental: not yet avail
|
||||
const areaInfo = self.messageAreas[areaIndex];
|
||||
|
||||
[ MCICodesIDs.SelAreaInfo1, MCICodesIDs.SelAreaInfo2 ].forEach(mciId => {
|
||||
[ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => {
|
||||
const v = self.viewControllers.areaList.getView(mciId);
|
||||
if(v) {
|
||||
v.setFormatObject(areaInfo.area);
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
require('util').inherits(MessageAreaListModule, MenuModule);
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
||||
const self = this;
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
MessageAreaListModule.super_.prototype.mciReady.call(this, mciData, function parentMciReady(err) {
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
|
@ -150,7 +138,7 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
|||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
const areaListView = vc.getView(MCICodesIDs.AreaList);
|
||||
const areaListView = vc.getView(MciViewIds.AreaList);
|
||||
let i = 1;
|
||||
areaListView.setItems(_.map(self.messageAreas, v => {
|
||||
return stringFormat(listFormat, {
|
||||
|
@ -184,4 +172,6 @@ MessageAreaListModule.prototype.mciReady = function(mciData, cb) {
|
|||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,15 +1,11 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
let FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
|
||||
//var Message = require('../core/message.js').Message;
|
||||
let persistMessage = require('../core/message_area.js').persistMessage;
|
||||
let user = require('../core/user.js');
|
||||
const FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
|
||||
const persistMessage = require('../core/message_area.js').persistMessage;
|
||||
|
||||
let _ = require('lodash');
|
||||
let async = require('async');
|
||||
|
||||
exports.getModule = AreaPostFSEModule;
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area Post',
|
||||
|
@ -17,10 +13,11 @@ exports.moduleInfo = {
|
|||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
function AreaPostFSEModule(options) {
|
||||
FullScreenEditorModule.call(this, options);
|
||||
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
// we're posting, so always start with 'edit' mode
|
||||
this.editorMode = 'edit';
|
||||
|
@ -60,13 +57,11 @@ function AreaPostFSEModule(options) {
|
|||
};
|
||||
}
|
||||
|
||||
require('util').inherits(AreaPostFSEModule, FullScreenEditorModule);
|
||||
|
||||
AreaPostFSEModule.prototype.enter = function() {
|
||||
|
||||
enter() {
|
||||
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) {
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
|
||||
AreaPostFSEModule.super_.prototype.enter.call(this);
|
||||
super.enter();
|
||||
}
|
||||
};
|
|
@ -8,18 +8,15 @@ const Message = require('../core/message.js');
|
|||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.getModule = AreaViewFSEModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Area View',
|
||||
desc : 'Module for viewing an area message',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
function AreaViewFSEModule(options) {
|
||||
FullScreenEditorModule.call(this, options);
|
||||
|
||||
const self = this;
|
||||
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.editorType = 'area';
|
||||
this.editorMode = 'view';
|
||||
|
@ -33,7 +30,11 @@ function AreaViewFSEModule(options) {
|
|||
this.messageIndex = this.messageIndex || 0;
|
||||
this.messageTotal = this.messageList.length;
|
||||
|
||||
this.menuMethods.nextMessage = function(formData, extraArgs, cb) {
|
||||
const self = this;
|
||||
|
||||
// assign *additional* menuMethods
|
||||
Object.assign(this.menuMethods, {
|
||||
nextMessage : (formData, extraArgs, cb) => {
|
||||
if(self.messageIndex + 1 < self.messageList.length) {
|
||||
self.messageIndex++;
|
||||
|
||||
|
@ -41,9 +42,9 @@ function AreaViewFSEModule(options) {
|
|||
}
|
||||
|
||||
return cb(null);
|
||||
};
|
||||
},
|
||||
|
||||
this.menuMethods.prevMessage = function(formData, extraArgs, cb) {
|
||||
prevMessage : (formData, extraArgs, cb) => {
|
||||
if(self.messageIndex > 0) {
|
||||
self.messageIndex--;
|
||||
|
||||
|
@ -51,9 +52,9 @@ function AreaViewFSEModule(options) {
|
|||
}
|
||||
|
||||
return cb(null);
|
||||
};
|
||||
},
|
||||
|
||||
this.menuMethods.movementKeyPressed = function(formData, extraArgs, cb) {
|
||||
movementKeyPressed : (formData, extraArgs, cb) => {
|
||||
const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic #
|
||||
|
||||
// :TODO: Create methods for up/down vs using keyPressXXXXX
|
||||
|
@ -68,10 +69,9 @@ function AreaViewFSEModule(options) {
|
|||
// visible page off the screen at all .... this should be handled by MLTEV though...
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
this.menuMethods.replyMessage = function(formData, extraArgs, cb) {
|
||||
replyMessage : (formData, extraArgs, cb) => {
|
||||
if(_.isString(extraArgs.menu)) {
|
||||
const modOpts = {
|
||||
extraArgs : {
|
||||
|
@ -85,45 +85,41 @@ function AreaViewFSEModule(options) {
|
|||
|
||||
self.client.log(extraArgs, 'Missing extraArgs.menu');
|
||||
return cb(null);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.loadMessageByUuid = function(uuid, cb) {
|
||||
|
||||
loadMessageByUuid(uuid, cb) {
|
||||
const msg = new Message();
|
||||
msg.load( { uuid : uuid, user : self.client.user }, () => {
|
||||
self.setMessage(msg);
|
||||
msg.load( { uuid : uuid, user : this.client.user }, () => {
|
||||
this.setMessage(msg);
|
||||
|
||||
if(cb) {
|
||||
return cb(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(AreaViewFSEModule, FullScreenEditorModule);
|
||||
|
||||
AreaViewFSEModule.prototype.finishedLoading = function() {
|
||||
if(this.messageList.length) {
|
||||
finishedLoading() {
|
||||
this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid);
|
||||
}
|
||||
};
|
||||
|
||||
AreaViewFSEModule.prototype.getSaveState = function() {
|
||||
AreaViewFSEModule.super_.prototype.getSaveState.call(this);
|
||||
|
||||
getSaveState() {
|
||||
return {
|
||||
messageList : this.messageList,
|
||||
messageIndex : this.messageIndex,
|
||||
messageTotal : this.messageList.length,
|
||||
};
|
||||
};
|
||||
|
||||
AreaViewFSEModule.prototype.restoreSavedState = function(savedState) {
|
||||
AreaViewFSEModule.super_.prototype.restoreSavedState.call(this, savedState);
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
this.messageList = savedState.messageList;
|
||||
this.messageIndex = savedState.messageIndex;
|
||||
this.messageTotal = savedState.messageTotal;
|
||||
};
|
||||
}
|
||||
|
||||
AreaViewFSEModule.prototype.getMenuResult = function() {
|
||||
getMenuResult() {
|
||||
return this.messageIndex;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@ const MenuModule = require('../core/menu_module.js').MenuModule;
|
|||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const messageArea = require('../core/message_area.js');
|
||||
const displayThemeArt = require('../core/theme.js').displayThemeArt;
|
||||
const displayThemedPause = require('../core/theme.js').displayThemedPause;
|
||||
const resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
|
||||
|
@ -14,15 +13,13 @@ const stringFormat = require('../core/string_format.js');
|
|||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.getModule = MessageConfListModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message Conference List',
|
||||
desc : 'Module for listing / choosing message conferences',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const MCICodeIDs = {
|
||||
const MciViewIds = {
|
||||
ConfList : 1,
|
||||
|
||||
// :TODO:
|
||||
|
@ -30,18 +27,12 @@ const MCICodeIDs = {
|
|||
//
|
||||
};
|
||||
|
||||
function MessageConfListModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class MessageConfListModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
var self = this;
|
||||
|
||||
this.messageConfs = messageArea.getSortedAvailMessageConferences(self.client);
|
||||
|
||||
this.prevMenuOnTimeout = function(timeout, cb) {
|
||||
setTimeout( () => {
|
||||
self.prevMenu(cb);
|
||||
}, timeout);
|
||||
};
|
||||
this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client);
|
||||
const self = this;
|
||||
|
||||
this.menuMethods = {
|
||||
changeConference : function(formData, extraArgs, cb) {
|
||||
|
@ -71,7 +62,7 @@ function MessageConfListModule(options) {
|
|||
if(_.has(conf, 'options.pause') && false === conf.options.pause) {
|
||||
return self.prevMenuOnTimeout(1000, cb);
|
||||
} else {
|
||||
displayThemedPause( { client : self.client }, () => {
|
||||
self.pausePrompt( () => {
|
||||
return self.prevMenu(cb);
|
||||
});
|
||||
}
|
||||
|
@ -86,26 +77,25 @@ function MessageConfListModule(options) {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
const v = self.viewControllers.areaList.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(MessageConfListModule, MenuModule);
|
||||
prevMenuOnTimeout(timeout, cb) {
|
||||
setTimeout( () => {
|
||||
return this.prevMenu(cb);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
MessageConfListModule.prototype.mciReady = function(mciData, cb) {
|
||||
var self = this;
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const vc = self.viewControllers.areaList = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
MessageConfListModule.super_.prototype.mciReady.call(this, mciData, callback);
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
let loadOpts = {
|
||||
callingMenu : self,
|
||||
|
@ -119,7 +109,7 @@ MessageConfListModule.prototype.mciReady = function(mciData, cb) {
|
|||
const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}';
|
||||
const focusListFormat = self.menuConfig.config.focusListFormat || listFormat;
|
||||
|
||||
const confListView = vc.getView(MCICodeIDs.ConfList);
|
||||
const confListView = vc.getView(MciViewIds.ConfList);
|
||||
let i = 1;
|
||||
confListView.setItems(_.map(self.messageConfs, v => {
|
||||
return stringFormat(listFormat, {
|
||||
|
@ -153,4 +143,6 @@ MessageConfListModule.prototype.mciReady = function(mciData, cb) {
|
|||
cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -6,6 +6,7 @@ const MenuModule = require('../core/menu_module.js').MenuModule;
|
|||
const ViewController = require('../core/view_controller.js').ViewController;
|
||||
const messageArea = require('../core/message_area.js');
|
||||
const stringFormat = require('../core/string_format.js');
|
||||
const MessageAreaConfTempSwitcher = require('../core/mod_mixins.js').MessageAreaConfTempSwitcher;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -28,8 +29,6 @@ const moment = require('moment');
|
|||
TL2 : Message info 1: { msgNumSelected, msgNumTotal }
|
||||
*/
|
||||
|
||||
exports.getModule = MessageListModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Message List',
|
||||
desc : 'Module for listing/browsing available messages',
|
||||
|
@ -41,8 +40,9 @@ const MCICodesIDs = {
|
|||
MsgInfo1 : 2, // TL2
|
||||
};
|
||||
|
||||
function MessageListModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
@ -104,21 +104,10 @@ function MessageListModule(options) {
|
|||
return self.prevMenu(cb);
|
||||
}
|
||||
};
|
||||
|
||||
this.setViewText = function(id, text) {
|
||||
const v = self.viewControllers.allViews.getView(id);
|
||||
if(v) {
|
||||
v.setText(text);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(MessageListModule, MenuModule);
|
||||
|
||||
require('../core/mod_mixins.js').MessageAreaConfTempSwitcher.call(MessageListModule.prototype);
|
||||
|
||||
MessageListModule.prototype.enter = function() {
|
||||
MessageListModule.super_.prototype.enter.call(this);
|
||||
enter() {
|
||||
super.enter();
|
||||
|
||||
//
|
||||
// Config can specify |messageAreaTag| else it comes from
|
||||
|
@ -127,25 +116,26 @@ MessageListModule.prototype.enter = function() {
|
|||
if(this.messageAreaTag) {
|
||||
this.tempMessageConfAndAreaSwitch(this.messageAreaTag);
|
||||
} else {
|
||||
this.messageAreaTag = this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
this.messageAreaTag = this.client.user.properties.message_area_tag;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MessageListModule.prototype.leave = function() {
|
||||
leave() {
|
||||
this.tempMessageConfAndAreaRestore();
|
||||
super.leave();
|
||||
}
|
||||
|
||||
MessageListModule.super_.prototype.leave.call(this);
|
||||
};
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
MessageListModule.prototype.mciReady = function(mciData, cb) {
|
||||
const self = this;
|
||||
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||
|
||||
async.series(
|
||||
[
|
||||
function callParentMciReady(callback) {
|
||||
MessageListModule.super_.prototype.mciReady.call(self, mciData, callback);
|
||||
},
|
||||
function loadFromConfig(callback) {
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
|
@ -213,6 +203,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
|
||||
msgListView.on('index update', idx => {
|
||||
self.setViewText(
|
||||
'allViews',
|
||||
MCICodesIDs.MsgInfo1,
|
||||
stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.messageList.length } ));
|
||||
});
|
||||
|
@ -229,6 +220,7 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
function drawOtherViews(callback) {
|
||||
const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}';
|
||||
self.setViewText(
|
||||
'allViews',
|
||||
MCICodesIDs.MsgInfo1,
|
||||
stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.messageList.length } ));
|
||||
return callback(null);
|
||||
|
@ -241,18 +233,20 @@ MessageListModule.prototype.mciReady = function(mciData, cb) {
|
|||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
MessageListModule.prototype.getSaveState = function() {
|
||||
getSaveState() {
|
||||
return { initialFocusIndex : this.initialFocusIndex };
|
||||
};
|
||||
}
|
||||
|
||||
MessageListModule.prototype.restoreSavedState = function(savedState) {
|
||||
restoreSavedState(savedState) {
|
||||
if(savedState) {
|
||||
this.initialFocusIndex = savedState.initialFocusIndex;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MessageListModule.prototype.getMenuResult = function() {
|
||||
getMenuResult() {
|
||||
return this.menuResult;
|
||||
}
|
||||
};
|
||||
|
|
15
mods/nua.js
15
mods/nua.js
|
@ -9,8 +9,6 @@ const login = require('../core/system_menu_method.js').login;
|
|||
const Config = require('../core/config.js').config;
|
||||
const messageArea = require('../core/message_area.js');
|
||||
|
||||
exports.getModule = NewUserAppModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'NUA',
|
||||
desc : 'New User Application',
|
||||
|
@ -23,8 +21,10 @@ const MciViewIds = {
|
|||
errMsg : 11,
|
||||
};
|
||||
|
||||
function NewUserAppModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class NewUserAppModule extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
|
||||
|
@ -138,8 +138,7 @@ function NewUserAppModule(options) {
|
|||
};
|
||||
}
|
||||
|
||||
require('util').inherits(NewUserAppModule, MenuModule);
|
||||
|
||||
NewUserAppModule.prototype.mciReady = function(mciData, cb) {
|
||||
this.standardMCIReadyHandler(mciData, cb);
|
||||
mciReady(mciData, cb) {
|
||||
return this.standardMCIReadyHandler(mciData, cb);
|
||||
}
|
||||
};
|
|
@ -31,9 +31,7 @@ exports.moduleInfo = {
|
|||
packageName : 'codes.l33t.enigma.onelinerz',
|
||||
};
|
||||
|
||||
exports.getModule = OnelinerzModule;
|
||||
|
||||
const MciCodeIds = {
|
||||
const MciViewIds = {
|
||||
ViewForm : {
|
||||
Entries : 1,
|
||||
AddPrompt : 2,
|
||||
|
@ -50,185 +48,11 @@ const FormIds = {
|
|||
Add : 1,
|
||||
};
|
||||
|
||||
function OnelinerzModule(options) {
|
||||
MenuModule.call(this, options);
|
||||
exports.getModule = class OnelinerzModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const self = this;
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
this.initSequence = function() {
|
||||
async.series(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
self.displayViewScreen(false, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
// :TODO: Handle me -- initSequence() should really take a completion callback
|
||||
}
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.displayViewScreen = function(clearScreen, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
if(self.viewControllers.add) {
|
||||
self.viewControllers.add.setFocus(false);
|
||||
}
|
||||
|
||||
if(clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art.entries,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'view',
|
||||
new ViewController( { client : self.client, formId : FormIds.View } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.View,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.view.setFocus(true);
|
||||
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function fetchEntries(callback) {
|
||||
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
|
||||
const limit = entriesView.dimens.height;
|
||||
let entries = [];
|
||||
|
||||
self.db.each(
|
||||
`SELECT *
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM onelinerz
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ${limit}
|
||||
)
|
||||
ORDER BY timestamp ASC;`,
|
||||
(err, row) => {
|
||||
if(!err) {
|
||||
row.timestamp = moment(row.timestamp); // convert -> moment
|
||||
entries.push(row);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return callback(err, entriesView, entries);
|
||||
}
|
||||
);
|
||||
},
|
||||
function populateEntries(entriesView, entries, callback) {
|
||||
const listFormat = config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent
|
||||
const tsFormat = config.timestampFormat || 'ddd h:mma';
|
||||
|
||||
entriesView.setItems(entries.map( e => {
|
||||
return stringFormat(listFormat, {
|
||||
userId : e.user_id,
|
||||
username : e.user_name,
|
||||
oneliner : e.oneliner,
|
||||
ts : e.timestamp.format(tsFormat),
|
||||
} );
|
||||
}));
|
||||
|
||||
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);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.displayAddScreen = function(cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
self.viewControllers.view.setFocus(false);
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
|
||||
theme.displayThemedAsset(
|
||||
config.art.add,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'add',
|
||||
new ViewController( { client : self.client, formId : FormIds.Add } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.Add,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.add.setFocus(true);
|
||||
self.viewControllers.add.redrawAll();
|
||||
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.clearAddForm = function() {
|
||||
const newEntryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
|
||||
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
|
||||
|
||||
newEntryView.setText('');
|
||||
|
||||
// preview is optional
|
||||
if(previewView) {
|
||||
previewView.setText('');
|
||||
}
|
||||
};
|
||||
|
||||
this.menuMethods = {
|
||||
viewAddScreen : function(formData, extraArgs, cb) {
|
||||
|
@ -259,18 +83,194 @@ function OnelinerzModule(options) {
|
|||
return self.displayViewScreen(true, cb); // true=cls
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
async.series(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayViewScreen(false, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
// :TODO: Handle me -- initSequence() should really take a completion callback
|
||||
}
|
||||
self.finishedLoading();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayViewScreen(clearScreen, cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
if(self.viewControllers.add) {
|
||||
self.viewControllers.add.setFocus(false);
|
||||
}
|
||||
|
||||
if(clearScreen) {
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
}
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.menuConfig.config.art.entries,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, trailingLF : false },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'view',
|
||||
new ViewController( { client : self.client, formId : FormIds.View } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.View,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.view.setFocus(true);
|
||||
self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw();
|
||||
return callback(null);
|
||||
}
|
||||
},
|
||||
function fetchEntries(callback) {
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries);
|
||||
const limit = entriesView.dimens.height;
|
||||
let entries = [];
|
||||
|
||||
self.db.each(
|
||||
`SELECT *
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM onelinerz
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ${limit}
|
||||
)
|
||||
ORDER BY timestamp ASC;`,
|
||||
(err, row) => {
|
||||
if(!err) {
|
||||
row.timestamp = moment(row.timestamp); // convert -> moment
|
||||
entries.push(row);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
return callback(err, entriesView, entries);
|
||||
}
|
||||
);
|
||||
},
|
||||
function populateEntries(entriesView, entries, callback) {
|
||||
const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent
|
||||
const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma';
|
||||
|
||||
entriesView.setItems(entries.map( e => {
|
||||
return stringFormat(listFormat, {
|
||||
userId : e.user_id,
|
||||
username : e.user_name,
|
||||
oneliner : e.oneliner,
|
||||
ts : e.timestamp.format(tsFormat),
|
||||
} );
|
||||
}));
|
||||
|
||||
entriesView.redraw();
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function finalPrep(callback) {
|
||||
const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt);
|
||||
promptView.setFocusItemIndex(1); // default to NO
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayAddScreen(cb) {
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function clearAndDisplayArt(callback) {
|
||||
self.viewControllers.view.setFocus(false);
|
||||
self.client.term.rawWrite(ansi.resetScreen());
|
||||
|
||||
theme.displayThemedAsset(
|
||||
self.menuConfig.config.art.add,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
(err, artData) => {
|
||||
return callback(err, artData);
|
||||
}
|
||||
);
|
||||
},
|
||||
function initOrRedrawViewController(artData, callback) {
|
||||
if(_.isUndefined(self.viewControllers.add)) {
|
||||
const vc = self.addViewController(
|
||||
'add',
|
||||
new ViewController( { client : self.client, formId : FormIds.Add } )
|
||||
);
|
||||
|
||||
const loadOpts = {
|
||||
callingMenu : self,
|
||||
mciMap : artData.mciMap,
|
||||
formId : FormIds.Add,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
} else {
|
||||
self.viewControllers.add.setFocus(true);
|
||||
self.viewControllers.add.redrawAll();
|
||||
self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
clearAddForm() {
|
||||
this.setViewText('add', MciViewIds.AddForm.NewEntry, '');
|
||||
this.setViewText('add', MciViewIds.AddForm.EntryPreview, '');
|
||||
}
|
||||
|
||||
initDatabase(cb) {
|
||||
const self = this;
|
||||
|
||||
this.initDatabase = function(cb) {
|
||||
async.series(
|
||||
[
|
||||
function openDatabase(callback) {
|
||||
self.db = new sqlite3.Database(
|
||||
getModDatabasePath(exports.moduleInfo),
|
||||
callback
|
||||
err => {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function createTables(callback) {
|
||||
self.db.serialize( () => {
|
||||
self.db.run(
|
||||
`CREATE TABLE IF NOT EXISTS onelinerz (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
@ -278,17 +278,21 @@ function OnelinerzModule(options) {
|
|||
user_name VARCHAR NOT NULL,
|
||||
oneliner VARCHAR NOT NULL,
|
||||
timestamp DATETIME NOT NULL
|
||||
)`
|
||||
);
|
||||
);`
|
||||
,
|
||||
err => {
|
||||
return callback(err);
|
||||
});
|
||||
callback(null);
|
||||
}
|
||||
],
|
||||
cb
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.storeNewOneliner = function(oneliner, cb) {
|
||||
storeNewOneliner(oneliner, cb) {
|
||||
const self = this;
|
||||
const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
|
||||
async.series(
|
||||
|
@ -315,15 +319,15 @@ function OnelinerzModule(options) {
|
|||
);
|
||||
}
|
||||
],
|
||||
cb
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(OnelinerzModule, MenuModule);
|
||||
|
||||
OnelinerzModule.prototype.beforeArt = function(cb) {
|
||||
OnelinerzModule.super_.prototype.beforeArt.call(this, err => {
|
||||
beforeArt(cb) {
|
||||
super.beforeArt(err => {
|
||||
return err ? cb(err) : this.initDatabase(cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,8 +1,34 @@
|
|||
{
|
||||
/*
|
||||
./\/\.' ENiGMA½ Prompt Configuration -/--/-------- - -- -
|
||||
|
||||
_____________________ _____ ____________________ __________\_ /
|
||||
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
|
||||
// __|___// | \// |// | \// | | \// \ /___ /_____
|
||||
/____ _____| __________ ___|__| ____| \ / _____ \
|
||||
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
|
||||
/__ _\
|
||||
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
This configuration is in HJSON (http://hjson.org/) format. Strict to-spec
|
||||
JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON.
|
||||
|
||||
See http://hjson.org/ for more information and syntax.
|
||||
|
||||
|
||||
If you haven't yet, copy the conents of this file to something like
|
||||
sick_board_prompt.hjson. Point to it via config.hjson using the
|
||||
'general.promptFile' key:
|
||||
|
||||
general: { promptFile: "sick_board_prompt.hjson" }
|
||||
|
||||
*/
|
||||
// :TODO: this entire file needs cleaned up a LOT
|
||||
// :TODO: Convert all of this to HJSON
|
||||
"prompts" : {
|
||||
"userCredentials" : {
|
||||
prompts: {
|
||||
userCredentials: {
|
||||
"art" : "usercred",
|
||||
"mci" : {
|
||||
"ET1" : {
|
||||
|
@ -106,8 +132,47 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// File Base Related
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
fileMenuCommand: {
|
||||
art: FILPMPT
|
||||
mci: {
|
||||
TL1: {}
|
||||
ET2: {
|
||||
argName: menuOption
|
||||
width: 20
|
||||
maxLength: 20
|
||||
textStyle: upper
|
||||
focus: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseRateEntryPrompt: {
|
||||
art: RATEFILE
|
||||
mci: {
|
||||
SM1: {
|
||||
argName: rating
|
||||
items: [ "-----", "*----", "**---", "***--", "****-", "*****" ]
|
||||
}
|
||||
}
|
||||
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Standard / Required
|
||||
//
|
||||
// Prompts in this section are considered "standard" and are required
|
||||
// to be present
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
pause: {
|
||||
//
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue