diff --git a/.eslintrc.json b/.eslintrc.json
index c7757f0d..5e9b45b6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -24,6 +24,7 @@
"error",
"always"
],
- "comma-dangle": 0
+ "comma-dangle": 0,
+ "no-trailing-spaces" :"warn"
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index d7053333..93752343 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ENiGMA½ BBS Software
-![alt text](docs/images/enigma-bbs.png "ENiGMA½ BBS")
+![ENiGMA½ BBS](docs/images/enigma-bbs.png "ENiGMA½ BBS")
ENiGMA½ is a modern BBS software with a nostalgic flair!
@@ -8,7 +8,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
## Features Available Now
* 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
+ * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based [mods](docs/mods.md)
* [MCI support](docs/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles
* Telnet, **SSH**, and both secure and non-secure [WebSocket](https://en.wikipedia.org/wiki/WebSocket) access built in! Additional servers are easy to implement
* [CP437](http://www.ascii-codes.com/) and UTF-8 output
@@ -17,7 +17,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
* 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](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), and [Exodus](https://oddnetwork.org/exodus/) support!
+ * [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support!
* [Bunyan](https://github.com/trentm/node-bunyan) logging
* [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!
diff --git a/UPGRADE.md b/UPGRADE.md
index 802a8092..c7651fcd 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -1,65 +1,90 @@
-# 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).
+# 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.7-alpha to 0.0.8-alpha
+ENiGMA 0.0.8-alpha comes with some structure changes:
+* Configuration files are defaulted to `./config`. Related, the `--config` option now points to a configuration **directory**
+* `./mods/art` has been moved to `./art/general`
+* `./mods` is now reserved for actual user addon modules
+* Themes have been moved from `./mods/themes` to `./art/themes`
+
+With the change to the `./mods` directory, `@systemModule` is now implied for `module` declarations in `menu.hjson`. To use a user module in `./mods` you must specify `@userModule`!
+
+With the above changes, you'll need to to at least:
+* Move your `~/.config/enigma-bbs/config.hjson` to `./config/config.hjson` or utlize the `--config` option.
+* Move your `prompt.hjson` and `menu.hjson` (e.g. `myboardname.hjson`) to `./config`
+* Move any non-theme art files, and theme directories to their appropriate locations mentioned above
+* Move any module directories such as `message_post_evt` to `./mods/`
+* Move any certificates, pub/private keys, etc. from `./misc` to `./config`
+* Specify user modules as `@userModule:my_module_name`
+
+# 0.0.6-alpha to 0.0.7-alpha
+No issues
+
+# 0.0.5-alpha to 0.0.6-alpha
+No issues
+
+# 0.0.4-alpha to 0.0.5-alpha
+No 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).
diff --git a/WHATSNEW.md b/WHATSNEW.md
new file mode 100644
index 00000000..667c296f
--- /dev/null
+++ b/WHATSNEW.md
@@ -0,0 +1,26 @@
+# Whats New
+This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
+
+## 0.0.8-alpha
+* [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors.
+* File descriptions (FILE_ID.DIZ, etc.) now support Renegade |## pipe, PCBoard, and other less common color codes found commonly in BBS era scene releases.
+* New menu stack flags: `noHistory` now works as expected, and a new addition of `popParent`. See the default `menu.hjson` for usage.
+* File structure changes making ENiGMA½ much easier to maintain and run in Docker. Thanks to RiPuk ([Dave Stephens](https://github.com/davestephens))! See [UPGRADE.md](UPGRADE.md) for details.
+* Switch to pure JS [xxhash](https://github.com/mscdex/node-xxhash) instead of farmhash. Too many issues on ARM and other less popular CPUs with farmhash ([Dave Stephens](https://github.com/davestephens))
+* Native [CombatNet](http://combatnet.us/) support! ([Dave Stephens](https://github.com/davestephens))
+* Fix various issues with legacy DOS Telnet terminals. Note that some may still have issues with extensive CPR usage by ENiGMA½ that will be addressed in a future release.
+* Added web (http://, https://) based download manager including batch downloads. Clickable links if using [VTXClient](https://github.com/codewar65/VTX_ClientServer)!
+* General VTX hyperlink support for web links
+* DEL vs Backspace key differences in FSE
+* Correly parse oddball `INTL`, `TOPT`, `FMPT`, `Via`, etc. FTN kludge lines
+* NetMail support! You can now send and receive NetMail. To send a NetMail address a external user using `Name
` format from your personal email menu. For example, `Foo Bar <123:123/123>`. The system also detects other formats such asa `Name @ address` (`Foo Bar@123:123/123`)
+* `oputil.js`: Added `mb areafix` command to quickly send AreaFix messages from the command line. You can manually send them from personal mail as well.
+* `oputil.js fb rm|remove|del|delete` functionality to remove file base entries
+* Users can now (re)set File and Message base pointers
+* Add `--update` option to `oputil.js fb scan`
+* Fix @watch path support for event scheduler including FTN, e.g. when looking for a `toss!.now` file produced by Binkd.
+
+...LOTS more!
+
+## Pre 0.0.8-alpha
+See GitHub
\ No newline at end of file
diff --git a/mods/art/CONNECT1.ANS b/art/general/CONNECT1.ANS
similarity index 100%
rename from mods/art/CONNECT1.ANS
rename to art/general/CONNECT1.ANS
diff --git a/mods/art/DOORMANY.ANS b/art/general/DOORMANY.ANS
similarity index 100%
rename from mods/art/DOORMANY.ANS
rename to art/general/DOORMANY.ANS
diff --git a/mods/art/GNSPMPT.ANS b/art/general/GNSPMPT.ANS
similarity index 100%
rename from mods/art/GNSPMPT.ANS
rename to art/general/GNSPMPT.ANS
diff --git a/mods/art/LOGPMPT.ANS b/art/general/LOGPMPT.ANS
similarity index 100%
rename from mods/art/LOGPMPT.ANS
rename to art/general/LOGPMPT.ANS
diff --git a/mods/art/NEWSCAN.ANS b/art/general/NEWSCAN.ANS
similarity index 100%
rename from mods/art/NEWSCAN.ANS
rename to art/general/NEWSCAN.ANS
diff --git a/mods/art/NEWUSER1.ANS b/art/general/NEWUSER1.ANS
similarity index 77%
rename from mods/art/NEWUSER1.ANS
rename to art/general/NEWUSER1.ANS
index 70edd11e..267c331c 100644
Binary files a/mods/art/NEWUSER1.ANS and b/art/general/NEWUSER1.ANS differ
diff --git a/mods/art/ONEADD.ANS b/art/general/ONEADD.ANS
similarity index 100%
rename from mods/art/ONEADD.ANS
rename to art/general/ONEADD.ANS
diff --git a/mods/art/ONELINER.ANS b/art/general/ONELINER.ANS
similarity index 100%
rename from mods/art/ONELINER.ANS
rename to art/general/ONELINER.ANS
diff --git a/mods/art/PRELOGAD.ANS b/art/general/PRELOGAD.ANS
similarity index 100%
rename from mods/art/PRELOGAD.ANS
rename to art/general/PRELOGAD.ANS
diff --git a/mods/art/WELCOME1.ANS b/art/general/WELCOME1.ANS
similarity index 100%
rename from mods/art/WELCOME1.ANS
rename to art/general/WELCOME1.ANS
diff --git a/mods/art/WELCOME2.ANS b/art/general/WELCOME2.ANS
similarity index 100%
rename from mods/art/WELCOME2.ANS
rename to art/general/WELCOME2.ANS
diff --git a/mods/art/demo_edit_text_view.ans b/art/general/demo_edit_text_view.ans
similarity index 100%
rename from mods/art/demo_edit_text_view.ans
rename to art/general/demo_edit_text_view.ans
diff --git a/mods/art/demo_edit_text_view1.ans b/art/general/demo_edit_text_view1.ans
similarity index 100%
rename from mods/art/demo_edit_text_view1.ans
rename to art/general/demo_edit_text_view1.ans
diff --git a/mods/art/demo_fse_local_user.ans b/art/general/demo_fse_local_user.ans
similarity index 100%
rename from mods/art/demo_fse_local_user.ans
rename to art/general/demo_fse_local_user.ans
diff --git a/mods/art/demo_fse_netmail_body.ans b/art/general/demo_fse_netmail_body.ans
similarity index 100%
rename from mods/art/demo_fse_netmail_body.ans
rename to art/general/demo_fse_netmail_body.ans
diff --git a/mods/art/demo_fse_netmail_footer_edit.ans b/art/general/demo_fse_netmail_footer_edit.ans
similarity index 100%
rename from mods/art/demo_fse_netmail_footer_edit.ans
rename to art/general/demo_fse_netmail_footer_edit.ans
diff --git a/mods/art/demo_fse_netmail_footer_edit_menu.ans b/art/general/demo_fse_netmail_footer_edit_menu.ans
similarity index 100%
rename from mods/art/demo_fse_netmail_footer_edit_menu.ans
rename to art/general/demo_fse_netmail_footer_edit_menu.ans
diff --git a/mods/art/demo_fse_netmail_header.ans b/art/general/demo_fse_netmail_header.ans
similarity index 100%
rename from mods/art/demo_fse_netmail_header.ans
rename to art/general/demo_fse_netmail_header.ans
diff --git a/mods/art/demo_fse_netmail_help.ans b/art/general/demo_fse_netmail_help.ans
similarity index 100%
rename from mods/art/demo_fse_netmail_help.ans
rename to art/general/demo_fse_netmail_help.ans
diff --git a/mods/art/demo_horizontal_menu_view1.ans b/art/general/demo_horizontal_menu_view1.ans
similarity index 100%
rename from mods/art/demo_horizontal_menu_view1.ans
rename to art/general/demo_horizontal_menu_view1.ans
diff --git a/mods/art/demo_mask_edit_text_view1.ans b/art/general/demo_mask_edit_text_view1.ans
similarity index 100%
rename from mods/art/demo_mask_edit_text_view1.ans
rename to art/general/demo_mask_edit_text_view1.ans
diff --git a/mods/art/demo_multi_line_edit_text_view1.ans b/art/general/demo_multi_line_edit_text_view1.ans
similarity index 100%
rename from mods/art/demo_multi_line_edit_text_view1.ans
rename to art/general/demo_multi_line_edit_text_view1.ans
diff --git a/mods/art/demo_selection_vm.ans b/art/general/demo_selection_vm.ans
similarity index 100%
rename from mods/art/demo_selection_vm.ans
rename to art/general/demo_selection_vm.ans
diff --git a/mods/art/demo_spin_and_toggle.ans b/art/general/demo_spin_and_toggle.ans
similarity index 100%
rename from mods/art/demo_spin_and_toggle.ans
rename to art/general/demo_spin_and_toggle.ans
diff --git a/mods/art/demo_vertical_menu_view1.ans b/art/general/demo_vertical_menu_view1.ans
similarity index 100%
rename from mods/art/demo_vertical_menu_view1.ans
rename to art/general/demo_vertical_menu_view1.ans
diff --git a/mods/art/erc.ans b/art/general/erc.ans
similarity index 100%
rename from mods/art/erc.ans
rename to art/general/erc.ans
diff --git a/mods/art/menu_prompt.ans b/art/general/menu_prompt.ans
similarity index 100%
rename from mods/art/menu_prompt.ans
rename to art/general/menu_prompt.ans
diff --git a/mods/art/msg_area_footer_view.ans b/art/general/msg_area_footer_view.ans
similarity index 100%
rename from mods/art/msg_area_footer_view.ans
rename to art/general/msg_area_footer_view.ans
diff --git a/mods/art/msg_area_list.ans b/art/general/msg_area_list.ans
similarity index 100%
rename from mods/art/msg_area_list.ans
rename to art/general/msg_area_list.ans
diff --git a/mods/art/msg_area_post_header.ans b/art/general/msg_area_post_header.ans
similarity index 100%
rename from mods/art/msg_area_post_header.ans
rename to art/general/msg_area_post_header.ans
diff --git a/mods/art/msg_area_view_header.ans b/art/general/msg_area_view_header.ans
similarity index 100%
rename from mods/art/msg_area_view_header.ans
rename to art/general/msg_area_view_header.ans
diff --git a/mods/art/test.ans b/art/general/test.ans
similarity index 100%
rename from mods/art/test.ans
rename to art/general/test.ans
diff --git a/mods/themes/luciano_blocktronics/BBSADD.ANS b/art/themes/luciano_blocktronics/BBSADD.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/BBSADD.ANS
rename to art/themes/luciano_blocktronics/BBSADD.ANS
diff --git a/mods/themes/luciano_blocktronics/BBSLIST.ANS b/art/themes/luciano_blocktronics/BBSLIST.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/BBSLIST.ANS
rename to art/themes/luciano_blocktronics/BBSLIST.ANS
diff --git a/mods/themes/luciano_blocktronics/CCHANGE.ANS b/art/themes/luciano_blocktronics/CCHANGE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/CCHANGE.ANS
rename to art/themes/luciano_blocktronics/CCHANGE.ANS
diff --git a/mods/themes/luciano_blocktronics/CHANGE.ANS b/art/themes/luciano_blocktronics/CHANGE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/CHANGE.ANS
rename to art/themes/luciano_blocktronics/CHANGE.ANS
diff --git a/mods/themes/luciano_blocktronics/CONFSCR.ANS b/art/themes/luciano_blocktronics/CONFSCR.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/CONFSCR.ANS
rename to art/themes/luciano_blocktronics/CONFSCR.ANS
diff --git a/mods/themes/luciano_blocktronics/DONE.ANS b/art/themes/luciano_blocktronics/DONE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/DONE.ANS
rename to art/themes/luciano_blocktronics/DONE.ANS
diff --git a/mods/themes/luciano_blocktronics/DOORMNU.ANS b/art/themes/luciano_blocktronics/DOORMNU.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/DOORMNU.ANS
rename to art/themes/luciano_blocktronics/DOORMNU.ANS
diff --git a/mods/themes/luciano_blocktronics/FAREASEL.ANS b/art/themes/luciano_blocktronics/FAREASEL.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FAREASEL.ANS
rename to art/themes/luciano_blocktronics/FAREASEL.ANS
diff --git a/mods/themes/luciano_blocktronics/FBHELP.ANS b/art/themes/luciano_blocktronics/FBHELP.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FBHELP.ANS
rename to art/themes/luciano_blocktronics/FBHELP.ANS
diff --git a/mods/themes/luciano_blocktronics/FBNORES.ANS b/art/themes/luciano_blocktronics/FBNORES.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FBNORES.ANS
rename to art/themes/luciano_blocktronics/FBNORES.ANS
diff --git a/mods/themes/luciano_blocktronics/FBRWSE.ANS b/art/themes/luciano_blocktronics/FBRWSE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FBRWSE.ANS
rename to art/themes/luciano_blocktronics/FBRWSE.ANS
diff --git a/mods/themes/luciano_blocktronics/FDETAIL.ANS b/art/themes/luciano_blocktronics/FDETAIL.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FDETAIL.ANS
rename to art/themes/luciano_blocktronics/FDETAIL.ANS
diff --git a/mods/themes/luciano_blocktronics/FDETGEN.ANS b/art/themes/luciano_blocktronics/FDETGEN.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FDETGEN.ANS
rename to art/themes/luciano_blocktronics/FDETGEN.ANS
diff --git a/mods/themes/luciano_blocktronics/FDETLST.ANS b/art/themes/luciano_blocktronics/FDETLST.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FDETLST.ANS
rename to art/themes/luciano_blocktronics/FDETLST.ANS
diff --git a/mods/themes/luciano_blocktronics/FDETNFO.ANS b/art/themes/luciano_blocktronics/FDETNFO.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FDETNFO.ANS
rename to art/themes/luciano_blocktronics/FDETNFO.ANS
diff --git a/mods/themes/luciano_blocktronics/FDLMGR.ANS b/art/themes/luciano_blocktronics/FDLMGR.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FDLMGR.ANS
rename to art/themes/luciano_blocktronics/FDLMGR.ANS
diff --git a/mods/themes/luciano_blocktronics/FEMPTYQ.ANS b/art/themes/luciano_blocktronics/FEMPTYQ.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FEMPTYQ.ANS
rename to art/themes/luciano_blocktronics/FEMPTYQ.ANS
diff --git a/mods/themes/luciano_blocktronics/FFILEDT.ANS b/art/themes/luciano_blocktronics/FFILEDT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FFILEDT.ANS
rename to art/themes/luciano_blocktronics/FFILEDT.ANS
diff --git a/mods/themes/luciano_blocktronics/FILPMPT.ANS b/art/themes/luciano_blocktronics/FILPMPT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FILPMPT.ANS
rename to art/themes/luciano_blocktronics/FILPMPT.ANS
diff --git a/mods/themes/luciano_blocktronics/FMENU.ANS b/art/themes/luciano_blocktronics/FMENU.ANS
similarity index 85%
rename from mods/themes/luciano_blocktronics/FMENU.ANS
rename to art/themes/luciano_blocktronics/FMENU.ANS
index a187491b..55879dd7 100644
Binary files a/mods/themes/luciano_blocktronics/FMENU.ANS and b/art/themes/luciano_blocktronics/FMENU.ANS differ
diff --git a/mods/themes/luciano_blocktronics/FNEWBRWSE.ANS b/art/themes/luciano_blocktronics/FNEWBRWSE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FNEWBRWSE.ANS
rename to art/themes/luciano_blocktronics/FNEWBRWSE.ANS
diff --git a/mods/themes/luciano_blocktronics/FORGOTPW.ANS b/art/themes/luciano_blocktronics/FORGOTPW.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FORGOTPW.ANS
rename to art/themes/luciano_blocktronics/FORGOTPW.ANS
diff --git a/mods/themes/luciano_blocktronics/FORGOTPWSENT.ANS b/art/themes/luciano_blocktronics/FORGOTPWSENT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FORGOTPWSENT.ANS
rename to art/themes/luciano_blocktronics/FORGOTPWSENT.ANS
diff --git a/mods/themes/luciano_blocktronics/FPROSEL.ANS b/art/themes/luciano_blocktronics/FPROSEL.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FPROSEL.ANS
rename to art/themes/luciano_blocktronics/FPROSEL.ANS
diff --git a/mods/themes/luciano_blocktronics/FSEARCH.ANS b/art/themes/luciano_blocktronics/FSEARCH.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/FSEARCH.ANS
rename to art/themes/luciano_blocktronics/FSEARCH.ANS
diff --git a/art/themes/luciano_blocktronics/FWDLMGR.ANS b/art/themes/luciano_blocktronics/FWDLMGR.ANS
new file mode 100644
index 00000000..b0e7fffc
Binary files /dev/null and b/art/themes/luciano_blocktronics/FWDLMGR.ANS differ
diff --git a/mods/themes/luciano_blocktronics/IDLELOG.ANS b/art/themes/luciano_blocktronics/IDLELOG.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/IDLELOG.ANS
rename to art/themes/luciano_blocktronics/IDLELOG.ANS
diff --git a/mods/themes/luciano_blocktronics/LASTCALL.ANS b/art/themes/luciano_blocktronics/LASTCALL.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/LASTCALL.ANS
rename to art/themes/luciano_blocktronics/LASTCALL.ANS
diff --git a/mods/themes/luciano_blocktronics/LETTER.ANS b/art/themes/luciano_blocktronics/LETTER.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/LETTER.ANS
rename to art/themes/luciano_blocktronics/LETTER.ANS
diff --git a/mods/themes/luciano_blocktronics/MAILMNU.ANS b/art/themes/luciano_blocktronics/MAILMNU.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MAILMNU.ANS
rename to art/themes/luciano_blocktronics/MAILMNU.ANS
diff --git a/mods/themes/luciano_blocktronics/MATRIX.ANS b/art/themes/luciano_blocktronics/MATRIX.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MATRIX.ANS
rename to art/themes/luciano_blocktronics/MATRIX.ANS
diff --git a/mods/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MMENU.ANS
rename to art/themes/luciano_blocktronics/MMENU.ANS
diff --git a/mods/themes/luciano_blocktronics/MNUPRMT.ANS b/art/themes/luciano_blocktronics/MNUPRMT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MNUPRMT.ANS
rename to art/themes/luciano_blocktronics/MNUPRMT.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGBODY.ANS b/art/themes/luciano_blocktronics/MSGBODY.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGBODY.ANS
rename to art/themes/luciano_blocktronics/MSGBODY.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGEFTR.ANS b/art/themes/luciano_blocktronics/MSGEFTR.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGEFTR.ANS
rename to art/themes/luciano_blocktronics/MSGEFTR.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGEHDR.ANS b/art/themes/luciano_blocktronics/MSGEHDR.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGEHDR.ANS
rename to art/themes/luciano_blocktronics/MSGEHDR.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGEHLP.ANS b/art/themes/luciano_blocktronics/MSGEHLP.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGEHLP.ANS
rename to art/themes/luciano_blocktronics/MSGEHLP.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGEMFT.ANS b/art/themes/luciano_blocktronics/MSGEMFT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGEMFT.ANS
rename to art/themes/luciano_blocktronics/MSGEMFT.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGLIST.ANS b/art/themes/luciano_blocktronics/MSGLIST.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGLIST.ANS
rename to art/themes/luciano_blocktronics/MSGLIST.ANS
diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS
new file mode 100644
index 00000000..ce6f4815
Binary files /dev/null and b/art/themes/luciano_blocktronics/MSGMNU.ANS differ
diff --git a/mods/themes/luciano_blocktronics/MSGPMPT.ANS b/art/themes/luciano_blocktronics/MSGPMPT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGPMPT.ANS
rename to art/themes/luciano_blocktronics/MSGPMPT.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGQUOT.ANS b/art/themes/luciano_blocktronics/MSGQUOT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGQUOT.ANS
rename to art/themes/luciano_blocktronics/MSGQUOT.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGVFTR.ANS b/art/themes/luciano_blocktronics/MSGVFTR.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGVFTR.ANS
rename to art/themes/luciano_blocktronics/MSGVFTR.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGVHDR.ANS b/art/themes/luciano_blocktronics/MSGVHDR.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGVHDR.ANS
rename to art/themes/luciano_blocktronics/MSGVHDR.ANS
diff --git a/mods/themes/luciano_blocktronics/MSGVHLP.ANS b/art/themes/luciano_blocktronics/MSGVHLP.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/MSGVHLP.ANS
rename to art/themes/luciano_blocktronics/MSGVHLP.ANS
diff --git a/mods/themes/luciano_blocktronics/NEWMSGS.ANS b/art/themes/luciano_blocktronics/NEWMSGS.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/NEWMSGS.ANS
rename to art/themes/luciano_blocktronics/NEWMSGS.ANS
diff --git a/mods/themes/luciano_blocktronics/NUA.ANS b/art/themes/luciano_blocktronics/NUA.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/NUA.ANS
rename to art/themes/luciano_blocktronics/NUA.ANS
diff --git a/mods/themes/luciano_blocktronics/ONEADD.ANS b/art/themes/luciano_blocktronics/ONEADD.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ONEADD.ANS
rename to art/themes/luciano_blocktronics/ONEADD.ANS
diff --git a/mods/themes/luciano_blocktronics/ONELINER.ANS b/art/themes/luciano_blocktronics/ONELINER.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ONELINER.ANS
rename to art/themes/luciano_blocktronics/ONELINER.ANS
diff --git a/mods/themes/luciano_blocktronics/PAUSE.ANS b/art/themes/luciano_blocktronics/PAUSE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/PAUSE.ANS
rename to art/themes/luciano_blocktronics/PAUSE.ANS
diff --git a/mods/themes/luciano_blocktronics/RATEFILE.ANS b/art/themes/luciano_blocktronics/RATEFILE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/RATEFILE.ANS
rename to art/themes/luciano_blocktronics/RATEFILE.ANS
diff --git a/mods/themes/luciano_blocktronics/RUMORADD.ANS b/art/themes/luciano_blocktronics/RUMORADD.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/RUMORADD.ANS
rename to art/themes/luciano_blocktronics/RUMORADD.ANS
diff --git a/mods/themes/luciano_blocktronics/RUMORS.ANS b/art/themes/luciano_blocktronics/RUMORS.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/RUMORS.ANS
rename to art/themes/luciano_blocktronics/RUMORS.ANS
diff --git a/art/themes/luciano_blocktronics/SETFNSDATE.ANS b/art/themes/luciano_blocktronics/SETFNSDATE.ANS
new file mode 100644
index 00000000..8294a6f7
Binary files /dev/null and b/art/themes/luciano_blocktronics/SETFNSDATE.ANS differ
diff --git a/art/themes/luciano_blocktronics/SETMNSDATE.ANS b/art/themes/luciano_blocktronics/SETMNSDATE.ANS
new file mode 100644
index 00000000..4d3b43a3
Binary files /dev/null and b/art/themes/luciano_blocktronics/SETMNSDATE.ANS differ
diff --git a/mods/themes/luciano_blocktronics/STATUS.ANS b/art/themes/luciano_blocktronics/STATUS.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/STATUS.ANS
rename to art/themes/luciano_blocktronics/STATUS.ANS
diff --git a/mods/themes/luciano_blocktronics/SYSSTAT.ANS b/art/themes/luciano_blocktronics/SYSSTAT.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/SYSSTAT.ANS
rename to art/themes/luciano_blocktronics/SYSSTAT.ANS
diff --git a/mods/themes/luciano_blocktronics/TBRIDGE.ANS b/art/themes/luciano_blocktronics/TBRIDGE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/TBRIDGE.ANS
rename to art/themes/luciano_blocktronics/TBRIDGE.ANS
diff --git a/mods/themes/luciano_blocktronics/TOONODE.ANS b/art/themes/luciano_blocktronics/TOONODE.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/TOONODE.ANS
rename to art/themes/luciano_blocktronics/TOONODE.ANS
diff --git a/mods/themes/luciano_blocktronics/ULCHECK.ANS b/art/themes/luciano_blocktronics/ULCHECK.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ULCHECK.ANS
rename to art/themes/luciano_blocktronics/ULCHECK.ANS
diff --git a/mods/themes/luciano_blocktronics/ULDETAIL.ANS b/art/themes/luciano_blocktronics/ULDETAIL.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ULDETAIL.ANS
rename to art/themes/luciano_blocktronics/ULDETAIL.ANS
diff --git a/mods/themes/luciano_blocktronics/ULDUPES.ANS b/art/themes/luciano_blocktronics/ULDUPES.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ULDUPES.ANS
rename to art/themes/luciano_blocktronics/ULDUPES.ANS
diff --git a/mods/themes/luciano_blocktronics/ULNOAREA.ANS b/art/themes/luciano_blocktronics/ULNOAREA.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ULNOAREA.ANS
rename to art/themes/luciano_blocktronics/ULNOAREA.ANS
diff --git a/mods/themes/luciano_blocktronics/ULOPTS.ANS b/art/themes/luciano_blocktronics/ULOPTS.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/ULOPTS.ANS
rename to art/themes/luciano_blocktronics/ULOPTS.ANS
diff --git a/mods/themes/luciano_blocktronics/USERLOG.ANS b/art/themes/luciano_blocktronics/USERLOG.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/USERLOG.ANS
rename to art/themes/luciano_blocktronics/USERLOG.ANS
diff --git a/mods/themes/luciano_blocktronics/USERLST.ANS b/art/themes/luciano_blocktronics/USERLST.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/USERLST.ANS
rename to art/themes/luciano_blocktronics/USERLST.ANS
diff --git a/mods/themes/luciano_blocktronics/WHOSON.ANS b/art/themes/luciano_blocktronics/WHOSON.ANS
similarity index 100%
rename from mods/themes/luciano_blocktronics/WHOSON.ANS
rename to art/themes/luciano_blocktronics/WHOSON.ANS
diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson
similarity index 96%
rename from mods/themes/luciano_blocktronics/theme.hjson
rename to art/themes/luciano_blocktronics/theme.hjson
index d553ed83..19f63194 100644
--- a/mods/themes/luciano_blocktronics/theme.hjson
+++ b/art/themes/luciano_blocktronics/theme.hjson
@@ -787,6 +787,27 @@
}
}
+ fileBaseWebDownloadManager: {
+ config: {
+ queueListFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
+ focusQueueListFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
+ queueManagerInfoFormat10: "|03batch|08: |03{webBatchDlLink}"
+ queueManagerInfoFormat11: "|03exp |08: |03{webBatchDlExpire}"
+ }
+
+ 0: {
+ mci: {
+ VM1: {
+ height: 8
+ }
+ HM2: {
+ width: 50
+ focusTextStyle: first lower
+ }
+ }
+ }
+ }
+
fileBaseUploadFiles: {
config: {
// processing
diff --git a/mods/menu.hjson b/config/menu.hjson
similarity index 95%
rename from mods/menu.hjson
rename to config/menu.hjson
index 15a59cb8..386c6f89 100644
--- a/mods/menu.hjson
+++ b/config/menu.hjson
@@ -215,16 +215,14 @@
desc: Applying
options: {
pause: true
- cls: true
+ cls: true
+ menuFlags: [ "noHistory" ]
}
}
newUserApplication: {
module: nua
art: NUA
- options: {
- menuFlags: [ "noHistory" ]
- }
next: [
{
// Initial SysOp does not send feedback to themselves
@@ -333,6 +331,7 @@
options: {
pause: true
cls: true
+ menuFlags: [ "noHistory" ]
}
}
@@ -344,9 +343,6 @@
module: nua
art: NUA
fallback: logoff
- options: {
- menuFlags: [ "noHistory" ]
- }
next: newUserFeedbackToSysOpPreamble
form: {
0: {
@@ -709,7 +705,7 @@
fullLoginSequenceNewScan: {
desc: Performing New Scan
- module: @systemModule:new_scan
+ module: new_scan
art: NEWSCAN
next: fullLoginSequenceSysStats
config: {
@@ -1066,7 +1062,7 @@
}
mainMenuUserConfig: {
- module: @systemModule:user_config
+ module: user_config
art: CONFSCR
form: {
0: {
@@ -1157,7 +1153,7 @@
mainMenuGlobalNewScan: {
desc: Performing New Scan
- module: @systemModule:new_scan
+ module: new_scan
art: NEWSCAN
config: {
messageListMenu: newScanMessageList
@@ -1646,6 +1642,10 @@
action: @menu:doorParty
}
{
+ value: { command: "CN" }
+ action: @menu:combatNet
+ }
+ {
value: { command: "AGENT" }
action: @menu:telnetBridgeAgency
}
@@ -1689,10 +1689,10 @@
}
}
- // DoorParty! support. You'll need to registger to obtain credentials
+ // DoorParty! support. You'll need to register to obtain credentials
doorParty: {
desc: Using DoorParty!
- module: @systemModule:door_party
+ module: door_party
config: {
username: XXXXXXXX
password: XXXXXXXX
@@ -1700,6 +1700,16 @@
}
}
+ // CombatNet support. You'll need to register at http://combatnet.us/ to obtain credentials
+ combatNet: {
+ desc: Using CombatNet
+ module: combatnet
+ config: {
+ bbsTag: CBNxxx
+ password: XXXXXXXXX
+ }
+ }
+
telnetBridgeAgency: {
desc: Connected to HappyLand BBS
module: telnet_bridge
@@ -1756,6 +1766,10 @@
value: { command: "]" }
action: @systemMethod:nextArea
}
+ {
+ value: { command: "D" }
+ action: @menu:messageAreaSetNewScanDate
+ }
{
value: 1
action: @menu:messageArea
@@ -1793,6 +1807,47 @@
}
}
+ messageAreaSetNewScanDate: {
+ module: set_newscan_date
+ desc: Message Base
+ art: SETMNSDATE
+ config: {
+ target: message
+ scanDateFormat: YYYYMMDD
+ }
+ form: {
+ 0: {
+ mci: {
+ ME1: {
+ focus: true
+ submit: true
+ argName: scanDate
+ maskPattern: "####/##/##"
+ }
+ SM2: {
+ argName: targetSelection
+ submit: false
+ justify: right
+ }
+ }
+ submit: {
+ *: [
+ {
+ value: { scanDate: null }
+ action: @method:scanDateSubmit
+ }
+ ]
+ }
+ actionKeys: [
+ {
+ keys: [ "escape", "q", "shift + q" ]
+ action: @systemMethod:prevMenu
+ }
+ ]
+ }
+ }
+ }
+
messageAreaChangeCurrentArea: {
// :TODO: rename this art to ACHANGE
art: CHANGE
@@ -2314,7 +2369,7 @@
ET2: {
argName: to
focus: true
- validate: @systemMethod:validateUserNameExists
+ validate: @systemMethod:validateGeneralMailAddressedTo
}
ET3: {
argName: subject
@@ -2475,6 +2530,10 @@
value: { menuOption: "D" }
action: @menu:fileBaseDownloadManager
}
+ {
+ value: { menuOption: "W" }
+ action: @menu:fileBaseWebDownloadManager
+ }
{
value: { menuOption: "U" }
action: @menu:fileBaseUploadFiles
@@ -2483,9 +2542,49 @@
value: { menuOption: "S" }
action: @menu:fileBaseSearch
}
+ {
+ value: { menuOption: "P" }
+ action: @menu:fileBaseSetNewScanDate
+ }
]
}
+ fileBaseSetNewScanDate: {
+ module: set_newscan_date
+ desc: File Base
+ art: SETFNSDATE
+ config: {
+ target: file
+ scanDateFormat: YYYYMMDD
+ }
+ form: {
+ 0: {
+ mci: {
+ ME1: {
+ focus: true
+ submit: true
+ argName: scanDate
+ maskPattern: "####/##/##"
+ }
+ }
+ submit: {
+ *: [
+ {
+ value: { scanDate: null }
+ action: @method:scanDateSubmit
+ }
+ ]
+ }
+ actionKeys: [
+ {
+ keys: [ "escape", "q", "shift + q" ]
+ action: @systemMethod:prevMenu
+ }
+ ]
+ }
+ }
+ }
+
fileBaseListEntries: {
module: file_area_list
desc: Browsing Files
@@ -2706,7 +2805,7 @@
art: FBNORES
options: {
pause: true
- menuFlags: [ "noHistory" ]
+ menuFlags: [ "noHistory", "popParent" ]
}
}
@@ -2742,6 +2841,7 @@
"rating",
"estimated year",
"size",
+ "filename",
]
argName: sortByIndex
}
@@ -2933,12 +3033,70 @@
}
}
+ fileBaseWebDownloadManager: {
+ desc: Web D/L Manager
+ module: file_base_web_download_manager
+ config: {
+ art: {
+ queueManager: FWDLMGR
+ batchList: BATDLINF
+ }
+ emptyQueueMenu: fileBaseDownloadManagerEmptyQueue
+ }
+ form: {
+ 0: {
+ mci: {
+ VM1: {
+ argName: queueItem
+ }
+ HM2: {
+ focus: true
+ items: [ "get batch link", "quit", "help" ]
+ argName: navSelect
+ }
+ }
+
+ submit: {
+ *: [
+ {
+ value: { navSelect: 0 }
+ action: @method:getBatchLink
+ }
+ {
+ value: { navSelect: 1 }
+ action: @systemMethod:prevMenu
+ }
+ ]
+ }
+
+ actionKeys: [
+ {
+ keys: [ "b", "shift + b" ]
+ action: @method:getBatchLink
+ }
+ {
+ 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" ]
+ menuFlags: [ "noHistory", "popParent" ]
}
}
@@ -3108,13 +3266,13 @@
art: ULNOAREA
options: {
pause: true
- menuFlags: [ "noHistory" ]
+ menuFlags: [ "noHistory", "popParent" ]
}
}
sendFilesToUser: {
desc: Downloading
- module: @systemModule:file_transfer
+ module: file_transfer
config: {
// defaults - generally use extraArgs
protocol: zmodem8kSexyz
@@ -3124,7 +3282,7 @@
recvFilesFromUser: {
desc: Uploading
- module: @systemModule:file_transfer
+ module: file_transfer
config: {
// defaults - generally use extraArgs
protocol: zmodem8kSexyz
@@ -3496,7 +3654,7 @@
"art" : "test.ans"
},
"demoFullScreenEditor" : {
- "module" : "@systemModule:fse",
+ "module" : "fse",
"config" : {
"editorType" : "netMail",
"art" : {
diff --git a/mods/prompt.hjson b/config/prompt.hjson
similarity index 95%
rename from mods/prompt.hjson
rename to config/prompt.hjson
index 83f01ec5..b6bf6691 100644
--- a/mods/prompt.hjson
+++ b/config/prompt.hjson
@@ -72,6 +72,20 @@
}
}
+ loginSequenceFlavorSelect: {
+ art: LOGINSEL
+ mci: {
+ TM1: {
+ argName: promptValue
+ items: [ "yes", "no" ]
+ focus: true
+ focusItemIndex: 1
+ hotKeys: { Y: 0, N: 1 }
+ hotKeySubmit: true
+ }
+ }
+ }
+
loginGlobalNewScan: {
art: GNSPMPT
mci: {
diff --git a/mods/abracadabra.js b/core/abracadabra.js
similarity index 94%
rename from mods/abracadabra.js
rename to core/abracadabra.js
index a84d2c63..85d1e205 100644
--- a/mods/abracadabra.js
+++ b/core/abracadabra.js
@@ -1,11 +1,11 @@
/* jslint node: true */
'use strict';
-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');
+const MenuModule = require('./menu_module.js').MenuModule;
+const DropFile = require('./dropfile.js').DropFile;
+const door = require('./door.js');
+const theme = require('./theme.js');
+const ansi = require('./ansi_term.js');
const async = require('async');
const assert = require('assert');
diff --git a/core/ansi_prep.js b/core/ansi_prep.js
index 29bd5ee4..45b93d32 100644
--- a/core/ansi_prep.js
+++ b/core/ansi_prep.js
@@ -23,6 +23,8 @@ module.exports = function ansiPrep(input, options, cb) {
options.rows = options.rows || options.termHeight || 'auto';
options.startCol = options.startCol || 1;
options.exportMode = options.exportMode || false;
+ options.fillLines = _.get(options, 'fillLines', true);
+ options.indent = options.indent || 0;
// in auto we start out at 25 rows, but can always expand for more
const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) );
@@ -111,15 +113,18 @@ module.exports = function ansiPrep(input, options, cb) {
const lastCol = getLastPopulatedColumn(row) + 1;
let i;
- line = '';
+ line = options.indent ?
+ output.length > 0 ? ' '.repeat(options.indent) : '' :
+ '';
+
for(i = 0; i < lastCol; ++i) {
const col = row[i];
- sgr = 0 === i ?
+ sgr = !options.asciiMode && 0 === i ?
col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' :
'';
- if(col.sgr) {
+ if(!options.asciiMode && col.sgr) {
sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
}
@@ -129,7 +134,10 @@ module.exports = function ansiPrep(input, options, cb) {
output += line;
if(i < row.length) {
- output += `${ANSI.blackBG()}${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`;
+ output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
+ if(options.fillLines) {
+ output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`;
+ }
}
if(options.startCol + i < options.termWidth || options.forceLineTerm) {
diff --git a/core/ansi_term.js b/core/ansi_term.js
index 8b4094f1..7eb10ec2 100644
--- a/core/ansi_term.js
+++ b/core/ansi_term.js
@@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
exports.setCursorStyle = setCursorStyle;
exports.setEmulatedBaudRate = setEmulatedBaudRate;
-
+exports.vtxHyperlink = vtxHyperlink;
//
// See also
@@ -485,3 +485,14 @@ function setEmulatedBaudRate(rate) {
}[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
}
+
+function vtxHyperlink(client, url, len) {
+ if(!client.terminalSupports('vtx_hyperlink')) {
+ return '';
+ }
+
+ len = len || url.length;
+
+ url = url.split('').map(c => c.charCodeAt(0)).join(';');
+ return `${ESC_CSI}1;${len};1;1;${url}\\`;
+}
\ No newline at end of file
diff --git a/core/art.js b/core/art.js
index 28657fc0..19e0bafe 100644
--- a/core/art.js
+++ b/core/art.js
@@ -14,7 +14,7 @@ const paths = require('path');
const assert = require('assert');
const iconv = require('iconv-lite');
const _ = require('lodash');
-const farmhash = require('farmhash');
+const xxhash = require('xxhash');
exports.getArt = getArt;
exports.getArtFromPath = getArtFromPath;
@@ -288,7 +288,7 @@ function display(client, art, options, cb) {
}
if(!options.disableMciCache) {
- artHash = farmhash.hash32(art);
+ artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE);
// see if we have a mciMap cached for this art
if(client.mciCache) {
diff --git a/core/asset.js b/core/asset.js
index 0731a1b7..9f2831b7 100644
--- a/core/asset.js
+++ b/core/asset.js
@@ -21,7 +21,7 @@ const ALL_ASSETS = [
'art',
'menu',
'method',
- 'module',
+ 'userModule',
'systemMethod',
'systemModule',
'prompt',
@@ -58,12 +58,12 @@ function getAssetWithShorthand(spec, defaultType) {
assert(_.isString(asset.type));
return asset;
- } else {
- return {
- type : defaultType,
- asset : spec,
- };
}
+
+ return {
+ type : defaultType,
+ asset : spec,
+ };
}
function getArtAsset(spec) {
@@ -78,13 +78,14 @@ function getArtAsset(spec) {
}
function getModuleAsset(spec) {
- const asset = getAssetWithShorthand(spec, 'module');
+ const asset = getAssetWithShorthand(spec, 'systemModule');
if(!asset) {
return null;
}
- assert( ['module', 'systemModule' ].indexOf(asset.type) > -1);
+ assert( ['userModule', 'systemModule' ].includes(asset.type) );
+
return asset;
}
diff --git a/core/bbs.js b/core/bbs.js
index c43d63a3..43bf7cf3 100644
--- a/core/bbs.js
+++ b/core/bbs.js
@@ -29,11 +29,12 @@ const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby';
const HELP =
`${ENIGMA_COPYRIGHT}
usage: main.js
+eg : main.js --config /enigma_install_path/config/
valid args:
--version : display version
--help : displays this help
- --config PATH : override default config.hjson path
+ --config PATH : override default config path
`;
function printHelpAndExit() {
@@ -56,7 +57,8 @@ function main() {
return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath));
},
function initConfig(configPath, configPathSupplied, callback) {
- conf.init(resolvePath(configPath), function configInit(err) {
+ const configFile = configPath + 'config.hjson';
+ conf.init(resolvePath(configFile), function configInit(err) {
//
// If the user supplied a path and we can't read/parse it
@@ -65,7 +67,7 @@ function main() {
if(err) {
if('ENOENT' === err.code) {
if(configPathSupplied) {
- console.error('Configuration file does not exist: ' + configPath);
+ console.error('Configuration file does not exist: ' + configFile);
} else {
configPathSupplied = null; // make non-fatal; we'll go with defaults
}
@@ -234,6 +236,17 @@ function initialize(cb) {
}
);
},
+ function initFileAreaStats(callback) {
+ const getAreaStats = require('./file_base_area.js').getAreaStats;
+ getAreaStats( (err, stats) => {
+ if(!err) {
+ const StatLog = require('./stat_log.js');
+ StatLog.setNonPeristentSystemStat('file_base_area_stats', stats);
+ }
+
+ return callback(null);
+ });
+ },
function initMCI(callback) {
return require('./predefined_mci.js').init(callback);
},
diff --git a/mods/bbs_link.js b/core/bbs_link.js
similarity index 97%
rename from mods/bbs_link.js
rename to core/bbs_link.js
index 0cf0a5db..be341115 100644
--- a/mods/bbs_link.js
+++ b/core/bbs_link.js
@@ -1,8 +1,8 @@
/* jslint node: true */
'use strict';
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const resetScreen = require('../core/ansi_term.js').resetScreen;
+const MenuModule = require('./menu_module.js').MenuModule;
+const resetScreen = require('./ansi_term.js').resetScreen;
const async = require('async');
const _ = require('lodash');
diff --git a/mods/bbs_list.js b/core/bbs_list.js
similarity index 95%
rename from mods/bbs_list.js
rename to core/bbs_list.js
index e24beba6..33a7ff59 100644
--- a/mods/bbs_list.js
+++ b/core/bbs_list.js
@@ -2,13 +2,18 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const getModDatabasePath = require('../core/database.js').getModDatabasePath;
-const ViewController = require('../core/view_controller.js').ViewController;
-const ansi = require('../core/ansi_term.js');
-const theme = require('../core/theme.js');
-const User = require('../core/user.js');
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+
+const {
+ getModDatabasePath,
+ getTransactionDatabase
+} = require('./database.js');
+
+const ViewController = require('./view_controller.js').ViewController;
+const ansi = require('./ansi_term.js');
+const theme = require('./theme.js');
+const User = require('./user.js');
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
@@ -392,10 +397,10 @@ exports.getModule = class BBSListModule extends MenuModule {
async.series(
[
function openDatabase(callback) {
- self.database = new sqlite3.Database(
+ self.database = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(moduleInfo),
callback
- );
+ ));
},
function createTables(callback) {
self.database.serialize( () => {
diff --git a/core/client.js b/core/client.js
index 4501396d..424748a6 100644
--- a/core/client.js
+++ b/core/client.js
@@ -306,7 +306,21 @@ function Client(input, output) {
key.name = 'line feed';
} else if('\t' === s) {
key.name = 'tab';
- } else if ('\b' === s || '\x7f' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
+ } else if('\x7f' === s) {
+ //
+ // Backspace vs delete is a crazy thing, especially in *nix.
+ // - ANSI-BBS uses 0x7f for DEL
+ // - xterm et. al clients send 0x7f for backspace... ugg.
+ //
+ // See http://www.hypexr.org/linux_ruboff.php
+ // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
+ //
+ if(self.term.isNixTerm()) {
+ key.name = 'backspace';
+ } else {
+ key.name = 'delete';
+ }
+ } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
// backspace, CTRL-H
key.name = 'backspace';
key.meta = ('\x1b' === s.charAt(0));
@@ -493,10 +507,15 @@ Client.prototype.defaultHandlerMissingMod = function(err) {
};
Client.prototype.terminalSupports = function(query) {
+ const termClient = this.term.termClient;
+
switch(query) {
case 'vtx_audio' :
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
- return this.termClient === 'vtx';
+ return 'vtx' === termClient;
+
+ case 'vtx_hyperlink' :
+ return 'vtx' === termClient;
default :
return false;
diff --git a/core/client_term.js b/core/client_term.js
index 9992f9f3..b313841e 100644
--- a/core/client_term.js
+++ b/core/client_term.js
@@ -110,6 +110,17 @@ ClientTerminal.prototype.disconnect = function() {
this.output = null;
};
+ClientTerminal.prototype.isNixTerm = function() {
+ //
+ // Standard *nix type terminals
+ //
+ if(this.termType.startsWith('xterm')) {
+ return true;
+ }
+
+ return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType);
+};
+
ClientTerminal.prototype.isANSI = function() {
//
// ANSI terminals should be encoded to CP437
@@ -142,7 +153,7 @@ ClientTerminal.prototype.isANSI = function() {
// linux:
// * JuiceSSH (note: TERM=linux also)
//
- return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1;
+ return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType);
};
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
diff --git a/core/color_codes.js b/core/color_codes.js
index 4dc8da99..2e368aa3 100644
--- a/core/color_codes.js
+++ b/core/color_codes.js
@@ -11,11 +11,13 @@ exports.enigmaToAnsi = enigmaToAnsi;
exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes;
exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen;
exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
+exports.controlCodesToAnsi = controlCodesToAnsi;
// :TODO: Not really happy with the module name of "color_codes". Would like something better
+
// Also add:
// * fromCelerity(): |
// * fromPCBoard(): (@X)
@@ -85,6 +87,46 @@ function enigmaStrLen(s) {
return stripEnigmaCodes(s).length;
}
+function ansiSgrFromRenegadeColorCode(cc) {
+ return ansi.sgr({
+ 0 : [ 'reset', 'black' ],
+ 1 : [ 'reset', 'blue' ],
+ 2 : [ 'reset', 'green' ],
+ 3 : [ 'reset', 'cyan' ],
+ 4 : [ 'reset', 'red' ],
+ 5 : [ 'reset', 'magenta' ],
+ 6 : [ 'reset', 'yellow' ],
+ 7 : [ 'reset', 'white' ],
+
+ 8 : [ 'bold', 'black' ],
+ 9 : [ 'bold', 'blue' ],
+ 10 : [ 'bold', 'green' ],
+ 11 : [ 'bold', 'cyan' ],
+ 12 : [ 'bold', 'red' ],
+ 13 : [ 'bold', 'magenta' ],
+ 14 : [ 'bold', 'yellow' ],
+ 15 : [ 'bold', 'white' ],
+
+ 16 : [ 'blackBG' ],
+ 17 : [ 'blueBG' ],
+ 18 : [ 'greenBG' ],
+ 19 : [ 'cyanBG' ],
+ 20 : [ 'redBG' ],
+ 21 : [ 'magentaBG' ],
+ 22 : [ 'yellowBG' ],
+ 23 : [ 'whiteBG' ],
+
+ 24 : [ 'bold', 'blackBG' ],
+ 25 : [ 'bold', 'blueBG' ],
+ 26 : [ 'bold', 'greenBG' ],
+ 27 : [ 'bold', 'cyanBG' ],
+ 28 : [ 'bold', 'redBG' ],
+ 29 : [ 'bold', 'magentaBG' ],
+ 30 : [ 'bold', 'yellowBG' ],
+ 31 : [ 'bold', 'whiteBG' ],
+ }[cc] || 'normal');
+}
+
function renegadeToAnsi(s, client) {
if(-1 == s.indexOf('|')) {
return s; // no pipe codes present
@@ -111,35 +153,7 @@ function renegadeToAnsi(s, client) {
if(_.isString(val)) {
result += s.substr(lastIndex, m.index - lastIndex) + val;
} else {
- var attr = ansi.sgr({
- 0 : [ 'reset', 'black' ],
- 1 : [ 'reset', 'blue' ],
- 2 : [ 'reset', 'green' ],
- 3 : [ 'reset', 'cyan' ],
- 4 : [ 'reset', 'red' ],
- 5 : [ 'reset', 'magenta' ],
- 6 : [ 'reset', 'yellow' ],
- 7 : [ 'reset', 'white' ],
-
- 8 : [ 'bold', 'black' ],
- 9 : [ 'bold', 'blue' ],
- 10 : [ 'bold', 'green' ],
- 11 : [ 'bold', 'cyan' ],
- 12 : [ 'bold', 'red' ],
- 13 : [ 'bold', 'magenta' ],
- 14 : [ 'bold', 'yellow' ],
- 15 : [ 'bold', 'white' ],
-
- 16 : [ 'blackBG' ],
- 17 : [ 'blueBG' ],
- 18 : [ 'greenBG' ],
- 19 : [ 'cyanBG' ],
- 20 : [ 'redBG' ],
- 21 : [ 'magentaBG' ],
- 22 : [ 'yellowBG' ],
- 23 : [ 'whiteBG' ],
- }[val] || 'normal');
-
+ const attr = ansiSgrFromRenegadeColorCode(val);
result += s.substr(lastIndex, m.index - lastIndex) + attr;
}
@@ -148,3 +162,131 @@ function renegadeToAnsi(s, client) {
return (0 === result.length ? s : result + s.substr(lastIndex));
}
+
+//
+// Converts various control codes popular in BBS packages
+// to ANSI escape sequences. Additionaly supports ENiGMA style
+// MCI codes.
+//
+// Supported control code formats:
+// * Renegade : |##
+// * PCBoard : @X## where the first number/char is FG color, and second is BG
+// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
+// * WWIV : ^#
+//
+// TODO: Add Synchronet and Celerity format support
+//
+// Resources:
+// * http://wiki.synchro.net/custom:colors
+//
+function controlCodesToAnsi(s, client) {
+ const RE = /(\|([A-Z0-9]{2})|\|)|(\@X([0-9A-F]{2}))|(\@([0-9A-F]{2})\@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex
+
+ let m;
+ let result = '';
+ let lastIndex = 0;
+ let v;
+ let fg;
+ let bg;
+
+ while((m = RE.exec(s))) {
+ switch(m[0].charAt(0)) {
+ case '|' :
+ // Renegade or ENiGMA MCI
+ v = parseInt(m[2], 10);
+
+ if(isNaN(v)) {
+ v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
+ }
+
+ if(_.isString(v)) {
+ result += s.substr(lastIndex, m.index - lastIndex) + v;
+ } else {
+ v = ansiSgrFromRenegadeColorCode(v);
+ result += s.substr(lastIndex, m.index - lastIndex) + v;
+ }
+ break;
+
+ case '@' :
+ // PCBoard @X## or Wildcat! @##@
+ if('@' === m[0].substr(-1)) {
+ // Wildcat!
+ v = m[6];
+ } else {
+ v = m[4];
+ }
+
+ fg = {
+ 0 : [ 'reset', 'black' ],
+ 1 : [ 'reset', 'blue' ],
+ 2 : [ 'reset', 'green' ],
+ 3 : [ 'reset', 'cyan' ],
+ 4 : [ 'reset', 'red' ],
+ 5 : [ 'reset', 'magenta' ],
+ 6 : [ 'reset', 'yellow' ],
+ 7 : [ 'reset', 'white' ],
+
+ 8 : [ 'blink', 'black' ],
+ 9 : [ 'blink', 'blue' ],
+ A : [ 'blink', 'green' ],
+ B : [ 'blink', 'cyan' ],
+ C : [ 'blink', 'red' ],
+ D : [ 'blink', 'magenta' ],
+ E : [ 'blink', 'yellow' ],
+ F : [ 'blink', 'white' ],
+ }[v.charAt(0)] || ['normal'];
+
+ bg = {
+ 0 : [ 'blackBG' ],
+ 1 : [ 'blueBG' ],
+ 2 : [ 'greenBG' ],
+ 3 : [ 'cyanBG' ],
+ 4 : [ 'redBG' ],
+ 5 : [ 'magentaBG' ],
+ 6 : [ 'yellowBG' ],
+ 7 : [ 'whiteBG' ],
+
+ 8 : [ 'bold', 'blackBG' ],
+ 9 : [ 'bold', 'blueBG' ],
+ A : [ 'bold', 'greenBG' ],
+ B : [ 'bold', 'cyanBG' ],
+ C : [ 'bold', 'redBG' ],
+ D : [ 'bold', 'magentaBG' ],
+ E : [ 'bold', 'yellowBG' ],
+ F : [ 'bold', 'whiteBG' ],
+ }[v.charAt(1)] || [ 'normal' ];
+
+ v = ansi.sgr(fg.concat(bg));
+ result += s.substr(lastIndex, m.index - lastIndex) + v;
+ break;
+
+ case '\x03' :
+ v = parseInt(m[8], 10);
+
+ if(isNaN(v)) {
+ v += m[0];
+ } else {
+ v = ansi.sgr({
+ 0 : [ 'reset', 'black' ],
+ 1 : [ 'bold', 'cyan' ],
+ 2 : [ 'bold', 'yellow' ],
+ 3 : [ 'reset', 'magenta' ],
+ 4 : [ 'bold', 'white', 'blueBG' ],
+ 5 : [ 'reset', 'green' ],
+ 6 : [ 'bold', 'blink', 'red' ],
+ 7 : [ 'bold', 'blue' ],
+ 8 : [ 'reset', 'blue' ],
+ 9 : [ 'reset', 'cyan' ],
+ }[v] || 'normal');
+ }
+
+ result += s.substr(lastIndex, m.index - lastIndex) + v;
+
+ break;
+ }
+
+ lastIndex = RE.lastIndex;
+ }
+
+ return (0 === result.length ? s : result + s.substr(lastIndex));
+}
\ No newline at end of file
diff --git a/core/combatnet.js b/core/combatnet.js
new file mode 100644
index 00000000..6cde9c7b
--- /dev/null
+++ b/core/combatnet.js
@@ -0,0 +1,115 @@
+/* jslint node: true */
+'use strict';
+
+// enigma-bbs
+const MenuModule = require('../core/menu_module.js').MenuModule;
+const resetScreen = require('../core/ansi_term.js').resetScreen;
+
+// deps
+const async = require('async');
+const _ = require('lodash');
+const RLogin = require('rlogin');
+
+exports.moduleInfo = {
+ name : 'CombatNet',
+ desc : 'CombatNet Access Module',
+ author : 'Dave Stephens',
+};
+
+exports.getModule = class CombatNetModule extends MenuModule {
+ constructor(options) {
+ super(options);
+
+ // establish defaults
+ this.config = options.menuConfig.config;
+ this.config.host = this.config.host || 'bbs.combatnet.us';
+ this.config.rloginPort = this.config.rloginPort || 4513;
+ }
+
+ initSequence() {
+ const self = this;
+
+ async.series(
+ [
+ function validateConfig(callback) {
+ if(!_.isString(self.config.password)) {
+ return callback(new Error('Config requires "password"!'));
+ }
+ if(!_.isString(self.config.bbsTag)) {
+ return callback(new Error('Config requires "bbsTag"!'));
+ }
+ return callback(null);
+ },
+ function establishRloginConnection(callback) {
+ self.client.term.write(resetScreen());
+ self.client.term.write('Connecting to CombatNet, please wait...\n');
+
+ const restorePipeToNormal = function() {
+ self.client.term.output.removeListener('data', sendToRloginBuffer);
+ };
+
+ const rlogin = new RLogin(
+ { 'clientUsername' : self.config.password,
+ 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`,
+ 'host' : self.config.host,
+ 'port' : self.config.rloginPort,
+ 'terminalType' : self.client.term.termClient,
+ 'terminalSpeed' : 57600
+ }
+ );
+
+ // If there was an error ...
+ rlogin.on('error', err => {
+ self.client.log.info(`CombatNet rlogin client error: ${err.message}`);
+ restorePipeToNormal();
+ callback(err);
+ });
+
+ // If we've been disconnected ...
+ rlogin.on('disconnect', () => {
+ self.client.log.info(`Disconnected from CombatNet`);
+ restorePipeToNormal();
+ callback(null);
+ });
+
+ function sendToRloginBuffer(buffer) {
+ rlogin.send(buffer);
+ };
+
+ rlogin.on("connect",
+ /* The 'connect' event handler will be supplied with one argument,
+ a boolean indicating whether or not the connection was established. */
+
+ function(state) {
+ if(state) {
+ self.client.log.info('Connected to CombatNet');
+ self.client.term.output.on('data', sendToRloginBuffer);
+
+ } else {
+ return callback(new Error('Failed to establish establish CombatNet connection'));
+ }
+ }
+ );
+
+ // If data (a Buffer) has been received from the server ...
+ rlogin.on("data", (data) => {
+ self.client.term.rawWrite(data);
+ });
+
+ // connect...
+ rlogin.connect();
+
+ // note: no explicit callback() until we're finished!
+ }
+ ],
+ err => {
+ if(err) {
+ self.client.log.warn( { error : err.message }, 'CombatNet error');
+ }
+
+ // if the client is still here, go to previous
+ self.prevMenu();
+ }
+ );
+ }
+};
diff --git a/core/config.js b/core/config.js
index 449a5c98..62ce6032 100644
--- a/core/config.js
+++ b/core/config.js
@@ -2,7 +2,6 @@
'use strict';
// ENiGMA½
-const miscUtil = require('./misc_util.js');
// deps
const fs = require('graceful-fs');
@@ -19,7 +18,7 @@ function hasMessageConferenceAndArea(config) {
assert(_.isObject(config.messageConferences)); // we create one ourself!
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
- return 'system_internal' !== confTag;
+ return 'system_internal' !== confTag;
});
if(0 === nonInternalConfs.length) {
@@ -32,7 +31,7 @@ function hasMessageConferenceAndArea(config) {
_.forEach(nonInternalConfs, confTag => {
if(_.has(config.messageConferences[confTag], 'areas') &&
Object.keys(config.messageConferences[confTag].areas) > 0)
- {
+ {
result = true;
return false; // stop iteration
}
@@ -53,12 +52,12 @@ function init(configPath, options, cb) {
if(!_.isString(configPath)) {
return callback(null, { } );
}
-
+
fs.readFile(configPath, { encoding : 'utf8' }, (err, configData) => {
if(err) {
return callback(err);
}
-
+
let configJson;
try {
configJson = hjson.parse(configData, options);
@@ -67,12 +66,12 @@ function init(configPath, options, cb) {
}
return callback(null, configJson);
- });
+ });
},
function mergeWithDefaultConfig(configJson, callback) {
-
+
const mergedConfig = _.mergeWith(
- getDefaultConfig(),
+ getDefaultConfig(),
configJson, (conf1, conf2) => {
// Arrays should always concat
if(_.isArray(conf1)) {
@@ -111,11 +110,8 @@ function init(configPath, options, cb) {
}
function getDefaultPath() {
- 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');
- }
+ // e.g. /enigma-bbs-install-path/config/
+ return './config/';
}
function getDefaultConfig() {
@@ -127,8 +123,8 @@ function getDefaultConfig() {
loginAttempts : 3,
- menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./mods)
- promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./mods)
+ menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config)
+ promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config)
},
// :TODO: see notes below about 'theme' section - move this!
@@ -150,15 +146,16 @@ function getDefaultConfig() {
webMax : 255,
requireActivation : false, // require SysOp activation? false = auto-activate
- invalidUsernames : [],
groups : [ 'users', 'sysops' ], // built in groups
defaultGroups : [ 'users' ], // default groups new users belong to
newUserNames : [ 'new', 'apply' ], // Names reserved for applying
- // :TODO: Mystic uses TRASHCAN.DAT for this -- is there a reason to support something like that?
- badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all' ],
+ badUserNames : [
+ 'sysop', 'admin', 'administrator', 'root', 'all',
+ 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix'
+ ],
},
// :TODO: better name for "defaults"... which is redundant here!
@@ -187,9 +184,10 @@ function getDefaultConfig() {
menus : {
cls : true, // Clear screen before each menu by default?
- },
+ },
paths : {
+ config : paths.join(__dirname, './../config/'),
mods : paths.join(__dirname, './../mods/'),
loginServers : paths.join(__dirname, './servers/login/'),
contentServers : paths.join(__dirname, './servers/content/'),
@@ -197,15 +195,15 @@ function getDefaultConfig() {
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
mailers : paths.join(__dirname, './mailers/') ,
- art : paths.join(__dirname, './../mods/art/'),
- themes : paths.join(__dirname, './../mods/themes/'),
+ art : paths.join(__dirname, './../art/general/'),
+ themes : paths.join(__dirname, './../art/themes/'),
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
db : paths.join(__dirname, './../db/'),
- modsDb : paths.join(__dirname, './../db/mods/'),
+ modsDb : paths.join(__dirname, './../db/mods/'),
dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/
misc : paths.join(__dirname, './../misc/'),
},
-
+
loginServers : {
telnet : {
port : 8888,
@@ -214,18 +212,18 @@ function getDefaultConfig() {
},
ssh : {
port : 8889,
- enabled : false, // defualt to false as PK/pass in config.hjson are required
+ enabled : false, // default to false as PK/pass in config.hjson are required
//
// Private key in PEM format
- //
+ //
// Generating your PK:
- // > openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
+ // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048
//
// Then, set servers.ssh.privateKeyPass to the password you use above
// in your config.hjson
//
- privateKeyPem : paths.join(__dirname, './../misc/ssh_private_key.pem'),
+ privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'),
firstMenu : 'sshConnected',
firstMenuNewUser : 'sshConnectedNewUser',
},
@@ -233,8 +231,8 @@ function getDefaultConfig() {
port : 8810, // ws://
enabled : false,
securePort : 8811, // wss:// - must provide certPem and keyPem
- certPem : paths.join(__dirname, './../misc/https_cert.pem'),
- keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'),
+ certPem : paths.join(__dirname, './../config/https_cert.pem'),
+ keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
},
},
@@ -247,7 +245,7 @@ function getDefaultConfig() {
resetPassword : {
//
// The following templates have these variables available to them:
- //
+ //
// * %BOARDNAME% : Name of BBS
// * %USERNAME% : Username of whom to reset password
// * %TOKEN% : Reset token
@@ -262,16 +260,16 @@ function getDefaultConfig() {
//
resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'),
},
-
+
http : {
enabled : false,
- port : 8080,
+ port : 8080,
},
https : {
enabled : false,
port : 8443,
- certPem : paths.join(__dirname, './../misc/https_cert.pem'),
- keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'),
+ certPem : paths.join(__dirname, './../config/https_cert.pem'),
+ keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
}
}
},
@@ -282,10 +280,10 @@ function getDefaultConfig() {
},
Exiftool : {
cmd : 'exiftool',
- args : [
+ args : [
'-charset', 'utf8', '{filePath}',
// exclude the following:
- '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize',
+ '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize',
'--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate',
'--metadatadate', '--xmptoolkit'
]
@@ -304,7 +302,7 @@ function getDefaultConfig() {
//
//
// :TODO: text/x-ansi -> SAUCE extraction for .ans uploads
- // :TODO: textual : bool -- if text, we can view.
+ // :TODO: textual : bool -- if text, we can view.
// :TODO: asText : { cmd, args[] } -> viewable text
//
@@ -387,7 +385,7 @@ function getDefaultConfig() {
sig : '526172211a0700',
offset : 0,
archiveHandler : 'Rar',
- },
+ },
'application/gzip' : {
desc : 'Gzip Archive',
sig : '1f8b',
@@ -399,28 +397,28 @@ function getDefaultConfig() {
desc : 'BZip2 Archive',
sig : '425a68',
offset : 0,
- archiveHandler : '7Zip',
+ archiveHandler : '7Zip',
},
'application/x-lzh-compressed' : {
desc : 'LHArc Archive',
sig : '2d6c68',
offset : 2,
- archiveHandler : 'Lha',
+ archiveHandler : 'Lha',
},
'application/x-7z-compressed' : {
desc : '7-Zip Archive',
sig : '377abcaf271c',
offset : 0,
- archiveHandler : '7Zip',
+ archiveHandler : '7Zip',
}
// :TODO: update archives::formats to fall here
// * archive handler -> archiveHandler (consider archive if archiveHandler present)
// * sig, offset, ...
// * mime-db -> exts lookup
- // *
+ // *
},
-
+
archives : {
archivers : {
'7Zip' : {
@@ -513,7 +511,7 @@ function getDefaultConfig() {
list : {
cmd : 'tar',
args : [ '-tvf', '{archivePath}' ],
- entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$',
+ entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$',
},
extract : {
cmd : 'tar',
@@ -522,7 +520,7 @@ function getDefaultConfig() {
}
},
},
-
+
fileTransferProtocols : {
//
// See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ
@@ -538,7 +536,7 @@ function getDefaultConfig() {
recvCmd : 'sexyz',
recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ],
recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ],
- }
+ }
},
xmodemSexyz : {
@@ -569,7 +567,7 @@ function getDefaultConfig() {
name : 'ZModem 8k',
type : 'external',
sort : 2,
- external : {
+ external : {
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
sendArgs : [
// :TODO: try -q
@@ -580,11 +578,11 @@ function getDefaultConfig() {
'--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
- }
+ escapeTelnet : true, // set to true to escape Telnet codes such as IAC
+ }
}
},
-
+
messageAreaDefaults : {
//
// The following can be override per-area as well
@@ -593,15 +591,16 @@ function getDefaultConfig() {
maxAgeDays : 0, // 0 = unlimited
},
- messageConferences : {
+ messageConferences : {
system_internal : {
name : 'System Internal',
desc : 'Built in conference for private messages, bulletins, etc.',
-
+
areas : {
private_mail : {
- name : 'Private Mail',
- desc : 'Private user to user mail/email',
+ name : 'Private Mail',
+ desc : 'Private user to user mail/email',
+ maxExternalSentAgeDays : 30, // max external "outbox" item age
},
local_bulletin : {
@@ -611,14 +610,15 @@ function getDefaultConfig() {
}
}
},
-
+
scannerTossers : {
ftn_bso : {
paths : {
- outbound : paths.join(__dirname, './../mail/ftn_out/'),
- inbound : paths.join(__dirname, './../mail/ftn_in/'),
- secInbound : paths.join(__dirname, './../mail/ftn_secin/'),
- reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc.
+ outbound : paths.join(__dirname, './../mail/ftn_out/'),
+ inbound : paths.join(__dirname, './../mail/ftn_in/'),
+ secInbound : paths.join(__dirname, './../mail/ftn_secin/'),
+ reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc.
+ //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'),
// set 'retain' to a valid path to keep good pkt files
},
@@ -636,12 +636,13 @@ function getDefaultConfig() {
secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected)
uploadBy : 'ENiGMA TIC', // default upload by username (override @ network)
allowReplace : false, // use "Replaces" TIC field
+ descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc
}
}
},
fileBase: {
- // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|:
+ // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|:
areaStoragePrefix : paths.join(__dirname, './../file_base/'),
maxDescFileByteSize : 471859, // ~1/4 MB
@@ -650,12 +651,13 @@ function getDefaultConfig() {
fileNamePatterns: {
// 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$'
+ // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available.
+ desc : [
+ '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$'
],
// common README filename - https://en.wikipedia.org/wiki/README
- descLong : [
+ descLong : [
'^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$'
],
},
@@ -665,12 +667,17 @@ function getDefaultConfig() {
// 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((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ...
+ '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ...
+ '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ...
+ '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ...
+ //'\\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
- '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- do this before 19xx 20xx such that this has priority
+ '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority
'\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries
+ '\\b\'([17-9][0-9])\\b', // '95, '17, ...
// :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc.
],
@@ -683,42 +690,54 @@ function getDefaultConfig() {
//
// File area storage location tag/value pairs.
// Non-absolute paths are relative to |areaStoragePrefix|.
- //
+ //
storageTags : {
- sys_msg_attach : 'msg_attach',
+ sys_msg_attach : 'sys_msg_attach',
+ sys_temp_download : 'sys_temp_download',
},
areas: {
system_message_attachment : {
- name : 'Message attachments',
+ name : 'System Message Attachments',
desc : 'File attachments to messages',
- storageTags : 'sys_msg_attach', // may be string or array of strings
+ storageTags : [ 'sys_msg_attach' ],
+ },
+
+ system_temporary_download : {
+ name : 'System Temporary Downloads',
+ desc : 'Temporary downloadables',
+ storageTags : [ 'sys_temp_download' ],
}
}
},
-
+
eventScheduler : {
-
+
events : {
trimMessageAreas : {
// may optionally use [or ]@watch:/path/to/file
schedule : 'every 24 hours',
-
+
// action:
// - @method:path/to/module.js:theMethodName
// (path is relative to engima base dir)
//
- // - @execute:/path/to/something/executable.sh
- //
+ // - @execute:/path/to/something/executable.sh
+ //
action : '@method:core/message_area.js:trimMessageAreasScheduledEvent',
},
+ updateFileAreaStats : {
+ schedule : 'every 1 hours',
+ action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent',
+ },
+
forgotPasswordMaintenance : {
schedule : 'every 24 hours',
action : '@method:core/web_password_reset.js:performMaintenanceTask',
args : [ '24 hours' ] // items older than this will be removed
}
- }
+ }
},
misc : {
diff --git a/core/config_util.js b/core/config_util.js
index f078f758..40723d9a 100644
--- a/core/config_util.js
+++ b/core/config_util.js
@@ -1,16 +1,15 @@
/* jslint node: true */
'use strict';
-
-var configCache = require('./config_cache.js');
-
-var paths = require('path');
+const Config = require('./config.js').config;
+const configCache = require('./config_cache.js');
+const paths = require('path');
exports.getFullConfig = getFullConfig;
function getFullConfig(filePath, cb) {
- // |filePath| is assumed to be in 'mods' if it's only a file name
+ // |filePath| is assumed to be in the config path if it's only a file name
if('.' === paths.dirname(filePath)) {
- filePath = paths.join(__dirname, '../mods', filePath);
+ filePath = paths.join(Config.paths.config, filePath);
}
configCache.getConfig(filePath, function loaded(err, configJson) {
diff --git a/core/database.js b/core/database.js
index d4fb4795..14b3bf95 100644
--- a/core/database.js
+++ b/core/database.js
@@ -6,6 +6,7 @@ const conf = require('./config.js');
// deps
const sqlite3 = require('sqlite3');
+const sqlite3Trans = require('sqlite3-trans');
const paths = require('path');
const async = require('async');
const _ = require('lodash');
@@ -13,14 +14,19 @@ const assert = require('assert');
const moment = require('moment');
// database handles
-let dbs = {};
+const dbs = {};
+exports.getTransactionDatabase = getTransactionDatabase;
exports.getModDatabasePath = getModDatabasePath;
exports.getISOTimestampString = getISOTimestampString;
exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs;
+function getTransactionDatabase(db) {
+ return sqlite3Trans.wrap(db);
+}
+
function getDatabasePath(name) {
return paths.join(conf.config.paths.db, `${name}.sqlite3`);
}
@@ -55,7 +61,7 @@ function getISOTimestampString(ts) {
function initializeDatabases(cb) {
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
- dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => {
+ dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => {
if(err) {
return cb(err);
}
@@ -65,7 +71,7 @@ function initializeDatabases(cb) {
return next(null);
});
});
- });
+ }));
}, err => {
return cb(err);
});
@@ -368,6 +374,15 @@ const DB_INIT_TABLE = {
);`
);
+ dbs.file.run(
+ `CREATE TABLE IF NOT EXISTS file_web_serve_batch (
+ hash_id VARCHAR NOT NULL,
+ file_id INTEGER NOT NULL,
+
+ UNIQUE(hash_id, file_id)
+ );`
+ );
+
return cb(null);
}
};
\ No newline at end of file
diff --git a/mods/erc_client.js b/core/erc_client.js
similarity index 97%
rename from mods/erc_client.js
rename to core/erc_client.js
index 02b42ad5..4fb549f6 100644
--- a/mods/erc_client.js
+++ b/core/erc_client.js
@@ -1,8 +1,8 @@
/* jslint node: true */
'use strict';
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
diff --git a/mods/file_area_filter_edit.js b/core/file_area_filter_edit.js
similarity index 96%
rename from mods/file_area_filter_edit.js
rename to core/file_area_filter_edit.js
index cb3322f9..4a53096c 100644
--- a/mods/file_area_filter_edit.js
+++ b/core/file_area_filter_edit.js
@@ -2,11 +2,11 @@
'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');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
+const FileBaseFilters = require('./file_base_filter.js');
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
diff --git a/mods/file_area_list.js b/core/file_area_list.js
similarity index 90%
rename from mods/file_area_list.js
rename to core/file_area_list.js
index 076d2a98..3bcfd7c2 100644
--- a/mods/file_area_list.js
+++ b/core/file_area_list.js
@@ -2,22 +2,23 @@
'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 FileArea = require('../core/file_base_area.js');
-const Errors = require('../core/enig_error.js').Errors;
-const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled;
-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 resolveMimeType = require('../core/mime_util.js').resolveMimeType;
-const isAnsi = require('../core/string_util.js').isAnsi;
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const ansi = require('./ansi_term.js');
+const theme = require('./theme.js');
+const FileEntry = require('./file_entry.js');
+const stringFormat = require('./string_format.js');
+const FileArea = require('./file_base_area.js');
+const Errors = require('./enig_error.js').Errors;
+const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
+const ArchiveUtil = require('./archive_util.js');
+const Config = require('./config.js').config;
+const DownloadQueue = require('./download_queue.js');
+const FileAreaWeb = require('./file_area_web.js');
+const FileBaseFilters = require('./file_base_filter.js');
+const resolveMimeType = require('./mime_util.js').resolveMimeType;
+const isAnsi = require('./string_util.js').isAnsi;
+const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi;
// deps
const async = require('async');
@@ -274,7 +275,7 @@ exports.getModule = class FileAreaList extends MenuModule {
} else {
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
- entryInfo.webDlLink = serveItem.url;
+ entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
}
@@ -385,9 +386,19 @@ exports.getModule = class FileAreaList extends MenuModule {
if(_.isString(self.currentFileEntry.desc)) {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
if(descView) {
- if(isAnsi(self.currentFileEntry.desc)) {
+ //
+ // For descriptions we want to support as many color code systems
+ // as we can for coverage of what is found in the while (e.g. Renegade
+ // pipes, PCB @X##, etc.)
+ //
+ // MLTEV doesn't support all of this, so convert. If we produced ANSI
+ // esc sequences, we'll proceed with specialization, else just treat
+ // it as text.
+ //
+ const desc = controlCodesToAnsi(self.currentFileEntry.desc);
+ if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) {
descView.setAnsi(
- self.currentFileEntry.desc,
+ desc,
{
prepped : false,
forceLineTerm : true
@@ -497,7 +508,7 @@ exports.getModule = class FileAreaList extends MenuModule {
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
- self.currentFileEntry.entryInfo.webDlLink = url;
+ self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
return callback(null);
@@ -675,7 +686,13 @@ exports.getModule = class FileAreaList extends MenuModule {
loadFileIds(force, cb) {
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) {
this.fileListPosition = 0;
- FileEntry.findFiles(this.filterCriteria, (err, fileIds) => {
+
+ const filterCriteria = Object.assign({}, this.filterCriteria);
+ if(!filterCriteria.areaTag) {
+ filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client);
+ }
+
+ FileEntry.findFiles(filterCriteria, (err, fileIds) => {
this.fileList = fileIds;
return cb(err);
});
diff --git a/core/file_area_web.js b/core/file_area_web.js
index bac85de7..b12f4f7d 100644
--- a/core/file_area_web.js
+++ b/core/file_area_web.js
@@ -22,14 +22,7 @@ const paths = require('path');
const async = require('async');
const fs = require('graceful-fs');
const mimeTypes = require('mime-types');
-const _ = require('lodash');
-
- /*
- :TODO:
- * Load temp download URLs @ startup & set expire timers via scheduler.
- * At creation, set expire timer via scheduler
- *
- */
+const yazl = require('yazl');
function notEnabledError() {
return Errors.General('Web server is not enabled', ErrNotEnabled);
@@ -59,7 +52,7 @@ class FileAreaWebAccess {
const routeAdded = self.webServer.instance.addRoute({
method : 'GET',
path : Config.fileBase.web.routePath,
- handler : self.routeWebRequestForFile.bind(self),
+ handler : self.routeWebRequest.bind(self),
});
return callback(routeAdded ? null : Errors.General('Failed adding route'));
} else {
@@ -81,6 +74,13 @@ class FileAreaWebAccess {
return this.webServer.instance.isEnabled();
}
+ static getHashIdTypes() {
+ return {
+ SingleFile : 0,
+ BatchArchive : 1,
+ };
+ }
+
load(cb) {
//
// Load entries, register expiration timers
@@ -141,68 +141,53 @@ class FileAreaWebAccess {
WHERE hash_id = ?`,
[ hashId ],
(err, result) => {
- if(err) {
- return cb(err);
+ if(err || !result) {
+ return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID'));
}
const decoded = this.hashids.decode(hashId);
- if(!result || 2 !== decoded.length) {
+
+ // decode() should provide an array of [ userId, hashIdType, id, ... ]
+ if(!Array.isArray(decoded) || decoded.length < 3) {
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),
- }
- );
+ const servedItem = {
+ hashId : hashId,
+ userId : decoded[0],
+ hashIdType : decoded[1],
+ expireTimestamp : moment(result.expire_timestamp),
+ };
+
+ if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) {
+ servedItem.fileIds = decoded.slice(2);
+ }
+
+ return cb(null, servedItem);
}
);
}
- getHashId(client, fileEntry) {
- //
- // Hashid is a unique combination of userId & fileId
- //
- return this.hashids.encode(client.user.userId, fileEntry.fileId);
+ getSingleFileHashId(client, fileEntry) {
+ return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] );
}
- buildTempDownloadLink(client, fileEntry, hashId) {
- hashId = hashId || this.getHashId(client, fileEntry);
+ getBatchArchiveHashId(client, batchId) {
+ return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId);
+ }
+
+ getHashId(client, hashIdType, identifier) {
+ return this.hashids.encode(client.user.userId, hashIdType, identifier);
+ }
+
+ buildSingleFileTempDownloadLink(client, fileEntry, hashId) {
+ hashId = hashId || this.getSingleFileHashId(client, fileEntry);
return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`);
- /*
-
- //
- // 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(_.isString(Config.contentServers.web.overrideUrlPrefix)) {
- return `${Config.contentServers.web.overrideUrlPrefix}${Config.fileBase.web.path}${hashId}`;
- } else {
- 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}`;
- }
- */
+ }
+
+ buildBatchArchiveTempDownloadLink(client, hashId) {
+ return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`);
}
getExistingTempDownloadServeItem(client, fileEntry, cb) {
@@ -210,95 +195,266 @@ class FileAreaWebAccess {
return cb(notEnabledError());
}
- const hashId = this.getHashId(client, fileEntry);
+ const hashId = this.getSingleFileHashId(client, fileEntry);
this.loadServedHashId(hashId, (err, servedItem) => {
if(err) {
return cb(err);
}
- servedItem.url = this.buildTempDownloadLink(client, fileEntry);
+ servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry);
return cb(null, servedItem);
});
}
+ _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) {
+ // add/update rec with hash id and (latest) timestamp
+ dbOrTrans.run(
+ `REPLACE INTO file_web_serve (hash_id, expire_timestamp)
+ VALUES (?, ?);`,
+ [ hashId, getISOTimestampString(expireTime) ],
+ err => {
+ if(err) {
+ return cb(err);
+ }
+
+ this.scheduleExpire(hashId, expireTime);
+
+ return cb(null);
+ }
+ );
+ }
+
createAndServeTempDownload(client, fileEntry, options, cb) {
if(!this.isEnabled()) {
return cb(notEnabledError());
}
- const hashId = this.getHashId(client, fileEntry);
- const url = this.buildTempDownloadLink(client, fileEntry, hashId);
+ const hashId = this.getSingleFileHashId(client, fileEntry);
+ const url = this.buildSingleFileTempDownloadLink(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 => {
+ this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
+ return cb(err, url);
+ });
+ }
+
+ createAndServeTempBatchDownload(client, fileEntries, options, cb) {
+ if(!this.isEnabled()) {
+ return cb(notEnabledError());
+ }
+
+ const batchId = moment().utc().unix();
+ const hashId = this.getBatchArchiveHashId(client, batchId);
+ const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
+ options.expireTime = options.expireTime || moment().add(2, 'days');
+
+ FileDb.beginTransaction( (err, trans) => {
+ if(err) {
+ return cb(err);
+ }
+
+ this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
if(err) {
- return cb(err);
+ return trans.rollback( () => {
+ return cb(err);
+ });
}
- this.scheduleExpire(hashId, options.expireTime);
-
- return cb(null, url);
- }
- );
+ async.eachSeries(fileEntries, (entry, nextEntry) => {
+ trans.run(
+ `INSERT INTO file_web_serve_batch (hash_id, file_id)
+ VALUES (?, ?);`,
+ [ hashId, entry.fileId ],
+ err => {
+ return nextEntry(err);
+ }
+ );
+ }, err => {
+ trans[err ? 'rollback' : 'commit']( () => {
+ return cb(err, url);
+ });
+ });
+ });
+ });
}
fileNotFound(resp) {
return this.webServer.instance.fileNotFound(resp);
}
- routeWebRequestForFile(req, resp) {
+ routeWebRequest(req, resp) {
const hashId = paths.basename(req.url);
+ Log.debug( { hashId : hashId, url : req.url }, 'File area web request');
+
this.loadServedHashId(hashId, (err, servedItem) => {
if(err) {
return this.fileNotFound(resp);
}
- const fileEntry = new FileEntry();
- fileEntry.load(servedItem.fileId, err => {
+ const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
+ switch(servedItem.hashIdType) {
+ case hashIdTypes.SingleFile :
+ return this.routeWebRequestForSingleFile(servedItem, req, resp);
+
+ case hashIdTypes.BatchArchive :
+ return this.routeWebRequestForBatchArchive(servedItem, req, resp);
+
+ default :
+ return this.fileNotFound(resp);
+ }
+ });
+ }
+
+ routeWebRequestForSingleFile(servedItem, req, resp) {
+ Log.debug( { servedItem : servedItem }, 'Single file web request');
+
+ const fileEntry = new FileEntry();
+
+ servedItem.fileId = servedItem.fileIds[0];
+
+ 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);
}
- const filePath = fileEntry.filePath;
- if(!filePath) {
+ resp.on('close', () => {
+ // connection closed *before* the response was fully sent
+ // :TODO: Log and such
+ });
+
+ resp.on('finish', () => {
+ // transfer completed fully
+ this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size);
+ });
+
+ 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);
+ });
+ });
+ }
+
+ routeWebRequestForBatchArchive(servedItem, req, resp) {
+ Log.debug( { servedItem : servedItem }, 'Batch file web request');
+
+ //
+ // We are going to build an on-the-fly zip file stream of 1:n
+ // files in the batch.
+ //
+ // First, collect all file IDs
+ //
+ const self = this;
+
+ async.waterfall(
+ [
+ function fetchFileIds(callback) {
+ FileDb.all(
+ `SELECT file_id
+ FROM file_web_serve_batch
+ WHERE hash_id = ?;`,
+ [ servedItem.hashId ],
+ (err, fileIdRows) => {
+ if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) {
+ return callback(Errors.DoesNotExist('Could not get file IDs for batch'));
+ }
+
+ return callback(null, fileIdRows.map(r => r.file_id));
+ }
+ );
+ },
+ function loadFileEntries(fileIds, callback) {
+ const filePaths = [];
+ async.eachSeries(fileIds, (fileId, nextFileId) => {
+ const fileEntry = new FileEntry();
+ fileEntry.load(fileId, err => {
+ if(!err) {
+ filePaths.push(fileEntry.filePath);
+ }
+ return nextFileId(err);
+ });
+ }, err => {
+ if(err) {
+ return callback(Errors.DoesNotExist('Coudl not load file IDs for batch'));
+ }
+
+ return callback(null, filePaths);
+ });
+ },
+ function createAndServeStream(filePaths, callback) {
+ Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request');
+
+ const zipFile = new yazl.ZipFile();
+
+ zipFile.on('error', err => {
+ Log.warn( { error : err.message }, 'Error adding file to batch web request archive');
+ });
+
+ filePaths.forEach(fp => {
+ zipFile.addFile(
+ fp, // path to physical file
+ paths.basename(fp), // filename/path *stored in archive*
+ {
+ compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us.
+ }
+ );
+ });
+
+ zipFile.end( finalZipSize => {
+ if(-1 === finalZipSize) {
+ return callback(Errors.UnexpectedState('Unable to acquire final zip size'));
+ }
+
+ resp.on('close', () => {
+ // connection closed *before* the response was fully sent
+ // :TODO: Log and such
+ });
+
+ resp.on('finish', () => {
+ // transfer completed fully
+ self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize);
+ });
+
+ const batchFileName = `batch_${servedItem.hashId}.zip`;
+
+ const headers = {
+ 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'),
+ 'Content-Length' : finalZipSize,
+ 'Content-Disposition' : `attachment; filename="${batchFileName}"`,
+ };
+
+ resp.writeHead(200, headers);
+ return zipFile.outputStream.pipe(resp);
+ });
+ }
+ ],
+ err => {
+ if(err) {
+ // :TODO: Log me!
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
- this.updateDownloadStatsForUserIdAndSystemAndSystem(servedItem.userId, stats.size);
- });
-
- 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);
- });
- });
- });
+ // ...otherwise, we would have called resp() already.
+ }
+ );
}
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, cb) {
diff --git a/core/file_base_area.js b/core/file_base_area.js
index 79b91ce8..f3845cc1 100644
--- a/core/file_base_area.js
+++ b/core/file_base_area.js
@@ -13,6 +13,7 @@ const Log = require('./logger.js').log;
const resolveMimeType = require('./mime_util.js').resolveMimeType;
const stringFormat = require('./string_format.js');
const wordWrapText = require('./word_wrap.js').wordWrapText;
+const StatLog = require('./stat_log.js');
// deps
const _ = require('lodash');
@@ -27,6 +28,7 @@ const moment = require('moment');
exports.isInternalArea = isInternalArea;
exports.getAvailableFileAreas = getAvailableFileAreas;
+exports.getAvailableFileAreaTags = getAvailableFileAreaTags;
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
exports.isValidStorageTag = isValidStorageTag;
exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag;
@@ -39,14 +41,19 @@ exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
exports.scanFile = scanFile;
exports.scanFileAreaForChanges = scanFileAreaForChanges;
exports.getDescFromFileName = getDescFromFileName;
+exports.getAreaStats = getAreaStats;
+
+// for scheduler:
+exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent;
const WellKnownAreaTags = exports.WellKnownAreaTags = {
Invalid : '',
MessageAreaAttach : 'system_message_attachment',
+ TempDownloads : 'system_temporary_download',
};
function isInternalArea(areaTag) {
- return areaTag === WellKnownAreaTags.MessageAreaAttach;
+ return [ WellKnownAreaTags.MessageAreaAttach, WellKnownAreaTags.TempDownloads ].includes(areaTag);
}
function getAvailableFileAreas(client, options) {
@@ -60,6 +67,10 @@ function getAvailableFileAreas(client, options) {
return true;
}
+ if(options.skipAcsCheck) {
+ return false; // no ACS checks (below)
+ }
+
if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) {
return true; // omit
}
@@ -68,6 +79,10 @@ function getAvailableFileAreas(client, options) {
});
}
+function getAvailableFileAreaTags(client, options) {
+ return _.map(getAvailableFileAreas(client, options), area => area.areaTag);
+}
+
function getSortedAvailableFileAreas(client, options) {
const areas = _.map(getAvailableFileAreas(client, options), v => v);
sortAreasOrConfs(areas);
@@ -341,7 +356,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
// 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');
+ fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437');
+ fileEntry[`${descType}Src`] = 'descFile';
return next(null);
});
});
@@ -389,7 +405,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries
function processSingleExtractedFile(extractedFile, callback) {
populateFileEntryInfoFromFile(fileEntry, extractedFile, err => {
if(!fileEntry.desc) {
- fileEntry.desc = getDescFromFileName(filePath);
+ fileEntry.desc = getDescFromFileName(filePath);
+ fileEntry.descSrc = 'fileName';
}
return callback(err);
});
@@ -514,7 +531,8 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) {
stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n');
}
- fileEntry[key] = stdout;
+ fileEntry[key] = stdout;
+ fileEntry[`${key}Src`] = 'infoTool';
}
}
@@ -536,7 +554,8 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb
function getDescriptions(callback) {
populateFileEntryInfoFromFile(fileEntry, filePath, err => {
if(!fileEntry.desc) {
- fileEntry.desc = getDescFromFileName(filePath);
+ fileEntry.desc = getDescFromFileName(filePath);
+ fileEntry.descSrc = 'fileName';
}
return callback(err);
});
@@ -856,4 +875,64 @@ function getDescFromFileName(fileName) {
const name = paths.basename(fileName, ext);
return _.upperFirst(name.replace(/[\-_.+]/g, ' ').replace(/\s+/g, ' '));
+}
+
+//
+// Return an object of stats about an area(s)
+//
+// {
+//
+// totalFiles : ,
+// totalBytes : ,
+// areas : {
+// : {
+// files : ,
+// bytes :
+// }
+// }
+// }
+//
+function getAreaStats(cb) {
+ FileDb.all(
+ `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size
+ FROM file f, file_meta m
+ WHERE f.file_id = m.file_id AND m.meta_name='byte_size'
+ GROUP BY f.area_tag;`,
+ (err, statRows) => {
+ if(err) {
+ return cb(err);
+ }
+
+ if(!statRows || 0 === statRows.length) {
+ return cb(Errors.DoesNotExist('No file areas to acquire stats from'));
+ }
+
+ return cb(
+ null,
+ statRows.reduce( (stats, v) => {
+ stats.totalFiles = (stats.totalFiles || 0) + v.total_files;
+ stats.totalBytes = (stats.totalBytes || 0) + v.total_byte_size;
+
+ stats.areas = stats.areas || {};
+
+ stats.areas[v.area_tag] = {
+ files : v.total_files,
+ bytes : v.total_byte_size,
+ };
+ return stats;
+ }, {})
+ );
+ }
+ );
+}
+
+// method exposed for event scheduler
+function updateAreaStatsScheduledEvent(args, cb) {
+ getAreaStats( (err, stats) => {
+ if(!err) {
+ StatLog.setNonPeristentSystemStat('file_base_area_stats', stats);
+ }
+
+ return cb(err);
+ });
}
\ No newline at end of file
diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js
new file mode 100644
index 00000000..5ec266fd
--- /dev/null
+++ b/core/file_base_area_select.js
@@ -0,0 +1,104 @@
+/* jslint node: true */
+'use strict';
+
+// enigma-bbs
+const MenuModule = require('./menu_module.js').MenuModule;
+const stringFormat = require('./string_format.js');
+const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
+const StatLog = require('./stat_log.js');
+
+// deps
+const async = require('async');
+
+exports.moduleInfo = {
+ name : 'File Area Selector',
+ desc : 'Select from available file areas',
+ author : 'NuSkooler',
+};
+
+const MciViewIds = {
+ areaList : 1,
+};
+
+exports.getModule = class FileAreaSelectModule extends MenuModule {
+ constructor(options) {
+ super(options);
+
+ this.config = this.menuConfig.config || {};
+
+ this.loadAvailAreas();
+
+ this.menuMethods = {
+ selectArea : (formData, extraArgs, cb) => {
+ const area = this.availAreas[formData.value.areaSelect] || 0;
+
+ const filterCriteria = {
+ areaTag : area.areaTag,
+ };
+
+ const menuOpts = {
+ extraArgs : {
+ filterCriteria : filterCriteria,
+ },
+ menuFlags : [ 'popParent' ],
+ };
+
+ return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
+ }
+ };
+ }
+
+ loadAvailAreas() {
+ this.availAreas = getSortedAvailableFileAreas(this.client);
+ }
+
+ mciReady(mciData, cb) {
+ super.mciReady(mciData, err => {
+ if(err) {
+ return cb(err);
+ }
+
+ const self = this;
+
+ async.series(
+ [
+ function mergeAreaStats(callback) {
+ const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} };
+
+ self.availAreas.forEach(area => {
+ const stats = areaStats.areas[area.areaTag];
+ area.totalFiles = stats ? stats.files : 0;
+ area.totalBytes = stats ? stats.bytes : 0;
+ });
+
+ return callback(null);
+ },
+ function prepView(callback) {
+ self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => {
+ if(err) {
+ return callback(err);
+ }
+
+ const areaListView = vc.getView(MciViewIds.areaList);
+
+ const areaListFormat = self.config.areaListFormat || '{name}';
+
+ areaListView.setItems(self.availAreas.map(a => stringFormat(areaListFormat, a) ) );
+
+ if(self.config.areaListFocusFormat) {
+ areaListView.setFocusItems(self.availAreas.map(a => stringFormat(self.config.areaListFocusFormat, a) ) );
+ }
+
+ areaListView.redraw();
+
+ return callback(null);
+ });
+ }
+ ],
+ err => {
+ return cb(err);
+ }
+ );
+ });
+ }
+};
diff --git a/mods/file_base_download_manager.js b/core/file_base_download_manager.js
similarity index 77%
rename from mods/file_base_download_manager.js
rename to core/file_base_download_manager.js
index 812a2422..7444af56 100644
--- a/mods/file_base_download_manager.js
+++ b/core/file_base_download_manager.js
@@ -2,17 +2,19 @@
'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');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const DownloadQueue = require('./download_queue.js');
+const theme = require('./theme.js');
+const ansi = require('./ansi_term.js');
+const Errors = require('./enig_error.js').Errors;
+const stringFormat = require('./string_format.js');
+const FileAreaWeb = require('./file_area_web.js');
// deps
const async = require('async');
const _ = require('lodash');
+const moment = require('moment');
exports.moduleInfo = {
name : 'File Base Download Queue Manager',
@@ -22,17 +24,15 @@ exports.moduleInfo = {
const FormIds = {
queueManager : 0,
- details : 1,
};
const MciViewIds = {
queueManager : {
- queue : 1,
- navMenu : 2,
- },
- details : {
+ queue : 1,
+ navMenu : 2,
- }
+ customRangeStart : 10,
+ },
};
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
@@ -126,6 +126,26 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
return cb(null);
}
+ displayWebDownloadLinkForFileEntry(fileEntry) {
+ FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => {
+ if(serveItem && serveItem.url) {
+ const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
+
+ fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
+ fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
+ } else {
+ fileEntry.webDlLink = '';
+ fileEntry.webDlExpire = '';
+ }
+
+ this.updateCustomViewTextsWithFilter(
+ 'queueManager',
+ MciViewIds.queueManager.customRangeStart, fileEntry,
+ { filter : [ '{webDlLink}', '{webDlExpire}' ] }
+ );
+ });
+ }
+
updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
if(!queueView) {
@@ -138,7 +158,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
+ queueView.on('index update', idx => {
+ const fileEntry = this.dlQueue.items[idx];
+ this.displayWebDownloadLinkForFileEntry(fileEntry);
+ });
+
queueView.redraw();
+ this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]);
return cb(null);
}
diff --git a/core/file_base_filter.js b/core/file_base_filter.js
index fadd41fd..320d36d3 100644
--- a/core/file_base_filter.js
+++ b/core/file_base_filter.js
@@ -24,6 +24,7 @@ module.exports = class FileBaseFilters {
'user_rating',
'est_release_year',
'byte_size',
+ 'file_name',
];
}
@@ -135,9 +136,14 @@ module.exports = class FileBaseFilters {
return parseInt((user.properties.user_file_base_last_viewed || 0));
}
- static setFileBaseLastViewedFileIdForUser(user, fileId, cb) {
+ static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
+ if(!cb && _.isFunction(allowOlder)) {
+ cb = allowOlder;
+ allowOlder = false;
+ }
+
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
- if(fileId < current) {
+ if(!allowOlder && fileId < current) {
if(cb) {
cb(null);
}
diff --git a/mods/file_base_search.js b/core/file_base_search.js
similarity index 88%
rename from mods/file_base_search.js
rename to core/file_base_search.js
index e984e1a4..27656123 100644
--- a/mods/file_base_search.js
+++ b/core/file_base_search.js
@@ -2,10 +2,10 @@
'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 MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
+const FileBaseFilters = require('./file_base_filter.js');
// deps
const async = require('async');
@@ -56,7 +56,7 @@ exports.getModule = class FileBaseSearch extends MenuModule {
},
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();
@@ -112,7 +112,7 @@ exports.getModule = class FileBaseSearch extends MenuModule {
extraArgs : {
filterCriteria : filterCriteria,
},
- menuFlags : [ 'noHistory' ],
+ menuFlags : [ 'popParent' ],
};
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js
new file mode 100644
index 00000000..dea7c5a8
--- /dev/null
+++ b/core/file_base_web_download_manager.js
@@ -0,0 +1,287 @@
+/* jslint node: true */
+'use strict';
+
+// ENiGMA½
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const DownloadQueue = require('./download_queue.js');
+const theme = require('./theme.js');
+const ansi = require('./ansi_term.js');
+const Errors = require('./enig_error.js').Errors;
+const stringFormat = require('./string_format.js');
+const FileAreaWeb = require('./file_area_web.js');
+const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
+const Config = require('./config.js').config;
+
+// deps
+const async = require('async');
+const _ = require('lodash');
+const moment = require('moment');
+
+exports.moduleInfo = {
+ name : 'File Base Download Web Queue Manager',
+ desc : 'Module for interacting with web backed download queue/batch',
+ author : 'NuSkooler',
+};
+
+const FormIds = {
+ queueManager : 0
+};
+
+const MciViewIds = {
+ queueManager : {
+ queue : 1,
+ navMenu : 2,
+
+ customRangeStart : 10,
+ }
+};
+
+exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
+
+ constructor(options) {
+ super(options);
+
+ this.dlQueue = new DownloadQueue(this.client);
+
+ this.menuMethods = {
+ 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);
+ },
+ getBatchLink : (formData, extraArgs, cb) => {
+ return this.generateAndDisplayBatchLink(cb);
+ }
+ };
+ }
+
+ initSequence() {
+ if(0 === this.dlQueue.items.length) {
+ 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);
+ }
+
+ displayFileInfoForFileEntry(fileEntry) {
+ this.updateCustomViewTextsWithFilter(
+ 'queueManager',
+ MciViewIds.queueManager.customRangeStart, fileEntry,
+ { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others....
+ );
+ }
+
+ 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 || '{webDlLink}';
+ 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.on('index update', idx => {
+ const fileEntry = this.dlQueue.items[idx];
+ this.displayFileInfoForFileEntry(fileEntry);
+ });
+
+ queueView.redraw();
+ this.displayFileInfoForFileEntry(this.dlQueue.items[0]);
+
+ return cb(null);
+ }
+
+ generateAndDisplayBatchLink(cb) {
+ const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
+
+ FileAreaWeb.createAndServeTempBatchDownload(
+ this.client,
+ this.dlQueue.items,
+ {
+ expireTime : expireTime
+ },
+ (err, webBatchDlLink) => {
+ // :TODO: handle not enabled -> display such
+ if(err) {
+ return cb(err);
+ }
+
+ const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
+
+ const formatObj = {
+ webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
+ webBatchDlExpire : expireTime.format(webDlExpireTimeFormat),
+ };
+
+ this.updateCustomViewTextsWithFilter(
+ 'queueManager',
+ MciViewIds.queueManager.customRangeStart,
+ formatObj,
+ { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) }
+ );
+
+ return cb(null);
+ }
+ );
+ }
+
+ displayQueueManagerPage(clearScreen, cb) {
+ const self = this;
+
+ async.series(
+ [
+ function prepArtAndViewController(callback) {
+ return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
+ },
+ function prepareQueueDownloadLinks(callback) {
+ const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
+
+ async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
+ FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
+ if(err) {
+ if(ErrNotEnabled === err.reasonCode) {
+ return nextFileEntry(err); // we should have caught this prior
+ }
+
+ const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
+
+ FileAreaWeb.createAndServeTempDownload(
+ self.client,
+ fileEntry,
+ { expireTime : expireTime },
+ (err, url) => {
+ if(err) {
+ return nextFileEntry(err);
+ }
+
+ fileEntry.webDlLinkRaw = url;
+ fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url;
+ fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat);
+
+ return nextFileEntry(null);
+ }
+ );
+ } else {
+ fileEntry.webDlLinkRaw = serveItem.url;
+ fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url;
+ fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
+ return nextFileEntry(null);
+ }
+ });
+ }, err => {
+ return callback(err);
+ });
+ },
+ 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);
+ }
+ );
+ }
+};
+
\ No newline at end of file
diff --git a/core/file_entry.js b/core/file_entry.js
index b88d41a0..8bf7a69d 100644
--- a/core/file_entry.js
+++ b/core/file_entry.js
@@ -11,10 +11,13 @@ const async = require('async');
const _ = require('lodash');
const paths = require('path');
const fse = require('fs-extra');
+const { unlink, readFile } = require('graceful-fs');
+const crypto = require('crypto');
+const moment = require('moment');
const FILE_TABLE_MEMBERS = [
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag',
- 'desc', 'desc_long', 'upload_timestamp'
+ 'desc', 'desc_long', 'upload_timestamp'
];
const FILE_WELL_KNOWN_META = {
@@ -110,9 +113,8 @@ module.exports = class FileEntry {
}
const self = this;
- let inTransaction = false;
- async.series(
+ async.waterfall(
[
function check(callback) {
if(isUpdate && !self.fileId) {
@@ -120,23 +122,41 @@ module.exports = class FileEntry {
}
return callback(null);
},
- function startTrans(callback) {
- return fileDb.run('BEGIN;', callback);
- },
- function storeEntry(callback) {
- inTransaction = true;
+ function calcSha256IfNeeded(callback) {
+ if(self.fileSha256) {
+ return callback(null);
+ }
if(isUpdate) {
- fileDb.run(
+ return callback(Errors.MissingParam('fileSha256 property must be set for updates!'));
+ }
+
+ readFile(self.filePath, (err, data) => {
+ if(err) {
+ return callback(err);
+ }
+
+ const sha256 = crypto.createHash('sha256');
+ sha256.update(data);
+ self.fileSha256 = sha256.digest('hex');
+ return callback(null);
+ });
+ },
+ function startTrans(callback) {
+ return fileDb.beginTransaction(callback);
+ },
+ function storeEntry(trans, callback) {
+ if(isUpdate) {
+ trans.run(
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
err => {
- return callback(err);
+ return callback(err, trans);
}
);
} else {
- fileDb.run(
+ trans.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() ],
@@ -144,35 +164,35 @@ module.exports = class FileEntry {
if(!err) {
self.fileId = this.lastID;
}
- return callback(err);
+ return callback(err, trans);
}
);
}
},
- function storeMeta(callback) {
+ function storeMeta(trans, callback) {
async.each(Object.keys(self.meta), (n, next) => {
const v = self.meta[n];
- return FileEntry.persistMetaValue(self.fileId, n, v, next);
+ return FileEntry.persistMetaValue(self.fileId, n, v, trans, next);
},
err => {
- return callback(err);
+ return callback(err, trans);
});
},
- function storeHashTags(callback) {
+ function storeHashTags(trans, callback) {
const hashTagsArray = Array.from(self.hashTags);
async.each(hashTagsArray, (hashTag, next) => {
- return FileEntry.persistHashTag(self.fileId, hashTag, next);
+ return FileEntry.persistHashTag(self.fileId, hashTag, trans, next);
},
err => {
- return callback(err);
+ return callback(err, trans);
});
}
],
- err => {
+ (err, trans) => {
// :TODO: Log orig err
- if(inTransaction) {
- fileDb.run(err ? 'ROLLBACK;' : 'COMMIT;', err => {
- return cb(err);
+ if(trans) {
+ trans[err ? 'rollback' : 'commit'](transErr => {
+ return cb(transErr ? transErr : err);
});
} else {
return cb(err);
@@ -207,8 +227,13 @@ module.exports = class FileEntry {
);
}
- static persistMetaValue(fileId, name, value, cb) {
- return fileDb.run(
+ static persistMetaValue(fileId, name, value, transOrDb, cb) {
+ if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
+ cb = transOrDb;
+ transOrDb = fileDb;
+ }
+
+ return transOrDb.run(
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
VALUES (?, ?, ?);`,
[ fileId, name, value ],
@@ -249,15 +274,20 @@ module.exports = class FileEntry {
);
}
- static persistHashTag(fileId, hashTag, cb) {
- fileDb.serialize( () => {
- fileDb.run(
+ static persistHashTag(fileId, hashTag, transOrDb, cb) {
+ if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
+ cb = transOrDb;
+ transOrDb = fileDb;
+ }
+
+ transOrDb.serialize( () => {
+ transOrDb.run(
`INSERT OR IGNORE INTO hash_tag (hash_tag)
VALUES (?);`,
[ hashTag ]
);
- fileDb.run(
+ transOrDb.run(
`REPLACE INTO file_hash_tag (hash_tag_id, file_id)
VALUES (
(SELECT hash_tag_id
@@ -395,7 +425,11 @@ module.exports = class FileEntry {
let sqlWhere = '';
let sqlOrderBy;
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
-
+
+ if(moment.isMoment(filter.newerThanTimestamp)) {
+ filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp);
+ }
+
function getOrderByWithCast(ob) {
if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) {
return `ORDER BY CAST(${ob} AS INTEGER)`;
@@ -415,7 +449,7 @@ module.exports = class FileEntry {
if(filter.sort && filter.sort.length > 0) {
if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value?
- sql =
+ sql =
`SELECT DISTINCT f.file_id
FROM file f, file_meta m`;
@@ -432,7 +466,7 @@ module.exports = class FileEntry {
WHERE file_id = f.file_id)
AS avg_rating
FROM file f`;
-
+
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
} else {
sql =
@@ -443,7 +477,7 @@ module.exports = class FileEntry {
}
}
} else {
- sql =
+ sql =
`SELECT DISTINCT f.file_id
FROM file f`;
@@ -451,7 +485,12 @@ module.exports = class FileEntry {
}
if(filter.areaTag && filter.areaTag.length > 0) {
- appendWhereClause(`f.area_tag = "${filter.areaTag}"`);
+ if(Array.isArray(filter.areaTag)) {
+ const areaList = filter.areaTag.map(t => `"${t}"`).join(', ');
+ appendWhereClause(`f.area_tag IN(${areaList})`);
+ } else {
+ appendWhereClause(`f.area_tag = "${filter.areaTag}"`);
+ }
}
if(filter.metaPairs && filter.metaPairs.length > 0) {
@@ -494,8 +533,8 @@ module.exports = class FileEntry {
}
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(',');
+ // build list of quoted tags; filter.tags comes in as a space and/or comma separated values
+ const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${tag}"` ).join(',');
appendWhereClause(
`f.file_id IN (
@@ -518,7 +557,13 @@ module.exports = class FileEntry {
appendWhereClause(`f.file_id > ${filter.newerThanFileId}`);
}
- sql += `${sqlWhere} ${sqlOrderBy};`;
+ sql += `${sqlWhere} ${sqlOrderBy}`;
+
+ if(_.isNumber(filter.limit)) {
+ sql += ` LIMIT ${filter.limit}`;
+ }
+
+ sql += ';';
const matchingFileIds = [];
fileDb.each(sql, (err, fileId) => {
@@ -530,6 +575,40 @@ module.exports = class FileEntry {
});
}
+ static removeEntry(srcFileEntry, options, cb) {
+ if(!_.isFunction(cb) && _.isFunction(options)) {
+ cb = options;
+ options = {};
+ }
+
+ async.series(
+ [
+ function removeFromDatabase(callback) {
+ fileDb.run(
+ `DELETE FROM file
+ WHERE file_id = ?;`,
+ [ srcFileEntry.fileId ],
+ err => {
+ return callback(err);
+ }
+ );
+ },
+ function optionallyRemovePhysicalFile(callback) {
+ if(true !== options.removePhysFile) {
+ return callback(null);
+ }
+
+ unlink(srcFileEntry.filePath, err => {
+ return callback(err);
+ });
+ }
+ ],
+ err => {
+ return cb(err);
+ }
+ );
+ }
+
static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) {
if(!cb && _.isFunction(destFileName)) {
cb = destFileName;
@@ -539,7 +618,6 @@ module.exports = class FileEntry {
const srcPath = srcFileEntry.filePath;
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
-
if(!dstDir) {
return cb(Errors.Invalid('Invalid storage tag'));
}
diff --git a/mods/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js
similarity index 93%
rename from mods/file_transfer_protocol_select.js
rename to core/file_transfer_protocol_select.js
index 6efa5a93..f1b3dbed 100644
--- a/mods/file_transfer_protocol_select.js
+++ b/core/file_transfer_protocol_select.js
@@ -2,10 +2,10 @@
'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;
+const MenuModule = require('./menu_module.js').MenuModule;
+const Config = require('./config.js').config;
+const stringFormat = require('./string_format.js');
+const ViewController = require('./view_controller.js').ViewController;
// deps
const async = require('async');
diff --git a/core/fse.js b/core/fse.js
index ed9d3b13..b266a9c9 100644
--- a/core/fse.js
+++ b/core/fse.js
@@ -15,6 +15,7 @@ const stringFormat = require('./string_format.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const { isAnsi, cleanControlCodes, insert } = require('./string_util.js');
const Config = require('./config.js').config;
+const { getAddressedToInfo } = require('./mail_util.js');
// deps
const async = require('async');
@@ -264,12 +265,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
isEditMode() {
return 'edit' === this.editorMode;
}
-
+
isViewMode() {
return 'view' === this.editorMode;
}
- isLocalEmail() {
+ isPrivateMail() {
return Message.WellKnownAreaTags.Private === this.messageAreaTag;
}
@@ -342,8 +343,6 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
// really don't like ANSI messages in UTF-8 encoding (they should!)
//
msgOpts.meta = { System : { 'explicit_encoding' : Config.scannerTossers.ftn_bso.packetAnsiMsgEncoding || 'cp437' } };
- // :TODO: change to \r\nESC[A
- //msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}${msgOpts.message}`;
msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`;
}
}
@@ -411,30 +410,54 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
return callback(null);
},
function populateLocalUserInfo(callback) {
- if(self.isLocalEmail()) {
- self.message.setLocalFromUserId(self.client.user.userId);
-
- if(self.toUserId > 0) {
- self.message.setLocalToUserId(self.toUserId);
- callback(null);
- } else {
- // we need to look it up
- User.getUserIdAndName(self.message.toUserName, function userInfo(err, toUserId) {
- if(err) {
- callback(err);
- } else {
- self.message.setLocalToUserId(toUserId);
- callback(null);
- }
- });
- }
- } else {
- callback(null);
+ if(!self.isPrivateMail()) {
+ return callback(null);
}
+
+ // :TODO: shouldn't local from user ID be set for all mail?
+ self.message.setLocalFromUserId(self.client.user.userId);
+
+ if(self.toUserId > 0) {
+ self.message.setLocalToUserId(self.toUserId);
+ return callback(null);
+ }
+
+ //
+ // If the message we're replying to is from a remote user
+ // don't try to look up the local user ID. Instead, mark the mail
+ // for export with the remote to address.
+ //
+ if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) {
+ self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]);
+ self.message.setExternalFlavor(self.replyToMessage.meta.System[Message.SystemMetaNames.ExternalFlavor]);
+ return callback(null);
+ }
+
+ //
+ // Detect if the user is attempting to send to a remote mail type that we support
+ //
+ // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such
+ const addressedToInfo = getAddressedToInfo(self.message.toUserName);
+ if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) {
+ self.message.setRemoteToUser(addressedToInfo.remote);
+ self.message.setExternalFlavor(addressedToInfo.flavor);
+ self.message.toUserName = addressedToInfo.name;
+ return callback(null);
+ }
+
+ // we need to look it up
+ User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => {
+ if(err) {
+ return callback(err);
+ }
+
+ self.message.setLocalToUserId(toUserId);
+ return callback(null);
+ });
}
],
- function complete(err) {
- cb(err, self.message);
+ err => {
+ return cb(err, self.message);
}
);
}
@@ -967,7 +990,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.viewControllers.body.switchFocus(1);
this.observeEditorEvents();
- };
+ }
switchToFooter() {
this.viewControllers.header.setFocus(false);
@@ -995,14 +1018,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
const quoteMsgView = this.viewControllers.quoteBuilder.getView(1);
const msgView = this.viewControllers.body.getView(1);
- let quoteLines = quoteMsgView.getData();
+ let quoteLines = quoteMsgView.getData().trim();
- if(quoteLines.trim().length > 0) {
+ if(quoteLines.length > 0) {
if(this.replyIsAnsi) {
const bodyMessageView = this.viewControllers.body.getView(1);
quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`;
}
- msgView.addText(`${quoteLines}\n`);
+ msgView.addText(`${quoteLines}\n\n`);
}
quoteMsgView.setText('');
diff --git a/core/ftn_address.js b/core/ftn_address.js
index 9edb3819..f0936e1d 100644
--- a/core/ftn_address.js
+++ b/core/ftn_address.js
@@ -20,6 +20,15 @@ module.exports = class Address {
}
}
+ static isValidAddress(addr) {
+ return addr && addr.isValid();
+ }
+
+ isValid() {
+ // FTN address is valid if we have at least a net/node
+ return _.isNumber(this.net) && _.isNumber(this.node);
+ }
+
isEqual(other) {
if(_.isString(other)) {
other = Address.fromString(other);
diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js
index 095628ea..d84b4a69 100644
--- a/core/ftn_mail_packet.js
+++ b/core/ftn_mail_packet.js
@@ -19,15 +19,6 @@ const moment = require('moment');
exports.Packet = Packet;
-/*
- :TODO: things
- * Test SAUCE ignore/extraction
- * FSP-1010 for netmail (see SBBS)
- * Syncronet apparently uses odd origin lines
- * Origin lines starting with "#" instead of "*" ?
-
-*/
-
const FTN_PACKET_HEADER_SIZE = 58; // fixed header size
const FTN_PACKET_HEADER_TYPE = 2;
const FTN_PACKET_MESSAGE_TYPE = 2;
@@ -63,7 +54,7 @@ class PacketHeader {
this.capWord = 0x0001;
this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap
-
+
this.prodCodeHi = 0xfe; // see above
this.prodRevHi = 0;
}
@@ -358,9 +349,9 @@ function Packet(options) {
buffer.writeUInt16LE(packetHeader.origPoint, 50);
buffer.writeUInt16LE(packetHeader.destPoint, 52);
buffer.writeUInt32LE(packetHeader.prodData, 54);
-
+
ws.write(buffer);
-
+
return buffer.length;
};
@@ -393,9 +384,19 @@ function Packet(options) {
};
function addKludgeLine(line) {
- const sepIndex = line.indexOf(':');
- const key = line.substr(0, sepIndex).toUpperCase();
- const value = line.substr(sepIndex + 1).trim();
+ //
+ // We have to special case INTL/TOPT/FMPT as they don't contain
+ // a ':' name/value separator like the rest of the kludge lines... because stupdity.
+ //
+ let key = line.substr(0, 4).trim();
+ let value;
+ if( ['INTL', 'TOPT', 'FMPT', 'Via' ].includes(key)) {
+ value = line.substr(key.length).trim();
+ } else {
+ const sepIndex = line.indexOf(':');
+ key = line.substr(0, sepIndex).toUpperCase();
+ value = line.substr(sepIndex + 1).trim();
+ }
//
// Allow mapped value to be either a key:value if there is only
@@ -636,10 +637,30 @@ function Packet(options) {
});
});
};
-
+
+ this.sanatizeFtnProperties = function(message) {
+ [
+ Message.FtnPropertyNames.FtnOrigNode,
+ Message.FtnPropertyNames.FtnDestNode,
+ Message.FtnPropertyNames.FtnOrigNetwork,
+ Message.FtnPropertyNames.FtnDestNetwork,
+ Message.FtnPropertyNames.FtnAttrFlags,
+ Message.FtnPropertyNames.FtnCost,
+ Message.FtnPropertyNames.FtnOrigZone,
+ Message.FtnPropertyNames.FtnDestZone,
+ Message.FtnPropertyNames.FtnOrigPoint,
+ Message.FtnPropertyNames.FtnDestPoint,
+ Message.FtnPropertyNames.FtnAttribute,
+ ].forEach( propName => {
+ if(message.meta.FtnProperty[propName]) {
+ message.meta.FtnProperty[propName] = parseInt(message.meta.FtnProperty[propName]) || 0;
+ }
+ });
+ };
+
this.getMessageEntryBuffer = function(message, options, cb) {
-
- function getAppendMeta(k, m) {
+
+ function getAppendMeta(k, m, sepChar=':') {
let append = '';
if(m) {
let a = m;
@@ -647,7 +668,7 @@ function Packet(options) {
a = [ a ];
}
a.forEach(v => {
- append += `${k}: ${v}\r`;
+ append += `${k}${sepChar} ${v}\r`;
});
}
return append;
@@ -657,7 +678,10 @@ function Packet(options) {
[
function prepareHeaderAndKludges(callback) {
const basicHeader = new Buffer(34);
-
+
+ // ensure address FtnProperties are numbers
+ self.sanatizeFtnProperties(message);
+
basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4);
@@ -693,10 +717,22 @@ function Packet(options) {
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01)
}
+ // :TODO: DRY with similar function in this file!
Object.keys(message.meta.FtnKludge).forEach(k => {
- // we want PATH to be last
- if('PATH' !== k) {
- msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]);
+ switch(k) {
+ case 'PATH' :
+ break; // skip & save for last
+
+ case 'Via' :
+ case 'FMPT' :
+ case 'TOPT' :
+ case 'INTL' :
+ msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar
+ break;
+
+ default :
+ msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]);
+ break;
}
});
@@ -810,14 +846,14 @@ function Packet(options) {
// :TODO: Put this in it's own method
let msgBody = '';
- function appendMeta(k, m) {
+ function appendMeta(k, m, sepChar=':') {
if(m) {
let a = m;
if(!_.isArray(a)) {
a = [ a ];
}
a.forEach(v => {
- msgBody += `${k}: ${v}\r`;
+ msgBody += `${k}${sepChar} ${v}\r`;
});
}
}
@@ -832,9 +868,15 @@ function Packet(options) {
}
Object.keys(message.meta.FtnKludge).forEach(k => {
- // we want PATH to be last
- if('PATH' !== k) {
- appendMeta(`\x01${k}`, message.meta.FtnKludge[k]);
+ switch(k) {
+ case 'PATH' : break; // skip & save for last
+
+ case 'Via' :
+ case 'FMPT' :
+ case 'TOPT' :
+ case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar
+
+ default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break;
}
});
diff --git a/core/ftn_util.js b/core/ftn_util.js
index 4d779c4a..39093fab 100644
--- a/core/ftn_util.js
+++ b/core/ftn_util.js
@@ -26,6 +26,7 @@ exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
exports.getOrigin = getOrigin;
exports.getTearLine = getTearLine;
exports.getVia = getVia;
+exports.getIntl = getIntl;
exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList;
exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList;
exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries;
@@ -134,9 +135,20 @@ function getMessageSerialNumber(messageId) {
//
// ENiGMA½: .@<5dFtnAddress>
//
-function getMessageIdentifier(message, address) {
+// 0.0.8-alpha:
+// Made compliant with FTN spec *when exporting NetMail* due to
+// Mystic rejecting messages with the true-unique version.
+// Strangely, Synchronet uses the unique format and Mystic does
+// OK with it. Will need to research further. Note also that
+// g00r00 was kind enough to fix Mystic to allow for the Sync/Enig
+// format, but that will only help when using newer Mystic versions.
+//
+function getMessageIdentifier(message, address, isNetMail = false) {
const addrStr = new Address(address).toString('5D');
- return `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`;
+ return isNetMail ?
+ `${addrStr} ${getMessageSerialNumber(message.messageId)}` :
+ `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}`
+ ;
}
//
@@ -188,7 +200,7 @@ function getQuotePrefix(name) {
//
function getOrigin(address) {
const origin = _.has(Config, 'messageNetworks.originLine') ?
- Config.messageNetworks.originLine :
+ Config.messageNetworks.originLine :
Config.general.boardName;
const addrStr = new Address(address).toString('5D');
@@ -222,6 +234,20 @@ function getVia(address) {
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
}
+//
+// Creates a INTL kludge value as per FTS-4001
+// http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac
+//
+function getIntl(toAddress, fromAddress) {
+ //
+ // INTL differs from 'standard' kludges in that there is no ':' after "INTL"
+ //
+ // ""INTL "" ""
+ // "...These addresses shall be given on the form :/"
+ //
+ return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`;
+}
+
function getAbbreviatedNetNodeList(netNodes) {
let abbrList = '';
let currNet;
diff --git a/mods/last_callers.js b/core/last_callers.js
similarity index 92%
rename from mods/last_callers.js
rename to core/last_callers.js
index afb429d8..3a889468 100644
--- a/mods/last_callers.js
+++ b/core/last_callers.js
@@ -2,11 +2,11 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const ViewController = require('../core/view_controller.js').ViewController;
-const StatLog = require('../core/stat_log.js');
-const User = require('../core/user.js');
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const StatLog = require('./stat_log.js');
+const User = require('./user.js');
+const stringFormat = require('./string_format.js');
// deps
const moment = require('moment');
diff --git a/core/logger.js b/core/logger.js
index 3b0b47e2..f90aec41 100644
--- a/core/logger.js
+++ b/core/logger.js
@@ -62,7 +62,7 @@ module.exports = class Log {
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be
//
return JSON.parse(
- JSON.stringify(obj).replace(/"(password|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
+ JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
return `"${valueName}":"********"`;
})
);
diff --git a/core/mail_util.js b/core/mail_util.js
new file mode 100644
index 00000000..654b1617
--- /dev/null
+++ b/core/mail_util.js
@@ -0,0 +1,81 @@
+/* jslint node: true */
+'use strict';
+
+const Address = require('./ftn_address.js');
+const Message = require('./message.js');
+
+exports.getAddressedToInfo = getAddressedToInfo;
+
+const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+
+/*
+ Input Output
+ ----------------------------------------------------------------------------------------------------
+ User { name : 'User', flavor : 'local' }
+ Some User { name : 'Some User', flavor : 'local' }
+ JoeUser @ 1:103/75 { name : 'JoeUser', flavor : 'ftn', remote : '1:103/75' }
+ Bob@1:103/705@fidonet.org { name : 'Bob', flavor : 'ftn', remote : '1:103/705@fidonet.org' }
+ 1:103/705@fidonet.org { flavor : 'ftn', remote : '1:103/705@fidonet.org' }
+ Jane <23:4/100> { name : 'Jane', flavor : 'ftn', remote : '23:4/100' }
+ 43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' }
+ foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' }
+ Bar { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' }
+*/
+function getAddressedToInfo(input) {
+ input = input.trim();
+
+ const firstAtPos = input.indexOf('@');
+
+ if(firstAtPos < 0) {
+ let addr = Address.fromString(input);
+ if(Address.isValidAddress(addr)) {
+ return { flavor : Message.AddressFlavor.FTN, remote : input };
+ }
+
+ const lessThanPos = input.indexOf('<');
+ if(lessThanPos < 0) {
+ return { name : input, flavor : Message.AddressFlavor.Local };
+ }
+
+ const greaterThanPos = input.indexOf('>');
+ if(greaterThanPos < lessThanPos) {
+ return { name : input, flavor : Message.AddressFlavor.Local };
+ }
+
+ addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
+ if(Address.isValidAddress(addr)) {
+ return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
+ }
+
+ return { name : input, flavor : Message.AddressFlavor.Local };
+ }
+
+ const lessThanPos = input.indexOf('<');
+ const greaterThanPos = input.indexOf('>');
+ if(lessThanPos > 0 && greaterThanPos > lessThanPos) {
+ const addr = input.slice(lessThanPos + 1, greaterThanPos);
+ const m = addr.match(EMAIL_REGEX);
+ if(m) {
+ return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr };
+ }
+
+ return { name : input, flavor : Message.AddressFlavor.Local };
+ }
+
+ let m = input.match(EMAIL_REGEX);
+ if(m) {
+ return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input };
+ }
+
+ let addr = Address.fromString(input); // 5D?
+ if(Address.isValidAddress(addr)) {
+ return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ;
+ }
+
+ addr = Address.fromString(input.slice(firstAtPos + 1).trim());
+ if(Address.isValidAddress(addr)) {
+ return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() };
+ }
+
+ return { name : input, flavor : Message.AddressFlavor.Local };
+}
diff --git a/core/menu_stack.js b/core/menu_stack.js
index f4b29460..b4bebea6 100644
--- a/core/menu_stack.js
+++ b/core/menu_stack.js
@@ -129,15 +129,19 @@ module.exports = class MenuStack {
} else {
self.client.log.debug( { menuName : name }, 'Goto menu module');
+ const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags;
+
if(currentModuleInfo) {
// save stack state
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState();
currentModuleInfo.instance.leave();
- const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags;
+ if(currentModuleInfo.menuFlags.includes('noHistory')) {
+ this.pop();
+ }
- if(menuFlags.includes('noHistory')) {
+ if(menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current
}
}
@@ -146,6 +150,7 @@ module.exports = class MenuStack {
name : name,
instance : modInst,
extraArgs : loadOpts.extraArgs,
+ menuFlags : menuFlags,
});
// restore previous state if requested
diff --git a/core/message.js b/core/message.js
index 710444ce..5d3c8db9 100644
--- a/core/message.js
+++ b/core/message.js
@@ -76,6 +76,10 @@ function Message(options) {
this.isPrivate = function() {
return Message.isPrivateAreaTag(this.areaTag);
};
+
+ this.isFromRemoteUser = function() {
+ return null !== _.get(this, 'meta.System.remote_from_user', null);
+ };
}
Message.WellKnownAreaTags = {
@@ -93,6 +97,16 @@ Message.SystemMetaNames = {
LocalFromUserID : 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0
ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc.
+ ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor
+ RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address
+ RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address
+};
+
+// Types for Message.SystemMetaNames.ExternalFlavor meta
+Message.AddressFlavor = {
+ Local : 'local', // local / non-remote addressing
+ FTN : 'ftn', // FTN style
+ Email : 'email',
};
Message.StateFlags0 = {
@@ -112,7 +126,7 @@ Message.FtnPropertyNames = {
FtnDestZone : 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point',
-
+
FtnAttribute : 'ftn_attribute',
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
@@ -124,11 +138,23 @@ Message.FtnPropertyNames = {
// Note: kludges are stored with their names as-is
Message.prototype.setLocalToUserId = function(userId) {
- this.meta.System.local_to_user_id = userId;
+ this.meta.System = this.meta.System || {};
+ this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId;
};
Message.prototype.setLocalFromUserId = function(userId) {
- this.meta.System.local_from_user_id = userId;
+ this.meta.System = this.meta.System || {};
+ this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId;
+};
+
+Message.prototype.setRemoteToUser = function(remoteTo) {
+ this.meta.System = this.meta.System || {};
+ this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo;
+};
+
+Message.prototype.setExternalFlavor = function(flavor) {
+ this.meta.System = this.meta.System || {};
+ this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor;
};
Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) {
@@ -321,8 +347,13 @@ Message.prototype.load = function(options, cb) {
);
};
-Message.prototype.persistMetaValue = function(category, name, value, cb) {
- const metaStmt = msgDb.prepare(
+Message.prototype.persistMetaValue = function(category, name, value, transOrDb, cb) {
+ if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
+ cb = transOrDb;
+ transOrDb = msgDb;
+ }
+
+ const metaStmt = transOrDb.prepare(
`INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value)
VALUES (?, ?, ?, ?);`);
@@ -341,18 +372,6 @@ Message.prototype.persistMetaValue = function(category, name, value, cb) {
});
};
-Message.startTransaction = function(cb) {
- msgDb.run('BEGIN;', err => {
- cb(err);
- });
-};
-
-Message.endTransaction = function(hadError, cb) {
- msgDb.run(hadError ? 'ROLLBACK;' : 'COMMIT;', err => {
- cb(err);
- });
-};
-
Message.prototype.persist = function(cb) {
if(!this.isValid()) {
@@ -361,14 +380,12 @@ Message.prototype.persist = function(cb) {
const self = this;
- async.series(
+ async.waterfall(
[
function beginTransaction(callback) {
- Message.startTransaction(err => {
- return callback(err);
- });
+ return msgDb.beginTransaction(callback);
},
- function storeMessage(callback) {
+ function storeMessage(trans, callback) {
// generate a UUID for this message if required (general case)
const msgTimestamp = moment();
if(!self.uuid) {
@@ -379,7 +396,7 @@ Message.prototype.persist = function(cb) {
self.message);
}
- msgDb.run(
+ trans.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, getISOTimestampString(msgTimestamp) ],
@@ -388,13 +405,13 @@ Message.prototype.persist = function(cb) {
self.messageId = this.lastID;
}
- return callback(err);
+ return callback(err, trans);
}
);
},
- function storeMeta(callback) {
+ function storeMeta(trans, callback) {
if(!self.meta) {
- return callback(null);
+ return callback(null, trans);
}
/*
Example of self.meta:
@@ -410,7 +427,7 @@ Message.prototype.persist = function(cb) {
*/
async.each(Object.keys(self.meta), (category, nextCat) => {
async.each(Object.keys(self.meta[category]), (name, nextName) => {
- self.persistMetaValue(category, name, self.meta[category][name], err => {
+ self.persistMetaValue(category, name, self.meta[category][name], trans, err => {
nextName(err);
});
}, err => {
@@ -418,18 +435,22 @@ Message.prototype.persist = function(cb) {
});
}, err => {
- callback(err);
+ callback(err, trans);
});
},
- function storeHashTags(callback) {
+ function storeHashTags(trans, callback) {
// :TODO: hash tag support
- return callback(null);
+ return callback(null, trans);
}
],
- err => {
- Message.endTransaction(err, transErr => {
- return cb(err ? err : transErr, self.messageId);
- });
+ (err, trans) => {
+ if(trans) {
+ trans[err ? 'rollback' : 'commit'](transErr => {
+ return cb(err ? err : transErr, self.messageId);
+ });
+ } else {
+ return cb(err);
+ }
}
);
};
diff --git a/core/message_area.js b/core/message_area.js
index 484d22ec..53dd3086 100644
--- a/core/message_area.js
+++ b/core/message_area.js
@@ -2,17 +2,19 @@
'use strict';
// ENiGMA½
-const msgDb = require('./database.js').dbs.message;
-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;
+const msgDb = require('./database.js').dbs.message;
+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;
+const { getISOTimestampString } = require('./database.js');
// deps
const async = require('async');
const _ = require('lodash');
const assert = require('assert');
+const moment = require('moment');
exports.getAvailableMessageConferences = getAvailableMessageConferences;
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
@@ -28,6 +30,7 @@ exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea;
exports.getMessageListForArea = getMessageListForArea;
exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser;
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
+exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea;
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
exports.persistMessage = persistMessage;
@@ -347,13 +350,13 @@ function getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, what)
'COUNT() AS count' :
'message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count';
- let sql =
+ let sql =
`SELECT ${selectWhat}
FROM message
WHERE area_tag = "${areaTag}" AND message_id > ${lastMessageId}`;
if(Message.isPrivateAreaTag(areaTag)) {
- sql +=
+ sql +=
` AND message_id in (
SELECT message_id
FROM message_meta
@@ -482,6 +485,28 @@ function getMessageListForArea(options, areaTag, cb) {
);
}
+function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
+ if(moment.isMoment(newerThanTimestamp)) {
+ newerThanTimestamp = getISOTimestampString(newerThanTimestamp);
+ }
+
+ msgDb.get(
+ `SELECT message_id
+ FROM message
+ WHERE area_tag = ? AND DATETIME(modified_timestamp) > DATETIME("${newerThanTimestamp}", "+1 seconds")
+ ORDER BY modified_timestamp ASC
+ LIMIT 1;`,
+ [ areaTag ],
+ (err, row) => {
+ if(err) {
+ return cb(err);
+ }
+
+ return cb(null, row ? row.message_id : null);
+ }
+ );
+}
+
function getMessageAreaLastReadId(userId, areaTag, cb) {
msgDb.get(
'SELECT message_id ' +
@@ -494,7 +519,12 @@ function getMessageAreaLastReadId(userId, areaTag, cb) {
);
}
-function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) {
+function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) {
+ if(!cb && _.isFunction(allowOlder)) {
+ cb = allowOlder;
+ allowOlder = false;
+ }
+
// :TODO: likely a better way to do this...
async.waterfall(
[
@@ -505,7 +535,7 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, cb) {
});
},
function update(lastId, callback) {
- if(messageId > lastId) {
+ if(allowOlder || messageId > lastId) {
msgDb.run(
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
'VALUES (?, ?, ?);',
@@ -568,11 +598,11 @@ function trimMessageAreasScheduledEvent(args, cb) {
LIMIT -1 OFFSET ${areaInfo.maxMessages}
);`,
[ areaInfo.areaTag.toLowerCase() ],
- err => {
+ function result(err) { // no arrow func; need this
if(err) {
- Log.error( { areaInfo : areaInfo, err : err, type : 'maxMessages' }, 'Error trimming message area');
+ Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area');
} else {
- Log.debug( { areaInfo : areaInfo, type : 'maxMessages' }, 'Area trimmed successfully');
+ Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully');
}
return cb(err);
}
@@ -588,21 +618,25 @@ function trimMessageAreasScheduledEvent(args, cb) {
`DELETE FROM message
WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`,
[ areaInfo.areaTag ],
- err => {
+ function result(err) { // no arrow func; need this
if(err) {
- Log.warn( { areaInfo : areaInfo, err : err, type : 'maxAgeDays' }, 'Error trimming message area');
+ Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area');
} else {
- Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays' }, 'Area trimmed successfully');
+ Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully');
}
return cb(err);
}
);
}
-
+
async.waterfall(
- [
+ [
function getAreaTags(callback) {
- let areaTags = [];
+ const areaTags = [];
+
+ //
+ // We use SQL here vs API such that no-longer-used tags are picked up
+ //
msgDb.each(
`SELECT DISTINCT area_tag
FROM message;`,
@@ -610,7 +644,11 @@ function trimMessageAreasScheduledEvent(args, cb) {
if(err) {
return callback(err);
}
- areaTags.push(row.area_tag);
+
+ // We treat private mail special
+ if(!Message.isPrivateAreaTag(row.area_tag)) {
+ areaTags.push(row.area_tag);
+ }
},
err => {
return callback(err, areaTags);
@@ -622,30 +660,26 @@ function trimMessageAreasScheduledEvent(args, cb) {
// determine maxMessages & maxAgeDays per area
areaTags.forEach(areaTag => {
-
+
let maxMessages = Config.messageAreaDefaults.maxMessages;
let maxAgeDays = Config.messageAreaDefaults.maxAgeDays;
-
+
const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here
if(area) {
- if(area.maxMessages) {
- maxMessages = area.maxMessages;
- }
- if(area.maxAgeDays) {
- maxAgeDays = area.maxAgeDays;
- }
+ maxMessages = area.maxMessages || maxMessages;
+ maxAgeDays = area.maxAgeDays || maxAgeDays;
}
areaInfos.push( {
areaTag : areaTag,
maxMessages : maxMessages,
maxAgeDays : maxAgeDays,
- } );
+ } );
});
return callback(null, areaInfos);
},
- function trimAreas(areaInfos, callback) {
+ function trimGeneralAreas(areaInfos, callback) {
async.each(
areaInfos,
(areaInfo, next) => {
@@ -661,11 +695,50 @@ function trimMessageAreasScheduledEvent(args, cb) {
},
callback
);
- }
+ },
+ function trimExternalPrivateSentMail(callback) {
+ //
+ // *External* (FTN, email, ...) outgoing is cleaned up *after export*
+ // if it is older than the configured |maxExternalSentAgeDays| days
+ //
+ // Outgoing externally exported private mail is:
+ // - In the 'private_mail' area
+ // - Marked exported (state_flags0 exported bit set)
+ // - Marked with any external flavor (we don't mark local)
+ //
+ const maxExternalSentAgeDays = _.get(
+ Config,
+ 'messageConferences.system_internal.areas.private_mail.maxExternalSentAgeDays',
+ 30
+ );
+
+ msgDb.run(
+ `DELETE FROM message
+ WHERE message_id IN (
+ SELECT m.message_id
+ FROM message m
+ JOIN message_meta mms
+ ON m.message_id = mms.message_id AND
+ (mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported}))
+ JOIN message_meta mmf
+ ON m.message_id = mmf.message_id AND
+ (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}')
+ WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days')
+ );`,
+ function results(err) { // no arrow func; need this
+ if(err) {
+ Log.warn( { error : err.message }, 'Error trimming private externally sent messages');
+ } else {
+ Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully');
+ }
+ }
+ );
+
+ return callback(null);
+ }
],
err => {
return cb(err);
}
);
-
}
\ No newline at end of file
diff --git a/core/mime_util.js b/core/mime_util.js
index bdb88a53..1e2bd32c 100644
--- a/core/mime_util.js
+++ b/core/mime_util.js
@@ -14,7 +14,6 @@ function startup(cb) {
// Add in types (not yet) supported by mime-db -- and therefor, mime-types
//
const ADDITIONAL_EXT_MIMETYPES = {
- arj : 'application/x-arj',
ans : 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :(
};
diff --git a/mods/msg_area_list.js b/core/msg_area_list.js
similarity index 91%
rename from mods/msg_area_list.js
rename to core/msg_area_list.js
index a6a0df4c..eaedbef8 100644
--- a/mods/msg_area_list.js
+++ b/core/msg_area_list.js
@@ -2,12 +2,12 @@
'use strict';
// ENiGMA½
-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 resetScreen = require('../core/ansi_term.js').resetScreen;
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const messageArea = require('./message_area.js');
+const displayThemeArt = require('./theme.js').displayThemeArt;
+const resetScreen = require('./ansi_term.js').resetScreen;
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
diff --git a/mods/msg_area_post_fse.js b/core/msg_area_post_fse.js
similarity index 90%
rename from mods/msg_area_post_fse.js
rename to core/msg_area_post_fse.js
index 21b5d068..c13f39a6 100644
--- a/mods/msg_area_post_fse.js
+++ b/core/msg_area_post_fse.js
@@ -1,8 +1,8 @@
/* jslint node: true */
'use strict';
-const FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
-const persistMessage = require('../core/message_area.js').persistMessage;
+const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
+const persistMessage = require('./message_area.js').persistMessage;
const _ = require('lodash');
const async = require('async');
diff --git a/mods/msg_area_reply_fse.js b/core/msg_area_reply_fse.js
similarity index 82%
rename from mods/msg_area_reply_fse.js
rename to core/msg_area_reply_fse.js
index d1cb5faa..24ee5377 100644
--- a/mods/msg_area_reply_fse.js
+++ b/core/msg_area_reply_fse.js
@@ -1,7 +1,7 @@
/* jslint node: true */
'use strict';
-var FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
+var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
exports.getModule = AreaReplyFSEModule;
diff --git a/mods/msg_area_view_fse.js b/core/msg_area_view_fse.js
similarity index 96%
rename from mods/msg_area_view_fse.js
rename to core/msg_area_view_fse.js
index de4657f1..02915f79 100644
--- a/mods/msg_area_view_fse.js
+++ b/core/msg_area_view_fse.js
@@ -2,8 +2,8 @@
'use strict';
// ENiGMA½
-const FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule;
-const Message = require('../core/message.js');
+const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
+const Message = require('./message.js');
// deps
const _ = require('lodash');
diff --git a/mods/msg_conf_list.js b/core/msg_conf_list.js
similarity index 89%
rename from mods/msg_conf_list.js
rename to core/msg_conf_list.js
index 91c24de4..6f42cf36 100644
--- a/mods/msg_conf_list.js
+++ b/core/msg_conf_list.js
@@ -2,12 +2,12 @@
'use strict';
// ENiGMA½
-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 resetScreen = require('../core/ansi_term.js').resetScreen;
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const messageArea = require('./message_area.js');
+const displayThemeArt = require('./theme.js').displayThemeArt;
+const resetScreen = require('./ansi_term.js').resetScreen;
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
diff --git a/mods/msg_list.js b/core/msg_list.js
similarity index 95%
rename from mods/msg_list.js
rename to core/msg_list.js
index bc80e27b..e5a69e80 100644
--- a/mods/msg_list.js
+++ b/core/msg_list.js
@@ -2,11 +2,11 @@
'use strict';
// ENiGMA½
-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;
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const messageArea = require('./message_area.js');
+const stringFormat = require('./string_format.js');
+const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
// deps
const async = require('async');
diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js
index 9d46e8a1..68d8b3d6 100644
--- a/core/multi_line_edit_text_view.js
+++ b/core/multi_line_edit_text_view.js
@@ -67,7 +67,7 @@ const SPECIAL_KEY_MAP_DEFAULT = {
'line feed' : [ 'return' ],
exit : [ 'esc' ],
backspace : [ 'backspace' ],
- delete : [ 'del' ],
+ delete : [ 'delete' ],
tab : [ 'tab' ],
up : [ 'up arrow' ],
down : [ 'down arrow' ],
@@ -354,21 +354,14 @@ function MultiLineEditTextView(options) {
};
this.removeCharactersFromText = function(index, col, operation, count) {
- if('right' === operation) {
+ if('delete' === operation) {
self.textLines[index].text =
- self.textLines[index].text.slice(col, count) +
+ self.textLines[index].text.slice(0, col) +
self.textLines[index].text.slice(col + count);
- self.cursorPos.col -= count;
-
self.updateTextWordWrap(index);
self.redrawRows(self.cursorPos.row, self.dimens.height);
-
- if(0 === self.textLines[index].text) {
-
- } else {
- self.redrawRows(self.cursorPos.row, self.dimens.height);
- }
+ self.moveClientCursorToCursorPos();
} else if ('backspace' === operation) {
// :TODO: method for splicing text
self.textLines[index].text =
@@ -868,11 +861,25 @@ function MultiLineEditTextView(options) {
};
this.keyPressDelete = function() {
- self.removeCharactersFromText(
- self.getTextLinesIndex(),
- self.cursorPos.col,
- 'right',
- 1);
+ const lineIndex = self.getTextLinesIndex();
+
+ if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) {
+ //
+ // Start of line and nothing left. Just delete the line
+ //
+ self.removeCharactersFromText(
+ lineIndex,
+ 0,
+ 'delete line'
+ );
+ } else {
+ self.removeCharactersFromText(
+ lineIndex,
+ self.cursorPos.col,
+ 'delete',
+ 1
+ );
+ }
self.emitEditPosition();
};
@@ -1068,9 +1075,9 @@ MultiLineEditTextView.prototype.setFocus = function(focused) {
MultiLineEditTextView.super_.prototype.setFocus.call(this, focused);
};
-MultiLineEditTextView.prototype.setText = function(text) {
+MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode : 'default' } ) {
this.textLines = [ ];
- this.addText(text);
+ this.addText(text, options);
/*this.insertRawText(text);
if(this.isEditMode()) {
@@ -1085,13 +1092,27 @@ MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : f
return this.setAnsiWithOptions(ansi, options, cb);
};
-MultiLineEditTextView.prototype.addText = function(text) {
+MultiLineEditTextView.prototype.addText = function(text, options = { scrollMode : 'default' }) {
this.insertRawText(text);
- if(this.isEditMode() || this.autoScroll) {
- this.cursorEndOfDocument();
- } else {
- this.cursorStartOfDocument();
+ switch(options.scrollMode) {
+ case 'default' :
+ if(this.isEditMode() || this.autoScroll) {
+ this.cursorEndOfDocument();
+ } else {
+ this.cursorStartOfDocument();
+ }
+ break;
+
+ case 'top' :
+ case 'start' :
+ this.cursorStartOfDocument();
+ break;
+
+ case 'end' :
+ case 'bottom' :
+ this.cursorEndOfDocument();
+ break;
}
};
@@ -1129,7 +1150,7 @@ const HANDLED_SPECIAL_KEYS = [
'line feed',
'insert',
'tab',
- 'backspace', 'del',
+ 'backspace', 'delete',
'delete line',
];
diff --git a/core/new_scan.js b/core/new_scan.js
index 3c3f1371..f3e851fb 100644
--- a/core/new_scan.js
+++ b/core/new_scan.js
@@ -2,13 +2,14 @@
'use strict';
// ENiGMA½
-const msgArea = require('./message_area.js');
-const MenuModule = require('./menu_module.js').MenuModule;
-const ViewController = require('./view_controller.js').ViewController;
-const stringFormat = require('./string_format.js');
-const FileEntry = require('./file_entry.js');
-const FileBaseFilters = require('./file_base_filter.js');
-const Errors = require('./enig_error.js').Errors;
+const msgArea = require('./message_area.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const stringFormat = require('./string_format.js');
+const FileEntry = require('./file_entry.js');
+const FileBaseFilters = require('./file_base_filter.js');
+const Errors = require('./enig_error.js').Errors;
+const { getAvailableFileAreaTags } = require('./file_base_area.js');
// deps
const _ = require('lodash');
@@ -166,18 +167,24 @@ exports.getModule = class NewScanModule extends MenuModule {
newScanFileBase(cb) {
// :TODO: add in steps
+ const filterCriteria = {
+ newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user),
+ areaTag : getAvailableFileAreaTags(this.client),
+ order : 'ascending', // oldest first
+ };
+
FileEntry.findFiles(
- { newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user) },
+ filterCriteria,
(err, fileIds) => {
if(err || 0 === fileIds.length) {
return cb(err ? err : Errors.DoesNotExist('No more new files'));
}
- FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[0] );
+ FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[fileIds.length - 1] );
const menuOpts = {
extraArgs : {
- fileList : fileIds,
+ fileList : fileIds,
},
};
diff --git a/mods/nua.js b/core/nua.js
similarity index 92%
rename from mods/nua.js
rename to core/nua.js
index 878e0581..7939e739 100644
--- a/mods/nua.js
+++ b/core/nua.js
@@ -2,12 +2,12 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const User = require('../core/user.js');
-const theme = require('../core/theme.js');
-const login = require('../core/system_menu_method.js').login;
-const Config = require('../core/config.js').config;
-const messageArea = require('../core/message_area.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const User = require('./user.js');
+const theme = require('./theme.js');
+const login = require('./system_menu_method.js').login;
+const Config = require('./config.js').config;
+const messageArea = require('./message_area.js');
exports.moduleInfo = {
name : 'NUA',
diff --git a/mods/onelinerz.js b/core/onelinerz.js
similarity index 94%
rename from mods/onelinerz.js
rename to core/onelinerz.js
index 335c25ce..9e89addf 100644
--- a/mods/onelinerz.js
+++ b/core/onelinerz.js
@@ -2,12 +2,17 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const getModDatabasePath = require('../core/database.js').getModDatabasePath;
-const ViewController = require('../core/view_controller.js').ViewController;
-const theme = require('../core/theme.js');
-const ansi = require('../core/ansi_term.js');
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+
+const {
+ getModDatabasePath,
+ getTransactionDatabase
+} = require('./database.js');
+
+const ViewController = require('./view_controller.js').ViewController;
+const theme = require('./theme.js');
+const ansi = require('./ansi_term.js');
+const stringFormat = require('./string_format.js');
// deps
const sqlite3 = require('sqlite3');
@@ -263,12 +268,12 @@ exports.getModule = class OnelinerzModule extends MenuModule {
async.series(
[
function openDatabase(callback) {
- self.db = new sqlite3.Database(
+ self.db = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(exports.moduleInfo),
err => {
return callback(err);
}
- );
+ ));
},
function createTables(callback) {
self.db.run(
diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js
index 21e1a6d0..e175a166 100644
--- a/core/oputil/oputil_common.js
+++ b/core/oputil/oputil_common.js
@@ -45,11 +45,12 @@ function printUsageAndSetExitCode(errMsg, exitCode) {
}
function getDefaultConfigPath() {
- return resolvePath('~/.config/enigma-bbs/config.hjson');
+ return './config/';
}
function getConfigPath() {
- return argv.config ? argv.config : config.getDefaultPath();
+ const baseConfigPath = argv.config ? argv.config : config.getDefaultPath();
+ return baseConfigPath + 'config.hjson';
}
function initConfig(cb) {
diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js
index 5cbe10e3..d6a18026 100644
--- a/core/oputil/oputil_file_base.js
+++ b/core/oputil/oputil_file_base.js
@@ -8,7 +8,7 @@ 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 Errors = require('../../core/enig_error.js').Errors;
+const Errors = require('../enig_error.js').Errors;
const async = require('async');
const fs = require('graceful-fs');
@@ -34,7 +34,7 @@ exports.handleFileBaseCommand = handleFileBaseCommand;
let fileArea; // required during init
-function finalizeEntryAndPersist(fileEntry, descHandler, cb) {
+function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) {
async.series(
[
function getDescFromHandlerIfNeeded(callback) {
@@ -53,18 +53,24 @@ function finalizeEntryAndPersist(fileEntry, descHandler, cb) {
return callback(null);
},
function getDescFromUserIfNeeded(callback) {
- if(false === argv.prompt || ( fileEntry.desc && fileEntry.desc.length > 0 ) ) {
+ if(fileEntry.desc && fileEntry.desc.length > 0 ) {
return callback(null);
}
- const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName;
+ const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName;
+ const descFromFile = getDescFromFileName(fileEntry.fileName);
+
+ if(false === argv.prompt) {
+ fileEntry.desc = descFromFile;
+ return callback(null);
+ }
const questions = [
{
name : 'desc',
message : `Description for ${fileEntry.fileName}:`,
type : 'input',
- default : getDescFromFileName(fileEntry.fileName),
+ default : descFromFile,
}
];
@@ -74,7 +80,7 @@ function finalizeEntryAndPersist(fileEntry, descHandler, cb) {
});
},
function persist(callback) {
- fileEntry.persist( err => {
+ fileEntry.persist(isUpdate, err => {
return callback(err);
});
}
@@ -104,6 +110,12 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
return !asi.storageTag || sl.storageTag === asi.storageTag;
});
});
+
+ function updateTags(fe) {
+ if(Array.isArray(options.tags)) {
+ fe.hashTags = new Set(options.tags);
+ }
+ }
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
async.waterfall(
@@ -153,27 +165,68 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
},
(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???
+ //
+ // We'll update the entry if the following conditions are met:
+ // * We have a single duplicate, and:
+ // * --update was passed or the existing entry's desc,
+ // longDesc, or est_release_year meta are blank/empty
+ //
+ if(argv.update && 1 === dupeEntries.length) {
+ const FileEntry = require('../../core/file_entry.js');
+ const existingEntry = new FileEntry();
+
+ return existingEntry.load(dupeEntries[0].fileId, err => {
+ if(err) {
+ console.info('Dupe (cannot update)');
+ return nextFile(null);
+ }
+
+ //
+ // Update only if tags or desc changed
+ //
+ const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags;
+ const tagsEq = _.isEqual(optTags, existingEntry.hashTags);
+
+ if( tagsEq &&
+ fileEntry.desc === existingEntry.desc &&
+ fileEntry.descLong == existingEntry.descLong &&
+ fileEntry.meta.est_release_year == existingEntry.meta.est_release_year)
+ {
+ console.info('Dupe');
+ return nextFile(null);
+ }
+
+ console.info('Dupe (updating)');
+
+ // don't allow overwrite of values if new version is blank
+ existingEntry.desc = fileEntry.desc || existingEntry.desc;
+ existingEntry.descLong = fileEntry.descLong || existingEntry.descLong;
+
+ if(fileEntry.meta.est_release_year) {
+ existingEntry.meta.est_release_year = fileEntry.meta.est_release_year;
+ }
+
+ updateTags(existingEntry);
+
+ finalizeEntryAndPersist(true, existingEntry, descHandler, err => {
+ return nextFile(err);
+ });
+ });
+ } else if(dupeEntries.length > 0) {
console.info('Dupe');
return nextFile(null);
- } else {
- console.info('Done!');
- if(Array.isArray(options.tags)) {
- options.tags.forEach(tag => {
- fileEntry.hashTags.add(tag);
- });
- }
-
- finalizeEntryAndPersist(fileEntry, descHandler, err => {
- return nextFile(err);
- });
}
+
+ console.info('Done!');
+ updateTags(fileEntry);
+
+ finalizeEntryAndPersist(false, fileEntry, descHandler, err => {
+ return nextFile(err);
+ });
}
);
});
@@ -395,6 +448,62 @@ function scanFileAreas() {
);
}
+function expandFileTargets(targets, cb) {
+ let entries = [];
+
+ // Each entry may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
+ const FileEntry = require('../../core/file_entry.js');
+
+ async.eachSeries(targets, (areaAndStorage, next) => {
+ const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
+
+ if(areaInfo) {
+ // AREA_TAG[@STORAGE_TAG] - all files in area@tag
+ const findFilter = {
+ areaTag : areaAndStorage.areaTag,
+ };
+
+ if(areaAndStorage.storageTag) {
+ findFilter.storageTag = areaAndStorage.storageTag;
+ }
+
+ FileEntry.findFiles(findFilter, (err, fileIds) => {
+ if(err) {
+ return next(err);
+ }
+
+ async.each(fileIds, (fileId, nextFileId) => {
+ const fileEntry = new FileEntry();
+ fileEntry.load(fileId, err => {
+ if(!err) {
+ entries.push(fileEntry);
+ }
+ return nextFileId(err);
+ });
+ },
+ err => {
+ return next(err);
+ });
+ });
+
+ } else {
+ // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA
+ // :TODO: FULL_PATH -> entries
+ getFileEntries(areaAndStorage.pattern, (err, fileEntries) => {
+ if(err) {
+ return next(err);
+ }
+
+ entries = entries.concat(fileEntries);
+ return next(null);
+ });
+ }
+ },
+ err => {
+ return cb(err, entries);
+ });
+}
+
function moveFiles() {
//
// oputil fb move SRC [SRC2 ...] DST
@@ -407,8 +516,9 @@ function moveFiles() {
}
const moveArgs = argv._.slice(2);
- let src = getAreaAndStorage(moveArgs.slice(0, -1));
- let dst = getAreaAndStorage(moveArgs.slice(-1))[0];
+ const src = getAreaAndStorage(moveArgs.slice(0, -1));
+ const dst = getAreaAndStorage(moveArgs.slice(-1))[0];
+
let FileEntry;
async.waterfall(
@@ -422,8 +532,6 @@ function moveFiles() {
});
},
function validateAndExpandSourceAndDest(callback) {
- let srcEntries = [];
-
const areaInfo = fileArea.getFileAreaByTag(dst.areaTag);
if(areaInfo) {
dst.areaInfo = areaInfo;
@@ -431,57 +539,9 @@ function moveFiles() {
return callback(Errors.DoesNotExist('Invalid or unknown destination area'));
}
- // Each SRC may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
FileEntry = require('../../core/file_entry.js');
- async.eachSeries(src, (areaAndStorage, next) => {
- const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
-
- if(areaInfo) {
- // AREA_TAG[@STORAGE_TAG] - all files in area@tag
- src.areaInfo = areaInfo;
-
- const findFilter = {
- areaTag : areaAndStorage.areaTag,
- };
-
- if(areaAndStorage.storageTag) {
- findFilter.storageTag = areaAndStorage.storageTag;
- }
-
- FileEntry.findFiles(findFilter, (err, fileIds) => {
- if(err) {
- return next(err);
- }
-
- async.each(fileIds, (fileId, nextFileId) => {
- const fileEntry = new FileEntry();
- fileEntry.load(fileId, err => {
- if(!err) {
- srcEntries.push(fileEntry);
- }
- return nextFileId(err);
- });
- },
- err => {
- return next(err);
- });
- });
-
- } else {
- // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA
- // :TODO: FULL_PATH -> entries
- getFileEntries(areaAndStorage.pattern, (err, entries) => {
- if(err) {
- return next(err);
- }
-
- srcEntries = srcEntries.concat(entries);
- return next(null);
- });
- }
- },
- err => {
+ expandFileTargets(src, (err, srcEntries) => {
return callback(err, srcEntries);
});
},
@@ -512,13 +572,80 @@ function moveFiles() {
return callback(err);
});
}
- ]
+ ],
+ err => {
+ if(err) {
+ process.exitCode = ExitCodes.ERROR;
+ console.error(err.message);
+ }
+ }
);
}
function removeFiles() {
//
- // REMOVE SHA|FILE_ID [SHA|FILE_ID ...]
+ // oputil fb rm|remove|del|delete SRC [SRC2 ...]
+ //
+ // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
+ //
+ // AREA_TAG[@STORAGE_TAG] remove all entries matching
+ // supplied area/storage tags
+ //
+ // --phys-file removes backing physical file(s)
+ //
+ if(argv._.length < 3) {
+ return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
+ }
+
+ const removePhysFile = argv['phys-file'];
+
+ const src = getAreaAndStorage(argv._.slice(2));
+
+ async.waterfall(
+ [
+ function init(callback) {
+ return initConfigAndDatabases( err => {
+ if(!err) {
+ fileArea = require('../../core/file_base_area.js');
+ }
+ return callback(err);
+ });
+ },
+ function expandSources(callback) {
+ expandFileTargets(src, (err, srcEntries) => {
+ return callback(err, srcEntries);
+ });
+ },
+ function removeEntries(srcEntries, callback) {
+ const FileEntry = require('../../core/file_entry.js');
+
+ const extraOutput = removePhysFile ? ' (including physical file)' : '';
+
+ async.eachSeries(srcEntries, (entry, nextEntry) => {
+
+ process.stdout.write(`Removing ${entry.filePath}${extraOutput}... `);
+
+ FileEntry.removeEntry(entry, { removePhysFile }, err => {
+ if(err) {
+ console.info(`Failed: ${err.message}`);
+ } else {
+ console.info('Done');
+ }
+
+ return nextEntry(err);
+ });
+ }, err => {
+ return callback(err);
+ });
+ }
+ ],
+ err => {
+ if(err) {
+ process.exitCode = ExitCodes.ERROR;
+ console.error(err.message);
+ }
+ }
+ );
}
function handleFileBaseCommand() {
@@ -539,7 +666,13 @@ function handleFileBaseCommand() {
return ({
info : displayFileAreaInfo,
scan : scanFileAreas,
+
+ mv : moveFiles,
move : moveFiles,
+
+ rm : removeFiles,
remove : removeFiles,
+ del : removeFiles,
+ delete : removeFiles,
}[action] || errUsage)();
}
\ No newline at end of file
diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js
index 5c20e3a5..b4c48267 100644
--- a/core/oputil/oputil_help.js
+++ b/core/oputil/oputil_help.js
@@ -45,7 +45,7 @@ import-areas args:
--type TYPE specifies area import type. valid options are "bbs" and "na"
`,
FileBase :
-`usage: oputil.js fb [] []
+`usage: oputil.js fb []
actions:
scan AREA_TAG[@STORAGE_TAG] scan specified area
@@ -53,24 +53,27 @@ actions:
info AREA_TAG|SHA|FILE_ID display information about areas and/or files
SHA may be a full or partial SHA-256
- move SRC [SRC...]] DST move entry(s) from SRC to DST
- * SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG]
- * DST: AREA_TAG[@STORAGE_TAG]
+ mv SRC [SRC...] DST move entry(s) from SRC to DST
+ SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG]
+ DST: AREA_TAG[@STORAGE_TAG]
- remove SHA|FILE_ID removes a entry from the system
+ rm SRC [SRC...] remove entry(s) from the system matching SRC
+ SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG]
scan args:
--tags TAG1,TAG2,... specify tag(s) to assign to discovered entries
+
--desc-file [PATH] prefer file descriptions from DESCRIPT.ION file over
other sources such as FILE_ID.DIZ.
if PATH is specified, use DESCRIPT.ION at PATH instead
of looking in specific storage locations
+ --update attempt to update information for existing entries
info args:
--show-desc display short description, if any
remove args:
- --delete also remove underlying physical file
+ --phys-file also remove underlying physical file
`,
FileOpsInfo :
`
@@ -81,6 +84,14 @@ general information:
FILENAME_WC filename with * and ? wildcard support. may match 0:n entries
SHA full or partial SHA-256
FILE_ID a file identifier. see file.sqlite3
+`,
+ MessageBase :
+ `usage: oputil.js mb []
+
+ actions:
+ areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s)
+ one or more commands may be supplied. commands that are multi
+ part such as "%COMPRESS ZIP" should be quoted.
`
};
diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js
index 83af6b5e..aa373ef2 100644
--- a/core/oputil/oputil_main.js
+++ b/core/oputil/oputil_main.js
@@ -7,6 +7,7 @@ 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 handleMessageBaseCommand = require('./oputil_message_base.js').handleMessageBaseCommand;
const handleConfigCommand = require('./oputil_config.js').handleConfigCommand;
const getHelpFor = require('./oputil_help.js').getHelpFor;
@@ -26,19 +27,10 @@ module.exports = function() {
}
switch(argv._[0]) {
- case 'user' :
- handleUserCommand();
- break;
-
- case 'config' :
- handleConfigCommand();
- break;
-
- case 'fb' :
- handleFileBaseCommand();
- break;
-
- default:
- return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND);
+ case 'user' : return handleUserCommand();
+ case 'config' : return handleConfigCommand();
+ case 'fb' : return handleFileBaseCommand();
+ case 'mb' : return handleMessageBaseCommand();
+ default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND);
}
};
diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js
new file mode 100644
index 00000000..6e89cf73
--- /dev/null
+++ b/core/oputil/oputil_message_base.js
@@ -0,0 +1,142 @@
+/* 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 Address = require('../ftn_address.js');
+const Errors = require('../enig_error.js').Errors;
+
+// deps
+const async = require('async');
+
+exports.handleMessageBaseCommand = handleMessageBaseCommand;
+
+function areaFix() {
+ //
+ // oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS]
+ //
+ if(argv._.length < 3) {
+ return printUsageAndSetExitCode(
+ getHelpFor('MessageBase'),
+ ExitCodes.ERROR
+ );
+ }
+
+ async.waterfall(
+ [
+ function init(callback) {
+ return initConfigAndDatabases(callback);
+ },
+ function validateAddress(callback) {
+ const addrArg = argv._.slice(-1)[0];
+ const ftnAddr = Address.fromString(addrArg);
+
+ if(!ftnAddr) {
+ return callback(Errors.Invalid(`"${addrArg}" is not a valid FTN address`));
+ }
+
+ //
+ // We need to validate the address targets a system we know unless
+ // the --force option is used
+ //
+ // :TODO:
+ return callback(null, ftnAddr);
+ },
+ function fetchFromUser(ftnAddr, callback) {
+ //
+ // --from USER || +op from system
+ //
+ // If possible, we want the user ID of the supplied user as well
+ //
+ const User = require('../user.js');
+
+ if(argv.from) {
+ User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => {
+ if(err) {
+ return callback(null, ftnAddr, argv.from, 0);
+ }
+
+ // fromName is the same as argv.from, but case may be differnet (yet correct)
+ return callback(null, ftnAddr, fromName, userId);
+ });
+ } else {
+ User.getUserName(User.RootUserID, (err, fromName) => {
+ return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID);
+ });
+ }
+ },
+ function createMessage(ftnAddr, fromName, fromUserId, callback) {
+ //
+ // Build message as commands separated by line feed
+ //
+ // We need to remove quotes from arguments. These are required
+ // in the case of e.g. removing an area: "-SOME_AREA" would end
+ // up confusing minimist, therefor they must be quoted: "'-SOME_AREA'"
+ //
+ const messageBody = argv._.slice(2, -1).map(arg => {
+ return arg.replace(/["']/g, '');
+ }).join('\r\n') + '\n';
+
+ const Message = require('../message.js');
+
+ const message = new Message({
+ toUserName : argv.to || 'AreaFix',
+ fromUserName : fromName,
+ subject : argv.password || '',
+ message : messageBody,
+ areaTag : Message.WellKnownAreaTags.Private, // mark private
+ meta : {
+ System : {
+ [ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it
+ [ Message.SystemMetaNames.ExternalFlavor ] : Message.AddressFlavor.FTN, // on FTN-style network
+ }
+ }
+ });
+
+ if(0 !== fromUserId) {
+ message.setLocalFromUserId(fromUserId);
+ }
+
+ return callback(null, message);
+ },
+ function persistMessage(message, callback) {
+ message.persist(err => {
+ if(!err) {
+ console.log('AreaFix message persisted and will be exported at next scheduled scan');
+ }
+ return callback(err);
+ });
+ }
+ ],
+ err => {
+ if(err) {
+ process.exitCode = ExitCodes.ERROR;
+ console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`);
+ }
+ }
+ );
+}
+
+function handleMessageBaseCommand() {
+
+ function errUsage() {
+ return printUsageAndSetExitCode(
+ getHelpFor('MessageBase'),
+ ExitCodes.ERROR
+ );
+ }
+
+ if(true === argv.help) {
+ return errUsage();
+ }
+
+ const action = argv._[1];
+
+ return({
+ areafix : areaFix,
+ }[action] || errUsage)();
+}
\ No newline at end of file
diff --git a/core/predefined_mci.js b/core/predefined_mci.js
index afebd24c..7fe921b3 100644
--- a/core/predefined_mci.js
+++ b/core/predefined_mci.js
@@ -45,11 +45,11 @@ function getUserRatio(client, propA, propB) {
}
function userStatAsString(client, statName, defaultValue) {
- return (StatLog.getUserStat(client.user, statName) || defaultValue).toString();
+ return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString();
}
function sysStatAsString(statName, defaultValue) {
- return (StatLog.getSystemStat(statName) || defaultValue).toString();
+ return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString();
}
const PREDEFINED_MCI_GENERATORS = {
@@ -177,7 +177,7 @@ const PREDEFINED_MCI_GENERATORS = {
AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); },
- TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); },
+ TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); },
RR : function randomRumor() {
// start the process of picking another random one
@@ -201,12 +201,20 @@ const PREDEFINED_MCI_GENERATORS = {
const byteSize = StatLog.getSystemStatNum('ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
+ TF : function totalFilesOnSystem() {
+ const areaStats = StatLog.getSystemStat('file_base_area_stats');
+ return _.get(areaStats, 'totalFiles', 0).toLocaleString();
+ },
+ TB : function totalBytesOnSystem() {
+ const areaStats = StatLog.getSystemStat('file_base_area_stats');
+ const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0));
+ return formatByteSize(totalBytes, true); // true=withAbbr
+ },
// :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)
diff --git a/mods/rumorz.js b/core/rumorz.js
similarity index 93%
rename from mods/rumorz.js
rename to core/rumorz.js
index 20aace03..b83853f0 100644
--- a/mods/rumorz.js
+++ b/core/rumorz.js
@@ -2,13 +2,13 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const ViewController = require('../core/view_controller.js').ViewController;
-const theme = require('../core/theme.js');
-const resetScreen = require('../core/ansi_term.js').resetScreen;
-const StatLog = require('../core/stat_log.js');
-const renderStringLength = require('../core/string_util.js').renderStringLength;
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const theme = require('./theme.js');
+const resetScreen = require('./ansi_term.js').resetScreen;
+const StatLog = require('./stat_log.js');
+const renderStringLength = require('./string_util.js').renderStringLength;
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js
index 57ff0525..8da71d02 100644
--- a/core/scanner_tossers/ftn_bso.js
+++ b/core/scanner_tossers/ftn_bso.js
@@ -20,6 +20,7 @@ const getDescFromFileName = require('../file_base_area.js').getDescFromFileNa
const copyFileWithCollisionHandling = require('../file_util.js').copyFileWithCollisionHandling;
const getAreaStorageDirectoryByTag = require('../file_base_area.js').getAreaStorageDirectoryByTag;
const isValidStorageTag = require('../file_base_area.js').isValidStorageTag;
+const User = require('../user.js');
// deps
const moment = require('moment');
@@ -43,14 +44,10 @@ exports.moduleInfo = {
/*
:TODO:
- * Support (approx) max bundle size
- * Support NetMail
- * NetMail needs explicit isNetMail() check
- * NetMail filename / location / etc. is still unknown - need to post on groups & get real answers
+ * Support (approx) max bundle size
* Validate packet passwords!!!!
=> secure vs insecure landing areas
-
-*/
+*/
exports.getModule = FTNMessageScanTossModule;
@@ -58,32 +55,31 @@ const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:|@immediate)([^\0]+)?$/;
function FTNMessageScanTossModule() {
MessageScanTossModule.call(this);
-
- let self = this;
+
+ const self = this;
this.archUtil = ArchiveUtil.getInstance();
if(_.has(Config, 'scannerTossers.ftn_bso')) {
- this.moduleConfig = Config.scannerTossers.ftn_bso;
+ this.moduleConfig = Config.scannerTossers.ftn_bso;
}
-
+
this.getDefaultNetworkName = function() {
if(this.moduleConfig.defaultNetwork) {
return this.moduleConfig.defaultNetwork.toLowerCase();
}
-
+
const networkNames = Object.keys(Config.messageNetworks.ftn.networks);
if(1 === networkNames.length) {
return networkNames[0].toLowerCase();
}
};
-
-
+
this.getDefaultZone = function(networkName) {
if(_.isNumber(Config.messageNetworks.ftn.networks[networkName].defaultZone)) {
return Config.messageNetworks.ftn.networks[networkName].defaultZone;
}
-
+
// non-explicit: default to local address zone
const networkLocalAddress = Config.messageNetworks.ftn.networks[networkName].localAddress;
if(networkLocalAddress) {
@@ -91,45 +87,45 @@ function FTNMessageScanTossModule() {
return addr.zone;
}
};
-
+
/*
this.isDefaultDomainZone = function(networkName, address) {
- const defaultNetworkName = this.getDefaultNetworkName();
+ const defaultNetworkName = this.getDefaultNetworkName();
return(networkName === defaultNetworkName && address.zone === this.moduleConfig.defaultZone);
};
*/
-
+
this.getNetworkNameByAddress = function(remoteAddress) {
return _.findKey(Config.messageNetworks.ftn.networks, network => {
- const localAddress = Address.fromString(network.localAddress);
+ const localAddress = Address.fromString(network.localAddress);
return !_.isUndefined(localAddress) && localAddress.isEqual(remoteAddress);
});
};
-
+
this.getNetworkNameByAddressPattern = function(remoteAddressPattern) {
return _.findKey(Config.messageNetworks.ftn.networks, network => {
- const localAddress = Address.fromString(network.localAddress);
+ const localAddress = Address.fromString(network.localAddress);
return !_.isUndefined(localAddress) && localAddress.isPatternMatch(remoteAddressPattern);
- });
+ });
};
-
+
this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) {
ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper
return _.findKey(Config.messageNetworks.ftn.areas, areaConf => {
return areaConf.tag.toUpperCase() === ftnAreaTag;
});
};
-
+
this.getExportType = function(nodeConfig) {
- return _.isString(nodeConfig.exportType) ? nodeConfig.exportType.toLowerCase() : 'crash';
+ return _.isString(nodeConfig.exportType) ? nodeConfig.exportType.toLowerCase() : 'crash';
};
-
+
/*
this.getSeenByAddresses = function(messageSeenBy) {
if(!_.isArray(messageSeenBy)) {
messageSeenBy = [ messageSeenBy ];
}
-
+
let seenByAddrs = [];
messageSeenBy.forEach(sb => {
seenByAddrs = seenByAddrs.concat(ftnUtil.parseAbbreviatedNetNodeList(sb));
@@ -137,13 +133,13 @@ function FTNMessageScanTossModule() {
return seenByAddrs;
};
*/
-
+
this.messageHasValidMSGID = function(msg) {
- return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0;
+ return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0;
};
-
+
/*
- this.getOutgoingPacketDir = function(networkName, destAddress) {
+ this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) {
let dir = this.moduleConfig.paths.outbound;
if(!this.isDefaultDomainZone(networkName, destAddress)) {
const hexZone = `000${destAddress.zone.toString(16)}`.substr(-3);
@@ -152,31 +148,31 @@ function FTNMessageScanTossModule() {
return dir;
};
*/
-
- this.getOutgoingPacketDir = function(networkName, destAddress) {
+
+ this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) {
networkName = networkName.toLowerCase();
-
+
let dir = this.moduleConfig.paths.outbound;
-
- const defaultNetworkName = this.getDefaultNetworkName();
+
+ const defaultNetworkName = this.getDefaultNetworkName();
const defaultZone = this.getDefaultZone(networkName);
-
+
let zoneExt;
if(defaultZone !== destAddress.zone) {
zoneExt = '.' + `000${destAddress.zone.toString(16)}`.substr(-3);
} else {
zoneExt = '';
}
-
+
if(defaultNetworkName === networkName) {
dir = paths.join(dir, `outbound${zoneExt}`);
} else {
dir = paths.join(dir, `${networkName}${zoneExt}`);
}
-
+
return dir;
};
-
+
this.getOutgoingPacketFileName = function(basePath, messageId, isTemp, fileCase) {
//
// Generating an outgoing packet file name comes with a few issues:
@@ -189,15 +185,15 @@ function FTNMessageScanTossModule() {
// There are a lot of systems in use here for the name:
// * HEX CRC16/32 of data
// * HEX UNIX timestamp
- // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second)
+ // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second)
// See https://groups.google.com/forum/#!searchin/alt.bbs.mystic/netmail$20filename/alt.bbs.mystic/m1xLnY8i1pU/YnG2excdl6MJ
// * SBBSEcho uses DDHHMMSS - see https://github.com/ftnapps/pkg-sbbs/blob/master/docs/fidonet.txt
// * We already have a system for 8-character serial number gernation that is
// used for e.g. in FTS-0009.001 MSGIDs... let's use that!
- //
+ //
const name = ftnUtil.getMessageSerialNumber(messageId);
const ext = (true === isTemp) ? 'pk_' : 'pkt';
-
+
let fileName = `${name}.${ext}`;
if('upper' === fileCase) {
fileName = fileName.toUpperCase();
@@ -205,10 +201,10 @@ function FTNMessageScanTossModule() {
return paths.join(basePath, fileName);
};
-
+
this.getOutgoingFlowFileExtension = function(destAddress, flowType, exportType, fileCase) {
let ext;
-
+
switch(flowType) {
case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break;
case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break;
@@ -220,20 +216,20 @@ function FTNMessageScanTossModule() {
if('upper' === fileCase) {
ext = ext.toUpperCase();
}
-
- return ext;
+
+ return ext;
};
this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) {
- let basename;
-
+ let basename;
+
const ext = self.getOutgoingFlowFileExtension(
- destAddress,
- flowType,
- exportType,
+ destAddress,
+ flowType,
+ exportType,
fileCase
);
-
+
if(destAddress.point) {
} else {
@@ -242,32 +238,32 @@ function FTNMessageScanTossModule() {
// node. This seems to match what Mystic does
//
basename =
- `0000${destAddress.net.toString(16)}`.substr(-4) +
- `0000${destAddress.node.toString(16)}`.substr(-4);
+ `0000${destAddress.net.toString(16)}`.substr(-4) +
+ `0000${destAddress.node.toString(16)}`.substr(-4);
}
if('upper' === fileCase) {
basename = basename.toUpperCase();
}
-
+
return paths.join(basePath, `${basename}.${ext}`);
};
-
+
this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) {
const appendLines = fileRefs.reduce( (content, ref) => {
return content + `${directive}${ref}\n`;
}, '');
-
+
fs.appendFile(filePath, appendLines, err => {
cb(err);
});
};
-
+
this.getOutgoingBundleFileName = function(basePath, sourceAddress, destAddress, cb) {
//
// Base filename is constructed as such:
- // * If this |destAddress| is *not* a point address, we use NNNNnnnn where
- // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded
+ // * If this |destAddress| is *not* a point address, we use NNNNnnnn where
+ // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded
// hex of dest node - source node.
// * If |destAddress| is a point, NNNN becomes 0000 and nnnn becomes 'p' +
// 3 digit 0 padded hex point
@@ -279,19 +275,19 @@ function FTNMessageScanTossModule() {
const pointHex = `000${destAddress.point}`.substr(-3);
basename = `0000p${pointHex}`;
} else {
- basename =
- `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) +
- `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4);
+ basename =
+ `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) +
+ `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4);
}
-
+
//
// We need to now find the first entry that does not exist starting
// with dd0 to ddz
//
- const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split('');
+ const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split('');
let fileName = `${basename}.${moment().format('dd').toLowerCase()}`;
async.detectSeries(EXT_SUFFIXES, (suffix, callback) => {
- const checkFileName = fileName + suffix;
+ const checkFileName = fileName + suffix;
fs.stat(paths.join(basePath, checkFileName), err => {
callback(null, (err && 'ENOENT' === err.code) ? true : false);
});
@@ -299,42 +295,68 @@ function FTNMessageScanTossModule() {
if(finalSuffix) {
return cb(null, paths.join(basePath, fileName + finalSuffix));
}
-
- return cb(new Error('Could not acquire a bundle filename!'));
+
+ return cb(new Error('Could not acquire a bundle filename!'));
});
};
-
+
this.prepareMessage = function(message, options) {
//
// Set various FTN kludges/etc.
//
+ const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version
+
message.meta.FtnProperty = message.meta.FtnProperty || {};
message.meta.FtnKludge = message.meta.FtnKludge || {};
-
- message.meta.FtnProperty.ftn_orig_node = options.network.localAddress.node;
- message.meta.FtnProperty.ftn_dest_node = options.destAddress.node;
- message.meta.FtnProperty.ftn_orig_network = options.network.localAddress.net;
- message.meta.FtnProperty.ftn_dest_network = options.destAddress.net;
- message.meta.FtnProperty.ftn_cost = 0;
- message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine();
- // :TODO: Need an explicit isNetMail() check
- let ftnAttribute =
- ftnMailPacket.Packet.Attribute.Local; // message from our system
-
- if(message.isPrivate()) {
+ message.meta.FtnProperty.ftn_orig_node = localAddress.node;
+ message.meta.FtnProperty.ftn_orig_network = localAddress.net;
+ message.meta.FtnProperty.ftn_cost = 0;
+
+ // tear line and origin can both go in EchoMail & NetMail
+ message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine();
+ message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(localAddress);
+
+ let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system
+
+ if(self.isNetMailMessage(message)) {
+ // These should be set for Private/NetMail already
+ assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_node)));
+ assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_network)));
+
ftnAttribute |= ftnMailPacket.Packet.Attribute.Private;
-
+
//
// NetMail messages need a FRL-1005.001 "Via" line
// http://ftsc.org/docs/frl-1005.001
//
+ // :TODO: We need to do this when FORWARDING NetMail
+ /*
if(_.isString(message.meta.FtnKludge.Via)) {
message.meta.FtnKludge.Via = [ message.meta.FtnKludge.Via ];
}
message.meta.FtnKludge.Via = message.meta.FtnKludge.Via || [];
message.meta.FtnKludge.Via.push(ftnUtil.getVia(options.network.localAddress));
+ */
+
+ //
+ // We need to set INTL, and possibly FMPT and/or TOPT
+ // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac
+ //
+ message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, localAddress);
+
+ if(_.isNumber(localAddress.point) && localAddress.point > 0) {
+ message.meta.FtnKludge.FMPT = localAddress.point;
+ }
+
+ if(_.isNumber(options.destAddress.point) && options.destAddress.point > 0) {
+ message.meta.FtnKludge.TOPT = options.destAddress.point;
+ }
} else {
+ // We need to set some destination info for EchoMail
+ message.meta.FtnProperty.ftn_dest_node = options.destAddress.node;
+ message.meta.FtnProperty.ftn_dest_network = options.destAddress.net;
+
//
// Set appropriate attribute flag for export type
//
@@ -343,52 +365,54 @@ function FTNMessageScanTossModule() {
case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break;
// :TODO: Others?
}
-
+
//
// EchoMail requires some additional properties & kludges
- //
- message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(options.network.localAddress);
- message.meta.FtnProperty.ftn_area = Config.messageNetworks.ftn.areas[message.areaTag].tag;
-
+ //
+ message.meta.FtnProperty.ftn_area = Config.messageNetworks.ftn.areas[message.areaTag].tag;
+
//
// When exporting messages, we should create/update SEEN-BY
// with remote address(s) we are exporting to.
//
- const seenByAdditions =
- [ `${options.network.localAddress.net}/${options.network.localAddress.node}` ].concat(Config.messageNetworks.ftn.areas[message.areaTag].uplinks);
- message.meta.FtnProperty.ftn_seen_by =
+ const seenByAdditions =
+ [ `${localAddress.net}/${localAddress.node}` ].concat(Config.messageNetworks.ftn.areas[message.areaTag].uplinks);
+ message.meta.FtnProperty.ftn_seen_by =
ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions);
//
// And create/update PATH for ourself
//
- message.meta.FtnKludge.PATH =
- ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, options.network.localAddress);
+ message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, localAddress);
}
-
+
message.meta.FtnProperty.ftn_attr_flags = ftnAttribute;
-
+
//
// Additional kludges
//
// Check for existence of MSGID as we may already have stored it from a previous
// export that failed to finish
- //
+ //
if(!message.meta.FtnKludge.MSGID) {
- message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier(message, options.network.localAddress);
+ message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier(
+ message,
+ localAddress,
+ message.isPrivate() // true = isNetMail
+ );
}
-
+
message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset();
-
+
//
// According to FSC-0046:
- //
+ //
// "When a Conference Mail processor adds a TID to a message, it may not
// add a PID. An existing TID should, however, be replaced. TIDs follow
// the same format used for PIDs, as explained above."
//
- message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier();
-
+ message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier();
+
//
// Determine CHRS and actual internal encoding name. If the message has an
// explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set.
@@ -403,77 +427,76 @@ function FTNMessageScanTossModule() {
encoding = encFromChars;
}
}
-
+
//
// Ensure we ended up with something useable. If not, back to utf8!
//
if(!iconv.encodingExists(encoding)) {
Log.debug( { encoding : encoding }, 'Unknown encoding. Falling back to utf8');
encoding = 'utf8';
- }
-
+ }
+
options.encoding = encoding; // save for later
message.meta.FtnKludge.CHRS = ftnUtil.getCharacterSetIdentifierByEncoding(encoding);
- // :TODO: FLAGS kludge?
};
-
+
this.setReplyKludgeFromReplyToMsgId = function(message, cb) {
//
// Look up MSGID kludge for |message.replyToMsgId|, if any.
// If found, we can create a REPLY kludge with the previously
// discovered MSGID.
//
-
+
if(0 === message.replyToMsgId) {
return cb(null); // nothing to do
}
-
- Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => {
+
+ Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => {
if(!err) {
- assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')');
+ assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')');
// got a MSGID - create a REPLY
message.meta.FtnKludge.REPLY = msgIdVal;
}
-
+
cb(null); // this method always passes
- });
+ });
};
-
+
// check paths, Addresses, etc.
this.isAreaConfigValid = function(areaConfig) {
if(!areaConfig || !_.isString(areaConfig.tag) || !_.isString(areaConfig.network)) {
return false;
}
-
+
if(_.isString(areaConfig.uplinks)) {
areaConfig.uplinks = areaConfig.uplinks.split(' ');
}
-
+
return (_.isArray(areaConfig.uplinks));
};
-
-
+
+
this.hasValidConfiguration = function() {
if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config, 'messageNetworks.ftn.areas')) {
return false;
}
-
+
// :TODO: need to check more!
-
+
return true;
};
-
+
this.parseScheduleString = function(schedStr) {
if(!schedStr) {
return; // nothing to parse!
}
-
+
let schedule = {};
-
+
const m = SCHEDULE_REGEXP.exec(schedStr);
if(m) {
schedStr = schedStr.substr(0, m.index).trim();
-
+
if('@watch:' === m[1]) {
schedule.watchFile = m[2];
} else if('@immediate' === m[1]) {
@@ -485,46 +508,111 @@ function FTNMessageScanTossModule() {
const sched = later.parse.text(schedStr);
if(-1 === sched.error) {
schedule.sched = sched;
- }
+ }
}
-
+
// return undefined if we couldn't parse out anything useful
if(!_.isEmpty(schedule)) {
return schedule;
- }
+ }
};
-
+
this.getAreaLastScanId = function(areaTag, cb) {
- const sql =
+ const sql =
`SELECT area_tag, message_id
FROM message_area_last_scan
WHERE scan_toss = "ftn_bso" AND area_tag = ?
LIMIT 1;`;
-
+
msgDb.get(sql, [ areaTag ], (err, row) => {
- cb(err, row ? row.message_id : 0);
+ return cb(err, row ? row.message_id : 0);
});
};
-
+
this.setAreaLastScanId = function(areaTag, lastScanId, cb) {
const sql =
`REPLACE INTO message_area_last_scan (scan_toss, area_tag, message_id)
VALUES ("ftn_bso", ?, ?);`;
-
+
msgDb.run(sql, [ areaTag, lastScanId ], err => {
- cb(err);
+ return cb(err);
});
};
-
+
+ this.getNodeConfigByAddress = function(addr) {
+ addr = _.isString(addr) ? Address.fromString(addr) : addr;
+
+ // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy
+ return _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => {
+ return addr.isPatternMatch(nodeAddrWildcard);
+ });
+ };
+
+ // :TODO: deprecate this in favor of getNodeConfigByAddress()
this.getNodeConfigKeyByAddress = function(uplink) {
- // :TODO: sort by least # of '*' & take top?
const nodeKey = _.filter(Object.keys(this.moduleConfig.nodes), addr => {
return Address.fromString(addr).isPatternMatch(uplink);
})[0];
return nodeKey;
};
-
+
+ this.exportNetMailMessagePacket = function(message, exportOpts, cb) {
+ //
+ // For NetMail, we always create a *single* packet per message.
+ //
+ async.series(
+ [
+ function generalPrep(callback) {
+ self.prepareMessage(message, exportOpts);
+
+ return self.setReplyKludgeFromReplyToMsgId(message, callback);
+ },
+ function createPacket(callback) {
+ const packet = new ftnMailPacket.Packet();
+
+ const packetHeader = new ftnMailPacket.PacketHeader(
+ exportOpts.network.localAddress,
+ exportOpts.destAddress,
+ exportOpts.nodeConfig.packetType
+ );
+
+ packetHeader.password = exportOpts.nodeConfig.packetPassword || '';
+
+ // use current message ID for filename seed
+ exportOpts.pktFileName = self.getOutgoingPacketFileName(
+ self.exportTempDir,
+ message.messageId,
+ false, // createTempPacket=false
+ exportOpts.fileCase
+ );
+
+ const ws = fs.createWriteStream(exportOpts.pktFileName);
+
+ packet.writeHeader(ws, packetHeader);
+
+ packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => {
+ if(err) {
+ return callback(err);
+ }
+
+ ws.write(msgBuf);
+
+ packet.writeTerminator(ws);
+
+ ws.end();
+ ws.once('finish', () => {
+ return callback(null);
+ });
+ });
+ }
+ ],
+ err => {
+ return cb(err);
+ }
+ );
+ };
+
this.exportMessagesByUuid = function(messageUuids, exportOpts, cb) {
//
// This method has a lot of madness going on:
@@ -538,7 +626,7 @@ function FTNMessageScanTossModule() {
let ws;
let remainMessageBuf;
let remainMessageId;
- const createTempPacket = !_.isString(exportOpts.nodeConfig.archiveType) || 0 === exportOpts.nodeConfig.archiveType.length;
+ const createTempPacket = !_.isString(exportOpts.nodeConfig.archiveType) || 0 === exportOpts.nodeConfig.archiveType.length;
function finalizePacket(cb) {
packet.writeTerminator(ws);
@@ -547,10 +635,10 @@ function FTNMessageScanTossModule() {
return cb(null);
});
}
-
+
async.each(messageUuids, (msgUuid, nextUuid) => {
let message = new Message();
-
+
async.series(
[
function finalizePrevious(callback) {
@@ -565,47 +653,47 @@ function FTNMessageScanTossModule() {
if(err) {
return callback(err);
}
-
+
// General preperation
self.prepareMessage(message, exportOpts);
-
+
self.setReplyKludgeFromReplyToMsgId(message, err => {
callback(err);
});
});
},
- function createNewPacket(callback) {
- if(currPacketSize >= self.moduleConfig.packetTargetByteSize) {
+ function createNewPacket(callback) {
+ if(currPacketSize >= self.moduleConfig.packetTargetByteSize) {
packet = new ftnMailPacket.Packet();
-
+
const packetHeader = new ftnMailPacket.PacketHeader(
exportOpts.network.localAddress,
exportOpts.destAddress,
exportOpts.nodeConfig.packetType);
packetHeader.password = exportOpts.nodeConfig.packetPassword || '';
-
+
// use current message ID for filename seed
const pktFileName = self.getOutgoingPacketFileName(
- self.exportTempDir,
- message.messageId,
+ self.exportTempDir,
+ message.messageId,
createTempPacket,
exportOpts.fileCase
);
exportedFiles.push(pktFileName);
-
+
ws = fs.createWriteStream(pktFileName);
-
+
currPacketSize = packet.writeHeader(ws, packetHeader);
-
+
if(remainMessageBuf) {
currPacketSize += packet.writeMessageEntry(ws, remainMessageBuf);
remainMessageBuf = null;
- }
+ }
}
-
- callback(null);
+
+ callback(null);
},
function appendMessage(callback) {
packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => {
@@ -614,16 +702,16 @@ function FTNMessageScanTossModule() {
}
currPacketSize += msgBuf.length;
-
+
if(currPacketSize >= self.moduleConfig.packetTargetByteSize) {
- remainMessageBuf = msgBuf; // save for next packet
- remainMessageId = message.messageId;
+ remainMessageBuf = msgBuf; // save for next packet
+ remainMessageId = message.messageId;
} else {
ws.write(msgBuf);
}
-
+
return callback(null);
- });
+ });
},
function storeStateFlags0Meta(callback) {
message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), err => {
@@ -641,9 +729,9 @@ function FTNMessageScanTossModule() {
});
} else {
callback(null);
- }
+ }
}
- ],
+ ],
err => {
nextUuid(err);
}
@@ -665,26 +753,26 @@ function FTNMessageScanTossModule() {
if(remainMessageBuf) {
// :TODO: DRY this with the code above -- they are basically identical
packet = new ftnMailPacket.Packet();
-
+
const packetHeader = new ftnMailPacket.PacketHeader(
exportOpts.network.localAddress,
exportOpts.destAddress,
exportOpts.nodeConfig.packetType);
packetHeader.password = exportOpts.nodeConfig.packetPassword || '';
-
+
// use current message ID for filename seed
const pktFileName = self.getOutgoingPacketFileName(
- self.exportTempDir,
- remainMessageId,
+ self.exportTempDir,
+ remainMessageId,
createTempPacket,
exportOpts.filleCase
);
exportedFiles.push(pktFileName);
-
+
ws = fs.createWriteStream(pktFileName);
-
+
packet.writeHeader(ws, packetHeader);
ws.write(remainMessageBuf);
return finalizePacket(callback);
@@ -696,18 +784,176 @@ function FTNMessageScanTossModule() {
err => {
cb(err, exportedFiles);
}
- );
- }
+ );
+ }
});
};
-
- this.exportMessagesToUplinks = function(messageUuids, areaConfig, cb) {
+
+ this.getNetMailRoute = function(dstAddr) {
+ //
+ // messageNetworks.ftn.netMail.routes{} full|wildcard -> full adddress lookup
+ //
+ const routes = _.get(Config, 'messageNetworks.ftn.netMail.routes');
+ if(!routes) {
+ return;
+ }
+
+ return _.find(routes, (route, addrWildcard) => {
+ return dstAddr.isPatternMatch(addrWildcard);
+ });
+
+ /*
+ const route = _.find(routes, (route, addrWildcard) => {
+ return dstAddr.isPatternMatch(addrWildcard);
+ });
+
+ if(route && route.address) {
+ return Address.fromString(route.address);
+ }
+ */
+ };
+
+ this.getAcceptableNetMailNetworkInfoFromAddress = function(dstAddr, cb) {
+ //
+ // Attempt to find an acceptable network configuration using the following
+ // lookup order (most to least explicit config):
+ //
+ // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config
+ // - Where we send may not be where dstAddress is (it's routed!)
+ // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config
+ // - Where we send is direct to dstAddr
+ //
+ // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address
+ // falling back to Config.scannerTossers.ftn_bso.defaultNetwork
+ //
+ const route = this.getNetMailRoute(dstAddr);
+
+ let routeAddress;
+ let networkName;
+ if(route) {
+ routeAddress = Address.fromString(route.address);
+ networkName = route.network;
+ } else {
+ routeAddress = dstAddr;
+ }
+
+ networkName = networkName ||
+ this.getNetworkNameByAddressPattern(`${routeAddress.zone}:${routeAddress.net}/*`) ||
+ Config.scannerTossers.ftn_bso.defaultNetwork
+ ;
+
+ const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => {
+ return routeAddress.isPatternMatch(nodeAddrWildcard);
+ }) || {
+ packetType : '2+',
+ encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding,
+ };
+
+ return cb(
+ config ? null : Errors.DoesNotExist(`No configuration found for ${dstAddr.toString()}`),
+ config, routeAddress, networkName
+ );
+ };
+
+ this.exportNetMailMessagesToUplinks = function(messagesOrMessageUuids, cb) {
+ // for each message/UUID, find where to send the thing
+ async.each(messagesOrMessageUuids, (msgOrUuid, nextMessageOrUuid) => {
+
+ const exportOpts = {};
+ const message = new Message();
+
+ async.series(
+ [
+ function loadMessage(callback) {
+ if(_.isString(msgOrUuid)) {
+ message.load( { uuid : msgOrUuid }, err => {
+ return callback(err, message);
+ });
+ } else {
+ return callback(null, msgOrUuid);
+ }
+ },
+ function discoverUplink(callback) {
+ const dstAddr = new Address(message.meta.System[Message.SystemMetaNames.RemoteToUser]);
+
+ return self.getAcceptableNetMailNetworkInfoFromAddress(dstAddr, (err, config, routeAddress, networkName) => {
+ if(err) {
+ return callback(err);
+ }
+
+ exportOpts.nodeConfig = config;
+ exportOpts.destAddress = routeAddress;
+ exportOpts.fileCase = config.fileCase || 'lower';
+ exportOpts.network = Config.messageNetworks.ftn.networks[networkName];
+ exportOpts.networkName = networkName;
+ exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress);
+ exportOpts.exportType = self.getExportType(config);
+
+ if(!exportOpts.network) {
+ return callback(Errors.DoesNotExist(`No configuration found for network ${networkName}`));
+ }
+
+ return callback(null);
+ });
+ },
+ function createOutgoingDir(callback) {
+ // ensure outgoing NetMail directory exists
+ return fse.mkdirs(exportOpts.outgoingDir, callback);
+ },
+ function exportPacket(callback) {
+ return self.exportNetMailMessagePacket(message, exportOpts, callback);
+ },
+ function moveToOutgoing(callback) {
+ const newExt = exportOpts.fileCase === 'lower' ? '.pkt' : '.PKT';
+ exportOpts.exportedToPath = paths.join(
+ exportOpts.outgoingDir,
+ `${paths.basename(exportOpts.pktFileName, paths.extname(exportOpts.pktFileName))}${newExt}`
+ );
+
+ return fse.move(exportOpts.pktFileName, exportOpts.exportedToPath, callback);
+ },
+ function prepareFloFile(callback) {
+ const flowFilePath = self.getOutgoingFlowFileName(
+ exportOpts.outgoingDir,
+ exportOpts.destAddress,
+ 'ref',
+ exportOpts.exportType,
+ exportOpts.fileCase
+ );
+
+ return self.flowFileAppendRefs(flowFilePath, [ exportOpts.exportedToPath ], '^', callback);
+ },
+ function storeStateFlags0Meta(callback) {
+ return message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), callback);
+ },
+ function storeMsgIdMeta(callback) {
+ // Store meta as if we had imported this message -- for later reference
+ if(message.meta.FtnKludge.MSGID) {
+ return message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, callback);
+ }
+
+ return callback(null);
+ }
+ ],
+ err => {
+ if(err) {
+ Log.warn( { error :err.message }, 'Error exporting message' );
+ }
+ return nextMessageOrUuid(null);
+ }
+ );
+ }, err => {
+ return cb(err);
+ });
+ };
+
+ this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) {
async.each(areaConfig.uplinks, (uplink, nextUplink) => {
const nodeConfigKey = self.getNodeConfigKeyByAddress(uplink);
if(!nodeConfigKey) {
return nextUplink();
}
-
+
const exportOpts = {
nodeConfig : self.moduleConfig.nodes[nodeConfigKey],
network : Config.messageNetworks.ftn.networks[areaConfig.network],
@@ -715,14 +961,14 @@ function FTNMessageScanTossModule() {
networkName : areaConfig.network,
fileCase : self.moduleConfig.nodes[nodeConfigKey].fileCase || 'lower',
};
-
+
if(_.isString(exportOpts.network.localAddress)) {
- exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress);
+ exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress);
}
-
- const outgoingDir = self.getOutgoingPacketDir(exportOpts.networkName, exportOpts.destAddress);
+
+ const outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress);
const exportType = self.getExportType(exportOpts.nodeConfig);
-
+
async.waterfall(
[
function createOutgoingDir(callback) {
@@ -746,17 +992,17 @@ function FTNMessageScanTossModule() {
if(err) {
return callback(err);
}
-
+
// adjust back to temp path
const tempBundlePath = paths.join(self.exportTempDir, paths.basename(bundlePath));
-
+
self.archUtil.compressTo(
- exportOpts.nodeConfig.archiveType,
+ exportOpts.nodeConfig.archiveType,
tempBundlePath,
exportedFileNames, err => {
callback(err, [ tempBundlePath ] );
}
- );
+ );
});
} else {
callback(null, exportedFileNames);
@@ -769,18 +1015,18 @@ function FTNMessageScanTossModule() {
//
// For a given temporary .pk_ file, we need to move it to the outoing
// directory with the appropriate BSO style filename.
- //
+ //
const newExt = self.getOutgoingFlowFileExtension(
exportOpts.destAddress,
- 'mail',
+ 'mail',
exportType,
exportOpts.fileCase
);
-
+
const newPath = paths.join(
- outgoingDir,
+ outgoingDir,
`${paths.basename(oldPath, ext)}${newExt}`);
-
+
fse.move(oldPath, newPath, nextFile);
} else {
const newPath = paths.join(outgoingDir, paths.basename(oldPath));
@@ -789,25 +1035,25 @@ function FTNMessageScanTossModule() {
Log.warn(
{ oldPath : oldPath, newPath : newPath, error : err.toString() },
'Failed moving temporary bundle file!');
-
+
return nextFile();
}
-
+
//
// For bundles, we need to append to the appropriate flow file
//
const flowFilePath = self.getOutgoingFlowFileName(
- outgoingDir,
+ outgoingDir,
exportOpts.destAddress,
'ref',
exportType,
exportOpts.fileCase
);
-
+
// directive of '^' = delete file after transfer
self.flowFileAppendRefs(flowFilePath, [ newPath ], '^', err => {
if(err) {
- Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!');
+ Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!');
}
nextFile();
});
@@ -823,10 +1069,10 @@ function FTNMessageScanTossModule() {
}
nextUplink();
}
- );
+ );
}, cb); // complete
};
-
+
this.setReplyToMsgIdFtnReplyKludge = function(message, cb) {
//
// Given a FTN REPLY kludge, set |message.replyToMsgId|, if possible,
@@ -838,12 +1084,12 @@ function FTNMessageScanTossModule() {
// nothing to do
return cb();
}
-
+
Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => {
if(msgIds && msgIds.length > 0) {
// expect a single match, but dupe checking is not perfect - warn otherwise
if(1 === msgIds.length) {
- message.replyToMsgId = msgIds[0];
+ message.replyToMsgId = msgIds[0];
} else {
Log.warn( { msgIds : msgIds, replyKludge : message.meta.FtnKludge.REPLY }, 'Found 2:n MSGIDs matching REPLY kludge!');
}
@@ -851,15 +1097,56 @@ function FTNMessageScanTossModule() {
cb();
});
};
-
- this.importEchoMailToArea = function(localAreaTag, header, message, cb) {
+
+ this.getLocalUserNameFromAlias = function(lookup) {
+ lookup = lookup.toLowerCase();
+
+ const aliases = _.get(Config, 'messageNetworks.ftn.netMail.aliases');
+ if(!aliases) {
+ return lookup; // keep orig
+ }
+
+ const alias = _.find(aliases, (localName, alias) => {
+ return alias.toLowerCase() === lookup;
+ });
+
+ return alias || lookup;
+ };
+
+ this.getAddressesFromNetMailMessage = function(message) {
+ const intlKludge = _.get(message, 'meta.FtnKludge.INTL');
+
+ if(!intlKludge) {
+ return {};
+ }
+
+ let [ to, from ] = intlKludge.split(' ');
+ if(!to || !from) {
+ return {};
+ }
+
+ const fromPoint = _.get(message, 'meta.FtnKludge.FMPT');
+ const toPoint = _.get(message, 'meta.FtnKludge.TOPT');
+
+ if(fromPoint) {
+ from += `.${fromPoint}`;
+ }
+
+ if(toPoint) {
+ to += `.${toPoint}`;
+ }
+
+ return { to : Address.fromString(to), from : Address.fromString(from) };
+ };
+
+ this.importMailToArea = function(config, header, message, cb) {
async.series(
[
- function validateDestinationAddress(callback) {
- const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`;
+ function validateDestinationAddress(callback) {
+ const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`;
const localNetworkName = self.getNetworkNameByAddressPattern(localNetworkPattern);
-
- callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us'));
+
+ return callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us'));
},
function checkForDupeMSGID(callback) {
//
@@ -880,37 +1167,77 @@ function FTNMessageScanTossModule() {
});
},
function basicSetup(callback) {
- message.areaTag = localAreaTag;
-
+ message.areaTag = config.localAreaTag;
+
+ // indicate this was imported from FTN
+ message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.FTN;
+
//
// If we *allow* dupes (disabled by default), then just generate
// a random UUID. Otherwise, don't assign the UUID just yet. It will be
// generated at persist() time and should be consistent across import/exports
//
- if(Config.messageNetworks.ftn.areas[localAreaTag].allowDupes) {
+ if(true === _.get(Config, [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) {
// just generate a UUID & therefor always allow for dupes
message.uuid = uuidV4();
}
-
- callback(null);
+
+ return callback(null);
},
function setReplyToMessageId(callback) {
self.setReplyToMsgIdFtnReplyKludge(message, () => {
- callback(null);
+ return callback(null);
+ });
+ },
+ function setupPrivateMessage(callback) {
+ //
+ // If this is a private message (e.g. NetMail) we set the local user ID
+ //
+ if(Message.WellKnownAreaTags.Private !== config.localAreaTag) {
+ return callback(null);
+ }
+
+ //
+ // Create a meta value for the *remote* from user. In the case here with FTN,
+ // their fully qualified FTN from address
+ //
+ const { from } = self.getAddressesFromNetMailMessage(message);
+
+ if(!from) {
+ return callback(Errors.Invalid('Cannot import FTN NetMail without valid INTL line'));
+ }
+
+ message.meta.System[Message.SystemMetaNames.RemoteFromUser] = from.toString();
+
+ const lookupName = self.getLocalUserNameFromAlias(message.toUserName);
+
+ User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => {
+ if(err) {
+ return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`));
+ }
+
+ // we do this after such that error cases can be preseved above
+ if(lookupName !== message.toUserName) {
+ message.toUserName = localUserName;
+ }
+
+ // set the meta information - used elsehwere for retrieval
+ message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId;
+ return callback(null);
});
},
function persistImport(callback) {
// mark as imported
message.meta.System.state_flags0 = Message.StateFlags0.Imported.toString();
-
+
// save to disc
message.persist(err => {
- callback(err);
+ return callback(err);
});
}
- ],
+ ],
err => {
- cb(err);
+ cb(err);
}
);
};
@@ -924,94 +1251,102 @@ function FTNMessageScanTossModule() {
message.message += `${message.meta.FtnProperty.ftn_origin}\r\n`;
}
};
-
+
//
- // Ref. implementations on import:
+ // Ref. implementations on import:
// * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c
// https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/handle.c
//
this.importMessagesFromPacketFile = function(packetPath, password, cb) {
let packetHeader;
-
+
const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later
-
+
let importStats = {
areaSuccess : {}, // areaTag->count
areaFail : {}, // areaTag->count
otherFail : 0,
};
-
+
new ftnMailPacket.Packet(packetOpts).read(packetPath, (entryType, entryData, next) => {
if('header' === entryType) {
packetHeader = entryData;
-
+
const localNetworkName = self.getNetworkNameByAddress(packetHeader.destAddress);
if(!_.isString(localNetworkName)) {
const addrString = new Address(packetHeader.destAddress).toString();
return next(new Error(`No local configuration for packet addressed to ${addrString}`));
} else {
-
- // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?!
+
+ // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?!
return next(null);
}
-
+
} else if('message' === entryType) {
const message = entryData;
const areaTag = message.meta.FtnProperty.ftn_area;
+ let localAreaTag;
if(areaTag) {
- //
- // EchoMail
- //
- const localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag);
- if(localAreaTag) {
- message.uuid = Message.createMessageUUID(
- localAreaTag,
- message.modTimestamp,
- message.subject,
- message.message);
+ localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag);
- self.appendTearAndOrigin(message);
-
- self.importEchoMailToArea(localAreaTag, packetHeader, message, err => {
- if(err) {
- // bump area fail stats
- importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1;
-
- if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) {
- const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A';
- Log.info(
- { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId },
- 'Not importing non-unique message');
-
- return next(null);
- }
- } else {
- // bump area success
- importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1;
- }
-
- return next(err);
- });
- } else {
+ if(!localAreaTag) {
//
// No local area configured for this import
//
- // :TODO: Handle the "catch all" case, if configured
+ // :TODO: Handle the "catch all" area bucket case if configured
Log.warn( { areaTag : areaTag }, 'No local area configured for this packet file!');
-
+
// bump generic failure
importStats.otherFail += 1;
-
+
return next(null);
}
} else {
//
- // NetMail
+ // No area tag: If marked private in attributes, this is a NetMail
//
- Log.warn('NetMail import not yet implemented!');
- return next(null);
+ if(message.meta.FtnProperty.ftn_attr_flags & ftnMailPacket.Packet.Attribute.Private) {
+ localAreaTag = Message.WellKnownAreaTags.Private;
+ } else {
+ Log.warn('Non-private message without area tag');
+ importStats.otherFail += 1;
+ return next(null);
+ }
}
+
+ message.uuid = Message.createMessageUUID(
+ localAreaTag,
+ message.modTimestamp,
+ message.subject,
+ message.message);
+
+ self.appendTearAndOrigin(message);
+
+ const importConfig = {
+ localAreaTag : localAreaTag,
+ };
+
+ self.importMailToArea(importConfig, packetHeader, message, err => {
+ if(err) {
+ // bump area fail stats
+ importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1;
+
+ if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) {
+ const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A';
+ Log.info(
+ { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId },
+ 'Not importing non-unique message');
+
+ return next(null);
+ }
+ } else {
+ // bump area success
+ importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1;
+ }
+
+ return next(err);
+ });
}
}, err => {
//
@@ -1027,7 +1362,7 @@ function FTNMessageScanTossModule() {
} else {
Log.info(finalStats, 'Import complete');
}
-
+
cb(err);
});
};
@@ -1048,7 +1383,7 @@ function FTNMessageScanTossModule() {
if(!_.isString(self.moduleConfig.paths.retain)) {
return cb(null);
}
-
+
archivePath = paths.join(self.moduleConfig.paths.retain, `good-pkt-${ts}--${fn}`);
} else if('good' !== status) {
archivePath = paths.join(self.moduleConfig.paths.reject, `${status}-${type}--${ts}-${fn}`);
@@ -1066,7 +1401,7 @@ function FTNMessageScanTossModule() {
return cb(null); // never fatal
});
};
-
+
this.importPacketFilesFromDirectory = function(importDir, password, cb) {
async.waterfall(
[
@@ -1081,12 +1416,12 @@ function FTNMessageScanTossModule() {
function importPacketFiles(packetFiles, callback) {
let rejects = [];
async.eachSeries(packetFiles, (packetFile, nextFile) => {
- self.importMessagesFromPacketFile(paths.join(importDir, packetFile), '', err => {
+ self.importMessagesFromPacketFile(paths.join(importDir, packetFile), '', err => {
if(err) {
- Log.debug(
- { path : paths.join(importDir, packetFile), error : err.toString() },
+ Log.debug(
+ { path : paths.join(importDir, packetFile), error : err.toString() },
'Failed to import packet file');
-
+
rejects.push(packetFile);
}
nextFile();
@@ -1097,13 +1432,13 @@ function FTNMessageScanTossModule() {
});
},
function handleProcessedFiles(packetFiles, rejects, callback) {
- async.each(packetFiles, (packetFile, nextFile) => {
+ async.each(packetFiles, (packetFile, nextFile) => {
// possibly archive, then remove original
const fullPath = paths.join(importDir, packetFile);
self.maybeArchiveImportFile(
- fullPath,
- 'pkt',
- rejects.includes(packetFile) ? 'reject' : 'good',
+ fullPath,
+ 'pkt',
+ rejects.includes(packetFile) ? 'reject' : 'good',
() => {
fs.unlink(fullPath, () => {
return nextFile(null);
@@ -1120,7 +1455,7 @@ function FTNMessageScanTossModule() {
}
);
};
-
+
this.importFromDirectory = function(inboundType, importDir, cb) {
async.waterfall(
[
@@ -1138,7 +1473,7 @@ function FTNMessageScanTossModule() {
const fext = paths.extname(f);
return bundleRegExp.test(fext);
});
-
+
async.map(files, (file, transform) => {
const fullPath = paths.join(importDir, file);
self.archUtil.detectType(fullPath, (err, archName) => {
@@ -1151,48 +1486,48 @@ function FTNMessageScanTossModule() {
},
function importBundles(bundleFiles, callback) {
let rejects = [];
-
+
async.each(bundleFiles, (bundleFile, nextFile) => {
if(_.isUndefined(bundleFile.archName)) {
- Log.warn(
+ Log.warn(
{ fileName : bundleFile.path },
'Unknown bundle archive type');
-
+
rejects.push(bundleFile.path);
-
+
return nextFile(); // unknown archive type
}
Log.debug( { bundleFile : bundleFile }, 'Processing bundle' );
-
+
self.archUtil.extractTo(
bundleFile.path,
self.importTempDir,
bundleFile.archName,
err => {
- if(err) {
+ if(err) {
Log.warn(
{ path : bundleFile.path, error : err.message },
'Failed to extract bundle');
-
+
rejects.push(bundleFile.path);
}
-
- nextFile();
+
+ nextFile();
}
);
}, err => {
if(err) {
return callback(err);
}
-
+
//
// All extracted - import .pkt's
//
self.importPacketFilesFromDirectory(self.importTempDir, '', err => {
// :TODO: handle |err|
callback(null, bundleFiles, rejects);
- });
+ });
});
},
function handleProcessedBundleFiles(bundleFiles, rejects, callback) {
@@ -1209,7 +1544,7 @@ function FTNMessageScanTossModule() {
return nextFile(null);
});
}
- );
+ );
}, err => {
callback(err);
});
@@ -1225,41 +1560,45 @@ function FTNMessageScanTossModule() {
}
);
};
-
+
this.createTempDirectories = function(cb) {
temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => {
if(err) {
return cb(err);
}
-
+
self.exportTempDir = tempDir;
-
+
temptmp.mkdir( { prefix : 'enigftnimport-' }, (err, tempDir) => {
self.importTempDir = tempDir;
-
+
cb(err);
});
});
};
-
+
// Starts an export block - returns true if we can proceed
this.exportingStart = function() {
if(!this.exportRunning) {
this.exportRunning = true;
return true;
}
-
+
return false;
};
// ends an export block
- this.exportingEnd = function() {
- this.exportRunning = false;
+ this.exportingEnd = function(cb) {
+ this.exportRunning = false;
+
+ if(cb) {
+ return cb(null);
+ }
};
this.copyTicAttachment = function(src, dst, isUpdate, cb) {
if(isUpdate) {
- fse.copy(src, dst, err => {
+ fse.copy(src, dst, { overwrite : true }, err => {
return cb(err, dst);
});
} else {
@@ -1274,8 +1613,6 @@ function FTNMessageScanTossModule() {
};
this.processSingleTicFile = function(ticFileInfo, cb) {
- const self = this;
-
Log.debug( { tic : ticFileInfo.path, file : ticFileInfo.getAsString('File') }, 'Processing TIC file');
async.waterfall(
@@ -1289,6 +1626,7 @@ function FTNMessageScanTossModule() {
return ticFileInfo.validate(config, (err, localInfo) => {
if(err) {
+ Log.trace( { reason : err.message }, 'Validation failure');
return callback(err);
}
@@ -1320,7 +1658,7 @@ function FTNMessageScanTossModule() {
// Lastly, we will only replace if the item is in the same/specified area
// and that come from the same origin as a previous entry.
//
- const allowReplace = _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ] ) || Config.scannerTossers.ftn_bso.tic.allowReplace;
+ const allowReplace = _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config.scannerTossers.ftn_bso.tic.allowReplace);
const replaces = ticFileInfo.getAsString('Replaces');
if(!allowReplace || !replaces) {
@@ -1349,10 +1687,17 @@ function FTNMessageScanTossModule() {
localInfo.existingFileId = fileIds[0];
// fetch old filename - we may need to remove it if replacing with a new name
- FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (cb, info) => {
- localInfo.oldFileName = info.fileName;
- localInfo.oldStorageTag = info.storageTag;
- return callback(null, localInfo);
+ FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (err, info) => {
+ if(info) {
+ Log.trace(
+ { fileId : localInfo.existingFileId, oldFileName : info.fileName, oldStorageTag : info.storageTag },
+ 'Existing TIC file target to be replaced'
+ );
+
+ localInfo.oldFileName = info.fileName;
+ localInfo.oldStorageTag = info.storageTag;
+ }
+ return callback(null, localInfo); // continue even if we couldn't find an old match
});
} else if(fileIds.legnth > 1) {
return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`));
@@ -1369,7 +1714,7 @@ function FTNMessageScanTossModule() {
short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name
tic_origin : ticFileInfo.getAsString('Origin'),
tic_desc : ticFileInfo.getAsString('Desc'),
- upload_by_username : _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ]) || Config.scannerTossers.ftn_bso.tic.uploadBy,
+ upload_by_username : _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config.scannerTossers.ftn_bso.tic.uploadBy),
}
};
@@ -1381,8 +1726,8 @@ function FTNMessageScanTossModule() {
//
// We may have TIC auto-tagging for this node and/or specific (remote) area
//
- const hashTags =
- localInfo.hashTags ||
+ const hashTags =
+ localInfo.hashTags ||
_.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/
if(hashTags) {
@@ -1397,6 +1742,10 @@ function FTNMessageScanTossModule() {
ticFileInfo.filePath,
scanOpts,
(err, fileEntry) => {
+ if(err) {
+ Log.trace( { reason : err.message }, 'Scanning failed');
+ }
+
localInfo.fileEntry = fileEntry;
return callback(err, localInfo);
}
@@ -1420,9 +1769,25 @@ function FTNMessageScanTossModule() {
localInfo.fileEntry.areaTag = localInfo.areaTag;
localInfo.fileEntry.fileName = ticFileInfo.longFileName;
- // we default to .DIZ/etc. desc, but use from TIC if needed
- if(!localInfo.fileEntry.desc || 0 === localInfo.fileEntry.desc.length) {
- localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || ticFileInfo.getAsString('Desc') || getDescFromFileName(ticFileInfo.filePath);
+ //
+ // We may now have two descriptions: from .DIZ/etc. or the TIC itself.
+ // Determine which one to use using |descPriority| and availability.
+ //
+ // We will still fallback as needed from -> ->
+ //
+ const descPriority = _.get(
+ Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ],
+ Config.scannerTossers.ftn_bso.tic.descPriority
+ );
+
+ if('tic' === descPriority) {
+ const origDesc = localInfo.fileEntry.desc;
+ localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || origDesc || getDescFromFileName(ticFileInfo.filePath);
+ } else {
+ // see if we got desc from .DIZ/etc.
+ const fromDescFile = 'descFile' === localInfo.fileEntry.descSrc;
+ localInfo.fileEntry.desc = fromDescFile ? localInfo.fileEntry.desc : ticFileInfo.getAsString('Ldesc');
+ localInfo.fileEntry.desc = localInfo.fileEntry.desc || getDescFromFileName(ticFileInfo.filePath);
}
const areaStorageDir = getAreaStorageDirectoryByTag(storageTag);
@@ -1434,26 +1799,27 @@ function FTNMessageScanTossModule() {
if(isUpdate) {
// we need to *update* an existing record/file
- localInfo.fileEntry.fileId = localInfo.existingFileId;
+ localInfo.fileEntry.fileId = localInfo.existingFileId;
}
const dst = paths.join(areaStorageDir, localInfo.fileEntry.fileName);
self.copyTicAttachment(ticFileInfo.filePath, dst, isUpdate, (err, finalPath) => {
if(err) {
+ Log.info( { reason : err.message }, 'Failed to copy TIC attachment');
return callback(err);
}
if(dst !== finalPath) {
localInfo.fileEntry.fileName = paths.basename(finalPath);
}
-
+
localInfo.fileEntry.persist(isUpdate, err => {
return callback(err, localInfo);
});
- });
+ });
},
- // :TODO: from here, we need to re-toss files if needed, before they are removed
+ // :TODO: from here, we need to re-toss files if needed, before they are removed
function cleanupOldFile(localInfo, callback) {
if(!localInfo.existingFileId) {
return callback(null, localInfo);
@@ -1461,7 +1827,7 @@ function FTNMessageScanTossModule() {
const oldStorageDir = getAreaStorageDirectoryByTag(localInfo.oldStorageTag);
const oldPath = paths.join(oldStorageDir, localInfo.oldFileName);
-
+
fs.unlink(oldPath, err => {
if(err) {
Log.warn( { error : err.message, oldPath : oldPath }, 'Failed removing old physical file during TIC replacement');
@@ -1474,7 +1840,7 @@ function FTNMessageScanTossModule() {
],
(err, localInfo) => {
if(err) {
- Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.path }, 'Failed import/update TIC record' );
+ Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed import/update TIC record' );
} else {
Log.debug(
{ tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag },
@@ -1498,6 +1864,153 @@ function FTNMessageScanTossModule() {
return cb(err);
});
};
+
+
+ this.performEchoMailExport = function(cb) {
+ //
+ // Select all messages with a |message_id| > |lastScanId|.
+ // Additionally exclude messages with the System state_flags0 which will be present for
+ // imported or already exported messages
+ //
+ // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here!
+ //
+ const getNewUuidsSql =
+ `SELECT message_id, message_uuid
+ FROM message m
+ WHERE area_tag = ? AND message_id > ? AND
+ (SELECT COUNT(message_id)
+ FROM message_meta
+ WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0
+ ORDER BY message_id;`
+ ;
+
+ // we shouldn't, but be sure we don't try to pick up private mail here
+ const areaTags = Object.keys(Config.messageNetworks.ftn.areas)
+ .filter(areaTag => Message.WellKnownAreaTags.Private !== areaTag);
+
+ async.each(areaTags, (areaTag, nextArea) => {
+ const areaConfig = Config.messageNetworks.ftn.areas[areaTag];
+ if(!this.isAreaConfigValid(areaConfig)) {
+ return nextArea();
+ }
+
+ //
+ // For each message that is newer than that of the last scan
+ // we need to export to each configured associated uplink(s)
+ //
+ async.waterfall(
+ [
+ function getLastScanId(callback) {
+ self.getAreaLastScanId(areaTag, callback);
+ },
+ function getNewUuids(lastScanId, callback) {
+ msgDb.all(getNewUuidsSql, [ areaTag, lastScanId ], (err, rows) => {
+ if(err) {
+ callback(err);
+ } else {
+ if(0 === rows.length) {
+ let nothingToDoErr = new Error('Nothing to do!');
+ nothingToDoErr.noRows = true;
+ callback(nothingToDoErr);
+ } else {
+ callback(null, rows);
+ }
+ }
+ });
+ },
+ function exportToConfiguredUplinks(msgRows, callback) {
+ const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only
+ self.exportEchoMailMessagesToUplinks(uuidsOnly, areaConfig, err => {
+ const newLastScanId = msgRows[msgRows.length - 1].message_id;
+
+ Log.info(
+ { areaTag : areaTag, messagesExported : msgRows.length, newLastScanId : newLastScanId },
+ 'Export complete');
+
+ callback(err, newLastScanId);
+ });
+ },
+ function updateLastScanId(newLastScanId, callback) {
+ self.setAreaLastScanId(areaTag, newLastScanId, callback);
+ }
+ ],
+ () => {
+ return nextArea();
+ }
+ );
+ },
+ err => {
+ return cb(err);
+ });
+ };
+
+ this.performNetMailExport = function(cb) {
+ //
+ // Select all messages with a |message_id| > |lastScanId| in the private area
+ // that are schedule for export to FTN-style networks.
+ //
+ // Just like EchoMail, we additionally exclude messages with the System state_flags0
+ // which will be present for imported or already exported messages
+ //
+ //
+ // :TODO: fill out the rest of the consts here
+ // :TODO: this statement is crazy ugly -- use JOIN / NOT EXISTS for state_flags & 0x02
+ const getNewUuidsSql =
+ `SELECT message_id, message_uuid
+ FROM message m
+ WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND
+ (SELECT COUNT(message_id)
+ FROM message_meta
+ WHERE message_id = m.message_id
+ AND meta_category = 'System'
+ AND (meta_name = 'state_flags0' OR meta_name = 'local_to_user_id')
+ ) = 0
+ AND
+ (SELECT COUNT(message_id)
+ FROM message_meta
+ WHERE message_id = m.message_id
+ AND meta_category = 'System'
+ AND meta_name = '${Message.SystemMetaNames.ExternalFlavor}'
+ AND meta_value = '${Message.AddressFlavor.FTN}'
+ ) = 1
+ ORDER BY message_id;
+ `;
+
+ async.waterfall(
+ [
+ function getLastScanId(callback) {
+ return self.getAreaLastScanId(Message.WellKnownAreaTags.Private, callback);
+ },
+ function getNewUuids(lastScanId, callback) {
+ msgDb.all(getNewUuidsSql, [ lastScanId ], (err, rows) => {
+ if(err) {
+ return callback(err);
+ }
+
+ if(0 === rows.length) {
+ return cb(null); // note |cb| -- early bail out!
+ }
+
+ return callback(null, rows);
+ });
+ },
+ function exportMessages(rows, callback) {
+ const messageUuids = rows.map(r => r.message_uuid);
+ return self.exportNetMailMessagesToUplinks(messageUuids, callback);
+ }
+ ],
+ err => {
+ return cb(err);
+ }
+ );
+ };
+
+ this.isNetMailMessage = function(message) {
+ return message.isPrivate() &&
+ null === _.get(message, 'meta.System.LocalToUserID', null) &&
+ Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null)
+ ;
+ };
}
require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule);
@@ -1545,19 +2058,19 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD
if(err) {
// archive rejected TIC stuff (.TIC + attach)
async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => {
- if(!path) { // possibly rejected due to "File" not existing/etc.
+ if(!path) { // possibly rejected due to "File" not existing/etc.
return nextPath(null);
}
self.maybeArchiveImportFile(
- path,
- 'tic',
+ path,
+ 'tic',
'reject',
() => {
return nextPath(null);
}
);
- },
+ },
() => {
self.removeAssocTicFiles(ticFileInfo, () => {
return nextTicInfo(null);
@@ -1567,7 +2080,7 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD
self.removeAssocTicFiles(ticFileInfo, () => {
return nextTicInfo(null);
});
- }
+ }
});
}, err => {
return callback(err);
@@ -1582,59 +2095,59 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD
FTNMessageScanTossModule.prototype.startup = function(cb) {
Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`);
-
+
let importing = false;
-
+
let self = this;
-
- function tryImportNow(reasonDesc) {
+
+ function tryImportNow(reasonDesc, extraInfo) {
if(!importing) {
importing = true;
-
- Log.info( { module : exports.moduleInfo.name }, reasonDesc);
-
+
+ Log.info( Object.assign({ module : exports.moduleInfo.name }, extraInfo), reasonDesc);
+
self.performImport( () => {
importing = false;
});
}
}
-
+
this.createTempDirectories(err => {
if(err) {
Log.warn( { error : err.toStrong() }, 'Failed creating temporary directories!');
return cb(err);
}
-
+
if(_.isObject(this.moduleConfig.schedule)) {
const exportSchedule = this.parseScheduleString(this.moduleConfig.schedule.export);
if(exportSchedule) {
Log.debug(
- {
- schedule : this.moduleConfig.schedule.export,
+ {
+ schedule : this.moduleConfig.schedule.export,
schedOK : -1 === exportSchedule.sched.error,
next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'),
immediate : exportSchedule.immediate ? true : false,
},
'Export schedule loaded'
);
-
+
if(exportSchedule.sched) {
this.exportTimer = later.setInterval( () => {
if(this.exportingStart()) {
Log.info( { module : exports.moduleInfo.name }, 'Performing scheduled message scan/export...');
-
+
this.performExport( () => {
this.exportingEnd();
});
}
}, exportSchedule.sched);
}
-
+
if(_.isBoolean(exportSchedule.immediate)) {
this.exportImmediate = exportSchedule.immediate;
}
}
-
+
const importSchedule = this.parseScheduleString(this.moduleConfig.schedule.import);
if(importSchedule) {
Log.debug(
@@ -1646,13 +2159,13 @@ FTNMessageScanTossModule.prototype.startup = function(cb) {
},
'Import schedule loaded'
);
-
- if(importSchedule.sched) {
+
+ if(importSchedule.sched) {
this.importTimer = later.setInterval( () => {
- tryImportNow('Performing scheduled message import/toss...');
+ tryImportNow('Performing scheduled message import/toss...');
}, importSchedule.sched);
}
-
+
if(_.isString(importSchedule.watchFile)) {
const watcher = sane(
paths.dirname(importSchedule.watchFile),
@@ -1665,7 +2178,7 @@ FTNMessageScanTossModule.prototype.startup = function(cb) {
watcher.on(event, (fileName, fileRoot) => {
const eventPath = paths.join(fileRoot, fileName);
if(paths.join(fileRoot, fileName) === importSchedule.watchFile) {
- tryImportNow(`Performing import/toss due to @watch: ${eventPath} (${event})`);
+ tryImportNow('Performing import/toss due to @watch', { eventPath, event } );
}
});
});
@@ -1676,28 +2189,28 @@ FTNMessageScanTossModule.prototype.startup = function(cb) {
//
fse.exists(importSchedule.watchFile, exists => {
if(exists) {
- tryImportNow(`Performing import/toss due to @watch: ${importSchedule.watchFile} (initial exists)`);
+ tryImportNow('Performing import/toss due to @watch', { eventPath : importSchedule.watchFile, event : 'initial exists' } );
}
});
}
}
}
-
+
FTNMessageScanTossModule.super_.prototype.startup.call(this, cb);
});
};
FTNMessageScanTossModule.prototype.shutdown = function(cb) {
Log.info('FidoNet Scanner/Tosser shutting down');
-
+
if(this.exportTimer) {
this.exportTimer.clear();
}
-
+
if(this.importTimer) {
this.importTimer.clear();
}
-
+
//
// Clean up temp dir/files we created
//
@@ -1708,9 +2221,9 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) {
paths : paths,
sessionId : temptmp.sessionId,
};
-
+
Log.trace(fullStats, 'Temporary directories cleaned up');
-
+
FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb);
});
@@ -1721,9 +2234,9 @@ FTNMessageScanTossModule.prototype.performImport = function(cb) {
if(!this.hasValidConfiguration()) {
return cb(new Error('Missing or invalid configuration'));
}
-
+
const self = this;
-
+
async.each( [ 'inbound', 'secInbound' ], (inboundType, nextDir) => {
self.importFromDirectory(inboundType, self.moduleConfig.paths[inboundType], () => {
return nextDir(null);
@@ -1739,77 +2252,18 @@ FTNMessageScanTossModule.prototype.performExport = function(cb) {
if(!this.hasValidConfiguration()) {
return cb(new Error('Missing or invalid configuration'));
}
-
- //
- // Select all messages with a |message_id| > |lastScanId|.
- // Additionally exclude messages with the System state_flags0 which will be present for
- // imported or already exported messages
- //
- // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here!
- //
- const getNewUuidsSql =
- `SELECT message_id, message_uuid
- FROM message m
- WHERE area_tag = ? AND message_id > ? AND
- (SELECT COUNT(message_id)
- FROM message_meta
- WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0
- ORDER BY message_id;`;
-
- let self = this;
-
- async.each(Object.keys(Config.messageNetworks.ftn.areas), (areaTag, nextArea) => {
- const areaConfig = Config.messageNetworks.ftn.areas[areaTag];
- if(!this.isAreaConfigValid(areaConfig)) {
- return nextArea();
- }
-
- //
- // For each message that is newer than that of the last scan
- // we need to export to each configured associated uplink(s)
- //
- async.waterfall(
- [
- function getLastScanId(callback) {
- self.getAreaLastScanId(areaTag, callback);
- },
- function getNewUuids(lastScanId, callback) {
- msgDb.all(getNewUuidsSql, [ areaTag, lastScanId ], (err, rows) => {
- if(err) {
- callback(err);
- } else {
- if(0 === rows.length) {
- let nothingToDoErr = new Error('Nothing to do!');
- nothingToDoErr.noRows = true;
- callback(nothingToDoErr);
- } else {
- callback(null, rows);
- }
- }
- });
- },
- function exportToConfiguredUplinks(msgRows, callback) {
- const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only
- self.exportMessagesToUplinks(uuidsOnly, areaConfig, err => {
- const newLastScanId = msgRows[msgRows.length - 1].message_id;
-
- Log.info(
- { areaTag : areaTag, messagesExported : msgRows.length, newLastScanId : newLastScanId },
- 'Export complete');
-
- callback(err, newLastScanId);
- });
- },
- function updateLastScanId(newLastScanId, callback) {
- self.setAreaLastScanId(areaTag, newLastScanId, callback);
- }
- ],
- () => {
- return nextArea();
+
+ const self = this;
+
+ async.eachSeries( [ 'EchoMail', 'NetMail' ], (type, nextType) => {
+ self[`perform${type}Export`]( err => {
+ if(err) {
+ Log.warn( { error : err.message, type : type }, 'Error(s) during export' );
}
- );
- }, err => {
- return cb(err);
+ return nextType(null); // try next, always
+ });
+ }, () => {
+ return cb(null);
});
};
@@ -1820,27 +2274,37 @@ FTNMessageScanTossModule.prototype.record = function(message) {
if(true !== this.exportImmediate || !this.hasValidConfiguration()) {
return;
}
-
- if(message.isPrivate()) {
- // :TODO: support NetMail
+
+ const info = { uuid : message.uuid, subject : message.subject };
+
+ function exportLog(err) {
+ if(err) {
+ Log.warn(info, 'Failed exporting message');
+ } else {
+ Log.info(info, 'Message exported');
+ }
+ }
+
+ if(this.isNetMailMessage(message)) {
+ Object.assign(info, { type : 'NetMail' } );
+
+ if(this.exportingStart()) {
+ this.exportNetMailMessagesToUplinks( [ message.uuid ], err => {
+ this.exportingEnd( () => exportLog(err) );
+ });
+ }
} else if(message.areaTag) {
+ Object.assign(info, { type : 'EchoMail' } );
+
const areaConfig = Config.messageNetworks.ftn.areas[message.areaTag];
if(!this.isAreaConfigValid(areaConfig)) {
return;
}
-
+
if(this.exportingStart()) {
- this.exportMessagesToUplinks( [ message.uuid ], areaConfig, err => {
- const info = { uuid : message.uuid, subject : message.subject };
-
- if(err) {
- Log.warn(info, 'Failed exporting message');
- } else {
- Log.info(info, 'Message exported');
- }
-
- this.exportingEnd();
+ this.exportEchoMailMessagesToUplinks( [ message.uuid ], areaConfig, err => {
+ this.exportingEnd( () => exportLog(err) );
});
- }
- }
+ }
+ }
};
diff --git a/core/servers/content/web.js b/core/servers/content/web.js
index 31c617e2..8a9903b4 100644
--- a/core/servers/content/web.js
+++ b/core/servers/content/web.js
@@ -148,7 +148,7 @@ exports.getModule = class WebServerModule extends ServerModule {
const routeKey = route.getRouteKey();
if(routeKey in this.routes) {
- Log.warn( { route : route }, 'Cannot add route: duplicate method/path combination exists' );
+ Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' );
return false;
}
@@ -158,6 +158,11 @@ exports.getModule = class WebServerModule extends ServerModule {
routeRequest(req, resp) {
const route = _.find(this.routes, r => r.matchesRequest(req) );
+
+ if(!route && '/' === req.url) {
+ return this.routeIndex(req, resp);
+ }
+
return route ? route.handler(req, resp) : this.accessDenied(resp);
}
@@ -196,9 +201,20 @@ exports.getModule = class WebServerModule extends ServerModule {
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
}
+ routeIndex(req, resp) {
+ const filePath = paths.join(Config.contentServers.web.staticRoot, 'index.html');
+
+ return this.returnStaticPage(filePath, resp);
+ }
+
routeStaticFile(req, resp) {
const fileName = req.url.substr(req.url.indexOf('/', 1));
const filePath = paths.join(Config.contentServers.web.staticRoot, fileName);
+
+ return this.returnStaticPage(filePath, resp);
+ }
+
+ returnStaticPage(filePath, resp) {
const self = this;
fs.stat(filePath, (err, stats) => {
@@ -207,7 +223,7 @@ exports.getModule = class WebServerModule extends ServerModule {
}
const headers = {
- 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
+ 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size,
};
diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js
index 4377f029..a6fa0deb 100644
--- a/core/servers/login/telnet.js
+++ b/core/servers/login/telnet.js
@@ -88,6 +88,7 @@ const SB_COMMANDS = {
//
// Resources
// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html
+// * http://www.networksorcery.com/enp/protocol/telnet.htm
//
const OPTIONS = {
TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856
@@ -186,6 +187,8 @@ const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) {
function unknownOption(bufs, i, event) {
Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option');
+ event.buf = bufs.splice(0, i).toBuffer();
+ return event;
}
const OPTION_IMPLS = {};
@@ -538,6 +541,13 @@ function TelnetClient(input, output) {
const logger = self.log || Log;
return logger.warn(info, `Telnet: ${msg}`);
};
+
+ this.readyNow = () => {
+ if(!this.didReady) {
+ this.didReady = true;
+ this.emit('ready', { firstMenu : Config.loginServers.telnet.firstMenu } );
+ }
+ };
}
util.inherits(TelnetClient, baseClient.Client);
@@ -630,10 +640,7 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout
- if(!self.didReady) {
- self.didReady = true;
- self.emit('ready', { firstMenu : Config.loginServers.telnet.firstMenu } );
- }
+ self.readyNow();
} else if('new environment' === evt.option) {
//
// Handling is as follows:
@@ -829,6 +836,18 @@ exports.getModule = class TelnetServerModule extends LoginServerModule {
client.banner();
this.handleNewClient(client, sock, ModuleInfo);
+
+ //
+ // Set a timeout and attempt to proceed even if we don't know
+ // the term type yet, which is the preferred trigger
+ // for moving along
+ //
+ setTimeout( () => {
+ if(!client.didReady) {
+ Log.info('Proceeding after 3s without knowing term type');
+ client.readyNow();
+ }
+ }, 3000);
});
this.server.on('error', err => {
diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js
new file mode 100644
index 00000000..0e29e999
--- /dev/null
+++ b/core/set_newscan_date.js
@@ -0,0 +1,261 @@
+/* jslint node: true */
+'use strict';
+
+// ENiGMA½
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const Errors = require('./enig_error.js').Errors;
+const FileEntry = require('./file_entry.js');
+const FileBaseFilters = require('./file_base_filter.js');
+const { getAvailableFileAreaTags } = require('./file_base_area.js');
+const {
+ getSortedAvailMessageConferences,
+ getSortedAvailMessageAreasByConfTag,
+ updateMessageAreaLastReadId,
+ getMessageIdNewerThanTimestampByArea
+} = require('./message_area.js');
+const stringFormat = require('./string_format.js');
+
+// deps
+const async = require('async');
+const moment = require('moment');
+const _ = require('lodash');
+
+exports.moduleInfo = {
+ name : 'Set New Scan Date',
+ desc : 'Sets new scan date for applicable scans',
+ author : 'NuSkooler',
+};
+
+const MciViewIds = {
+ main : {
+ scanDate : 1,
+ targetSelection : 2,
+ }
+};
+
+// :TODO: for messages, we could insert "conf - all areas" into targets, and allow such
+
+exports.getModule = class SetNewScanDate extends MenuModule {
+ constructor(options) {
+ super(options);
+
+ const config = this.menuConfig.config;
+
+ this.target = config.target || 'message';
+ this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD';
+
+ this.menuMethods = {
+ scanDateSubmit : (formData, extraArgs, cb) => {
+ let scanDate = _.get(formData, 'value.scanDate');
+ if(!scanDate) {
+ return cb(Errors.MissingParam('"scanDate" missing from form data'));
+ }
+
+ scanDate = moment(scanDate, this.scanDateFormat);
+ if(!scanDate.isValid()) {
+ return cb(Errors.Invalid(`"${_.get(formData, 'value.scanDate')}" is not a valid date`));
+ }
+
+ const targetSelection = _.get(formData, 'value.targetSelection'); // may be undefined if N/A
+
+ this[`setNewScanDateFor${_.capitalize(this.target)}Base`](targetSelection, scanDate, () => {
+ return this.prevMenu(cb);
+ });
+ },
+ };
+ }
+
+ setNewScanDateForMessageBase(targetSelection, scanDate, cb) {
+ const target = this.targetSelections[targetSelection];
+ if(!target) {
+ return cb(Errors.UnexpectedState('Unable to get target in which to set new scan'));
+ }
+
+ // selected area, or all of 'em
+ let updateAreaTags;
+ if('' === target.area.areaTag) {
+ updateAreaTags = this.targetSelections
+ .map( targetSelection => targetSelection.area.areaTag )
+ .filter( areaTag => areaTag ); // remove the blank 'all' entry
+ } else {
+ updateAreaTags = [ target.area.areaTag ];
+ }
+
+ async.each(updateAreaTags, (areaTag, nextAreaTag) => {
+ getMessageIdNewerThanTimestampByArea(areaTag, scanDate, (err, messageId) => {
+ if(err) {
+ return nextAreaTag(err);
+ }
+
+ if(!messageId) {
+ return nextAreaTag(null); // nothing to do
+ }
+
+ messageId = Math.max(messageId - 1, 0);
+
+ return updateMessageAreaLastReadId(
+ this.client.user.userId,
+ areaTag,
+ messageId,
+ true, // allowOlder
+ nextAreaTag
+ );
+ });
+ }, err => {
+ return cb(err);
+ });
+ }
+
+ setNewScanDateForFileBase(targetSelection, scanDate, cb) {
+ //
+ // ENiGMA doesn't currently have the concept of per-area
+ // scan pointers for users, so we use all areas avail
+ // to the user.
+ //
+ const filterCriteria = {
+ areaTag : getAvailableFileAreaTags(this.client),
+ newerThanTimestamp : scanDate,
+ limit : 1,
+ orderBy : 'upload_timestamp',
+ order : 'ascending',
+ };
+
+ FileEntry.findFiles(filterCriteria, (err, fileIds) => {
+ if(err) {
+ return cb(err);
+ }
+
+ if(!fileIds || 0 === fileIds.length) {
+ // nothing to do
+ return cb(null);
+ }
+
+ const pointerFileId = Math.max(fileIds[0] - 1, 0);
+
+ return FileBaseFilters.setFileBaseLastViewedFileIdForUser(
+ this.client.user,
+ pointerFileId,
+ true, // allowOlder
+ cb
+ );
+ });
+ }
+
+ loadAvailMessageBaseSelections(cb) {
+ //
+ // Create an array of objects with conf/area information per entry,
+ // sorted naturally or via the 'sort' member in config
+ //
+ const selections = [];
+ getSortedAvailMessageConferences(this.client).forEach(conf => {
+ getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).forEach(area => {
+ selections.push({
+ conf : {
+ confTag : conf.confTag,
+ name : conf.conf.name,
+ desc : conf.conf.desc,
+ },
+ area : {
+ areaTag : area.areaTag,
+ name : area.area.name,
+ desc : area.area.desc,
+ }
+ });
+ });
+ });
+
+ selections.unshift({
+ conf : {
+ confTag : '',
+ name : 'All conferences',
+ desc : 'All conferences',
+ },
+ area : {
+ areaTag : '',
+ name : 'All areas',
+ desc : 'All areas',
+ }
+ });
+
+ // Find current conf/area & move it directly under "All"
+ const currConfTag = this.client.user.properties.message_conf_tag;
+ const currAreaTag = this.client.user.properties.message_area_tag;
+ if(currConfTag && currAreaTag) {
+ const confAreaIndex = selections.findIndex( confArea => {
+ return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag;
+ });
+
+ if(confAreaIndex > -1) {
+ selections.splice(1, 0, selections.splice(confAreaIndex, 1)[0]);
+ }
+ }
+
+ this.targetSelections = selections;
+
+ return cb(null);
+ }
+
+ mciReady(mciData, cb) {
+ super.mciReady(mciData, err => {
+ if(err) {
+ return cb(err);
+ }
+
+ const self = this;
+ const vc = self.addViewController( 'main', new ViewController( { client : this.client } ) );
+
+ async.series(
+ [
+ function validateConfig(callback) {
+ if(![ 'message', 'file' ].includes(self.target)) {
+ return callback(Errors.Invalid(`Invalid "target" in config: ${self.target}`));
+ }
+ // :TOD0: validate scanDateFormat
+ return callback(null);
+ },
+ function loadFromConfig(callback) {
+ return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
+ },
+ function loadAvailSelections(callback) {
+ switch(self.target) {
+ case 'message' :
+ return self.loadAvailMessageBaseSelections(callback);
+
+ default :
+ return callback(null);
+ }
+ },
+ function populateForm(callback) {
+ const today = moment();
+
+ const scanDateView = vc.getView(MciViewIds.main.scanDate);
+
+ // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now
+ const scanDateFormat = self.scanDateFormat.replace(/[\/\-. ]/g, '');
+ scanDateView.setText(today.format(scanDateFormat));
+
+ if('message' === self.target) {
+ const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}';
+ const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat;
+
+ const targetSelectionView = vc.getView(MciViewIds.main.targetSelection);
+
+ targetSelectionView.setItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection)));
+ targetSelectionView.setFocusItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection)));
+
+ targetSelectionView.setFocusItemIndex(0);
+ }
+
+ self.viewControllers.main.resetInitialFocus();
+ //vc.switchFocus(MciViewIds.main.scanDate);
+ return callback(null);
+ }
+ ],
+ err => {
+ return cb(err);
+ }
+ );
+ });
+ }
+};
diff --git a/core/string_format.js b/core/string_format.js
index dd1ece78..7fb7109a 100644
--- a/core/string_format.js
+++ b/core/string_format.js
@@ -2,12 +2,15 @@
'use strict';
const EnigError = require('./enig_error.js').EnigError;
-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;
+
+const {
+ pad,
+ stylizeString,
+ renderStringLength,
+ renderSubstr,
+ formatByteSize, formatByteSizeAbbr,
+ formatCount, formatCountAbbr,
+} = require('./string_util.js');
// deps
const _ = require('lodash');
@@ -268,11 +271,16 @@ const transformers = {
styleMixed : (s) => stylizeString(s, 'mixed'),
styleL33t : (s) => stylizeString(s, 'l33t'),
+ // :TODO:
// toMegs(), toKilobytes(), ...
// toList(), toCommaList(),
+
sizeWithAbbr : (n) => formatByteSize(n, true, 2),
sizeWithoutAbbr : (n) => formatByteSize(n, false, 2),
sizeAbbr : (n) => formatByteSizeAbbr(n),
+ countWithAbbr : (n) => formatCount(n, true, 0),
+ countWithoutAbbr : (n) => formatCount(n, false, 0),
+ countAbbr : (n) => formatCountAbbr(n),
};
function transformValue(transformerName, value) {
diff --git a/core/string_util.js b/core/string_util.js
index ed6df231..238aeeee 100644
--- a/core/string_util.js
+++ b/core/string_util.js
@@ -23,6 +23,8 @@ exports.renderSubstr = renderSubstr;
exports.renderStringLength = renderStringLength;
exports.formatByteSizeAbbr = formatByteSizeAbbr;
exports.formatByteSize = formatByteSize;
+exports.formatCountAbbr = formatCountAbbr;
+exports.formatCount = formatCount;
exports.cleanControlCodes = cleanControlCodes;
exports.isAnsi = isAnsi;
exports.isAnsiLine = isAnsiLine;
@@ -316,23 +318,40 @@ function renderStringLength(s) {
return len;
}
-const SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :)
+const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :)
function formatByteSizeAbbr(byteSize) {
if(0 === byteSize) {
- return SIZE_ABBRS[0]; // B
+ return BYTE_SIZE_ABBRS[0]; // B
}
- return SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))];
+ return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))];
}
-function formatByteSize(byteSize, withAbbr, decimals) {
- withAbbr = withAbbr || false;
- decimals = decimals || 3;
+function formatByteSize(byteSize, withAbbr = false, decimals = 2) {
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]}`;
+ result += ` ${BYTE_SIZE_ABBRS[i]}`;
+ }
+ return result;
+}
+
+const COUNT_ABBRS = [ '', 'K', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y' ];
+
+function formatCountAbbr(count) {
+ if(count < 1000) {
+ return '';
+ }
+
+ return COUNT_ABBRS[Math.floor(Math.log(count) / Math.log(1000))];
+}
+
+function formatCount(count, withAbbr = false, decimals = 2) {
+ const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000));
+ let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals));
+ if(withAbbr) {
+ result += `${COUNT_ABBRS[i]}`;
}
return result;
}
@@ -604,6 +623,10 @@ function isFormattedLine(line) {
}
function isAnsi(input) {
+ if(!input || 0 === input.length) {
+ return false;
+ }
+
//
// * ANSI found - limited, just colors
// * Full ANSI art
diff --git a/core/system_view_validate.js b/core/system_view_validate.js
index 63c7b2bc..e2a5b2e0 100644
--- a/core/system_view_validate.js
+++ b/core/system_view_validate.js
@@ -2,20 +2,24 @@
'use strict';
// ENiGMA½
-const User = require('./user.js');
-const Config = require('./config.js').config;
-const Log = require('./logger.js').log;
+const User = require('./user.js');
+const Config = require('./config.js').config;
+const Log = require('./logger.js').log;
+const { getAddressedToInfo } = require('./mail_util.js');
+const Message = require('./message.js');
// deps
const fs = require('graceful-fs');
-exports.validateNonEmpty = validateNonEmpty;
-exports.validateMessageSubject = validateMessageSubject;
-exports.validateUserNameAvail = validateUserNameAvail;
-exports.validateUserNameExists = validateUserNameExists;
-exports.validateEmailAvail = validateEmailAvail;
-exports.validateBirthdate = validateBirthdate;
-exports.validatePasswordSpec = validatePasswordSpec;
+exports.validateNonEmpty = validateNonEmpty;
+exports.validateMessageSubject = validateMessageSubject;
+exports.validateUserNameAvail = validateUserNameAvail;
+exports.validateUserNameExists = validateUserNameExists;
+exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists;
+exports.validateGeneralMailAddressedTo = validateGeneralMailAddressedTo;
+exports.validateEmailAvail = validateEmailAvail;
+exports.validateBirthdate = validateBirthdate;
+exports.validatePasswordSpec = validatePasswordSpec;
function validateNonEmpty(data, cb) {
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
@@ -42,29 +46,56 @@ function validateUserNameAvail(data, cb) {
} else if(/^[0-9]+$/.test(data)) {
return cb(new Error('Username cannot be a number'));
} else {
- User.getUserIdAndName(data, function userIdAndName(err) {
+ // a new user name cannot be an existing user name or an existing real name
+ User.getUserIdAndNameByLookup(data, function userIdAndName(err) {
if(!err) { // err is null if we succeeded -- meaning this user exists already
return cb(new Error('Username unavailable'));
}
-
+
return cb(null);
});
}
}
}
-function validateUserNameExists(data, cb) {
- const invalidUserNameError = new Error('Invalid username');
+const invalidUserNameError = () => new Error('Invalid username');
+function validateUserNameExists(data, cb) {
if(0 === data.length) {
- return cb(invalidUserNameError);
+ return cb(invalidUserNameError());
}
User.getUserIdAndName(data, (err) => {
- return cb(err ? invalidUserNameError : null);
+ return cb(err ? invalidUserNameError() : null);
});
}
+function validateUserNameOrRealNameExists(data, cb) {
+ if(0 === data.length) {
+ return cb(invalidUserNameError());
+ }
+
+ User.getUserIdAndNameByLookup(data, err => {
+ return cb(err ? invalidUserNameError() : null);
+ });
+}
+
+function validateGeneralMailAddressedTo(data, cb) {
+ //
+ // Allow any supported addressing:
+ // - Local username or real name
+ // - Supported remote flavors such as FTN, email, ...
+ //
+ // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
+ const addressedToInfo = getAddressedToInfo(data);
+
+ if(Message.AddressFlavor.FTN === addressedToInfo.flavor) {
+ return cb(null);
+ }
+
+ return validateUserNameOrRealNameExists(data, cb);
+}
+
function validateEmailAvail(data, cb) {
//
// This particular method allows empty data - e.g. no email entered
diff --git a/mods/telnet_bridge.js b/core/telnet_bridge.js
similarity index 93%
rename from mods/telnet_bridge.js
rename to core/telnet_bridge.js
index 1dbb1ae9..fa1754a5 100644
--- a/mods/telnet_bridge.js
+++ b/core/telnet_bridge.js
@@ -2,9 +2,9 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const resetScreen = require('../core/ansi_term.js').resetScreen;
-const setSyncTermFontWithAlias = require('../core/ansi_term.js').setSyncTermFontWithAlias;
+const MenuModule = require('./menu_module.js').MenuModule;
+const resetScreen = require('./ansi_term.js').resetScreen;
+const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias;
// deps
const async = require('async');
@@ -48,7 +48,7 @@ class TelnetClientConnection extends EventEmitter {
this.pipeRestored = true;
// client may have bailed
- if(_.has(this, 'client.term.output')) {
+ if(null !== _.get(this, 'client.term.output', null)) {
if(this.bridgeConnection) {
this.client.term.output.unpipe(this.bridgeConnection);
}
diff --git a/mods/upload.js b/core/upload.js
similarity index 90%
rename from mods/upload.js
rename to core/upload.js
index 5c5fd5b2..5a49a0ca 100644
--- a/mods/upload.js
+++ b/core/upload.js
@@ -2,19 +2,20 @@
'use strict';
// enigma-bbs
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const stringFormat = require('../core/string_format.js');
-const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
-const getAreaDefaultStorageDirectory = require('../core/file_base_area.js').getAreaDefaultStorageDirectory;
-const scanFile = require('../core/file_base_area.js').scanFile;
-const getFileAreaByTag = require('../core/file_base_area.js').getFileAreaByTag;
-const getDescFromFileName = require('../core/file_base_area.js').getDescFromFileName;
-const ansiGoto = require('../core/ansi_term.js').goto;
-const moveFileWithCollisionHandling = require('../core/file_util.js').moveFileWithCollisionHandling;
-const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTerminatingSeparator;
-const Log = require('../core/logger.js').log;
-const Errors = require('../core/enig_error.js').Errors;
-const FileEntry = require('../core/file_entry.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const stringFormat = require('./string_format.js');
+const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
+const getAreaDefaultStorageDirectory = require('./file_base_area.js').getAreaDefaultStorageDirectory;
+const scanFile = require('./file_base_area.js').scanFile;
+const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag;
+const getDescFromFileName = require('./file_base_area.js').getDescFromFileName;
+const ansiGoto = require('./ansi_term.js').goto;
+const moveFileWithCollisionHandling = require('./file_util.js').moveFileWithCollisionHandling;
+const pathWithTerminatingSeparator = require('./file_util.js').pathWithTerminatingSeparator;
+const Log = require('./logger.js').log;
+const Errors = require('./enig_error.js').Errors;
+const FileEntry = require('./file_entry.js');
+const isAnsi = require('./string_util.js').isAnsi;
// deps
const async = require('async');
@@ -421,9 +422,8 @@ exports.getModule = class UploadModule extends MenuModule {
return nextEntry(err);
}
- // if the file entry did *not* have a desc, take the user desc
- if(!this.fileEntryHasDetectedDesc(newEntry)) {
- newEntry.desc = newValues.shortDesc.trim();
+ if(!newEntry.descIsAnsi) {
+ newEntry.desc = _.trimEnd(newValues.shortDesc);
}
if(newValues.estYear.length > 0) {
@@ -659,14 +659,16 @@ exports.getModule = class UploadModule extends MenuModule {
displayFileDetailsPageForUploadEntry(fileEntry, cb) {
const self = this;
- async.series(
+ async.waterfall(
[
function prepArtAndViewController(callback) {
return self.prepViewControllerWithArt(
'fileDetails',
FormIds.fileDetails,
{ clearScreen : true, trailingLF : false },
- callback
+ err => {
+ return callback(err);
+ }
);
},
function populateViews(callback) {
@@ -679,18 +681,32 @@ exports.getModule = class UploadModule extends MenuModule {
tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse
yearView.setText(fileEntry.meta.est_release_year || '');
- if(self.fileEntryHasDetectedDesc(fileEntry)) {
- descView.setPropertyValue('mode', 'preview');
- descView.setText(fileEntry.desc);
- descView.acceptsFocus = false;
- self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.tags);
- } else {
- descView.setPropertyValue('mode', 'edit');
- descView.setText(getDescFromFileName(fileEntry.fileName)); // try to come up with something good as a default
- descView.acceptsFocus = true;
- self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.desc);
- }
+ if(isAnsi(fileEntry.desc)) {
+ fileEntry.descIsAnsi = true;
+ return descView.setAnsi(
+ fileEntry.desc,
+ {
+ prepped : false,
+ forceLineTerm : true,
+ },
+ () => {
+ return callback(null, descView, 'preview', MciViewIds.fileDetails.tags);
+ }
+ );
+ } else {
+ const hasDesc = self.fileEntryHasDetectedDesc(fileEntry);
+ descView.setText(
+ hasDesc ? fileEntry.desc : getDescFromFileName(fileEntry.fileName),
+ { scrollMode : 'top' } // override scroll mode; we want to be @ top
+ );
+ return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc);
+ }
+ },
+ function finalizeViews(descView, descViewMode, focusId, callback) {
+ descView.setPropertyValue('mode', descViewMode);
+ descView.acceptsFocus = 'preview' === descViewMode ? false : true;
+ self.viewControllers.fileDetails.switchFocus(focusId);
return callback(null);
}
],
diff --git a/core/user.js b/core/user.js
index 15e5a844..09d26163 100644
--- a/core/user.js
+++ b/core/user.js
@@ -189,15 +189,13 @@ module.exports = class User {
// :TODO: set various defaults, e.g. default activation status, etc.
self.properties.account_status = Config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active;
- async.series(
+ async.waterfall(
[
function beginTransaction(callback) {
- userDb.run('BEGIN;', err => {
- return callback(err);
- });
+ return userDb.beginTransaction(callback);
},
- function createUserRec(callback) {
- userDb.run(
+ function createUserRec(trans, callback) {
+ trans.run(
`INSERT INTO user (user_name)
VALUES (?);`,
[ self.username ],
@@ -213,11 +211,11 @@ module.exports = class User {
self.properties.account_status = User.AccountStatus.active;
}
- return callback(null);
+ return callback(null, trans);
}
);
},
- function genAuthCredentials(callback) {
+ function genAuthCredentials(trans, callback) {
User.generatePasswordDerivedKeyAndSalt(password, (err, info) => {
if(err) {
return callback(err);
@@ -225,85 +223,56 @@ module.exports = class User {
self.properties.pw_pbkdf2_salt = info.salt;
self.properties.pw_pbkdf2_dk = info.dk;
- return callback(null);
+ return callback(null, trans);
});
},
- function setInitialGroupMembership(callback) {
+ function setInitialGroupMembership(trans, callback) {
self.groups = Config.users.defaultGroups;
if(User.RootUserID === self.userId) { // root/SysOp?
self.groups.push('sysops');
}
- return callback(null);
+ return callback(null, trans);
},
- function saveAll(callback) {
- self.persist(false, err => {
- return callback(err);
+ function saveAll(trans, callback) {
+ self.persistWithTransaction(trans, err => {
+ return callback(err, trans);
});
}
],
- err => {
- if(err) {
- const originalError = err;
- userDb.run('ROLLBACK;', err => {
- assert(!err);
- return cb(originalError);
+ (err, trans) => {
+ if(trans) {
+ trans[err ? 'rollback' : 'commit'](transErr => {
+ return cb(err ? err : transErr);
});
} else {
- userDb.run('COMMIT;', err => {
- return cb(err);
- });
+ return cb(err);
}
}
);
}
- persist(useTransaction, cb) {
+ persistWithTransaction(trans, cb) {
assert(this.userId > 0);
const self = this;
async.series(
[
- function beginTransaction(callback) {
- if(useTransaction) {
- userDb.run('BEGIN;', err => {
- return callback(err);
- });
- } else {
- return callback(null);
- }
- },
function saveProps(callback) {
- self.persistProperties(self.properties, err => {
+ self.persistProperties(self.properties, trans, err => {
return callback(err);
});
},
function saveGroups(callback) {
- userGroup.addUserToGroups(self.userId, self.groups, err => {
+ userGroup.addUserToGroups(self.userId, self.groups, trans, err => {
return callback(err);
});
}
],
err => {
- if(err) {
- if(useTransaction) {
- userDb.run('ROLLBACK;', err => {
- return cb(err);
- });
- } else {
- return cb(err);
- }
- } else {
- if(useTransaction) {
- userDb.run('COMMIT;', err => {
- return cb(err);
- });
- } else {
- return cb(null);
- }
- }
+ return cb(err);
}
);
}
@@ -340,13 +309,18 @@ module.exports = class User {
);
}
- persistProperties(properties, cb) {
+ persistProperties(properties, transOrDb, cb) {
+ if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
+ cb = transOrDb;
+ transOrDb = userDb;
+ }
+
const self = this;
// update live props
_.merge(this.properties, properties);
- const stmt = userDb.prepare(
+ const stmt = transOrDb.prepare(
`REPLACE INTO user_property (user_id, prop_name, prop_value)
VALUES (?, ?, ?);`
);
@@ -440,12 +414,48 @@ module.exports = class User {
if(row) {
return cb(null, row.id, row.user_name);
}
-
+
return cb(Errors.DoesNotExist('No matching username'));
}
);
}
+ static getUserIdAndNameByRealName(realName, cb) {
+ userDb.get(
+ `SELECT id, user_name
+ FROM user
+ WHERE id = (
+ SELECT user_id
+ FROM user_property
+ WHERE prop_name='real_name' AND prop_value LIKE ?
+ );`,
+ [ realName ],
+ (err, row) => {
+ if(err) {
+ return cb(err);
+ }
+
+ if(row) {
+ return cb(null, row.id, row.user_name);
+ }
+
+ return cb(Errors.DoesNotExist('No matching real name'));
+ }
+ );
+ }
+
+ static getUserIdAndNameByLookup(lookup, cb) {
+ User.getUserIdAndName(lookup, (err, userId, userName) => {
+ if(err) {
+ User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => {
+ return cb(err, userId, userName);
+ });
+ } else {
+ return cb(null, userId, userName);
+ }
+ });
+ }
+
static getUserName(userId, cb) {
userDb.get(
`SELECT user_name
diff --git a/core/user_group.js b/core/user_group.js
index 2fcaacf3..3903f2c3 100644
--- a/core/user_group.js
+++ b/core/user_group.js
@@ -33,8 +33,13 @@ function getGroupsForUser(userId, cb) {
});
}
-function addUserToGroup(userId, groupName, cb) {
- userDb.run(
+function addUserToGroup(userId, groupName, transOrDb, cb) {
+ if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
+ cb = transOrDb;
+ transOrDb = userDb;
+ }
+
+ transOrDb.run(
'REPLACE INTO user_group_member (group_name, user_id) ' +
'VALUES(?, ?);',
[ groupName, userId ],
@@ -44,10 +49,10 @@ function addUserToGroup(userId, groupName, cb) {
);
}
-function addUserToGroups(userId, groups, cb) {
+function addUserToGroups(userId, groups, transOrDb, cb) {
async.each(groups, function item(groupName, next) {
- addUserToGroup(userId, groupName, next);
+ addUserToGroup(userId, groupName, transOrDb, next);
}, function complete(err) {
cb(err);
});
diff --git a/mods/user_list.js b/core/user_list.js
similarity index 91%
rename from mods/user_list.js
rename to core/user_list.js
index b2a88e79..be85c586 100644
--- a/mods/user_list.js
+++ b/core/user_list.js
@@ -1,10 +1,10 @@
/* jslint node: true */
'use strict';
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const User = require('../core/user.js');
-const ViewController = require('../core/view_controller.js').ViewController;
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const User = require('./user.js');
+const ViewController = require('./view_controller.js').ViewController;
+const stringFormat = require('./string_format.js');
const moment = require('moment');
const async = require('async');
diff --git a/mods/whos_online.js b/core/whos_online.js
similarity index 87%
rename from mods/whos_online.js
rename to core/whos_online.js
index a0a87829..6abd76ef 100644
--- a/mods/whos_online.js
+++ b/core/whos_online.js
@@ -2,10 +2,10 @@
'use strict';
// ENiGMA½
-const MenuModule = require('../core/menu_module.js').MenuModule;
-const ViewController = require('../core/view_controller.js').ViewController;
-const getActiveNodeList = require('../core/client_connections.js').getActiveNodeList;
-const stringFormat = require('../core/string_format.js');
+const MenuModule = require('./menu_module.js').MenuModule;
+const ViewController = require('./view_controller.js').ViewController;
+const getActiveNodeList = require('./client_connections.js').getActiveNodeList;
+const stringFormat = require('./string_format.js');
// deps
const async = require('async');
diff --git a/docs/config.md b/docs/config.md
index fba48119..98a6730f 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -2,15 +2,18 @@
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 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.
+The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location 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 Windows installations, e.g. `C:\Users\NuSkooler\.config\enigma-bbs\config.hjson`
+### Creating a Configuration
+Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory:
+```
+./oputil.js config new
+```
-### oputil.js
-Please see `oputil.js config` for configuration generation options.
+You will be asked a series of questions to create an initial configuration.
-### Example: System Name
-`core/config.js` provides the default system name as follows:
+### Overriding Defaults
+The file `core/config.js` provides various defaults to the system that you can override via `config.hjson`. For example, the default system name is defined as follows:
```javascript
general : {
boardName : 'Another Fine ENiGMA½ System'
@@ -26,17 +29,14 @@ general: {
(Note the very slightly different syntax. **You can use standard JSON if you wish**)
+While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)!
+
### 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](./)
-
### A Sample Configuration
Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked.
@@ -126,5 +126,8 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all
}
```
-## Menus
-See [the menu system docs](menu_system.md)
+## See Also
+* [Modding](modding.md)
+* [Doors](doors.md)
+* [MCI Codes](mci.md)
+* [Menu System docs](menu_system.md)
diff --git a/docs/doors.md b/docs/doors.md
index 702d75d9..5f72d761 100644
--- a/docs/doors.md
+++ b/docs/doors.md
@@ -183,7 +183,7 @@ The module `door_party` provides native support for [DoorParty!](http://www.thro
```hjson
doorParty: {
desc: Using DoorParty!
- module: @systemModule:door_party
+ module: door_party
config: {
username: XXXXXXXX
password: XXXXXXXX
@@ -194,6 +194,22 @@ doorParty: {
Fill in `username`, `password`, and `bbsTag` with credentials provided to you and you should be in business!
+## The CombatNet Module
+The `combatnet` module provides native support for [CombatNet](http://combatnet.us/). Add the following to your menu config:
+
+````hjson
+combatNet: {
+ desc: Using CombatNet
+ module: combatnet
+ config: {
+ bbsTag: CBNxxx
+ password: XXXXXXXXX
+ }
+}
+````
+Update `bbsTag` (in the format CBNxxx) and `password` with the details provided when you register, then
+you should be ready to rock!
+
# Resources
### DOSBox
diff --git a/docs/file_base.md b/docs/file_base.md
index bfd802b9..893a93de 100644
--- a/docs/file_base.md
+++ b/docs/file_base.md
@@ -1,7 +1,7 @@
# 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
+## A Different Approach
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!)
diff --git a/docs/images/vtxclient.png b/docs/images/vtxclient.png
new file mode 100644
index 00000000..99261ced
Binary files /dev/null and b/docs/images/vtxclient.png differ
diff --git a/docs/index.md b/docs/index.md
index b57fac84..2320829c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -24,7 +24,7 @@ For Windows environments or if you simply like to do things manually, read on...
### New to Node
-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!):
+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 environments (Please consider the `install.sh` approach unless you really want to manually install!):
```bash
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
@@ -51,11 +51,11 @@ npm install
## Generate a SSH Private Key
To utilize the SSH server, a SSH Private Key will need generated. This step can be skipped if you do not wish to enable SSH access.
```bash
-openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
+openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048
```
### Create a Minimal Config
-The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information.
+The main system configuration is handled via `/enigma-bbs-install-path/config/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information.
#### Via oputil.js
`oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**:
@@ -64,7 +64,7 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
./oputil.js config new
```
-(You wil be asked a series of basic questions)
+(You will 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**.
@@ -110,7 +110,7 @@ Below is an _example_ configuration. It is recommended that you at least **start
Read the Points of Interest below for more info. Also check-out all the other documentation files in the [docs](.) directory.
## Points of Interest
-* **The first user you create via register/applying (user ID = 1) will be automatically be added to the `sysops` group. And thus becomes SysOp.** (aka root)
+* **The first user you create via register/applying (user ID = 1) will be automatically be added to the `sysops` group, and thus becomes SysOp.** (aka root)
* Default port for Telnet is 8888 and for SSH 8889
* Note that on *nix systems port such as telnet/23 are privileged (e.g. require root). See [this SO article](http://stackoverflow.com/questions/16573668/best-practices-when-running-node-js-with-port-80-ubuntu-linode) for some tips on using these ports on your system if desired.
* All data is stored by default in Sqlite3 database files, within the `db` sub folder. Including user data, messages, system logs and file meta data.
diff --git a/docs/menu_system.md b/docs/menu_system.md
index 1dea637c..aef51199 100644
--- a/docs/menu_system.md
+++ b/docs/menu_system.md
@@ -1,17 +1,26 @@
# 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.
+ENiGMA½'s menu system is highly flexible and moddable. The possibilities are almost endless!
-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:
+This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` (or whatever you reference in `config.hjson` using the `menuFile` property — see below). By modifying your `menu.hjson` you will be able to create a custom experience unique to your board.
+
+The default `menu.hjson` file lives within the `config` 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
+ menuFile: yourboardname.hjson
}
```
-(You can start by copying the default `menu.hjson` to `mybbs.hjson`)
+
+You can start by copying the default `mods/menu.hjson` to `yourboardname.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`.
+Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format.
+
+Entries in `menu.hjson` are objects defining a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to:
+* Classical Main, Messages, and File menus
+* Art file display
+* Module driven menus such as door launchers
+
Each entry in `menu.hjson` defines an object that represents a menu. These objects live within the `menus` parent object. Each object's *key* is a menu name you can reference within other menus in the system.
@@ -26,9 +35,9 @@ telnetConnected: {
}
```
-The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the server's config).
+The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config).
-An art pattern of `CONNECT` is set telling the system to look for `CONNECT` in the current theme location, then in the common `mods/art` directory where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. You can be explicit here if desired, by specifying a file extension.
+An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`.
The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`.
@@ -47,20 +56,21 @@ matrix: {
submit: true
focus: true
items: [ "login", "apply", "log off" ]
+ argName: matrixSubmit
}
}
submit: {
*: [
{
- value: { 1: 0 }
+ value: { matrixSubmit: 0 }
action: @menu:login
}
{
- value: { 1: 1 },
+ value: { matrixSubmit: 1 },
action: @menu:newUserApplication
}
{
- value: { 1: 2 },
+ value: { matrixSubmit: 2 },
action: @menu:logoff
}
]
@@ -71,6 +81,6 @@ matrix: {
}
```
-In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined.
+In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` for this action as `matrixSubmit`.
-The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ 1: 0 }` or view ID 1, value 0 will match causing `action` of `@menu:login` to be executed (go to `login` menu).
\ No newline at end of file
+The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go to `login` menu).
diff --git a/docs/modding.md b/docs/modding.md
new file mode 100644
index 00000000..609729b4
--- /dev/null
+++ b/docs/modding.md
@@ -0,0 +1,15 @@
+# Modding
+
+## General Configuraiton
+See [Configuration](config.md)
+
+## Menus
+See [Menu System](menu_system.md)
+
+## Theming
+Take a look at how the default `luciano_blocktronics` theme found under `art/themes` works!
+
+TODO document me!
+
+## Add-On Modules
+See [Mods](mods.md)
\ No newline at end of file
diff --git a/docs/mods.md b/docs/mods.md
new file mode 100644
index 00000000..f73a3fc6
--- /dev/null
+++ b/docs/mods.md
@@ -0,0 +1,9 @@
+# Mods
+Custom mods should be added to `/enigma-install-path/mods`.
+
+## Existing Mods
+* **Married Bob Fetch Event**: An event for fetching the latest Married Bob ANSI's for display on you board. ACiDic release [ACD-MB4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MB4E.ZIP). Can also be [found on GitHub](https://github.com/NuSkooler/enigma-bbs-married_bob_evt)
+* **Latest Files Announcement**: An event for posting the latest file arrivals of your board to message areas such as FTN style networks. ACiDic release [ACD-LFA1.ZIP](https://l33t.codes/outgoing/ACD/ACD-LFA1.ZIP) Also [found on GitHub](https://github.com/NuSkooler/enigma-bbs-latest_files_announce_evt)
+* **Message Post Event**: An event for posting messages/ads to networks. ACiDic release [ACD-MP4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MP4E.ZIP)
+
+See also [ACiDic BBS Mods by Myself](https://l33t.codes/acidic-mods-by-myself/)
\ No newline at end of file
diff --git a/docs/msg_networks.md b/docs/msg_networks.md
index ca0afe17..11fa9202 100644
--- a/docs/msg_networks.md
+++ b/docs/msg_networks.md
@@ -6,7 +6,7 @@ Message networks are configured in `messageNetworks` section of `config.hjson`.
* `originLine` (optional): Overrwrite the default origin line for networks that support it. For example: `originLine: Xibalba - xibalba.l33t.codes:44510`
## FidoNet Technology Network (FTN)
-FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`.
+FTN networks are configured under the `messageNetworks.ftn` section of `config.hjson`.
### Networks
The `networks` section contains a sub section for network(s) you wish you join your board with. Each entry's key name can be referenced elsewhere in `config.hjson` for FTN oriented configurations.
@@ -30,7 +30,7 @@ The `networks` section contains a sub section for network(s) you wish you join y
```
### Areas
-The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages.
+The `areas` section describes a mapping of local **area tags** found in your `messageConferences` to a message network (from `networks` described previously), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages (In fact you can import AREAS.BBS using `oputil.js`!)
When importing, messages will be placed in the local area that matches key under `areas`.
@@ -57,11 +57,11 @@ When importing, messages will be placed in the local area that matches key under
```
### BSO Import / Export
-The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers::ftn_bso`.
+The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss & scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`.
**Members**:
* `defaultZone` (required): Sets the default BSO outbound zone
- * `defaultNetwork` (optional): Sets the default network name from `messageNetworks::ftn::networks`. **Required if more than one network is defined**.
+ * `defaultNetwork` (optional): Sets the default network name from `messageNetworks.ftn.networks`. **Required if more than one network is defined**.
* `paths` (optional): Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`.
* `packetTargetByteSize` (optional): Overrides the system *target* packet (.pkt) size of 512000 bytes (512k)
* `bundleTargetByteSize` (optional): Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M)
@@ -85,7 +85,7 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config.
scannerTossers: {
ftn_bso: {
nodes: {
- "46:*: {
+ "46:*": {
packetType: 2+
packetPassword: mypass
encoding: cp437
@@ -97,6 +97,78 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config.
}
```
+#### TIC Support
+ENiGMA½ supports TIC files. This is handled by mapping TIC areas to local file areas.
+
+Under a given node (like the one configured above), TIC configuration may be supplied:
+
+```hjson
+{
+ scannerTossers: {
+ ftn_bso: {
+ nodes: {
+ "46:*": {
+ packetType: 2+
+ packetPassword: mypass
+ encoding: cp437
+ archiveType: zip
+ tic: {
+ password: TESTY-TEST
+ uploadBy: Agoranet TIC
+ allowReplace: true
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+You then need to configure the mapping between TIC areas you want to carry, and the file
+base area for them to be tossed to. Start by creating a storage tag and file base, if you haven't
+already:
+
+````hjson
+fileBase: {
+ areaStoragePrefix: /home/bbs/file_areas/
+
+ storageTags: {
+ msg_network: "msg_network"
+ }
+
+ areas: {
+ msgNetworks: {
+ name: Message Networks
+ desc: Message networks news & info
+ storageTags: [
+ "msg_network"
+ ]
+ }
+ }
+}
+
+````
+and then create the mapping between the TIC area and the file area created:
+
+````hjson
+ticAreas: {
+ agn_node: {
+ areaTag: msgNetworks
+ hashTags: agoranet,nodelist
+ storageTag: msg_network
+ }
+
+ agn_info: {
+ areaTag: msgNetworks
+ hashTags: agoranet,infopack
+ storageTag: msg_network
+ }
+}
+
+````
+Multiple TIC areas can be mapped to a single file base area.
+
+
#### Scheduling
Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers.
diff --git a/docs/rpi.md b/docs/rpi.md
index 4794bbcf..f474e959 100644
--- a/docs/rpi.md
+++ b/docs/rpi.md
@@ -3,39 +3,32 @@
ENiGMA½ can run under your Linux / RPi installation! The following instructions should help get you started.
## Tested RPi Models
-###Model A
-Works, but fairly slow (Node itself is not the fastest on this device). May work better overlocked, etc.
+### Model A
+Works, but fairly slow when browsing message areas (Node itself is not the fastest on this device). May work better overlocked, etc.
-###v2 Model B
-Works well with default rasbian, follow the normal quickstart install procedure, except for installing nodejs. To install nodejs do the following:
-
- curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
- sudo apt-get install -y nodejs
+### v2 Model B
+Works well with Raspbian!
Keep in mind, compiling the dependencies with `npm install` will take some time and appear to hang. Just be patient.
-##Example Configuration: RPi Model A + Minibian
+## Example Configuration: RPi Model A + Raspbian Stretch Lite
### Basic Instructions
-1. Download and `dd` the Minibian .img file from https://minibianpi.wordpress.com/ to a SDCARD. Cards >= 16GB recommended.
-2. After booting Minibian, expand your file system. See http://elinux.org/RPi_Resize_Flash_Partitions#Manually_resizing_the_SD_card_on_Raspberry_Pi for information.
-3. Update & upgrade: `apt-get update && apt-get upgrade`
-4. It is recommended that you install `sudo` and create an admin user: `apt-get install sudo`, `adduser `, `adduser sudo` (reboot & login as the user your just created)
-5. We want to build dependencies with a updated version of GCC. The following works to install GCC 4.9 on Minibian "wheezy":
-a. Update */etc/apt/sources.list* replacing all "wheezy" with "jessie"
-b. `sudo apt-get update`
-c. `sudo apt-get install gcc-4.9 g++-4.9`
-d. Update */etc/apt/sources.list* reverting all "jessie" back to "wheezy"
-e. `sudo apt-get update`
-f. Update alternatives: `sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 60 --slave /usr/bin/g++ g++ /usr/bin/g++-4.9`
-6. Install dependencies: `sudo apt-get install make python libicu-dev libssl-dev git`
-7. Install the latest Node.js from here: http://node-arm.herokuapp.com/ (**only download the .dep and dpkg install it!**)
-8. The RPi A has very low memory, we'll need a swap file:
-a. `sudo dd if=/dev/zero of=tmpswap bs=1024 count=1M`
-b. `sudo mkswap tmpswap`
-c. `sudo swapon tmpswap`
-9. Clone enigma-bbs.git
-10. Install dependencies. Here we will force GCC 4.9 for compilation: `CC=gcc-4.9 npm install`
-11. Follow generic setup for creating a config.hjson, etc. and you should be ready to go!
+1. Download [Raspbian Stretch Lite](https://www.raspberrypi.org/downloads/raspbian/). Follow the instructions
+on the [Raspbian site](https://www.raspberrypi.org/documentation/installation/installing-images/README.md) regarding how
+to get it written to an SD card.
+
+2. Run `sudo raspi-config`, then:
+ 1. Set your timezone (option 4, option I2)
+ 2. Enable SSH (option 5, option P2)
+ 3. Expand the filesystem to use the entire SD card (option 7, option A1)
+
+3. Update & upgrade all packages: `apt-get update && apt-get upgrade`
+
+4. Install required packages: `sudo apt install lrzsz p7zip-full`
+
+5. Follow the [Quickstart](docs/index.md) instructions to install ENiGMA½.
+
+6. Profit!
diff --git a/docs/vtx_web_client.md b/docs/vtx_web_client.md
new file mode 100644
index 00000000..99c45f93
--- /dev/null
+++ b/docs/vtx_web_client.md
@@ -0,0 +1,87 @@
+# VTX Web Client
+ENiGMA supports the VTX websocket client for connecting to your BBS from a web page. Example usage can be found at
+[Xibalba](https://l33t.codes/vtx/xibalba.html) and [fORCE9](https://bbs.force9.org/vtx/force9.html).
+
+## Before You Start
+
+There are a few things out of scope of this document:
+
+ - You'll need a web server for hosting the files - this can be anywhere, but it obviously makes sense to host it
+ somewhere with a hostname relevant to your BBS!
+
+ - It's not required, but you should use SSL certificates to secure your website, and for supplying to ENiGMA to
+ secure the websocket connections. [Let's Encrypt](https://letsencrypt.org/) provide a free well-respected service.
+
+ - How you make the websocket service available on the internet is up to you, but it'll likely by forwarding ports on
+ your router to the box hosting ENiGMA. Use the same method you did for forwarding the telnet port.
+
+## Setup
+
+1. Enable the websocket in ENiGMA, by adding `webSocket` configuration to the `loginServers` block (create it if you
+don't already have it defined).
+
+ ````hjson
+ loginServers: {
+ webSocket : {
+ port: 8810
+ enabled: true
+ securePort: 8811
+ certPem: /path/to/https_cert.pem
+ keyPem: /path/to/https_cert_key.pem
+ }
+ }
+ ````
+
+2. Restart ENiGMA and check the logs to ensure the websocket service starts successfully, you'll see something like the
+following:
+
+ ````
+ [2017-10-29T12:13:30.668Z] INFO: ENiGMA½ BBS/30978 on force9: Listening for connections (server="WebSocket (insecure)", port=8810)
+ [2017-10-29T12:13:30.669Z] INFO: ENiGMA½ BBS/30978 on force9: Listening for connections (server="WebSocket (secure)", port=8811)
+ ````
+
+3. Download the [VTX_ClientServer](https://github.com/codewar65/VTX_ClientServer/archive/master.zip) to your
+webserver, and unpack it to a temporary directory.
+
+4. Download the example [VTX client HTML file](/misc/vtx/vtx.html) and save it to your webserver root.
+
+5. Create an `assets/vtx` directory within your webserver root, so you have a structure like the following:
+
+ ````text
+ ├── assets
+ │ └── vtx
+ └── vtx.html
+ ````
+
+6. From the VTX_ClientServer package unpacked earlier, copy the contents of the `www` directory into `assets/vtx` directory.
+
+7. Create a vtxdata.js file, and save it to `assets/vtx`:
+
+ ````javascript
+ var vtxdata = {
+ sysName: "Your Awesome BBS",
+ wsConnect: "wss://your-hostname.here:8811"
+ term: "ansi-bbs",
+ codePage: "CP437",
+ fontName: "UVGA16",
+ fontSize: "24px",
+ crtCols: 80,
+ crtRows: 25,
+ crtHistory: 500,
+ xScale: 1,
+ initStr: "",
+ defPageAttr: 0x1010,
+ defCrsrAttr: 0x0207,
+ defCellAttr: 0x0007,
+ telnet: 1,
+ autoConnect: 0
+ };
+ ````
+
+8. Update `sysName` and `wsConnect` accordingly. Use `wss://` if you set up the websocket service with SSL, `ws://`
+otherwise.
+
+9. If you navigate to http://your-hostname.here/vtx.html, you should see a splash screen like the following:
+ ![VTXClient](images/vtxclient.png "VTXClient")
+
+
\ No newline at end of file
diff --git a/docs/web_server.md b/docs/web_server.md
index 8c341191..1f87cd80 100644
--- a/docs/web_server.md
+++ b/docs/web_server.md
@@ -1,8 +1,10 @@
# 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!
+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`:
+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: {
@@ -16,12 +18,17 @@ contentServers: {
}
```
-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:
+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. [LetsEncrypt](https://letsencrypt.org/) supply free trusted
+certificates that work perfectly with ENiGMA½.
+
+Once obtained, simply enable the HTTPS server:
+
```hjson
contentServers: {
web: {
domain: bbs.yourdomain.com
- // set 'overrideUrlPrefix' if for example, you use a transparent proxy in front of ENiGMA and need to be explicit about URLs the system hands out
+ // set 'overrideUrlPrefix' if for example, you use a transparent proxy in front of ENiGMA and need to be explicit about URLs the system hands out
overrideUrlPrefix: https://bbs.yourdomain.com
https: {
enabled: true
@@ -37,4 +44,5 @@ contentServers: {
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 `.html` file in the *static routes* area. For example: `404.html`.
+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 `.html` file in the *static routes* area. For example: `404.html`.
diff --git a/main.js b/main.js
index 0bc7bee9..53a320f6 100755
--- a/main.js
+++ b/main.js
@@ -7,6 +7,6 @@
ENiGMA½ entry point
If this file does not run directly, ensure it's executable:
- > chmod u+x main.js
+ > chmod u+x main.js
*/
require('./core/bbs.js').main();
\ No newline at end of file
diff --git a/misc/vtx/vtx.html b/misc/vtx/vtx.html
new file mode 100644
index 00000000..156246ed
--- /dev/null
+++ b/misc/vtx/vtx.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mods/.keep b/mods/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/mods/file_base_area_select.js b/mods/file_base_area_select.js
deleted file mode 100644
index 5eef583b..00000000
--- a/mods/file_base_area_select.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/* 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;
-const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
-
-// deps
-const async = require('async');
-const _ = require('lodash');
-
-exports.moduleInfo = {
- name : 'File Area Selector',
- desc : 'Select from available file areas',
- author : 'NuSkooler',
-};
-
-const MciViewIds = {
- areaList : 1,
-};
-
-exports.getModule = class FileAreaSelectModule extends MenuModule {
- constructor(options) {
- super(options);
-
- this.config = this.menuConfig.config || {};
-
- this.loadAvailAreas();
-
- this.menuMethods = {
- selectArea : (formData, extraArgs, cb) => {
- const area = this.availAreas[formData.value.areaSelect] || 0;
-
- const filterCriteria = {
- areaTag : area.areaTag,
- };
-
- const menuOpts = {
- extraArgs : {
- filterCriteria : filterCriteria,
- },
- menuFlags : [ 'noHistory' ],
- };
-
- return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
- }
- };
- }
-
- loadAvailAreas() {
- this.availAreas = getSortedAvailableFileAreas(this.client);
- }
-
- mciReady(mciData, cb) {
- super.mciReady(mciData, err => {
- if(err) {
- return cb(err);
- }
-
- this.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => {
- if(err) {
- return cb(err);
- }
-
- const areaListView = vc.getView(MciViewIds.areaList);
-
- const areaListFormat = this.config.areaListFormat || '{name}';
-
- areaListView.setItems(this.availAreas.map(a => stringFormat(areaListFormat, a) ) );
-
- if(this.config.areaListFocusFormat) {
- areaListView.setFocusItems(this.availAreas.map(a => stringFormat(this.config.areaListFocusFormat, a) ) );
- }
-
- areaListView.redraw();
-
- return cb(null);
- });
- });
- }
-};
diff --git a/mods/themes/luciano_blocktronics/MSGMNU.ANS b/mods/themes/luciano_blocktronics/MSGMNU.ANS
deleted file mode 100644
index e27fed73..00000000
Binary files a/mods/themes/luciano_blocktronics/MSGMNU.ANS and /dev/null differ
diff --git a/package.json b/package.json
index fb04a0f4..678628f1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "enigma-bbs",
- "version": "0.0.7-alpha",
+ "version": "0.0.8-alpha",
"description": "ENiGMA½ Bulletin Board System",
"author": "Bryan Ashby ",
"license": "BSD-2-Clause",
@@ -22,34 +22,37 @@
"retro"
],
"dependencies": {
- "async": "^2.4.0",
+ "async": "^2.5.0",
"binary": "0.3.x",
"buffers": "NuSkooler/node-buffers",
- "bunyan": "^1.8.10",
+ "bunyan": "^1.8.12",
"exiftool": "^0.0.3",
- "farmhash": "^1.2.1",
- "fs-extra": "^3.0.1",
+ "fs-extra": "^5.0.0",
+ "glob": "^7.1.2",
"graceful-fs": "^4.1.11",
"hashids": "^1.1.1",
- "hjson": "^2.4.2",
- "iconv-lite": "^0.4.17",
- "inquirer": "^3.0.6",
+ "hjson": "^3.1.0",
+ "iconv-lite": "^0.4.18",
+ "inquirer": "^4.0.1",
"later": "1.2.0",
"lodash": "^4.17.4",
- "mime-types": "^2.1.15",
+ "mime-types": "^2.1.17",
"minimist": "1.2.x",
- "moment": "^2.18.1",
- "node-glob": "^1.2.0",
- "nodemailer": "^4.0.1",
+ "moment": "^2.20.0",
+ "nodemailer": "^4.4.1",
"ptyw.js": "NuSkooler/ptyw.js",
+ "rlogin": "^1.0.0",
"sane": "^2.2.0",
"sanitize-filename": "^1.6.1",
- "sqlite3": "^3.1.1",
+ "sqlite3": "^3.1.9",
+ "sqlite3-trans": "^1.2.0",
"ssh2": "^0.5.5",
"temptmp": "^1.0.0",
- "uuid": "^3.0.1",
+ "uuid": "^3.1.0",
"uuid-parse": "^1.0.0",
- "ws": "^3.0.0"
+ "ws": "^3.3.3",
+ "xxhash": "^0.2.4",
+ "yazl": "^2.4.2"
},
"devDependencies": {},
"engines": {
diff --git a/util/dump_ftn_packet.js b/util/dump_ftn_packet.js
new file mode 100755
index 00000000..144d18f3
--- /dev/null
+++ b/util/dump_ftn_packet.js
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+
+/* jslint node: true */
+/* eslint-disable no-console */
+'use strict';
+
+const { Packet } = require('../core/ftn_mail_packet.js');
+
+const argv = require('minimist')(process.argv.slice(2));
+
+function main() {
+ if(0 === argv._.length) {
+ console.error('usage: dump_ftn_packet.js PATH');
+ process.exitCode = -1;
+ return;
+ }
+
+ const packet = new Packet();
+ const packetPath = argv._[0];
+
+ packet.read(
+ packetPath,
+ (dataType, data, next) => {
+ if('header' === dataType) {
+ console.info('--- header ---');
+ console.info(`Created : ${data.created.format('dddd, MMMM Do YYYY, h:mm:ss a')}`);
+ console.info(`Dst. Addr : ${data.destAddress.toString()}`);
+ console.info(`Src. Addr : ${data.origAddress.toString()}`);
+ console.info('--- raw header ---');
+ console.info(data);
+ console.info('--------------');
+ console.info('');
+ } else if('message' === dataType) {
+ console.info('--- message ---');
+ console.info(`To : ${data.toUserName}`);
+ console.info(`From : ${data.fromUserName}`);
+ console.info(`Subject : ${data.subject}`);
+ console.info('--- raw message ---');
+ console.info(data);
+ console.info('---------------');
+ }
+
+ return next(null);
+ },
+ () => {
+ console.info('');
+ console.info('--- EOF --- ');
+ console.info('');
+ }
+ );
+}
+
+main();
diff --git a/util/exiftool2desc.js b/util/exiftool2desc.js
index 210c800d..4b3b350f 100755
--- a/util/exiftool2desc.js
+++ b/util/exiftool2desc.js
@@ -43,12 +43,12 @@ function documentFile(metadata) {
return;
}
- let desc = `${metadata.author||'Unknown Author'} - ${metadata.title||'Unknown'}`;
- const created = moment(metadata.createdate);
- if(created.isValid()) {
- desc += ` (${created.format('YYYY')})`;
+ let result = metadata.author || '';
+ if(result) {
+ result += ' - ';
}
- return desc;
+ result += metadata.title || 'Unknown Title';
+ return result;
}
function imageFile(metadata) {