Merge 0.0.9-alpha into develop
This commit is contained in:
commit
562c73e096
|
@ -0,0 +1,2 @@
|
|||
# ACS parser is generated
|
||||
core/acs_parser.js
|
|
@ -7,7 +7,7 @@
|
|||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
4,
|
||||
{
|
||||
"SwitchCase" : 1
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ For :bug: bug reports, please fill out the information below plus any additional
|
|||
**Short problem description**
|
||||
|
||||
**Environment**
|
||||
- [ ] I am using Node.js v6.x or higher
|
||||
- [ ] `npm install` reports success
|
||||
- [ ] I am using Node.js v10.x LTS or higher
|
||||
- [ ] `npm install` or `yarn` reports success
|
||||
- Actual Node.js version (`node --version`):
|
||||
- Operating system (`uname -a` on *nix systems):
|
||||
- Revision (`git rev-parse --short HEAD`):
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
*.pem
|
||||
|
||||
# Various directories
|
||||
config/config.hjson
|
||||
logs/
|
||||
db/
|
||||
dropfiles/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2015-2018, Bryan D. Ashby
|
||||
Copyright (c) 2015-2019, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
46
README.md
46
README.md
|
@ -12,29 +12,27 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
|
|||
* [MCI support](docs/art/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
|
||||
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior
|
||||
* Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support
|
||||
* 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
|
||||
* [SyncTERM](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior.
|
||||
* Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support.
|
||||
* Renegade style [pipe color codes](/docs/configuration/colour-codes.md).
|
||||
* [SQLite](http://sqlite.org/) storage of users, message areas, etc.
|
||||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption.
|
||||
* [Door support](docs/modding/door-servers.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/messageareas/message-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/servers/web-server.md). Legacy X/Y/Z modem also supported!
|
||||
* [Bunyan](https://github.com/trentm/node-bunyan) logging!
|
||||
* [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be exposed via [Gopher](docs/servers/gopher.md), or [NNTP](docs/servers/nntp.md)!
|
||||
* [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/web-server.md). Legacy X/Y/Z modem also supported!
|
||||
* Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more!
|
||||
* ANSI support in the Full Screen Editor (FSE), file descriptions, and so on
|
||||
* ANSI support in the Full Screen Editor (FSE), file descriptions, etc.
|
||||
* A built in achievement system. BBSing gamified!
|
||||
|
||||
## Documentation
|
||||
[Browse the docs online](https://nuskooler.github.io/enigma-bbs/)
|
||||
[Browse the docs online](https://nuskooler.github.io/enigma-bbs/). Be sure to checkout the [/docs/](/docs/) folder as well for the latest and greatest documentation.
|
||||
|
||||
## In the Works
|
||||
* More ACS support coverage
|
||||
* SysOp dashboard (ye ol' WFC)
|
||||
* Native DOS emulation
|
||||
* A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
||||
Many more features are in the pipeline. Checkout the [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) and feel free to request features (or contribute!) features.
|
||||
|
||||
## Known Issues
|
||||
As of now this is considered **alpha** code! Please **expect bugs** :bug: -- and when you find them, log issues and/or submit pull requests. Feature requests, suggestions, and so on are always welcome! I am also **looking for semi dedicated testers, artists, etc**!
|
||||
As of now this is considered **alpha** code! Please **expect bugs** :bug: -- and when you find them, log issues and/or submit pull requests. With that said, the code is actually quite stable and is used by a number of boards.
|
||||
|
||||
See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more information.
|
||||
|
||||
|
@ -52,21 +50,25 @@ ENiGMA has been tested with many terminals. However, the following are suggested
|
|||
* [SyncTERM](http://syncterm.bbsdev.net/)
|
||||
* [EtherTerm](https://github.com/M-griffin/EtherTerm)
|
||||
* [NetRunner](http://mysticbbs.com/downloads.html)
|
||||
* [MagiTerm](https://magickabbs.com/index.php/magiterm/)
|
||||
|
||||
## Boards
|
||||
* WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**telnet://xibalba.l33t.codes:44510** or via SSH secure on port 44511)
|
||||
* [fORCE9](https://bbs.force9.org/): (**telnet://bbs.force9.org**)
|
||||
* WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**ssh://xibalba.l33t.codes:44511** or **telnet://xibalba.l33t.codes:44510**)
|
||||
* [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**)
|
||||
* [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**)
|
||||
* [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**)
|
||||
|
||||
|
||||
## Installation
|
||||
On *nix type systems:
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash
|
||||
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh | bash
|
||||
```
|
||||
|
||||
Please see the [Quickstart](docs/index.md) for more information.
|
||||
Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) for Windows, Docker, and so on...
|
||||
|
||||
## Special Thanks
|
||||
* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc.
|
||||
* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) and [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc.
|
||||
* [Daniel Mecklenburg Jr.](https://github.com/codewar65) for the awesome VTX terminal and general coding talk
|
||||
* [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!)
|
||||
* [M. Griffin](https://github.com/M-griffin), author of [Enthral BBS](https://github.com/M-griffin/Enthral), [Oblivion/2 XRM](https://github.com/M-griffin/Oblivion2-XRM) and [EtherTerm](https://github.com/M-griffin/EtherTerm)!
|
||||
|
@ -77,11 +79,13 @@ Please see the [Quickstart](docs/index.md) for more information.
|
|||
* Avon of [Agency BBS](http://bbs.geek.nz/) and [fsxNet](http://bbs.geek.nz/#fsxNet) for putting up with my experiments to his system
|
||||
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
|
||||
* [Apam](https://github.com/apamment) of [Magicka](https://magickabbs.com/)
|
||||
* [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)!
|
||||
* [Whazzit/blocktronics](http://blocktronics.org/tag/whazzit/) for the amazing Mayan ANSI pieces scattered about Xibalba BBS!
|
||||
|
||||
## License
|
||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
|
||||
|
||||
Copyright (c) 2015-2018, Bryan D. Ashby
|
||||
Copyright (c) 2015-2019, Bryan D. Ashby
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
42
UPGRADE.md
42
UPGRADE.md
|
@ -1,16 +1,17 @@
|
|||
# Introduction
|
||||
This document covers basic upgrade notes for major ENiGMA½ version updates.
|
||||
|
||||
|
||||
# Before Upgrading
|
||||
* Always back up your system!
|
||||
* Seriously, 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:
|
||||
## Configuration File Updates
|
||||
In general, look at the `menu_template.in.hjson`, and `config_template.in.hjson` as well as the defualt `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system!
|
||||
|
||||
### menu.hjson
|
||||
Upgrades often come with changes to the default `menu_template.in.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: {
|
||||
|
@ -21,6 +22,9 @@ general: {
|
|||
After updating code, use a program such as DiffMerge to merge in updates to
|
||||
`my_bbs.hjson` from the shipping `menu.hjson`.
|
||||
|
||||
### theme.hjson
|
||||
Any custom themes you have created may now be missing features as well. Take a look at the default `luciano_blocktronics/theme.hjson` file. You can use missing sections in your `theme.hjson` (which will generally correspond to sections you've also merged in to your `menu.hjson`).
|
||||
|
||||
|
||||
# Upgrading the Code
|
||||
Upgrading from GitHub is easy:
|
||||
|
@ -32,11 +36,37 @@ 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.8-alpha to 0.0.9-alpha
|
||||
* Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha!
|
||||
* The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well.
|
||||
* Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork.
|
||||
* Removed view width auto-size: Some views still can auto-size their height, but in general you should be explicit in your themes
|
||||
* More standardization using "custom ranges" and `itemFormat` / `focusItemFormat` semantics. Update your themes!
|
||||
* In addition to using `itemFormat`, the `onelinerz` module uses `userName` vs `username` (note the case) to match other modules
|
||||
* `loginServers.webSocket` configuration block has changed to be more consistent with other servers. Example:
|
||||
```
|
||||
webSocket: {
|
||||
ws: {
|
||||
enabled: true
|
||||
}
|
||||
wss: {
|
||||
enabled: true
|
||||
port: 1234
|
||||
}
|
||||
proxied: true // X-Forwarded-Proto: https support
|
||||
}
|
||||
```
|
||||
* The module export `registerEvents` has been deprecated. If you have a module that depends on this, use the new more generic `moduleInitialize` export instead.
|
||||
* The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup.
|
||||
* If you have art configured for message conference or area selection via the `art` configuration value, you will need to include a `show_art` menu reference. Defaulted to `changeMessageConfPreArt` for conferences and `changeMessageAreaPreArt` for areas & included in the example `menu.hjson`.
|
||||
* Config `defaults` section was theme related and as such, has been renamed to `theme`. `defaults.theme` is now `theme.default`, and `preLoginTheme` is now `theme.preLogin`. See `config.js` if this isn't clear as mud.
|
||||
* Similar to the last item, `defaults.general.passwordChar` in `theme.hjson` is now just `defaults.passwordChar`.
|
||||
|
||||
|
||||
# 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**
|
||||
|
|
31
WHATSNEW.md
31
WHATSNEW.md
|
@ -1,6 +1,34 @@
|
|||
# Whats New
|
||||
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
|
||||
|
||||
## 0.0.9-alpha
|
||||
* Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV!
|
||||
* Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!)
|
||||
* Menu items can now be arrays of *objects* not just arrays of strings.
|
||||
* The properties `itemFormat` and `focusItemFormat` allow you to supply the string format for items. For example if a menu object is `{ "userName" : "Bob", "age" : 35 }`, a `itemFormat` might be `|04{userName} |08- |14{age}`.
|
||||
* If no `itemFormat` is supplied, the default formatter is `{text}`.
|
||||
* Setting the `data` member of an object will cause form submissions to use this value instead of the selected items index.
|
||||
* See the default `luciano_blocktronics` `matrix` menu for example usage.
|
||||
* You can now set the `sort` property on a menu to sort items. If `true` items are sorted by `text`. If the value is a string, it represents the key in menu objects to sort by.
|
||||
* Hot-reload of configuration files such as menu.hjson, config.hjson, your themes.hjson, etc.: When a file is saved, it will be hot-reloaded into the running system
|
||||
* Note that any custom modules should make use of the new Config.get() method.
|
||||
* The old concept of `autoScale` has been removed. See https://github.com/NuSkooler/enigma-bbs/issues/166
|
||||
* Ability to delete from personal mailbox (finally!)
|
||||
* Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson
|
||||
* `{userName}` (sanitized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door.
|
||||
* Any module may now register for a system startup initialization via the `initializeModules(initInfo, cb)` export.
|
||||
* User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`!
|
||||
* New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md)
|
||||
* `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected.
|
||||
* `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout.
|
||||
* Handling of failed login attempts is now fully in. Disconnect clients, lock out accounts, ability to auto or unlock at (email-driven) password reset, etc. See `users.failedLogin` in `config.hjson`.
|
||||
* NNTP support! See [NNTP docs](/docs/servers/nntp.md) for more information.
|
||||
* `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md).
|
||||
* Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats.
|
||||
* Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt.
|
||||
* Total minutes online is now tracked for users. Of course, it only starts after you get the update :)
|
||||
|
||||
|
||||
## 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.
|
||||
|
@ -15,7 +43,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
|
|||
* 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 <address>` 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
|
||||
* `oputil.js fb rm|remove|del|delete` functionality to remove file base entries.
|
||||
* `oputil.js fb desc` for setting/updating a file entry description.
|
||||
* 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.
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -9,9 +9,7 @@
|
|||
customization: {
|
||||
|
||||
defaults: {
|
||||
general: {
|
||||
passwordChar: *
|
||||
}
|
||||
|
||||
dateTimeFormat: {
|
||||
short: MMM Do h:mm a
|
||||
|
@ -22,7 +20,8 @@
|
|||
matrix: {
|
||||
mci: {
|
||||
VM1: {
|
||||
focusTextStyle: first lower
|
||||
itemFormat: "|03{text}"
|
||||
focusItemFormat: "|11{text!styleFirstLower}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,11 +86,15 @@
|
|||
|
||||
fullLoginSequenceOnelinerz: {
|
||||
config: {
|
||||
listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.58}"
|
||||
dateTimeFormat: ddd h:mma
|
||||
}
|
||||
0: {
|
||||
mci: {
|
||||
VM1: { height: 10 }
|
||||
VM1: {
|
||||
height: 10
|
||||
width: 20
|
||||
itemFormat: "|00|11{userName:<12}|08: |03{oneliner:<59.59}"
|
||||
}
|
||||
TM2: {
|
||||
focusTextStyle: first lower
|
||||
}
|
||||
|
@ -108,61 +111,92 @@
|
|||
}
|
||||
}
|
||||
|
||||
mainMenuUserAchievementsEarned: {
|
||||
config: {
|
||||
dateTimeFormat: MMM Do h:mma
|
||||
achievementsInfoFormat10: "|00|07\"|11{title}|07\""
|
||||
achievementsInfoFormat11: "|00|03{text}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 11
|
||||
width: 76
|
||||
itemFormat: "|00|15{ts} |07- |03{title:<47.46} |15{points:,}|07 pts"
|
||||
focusItemFormat: "|00|19|15{ts} - {title:<47.46} {points:,} pts"
|
||||
}
|
||||
TL10: {
|
||||
width: 76
|
||||
}
|
||||
TL11: {
|
||||
width: 76
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainMenuUserStats: {
|
||||
mci: {
|
||||
UN1: { width: 17 }
|
||||
UR2: { width: 17 }
|
||||
LO3: { width: 17 }
|
||||
UF4: { width: 17 }
|
||||
UG5: { width: 17 }
|
||||
UT6: { width: 17 }
|
||||
UC7: { width: 17 }
|
||||
ST8: { width: 17 }
|
||||
UN1: { width: 15 }
|
||||
UR2: { width: 15 }
|
||||
LO3: { width: 15 }
|
||||
UF4: { width: 15 }
|
||||
UG5: { width: 15 }
|
||||
UT6: { width: 15 }
|
||||
UC7: { width: 15 }
|
||||
ST8: { width: 15 }
|
||||
}
|
||||
}
|
||||
|
||||
mainMenuSystemStats: {
|
||||
mci: {
|
||||
BN1: { width: 17 }
|
||||
VL2: { width: 17 }
|
||||
VN2: { width: 17 }
|
||||
OS3: { width: 33 }
|
||||
SC4: { width: 33 }
|
||||
DT5: { width: 33 }
|
||||
CT6: { width: 33 }
|
||||
AN7: { width: 6 }
|
||||
ND8: { width: 6 }
|
||||
TC9: { width: 6 }
|
||||
TT11: { width: 6 }
|
||||
PT12: { width: 6 }
|
||||
TP13: { width: 6 }
|
||||
NV14: { width: 17 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mainMenuLastCallers: {
|
||||
config: {
|
||||
listFormat: "|00|11{userName:<17.17}|03{location:<20.20}|11{affils:<17.17}|03{ts:<15}"
|
||||
dateTimeFormat: MMM Do h:mma
|
||||
}
|
||||
mci: {
|
||||
VM1: { height: 10 }
|
||||
VM1: {
|
||||
height: 10,
|
||||
width: 20
|
||||
itemFormat: "|00|11{userName:<17.16} |03{location:<20.19} |11{affils:<18.17} |03{ts:<15}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainMenuUserList: {
|
||||
config: {
|
||||
listFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{note:<19.19}|03{lastLoginTs}"
|
||||
focusListFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{note:<19.19}{lastLoginTs}"
|
||||
dateTimeFormat: MMM Do h:mma
|
||||
}
|
||||
mci: {
|
||||
VM1: { height: 15 }
|
||||
VM1: {
|
||||
height: 15,
|
||||
width: 50
|
||||
itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{location:<19.19}|03{lastLoginTs}"
|
||||
focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{location:<19.19}{lastLoginTs}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainMenuWhosOnline: {
|
||||
config: {
|
||||
listFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}"
|
||||
}
|
||||
mci: {
|
||||
VM1: { height: 10 }
|
||||
VM1: {
|
||||
height: 10,
|
||||
width: 20
|
||||
itemFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,13 +216,15 @@
|
|||
}
|
||||
|
||||
mainMenuOnelinerz: {
|
||||
// :TODO: Need way to just duplicate entry here & in menu.hjson, e.g. use: someName + must supply next/etc. in menu
|
||||
config: {
|
||||
listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.58}"
|
||||
dateTimeFormat: ddd h:mma
|
||||
}
|
||||
0: {
|
||||
mci: {
|
||||
VM1: { height: 10 }
|
||||
VM1: {
|
||||
height: 10
|
||||
itemFormat: "|00|11{userName:<12}|08: |03{oneliner:<59.59}"
|
||||
}
|
||||
TM2: {
|
||||
focusTextStyle: first lower
|
||||
}
|
||||
|
@ -207,39 +243,47 @@
|
|||
|
||||
messageAreaMessageList: {
|
||||
config: {
|
||||
listFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}"
|
||||
focusListFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
|
||||
dateTimeFormat: ddd MMM Do
|
||||
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
width: 70
|
||||
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}"
|
||||
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaChangeCurrentConference: {
|
||||
config: {
|
||||
listFormat: "|00|15{index} |07- |03{name}"
|
||||
focusListFormat: "|00|19|15{index} - {name}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
width: 26
|
||||
height: 19
|
||||
itemFormat: "|00|15{index} |07- |03{name}"
|
||||
focusItemFormat: "|00|19|15{index} - {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaChangeCurrentArea: {
|
||||
config: {
|
||||
listFormat: "|00|15{index} |07- |03{name}"
|
||||
focusListFormat: "|00|19|15{index} - {name}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
width: 26
|
||||
height: 19
|
||||
itemFormat: "|00|15{index:.2} |07- |03{name}"
|
||||
focusItemFormat: "|00|09|15{index:.2} - {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaSetNewScanDate: {
|
||||
mci: {
|
||||
SM2: {
|
||||
width: 54
|
||||
itemFormat: "|00|07{conf.name} |08- |07{area.name}"
|
||||
focusItemFormat: "|00|15{conf.name} |07- |15{area.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,25 +305,31 @@
|
|||
|
||||
mailMenuInbox: {
|
||||
config: {
|
||||
listFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}"
|
||||
focusListFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
|
||||
dateTimeFormat: ddd MMM Do
|
||||
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
width: 70
|
||||
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}"
|
||||
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
|
||||
}
|
||||
XY2: {
|
||||
width: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainMenuRumorz: {
|
||||
config: {
|
||||
listFormat: "|00|11 {rumor}"
|
||||
focusListFormat: "|00|15> |14{rumor}"
|
||||
}
|
||||
0: {
|
||||
mci: {
|
||||
VM1: { height: 14 }
|
||||
VM1: {
|
||||
height: 14,
|
||||
width: 70
|
||||
itemFormat: "|00|11 {rumor}"
|
||||
focusItemFormat: "|00|15> |14{rumor}"
|
||||
}
|
||||
TM2: {
|
||||
focusTextStyle: upper
|
||||
items: [ "yes", "no" ]
|
||||
|
@ -298,16 +348,14 @@
|
|||
}
|
||||
|
||||
bbsList: {
|
||||
config: {
|
||||
listFormat: "|00|07{bbsName}"
|
||||
focusListFormat: "|00|19|15{bbsName!styleFirstLower}"
|
||||
}
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 11
|
||||
width: 22
|
||||
focusTextStyle: first upper
|
||||
itemFormat: "|00|07{bbsName}"
|
||||
focusItemFormat: "|00|19|15{bbsName!styleFirstLower}"
|
||||
}
|
||||
TL2: { width: 28 }
|
||||
TL3: { width: 28 }
|
||||
|
@ -337,6 +385,70 @@
|
|||
}
|
||||
}
|
||||
|
||||
nodeMessage: {
|
||||
config: {
|
||||
messageFormat: "|00|08 :: |03message from |11{fromUserName} |08/ |03node |11{fromNodeId}|08 @ |11{timestamp} |08::\r\n|07 {message}"
|
||||
}
|
||||
0: {
|
||||
mci: {
|
||||
SM1: {
|
||||
width: 25
|
||||
itemFormat: "|00|03node |07{text} |08(|07{userName}|08)"
|
||||
focusItemFormat: "|00|11node |15{text} |07(|15{userName}|07)"
|
||||
}
|
||||
ET2: {
|
||||
width: 65
|
||||
}
|
||||
TL3: {
|
||||
width: 65
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageSearch: {
|
||||
0: {
|
||||
mci: {
|
||||
ET1: {
|
||||
width: 42
|
||||
}
|
||||
BT2: {
|
||||
focusTextStyle: upper
|
||||
}
|
||||
SM3: {
|
||||
width: 42
|
||||
}
|
||||
SM4: {
|
||||
width: 42
|
||||
}
|
||||
ET5: {
|
||||
width: 42
|
||||
}
|
||||
ET6: {
|
||||
width: 42
|
||||
}
|
||||
BT7: {
|
||||
focusTextStyle: upper
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaSearchMessageList: {
|
||||
config: {
|
||||
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
|
||||
// Fri Sep 25th
|
||||
dateTimeFormat: ddd MMM Do
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
width: 71
|
||||
itemFormat: "|00|15 {msgNum:<4.4} |03{subject:<27.26} |07{toUserName:<13.12} {fromUserName:<13.12} |03{ts:<12.12}"
|
||||
focusItemFormat: "|00|19> |15{msgNum:<4.4} {subject:<27.26} {toUserName:<13.12} {fromUserName:<13.12} {ts:<12.12}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageAreaViewPost: {
|
||||
0: {
|
||||
|
@ -410,20 +522,24 @@
|
|||
|
||||
fullLoginSequenceLastCallers: {
|
||||
config: {
|
||||
listFormat: "|00|11{userName:<17.17}|03{location:<20.20}|11{affils:<17.17}|03{ts:<15}"
|
||||
dateTimeFormat: MMM Do h:mma
|
||||
}
|
||||
mci: {
|
||||
VM1: { height: 10 }
|
||||
VM1: {
|
||||
height: 10,
|
||||
width: 20
|
||||
itemFormat: "|00|11{userName:<17.16} |03{location:<20.19} |11{affils:<18.17} |03{ts:<15}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fullLoginSequenceWhosOnline: {
|
||||
config: {
|
||||
listFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}"
|
||||
}
|
||||
mci: {
|
||||
VM1: { height: 10 }
|
||||
VM1: {
|
||||
height: 10,
|
||||
width: 20
|
||||
itemFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -433,14 +549,14 @@
|
|||
|
||||
fullLoginSequenceUserStats: {
|
||||
mci: {
|
||||
UN1: { width: 17 }
|
||||
UR2: { width: 17 }
|
||||
LO3: { width: 17 }
|
||||
UF4: { width: 17 }
|
||||
UG5: { width: 17 }
|
||||
UT6: { width: 17 }
|
||||
UC7: { width: 17 }
|
||||
ST8: { width: 17 }
|
||||
UN1: { width: 15 }
|
||||
UR2: { width: 15 }
|
||||
LO3: { width: 15 }
|
||||
UF4: { width: 15 }
|
||||
UG5: { width: 15 }
|
||||
UT6: { width: 15 }
|
||||
UC7: { width: 15 }
|
||||
ST8: { width: 15 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -468,13 +584,15 @@
|
|||
|
||||
newScanMessageList: {
|
||||
config: {
|
||||
listFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}"
|
||||
focusListFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}"
|
||||
dateTimeFormat: ddd MMM Do
|
||||
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
|
||||
}
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 14
|
||||
width: 70
|
||||
itemFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}"
|
||||
focusItemFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -493,7 +611,7 @@
|
|||
fileBaseListEntries: {
|
||||
config: {
|
||||
hashTagsSep: "|08, |07"
|
||||
browseInfoFormat10: "|00|10{fileName} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
|
||||
browseInfoFormat10: "|00|10{fileName:<.44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
|
||||
browseInfoFormat11: "|00|15{areaName}"
|
||||
browseInfoFormat12: "|00|07{hashTags}"
|
||||
browseInfoFormat13: "|00|07{estReleaseYear}"
|
||||
|
@ -525,9 +643,6 @@
|
|||
detailsGeneralInfoFormat21: "{uploadTimestamp}"
|
||||
detailsGeneralInfoFormat22: "{archiveTypeDesc}"
|
||||
|
||||
fileListEntryFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusFileListEntryFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
|
||||
notAnArchiveFormat: "|00|08( |07{fileName} is not an archive |08)"
|
||||
}
|
||||
|
||||
|
@ -586,6 +701,8 @@
|
|||
VM1: {
|
||||
height: 17
|
||||
width: 79
|
||||
itemFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusItemFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -594,7 +711,7 @@
|
|||
newScanFileBaseList: {
|
||||
config: {
|
||||
hashTagsSep: "|08, |07"
|
||||
browseInfoFormat10: "|00|10{fileName} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
|
||||
browseInfoFormat10: "|00|10{fileName:<44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
|
||||
browseInfoFormat11: "|00|15{areaName}"
|
||||
browseInfoFormat12: "|00|07{hashTags}"
|
||||
browseInfoFormat13: "|00|07{estReleaseYear}"
|
||||
|
@ -626,9 +743,6 @@
|
|||
detailsGeneralInfoFormat21: "{uploadTimestamp}"
|
||||
detailsGeneralInfoFormat22: "{archiveTypeDesc}"
|
||||
|
||||
fileListEntryFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusFileListEntryFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
|
||||
notAnArchiveFormat: "|00|08( |07{fileName} is not an archive |08)"
|
||||
}
|
||||
|
||||
|
@ -687,23 +801,22 @@
|
|||
VM1: {
|
||||
height: 17
|
||||
width: 79
|
||||
itemFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusItemFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseBrowseByAreaSelect: {
|
||||
config: {
|
||||
protListFormat: "|00|03{name}"
|
||||
protListFocusFormat: "|00|19|15{name}"
|
||||
}
|
||||
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 15
|
||||
width: 30
|
||||
focusTextStyle: first lower
|
||||
itemFormat: "|00|03{name}"
|
||||
focusItemFormat: "|00|19|15{name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -722,15 +835,15 @@
|
|||
}
|
||||
SM4: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
}
|
||||
SM5: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
}
|
||||
SM6: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
}
|
||||
BT7: {
|
||||
focusTextStyle: first lower
|
||||
|
@ -738,6 +851,51 @@
|
|||
}
|
||||
}
|
||||
|
||||
fileBaseExportListFilter: {
|
||||
mci: {
|
||||
ET1: {
|
||||
width: 42
|
||||
}
|
||||
BT2: {
|
||||
focusTextStyle: first lower
|
||||
}
|
||||
ET3: {
|
||||
width: 42
|
||||
}
|
||||
SM4: {
|
||||
width: 14
|
||||
justify: left
|
||||
}
|
||||
SM5: {
|
||||
width: 14
|
||||
justify: left
|
||||
}
|
||||
SM6: {
|
||||
width: 14
|
||||
justify: left
|
||||
}
|
||||
BT7: {
|
||||
focusTextStyle: first lower
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBaseExportList: {
|
||||
config: {
|
||||
progBarChar: "|15▒"
|
||||
mainInfoFormat10: "|07{currentFile} |08/ |07{totalFileCount} |08(|07{progress} %|08)"
|
||||
}
|
||||
mci: {
|
||||
TL1: {
|
||||
width: 60
|
||||
}
|
||||
TL2: {
|
||||
width: 56
|
||||
fillChar: "|06░"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileAreaFilterEditor: {
|
||||
mci: {
|
||||
ET1: {
|
||||
|
@ -748,15 +906,15 @@
|
|||
}
|
||||
SM3: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
}
|
||||
SM4: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
}
|
||||
SM5: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
}
|
||||
ET6: {
|
||||
width: 26
|
||||
|
@ -768,16 +926,13 @@
|
|||
}
|
||||
|
||||
fileBaseDownloadManager: {
|
||||
config: {
|
||||
queueListFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusQueueListFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
}
|
||||
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 11
|
||||
width: 69
|
||||
itemFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusItemFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
}
|
||||
HM2: {
|
||||
width: 50
|
||||
|
@ -789,8 +944,6 @@
|
|||
|
||||
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}"
|
||||
}
|
||||
|
@ -799,6 +952,8 @@
|
|||
mci: {
|
||||
VM1: {
|
||||
height: 8
|
||||
itemFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
|
||||
focusItemFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
|
||||
}
|
||||
HM2: {
|
||||
width: 50
|
||||
|
@ -826,7 +981,7 @@
|
|||
mci: {
|
||||
SM1: {
|
||||
width: 14
|
||||
justify: right
|
||||
justify: left
|
||||
focusTextStyle: first lower
|
||||
}
|
||||
|
||||
|
@ -895,17 +1050,14 @@
|
|||
}
|
||||
|
||||
fileTransferProtocolSelection: {
|
||||
config: {
|
||||
protListFormat: "|00|03{name}"
|
||||
protListFocusFormat: "|00|19|15{name}"
|
||||
}
|
||||
|
||||
0: {
|
||||
mci: {
|
||||
VM1: {
|
||||
height: 15
|
||||
width: 30
|
||||
focusTextStyle: first lower
|
||||
itemFormat: "|00|03{name}"
|
||||
focusItemFormat: "|00|19|15{name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -938,5 +1090,31 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
achievements: {
|
||||
defaults: {
|
||||
format: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}"
|
||||
globalFormat: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}"
|
||||
titleSGR: "|10"
|
||||
pointsSGR: "|12"
|
||||
textSGR: "|00|03"
|
||||
globalTextSGR: "|03"
|
||||
boardNameSGR: "|10"
|
||||
userNameSGR: "|11"
|
||||
achievedValueSGR: "|15"
|
||||
}
|
||||
|
||||
overrides: {
|
||||
user_login_count: {
|
||||
match: {
|
||||
2: {
|
||||
//
|
||||
// You may override title, text, and globalText here
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,469 @@
|
|||
/*
|
||||
./\/\." ENiGMA½ Achievement Configuration -/--/-------- - -- -
|
||||
|
||||
_____________________ _____ ____________________ __________\_ /
|
||||
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
|
||||
// __|___// | \// |// | \// | | \// \ /___ /_____
|
||||
/____ _____| __________ ___|__| ____| \ / _____ \
|
||||
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
|
||||
/__ _\
|
||||
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
|
||||
|
||||
*-----------------------------------------------------------------------------*
|
||||
|
||||
General Information
|
||||
------------------------------- - -
|
||||
This configuration is in HJSON (http://hjson.org/) format. Strict to-spec
|
||||
JSON is also perfectly valid. Use "hjson" from npm to convert to/from JSON.
|
||||
|
||||
See http://hjson.org/ for more information and syntax.
|
||||
|
||||
Various editors and IDEs such as Sublime Text 3 Visual Studio Code and so
|
||||
on have syntax highlighting for the HJSON format which are highly recommended.
|
||||
|
||||
------------------------------- -- - -
|
||||
Achievement Configuration
|
||||
------------------------------- - -
|
||||
Achievements are currently fairly limited in what can trigger them. This is
|
||||
being expanded upon and more will be available in the near future. For now
|
||||
you should mostly be interested in:
|
||||
- Perhaps adding additional *levels* of triggers & points
|
||||
- Applying customizations via the achievements section in theme.hjson
|
||||
|
||||
Some tips:
|
||||
- For 'userStatSet' types, see user_property.js
|
||||
|
||||
Don"t forget to RTFM ...er uh... see the documentation for more information and
|
||||
don"t be shy to ask for help:
|
||||
|
||||
BBS : Xibalba @ xibalba.l33t.codes
|
||||
FTN : BBS Discussion on fsxNet or ArakNet
|
||||
IRC : #enigma-bbs / FreeNode
|
||||
Email : bryan@l33t.codes
|
||||
*/
|
||||
{
|
||||
// Set to false to disable the achievement system
|
||||
enabled : true
|
||||
|
||||
art : {
|
||||
localHeader: achievement_local_header
|
||||
localFooter: achievement_local_footer
|
||||
globalHeader: achievement_global_header
|
||||
globalFooter: achievement_global_footer
|
||||
}
|
||||
|
||||
achievements: {
|
||||
user_login_count: {
|
||||
type: userStatSet
|
||||
statName: login_count
|
||||
match: {
|
||||
2: {
|
||||
title: "Return Caller"
|
||||
globalText: "{userName} has returned to {boardName}!"
|
||||
text: "You've returned to {boardName}!"
|
||||
points: 5
|
||||
}
|
||||
10: {
|
||||
title: "Curious Caller"
|
||||
globalText: "{userName} has logged into {boardName} {achievedValue} times!"
|
||||
text: "You've logged into {boardName} {achievedValue} times!"
|
||||
points: 10
|
||||
}
|
||||
25: {
|
||||
title: "Inquisitive"
|
||||
globalText: "{userName} has logged into {boardName} {achievedValue} times!"
|
||||
text: "You've logged into {boardName} {achievedValue} times!"
|
||||
points: 10
|
||||
}
|
||||
75: {
|
||||
title: "Still Interested!"
|
||||
globalText: "{userName} has logged into {boardName} {achievedValue} times!"
|
||||
text: "You've logged into {boardName} {achievedValue} times!"
|
||||
points: 15
|
||||
}
|
||||
100: {
|
||||
title: "Regular Customer"
|
||||
globalText: "{userName} has logged into {boardName} {achievedValue} times!"
|
||||
text: "You've logged into {boardName} {achievedValue} times!"
|
||||
points: 25
|
||||
}
|
||||
250: {
|
||||
title: "Speed Dial",
|
||||
globalText: "{userName} has logged into {boardName} {achievedValue} times!"
|
||||
text: "You've logged into {boardName} {achievedValue} times!"
|
||||
points: 50
|
||||
}
|
||||
500: {
|
||||
title: "System Addict"
|
||||
globalText: "{userName} the BBS {boardName} addict has logged in {achievedValue} times!"
|
||||
text: "You're a {boardName} addict! You've logged in {achievedValue} times!"
|
||||
points: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_post_count: {
|
||||
type: userStatSet
|
||||
statName: post_count
|
||||
match: {
|
||||
2: {
|
||||
title: "Poster"
|
||||
globalText: "{userName} has posted {achievedValue} messages!"
|
||||
text: "You've posted {achievedValue} messages!"
|
||||
points: 5
|
||||
}
|
||||
5: {
|
||||
title: "Poster... again!",
|
||||
globalText: "{userName} has posted {achievedValue} messages!"
|
||||
text: "You've posted {achievedValue} messages!"
|
||||
points: 5
|
||||
}
|
||||
20: {
|
||||
title: "Just Want to Talk",
|
||||
globalText: "{userName} has posted {achievedValue} messages!"
|
||||
text: "You've posted {achievedValue} messages!"
|
||||
points: 10
|
||||
}
|
||||
100: {
|
||||
title: "Probably Just Spam",
|
||||
globalText: "{userName} has posted {achievedValue} messages!"
|
||||
text: "You've posted {achievedValue} messages!"
|
||||
points: 25
|
||||
}
|
||||
250: {
|
||||
title: "Scribe"
|
||||
globalText: "{userName} the scribe has posted {achievedValue} messages!"
|
||||
text: "Such a scribe! You've posted {achievedValue} messages!"
|
||||
points: 50
|
||||
}
|
||||
500: {
|
||||
title: "Writing a Book"
|
||||
globalText: "{userName} is writing a book and has posted {achievedValue} messages!"
|
||||
text: "You've posted {achievedValue} messages!"
|
||||
points: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_upload_count: {
|
||||
type: userStatSet
|
||||
statName: ul_total_count
|
||||
match: {
|
||||
1: {
|
||||
title: "Uploader"
|
||||
globalText: "{userName} has uploaded a file!"
|
||||
text: "You've uploaded somthing!"
|
||||
points: 5
|
||||
}
|
||||
10: {
|
||||
title: "Moar Uploads!"
|
||||
globalText: "{userName} has uploaded {achievedValue} files!"
|
||||
text: "You've uploaded {achievedValue} files!"
|
||||
points: 10
|
||||
}
|
||||
50: {
|
||||
title: "Contributor"
|
||||
globalText: "{userName} has uploaded {achievedValue} files!"
|
||||
text: "You've uploaded {achievedValue} files!"
|
||||
points: 25
|
||||
|
||||
}
|
||||
100: {
|
||||
title: "Courier"
|
||||
globalText: "Courier {userName} has uploaded {achievedValue} files!"
|
||||
text: "You've uploaded {achievedValue} files!"
|
||||
points: 50
|
||||
}
|
||||
200: {
|
||||
title: "Must Be a Drop Site"
|
||||
globalText: "{userName} has uploaded a whomping {achievedValue} files!"
|
||||
text: "You've uploaded a whomping {achievedValue} files!"
|
||||
points: 55
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_upload_bytes: {
|
||||
type: userStatSet
|
||||
statName: ul_total_bytes
|
||||
match: {
|
||||
10240: {
|
||||
text: "UNIVAC Drum"
|
||||
globalText: "{userName} has uploaded 10k. Enough to fill a UNIVAC drum!"
|
||||
text: "You've uploaded 10k. Enough to fill a UNIVAC drum!"
|
||||
points: 5
|
||||
}
|
||||
524288: {
|
||||
title: "Kickstart"
|
||||
globalText: "{userName} has uploaded 512KB, enough for a Kickstart!"
|
||||
text: "You've uploaded 512KB, enough for a Kickstart!"
|
||||
points: 10
|
||||
}
|
||||
1474560: {
|
||||
title: "AOL Disk Anyone?"
|
||||
globalText: "{userName} has uploaded 1.44M worth of data. Hopefully it's not AOL!"
|
||||
title: "You've uploaded 1.44M worth of data. Hopefully it's not AOL!"
|
||||
points: 10
|
||||
}
|
||||
6291456: {
|
||||
title: "A Quake of a Upload"
|
||||
globalText: "{userName} has uploaded 6 x 1.44MB disks worth of data. That's the size of Quake for DOS!"
|
||||
text: "You've uploaded 6 x 1.44MB disks worth of data. That's the size of Quake for DOS!"
|
||||
points: 20
|
||||
}
|
||||
104857600: {
|
||||
title: "Zip 100"
|
||||
globalText: "{userName} has uploaded a Zip 100 disk's worth of data!"
|
||||
text: "You've uploaded a Zip 100 disk's worth of data!"
|
||||
points: 25
|
||||
}
|
||||
1073741824: {
|
||||
title: "Gigabyte!"
|
||||
globalText: "{userName} has uploaded a Gigabyte worth of data!"
|
||||
text: "You've uploaded a Gigabyte worth of data!"
|
||||
points: 50
|
||||
}
|
||||
3407872000: {
|
||||
title: "Encarta"
|
||||
globalText: "{userName} has uploaded 5xCD discs worth of data. That's the size of Encarta!"
|
||||
text: "You've uploaded 5xCD discs worth of data. That's the size of Encarta!"
|
||||
points: 50
|
||||
}
|
||||
7025459200: {
|
||||
title: "NFL_Madden_2007_USA_BLURAY_DIRFIX_PS3-PARADOX"
|
||||
globalText: "{userName} has uploaded 67x100 MiB worth of data, the size of the worlds first PS3 rip!"
|
||||
text: "You've uploaded 67x100 MiB worth of data, the size of the world first PS3 rip!"
|
||||
points: 100
|
||||
}
|
||||
25018184499: {
|
||||
title: "WaYsTeD"
|
||||
globalText: "{userName} has uploaded 23.3 GiB of data, the size of the first PS4 rip: Watch.Dogs.PS4-WaYsTeD!"
|
||||
text: "You've uploaded 23.3 GiB of data, the size of the first PS4 rip: Watch.Dogs.PS4-WaYsTeD!"
|
||||
points: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_download_count: {
|
||||
type: userStatSet
|
||||
statName: dl_total_count
|
||||
match: {
|
||||
1: {
|
||||
title: "Downloader"
|
||||
globalText: "{userName} has downloaded a file!"
|
||||
text: "You've downloaded somthing!"
|
||||
points: 5
|
||||
}
|
||||
10: {
|
||||
title: "Moar Downloads!"
|
||||
globalText: "{userName} has downloaded {achievedValue} files!"
|
||||
text: "You've downloaded {achievedValue} files!"
|
||||
points: 10
|
||||
}
|
||||
50: {
|
||||
title: "Leecher"
|
||||
globalText: "{userName} has leeched {achievedValue} files!"
|
||||
text: "You've leeched... er... downloaded {achievedValue} files!"
|
||||
points: 15
|
||||
}
|
||||
100: {
|
||||
title: "Hoarder"
|
||||
globalText: "{userName} has downloaded {achievedValue} files!"
|
||||
text: "Hoarding files? You've downloaded {achievedValue} files!"
|
||||
points: 20
|
||||
}
|
||||
200: {
|
||||
title: "Digital Archivist"
|
||||
globalText: "{userName} the digital archivist has {achievedValue} files!"
|
||||
text: "Building an archive? You've downloaded {achievedValue} files!"
|
||||
points: 25
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_download_bytes: {
|
||||
type: userStatSet
|
||||
statName: dl_total_bytes
|
||||
match: {
|
||||
655360: {
|
||||
title: "Ought to be Enough"
|
||||
globalText: "{userName} has downloaded 640K. Ought to be enough for anyone!"
|
||||
text: "You've downloaded 640K. Ought to be enough for anyone!"
|
||||
points: 5
|
||||
}
|
||||
1474560: {
|
||||
title: "Fits on a Floppy"
|
||||
globalText: "{userName} has downloaded 1.44MB worth of data!"
|
||||
text: "You've downloaded 1.44MB of data!"
|
||||
points: 5
|
||||
}
|
||||
104857600: {
|
||||
title: "Click of Death"
|
||||
globalText: "{userName} has downloaded 100MB... perhaps to a Zip Disk?"
|
||||
text: "You've downloaded 100MB of data... perhaps to a Zip Disk?"
|
||||
points: 10
|
||||
}
|
||||
681574400: {
|
||||
title: "CD Rip"
|
||||
globalText: "{userName} has downloaded a CD-ROM's worth of data!"
|
||||
text: "You've downloaded a CD-ROM's worth of data!"
|
||||
points: 15
|
||||
}
|
||||
1073741824: {
|
||||
title: "Like One Hundred Floppys, Man"
|
||||
globalText: "{userName} has downloaded {achievedValue!sizeWithAbbr} of data!"
|
||||
text: "You've downloaded {achievedValue!sizeWithAbbr} of data!"
|
||||
points: 25
|
||||
}
|
||||
5368709120: {
|
||||
title: "That's a Lot of Bits!"
|
||||
globalText: "{userName} has downloaded {achievedValue!sizeWithAbbr} of data!"
|
||||
text: "You've downloaded {achievedValue!sizeWithAbbr} of data!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_door_runs: {
|
||||
type: userStatSet
|
||||
statName: door_run_total_count
|
||||
match: {
|
||||
1: {
|
||||
title: "Nostalgia Toe Dip",
|
||||
globalText: "{userName} ran a door!"
|
||||
text: "You ran a door!"
|
||||
points: 5
|
||||
},
|
||||
10: {
|
||||
title: "This is Kinda Fun"
|
||||
globalText: "{userName} ran {achievedValue} doors!"
|
||||
text: "You've run {achievedValue} doors!"
|
||||
points: 10
|
||||
}
|
||||
50: {
|
||||
title: "Gamer"
|
||||
globalText: "{userName} ran {achievedValue} doors!"
|
||||
text: "You've run {achievedValue} doors!"
|
||||
points: 20
|
||||
}
|
||||
100: {
|
||||
title: "Trying Them All"
|
||||
globalText: "{userName} must really like textmode and has run {achievedValue} doors!"
|
||||
text: "You've run {achievedValue} doors! You must really like textmode!"
|
||||
points: 50
|
||||
}
|
||||
200: {
|
||||
title: "Dropfile Enthusiast"
|
||||
globalText: "{userName} the dropfile enthusiast ran {achievedValue} doors!"
|
||||
text: "You're a dropfile enthusiast! You've run {achievedValue} doors!"
|
||||
points: 55
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_individual_door_run_minutes: {
|
||||
type: userStatInc
|
||||
statName: door_run_total_minutes
|
||||
retroactive: false
|
||||
match: {
|
||||
1: {
|
||||
title: "Nevermind!"
|
||||
globalText: "{userName} ran a door for {achievedValue!durationMinutes}. Guess it's not their thing!"
|
||||
text: "You ran a door for only {achievedValue!durationMinutes}. Not your thing?"
|
||||
points: 5
|
||||
}
|
||||
10: {
|
||||
title: "It's OK I Guess"
|
||||
globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
|
||||
text: "You ran a door for {achievedValue!durationMinutes}!"
|
||||
points: 10
|
||||
}
|
||||
30: {
|
||||
title: "Good Game"
|
||||
globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
|
||||
text: "You ran a door for {achievedValue!durationMinutes}!"
|
||||
points: 20
|
||||
}
|
||||
60: {
|
||||
title: "What? Limited Turns?!"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!"
|
||||
text: "You've spent {achievedValue!durationMinutes} in a door!"
|
||||
points: 25
|
||||
}
|
||||
120: {
|
||||
title: "It's the Only One I Know!"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!"
|
||||
text: "You've spent {achievedValue!durationMinutes} in a door!"
|
||||
points: 50
|
||||
}
|
||||
240: {
|
||||
title: "Possible Addict"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!"
|
||||
text: "You've spent {achievedValue!durationMinutes} in a door!"
|
||||
points: 55
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_door_run_total_minutes: {
|
||||
type: userStatIncNewVal
|
||||
statName: door_run_total_minutes
|
||||
match: {
|
||||
10: {
|
||||
title: "Enough for the Instructions"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!"
|
||||
text: "You've spent {achievedValue!durationMinutes} playing doors!"
|
||||
points: 10
|
||||
}
|
||||
30: {
|
||||
title: "Probably Just L.O.R.D."
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!"
|
||||
text: "You've spent {achievedValue!durationMinutes} playing doors!"
|
||||
points: 20
|
||||
}
|
||||
60: {
|
||||
title: "Retro or Bust"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!"
|
||||
text: "You've spent {achievedValue!durationMinutes} playing doors!"
|
||||
points: 25
|
||||
}
|
||||
240: {
|
||||
title: "Textmode Dragon Slayer"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!"
|
||||
text: "You've spent {achievedValue!durationMinutes} playing doors!"
|
||||
points: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_total_system_online_minutes: {
|
||||
type: userStatSet
|
||||
statName: minutes_online_total_count
|
||||
match: {
|
||||
30: {
|
||||
title: "Just Poking Around"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
|
||||
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||
points: 5
|
||||
}
|
||||
60: {
|
||||
title: "Mildly Interesting"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
|
||||
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||
points: 15
|
||||
}
|
||||
120: {
|
||||
title: "Nothing Better to Do"
|
||||
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
|
||||
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||
points: 25
|
||||
}
|
||||
1440: {
|
||||
title: "Idle Bot"
|
||||
globalText: "{userName} is probably a bot. They've spent {achievedValue!durationMinutes} on {boardName}!"
|
||||
text: "You're a bot, aren't you? You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||
points: 55
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3782
config/menu.hjson
3782
config/menu.hjson
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +1,22 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const DropFile = require('./dropfile.js').DropFile;
|
||||
const door = require('./door.js');
|
||||
const { MenuModule } = require('./menu_module.js');
|
||||
const DropFile = require('./dropfile.js');
|
||||
const Door = require('./door.js');
|
||||
const theme = require('./theme.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const {
|
||||
trackDoorRunBegin,
|
||||
trackDoorRunEnd
|
||||
} = require('./door_util.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const paths = require('path');
|
||||
const _ = require('lodash');
|
||||
const mkdirs = require('fs-extra').mkdirs;
|
||||
|
||||
// :TODO: This should really be a system module... needs a little work to allow for such
|
||||
const paths = require('path');
|
||||
|
||||
const activeDoorNodeInstances = {};
|
||||
|
||||
|
@ -65,6 +68,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||
|
||||
this.config = options.menuConfig.config;
|
||||
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
||||
// .. and/or EnigAssert
|
||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
||||
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
|
||||
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
||||
|
@ -100,7 +104,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||
if(_.isString(self.config.tooManyArt)) {
|
||||
theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() {
|
||||
self.pausePrompt( () => {
|
||||
callback(new Error('Too many active instances'));
|
||||
return callback(Errors.AccessDenied('Too many active instances'));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
@ -108,7 +112,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||
|
||||
// :TODO: Use MenuModule.pausePrompt()
|
||||
self.pausePrompt( () => {
|
||||
callback(new Error('Too many active instances'));
|
||||
return callback(Errors.AccessDenied('Too many active instances'));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -122,19 +126,17 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||
callback(null);
|
||||
}
|
||||
},
|
||||
function prepareDoor(callback) {
|
||||
self.doorInstance = new Door(self.client);
|
||||
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
|
||||
},
|
||||
function generateDropfile(callback) {
|
||||
self.dropFile = new DropFile(self.client, self.config.dropFileType);
|
||||
var fullPath = self.dropFile.fullPath;
|
||||
const dropFileOpts = {
|
||||
fileType : self.config.dropFileType,
|
||||
};
|
||||
|
||||
mkdirs(paths.dirname(fullPath), function dirCreated(err) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
self.dropFile.createFile(function created(err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
self.dropFile = new DropFile(self.client, dropFileOpts);
|
||||
return self.dropFile.createFile(callback);
|
||||
}
|
||||
],
|
||||
function complete(err) {
|
||||
|
@ -150,20 +152,24 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||
}
|
||||
|
||||
runDoor() {
|
||||
this.client.term.write(ansi.resetScreen());
|
||||
|
||||
const exeInfo = {
|
||||
cmd : this.config.cmd,
|
||||
cwd : this.config.cwd || paths.dirname(this.config.cmd),
|
||||
args : this.config.args,
|
||||
io : this.config.io || 'stdio',
|
||||
encoding : this.config.encoding || this.client.term.outputEncoding,
|
||||
encoding : this.config.encoding || 'cp437',
|
||||
dropFile : this.dropFile.fileName,
|
||||
dropFilePath : this.dropFile.fullPath,
|
||||
node : this.client.node,
|
||||
//inhSocket : this.client.output._handle.fd,
|
||||
};
|
||||
|
||||
const doorInstance = new door.Door(this.client, exeInfo);
|
||||
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
||||
|
||||
this.doorInstance.run(exeInfo, () => {
|
||||
trackDoorRunEnd(doorTracking);
|
||||
|
||||
doorInstance.once('finished', () => {
|
||||
//
|
||||
// Try to clean up various settings such as scroll regions that may
|
||||
// have been set within the door
|
||||
|
@ -178,10 +184,6 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
|||
|
||||
this.prevMenu();
|
||||
});
|
||||
|
||||
this.client.term.write(ansi.resetScreen());
|
||||
|
||||
doorInstance.run();
|
||||
}
|
||||
|
||||
leave() {
|
||||
|
|
|
@ -0,0 +1,634 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Events = require('./events.js');
|
||||
const Config = require('./config.js').get;
|
||||
const {
|
||||
getConfigPath,
|
||||
getFullConfig,
|
||||
} = require('./config_util.js');
|
||||
const UserDb = require('./database.js').dbs.user;
|
||||
const {
|
||||
getISOTimestampString
|
||||
} = require('./database.js');
|
||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||
const {
|
||||
getConnectionByUserId
|
||||
} = require('./client_connections.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const {
|
||||
Errors,
|
||||
ErrorReasons
|
||||
} = require('./enig_error.js');
|
||||
const { getThemeArt } = require('./theme.js');
|
||||
const {
|
||||
pipeToAnsi,
|
||||
stripMciColorCodes
|
||||
} = require('./color_codes.js');
|
||||
const stringFormat = require('./string_format.js');
|
||||
const StatLog = require('./stat_log.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const ConfigCache = require('./config_cache.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const moment = require('moment');
|
||||
const paths = require('path');
|
||||
|
||||
exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser;
|
||||
|
||||
class Achievement {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
|
||||
// achievements are retroactive by default
|
||||
this.data.retroactive = _.get(this.data, 'retroactive', true);
|
||||
}
|
||||
|
||||
static factory(data) {
|
||||
if(!data) {
|
||||
return;
|
||||
}
|
||||
let achievement;
|
||||
switch(data.type) {
|
||||
case Achievement.Types.UserStatSet :
|
||||
case Achievement.Types.UserStatInc :
|
||||
case Achievement.Types.UserStatIncNewVal :
|
||||
achievement = new UserStatAchievement(data);
|
||||
break;
|
||||
|
||||
default : return;
|
||||
}
|
||||
|
||||
if(achievement.isValid()) {
|
||||
return achievement;
|
||||
}
|
||||
}
|
||||
|
||||
static get Types() {
|
||||
return {
|
||||
UserStatSet : 'userStatSet',
|
||||
UserStatInc : 'userStatInc',
|
||||
UserStatIncNewVal : 'userStatIncNewVal',
|
||||
};
|
||||
}
|
||||
|
||||
isValid() {
|
||||
switch(this.data.type) {
|
||||
case Achievement.Types.UserStatSet :
|
||||
case Achievement.Types.UserStatInc :
|
||||
case Achievement.Types.UserStatIncNewVal :
|
||||
if(!_.isString(this.data.statName)) {
|
||||
return false;
|
||||
}
|
||||
if(!_.isObject(this.data.match)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default : return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchDetails(/*matchAgainst*/) {
|
||||
}
|
||||
|
||||
isValidMatchDetails(details) {
|
||||
if(!details || !_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) {
|
||||
return false;
|
||||
}
|
||||
return (_.isString(details.globalText) || !details.globalText);
|
||||
}
|
||||
}
|
||||
|
||||
class UserStatAchievement extends Achievement {
|
||||
constructor(data) {
|
||||
super(data);
|
||||
|
||||
// sort match keys for quick match lookup
|
||||
this.matchKeys = Object.keys(this.data.match || {}).map(k => parseInt(k)).sort( (a, b) => b - a);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
if(!super.isValid()) {
|
||||
return false;
|
||||
}
|
||||
return !Object.keys(this.data.match).some(k => !parseInt(k));
|
||||
}
|
||||
|
||||
getMatchDetails(matchValue) {
|
||||
let ret = [];
|
||||
let matchField = this.matchKeys.find(v => matchValue >= v);
|
||||
if(matchField) {
|
||||
const match = this.data.match[matchField];
|
||||
matchField = parseInt(matchField);
|
||||
if(this.isValidMatchDetails(match) && !isNaN(matchField)) {
|
||||
ret = [ match, matchField, matchValue ];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
class Achievements {
|
||||
constructor(events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
getAchievementByTag(tag) {
|
||||
return this.achievementConfig.achievements[tag];
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return !_.isUndefined(this.achievementConfig);
|
||||
}
|
||||
|
||||
init(cb) {
|
||||
let achievementConfigPath = _.get(Config(), 'general.achievementFile');
|
||||
if(!achievementConfigPath) {
|
||||
Log.info('Achievements are not configured');
|
||||
return cb(null);
|
||||
}
|
||||
achievementConfigPath = getConfigPath(achievementConfigPath); // qualify
|
||||
|
||||
const configLoaded = (achievementConfig) => {
|
||||
if(true !== achievementConfig.enabled) {
|
||||
Log.info('Achievements are not enabled');
|
||||
this.stopMonitoringUserStatEvents();
|
||||
delete this.achievementConfig;
|
||||
} else {
|
||||
Log.info('Achievements are enabled');
|
||||
this.achievementConfig = achievementConfig;
|
||||
this.monitorUserStatEvents();
|
||||
}
|
||||
};
|
||||
|
||||
const changed = ( { fileName, fileRoot } ) => {
|
||||
const reCachedPath = paths.join(fileRoot, fileName);
|
||||
if(reCachedPath === achievementConfigPath) {
|
||||
getFullConfig(achievementConfigPath, (err, achievementConfig) => {
|
||||
if(err) {
|
||||
return Log.error( { error : err.message }, 'Failed to reload achievement config from cache');
|
||||
}
|
||||
configLoaded(achievementConfig);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ConfigCache.getConfigWithOptions(
|
||||
{
|
||||
filePath : achievementConfigPath,
|
||||
forceReCache : true,
|
||||
callback : changed,
|
||||
},
|
||||
(err, achievementConfig) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
configLoaded(achievementConfig);
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadAchievementHitCount(user, achievementTag, field, cb) {
|
||||
UserDb.get(
|
||||
`SELECT COUNT() AS count
|
||||
FROM user_achievement
|
||||
WHERE user_id = ? AND achievement_tag = ? AND match = ?;`,
|
||||
[ user.userId, achievementTag, field],
|
||||
(err, row) => {
|
||||
return cb(err, row ? row.count : 0);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
record(info, localInterruptItem, cb) {
|
||||
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1);
|
||||
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points);
|
||||
|
||||
const cleanTitle = stripMciColorCodes(localInterruptItem.title);
|
||||
const cleanText = stripMciColorCodes(localInterruptItem.achievText);
|
||||
|
||||
const recordData = [
|
||||
info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField,
|
||||
cleanTitle, cleanText, info.details.points,
|
||||
];
|
||||
|
||||
UserDb.run(
|
||||
`INSERT OR IGNORE INTO user_achievement (user_id, achievement_tag, timestamp, match, title, text, points)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
||||
recordData,
|
||||
err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.events.emit(
|
||||
Events.getSystemEvents().UserAchievementEarned,
|
||||
{
|
||||
user : info.client.user,
|
||||
achievementTag : info.achievementTag,
|
||||
points : info.details.points,
|
||||
title : cleanTitle,
|
||||
text : cleanText,
|
||||
}
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
display(info, interruptItems, cb) {
|
||||
if(interruptItems.local) {
|
||||
UserInterruptQueue.queue(interruptItems.local, { clients : info.client } );
|
||||
}
|
||||
|
||||
if(interruptItems.global) {
|
||||
UserInterruptQueue.queue(interruptItems.global, { omit : info.client } );
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
recordAndDisplayAchievement(info, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
(callback) => {
|
||||
return this.createAchievementInterruptItems(info, callback);
|
||||
},
|
||||
(interruptItems, callback) => {
|
||||
this.record(info, interruptItems.local, err => {
|
||||
return callback(err, interruptItems);
|
||||
});
|
||||
},
|
||||
(interruptItems, callback) => {
|
||||
return this.display(info, interruptItems, callback);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
monitorUserStatEvents() {
|
||||
if(this.userStatEventListeners) {
|
||||
return; // already listening
|
||||
}
|
||||
|
||||
const listenEvents = [
|
||||
Events.getSystemEvents().UserStatSet,
|
||||
Events.getSystemEvents().UserStatIncrement
|
||||
];
|
||||
|
||||
this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => {
|
||||
if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// :TODO: Make this code generic - find + return factory created object
|
||||
const achievementTags = Object.keys(_.pickBy(
|
||||
_.get(this.achievementConfig, 'achievements', {}),
|
||||
achievement => {
|
||||
if(false === achievement.enabled) {
|
||||
return false;
|
||||
}
|
||||
const acceptedTypes = [
|
||||
Achievement.Types.UserStatSet,
|
||||
Achievement.Types.UserStatInc,
|
||||
Achievement.Types.UserStatIncNewVal,
|
||||
];
|
||||
return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName;
|
||||
}
|
||||
));
|
||||
|
||||
if(0 === achievementTags.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
async.eachSeries(achievementTags, (achievementTag, nextAchievementTag) => {
|
||||
const achievement = Achievement.factory(this.getAchievementByTag(achievementTag));
|
||||
if(!achievement) {
|
||||
return nextAchievementTag(null);
|
||||
}
|
||||
|
||||
const statValue = parseInt(
|
||||
[ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ?
|
||||
userStatEvent.statValue :
|
||||
userStatEvent.statIncrementBy
|
||||
);
|
||||
if(isNaN(statValue)) {
|
||||
return nextAchievementTag(null);
|
||||
}
|
||||
|
||||
const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue);
|
||||
if(!details) {
|
||||
return nextAchievementTag(null);
|
||||
}
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
(callback) => {
|
||||
this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null);
|
||||
});
|
||||
},
|
||||
(callback) => {
|
||||
const client = getConnectionByUserId(userStatEvent.user.userId);
|
||||
if(!client) {
|
||||
return callback(Errors.UnexpectedState('Failed to get client for user ID'));
|
||||
}
|
||||
|
||||
const info = {
|
||||
achievementTag,
|
||||
achievement,
|
||||
details,
|
||||
client,
|
||||
matchField, // match - may be in odd format
|
||||
matchValue, // actual value
|
||||
achievedValue : matchField, // achievement value met
|
||||
user : userStatEvent.user,
|
||||
timestamp : moment(),
|
||||
};
|
||||
|
||||
const achievementsInfo = [ info ];
|
||||
return callback(null, achievementsInfo, info);
|
||||
},
|
||||
(achievementsInfo, basicInfo, callback) => {
|
||||
if(true !== achievement.data.retroactive) {
|
||||
return callback(null, achievementsInfo);
|
||||
}
|
||||
|
||||
const index = achievement.matchKeys.findIndex(v => v < matchField);
|
||||
if(-1 === index || !Array.isArray(achievement.matchKeys)) {
|
||||
return callback(null, achievementsInfo);
|
||||
}
|
||||
|
||||
// For userStat, any lesser match keys(values) are also met. Example:
|
||||
// matchKeys: [ 500, 200, 100, 20, 10, 2 ]
|
||||
// ^---- we met here
|
||||
// ^------------^ retroactive range
|
||||
//
|
||||
async.eachSeries(achievement.matchKeys.slice(index), (k, nextKey) => {
|
||||
const [ det, fld, val ] = achievement.getMatchDetails(k);
|
||||
if(!det) {
|
||||
return nextKey(null);
|
||||
}
|
||||
|
||||
this.loadAchievementHitCount(userStatEvent.user, achievementTag, fld, (err, count) => {
|
||||
if(!err || count && 0 === count) {
|
||||
achievementsInfo.push(Object.assign(
|
||||
{},
|
||||
basicInfo,
|
||||
{
|
||||
details : det,
|
||||
matchField : fld,
|
||||
achievedValue : fld,
|
||||
matchValue : val,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
return nextKey(null);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
return callback(null, achievementsInfo);
|
||||
});
|
||||
},
|
||||
(achievementsInfo, callback) => {
|
||||
// reverse achievementsInfo so we display smallest > largest
|
||||
achievementsInfo.reverse();
|
||||
|
||||
async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => {
|
||||
return this.recordAndDisplayAchievement(achInfo, err => {
|
||||
return nextAchInfo(err);
|
||||
});
|
||||
},
|
||||
err => {
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err && ErrorReasons.TooMany !== err.reasonCode) {
|
||||
Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event');
|
||||
}
|
||||
return nextAchievementTag(null); // always try the next, regardless
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stopMonitoringUserStatEvents() {
|
||||
if(this.userStatEventListeners) {
|
||||
this.events.removeMultipleEventListener(this.userStatEventListeners);
|
||||
delete this.userStatEventListeners;
|
||||
}
|
||||
}
|
||||
|
||||
getFormatObject(info) {
|
||||
return {
|
||||
userName : info.user.username,
|
||||
userRealName : info.user.properties[UserProps.RealName],
|
||||
userLocation : info.user.properties[UserProps.Location],
|
||||
userAffils : info.user.properties[UserProps.Affiliations],
|
||||
nodeId : info.client.node,
|
||||
title : info.details.title,
|
||||
//text : info.global ? info.details.globalText : info.details.text,
|
||||
points : info.details.points,
|
||||
achievedValue : info.achievedValue,
|
||||
matchField : info.matchField,
|
||||
matchValue : info.matchValue,
|
||||
timestamp : moment(info.timestamp).format(info.dateTimeFormat),
|
||||
boardName : Config().general.boardName,
|
||||
};
|
||||
}
|
||||
|
||||
getFormattedTextFor(info, textType, defaultSgr = '|07') {
|
||||
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
|
||||
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
|
||||
|
||||
const formatObj = this.getFormatObject(info);
|
||||
|
||||
const wrap = (input) => {
|
||||
const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g');
|
||||
return input.replace(re, (m, formatVar, formatOpts) => {
|
||||
const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr;
|
||||
let r = `${varSgr}{${formatVar}`;
|
||||
if(formatOpts) {
|
||||
r += formatOpts;
|
||||
}
|
||||
return `${r}}${textTypeSgr}`;
|
||||
});
|
||||
};
|
||||
|
||||
return stringFormat(`${textTypeSgr}${wrap(info.details[textType])}`, formatObj);
|
||||
}
|
||||
|
||||
createAchievementInterruptItems(info, cb) {
|
||||
info.dateTimeFormat =
|
||||
info.details.dateTimeFormat ||
|
||||
info.achievement.dateTimeFormat ||
|
||||
info.client.currentTheme.helpers.getDateTimeFormat();
|
||||
|
||||
const title = this.getFormattedTextFor(info, 'title');
|
||||
const text = this.getFormattedTextFor(info, 'text');
|
||||
|
||||
let globalText;
|
||||
if(info.details.globalText) {
|
||||
globalText = this.getFormattedTextFor(info, 'globalText');
|
||||
}
|
||||
|
||||
const getArt = (name, callback) => {
|
||||
const spec =
|
||||
_.get(info.details, `art.${name}`) ||
|
||||
_.get(info.achievement, `art.${name}`) ||
|
||||
_.get(this.achievementConfig, `art.${name}`);
|
||||
if(!spec) {
|
||||
return callback(null);
|
||||
}
|
||||
const getArtOpts = {
|
||||
name : spec,
|
||||
client : this.client,
|
||||
random : false,
|
||||
};
|
||||
getThemeArt(getArtOpts, (err, artInfo) => {
|
||||
// ignore errors
|
||||
return callback(artInfo ? artInfo.data : null);
|
||||
});
|
||||
};
|
||||
|
||||
const interruptItems = {};
|
||||
let itemTypes = [ 'local' ];
|
||||
if(globalText) {
|
||||
itemTypes.push('global');
|
||||
}
|
||||
|
||||
async.each(itemTypes, (itemType, nextItemType) => {
|
||||
async.waterfall(
|
||||
[
|
||||
(callback) => {
|
||||
getArt(`${itemType}Header`, headerArt => {
|
||||
return callback(null, headerArt);
|
||||
});
|
||||
},
|
||||
(headerArt, callback) => {
|
||||
getArt(`${itemType}Footer`, footerArt => {
|
||||
return callback(null, headerArt, footerArt);
|
||||
});
|
||||
},
|
||||
(headerArt, footerArt, callback) => {
|
||||
const itemText = 'global' === itemType ? globalText : text;
|
||||
interruptItems[itemType] = {
|
||||
title,
|
||||
achievText : itemText,
|
||||
text : `${title}\r\n${itemText}`,
|
||||
pause : true,
|
||||
};
|
||||
if(headerArt || footerArt) {
|
||||
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
|
||||
const defaultContentsFormat = '{title}\r\n{message}';
|
||||
const contentsFormat = 'global' === itemType ?
|
||||
themeDefaults.globalFormat || defaultContentsFormat :
|
||||
themeDefaults.format || defaultContentsFormat;
|
||||
|
||||
const formatObj = Object.assign(this.getFormatObject(info), {
|
||||
title : this.getFormattedTextFor(info, 'title', ''), // ''=defaultSgr
|
||||
message : itemText,
|
||||
});
|
||||
|
||||
const contents = pipeToAnsi(stringFormat(contentsFormat, formatObj));
|
||||
|
||||
interruptItems[itemType].contents =
|
||||
`${headerArt || ''}\r\n${contents}\r\n${footerArt || ''}`;
|
||||
}
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return nextItemType(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
return cb(err, interruptItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let achievementsInstance;
|
||||
|
||||
function getAchievementsEarnedByUser(userId, cb) {
|
||||
if(!achievementsInstance) {
|
||||
return cb(Errors.UnexpectedState('Achievements not initialized'));
|
||||
}
|
||||
|
||||
UserDb.all(
|
||||
`SELECT achievement_tag, timestamp, match, title, text, points
|
||||
FROM user_achievement
|
||||
WHERE user_id = ?
|
||||
ORDER BY DATETIME(timestamp);`,
|
||||
[ userId ],
|
||||
(err, rows) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const earned = rows.map(row => {
|
||||
|
||||
const achievement = Achievement.factory(achievementsInstance.getAchievementByTag(row.achievement_tag));
|
||||
if(!achievement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const earnedInfo = {
|
||||
achievementTag : row.achievement_tag,
|
||||
type : achievement.data.type,
|
||||
retroactive : achievement.data.retroactive,
|
||||
title : row.title,
|
||||
text : row.text,
|
||||
points : row.points,
|
||||
timestamp : moment(row.timestamp),
|
||||
};
|
||||
|
||||
switch(earnedInfo.type) {
|
||||
case [ Achievement.Types.UserStatSet ] :
|
||||
case [ Achievement.Types.UserStatInc ] :
|
||||
case [ Achievement.Types.UserStatIncNewVal ] :
|
||||
earnedInfo.statName = achievement.data.statName;
|
||||
break;
|
||||
}
|
||||
|
||||
return earnedInfo;
|
||||
}).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore).
|
||||
|
||||
return cb(null, earned);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
exports.moduleInitialize = (initInfo, cb) => {
|
||||
achievementsInstance = new Achievements(initInfo.events);
|
||||
achievementsInstance.init( err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
};
|
29
core/acs.js
29
core/acs.js
|
@ -10,15 +10,15 @@ const assert = require('assert');
|
|||
const _ = require('lodash');
|
||||
|
||||
class ACS {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
constructor(subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
check(acs, scope, defaultAcs) {
|
||||
acs = acs ? acs[scope] : defaultAcs;
|
||||
acs = acs || defaultAcs;
|
||||
try {
|
||||
return checkAcs(acs, { client : this.client } );
|
||||
return checkAcs(acs, { subject : this.subject } );
|
||||
} catch(e) {
|
||||
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS');
|
||||
return false;
|
||||
|
@ -51,20 +51,37 @@ class ACS {
|
|||
return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload);
|
||||
}
|
||||
|
||||
hasMenuModuleAccess(modInst) {
|
||||
const acs = _.get(modInst, 'menuConfig.config.acs');
|
||||
if(!_.isString(acs)) {
|
||||
return true; // no ACS check req.
|
||||
}
|
||||
try {
|
||||
return checkAcs(acs, { subject : this.subject } );
|
||||
} catch(e) {
|
||||
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getConditionalValue(condArray, memberName) {
|
||||
assert(_.isArray(condArray));
|
||||
if(!Array.isArray(condArray)) {
|
||||
// no cond array, just use the value
|
||||
return condArray;
|
||||
}
|
||||
|
||||
assert(_.isString(memberName));
|
||||
|
||||
const matchCond = condArray.find( cond => {
|
||||
if(_.has(cond, 'acs')) {
|
||||
try {
|
||||
return checkAcs(cond.acs, { client : this.client } );
|
||||
return checkAcs(cond.acs, { subject : this.subject } );
|
||||
} catch(e) {
|
||||
Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true; // no acs check req.
|
||||
return true; // no ACS check req.
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -844,107 +844,206 @@ function peg$parse(input, options) {
|
|||
}
|
||||
|
||||
|
||||
var client = options.client;
|
||||
var user = options.client.user;
|
||||
const UserProps = require('./user_property.js');
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
var _ = require('lodash');
|
||||
var assert = require('assert');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const client = _.get(options, 'subject.client');
|
||||
const user = _.get(options, 'subject.user');
|
||||
|
||||
function checkAccess(acsCode, value) {
|
||||
try {
|
||||
return {
|
||||
LC : function isLocalConnection() {
|
||||
return client.isLocal();
|
||||
return client && client.isLocal();
|
||||
},
|
||||
AG : function ageGreaterOrEqualThan() {
|
||||
return !isNaN(value) && user.getAge() >= value;
|
||||
return !isNaN(value) && user && user.getAge() >= value;
|
||||
},
|
||||
AS : function accountStatus() {
|
||||
if(!_.isArray(value)) {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
if(!Array.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
const userAccountStatus = parseInt(user.properties.account_status, 10);
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(userAccountStatus) > -1;
|
||||
const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus);
|
||||
return value.map(n => parseInt(n, 10)).includes(userAccountStatus);
|
||||
},
|
||||
EC : function isEncoding() {
|
||||
const encoding = _.get(client, 'term.outputEncoding', '').toLowerCase();
|
||||
switch(value) {
|
||||
case 0 : return 'cp437' === client.term.outputEncoding.toLowerCase();
|
||||
case 1 : return 'utf-8' === client.term.outputEncoding.toLowerCase();
|
||||
case 0 : return 'cp437' === encoding;
|
||||
case 1 : return 'utf-8' === encoding;
|
||||
default : return false;
|
||||
}
|
||||
},
|
||||
GM : function isOneOfGroups() {
|
||||
if(!_.isArray(value)) {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _.findIndex(value, function cmp(groupName) {
|
||||
return user.isGroupMember(groupName);
|
||||
}) > - 1;
|
||||
if(!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
return value.some(groupName => user.isGroupMember(groupName));
|
||||
},
|
||||
NN : function isNode() {
|
||||
return client.node === value;
|
||||
if(!client) {
|
||||
return false;
|
||||
}
|
||||
if(!Array.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
return value.map(n => parseInt(n, 10)).includes(client.node);
|
||||
},
|
||||
NP : function numberOfPosts() {
|
||||
const postCount = parseInt(user.properties.post_count, 10);
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
|
||||
return !isNaN(value) && postCount >= value;
|
||||
},
|
||||
NC : function numberOfCalls() {
|
||||
const loginCount = parseInt(user.properties.login_count, 10);
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const loginCount = user.getPropertyAsNumber(UserProps.LoginCount);
|
||||
return !isNaN(value) && loginCount >= value;
|
||||
},
|
||||
AA : function accountAge() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const accountCreated = moment(user.getProperty(UserProps.AccountCreated));
|
||||
const now = moment();
|
||||
const daysOld = accountCreated.diff(moment(), 'days');
|
||||
return !isNaN(value) &&
|
||||
accountCreated.isValid() &&
|
||||
now.isAfter(accountCreated) &&
|
||||
daysOld >= value;
|
||||
},
|
||||
BU : function bytesUploaded() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
|
||||
return !isNaN(value) && bytesUp >= value;
|
||||
},
|
||||
UP : function uploads() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
|
||||
return !isNaN(value) && uls >= value;
|
||||
},
|
||||
BD : function bytesDownloaded() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
|
||||
return !isNaN(value) && bytesDown >= value;
|
||||
},
|
||||
DL : function downloads() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
|
||||
return !isNaN(value) && dls >= value;
|
||||
},
|
||||
NR : function uploadDownloadRatioGreaterThan() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
|
||||
const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
|
||||
const ratio = ~~((ulCount / dlCount) * 100);
|
||||
return !isNaN(value) && ratio >= value;
|
||||
},
|
||||
KR : function uploadDownloadByteRatioGreaterThan() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
|
||||
const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
|
||||
const ratio = ~~((ulBytes / dlBytes) * 100);
|
||||
return !isNaN(value) && ratio >= value;
|
||||
},
|
||||
PC : function postCallRatio() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
|
||||
const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0;
|
||||
const ratio = ~~((postCount / loginCount) * 100);
|
||||
return !isNaN(value) && ratio >= value;
|
||||
},
|
||||
SC : function isSecureConnection() {
|
||||
return client.session.isSecure;
|
||||
return _.get(client, 'session.isSecure', false);
|
||||
},
|
||||
ML : function minutesLeft() {
|
||||
// :TODO: implement me!
|
||||
return false;
|
||||
},
|
||||
TH : function termHeight() {
|
||||
return !isNaN(value) && client.term.termHeight >= value;
|
||||
return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value;
|
||||
},
|
||||
TM : function isOneOfThemes() {
|
||||
if(!_.isArray(value)) {
|
||||
if(!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.indexOf(client.currentTheme.name) > -1;
|
||||
return value.includes(_.get(client, 'currentTheme.name'));
|
||||
},
|
||||
TT : function isOneOfTermTypes() {
|
||||
if(!_.isArray(value)) {
|
||||
if(!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.indexOf(client.term.termType) > -1;
|
||||
return value.includes(_.get(client, 'term.termType'));
|
||||
},
|
||||
TW : function termWidth() {
|
||||
return !isNaN(value) && client.term.termWidth >= value;
|
||||
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
|
||||
},
|
||||
ID : function isUserId(value) {
|
||||
if(!_.isArray(value)) {
|
||||
ID : function isUserId() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
if(!Array.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(user.userId) > -1;
|
||||
return value.map(n => parseInt(n, 10)).includes(user.userId);
|
||||
},
|
||||
WD : function isOneOfDayOfWeek() {
|
||||
if(!_.isArray(value)) {
|
||||
if(!Array.isArray(value)) {
|
||||
value = [ value ];
|
||||
}
|
||||
|
||||
value = value.map(n => parseInt(n, 10)); // ensure we have integers
|
||||
return value.indexOf(new Date().getDay()) > -1;
|
||||
return value.map(n => parseInt(n, 10)).includes(new Date().getDay());
|
||||
},
|
||||
MM : function isMinutesPastMidnight() {
|
||||
// :TODO: return true if value is >= minutes past midnight sys time
|
||||
const now = moment();
|
||||
const midnight = now.clone().startOf('day')
|
||||
const minutesPastMidnight = now.diff(midnight, 'minutes');
|
||||
return !isNaN(value) && minutesPastMidnight >= value;
|
||||
},
|
||||
AC : function achievementCount() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0;
|
||||
return !isNan(value) && points >= value;
|
||||
},
|
||||
AP : function achievementPoints() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0;
|
||||
return !isNan(value) && points >= value;
|
||||
}
|
||||
}[acsCode](value);
|
||||
} catch (e) {
|
||||
client.log.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!');
|
||||
const logger = _.get(client, 'log', Log);
|
||||
logger.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
const miscUtil = require('./misc_util.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const events = require('events');
|
||||
const util = require('util');
|
||||
const _ = require('lodash');
|
||||
|
@ -24,7 +26,7 @@ function ANSIEscapeParser(options) {
|
|||
this.graphicRendition = {};
|
||||
|
||||
this.parseState = {
|
||||
re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g,
|
||||
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||
};
|
||||
|
||||
options = miscUtil.valueWithDefault(options, {
|
||||
|
@ -63,7 +65,7 @@ function ANSIEscapeParser(options) {
|
|||
delete self.savedPosition;
|
||||
|
||||
self.positionUpdated();
|
||||
// self.rowUpdated();
|
||||
// self.rowUpdated();
|
||||
};
|
||||
|
||||
self.clearScreen = function() {
|
||||
|
@ -71,7 +73,7 @@ function ANSIEscapeParser(options) {
|
|||
self.emit('clear screen');
|
||||
};
|
||||
|
||||
/*
|
||||
/*
|
||||
self.rowUpdated = function() {
|
||||
self.emit('row update', self.row + self.scrollBack);
|
||||
};*/
|
||||
|
@ -142,17 +144,9 @@ function ANSIEscapeParser(options) {
|
|||
}
|
||||
}
|
||||
|
||||
function getProcessedMCI(mci) {
|
||||
if(self.mciReplaceChar.length > 0) {
|
||||
return ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + new Array(mci.length + 1).join(self.mciReplaceChar);
|
||||
} else {
|
||||
return mci;
|
||||
}
|
||||
}
|
||||
|
||||
function parseMCI(buffer) {
|
||||
// :TODO: move this to "constants" seciton @ top
|
||||
var mciRe = /\%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g;
|
||||
var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g;
|
||||
var pos = 0;
|
||||
var match;
|
||||
var mciCode;
|
||||
|
@ -197,16 +191,12 @@ function ANSIEscapeParser(options) {
|
|||
if(self.mciReplaceChar.length > 0) {
|
||||
const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase);
|
||||
|
||||
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[\;m]/).slice(0, 3));
|
||||
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3));
|
||||
|
||||
literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
|
||||
} else {
|
||||
literal(match[0]);
|
||||
}
|
||||
|
||||
//literal(getProcessedMCI(match[0]));
|
||||
|
||||
//self.emit('chunk', getProcessedMCI(match[0]));
|
||||
}
|
||||
|
||||
} while(0 !== mciRe.lastIndex);
|
||||
|
@ -220,7 +210,7 @@ function ANSIEscapeParser(options) {
|
|||
self.parseState = {
|
||||
// ignore anything past EOF marker, if any
|
||||
buffer : input.split(String.fromCharCode(0x1a), 1)[0],
|
||||
re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g,
|
||||
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||
stop : false,
|
||||
};
|
||||
};
|
||||
|
@ -291,13 +281,13 @@ function ANSIEscapeParser(options) {
|
|||
}
|
||||
}
|
||||
|
||||
parseMCI(lastBit)
|
||||
parseMCI(lastBit);
|
||||
}
|
||||
|
||||
self.emit('complete');
|
||||
};
|
||||
|
||||
/*
|
||||
/*
|
||||
self.parse = function(buffer, savedRe) {
|
||||
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
|
||||
// :TODO: move this to "constants" section @ top
|
||||
|
@ -448,7 +438,7 @@ function ANSIEscapeParser(options) {
|
|||
break;
|
||||
|
||||
default :
|
||||
console.log('Unknown attribute: ' + arg); // :TODO: Log properly
|
||||
Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,20 @@
|
|||
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt
|
||||
// * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm
|
||||
//
|
||||
// Modern Windows (Win10+)
|
||||
// * https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
||||
//
|
||||
// VT100
|
||||
// * http://www.noah.org/python/pexpect/ANSI-X3.64.htm
|
||||
//
|
||||
// VTX
|
||||
// * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
|
||||
//
|
||||
// General
|
||||
// * http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
// * http://www.inwap.com/pdp10/ansicode.txt
|
||||
// * Excellent information with many standards covered (for hterm):
|
||||
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/doc/ControlSequences.md
|
||||
//
|
||||
// Other Implementations
|
||||
// * https://github.com/chjj/term.js/blob/master/src/term.js
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const { MenuModule } = require('../core/menu_module.js');
|
||||
const { resetScreen } = require('../core/ansi_term.js');
|
||||
const { Errors } = require('../core/enig_error.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const SSHClient = require('ssh2').Client;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'ArchaicNET',
|
||||
desc : 'ArchaicNET Access Module',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
exports.getModule = class ArchaicNETModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
// establish defaults
|
||||
this.config = options.menuConfig.config;
|
||||
this.config.host = this.config.host || 'bbs.archaicbinary.net';
|
||||
this.config.sshPort = this.config.sshPort || 2222;
|
||||
this.config.rloginPort = this.config.rloginPort || 8513;
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
let clientTerminated;
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
const reqConfs = [ 'username', 'password', 'bbsTag' ];
|
||||
for(let req of reqConfs) {
|
||||
if(!_.isString(_.get(self, [ 'config', req ]))) {
|
||||
return callback(Errors.MissingConfig(`Config requires "${req}"`));
|
||||
}
|
||||
}
|
||||
return callback(null);
|
||||
},
|
||||
function establishSecureConnection(callback) {
|
||||
self.client.term.write(resetScreen());
|
||||
self.client.term.write('Connecting to ArchaicNET, please wait...\n');
|
||||
|
||||
const sshClient = new SSHClient();
|
||||
|
||||
let needRestore = false;
|
||||
//let pipedStream;
|
||||
const restorePipe = function() {
|
||||
if(needRestore && !clientTerminated) {
|
||||
self.client.restoreDataHandler();
|
||||
needRestore = false;
|
||||
}
|
||||
};
|
||||
|
||||
sshClient.on('ready', () => {
|
||||
// track client termination so we can clean up early
|
||||
self.client.once('end', () => {
|
||||
self.client.log.info('Connection ended. Terminating ArchaicNET connection');
|
||||
clientTerminated = true;
|
||||
return sshClient.end();
|
||||
});
|
||||
|
||||
// establish tunnel for rlogin
|
||||
const fwdPort = self.config.rloginPort + self.client.node;
|
||||
sshClient.forwardOut('127.0.0.1', fwdPort, self.config.host, self.config.rloginPort, (err, stream) => {
|
||||
if(err) {
|
||||
return sshClient.end();
|
||||
}
|
||||
|
||||
//
|
||||
// Send rlogin - [<bbsTag>]<userName> e.g. [Xibalba]NuSkooler
|
||||
//
|
||||
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
|
||||
stream.write(rlogin);
|
||||
|
||||
// we need to filter I/O for escape/de-escaping zmodem and the like
|
||||
self.client.setTemporaryDirectDataHandler(data => {
|
||||
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
|
||||
stream.write(Buffer.from(tmp, 'binary'));
|
||||
});
|
||||
needRestore = true;
|
||||
|
||||
stream.on('data', data => {
|
||||
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
|
||||
self.client.term.rawWrite(Buffer.from(tmp, 'binary'));
|
||||
});
|
||||
|
||||
stream.on('close', () => {
|
||||
restorePipe();
|
||||
return sshClient.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
sshClient.on('error', err => {
|
||||
return self.client.log.info(`ArchaicNET SSH client error: ${err.message}`);
|
||||
});
|
||||
|
||||
sshClient.on('close', hadError => {
|
||||
if(hadError) {
|
||||
self.client.warn('Closing ArchaicNET SSH due to error');
|
||||
}
|
||||
restorePipe();
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
self.client.log.trace( { host : self.config.host, port : self.config.sshPort }, 'Connecting to ArchaicNET');
|
||||
sshClient.connect( {
|
||||
host : self.config.host,
|
||||
port : self.config.sshPort,
|
||||
username : self.config.username,
|
||||
password : self.config.password,
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.message }, 'ArchaicNET error');
|
||||
}
|
||||
|
||||
// if the client is stil here, go to previous
|
||||
if(!clientTerminated) {
|
||||
self.prevMenu();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -2,15 +2,17 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const resolveMimeType = require('./mime_util.js').resolveMimeType;
|
||||
const Events = require('./events.js');
|
||||
|
||||
// base/modules
|
||||
const fs = require('graceful-fs');
|
||||
const _ = require('lodash');
|
||||
const pty = require('ptyw.js');
|
||||
const pty = require('node-pty');
|
||||
const paths = require('path');
|
||||
|
||||
let archiveUtil;
|
||||
|
||||
|
@ -48,22 +50,29 @@ module.exports = class ArchiveUtil {
|
|||
}
|
||||
|
||||
// singleton access
|
||||
static getInstance() {
|
||||
static getInstance(noWatch = false) {
|
||||
if(!archiveUtil) {
|
||||
archiveUtil = new ArchiveUtil();
|
||||
archiveUtil.init();
|
||||
archiveUtil.init(noWatch);
|
||||
}
|
||||
return archiveUtil;
|
||||
}
|
||||
|
||||
init() {
|
||||
//
|
||||
// Load configuration
|
||||
//
|
||||
if(_.has(Config, 'archives.archivers')) {
|
||||
Object.keys(Config.archives.archivers).forEach(archKey => {
|
||||
init(noWatch = false) {
|
||||
this.reloadConfig();
|
||||
if(!noWatch) {
|
||||
Events.on(Events.getSystemEvents().ConfigChanged, () => {
|
||||
this.reloadConfig();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const archConfig = Config.archives.archivers[archKey];
|
||||
reloadConfig() {
|
||||
const config = Config();
|
||||
if(_.has(config, 'archives.archivers')) {
|
||||
Object.keys(config.archives.archivers).forEach(archKey => {
|
||||
|
||||
const archConfig = config.archives.archivers[archKey];
|
||||
const archiver = new Archiver(archConfig);
|
||||
|
||||
if(!archiver.ok()) {
|
||||
|
@ -74,33 +83,58 @@ module.exports = class ArchiveUtil {
|
|||
});
|
||||
}
|
||||
|
||||
if(_.isObject(Config.fileTypes)) {
|
||||
Object.keys(Config.fileTypes).forEach(mimeType => {
|
||||
const fileType = Config.fileTypes[mimeType];
|
||||
if(fileType.sig) {
|
||||
fileType.sig = new Buffer(fileType.sig, 'hex');
|
||||
fileType.offset = fileType.offset || 0;
|
||||
if(_.isObject(config.fileTypes)) {
|
||||
const updateSig = (ft) => {
|
||||
ft.sig = Buffer.from(ft.sig, 'hex');
|
||||
ft.offset = ft.offset || 0;
|
||||
|
||||
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
|
||||
const sigLen =fileType.offset + fileType.sig.length;
|
||||
const sigLen = ft.offset + ft.sig.length;
|
||||
if(sigLen > this.longestSignature) {
|
||||
this.longestSignature = sigLen;
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(config.fileTypes).forEach(mimeType => {
|
||||
const fileType = config.fileTypes[mimeType];
|
||||
if(Array.isArray(fileType)) {
|
||||
fileType.forEach(ft => {
|
||||
if(ft.sig) {
|
||||
updateSig(ft);
|
||||
}
|
||||
});
|
||||
} else if(fileType.sig) {
|
||||
updateSig(fileType);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getArchiver(mimeTypeOrExtension) {
|
||||
mimeTypeOrExtension = resolveMimeType(mimeTypeOrExtension);
|
||||
getArchiver(mimeTypeOrExtension, justExtention) {
|
||||
const mimeType = resolveMimeType(mimeTypeOrExtension);
|
||||
|
||||
if(!mimeTypeOrExtension) { // lookup returns false on failure
|
||||
if(!mimeType) { // lookup returns false on failure
|
||||
return;
|
||||
}
|
||||
|
||||
const archiveHandler = _.get( Config, [ 'fileTypes', mimeTypeOrExtension, 'archiveHandler'] );
|
||||
if(archiveHandler) {
|
||||
return _.get( Config, [ 'archives', 'archivers', archiveHandler ] );
|
||||
const config = Config();
|
||||
let fileType = _.get(config, [ 'fileTypes', mimeType ] );
|
||||
|
||||
if(Array.isArray(fileType)) {
|
||||
if(!justExtention) {
|
||||
// need extention for lookup; ambiguous as-is :(
|
||||
return;
|
||||
}
|
||||
// further refine by extention
|
||||
fileType = fileType.find(ft => justExtention === ft.ext);
|
||||
}
|
||||
|
||||
if(!_.isObject(fileType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(fileType.archiveHandler) {
|
||||
return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,37 +142,48 @@ module.exports = class ArchiveUtil {
|
|||
return this.getArchiver(archType) ? true : false;
|
||||
}
|
||||
|
||||
// :TODO: implement me:
|
||||
/*
|
||||
detectTypeWithBuf(buf, cb) {
|
||||
// :TODO: implement me!
|
||||
}
|
||||
*/
|
||||
|
||||
detectType(path, cb) {
|
||||
const closeFile = (fd) => {
|
||||
fs.close(fd, () => { /* sadface */ });
|
||||
};
|
||||
|
||||
fs.open(path, 'r', (err, fd) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const buf = new Buffer(this.longestSignature);
|
||||
const buf = Buffer.alloc(this.longestSignature);
|
||||
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
|
||||
if(err) {
|
||||
closeFile(fd);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const archFormat = _.findKey(Config.fileTypes, fileTypeInfo => {
|
||||
if(!fileTypeInfo.sig) {
|
||||
const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => {
|
||||
const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ];
|
||||
return fileTypeInfos.find(fti => {
|
||||
if(!fti.sig || !fti.archiveHandler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lenNeeded = fileTypeInfo.offset + fileTypeInfo.sig.length;
|
||||
const lenNeeded = fti.offset + fti.sig.length;
|
||||
|
||||
if(bytesRead < lenNeeded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const comp = buf.slice(fileTypeInfo.offset, fileTypeInfo.offset + fileTypeInfo.sig.length);
|
||||
return (fileTypeInfo.sig.equals(comp));
|
||||
const comp = buf.slice(fti.offset, fti.offset + fti.sig.length);
|
||||
return (fti.sig.equals(comp));
|
||||
});
|
||||
});
|
||||
|
||||
closeFile(fd);
|
||||
return cb(archFormat ? null : Errors.General('Unknown type'), archFormat);
|
||||
});
|
||||
});
|
||||
|
@ -160,7 +205,7 @@ module.exports = class ArchiveUtil {
|
|||
}
|
||||
|
||||
compressTo(archType, archivePath, files, cb) {
|
||||
const archiver = this.getArchiver(archType);
|
||||
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
||||
|
||||
if(!archiver) {
|
||||
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
||||
|
@ -177,7 +222,9 @@ module.exports = class ArchiveUtil {
|
|||
try {
|
||||
proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts());
|
||||
} catch(e) {
|
||||
return cb(e);
|
||||
return cb(Errors.ExternalProcess(
|
||||
`Error spawning archiver process "${archiver.compress.cmd}" with args "${args.join(' ')}": ${e.message}`)
|
||||
);
|
||||
}
|
||||
|
||||
return this.spawnHandler(proc, 'Compression', cb);
|
||||
|
@ -194,7 +241,7 @@ module.exports = class ArchiveUtil {
|
|||
haveFileList = true;
|
||||
}
|
||||
|
||||
const archiver = this.getArchiver(archType);
|
||||
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
||||
|
||||
if(!archiver) {
|
||||
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
||||
|
@ -205,7 +252,12 @@ module.exports = class ArchiveUtil {
|
|||
extractPath : extractPath,
|
||||
};
|
||||
|
||||
const action = haveFileList ? 'extract' : 'decompress';
|
||||
let action = haveFileList ? 'extract' : 'decompress';
|
||||
if('extract' === action && !_.isObject(archiver[action])) {
|
||||
// we're forced to do a full decompress
|
||||
action = 'decompress';
|
||||
haveFileList = false;
|
||||
}
|
||||
|
||||
// we need to treat {fileList} special in that it should be broken up to 0:n args
|
||||
const args = archiver[action].args.map( arg => {
|
||||
|
@ -220,16 +272,18 @@ module.exports = class ArchiveUtil {
|
|||
|
||||
let proc;
|
||||
try {
|
||||
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts());
|
||||
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath));
|
||||
} catch(e) {
|
||||
return cb(e);
|
||||
return cb(Errors.ExternalProcess(
|
||||
`Error spawning archiver process "${archiver[action].cmd}" with args "${args.join(' ')}": ${e.message}`)
|
||||
);
|
||||
}
|
||||
|
||||
return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb);
|
||||
}
|
||||
|
||||
listEntries(archivePath, archType, cb) {
|
||||
const archiver = this.getArchiver(archType);
|
||||
const archiver = this.getArchiver(archType, paths.extname(archivePath));
|
||||
|
||||
if(!archiver) {
|
||||
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
|
||||
|
@ -245,7 +299,9 @@ module.exports = class ArchiveUtil {
|
|||
try {
|
||||
proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
|
||||
} catch(e) {
|
||||
return cb(e);
|
||||
return cb(Errors.ExternalProcess(
|
||||
`Error spawning archiver process "${archiver.list.cmd}" with args "${args.join(' ')}": ${e.message}`)
|
||||
);
|
||||
}
|
||||
|
||||
let output = '';
|
||||
|
@ -276,13 +332,17 @@ module.exports = class ArchiveUtil {
|
|||
});
|
||||
}
|
||||
|
||||
getPtyOpts() {
|
||||
return {
|
||||
// :TODO: cwd
|
||||
getPtyOpts(extractPath) {
|
||||
const opts = {
|
||||
name : 'enigma-archiver',
|
||||
cols : 80,
|
||||
rows : 24,
|
||||
env : process.env,
|
||||
};
|
||||
if(extractPath) {
|
||||
opts.cwd = extractPath;
|
||||
}
|
||||
// :TODO: set cwd to supplied temp path if not sepcific extract
|
||||
return opts;
|
||||
}
|
||||
};
|
||||
|
|
11
core/art.js
11
core/art.js
|
@ -2,11 +2,12 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const miscUtil = require('./misc_util.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const aep = require('./ansi_escape_parser.js');
|
||||
const sauce = require('./sauce.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
// deps
|
||||
const fs = require('graceful-fs');
|
||||
|
@ -126,7 +127,7 @@ function getArtFromPath(path, options, cb) {
|
|||
function getArt(name, options, cb) {
|
||||
const ext = paths.extname(name);
|
||||
|
||||
options.basePath = miscUtil.valueWithDefault(options.basePath, Config.paths.art);
|
||||
options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
|
||||
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
|
||||
|
||||
// :TODO: make use of asAnsi option and convert from supported -> ansi
|
||||
|
@ -209,7 +210,7 @@ function getArt(name, options, cb) {
|
|||
return getArtFromPath(readPath, options, cb);
|
||||
}
|
||||
|
||||
return cb(new Error(`No matching art for supplied criteria: ${name}`));
|
||||
return cb(Errors.DoesNotExist(`No matching art for supplied criteria: ${name}`));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -236,7 +237,7 @@ function display(client, art, options, cb) {
|
|||
}
|
||||
|
||||
if(!art || !art.length) {
|
||||
return cb(new Error('Empty art'));
|
||||
return cb(Errors.Invalid('No art supplied!'));
|
||||
}
|
||||
|
||||
options.mciReplaceChar = options.mciReplaceChar || ' ';
|
||||
|
@ -288,7 +289,7 @@ function display(client, art, options, cb) {
|
|||
}
|
||||
|
||||
if(!options.disableMciCache) {
|
||||
artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE);
|
||||
artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE);
|
||||
|
||||
// see if we have a mciMap cached for this art
|
||||
if(client.mciCache) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const StatLog = require('./stat_log.js');
|
||||
|
||||
// deps
|
||||
|
@ -29,17 +29,21 @@ const ALL_ASSETS = [
|
|||
'sysStat',
|
||||
];
|
||||
|
||||
const ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*');
|
||||
const ASSET_RE = new RegExp(
|
||||
'^@(' + ALL_ASSETS.join('|') + ')' +
|
||||
/:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source
|
||||
);
|
||||
|
||||
function parseAsset(s) {
|
||||
const m = ASSET_RE.exec(s);
|
||||
|
||||
if(m) {
|
||||
let result = { type : m[1] };
|
||||
const result = { type : m[1] };
|
||||
|
||||
if(m[3]) {
|
||||
result.location = m[2];
|
||||
result.asset = m[3];
|
||||
if(m[2]) {
|
||||
result.location = m[2];
|
||||
}
|
||||
} else {
|
||||
result.asset = m[2];
|
||||
}
|
||||
|
@ -95,7 +99,7 @@ function resolveConfigAsset(spec) {
|
|||
assert('config' === asset.type);
|
||||
|
||||
const path = asset.asset.split('.');
|
||||
let conf = Config;
|
||||
let conf = Config();
|
||||
for(let i = 0; i < path.length; ++i) {
|
||||
if(_.isUndefined(conf[path[i]])) {
|
||||
return spec;
|
||||
|
|
89
core/bbs.js
89
core/bbs.js
|
@ -10,6 +10,9 @@ const conf = require('./config.js');
|
|||
const logger = require('./logger.js');
|
||||
const database = require('./database.js');
|
||||
const resolvePath = require('./misc_util.js').resolvePath;
|
||||
const UserProps = require('./user_property.js');
|
||||
const SysProps = require('./system_property.js');
|
||||
const SysLogKeys = require('./system_log.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -18,6 +21,7 @@ const _ = require('lodash');
|
|||
const mkdirs = require('fs-extra').mkdirs;
|
||||
const fs = require('graceful-fs');
|
||||
const paths = require('path');
|
||||
const moment = require('moment');
|
||||
|
||||
// our main entry point
|
||||
exports.main = main;
|
||||
|
@ -25,9 +29,12 @@ exports.main = main;
|
|||
// object with various services we want to de-init/shutdown cleanly if possible
|
||||
const initServices = {};
|
||||
|
||||
const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby';
|
||||
// only include bbs.js once @ startup; this should be fine
|
||||
const COPYRIGHT = fs.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8').split(/\r?\n/g)[0];
|
||||
|
||||
const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`;
|
||||
const HELP =
|
||||
`${ENIGMA_COPYRIGHT}
|
||||
`${FULL_COPYRIGHT}
|
||||
usage: main.js <args>
|
||||
eg : main.js --config /enigma_install_path/config/
|
||||
|
||||
|
@ -42,6 +49,10 @@ function printHelpAndExit() {
|
|||
process.exit();
|
||||
}
|
||||
|
||||
function printVersionAndExit() {
|
||||
console.info(require('../package.json').version);
|
||||
}
|
||||
|
||||
function main() {
|
||||
async.waterfall(
|
||||
[
|
||||
|
@ -49,7 +60,11 @@ function main() {
|
|||
const argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
if(argv.help) {
|
||||
printHelpAndExit();
|
||||
return printHelpAndExit();
|
||||
}
|
||||
|
||||
if(argv.version) {
|
||||
return printVersionAndExit();
|
||||
}
|
||||
|
||||
const configOverridePath = argv.config;
|
||||
|
@ -72,10 +87,10 @@ function main() {
|
|||
configPathSupplied = null; // make non-fatal; we'll go with defaults
|
||||
}
|
||||
} else {
|
||||
console.error(err.toString());
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
callback(err);
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function initSystem(callback) {
|
||||
|
@ -88,14 +103,16 @@ function main() {
|
|||
}
|
||||
],
|
||||
function complete(err) {
|
||||
if(!err) {
|
||||
// note this is escaped:
|
||||
fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => {
|
||||
console.info(ENIGMA_COPYRIGHT);
|
||||
console.info(FULL_COPYRIGHT);
|
||||
if(!err) {
|
||||
console.info(banner);
|
||||
}
|
||||
console.info('System started!');
|
||||
});
|
||||
}
|
||||
|
||||
if(err) {
|
||||
console.error('Error initializing: ' + util.inspect(err));
|
||||
|
@ -116,7 +133,10 @@ function shutdownSystem() {
|
|||
const activeConnections = ClientConns.getActiveConnections();
|
||||
let i = activeConnections.length;
|
||||
while(i--) {
|
||||
activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
|
||||
const activeTerm = activeConnections[i].term;
|
||||
if(activeTerm) {
|
||||
activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
|
||||
}
|
||||
ClientConns.removeClient(activeConnections[i]);
|
||||
}
|
||||
callback(null);
|
||||
|
@ -187,48 +207,55 @@ function initialize(cb) {
|
|||
function initStatLog(callback) {
|
||||
return require('./stat_log.js').init(callback);
|
||||
},
|
||||
function initConfigs(callback) {
|
||||
return require('./config_util.js').init(callback);
|
||||
},
|
||||
function initThemes(callback) {
|
||||
// Have to pull in here so it's after Config init
|
||||
require('./theme.js').initAvailableThemes(function onThemesInit(err, themeCount) {
|
||||
logger.log.info({ themeCount : themeCount }, 'Themes initialized');
|
||||
require('./theme.js').initAvailableThemes( (err, themeCount) => {
|
||||
logger.log.info({ themeCount }, 'Themes initialized');
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function loadSysOpInformation(callback) {
|
||||
//
|
||||
// Copy over some +op information from the user DB -> system propertys.
|
||||
// Copy over some +op information from the user DB -> system properties.
|
||||
// * Makes this accessible for MCI codes, easy non-blocking access, etc.
|
||||
// * We do this every time as the op is free to change this information just
|
||||
// like any other user
|
||||
//
|
||||
const User = require('./user.js');
|
||||
|
||||
const propLoadOpts = {
|
||||
names : [
|
||||
UserProps.RealName, UserProps.Sex, UserProps.EmailAddress,
|
||||
UserProps.Location, UserProps.Affiliations,
|
||||
],
|
||||
};
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getOpUserName(next) {
|
||||
return User.getUserName(1, next);
|
||||
},
|
||||
function getOpProps(opUserName, next) {
|
||||
const propLoadOpts = {
|
||||
names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ],
|
||||
};
|
||||
User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => {
|
||||
return next(err, opUserName, opProps);
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
(err, opUserName, opProps) => {
|
||||
const StatLog = require('./stat_log.js');
|
||||
|
||||
if(err) {
|
||||
[ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => {
|
||||
StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A');
|
||||
propLoadOpts.names.concat('username').forEach(v => {
|
||||
StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A');
|
||||
});
|
||||
} else {
|
||||
opProps.username = opUserName;
|
||||
|
||||
_.each(opProps, (v, k) => {
|
||||
StatLog.setNonPeristentSystemStat(`sysop_${k}`, v);
|
||||
StatLog.setNonPersistentSystemStat(`sysop_${k}`, v);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -236,17 +263,24 @@ function initialize(cb) {
|
|||
}
|
||||
);
|
||||
},
|
||||
function initFileAreaStats(callback) {
|
||||
const getAreaStats = require('./file_base_area.js').getAreaStats;
|
||||
getAreaStats( (err, stats) => {
|
||||
if(!err) {
|
||||
function initCallsToday(callback) {
|
||||
const StatLog = require('./stat_log.js');
|
||||
StatLog.setNonPeristentSystemStat('file_base_area_stats', stats);
|
||||
}
|
||||
const filter = {
|
||||
logName : SysLogKeys.UserLoginHistory,
|
||||
resultType : 'count',
|
||||
date : moment(),
|
||||
};
|
||||
|
||||
StatLog.findSystemLogEntries(filter, (err, callsToday) => {
|
||||
if(!err) {
|
||||
StatLog.setNonPersistentSystemStat(SysProps.LoginsToday, callsToday);
|
||||
}
|
||||
return callback(null);
|
||||
});
|
||||
},
|
||||
function initMessageStats(callback) {
|
||||
return require('./message_area.js').startup(callback);
|
||||
},
|
||||
function initMCI(callback) {
|
||||
return require('./predefined_mci.js').init(callback);
|
||||
},
|
||||
|
@ -256,9 +290,15 @@ function initialize(cb) {
|
|||
function readyEvents(callback) {
|
||||
return require('./events.js').startup(callback);
|
||||
},
|
||||
function genericModulesInit(callback) {
|
||||
return require('./module_util.js').initializeModules(callback);
|
||||
},
|
||||
function listenConnections(callback) {
|
||||
return require('./listening_server.js').startup(callback);
|
||||
},
|
||||
function readyFileBaseArea(callback) {
|
||||
return require('./file_base_area.js').startup(callback);
|
||||
},
|
||||
function readyFileAreaWeb(callback) {
|
||||
return require('./file_area_web.js').startup(callback);
|
||||
},
|
||||
|
@ -272,6 +312,9 @@ function initialize(cb) {
|
|||
initServices.eventScheduler = modInst;
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function listenUserEventsForStatLog(callback) {
|
||||
return require('./stat_log.js').initUserEvents(callback);
|
||||
}
|
||||
],
|
||||
function onComplete(err) {
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const resetScreen = require('./ansi_term.js').resetScreen;
|
||||
const { MenuModule } = require('./menu_module.js');
|
||||
const { resetScreen } = require('./ansi_term.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const {
|
||||
trackDoorRunBegin,
|
||||
trackDoorRunEnd
|
||||
} = require('./door_util.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
@ -60,15 +65,17 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(_.isString(self.config.sysCode) &&
|
||||
_.isString(self.config.authCode) &&
|
||||
_.isString(self.config.schemeCode) &&
|
||||
_.isString(self.config.door))
|
||||
return self.validateConfigFields(
|
||||
{
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error('Configuration is missing option(s)'));
|
||||
}
|
||||
host : 'string',
|
||||
sysCode : 'string',
|
||||
authCode : 'string',
|
||||
schemeCode : 'string',
|
||||
door : 'string',
|
||||
port : 'number',
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
function acquireToken(callback) {
|
||||
//
|
||||
|
@ -95,7 +102,7 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||
//
|
||||
// Authenticate the token we acquired previously
|
||||
//
|
||||
var headers = {
|
||||
const headers = {
|
||||
'X-User' : self.client.user.userId.toString(),
|
||||
'X-System' : self.config.sysCode,
|
||||
'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'),
|
||||
|
@ -112,10 +119,9 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||
var status = body.trim();
|
||||
|
||||
if('complete' === status) {
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error('Bad authentication status: ' + status));
|
||||
return callback(null);
|
||||
}
|
||||
return callback(Errors.AccessDenied(`Bad authentication status: ${status}`));
|
||||
});
|
||||
},
|
||||
function createTelnetBridge(callback) {
|
||||
|
@ -123,17 +129,19 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||
// Authentication with BBSLink successful. Now, we need to create a telnet
|
||||
// bridge from us to them
|
||||
//
|
||||
var connectOpts = {
|
||||
const connectOpts = {
|
||||
port : self.config.port,
|
||||
host : self.config.host,
|
||||
};
|
||||
|
||||
var clientTerminated;
|
||||
let clientTerminated;
|
||||
|
||||
self.client.term.write(resetScreen());
|
||||
self.client.term.write(' Connecting to BBSLink.net, please wait...\n');
|
||||
|
||||
var bridgeConnection = net.createConnection(connectOpts, function connected() {
|
||||
const doorTracking = trackDoorRunBegin(self.client, `bbslink_${self.config.door}`);
|
||||
|
||||
const bridgeConnection = net.createConnection(connectOpts, function connected() {
|
||||
self.client.log.info(connectOpts, 'BBSLink bridge connection established');
|
||||
|
||||
self.client.term.output.pipe(bridgeConnection);
|
||||
|
@ -145,9 +153,11 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||
});
|
||||
});
|
||||
|
||||
var restorePipe = function() {
|
||||
const restorePipe = function() {
|
||||
self.client.term.output.unpipe(bridgeConnection);
|
||||
self.client.term.output.resume();
|
||||
|
||||
trackDoorRunEnd(doorTracking);
|
||||
};
|
||||
|
||||
bridgeConnection.on('data', function incomingData(data) {
|
||||
|
@ -158,7 +168,7 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
|||
|
||||
bridgeConnection.on('end', function connectionEnd() {
|
||||
restorePipe();
|
||||
callback(clientTerminated ? new Error('Client connection terminated') : null);
|
||||
return callback(clientTerminated ? Errors.General('Client connection terminated') : null);
|
||||
});
|
||||
|
||||
bridgeConnection.on('error', function error(err) {
|
||||
|
|
|
@ -99,6 +99,10 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||
self.displayAddScreen(cb);
|
||||
},
|
||||
deleteBBS : function(formData, extraArgs, cb) {
|
||||
if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList);
|
||||
|
||||
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) {
|
||||
|
@ -214,12 +218,7 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||
}
|
||||
|
||||
setEntries(entriesView) {
|
||||
const config = this.menuConfig.config;
|
||||
const listFormat = config.listFormat || '{bbsName}';
|
||||
const focusListFormat = config.focusListFormat || '{bbsName}';
|
||||
|
||||
entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) );
|
||||
entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) );
|
||||
return entriesView.setItems(this.entries);
|
||||
}
|
||||
|
||||
displayBBSList(clearScreen, cb) {
|
||||
|
@ -273,6 +272,7 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||
(err, row) => {
|
||||
if (!err) {
|
||||
self.entries.push({
|
||||
text : row.bbs_name, // standard field
|
||||
id : row.id,
|
||||
bbsName : row.bbs_name,
|
||||
sysOp : row.sysop,
|
||||
|
@ -323,6 +323,7 @@ exports.getModule = class BBSListModule extends MenuModule {
|
|||
entriesView.setFocusItemIndex(self.selectedBBS);
|
||||
self.drawSelectedEntry(self.entries[self.selectedBBS]);
|
||||
} else if (self.entries.length > 0) {
|
||||
self.selectedBBS = 0;
|
||||
entriesView.setFocusItemIndex(0);
|
||||
self.drawSelectedEntry(self.entries[0]);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ function ButtonView(options) {
|
|||
options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide');
|
||||
|
||||
TextView.call(this, options);
|
||||
|
||||
this.initDefaultWidth();
|
||||
}
|
||||
|
||||
util.inherits(ButtonView, TextView);
|
||||
|
|
110
core/client.js
110
core/client.js
|
@ -35,9 +35,12 @@
|
|||
const term = require('./client_term.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const User = require('./user.js');
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const MenuStack = require('./menu_stack.js');
|
||||
const ACS = require('./acs.js');
|
||||
const Events = require('./events.js');
|
||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
|
||||
// deps
|
||||
const stream = require('stream');
|
||||
|
@ -52,8 +55,9 @@ exports.Client = Client;
|
|||
// Resources & Standards:
|
||||
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html
|
||||
//
|
||||
const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9\;]+)(R)/;
|
||||
const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[\=\?]([0-9a-zA-Z\;]+)(c)/;
|
||||
/* eslint-disable no-control-regex */
|
||||
const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/;
|
||||
const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/;
|
||||
const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/;
|
||||
const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$');
|
||||
const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [
|
||||
|
@ -61,6 +65,7 @@ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:'
|
|||
'(?:M([@ #!a`])(.)(.))', // mouse stuff
|
||||
'(?:1;)?(\\d+)?([a-zA-Z@])'
|
||||
].join('|') + ')');
|
||||
/* eslint-enable no-control-regex */
|
||||
|
||||
const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
|
||||
const RE_ESC_CODE_ANYWHERE = new RegExp( [
|
||||
|
@ -68,11 +73,11 @@ const RE_ESC_CODE_ANYWHERE = new RegExp( [
|
|||
RE_META_KEYCODE_ANYWHERE.source,
|
||||
RE_DSR_RESPONSE_ANYWHERE.source,
|
||||
RE_DEV_ATTR_RESPONSE_ANYWHERE.source,
|
||||
/\u001b./.source
|
||||
/\u001b./.source // eslint-disable-line no-control-regex
|
||||
].join('|'));
|
||||
|
||||
|
||||
function Client(input, output) {
|
||||
function Client(/*input, output*/) {
|
||||
stream.call(this);
|
||||
|
||||
const self = this;
|
||||
|
@ -81,8 +86,9 @@ function Client(input, output) {
|
|||
this.currentTheme = { info : { name : 'N/A', description : 'None' } };
|
||||
this.lastKeyPressMs = Date.now();
|
||||
this.menuStack = new MenuStack(this);
|
||||
this.acs = new ACS(this);
|
||||
this.acs = new ACS( { client : this, user : this.user } );
|
||||
this.mciCache = {};
|
||||
this.interruptQueue = new UserInterruptQueue(this);
|
||||
|
||||
this.clearMciCache = function() {
|
||||
this.mciCache = {};
|
||||
|
@ -100,6 +106,23 @@ function Client(input, output) {
|
|||
}
|
||||
});
|
||||
|
||||
this.setTemporaryDirectDataHandler = function(handler) {
|
||||
this.input.removeAllListeners('data');
|
||||
this.input.on('data', handler);
|
||||
};
|
||||
|
||||
this.restoreDataHandler = function() {
|
||||
this.input.removeAllListeners('data');
|
||||
this.input.on('data', this.dataHandler);
|
||||
};
|
||||
|
||||
this.themeChangedListener = function( { themeId } ) {
|
||||
if(_.get(self.currentTheme, 'info.themeId') === themeId) {
|
||||
self.currentTheme = require('./theme.js').getAvailableThemes().get(themeId);
|
||||
}
|
||||
};
|
||||
|
||||
Events.on(Events.getSystemEvents().ThemeChanged, this.themeChangedListener);
|
||||
|
||||
//
|
||||
// Peek at incoming |data| and emit events for any special
|
||||
|
@ -138,6 +161,7 @@ function Client(input, output) {
|
|||
return termClient;
|
||||
};
|
||||
|
||||
/* eslint-disable no-control-regex */
|
||||
this.isMouseInput = function(data) {
|
||||
return /\x1b\[M/.test(data) ||
|
||||
/\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) ||
|
||||
|
@ -147,6 +171,7 @@ function Client(input, output) {
|
|||
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) ||
|
||||
/\u001b\[(O|I)/.test(data);
|
||||
};
|
||||
/* eslint-enable no-control-regex */
|
||||
|
||||
this.getKeyComponentsFromCode = function(code) {
|
||||
return {
|
||||
|
@ -383,7 +408,7 @@ function Client(input, output) {
|
|||
}
|
||||
|
||||
if(key || ch) {
|
||||
if(Config.logging.traceUserKeyboardInput) {
|
||||
if(Config().logging.traceUserKeyboardInput) {
|
||||
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
|
||||
}
|
||||
|
||||
|
@ -414,47 +439,86 @@ Client.prototype.setTermType = function(termType) {
|
|||
};
|
||||
|
||||
Client.prototype.startIdleMonitor = function() {
|
||||
var self = this;
|
||||
|
||||
self.lastKeyPressMs = Date.now();
|
||||
this.lastKeyPressMs = Date.now();
|
||||
|
||||
//
|
||||
// Every 1m, check for idle.
|
||||
// We also update minutes spent online the system here,
|
||||
// if we have a authenticated user.
|
||||
//
|
||||
self.idleCheck = setInterval(function checkForIdle() {
|
||||
this.idleCheck = setInterval( () => {
|
||||
const nowMs = Date.now();
|
||||
|
||||
const idleLogoutSeconds = self.user.isAuthenticated() ?
|
||||
Config.misc.idleLogoutSeconds :
|
||||
Config.misc.preAuthIdleLogoutSeconds;
|
||||
let idleLogoutSeconds;
|
||||
if(this.user.isAuthenticated()) {
|
||||
idleLogoutSeconds = Config().users.idleLogoutSeconds;
|
||||
|
||||
if(nowMs - self.lastKeyPressMs >= (idleLogoutSeconds * 1000)) {
|
||||
self.emit('idle timeout');
|
||||
//
|
||||
// We don't really want to be firing off an event every 1m for
|
||||
// every user, but want at least some updates for various things
|
||||
// such as achievements. Send off every 5m.
|
||||
//
|
||||
const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1);
|
||||
if(0 === (minOnline % 5)) {
|
||||
Events.emit(
|
||||
Events.getSystemEvents().UserStatIncrement,
|
||||
{
|
||||
user : this.user,
|
||||
statName : UserProps.MinutesOnlineTotalCount,
|
||||
statIncrementBy : 1,
|
||||
statValue : minOnline
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds;
|
||||
}
|
||||
|
||||
if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) {
|
||||
this.emit('idle timeout');
|
||||
}
|
||||
}, 1000 * 60);
|
||||
};
|
||||
|
||||
Client.prototype.stopIdleMonitor = function() {
|
||||
clearInterval(this.idleCheck);
|
||||
};
|
||||
|
||||
Client.prototype.end = function () {
|
||||
if(this.term) {
|
||||
this.term.disconnect();
|
||||
}
|
||||
|
||||
var currentModule = this.menuStack.getCurrentModule;
|
||||
Events.removeListener(Events.getSystemEvents().ThemeChanged, this.themeChangedListener);
|
||||
|
||||
const currentModule = this.menuStack.getCurrentModule;
|
||||
|
||||
if(currentModule) {
|
||||
currentModule.leave();
|
||||
}
|
||||
|
||||
clearInterval(this.idleCheck);
|
||||
// persist time online for authenticated users
|
||||
if(this.user.isAuthenticated()) {
|
||||
this.user.persistProperty(
|
||||
UserProps.MinutesOnlineTotalCount,
|
||||
this.user.getProperty(UserProps.MinutesOnlineTotalCount)
|
||||
);
|
||||
}
|
||||
|
||||
this.stopIdleMonitor();
|
||||
|
||||
try {
|
||||
//
|
||||
// We can end up calling 'end' before TTY/etc. is established, e.g. with SSH
|
||||
//
|
||||
// :TODO: is this OK?
|
||||
if(_.isFunction(this.disconnect)) {
|
||||
return this.disconnect();
|
||||
} else {
|
||||
// legacy fallback
|
||||
return this.output.end.apply(this.output, arguments);
|
||||
}
|
||||
} catch(e) {
|
||||
// TypeError
|
||||
// ie TypeError
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -473,8 +537,8 @@ Client.prototype.waitForKeyPress = function(cb) {
|
|||
};
|
||||
|
||||
Client.prototype.isLocal = function() {
|
||||
// :TODO: return rather client is a local connection or not
|
||||
return false;
|
||||
// :TODO: Handle ipv6 better
|
||||
return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress);
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -482,7 +546,7 @@ Client.prototype.isLocal = function() {
|
|||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something
|
||||
Client.prototype.defaultHandlerMissingMod = function(err) {
|
||||
Client.prototype.defaultHandlerMissingMod = function() {
|
||||
var self = this;
|
||||
|
||||
function handler(err) {
|
||||
|
|
|
@ -4,23 +4,30 @@
|
|||
// ENiGMA½
|
||||
const logger = require('./logger.js');
|
||||
const Events = require('./events.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const hashids = require('hashids');
|
||||
|
||||
exports.getActiveConnections = getActiveConnections;
|
||||
exports.getActiveNodeList = getActiveNodeList;
|
||||
exports.getActiveConnectionList = getActiveConnectionList;
|
||||
exports.addNewClient = addNewClient;
|
||||
exports.removeClient = removeClient;
|
||||
exports.getConnectionByUserId = getConnectionByUserId;
|
||||
exports.getConnectionByNodeId = getConnectionByNodeId;
|
||||
|
||||
const clientConnections = [];
|
||||
exports.clientConnections = clientConnections;
|
||||
|
||||
function getActiveConnections() { return clientConnections; }
|
||||
function getActiveConnections(authUsersOnly = false) {
|
||||
return clientConnections.filter(conn => {
|
||||
return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly);
|
||||
});
|
||||
}
|
||||
|
||||
function getActiveNodeList(authUsersOnly) {
|
||||
function getActiveConnectionList(authUsersOnly) {
|
||||
|
||||
if(!_.isBoolean(authUsersOnly)) {
|
||||
authUsersOnly = true;
|
||||
|
@ -28,16 +35,12 @@ function getActiveNodeList(authUsersOnly) {
|
|||
|
||||
const now = moment();
|
||||
|
||||
const activeConnections = getActiveConnections().filter(ac => {
|
||||
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
|
||||
});
|
||||
|
||||
return _.map(activeConnections, ac => {
|
||||
return _.map(getActiveConnections(authUsersOnly), ac => {
|
||||
const entry = {
|
||||
node : ac.node,
|
||||
authenticated : ac.user.isAuthenticated(),
|
||||
userId : ac.user.userId,
|
||||
action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown',
|
||||
action : _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'),
|
||||
};
|
||||
|
||||
//
|
||||
|
@ -45,11 +48,11 @@ function getActiveNodeList(authUsersOnly) {
|
|||
//
|
||||
if(ac.user.isAuthenticated()) {
|
||||
entry.userName = ac.user.username;
|
||||
entry.realName = ac.user.properties.real_name;
|
||||
entry.location = ac.user.properties.location;
|
||||
entry.affils = ac.user.properties.affiliation;
|
||||
entry.realName = ac.user.properties[UserProps.RealName];
|
||||
entry.location = ac.user.properties[UserProps.Location];
|
||||
entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations];
|
||||
|
||||
const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes');
|
||||
const diff = now.diff(moment(ac.user.properties[UserProps.LastLoginTs]), 'minutes');
|
||||
entry.timeOn = moment.duration(diff, 'minutes');
|
||||
}
|
||||
return entry;
|
||||
|
@ -57,12 +60,28 @@ function getActiveNodeList(authUsersOnly) {
|
|||
}
|
||||
|
||||
function addNewClient(client, clientSock) {
|
||||
const id = client.session.id = clientConnections.push(client) - 1;
|
||||
//
|
||||
// Assign ID/client ID to next lowest & available #
|
||||
//
|
||||
let id = 0;
|
||||
for(let i = 0; i < clientConnections.length; ++i) {
|
||||
if(clientConnections[i].id > id) {
|
||||
break;
|
||||
}
|
||||
id++;
|
||||
}
|
||||
|
||||
client.session.id = id;
|
||||
const remoteAddress = client.remoteAddress = clientSock.remoteAddress;
|
||||
// create a unique identifier one-time ID for this session
|
||||
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]);
|
||||
|
||||
clientConnections.push(client);
|
||||
clientConnections.sort( (c1, c2) => c1.session.id - c2.session.id);
|
||||
|
||||
// Create a client specific logger
|
||||
// Note that this will be updated @ login with additional information
|
||||
client.log = logger.log.child( { clientId : id } );
|
||||
client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } );
|
||||
|
||||
const connInfo = {
|
||||
remoteAddress : remoteAddress,
|
||||
|
@ -77,7 +96,10 @@ function addNewClient(client, clientSock) {
|
|||
|
||||
client.log.info(connInfo, 'Client connected');
|
||||
|
||||
Events.emit('codes.l33t.enigma.system.connected', { client : client, connectionCount : clientConnections.length } );
|
||||
Events.emit(
|
||||
Events.getSystemEvents().ClientConnected,
|
||||
{ client : client, connectionCount : clientConnections.length }
|
||||
);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
@ -97,10 +119,22 @@ function removeClient(client) {
|
|||
'Client disconnected'
|
||||
);
|
||||
|
||||
Events.emit('codes.l33t.enigma.system.disconnected', { client : client, connectionCount : clientConnections.length } );
|
||||
if(client.user && client.user.isValid()) {
|
||||
const minutesOnline = moment().diff(moment(client.user.properties[UserProps.LastLoginTs]), 'minutes');
|
||||
Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user, minutesOnline } );
|
||||
}
|
||||
|
||||
Events.emit(
|
||||
Events.getSystemEvents().ClientDisconnected,
|
||||
{ client : client, connectionCount : clientConnections.length }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectionByUserId(userId) {
|
||||
return getActiveConnections().find( ac => userId === ac.user.userId );
|
||||
}
|
||||
|
||||
function getConnectionByNodeId(nodeId) {
|
||||
return getActiveConnections().find( ac => nodeId == ac.node );
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
// ENiGMA½
|
||||
var Log = require('./logger.js').log;
|
||||
var enigmaToAnsi = require('./color_codes.js').enigmaToAnsi;
|
||||
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
||||
|
||||
var iconv = require('iconv-lite');
|
||||
|
@ -15,8 +14,6 @@ exports.ClientTerminal = ClientTerminal;
|
|||
function ClientTerminal(output) {
|
||||
this.output = output;
|
||||
|
||||
var self = this;
|
||||
|
||||
var outputEncoding = 'cp437';
|
||||
assert(iconv.encodingExists(outputEncoding));
|
||||
|
||||
|
@ -176,15 +173,8 @@ ClientTerminal.prototype.rawWrite = function(s, cb) {
|
|||
}
|
||||
};
|
||||
|
||||
ClientTerminal.prototype.pipeWrite = function(s, spec, cb) {
|
||||
spec = spec || 'renegade';
|
||||
|
||||
var conv = {
|
||||
enigma : enigmaToAnsi,
|
||||
renegade : renegadeToAnsi,
|
||||
}[spec] || renegadeToAnsi;
|
||||
|
||||
this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds|
|
||||
ClientTerminal.prototype.pipeWrite = function(s, cb) {
|
||||
this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds|
|
||||
};
|
||||
|
||||
ClientTerminal.prototype.encode = function(s, convertLineFeeds) {
|
||||
|
|
|
@ -1,94 +1,29 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var ansi = require('./ansi_term.js');
|
||||
var getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue;
|
||||
const ANSI = require('./ansi_term.js');
|
||||
const { getPredefinedMCIValue } = require('./predefined_mci.js');
|
||||
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.enigmaToAnsi = enigmaToAnsi;
|
||||
exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes;
|
||||
exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen;
|
||||
exports.stripMciColorCodes = stripMciColorCodes;
|
||||
exports.pipeStringLength = pipeStringLength;
|
||||
exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
|
||||
exports.controlCodesToAnsi = controlCodesToAnsi;
|
||||
|
||||
// :TODO: Not really happy with the module name of "color_codes". Would like something better
|
||||
// :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string?
|
||||
|
||||
|
||||
|
||||
|
||||
// Also add:
|
||||
// * fromCelerity(): |<case sensitive letter>
|
||||
// * fromPCBoard(): (@X<bg><fg>)
|
||||
// * fromWildcat(): (@<bg><fg>@ (same as PCBoard without 'X' prefix and '@' suffix)
|
||||
// * fromWWIV(): <ctrl-c><0-7>
|
||||
// * fromSyncronet(): <ctrl-a><colorCode>
|
||||
// See http://wiki.synchro.net/custom:colors
|
||||
|
||||
// :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc...
|
||||
function enigmaToAnsi(s, client) {
|
||||
if(-1 == s.indexOf('|')) {
|
||||
return s; // no pipe codes present
|
||||
}
|
||||
|
||||
var result = '';
|
||||
var re = /\|([A-Z\d]{2}|\|)/g;
|
||||
var m;
|
||||
var lastIndex = 0;
|
||||
while((m = re.exec(s))) {
|
||||
var val = m[1];
|
||||
|
||||
if('|' == val) {
|
||||
result += '|';
|
||||
continue;
|
||||
}
|
||||
|
||||
// convert to number
|
||||
val = parseInt(val, 10);
|
||||
if(isNaN(val)) {
|
||||
//
|
||||
// ENiGMA MCI code? Only available if |client|
|
||||
// is supplied.
|
||||
//
|
||||
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
|
||||
}
|
||||
|
||||
if(_.isString(val)) {
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + val;
|
||||
} else {
|
||||
assert(val >= 0 && val <= 47);
|
||||
|
||||
var attr = '';
|
||||
if(7 == val) {
|
||||
attr = ansi.sgr('normal');
|
||||
} else if (val < 7 || val >= 16) {
|
||||
attr = ansi.sgr(['normal', val]);
|
||||
} else if (val <= 15) {
|
||||
attr = ansi.sgr(['normal', val - 8, 'bold']);
|
||||
}
|
||||
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + attr;
|
||||
}
|
||||
|
||||
lastIndex = re.lastIndex;
|
||||
}
|
||||
|
||||
result = (0 === result.length ? s : result + s.substr(lastIndex));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function stripEnigmaCodes(s) {
|
||||
function stripMciColorCodes(s) {
|
||||
return s.replace(/\|[A-Z\d]{2}/g, '');
|
||||
}
|
||||
|
||||
function enigmaStrLen(s) {
|
||||
return stripEnigmaCodes(s).length;
|
||||
function pipeStringLength(s) {
|
||||
return stripMciColorCodes(s).length;
|
||||
}
|
||||
|
||||
function ansiSgrFromRenegadeColorCode(cc) {
|
||||
return ansi.sgr({
|
||||
return ANSI.sgr({
|
||||
0 : [ 'reset', 'black' ],
|
||||
1 : [ 'reset', 'blue' ],
|
||||
2 : [ 'reset', 'green' ],
|
||||
|
@ -116,14 +51,45 @@ function ansiSgrFromRenegadeColorCode(cc) {
|
|||
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' ],
|
||||
24 : [ 'blink', 'blackBG' ],
|
||||
25 : [ 'blink', 'blueBG' ],
|
||||
26 : [ 'blink', 'greenBG' ],
|
||||
27 : [ 'blink', 'cyanBG' ],
|
||||
28 : [ 'blink', 'redBG' ],
|
||||
29 : [ 'blink', 'magentaBG' ],
|
||||
30 : [ 'blink', 'yellowBG' ],
|
||||
31 : [ 'blink', 'whiteBG' ],
|
||||
}[cc] || 'normal');
|
||||
}
|
||||
|
||||
function ansiSgrFromCnetStyleColorCode(cc) {
|
||||
return ANSI.sgr({
|
||||
c0 : [ 'reset', 'black' ],
|
||||
c1 : [ 'reset', 'red' ],
|
||||
c2 : [ 'reset', 'green' ],
|
||||
c3 : [ 'reset', 'yellow' ],
|
||||
c4 : [ 'reset', 'blue' ],
|
||||
c5 : [ 'reset', 'magenta' ],
|
||||
c6 : [ 'reset', 'cyan' ],
|
||||
c7 : [ 'reset', 'white' ],
|
||||
|
||||
c8 : [ 'bold', 'black' ],
|
||||
c9 : [ 'bold', 'red' ],
|
||||
ca : [ 'bold', 'green' ],
|
||||
cb : [ 'bold', 'yellow' ],
|
||||
cc : [ 'bold', 'blue' ],
|
||||
cd : [ 'bold', 'magenta' ],
|
||||
ce : [ 'bold', 'cyan' ],
|
||||
cf : [ 'bold', 'white' ],
|
||||
|
||||
z0 : [ 'blackBG' ],
|
||||
z1 : [ 'redBG' ],
|
||||
z2 : [ 'greenBG' ],
|
||||
z3 : [ 'yellowBG' ],
|
||||
z4 : [ 'blueBG' ],
|
||||
z5 : [ 'magentaBG' ],
|
||||
z6 : [ 'cyanBG' ],
|
||||
z7 : [ 'whiteBG' ],
|
||||
}[cc] || 'normal');
|
||||
}
|
||||
|
||||
|
@ -132,29 +98,24 @@ function renegadeToAnsi(s, client) {
|
|||
return s; // no pipe codes present
|
||||
}
|
||||
|
||||
var result = '';
|
||||
var re = /\|([A-Z\d]{2}|\|)/g;
|
||||
var m;
|
||||
var lastIndex = 0;
|
||||
let result = '';
|
||||
const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g;
|
||||
let m;
|
||||
let lastIndex = 0;
|
||||
while((m = re.exec(s))) {
|
||||
var val = m[1];
|
||||
|
||||
if('|' == val) {
|
||||
result += '|';
|
||||
continue;
|
||||
}
|
||||
|
||||
// convert to number
|
||||
val = parseInt(val, 10);
|
||||
if(isNaN(val)) {
|
||||
val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal
|
||||
}
|
||||
|
||||
if(_.isString(val)) {
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + val;
|
||||
} else {
|
||||
if(m[3]) {
|
||||
// |## color
|
||||
const val = parseInt(m[3], 10);
|
||||
const attr = ansiSgrFromRenegadeColorCode(val);
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + attr;
|
||||
} else if(m[4] || m[1]) {
|
||||
// |AA MCI code or |Cx## movement where ## is in m[1]
|
||||
let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]);
|
||||
val = _.isString(val) ? val : m[0]; // value itself or literal
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + val;
|
||||
} else if(m[5]) {
|
||||
// || -- literal '|', that is.
|
||||
result += '|';
|
||||
}
|
||||
|
||||
lastIndex = re.lastIndex;
|
||||
|
@ -170,9 +131,11 @@ function renegadeToAnsi(s, client) {
|
|||
//
|
||||
// Supported control code formats:
|
||||
// * Renegade : |##
|
||||
// * PCBoard : @X## where the first number/char is FG color, and second is BG
|
||||
// * PCBoard : @X## where the first number/char is BG color, and second is FG
|
||||
// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
|
||||
// * WWIV : ^#
|
||||
// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format
|
||||
// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format
|
||||
//
|
||||
// TODO: Add Synchronet and Celerity format support
|
||||
//
|
||||
|
@ -180,7 +143,7 @@ function renegadeToAnsi(s, client) {
|
|||
// * 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
|
||||
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1)}|\x11)/g; // eslint-disable-line no-control-regex
|
||||
|
||||
let m;
|
||||
let result = '';
|
||||
|
@ -192,7 +155,7 @@ function controlCodesToAnsi(s, client) {
|
|||
while((m = RE.exec(s))) {
|
||||
switch(m[0].charAt(0)) {
|
||||
case '|' :
|
||||
// Renegade or ENiGMA MCI
|
||||
// Renegade |##
|
||||
v = parseInt(m[2], 10);
|
||||
|
||||
if(isNaN(v)) {
|
||||
|
@ -216,26 +179,6 @@ function controlCodesToAnsi(s, client) {
|
|||
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' ],
|
||||
|
@ -254,19 +197,40 @@ function controlCodesToAnsi(s, client) {
|
|||
D : [ 'bold', 'magentaBG' ],
|
||||
E : [ 'bold', 'yellowBG' ],
|
||||
F : [ 'bold', 'whiteBG' ],
|
||||
}[v.charAt(1)] || [ 'normal' ];
|
||||
}[v.charAt(0)] || [ 'normal' ];
|
||||
|
||||
v = ansi.sgr(fg.concat(bg));
|
||||
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(1)] || ['normal'];
|
||||
|
||||
v = ANSI.sgr(fg.concat(bg));
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
||||
break;
|
||||
|
||||
case '\x03' :
|
||||
// WWIV
|
||||
v = parseInt(m[8], 10);
|
||||
|
||||
if(isNaN(v)) {
|
||||
v += m[0];
|
||||
} else {
|
||||
v = ansi.sgr({
|
||||
v = ANSI.sgr({
|
||||
0 : [ 'reset', 'black' ],
|
||||
1 : [ 'bold', 'cyan' ],
|
||||
2 : [ 'bold', 'yellow' ],
|
||||
|
@ -281,7 +245,24 @@ function controlCodesToAnsi(s, client) {
|
|||
}
|
||||
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
||||
break;
|
||||
|
||||
case '\x19' :
|
||||
case '\0x11' :
|
||||
// CNET "Y-Style" & "Q-Style"
|
||||
v = m[9] || m[11];
|
||||
if(v) {
|
||||
if('n1' === v) {
|
||||
v = '\n';
|
||||
} else if('f1' === v) {
|
||||
v = ANSI.clearScreen();
|
||||
} else {
|
||||
v = ansiSgrFromCnetStyleColorCode(v);
|
||||
}
|
||||
} else {
|
||||
v = m[0];
|
||||
}
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + v;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||
const { MenuModule } = require('../core/menu_module.js');
|
||||
const { resetScreen } = require('../core/ansi_term.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const {
|
||||
trackDoorRunBegin,
|
||||
trackDoorRunEnd
|
||||
} = require('./door_util.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const RLogin = require('rlogin');
|
||||
|
||||
exports.moduleInfo = {
|
||||
|
@ -32,29 +36,40 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||
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);
|
||||
return self.validateConfigFields(
|
||||
{
|
||||
host : 'string',
|
||||
password : 'string',
|
||||
bbsTag : 'string',
|
||||
rloginPort : 'number',
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
function establishRloginConnection(callback) {
|
||||
self.client.term.write(resetScreen());
|
||||
self.client.term.write('Connecting to CombatNet, please wait...\n');
|
||||
|
||||
let doorTracking;
|
||||
|
||||
const restorePipeToNormal = function() {
|
||||
if(self.client.term.output) {
|
||||
self.client.term.output.removeListener('data', sendToRloginBuffer);
|
||||
|
||||
if(doorTracking) {
|
||||
trackDoorRunEnd(doorTracking);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -62,21 +77,21 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||
rlogin.on('error', err => {
|
||||
self.client.log.info(`CombatNet rlogin client error: ${err.message}`);
|
||||
restorePipeToNormal();
|
||||
callback(err);
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
// If we've been disconnected ...
|
||||
rlogin.on('disconnect', () => {
|
||||
self.client.log.info(`Disconnected from CombatNet`);
|
||||
self.client.log.info('Disconnected from CombatNet');
|
||||
restorePipeToNormal();
|
||||
callback(null);
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
function sendToRloginBuffer(buffer) {
|
||||
rlogin.send(buffer);
|
||||
};
|
||||
}
|
||||
|
||||
rlogin.on("connect",
|
||||
rlogin.on('connect',
|
||||
/* The 'connect' event handler will be supplied with one argument,
|
||||
a boolean indicating whether or not the connection was established. */
|
||||
|
||||
|
@ -85,14 +100,15 @@ exports.getModule = class CombatNetModule extends MenuModule {
|
|||
self.client.log.info('Connected to CombatNet');
|
||||
self.client.term.output.on('data', sendToRloginBuffer);
|
||||
|
||||
doorTracking = trackDoorRunBegin(self.client);
|
||||
} else {
|
||||
return callback(new Error('Failed to establish establish CombatNet connection'));
|
||||
return callback(Errors.General('Failed to establish establish CombatNet connection'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// If data (a Buffer) has been received from the server ...
|
||||
rlogin.on("data", (data) => {
|
||||
rlogin.on('data', (data) => {
|
||||
self.client.term.rawWrite(data);
|
||||
});
|
||||
|
||||
|
|
463
core/config.js
463
core/config.js
|
@ -2,17 +2,19 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
|
||||
// deps
|
||||
const fs = require('graceful-fs');
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const hjson = require('hjson');
|
||||
const assert = require('assert');
|
||||
|
||||
exports.init = init;
|
||||
exports.getDefaultPath = getDefaultPath;
|
||||
exports.getDefaultConfig = getDefaultConfig;
|
||||
|
||||
let currentConfiguration = {};
|
||||
|
||||
function hasMessageConferenceAndArea(config) {
|
||||
assert(_.isObject(config.messageConferences)); // we create one ourself!
|
||||
|
@ -40,43 +42,53 @@ function hasMessageConferenceAndArea(config) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function init(configPath, options, cb) {
|
||||
if(!cb && _.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
const ArrayReplaceKeyPaths = [
|
||||
'loginServers.ssh.algorithms.kex',
|
||||
'loginServers.ssh.algorithms.cipher',
|
||||
'loginServers.ssh.algorithms.hmac',
|
||||
'loginServers.ssh.algorithms.compress',
|
||||
];
|
||||
|
||||
const ArrayReplaceKeys = [
|
||||
'args',
|
||||
'sendArgs', 'recvArgs', 'recvArgsNonBatch',
|
||||
];
|
||||
|
||||
function mergeValidateAndFinalize(config, cb) {
|
||||
const defaultConfig = getDefaultConfig();
|
||||
|
||||
const arrayReplaceKeyPathsMutable = _.clone(ArrayReplaceKeyPaths);
|
||||
const shouldReplaceArray = (arr, key) => {
|
||||
if(ArrayReplaceKeys.includes(key)) {
|
||||
return true;
|
||||
}
|
||||
for(let i = 0; i < arrayReplaceKeyPathsMutable.length; ++i) {
|
||||
const o = _.get(defaultConfig, arrayReplaceKeyPathsMutable[i]);
|
||||
if(_.isEqual(o, arr)) {
|
||||
arrayReplaceKeyPathsMutable.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function loadUserConfig(callback) {
|
||||
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);
|
||||
} catch(e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
return callback(null, configJson);
|
||||
});
|
||||
},
|
||||
function mergeWithDefaultConfig(configJson, callback) {
|
||||
|
||||
function mergeWithDefaultConfig(callback) {
|
||||
const mergedConfig = _.mergeWith(
|
||||
getDefaultConfig(),
|
||||
configJson, (conf1, conf2) => {
|
||||
// Arrays should always concat
|
||||
if(_.isArray(conf1)) {
|
||||
// :TODO: look for collisions & override dupes
|
||||
return conf1.concat(conf2);
|
||||
defaultConfig,
|
||||
config,
|
||||
(defConfig, userConfig, key) => {
|
||||
if(Array.isArray(defConfig) && Array.isArray(userConfig)) {
|
||||
//
|
||||
// Arrays are special: Some we merge, while others
|
||||
// we simply replace.
|
||||
//
|
||||
if(shouldReplaceArray(defConfig, key)) {
|
||||
return userConfig;
|
||||
} else {
|
||||
return _.uniq(defConfig.concat(userConfig));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -89,26 +101,65 @@ function init(configPath, options, cb) {
|
|||
//
|
||||
// :TODO: Logic is broken here:
|
||||
if(hasMessageConferenceAndArea(mergedConfig)) {
|
||||
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
||||
msgAreasErr.code = 'EBADCONFIG';
|
||||
return callback(msgAreasErr);
|
||||
} else {
|
||||
return callback(null, mergedConfig);
|
||||
return callback(Errors.MissingConfig('Please create at least one message conference and area!'));
|
||||
}
|
||||
return callback(null, mergedConfig);
|
||||
},
|
||||
function setIt(mergedConfig, callback) {
|
||||
// :TODO: .config property is to be deprecated once conversions are done
|
||||
exports.config = currentConfiguration = mergedConfig;
|
||||
|
||||
exports.get = () => currentConfiguration;
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
function complete(err, mergedConfig) {
|
||||
exports.config = mergedConfig;
|
||||
|
||||
exports.config.get = function(path) {
|
||||
return _.get(exports.config, path);
|
||||
};
|
||||
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function init(configPath, options, cb) {
|
||||
if(!cb && _.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
const changed = ( { fileName, fileRoot } ) => {
|
||||
const reCachedPath = paths.join(fileRoot, fileName);
|
||||
ConfigCache.getConfig(reCachedPath, (err, config) => {
|
||||
if(!err) {
|
||||
mergeValidateAndFinalize(config, err => {
|
||||
if(!err) {
|
||||
const Events = require('./events.js');
|
||||
Events.emit(Events.getSystemEvents().ConfigChanged);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const ConfigCache = require('./config_cache.js');
|
||||
const getConfigOptions = {
|
||||
filePath : configPath,
|
||||
noWatch : options.noWatch,
|
||||
};
|
||||
if(!options.noWatch) {
|
||||
getConfigOptions.callback = changed;
|
||||
}
|
||||
ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return mergeValidateAndFinalize(config, cb);
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultPath() {
|
||||
// e.g. /enigma-bbs-install-path/config/
|
||||
return './config/';
|
||||
|
@ -119,25 +170,32 @@ function getDefaultConfig() {
|
|||
general : {
|
||||
boardName : 'Another Fine ENiGMA½ BBS',
|
||||
|
||||
// :TODO: closedSystem prob belongs under users{}?
|
||||
closedSystem : false, // is the system closed to new users?
|
||||
|
||||
loginAttempts : 3,
|
||||
|
||||
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)
|
||||
menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path
|
||||
promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path
|
||||
achievementFile : 'achievements.hjson',
|
||||
},
|
||||
|
||||
// :TODO: see notes below about 'theme' section - move this!
|
||||
preLoginTheme : 'luciano_blocktronics',
|
||||
|
||||
users : {
|
||||
usernameMin : 2,
|
||||
usernameMax : 16, // Note that FidoNet wants 36 max
|
||||
usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$',
|
||||
usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ .]+$',
|
||||
|
||||
passwordMin : 6,
|
||||
passwordMax : 128,
|
||||
badPassFile : paths.join(__dirname, '../misc/10_million_password_list_top_10000.txt'), // https://github.com/danielmiessler/SecLists
|
||||
|
||||
//
|
||||
// The bad password list is a text file containing a password per line.
|
||||
// Entries in this list are not allowed to be used on the system as they
|
||||
// are known to be too common.
|
||||
//
|
||||
// A great resource can be found at https://github.com/danielmiessler/SecLists
|
||||
//
|
||||
// Current list source: https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/probable-v2-top12000.txt
|
||||
//
|
||||
badPassFile : paths.join(__dirname, '../misc/bad_passwords.txt'),
|
||||
|
||||
realNameMax : 32,
|
||||
locationMax : 32,
|
||||
|
@ -156,29 +214,33 @@ function getDefaultConfig() {
|
|||
'sysop', 'admin', 'administrator', 'root', 'all',
|
||||
'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix'
|
||||
],
|
||||
|
||||
preAuthIdleLogoutSeconds : 60 * 3, // 3m
|
||||
idleLogoutSeconds : 60 * 6, // 6m
|
||||
|
||||
failedLogin : {
|
||||
disconnect : 3, // 0=disabled
|
||||
lockAccount : 9, // 0=disabled; Mark user status as "locked" if >= N
|
||||
autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes.
|
||||
},
|
||||
unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts
|
||||
},
|
||||
|
||||
// :TODO: better name for "defaults"... which is redundant here!
|
||||
/*
|
||||
Concept
|
||||
"theme" : {
|
||||
"default" : "defaultThemeName", // or "*"
|
||||
"preLogin" : "*",
|
||||
"passwordChar" : "*",
|
||||
...
|
||||
}
|
||||
*/
|
||||
defaults : {
|
||||
theme : 'luciano_blocktronics',
|
||||
passwordChar : '*', // TODO: move to user ?
|
||||
theme : {
|
||||
default : 'luciano_blocktronics',
|
||||
preLogin : 'luciano_blocktronics',
|
||||
|
||||
passwordChar : '*',
|
||||
dateFormat : {
|
||||
short : 'MM/DD/YYYY',
|
||||
long : 'ddd, MMMM Do, YYYY',
|
||||
},
|
||||
timeFormat : {
|
||||
short : 'h:mm a',
|
||||
},
|
||||
dateTimeFormat : {
|
||||
short : 'MM/DD/YYYY h:mm a',
|
||||
long : 'ddd, MMMM Do, YYYY, h:mm a',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -200,7 +262,7 @@ function getDefaultConfig() {
|
|||
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/'),
|
||||
dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node<x>/
|
||||
dropFiles : paths.join(__dirname, './../drop/'), // + "/node<x>/
|
||||
misc : paths.join(__dirname, './../misc/'),
|
||||
},
|
||||
|
||||
|
@ -213,37 +275,97 @@ function getDefaultConfig() {
|
|||
ssh : {
|
||||
port : 8889,
|
||||
enabled : false, // default to false as PK/pass in config.hjson are required
|
||||
|
||||
//
|
||||
// Private key in PEM format
|
||||
// Private Key (PK) in PEM format
|
||||
//
|
||||
// Generating your PK:
|
||||
// Choose a cipher (3DES, AES128, or AES256) and a bit strength (2048 or 4096)
|
||||
// Ciphers:
|
||||
// 3des: older, most compatible, least secure
|
||||
// aes128: newer, widely compatible, fairly secure
|
||||
// aes256: newest, least compatible, best security
|
||||
// Bit strength:
|
||||
// 2048: most compatible, decent strength
|
||||
// 4096: stronger, but some software is completely incompatible
|
||||
// 1 - Choose a cipher (3DES, AES128, or AES256)
|
||||
// 3des : older, most compatible, least secure
|
||||
// aes128 : newer, widely compatible, fairly secure
|
||||
// aes256 : newest, least compatible, best security
|
||||
//
|
||||
// 2 - Choose a bit strength (2048 or 4096)
|
||||
// 2048 : most compatible, decent strength
|
||||
// 4096 : stronger, but some software is completely incompatible
|
||||
//
|
||||
// Sample command:
|
||||
// openssl genrsa -aes128 -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, './../config/ssh_private_key.pem'),
|
||||
firstMenu : 'sshConnected',
|
||||
firstMenuNewUser : 'sshConnectedNewUser',
|
||||
|
||||
//
|
||||
// SSH details that can affect security. Stronger ciphers are better for example,
|
||||
// but terminals such as SyncTERM require KEX diffie-hellman-group14-sha1,
|
||||
// cipher 3des-cbc, etc.
|
||||
//
|
||||
// See https://github.com/mscdex/ssh2-streams for the full list of supported
|
||||
// algorithms.
|
||||
//
|
||||
algorithms : {
|
||||
kex : [
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1',
|
||||
'diffie-hellman-group-exchange-sha1',
|
||||
'diffie-hellman-group1-sha1',
|
||||
],
|
||||
cipher : [
|
||||
'aes128-ctr',
|
||||
'aes192-ctr',
|
||||
'aes256-ctr',
|
||||
'aes128-gcm',
|
||||
'aes128-gcm@openssh.com',
|
||||
'aes256-gcm',
|
||||
'aes256-gcm@openssh.com',
|
||||
'aes256-cbc',
|
||||
'aes192-cbc',
|
||||
'aes128-cbc',
|
||||
'blowfish-cbc',
|
||||
'3des-cbc',
|
||||
'arcfour256',
|
||||
'arcfour128',
|
||||
'cast128-cbc',
|
||||
'arcfour',
|
||||
],
|
||||
hmac : [
|
||||
'hmac-sha2-256',
|
||||
'hmac-sha2-512',
|
||||
'hmac-sha1',
|
||||
'hmac-md5',
|
||||
'hmac-sha2-256-96',
|
||||
'hmac-sha2-512-96',
|
||||
'hmac-ripemd160',
|
||||
'hmac-sha1-96',
|
||||
'hmac-md5-96',
|
||||
],
|
||||
// note that we disable compression by default due to issues with many clients. YMMV.
|
||||
compress : [ 'none' ]
|
||||
},
|
||||
},
|
||||
webSocket : {
|
||||
port : 8810, // ws://
|
||||
ws : {
|
||||
// non-secure ws://
|
||||
enabled : false,
|
||||
securePort : 8811, // wss:// - must provide certPem and keyPem
|
||||
port : 8810,
|
||||
},
|
||||
wss : {
|
||||
// secure ws://
|
||||
// must provide valid certPem and keyPem
|
||||
enabled : false,
|
||||
port : 8811,
|
||||
certPem : paths.join(__dirname, './../config/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
contentServers : {
|
||||
web : {
|
||||
|
@ -280,6 +402,46 @@ function getDefaultConfig() {
|
|||
certPem : paths.join(__dirname, './../config/https_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
|
||||
}
|
||||
},
|
||||
|
||||
gopher : {
|
||||
enabled : false,
|
||||
port : 8070,
|
||||
publicHostname : 'another-fine-enigma-bbs.org',
|
||||
publicPort : 8070, // adjust if behind NAT/etc.
|
||||
bannerFile : 'gopher_banner.asc',
|
||||
|
||||
//
|
||||
// Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ]
|
||||
// to export message confs/areas
|
||||
//
|
||||
},
|
||||
|
||||
nntp : {
|
||||
// internal caching of groups, message lists, etc.
|
||||
cache : {
|
||||
maxItems : 200,
|
||||
maxAge : 1000 * 30, // 30s
|
||||
},
|
||||
|
||||
//
|
||||
// Set publicMessageConferences{} to a map of confTag -> [ areaTag1, areaTag2, ... ]
|
||||
// in order to export *public* conf/areas that are available to anonymous
|
||||
// NNTP users. Other conf/areas: Standard ACS rules apply.
|
||||
//
|
||||
publicMessageConferences: {},
|
||||
|
||||
nntp : {
|
||||
enabled : false,
|
||||
port : 8119,
|
||||
},
|
||||
|
||||
nntps : {
|
||||
enabled : false,
|
||||
port : 8563,
|
||||
certPem : paths.join(__dirname, './../config/nntps_cert.pem'),
|
||||
keyPem : paths.join(__dirname, './../config/nntps_key.pem'),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -296,7 +458,17 @@ function getDefaultConfig() {
|
|||
'--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate',
|
||||
'--metadatadate', '--xmptoolkit'
|
||||
]
|
||||
}
|
||||
},
|
||||
XDMS2Desc : {
|
||||
// http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html
|
||||
cmd : 'xdms',
|
||||
args : [ 'd', '{filePath}' ]
|
||||
},
|
||||
XDMS2LongDesc : {
|
||||
// http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html
|
||||
cmd : 'xdms',
|
||||
args : [ 'f', '{filePath}' ]
|
||||
},
|
||||
},
|
||||
|
||||
fileTypes : {
|
||||
|
@ -391,7 +563,7 @@ function getDefaultConfig() {
|
|||
},
|
||||
'application/x-rar-compressed' : {
|
||||
desc : 'RAR Archive',
|
||||
sig : '526172211a0700',
|
||||
sig : '526172211a07',
|
||||
offset : 0,
|
||||
archiveHandler : 'Rar',
|
||||
},
|
||||
|
@ -414,18 +586,37 @@ function getDefaultConfig() {
|
|||
offset : 2,
|
||||
archiveHandler : 'Lha',
|
||||
},
|
||||
'application/x-lzx' : {
|
||||
desc : 'LZX Archive',
|
||||
sig : '4c5a5800',
|
||||
offset : 0,
|
||||
archiveHandler : 'Lzx',
|
||||
},
|
||||
'application/x-7z-compressed' : {
|
||||
desc : '7-Zip Archive',
|
||||
sig : '377abcaf271c',
|
||||
offset : 0,
|
||||
archiveHandler : '7Zip',
|
||||
}
|
||||
},
|
||||
|
||||
// :TODO: update archives::formats to fall here
|
||||
// * archive handler -> archiveHandler (consider archive if archiveHandler present)
|
||||
// * sig, offset, ...
|
||||
// * mime-db -> exts lookup
|
||||
// *
|
||||
//
|
||||
// Generics that need further mapping
|
||||
//
|
||||
'application/octet-stream' : [
|
||||
{
|
||||
desc : 'Amiga DISKMASHER',
|
||||
sig : '444d5321', // DMS!
|
||||
ext : '.dms',
|
||||
shortDescUtil : 'XDMS2Desc',
|
||||
longDescUtil : 'XDMS2LongDesc',
|
||||
},
|
||||
{
|
||||
desc : 'SIO2PC Atari Disk Image',
|
||||
sig : '9602', // 16bit sum of "NICKATARI"
|
||||
ext : '.atr',
|
||||
archiveHandler : 'Atr',
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
archives : {
|
||||
|
@ -459,7 +650,7 @@ function getDefaultConfig() {
|
|||
//
|
||||
decompress : {
|
||||
cmd : 'lha',
|
||||
args : [ '-ew={extractPath}', '{archivePath}' ],
|
||||
args : [ '-efw={extractPath}', '{archivePath}' ],
|
||||
},
|
||||
list : {
|
||||
cmd : 'lha',
|
||||
|
@ -468,7 +659,26 @@ function getDefaultConfig() {
|
|||
},
|
||||
extract : {
|
||||
cmd : 'lha',
|
||||
args : [ '-ew={extractPath}', '{archivePath}', '{fileList}' ]
|
||||
args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ]
|
||||
}
|
||||
},
|
||||
|
||||
Lzx : {
|
||||
//
|
||||
// 'unlzx' command can be obtained from:
|
||||
// * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64)
|
||||
// * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html
|
||||
// * Source: http://xavprods.free.fr/lzx/
|
||||
//
|
||||
decompress : {
|
||||
cmd : 'unlzx',
|
||||
// unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first
|
||||
args : [ '-x', '{archivePath}' ],
|
||||
},
|
||||
list : {
|
||||
cmd : 'unlzx',
|
||||
args : [ '-v', '{archivePath}' ],
|
||||
entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -504,7 +714,7 @@ function getDefaultConfig() {
|
|||
list : {
|
||||
cmd : 'unrar',
|
||||
args : [ 'l', '{archivePath}' ],
|
||||
entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$',
|
||||
entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$',
|
||||
},
|
||||
extract : {
|
||||
cmd : 'unrar',
|
||||
|
@ -526,6 +736,23 @@ function getDefaultConfig() {
|
|||
cmd : 'tar',
|
||||
args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ],
|
||||
}
|
||||
},
|
||||
|
||||
Atr : {
|
||||
decompress : {
|
||||
cmd : 'atr',
|
||||
args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}' ]
|
||||
},
|
||||
list : {
|
||||
cmd : 'atr',
|
||||
args : [ '{archivePath}', 'ls', '-la1' ],
|
||||
entryMatch : '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$',
|
||||
},
|
||||
extract : {
|
||||
cmd : 'atr',
|
||||
// note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course.
|
||||
args : [ '{archivePath}', 'x', '-a', '-l', '-o', '{extractPath}', '{fileList}' ]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -540,6 +767,7 @@ function getDefaultConfig() {
|
|||
sort : 1,
|
||||
external : {
|
||||
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
|
||||
// Linux x86_64 binary: https://l33t.codes/outgoing/sexyz
|
||||
sendCmd : 'sexyz',
|
||||
sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ],
|
||||
recvCmd : 'sexyz',
|
||||
|
@ -662,12 +890,24 @@ function getDefaultConfig() {
|
|||
// FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ
|
||||
// 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$'
|
||||
'^.*FILE_ID\.ANS$', '^.*FILE_ID\.DIZ$', // eslint-disable-line no-useless-escape
|
||||
'^.*DESC\.SDI$', // eslint-disable-line no-useless-escape
|
||||
'^.*DESCRIPT\.ION$', // eslint-disable-line no-useless-escape
|
||||
'^.*FILE\.DES$', // eslint-disable-line no-useless-escape
|
||||
'^.*FILE\.SDI$', // eslint-disable-line no-useless-escape
|
||||
'^.*DISK\.ID$' // eslint-disable-line no-useless-escape
|
||||
],
|
||||
|
||||
// common README filename - https://en.wikipedia.org/wiki/README
|
||||
descLong : [
|
||||
'^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$'
|
||||
'^[^/\]*\.NFO$', // eslint-disable-line no-useless-escape
|
||||
'^.*README\.1ST$', // eslint-disable-line no-useless-escape
|
||||
'^.*README\.NOW$', // eslint-disable-line no-useless-escape
|
||||
'^.*README\.TXT$', // eslint-disable-line no-useless-escape
|
||||
'^.*READ\.ME$', // eslint-disable-line no-useless-escape
|
||||
'^.*README$', // eslint-disable-line no-useless-escape
|
||||
'^.*README\.md$', // eslint-disable-line no-useless-escape
|
||||
'^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -723,19 +963,28 @@ function getDefaultConfig() {
|
|||
eventScheduler : {
|
||||
|
||||
events : {
|
||||
dailyMaintenance : {
|
||||
schedule : 'at 11:59pm',
|
||||
action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent',
|
||||
},
|
||||
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)
|
||||
// (path is relative to ENiGMA base dir)
|
||||
//
|
||||
// - @execute:/path/to/something/executable.sh
|
||||
//
|
||||
action : '@method:core/message_area.js:trimMessageAreasScheduledEvent',
|
||||
},
|
||||
|
||||
nntpMaintenance : {
|
||||
schedule : 'every 12 hours', // should generally be < trimMessageAreas interval
|
||||
action : '@method:core/servers/content/nntp.js:performMaintenanceTask',
|
||||
},
|
||||
|
||||
updateFileAreaStats : {
|
||||
schedule : 'every 1 hours',
|
||||
action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent',
|
||||
|
@ -745,18 +994,22 @@ function getDefaultConfig() {
|
|||
schedule : 'every 24 hours',
|
||||
action : '@method:core/web_password_reset.js:performMaintenanceTask',
|
||||
args : [ '24 hours' ] // items older than this will be removed
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
misc : {
|
||||
preAuthIdleLogoutSeconds : 60 * 3, // 2m
|
||||
idleLogoutSeconds : 60 * 6, // 6m
|
||||
//
|
||||
// Enable the following entry in your config.hjson to periodically create/update
|
||||
// DESCRIPT.ION files for your file base
|
||||
//
|
||||
/*
|
||||
updateDescriptIonFiles : {
|
||||
schedule : 'on the last day of the week',
|
||||
action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent',
|
||||
}
|
||||
*/
|
||||
}
|
||||
},
|
||||
|
||||
logging : {
|
||||
level : 'debug',
|
||||
|
||||
rotatingFile : { // set to 'disabled' or false to disable
|
||||
type : 'rotating-file',
|
||||
fileName : 'enigma-bbs.log',
|
||||
|
@ -770,6 +1023,12 @@ function getDefaultConfig() {
|
|||
|
||||
debug : {
|
||||
assertsEnabled : false,
|
||||
},
|
||||
|
||||
statLog : {
|
||||
systemEvents : {
|
||||
loginHistoryMax: -1, // set to -1 for forever
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,85 +1,76 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var Config = require('./config.js').config;
|
||||
var Log = require('./logger.js').log;
|
||||
// deps
|
||||
const paths = require('path');
|
||||
const fs = require('graceful-fs');
|
||||
const hjson = require('hjson');
|
||||
const sane = require('sane');
|
||||
|
||||
var paths = require('path');
|
||||
var fs = require('graceful-fs');
|
||||
var events = require('events');
|
||||
var util = require('util');
|
||||
var assert = require('assert');
|
||||
var hjson = require('hjson');
|
||||
var _ = require('lodash');
|
||||
module.exports = new class ConfigCache
|
||||
{
|
||||
constructor() {
|
||||
this.cache = new Map(); // path->parsed config
|
||||
}
|
||||
|
||||
function ConfigCache() {
|
||||
events.EventEmitter.call(this);
|
||||
getConfigWithOptions(options, cb) {
|
||||
const cached = this.cache.has(options.filePath);
|
||||
|
||||
var self = this;
|
||||
this.cache = {}; // filePath -> HJSON
|
||||
//this.gaze = new Gaze();
|
||||
if(options.forceReCache || !cached) {
|
||||
this.recacheConfigFromFile(options.filePath, (err, config) => {
|
||||
if(!err && !cached) {
|
||||
if(!options.noWatch) {
|
||||
const watcher = sane(
|
||||
paths.dirname(options.filePath),
|
||||
{
|
||||
glob : `**/${paths.basename(options.filePath)}`
|
||||
}
|
||||
);
|
||||
|
||||
this.reCacheConfigFromFile = function(filePath, cb) {
|
||||
fs.readFile(filePath, { encoding : 'utf-8' }, function fileRead(err, data) {
|
||||
try {
|
||||
self.cache[filePath] = hjson.parse(data);
|
||||
cb(null, self.cache[filePath]);
|
||||
} catch(e) {
|
||||
Log.error( { filePath : filePath, error : e.toString() }, 'Failed recaching');
|
||||
cb(e);
|
||||
watcher.on('change', (fileName, fileRoot) => {
|
||||
require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching');
|
||||
|
||||
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => {
|
||||
if(!err) {
|
||||
if(options.callback) {
|
||||
options.callback( { fileName, fileRoot } );
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
this.gaze.on('error', function gazeErr(err) {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
return cb(err, config, true);
|
||||
});
|
||||
} else {
|
||||
return cb(null, this.cache.get(options.filePath), false);
|
||||
}
|
||||
}
|
||||
|
||||
this.gaze.on('changed', function fileChanged(filePath) {
|
||||
assert(filePath in self.cache);
|
||||
getConfig(filePath, cb) {
|
||||
return this.getConfigWithOptions( { filePath }, cb);
|
||||
}
|
||||
|
||||
Log.info( { path : filePath }, 'Configuration file changed; re-caching');
|
||||
|
||||
self.reCacheConfigFromFile(filePath, function reCached(err) {
|
||||
recacheConfigFromFile(path, cb) {
|
||||
fs.readFile(path, { encoding : 'utf-8' }, (err, data) => {
|
||||
if(err) {
|
||||
Log.error( { error : err.message, path : filePath } , 'Failed re-caching configuration');
|
||||
} else {
|
||||
self.emit('recached', filePath);
|
||||
return cb(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
util.inherits(ConfigCache, events.EventEmitter);
|
||||
|
||||
ConfigCache.prototype.getConfigWithOptions = function(options, cb) {
|
||||
assert(_.isString(options.filePath));
|
||||
|
||||
// var self = this;
|
||||
var isCached = (options.filePath in this.cache);
|
||||
|
||||
if(options.forceReCache || !isCached) {
|
||||
this.reCacheConfigFromFile(options.filePath, function fileCached(err, config) {
|
||||
if(!err && !isCached) {
|
||||
//self.gaze.add(options.filePath);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = hjson.parse(data);
|
||||
this.cache.set(path, parsed);
|
||||
} catch(e) {
|
||||
try {
|
||||
require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' );
|
||||
} catch(ignored) {
|
||||
// nothing - we may be failing to parse the config in which we can't log here!
|
||||
}
|
||||
cb(err, config, true);
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
return cb(null, parsed);
|
||||
});
|
||||
} else {
|
||||
cb(null, this.cache[options.filePath], false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ConfigCache.prototype.getConfig = function(filePath, cb) {
|
||||
this.getConfigWithOptions( { filePath : filePath }, cb);
|
||||
};
|
||||
|
||||
ConfigCache.prototype.getModConfig = function(fileName, cb) {
|
||||
this.getConfig(paths.join(Config.paths.mods, fileName), cb);
|
||||
};
|
||||
|
||||
module.exports = exports = new ConfigCache();
|
||||
|
|
|
@ -1,18 +1,67 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
const Config = require('./config.js').config;
|
||||
const configCache = require('./config_cache.js');
|
||||
const paths = require('path');
|
||||
|
||||
const Config = require('./config.js').get;
|
||||
const ConfigCache = require('./config_cache.js');
|
||||
const Events = require('./events.js');
|
||||
|
||||
// deps
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
|
||||
exports.init = init;
|
||||
exports.getConfigPath = getConfigPath;
|
||||
exports.getFullConfig = getFullConfig;
|
||||
|
||||
function getFullConfig(filePath, cb) {
|
||||
function getConfigPath(filePath) {
|
||||
// |filePath| is assumed to be in the config path if it's only a file name
|
||||
if('.' === paths.dirname(filePath)) {
|
||||
filePath = paths.join(Config.paths.config, filePath);
|
||||
filePath = paths.join(Config().paths.config, filePath);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
configCache.getConfig(filePath, function loaded(err, configJson) {
|
||||
cb(err, configJson);
|
||||
function init(cb) {
|
||||
// pre-cache menu.hjson and prompt.hjson + establish events
|
||||
const changed = ( { fileName, fileRoot } ) => {
|
||||
const reCachedPath = paths.join(fileRoot, fileName);
|
||||
if(reCachedPath === getConfigPath(Config().general.menuFile)) {
|
||||
Events.emit(Events.getSystemEvents().MenusChanged);
|
||||
} else if(reCachedPath === getConfigPath(Config().general.promptFile)) {
|
||||
Events.emit(Events.getSystemEvents().PromptsChanged);
|
||||
}
|
||||
};
|
||||
|
||||
const config = Config();
|
||||
async.series(
|
||||
[
|
||||
function menu(callback) {
|
||||
return ConfigCache.getConfigWithOptions(
|
||||
{
|
||||
filePath : getConfigPath(config.general.menuFile),
|
||||
callback : changed,
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
function prompt(callback) {
|
||||
return ConfigCache.getConfigWithOptions(
|
||||
{
|
||||
filePath : getConfigPath(config.general.promptFile),
|
||||
callback : changed,
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getFullConfig(filePath, cb) {
|
||||
ConfigCache.getConfig(getConfigPath(filePath), (err, config) => {
|
||||
return cb(err, config);
|
||||
});
|
||||
}
|
113
core/connect.js
113
core/connect.js
|
@ -4,6 +4,7 @@
|
|||
// ENiGMA½
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Events = require('./events.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -15,7 +16,7 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||
// We want to find the home position. ANSI-BBS and most terminals
|
||||
// utilize 1,1 as home. However, some terminals such as ConnectBot
|
||||
// think of home as 0,0. If this is the case, we need to offset
|
||||
// our positioning to accomodate for such.
|
||||
// our positioning to accommodate for such.
|
||||
//
|
||||
const done = function(err) {
|
||||
client.removeListener('cursor position report', cprListener);
|
||||
|
@ -32,7 +33,7 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||
//
|
||||
if(h > 1 || w > 1) {
|
||||
client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values');
|
||||
return done(new Error('Home position CPR expected to be 0,0, or 1,1'));
|
||||
return done(Errors.UnexpectedState('Home position CPR expected to be 0,0, or 1,1'));
|
||||
}
|
||||
|
||||
if(0 === h & 0 === w) {
|
||||
|
@ -49,12 +50,80 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||
client.once('cursor position report', cprListener);
|
||||
|
||||
const giveUpTimer = setTimeout( () => {
|
||||
return done(new Error('Giving up on home position CPR'));
|
||||
return done(Errors.General('Giving up on home position CPR'));
|
||||
}, 3000); // 3s
|
||||
|
||||
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
|
||||
}
|
||||
|
||||
function ansiAttemptDetectUTF8(client, cb) {
|
||||
//
|
||||
// Trick to attempt and detect UTF-8. While there is a lot more than
|
||||
// just UTF-8 and CP437, many those are the main concerns, when it comes
|
||||
// terminals that for example tell us they are "xterm" but still want CP437.
|
||||
//
|
||||
// Try to detect UTF-8 by discovering the cursor position, writing some
|
||||
// multi-byte UTF-8, and checking the position again. If the term is really
|
||||
// UTF-8, we should get a proper position, otherwise we'll be further out.
|
||||
//
|
||||
// We currently only do this if the term hasn't already been ID'd as a
|
||||
// "*nix" terminal -- that is, xterm, etc.
|
||||
//
|
||||
if(!client.term.isNixTerm()) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
let posStage = 1;
|
||||
let initialPosition;
|
||||
let giveUpTimer;
|
||||
|
||||
const giveUp = () => {
|
||||
client.removeListener('cursor position report', cprListener);
|
||||
clearTimeout(giveUpTimer);
|
||||
return cb(null);
|
||||
};
|
||||
|
||||
const ASCIIPortion = ' Character encoding detection ';
|
||||
|
||||
const cprListener = (pos) => {
|
||||
switch(posStage) {
|
||||
case 1 :
|
||||
posStage = 2;
|
||||
|
||||
initialPosition = pos;
|
||||
clearTimeout(giveUpTimer);
|
||||
|
||||
giveUpTimer = setTimeout( () => {
|
||||
return giveUp();
|
||||
}, 2000);
|
||||
|
||||
client.once('cursor position report', cprListener);
|
||||
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
|
||||
client.term.rawWrite(ansi.queryPos());
|
||||
break;
|
||||
|
||||
case 2 :
|
||||
{
|
||||
clearTimeout(giveUpTimer);
|
||||
const len = pos[1] - initialPosition[1];
|
||||
if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull
|
||||
client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".');
|
||||
client.setTermType('ansi');
|
||||
}
|
||||
}
|
||||
return cb(null);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
giveUpTimer = setTimeout( () => {
|
||||
return giveUp();
|
||||
}, 2000);
|
||||
|
||||
client.once('cursor position report', cprListener);
|
||||
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
|
||||
}
|
||||
|
||||
function ansiQueryTermSizeIfNeeded(client, cb) {
|
||||
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
|
||||
return cb(null);
|
||||
|
@ -78,14 +147,15 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
|
|||
const w = pos[1];
|
||||
|
||||
//
|
||||
// Netrunner for example gives us 1x1 here. Not really useful. Ignore
|
||||
// values that seem obviously bad.
|
||||
// NetRunner for example gives us 1x1 here. Not really useful. Ignore
|
||||
// values that seem obviously bad. Included in the set is the explicit
|
||||
// 999x999 values we asked to move to.
|
||||
//
|
||||
if(h < 10 || w < 10) {
|
||||
if(h < 10 || h === 999 || w < 10 || w === 999) {
|
||||
client.log.warn(
|
||||
{ height : h, width : w },
|
||||
'Ignoring ANSI CPR screen size query response due to very small values');
|
||||
return done(new Error('Term size <= 10 considered invalid'));
|
||||
'Ignoring ANSI CPR screen size query response due to non-sane values');
|
||||
return done(Errors.Invalid('Term size <= 10 considered invalid'));
|
||||
}
|
||||
|
||||
client.term.termHeight = h;
|
||||
|
@ -107,24 +177,30 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
|
|||
|
||||
// give up after 2s
|
||||
const giveUpTimer = setTimeout( () => {
|
||||
return done(new Error('No term size established by CPR within timeout'));
|
||||
return done(Errors.General('No term size established by CPR within timeout'));
|
||||
}, 2000);
|
||||
|
||||
// Start the process: Query for CPR
|
||||
client.term.rawWrite(ansi.queryScreenSize());
|
||||
// Start the process:
|
||||
// 1 - Ask to goto 999,999 -- a very much "bottom right" (generally 80x25 for example
|
||||
// is the real size)
|
||||
// 2 - Query for screen size with bansi.txt style specialized Device Status Report (DSR)
|
||||
// request. We expect a CPR of:
|
||||
// a - Terms that support bansi.txt style: Screen size
|
||||
// b - Terms that do not support bansi.txt style: Since we moved to the bottom right
|
||||
// we should still be able to determine a screen size.
|
||||
//
|
||||
client.term.rawWrite(`${ansi.goto(999, 999)}${ansi.queryScreenSize()}`);
|
||||
}
|
||||
|
||||
function prepareTerminal(term) {
|
||||
term.rawWrite(ansi.normal());
|
||||
//term.rawWrite(ansi.disableVT100LineWrapping());
|
||||
// :TODO: set xterm stuff -- see x84/others
|
||||
term.rawWrite(`${ansi.normal()}${ansi.clearScreen()}`);
|
||||
}
|
||||
|
||||
function displayBanner(term) {
|
||||
// note: intentional formatting:
|
||||
term.pipeWrite(`
|
||||
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
||||
|06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Copyright (c) 2014-2019 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
||||
|00`
|
||||
);
|
||||
|
@ -156,7 +232,7 @@ function connectEntry(client, nextMenu) {
|
|||
// We still don't have something good for term height/width.
|
||||
// Default to DOS size 80x25.
|
||||
//
|
||||
// :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
|
||||
// :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
|
||||
client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!');
|
||||
|
||||
term.termHeight = 25;
|
||||
|
@ -167,6 +243,9 @@ function connectEntry(client, nextMenu) {
|
|||
return callback(null);
|
||||
});
|
||||
},
|
||||
function checkUtf8IfNeeded(callback) {
|
||||
return ansiAttemptDetectUTF8(client, callback);
|
||||
}
|
||||
],
|
||||
() => {
|
||||
prepareTerminal(term);
|
||||
|
@ -177,7 +256,7 @@ function connectEntry(client, nextMenu) {
|
|||
displayBanner(term);
|
||||
|
||||
// fire event
|
||||
Events.emit('codes.l33t.enigma.system.term_detected', { client : client } );
|
||||
Events.emit(Events.getSystemEvents().TermDetected, { client : client } );
|
||||
|
||||
setTimeout( () => {
|
||||
return client.menuStack.goto(nextMenu);
|
||||
|
|
41
core/crc.js
41
core/crc.js
|
@ -1,8 +1,45 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const CRC32_TABLE = new Int32Array(
|
||||
'00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D'.split(' ').map(s => parseInt(s, 16)));
|
||||
const CRC32_TABLE = new Int32Array([
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
|
||||
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
|
||||
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
|
||||
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
|
||||
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
|
||||
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
||||
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
|
||||
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
|
||||
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
|
||||
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
|
||||
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
|
||||
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
|
||||
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
|
||||
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
|
||||
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
|
||||
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
|
||||
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
|
||||
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
|
||||
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
|
||||
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
|
||||
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
|
||||
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
||||
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
|
||||
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
|
||||
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
||||
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||
]);
|
||||
|
||||
exports.CRC32 = class CRC32 {
|
||||
constructor() {
|
||||
|
|
|
@ -18,7 +18,9 @@ const dbs = {};
|
|||
|
||||
exports.getTransactionDatabase = getTransactionDatabase;
|
||||
exports.getModDatabasePath = getModDatabasePath;
|
||||
exports.loadDatabaseForMod = loadDatabaseForMod;
|
||||
exports.getISOTimestampString = getISOTimestampString;
|
||||
exports.sanitizeString = sanitizeString;
|
||||
exports.initializeDatabases = initializeDatabases;
|
||||
|
||||
exports.dbs = dbs;
|
||||
|
@ -37,7 +39,7 @@ function getModDatabasePath(moduleInfo, suffix) {
|
|||
// We expect that moduleInfo defines packageName which will be the base of the modules
|
||||
// filename. An optional suffix may be supplied as well.
|
||||
//
|
||||
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
||||
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||
|
||||
assert(_.isObject(moduleInfo));
|
||||
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
|
||||
|
@ -54,11 +56,47 @@ function getModDatabasePath(moduleInfo, suffix) {
|
|||
return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`);
|
||||
}
|
||||
|
||||
function loadDatabaseForMod(modInfo, cb) {
|
||||
const db = getTransactionDatabase(new sqlite3.Database(
|
||||
getModDatabasePath(modInfo),
|
||||
err => {
|
||||
return cb(err, db);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
function getISOTimestampString(ts) {
|
||||
ts = ts || moment();
|
||||
if(!moment.isMoment(ts)) {
|
||||
if(_.isString(ts)) {
|
||||
ts = ts.replace(/\//g, '-');
|
||||
}
|
||||
ts = moment(ts);
|
||||
}
|
||||
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
}
|
||||
|
||||
function sanitizeString(s) {
|
||||
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex
|
||||
switch (c) {
|
||||
case '\0' : return '\\0';
|
||||
case '\x08' : return '\\b';
|
||||
case '\x09' : return '\\t';
|
||||
case '\x1a' : return '\\z';
|
||||
case '\n' : return '\\n';
|
||||
case '\r' : return '\\r';
|
||||
|
||||
case '"' :
|
||||
case '\'' :
|
||||
return `${c}${c}`;
|
||||
|
||||
case '\\' :
|
||||
case '%' :
|
||||
return `\\${c}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initializeDatabases(cb) {
|
||||
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
|
||||
dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => {
|
||||
|
@ -109,10 +147,11 @@ const DB_INIT_TABLE = {
|
|||
id INTEGER PRIMARY KEY,
|
||||
timestamp DATETIME NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
session_id VARCHAR NOT NULL,
|
||||
log_name VARCHAR NOT NULL,
|
||||
log_value VARCHAR NOT NULL,
|
||||
|
||||
UNIQUE(timestamp, user_id, log_name)
|
||||
UNIQUE(timestamp, user_id, session_id, log_name)
|
||||
);`
|
||||
);
|
||||
|
||||
|
@ -151,10 +190,16 @@ const DB_INIT_TABLE = {
|
|||
);
|
||||
|
||||
dbs.user.run(
|
||||
`CREATE TABLE IF NOT EXISTS user_login_history (
|
||||
`CREATE TABLE IF NOT EXISTS user_achievement (
|
||||
user_id INTEGER NOT NULL,
|
||||
user_name VARCHAR NOT NULL,
|
||||
timestamp DATETIME NOT NULL
|
||||
achievement_tag VARCHAR NOT NULL,
|
||||
timestamp DATETIME NOT NULL,
|
||||
match VARCHAR NOT NULL,
|
||||
title VARCHAR NOT NULL,
|
||||
text VARCHAR NOT NULL,
|
||||
points INTEGER NOT NULL,
|
||||
UNIQUE(user_id, achievement_tag, match),
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);`
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
// deps
|
||||
const fs = require('graceful-fs');
|
||||
const iconv = require('iconv-lite');
|
||||
|
@ -64,7 +66,10 @@ module.exports = class DescriptIonFile {
|
|||
return nextLine(null);
|
||||
},
|
||||
() => {
|
||||
return cb(null, descIonFile);
|
||||
return cb(
|
||||
descIonFile.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized DESCRIPT.ION format'),
|
||||
descIonFile
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
199
core/door.js
199
core/door.js
|
@ -1,154 +1,137 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
|
||||
const stringFormat = require('./string_format.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
const events = require('events');
|
||||
const _ = require('lodash');
|
||||
const pty = require('ptyw.js');
|
||||
// deps
|
||||
const pty = require('node-pty');
|
||||
const decode = require('iconv-lite').decode;
|
||||
const createServer = require('net').createServer;
|
||||
const paths = require('path');
|
||||
|
||||
exports.Door = Door;
|
||||
|
||||
function Door(client, exeInfo) {
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
const self = this;
|
||||
module.exports = class Door {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.exeInfo = exeInfo;
|
||||
this.exeInfo.encoding = this.exeInfo.encoding || 'cp437';
|
||||
this.exeInfo.encoding = this.exeInfo.encoding.toLowerCase();
|
||||
let restored = false;
|
||||
|
||||
//
|
||||
// Members of exeInfo:
|
||||
// cmd
|
||||
// args[]
|
||||
// env{}
|
||||
// cwd
|
||||
// io
|
||||
// encoding
|
||||
// dropFile
|
||||
// node
|
||||
// inhSocket
|
||||
//
|
||||
|
||||
this.doorDataHandler = function(data) {
|
||||
if(self.client.term.outputEncoding === self.exeInfo.encoding) {
|
||||
self.client.term.rawWrite(data);
|
||||
} else {
|
||||
self.client.term.write(decode(data, self.exeInfo.encoding));
|
||||
this.restored = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.restoreIo = function(piped) {
|
||||
if(!restored && self.client.term.output) {
|
||||
self.client.term.output.unpipe(piped);
|
||||
self.client.term.output.resume();
|
||||
restored = true;
|
||||
prepare(ioType, cb) {
|
||||
this.io = ioType;
|
||||
|
||||
// we currently only have to do any real setup for 'socket'
|
||||
if('socket' !== ioType) {
|
||||
return cb(null);
|
||||
}
|
||||
};
|
||||
|
||||
this.prepareSocketIoServer = function(cb) {
|
||||
if('socket' === self.exeInfo.io) {
|
||||
const sockServer = createServer(conn => {
|
||||
|
||||
sockServer.getConnections( (err, count) => {
|
||||
|
||||
// We expect only one connection from our DOOR/emulator/etc.
|
||||
if(!err && count <= 1) {
|
||||
self.client.term.output.pipe(conn);
|
||||
|
||||
conn.on('data', self.doorDataHandler);
|
||||
|
||||
this.sockServer = createServer(conn => {
|
||||
conn.once('end', () => {
|
||||
return self.restoreIo(conn);
|
||||
return this.restoreIo(conn);
|
||||
});
|
||||
|
||||
conn.once('error', err => {
|
||||
self.client.log.info( { error : err.toString() }, 'Door socket server connection');
|
||||
return self.restoreIo(conn);
|
||||
this.client.log.info( { error : err.message }, 'Door socket server connection');
|
||||
return this.restoreIo(conn);
|
||||
});
|
||||
|
||||
this.sockServer.getConnections( (err, count) => {
|
||||
// We expect only one connection from our DOOR/emulator/etc.
|
||||
if(!err && count <= 1) {
|
||||
this.client.term.output.pipe(conn);
|
||||
conn.on('data', this.doorDataHandler.bind(this));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sockServer.listen(0, () => {
|
||||
return cb(null, sockServer);
|
||||
});
|
||||
} else {
|
||||
this.sockServer.listen(0, () => {
|
||||
return cb(null);
|
||||
}
|
||||
};
|
||||
|
||||
this.doorExited = function() {
|
||||
self.emit('finished');
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(Door, events.EventEmitter);
|
||||
|
||||
Door.prototype.run = function() {
|
||||
const self = this;
|
||||
|
||||
this.prepareSocketIoServer( (err, sockServer) => {
|
||||
if(err) {
|
||||
this.client.log.warn( { error : err.toString() }, 'Failed executing door');
|
||||
return self.doorExited();
|
||||
}
|
||||
|
||||
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
|
||||
// :TODO: Use .map() here
|
||||
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
|
||||
|
||||
for(let i = 0; i < args.length; ++i) {
|
||||
args[i] = stringFormat(self.exeInfo.args[i], {
|
||||
dropFile : self.exeInfo.dropFile,
|
||||
node : self.exeInfo.node.toString(),
|
||||
srvPort : sockServer ? sockServer.address().port.toString() : '-1',
|
||||
userId : self.client.user.userId.toString(),
|
||||
username : self.client.user.username,
|
||||
});
|
||||
}
|
||||
|
||||
const door = pty.spawn(self.exeInfo.cmd, args, {
|
||||
cols : self.client.term.termWidth,
|
||||
rows : self.client.term.termHeight,
|
||||
// :TODO: cwd
|
||||
env : self.exeInfo.env,
|
||||
run(exeInfo, cb) {
|
||||
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
||||
|
||||
if('socket' === this.io && !this.sockServer) {
|
||||
return cb(Errors.UnexpectedState('Socket server is not running'));
|
||||
}
|
||||
|
||||
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
|
||||
|
||||
const formatObj = {
|
||||
dropFile : exeInfo.dropFile,
|
||||
dropFilePath : exeInfo.dropFilePath,
|
||||
node : exeInfo.node.toString(),
|
||||
srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1',
|
||||
userId : this.client.user.userId.toString(),
|
||||
userName : this.client.user.getSanitizedName(),
|
||||
userNameRaw : this.client.user.username,
|
||||
cwd : cwd,
|
||||
};
|
||||
|
||||
const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) );
|
||||
|
||||
this.client.log.debug(
|
||||
{ cmd : exeInfo.cmd, args, io : this.io },
|
||||
'Executing door'
|
||||
);
|
||||
|
||||
let door;
|
||||
try {
|
||||
door = pty.spawn(exeInfo.cmd, args, {
|
||||
cols : this.client.term.termWidth,
|
||||
rows : this.client.term.termHeight,
|
||||
cwd : cwd,
|
||||
env : exeInfo.env,
|
||||
encoding : null, // we want to handle all encoding ourself
|
||||
});
|
||||
} catch(e) {
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
if('stdio' === self.exeInfo.io) {
|
||||
self.client.log.debug('Using stdio for door I/O');
|
||||
if('stdio' === this.io) {
|
||||
this.client.log.debug('Using stdio for door I/O');
|
||||
|
||||
self.client.term.output.pipe(door);
|
||||
this.client.term.output.pipe(door);
|
||||
|
||||
door.on('data', self.doorDataHandler);
|
||||
door.on('data', this.doorDataHandler.bind(this));
|
||||
|
||||
door.once('close', () => {
|
||||
return self.restoreIo(door);
|
||||
return this.restoreIo(door);
|
||||
});
|
||||
} else if('socket' === self.exeInfo.io) {
|
||||
self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O');
|
||||
} else if('socket' === this.io) {
|
||||
this.client.log.debug(
|
||||
{ srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket },
|
||||
'Using temporary socket server for door I/O'
|
||||
);
|
||||
}
|
||||
|
||||
door.once('exit', exitCode => {
|
||||
self.client.log.info( { exitCode : exitCode }, 'Door exited');
|
||||
this.client.log.info( { exitCode : exitCode }, 'Door exited');
|
||||
|
||||
if(sockServer) {
|
||||
sockServer.close();
|
||||
if(this.sockServer) {
|
||||
this.sockServer.close();
|
||||
}
|
||||
|
||||
// we may not get a close
|
||||
if('stdio' === self.exeInfo.io) {
|
||||
self.restoreIo(door);
|
||||
if('stdio' === this.io) {
|
||||
this.restoreIo(door);
|
||||
}
|
||||
|
||||
door.removeAllListeners();
|
||||
|
||||
return self.doorExited();
|
||||
});
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
doorDataHandler(data) {
|
||||
this.client.term.write(decode(data, this.encoding));
|
||||
}
|
||||
|
||||
restoreIo(piped) {
|
||||
if(!this.restored && this.client.term.output) {
|
||||
this.client.term.output.unpipe(piped);
|
||||
this.client.term.output.resume();
|
||||
this.restored = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
'use strict';
|
||||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||
const { MenuModule } = require('./menu_module.js');
|
||||
const { resetScreen } = require('./ansi_term.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const {
|
||||
trackDoorRunBegin,
|
||||
trackDoorRunEnd
|
||||
} = require('./door_util.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const SSHClient = require('ssh2').Client;
|
||||
|
||||
exports.moduleInfo = {
|
||||
|
@ -34,16 +38,17 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(!_.isString(self.config.username)) {
|
||||
return callback(new Error('Config requires "username"!'));
|
||||
}
|
||||
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);
|
||||
return self.validateConfigFields(
|
||||
{
|
||||
host : 'string',
|
||||
username : 'string',
|
||||
password : 'string',
|
||||
bbsTag : 'string',
|
||||
sshPort : 'number',
|
||||
rloginPort : 'number',
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
function establishSecureConnection(callback) {
|
||||
self.client.term.write(resetScreen());
|
||||
|
@ -53,10 +58,16 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||
|
||||
let pipeRestored = false;
|
||||
let pipedStream;
|
||||
let doorTracking;
|
||||
|
||||
const restorePipe = function() {
|
||||
if(pipedStream && !pipeRestored && !clientTerminated) {
|
||||
self.client.term.output.unpipe(pipedStream);
|
||||
self.client.term.output.resume();
|
||||
|
||||
if(doorTracking) {
|
||||
trackDoorRunEnd(doorTracking);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -71,9 +82,11 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||
// establish tunnel for rlogin
|
||||
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
|
||||
if(err) {
|
||||
return callback(new Error('Failed to establish tunnel'));
|
||||
return callback(Errors.General('Failed to establish tunnel'));
|
||||
}
|
||||
|
||||
doorTracking = trackDoorRunBegin(self.client);
|
||||
|
||||
//
|
||||
// Send rlogin
|
||||
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
|
||||
|
@ -99,6 +112,7 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||
|
||||
sshClient.on('error', err => {
|
||||
self.client.log.info(`DoorParty SSH client error: ${err.message}`);
|
||||
trackDoorRunEnd(doorTracking);
|
||||
});
|
||||
|
||||
sshClient.on('close', () => {
|
||||
|
@ -121,7 +135,7 @@ exports.getModule = class DoorPartyModule extends MenuModule {
|
|||
self.client.log.warn( { error : err.message }, 'DoorParty error');
|
||||
}
|
||||
|
||||
// if the client is stil here, go to previous
|
||||
// if the client is still here, go to previous
|
||||
if(!clientTerminated) {
|
||||
self.prevMenu();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const UserProps = require('./user_property.js');
|
||||
const Events = require('./events.js');
|
||||
const StatLog = require('./stat_log.js');
|
||||
|
||||
const moment = require('moment');
|
||||
|
||||
exports.trackDoorRunBegin = trackDoorRunBegin;
|
||||
exports.trackDoorRunEnd = trackDoorRunEnd;
|
||||
|
||||
function trackDoorRunBegin(client, doorTag) {
|
||||
const startTime = moment();
|
||||
return { startTime, client, doorTag };
|
||||
}
|
||||
|
||||
function trackDoorRunEnd(trackInfo) {
|
||||
const { startTime, client, doorTag } = trackInfo;
|
||||
|
||||
const diff = moment.duration(moment().diff(startTime));
|
||||
if(diff.asSeconds() >= 45) {
|
||||
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1);
|
||||
}
|
||||
|
||||
const runTimeMinutes = Math.floor(diff.asMinutes());
|
||||
if(runTimeMinutes > 0) {
|
||||
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes);
|
||||
|
||||
const eventInfo = {
|
||||
runTimeMinutes,
|
||||
user : client.user,
|
||||
doorTag : doorTag || 'unknown',
|
||||
};
|
||||
|
||||
Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo);
|
||||
}
|
||||
}
|
|
@ -2,14 +2,18 @@
|
|||
'use strict';
|
||||
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
|
||||
// deps
|
||||
const { partition } = require('lodash');
|
||||
|
||||
module.exports = class DownloadQueue {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
if(!Array.isArray(this.client.user.downloadQueue)) {
|
||||
if(this.client.user.properties.dl_queue) {
|
||||
this.loadFromProperty(this.client.user.properties.dl_queue);
|
||||
if(this.client.user.properties[UserProps.DownloadQueue]) {
|
||||
this.loadFromProperty(this.client.user.properties[UserProps.DownloadQueue]);
|
||||
} else {
|
||||
this.client.user.downloadQueue = [];
|
||||
}
|
||||
|
@ -24,21 +28,22 @@ module.exports = class DownloadQueue {
|
|||
this.client.user.downloadQueue = [];
|
||||
}
|
||||
|
||||
toggle(fileEntry) {
|
||||
toggle(fileEntry, systemFile=false) {
|
||||
if(this.isQueued(fileEntry)) {
|
||||
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
||||
} else {
|
||||
this.add(fileEntry);
|
||||
this.add(fileEntry, systemFile);
|
||||
}
|
||||
}
|
||||
|
||||
add(fileEntry) {
|
||||
add(fileEntry, systemFile=false) {
|
||||
this.client.user.downloadQueue.push({
|
||||
fileId : fileEntry.fileId,
|
||||
areaTag : fileEntry.areaTag,
|
||||
fileName : fileEntry.fileName,
|
||||
path : fileEntry.filePath,
|
||||
byteSize : fileEntry.meta.byte_size || 0,
|
||||
systemFile : systemFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -47,7 +52,9 @@ module.exports = class DownloadQueue {
|
|||
fileIds = [ fileIds ];
|
||||
}
|
||||
|
||||
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => ( -1 === fileIds.indexOf(e.fileId) ) );
|
||||
const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) ));
|
||||
this.client.user.downloadQueue = remain;
|
||||
return removed;
|
||||
}
|
||||
|
||||
isQueued(entryOrId) {
|
||||
|
|
210
core/dropfile.js
210
core/dropfile.js
|
@ -1,45 +1,46 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var Config = require('./config.js').config;
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').get;
|
||||
const StatLog = require('./stat_log.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const SysProps = require('./system_property.js');
|
||||
|
||||
var fs = require('graceful-fs');
|
||||
var paths = require('path');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var iconv = require('iconv-lite');
|
||||
|
||||
exports.DropFile = DropFile;
|
||||
// deps
|
||||
const fs = require('graceful-fs');
|
||||
const paths = require('path');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const iconv = require('iconv-lite');
|
||||
const { mkdirs } = require('fs-extra');
|
||||
|
||||
//
|
||||
// Resources
|
||||
// * https://github.com/NuSkooler/ansi-bbs/tree/master/docs/dropfile_formats
|
||||
// * http://goldfndr.home.mindspring.com/dropfile/
|
||||
// * https://en.wikipedia.org/wiki/Talk%3ADropfile
|
||||
// * http://thoughtproject.com/libraries/bbs/Sysop/Doors/DropFiles/index.htm
|
||||
// * http://thebbs.org/bbsfaq/ch06.02.htm
|
||||
|
||||
// http://lord.lordlegacy.com/dosemu/
|
||||
|
||||
function DropFile(client, fileType) {
|
||||
|
||||
var self = this;
|
||||
// * http://lord.lordlegacy.com/dosemu/
|
||||
//
|
||||
module.exports = class DropFile {
|
||||
constructor(client, { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {} ) {
|
||||
this.client = client;
|
||||
this.fileType = (fileType || 'DORINFO').toUpperCase();
|
||||
|
||||
Object.defineProperty(this, 'fullPath', {
|
||||
get : function() {
|
||||
return paths.join(Config.paths.dropFiles, ('node' + self.client.node), self.fileName);
|
||||
this.fileType = fileType.toUpperCase();
|
||||
this.baseDir = baseDir;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'fileName', {
|
||||
get : function() {
|
||||
get fullPath() {
|
||||
return paths.join(this.baseDir, ('node' + this.client.node), this.fileName);
|
||||
}
|
||||
|
||||
get fileName() {
|
||||
return {
|
||||
DOOR : 'DOOR.SYS', // GAP BBS, many others
|
||||
DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ...
|
||||
DOOR32 : 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec)
|
||||
CALLINFO : 'CALLINFO.BBS', // Citadel?
|
||||
DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
|
||||
DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
|
||||
CHAIN : 'CHAIN.TXT', // WWIV
|
||||
CURRUSER : 'CURRUSER.BBS', // RyBBS
|
||||
SFDOORS : 'SFDOORS.DAT', // Spitfire
|
||||
|
@ -47,26 +48,31 @@ function DropFile(client, fileType) {
|
|||
TRIBBS : 'TRIBBS.SYS', // TriBBS
|
||||
USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+
|
||||
JUMPER : 'JUMPER.DAT', // 2AM BBS
|
||||
SXDOOR : // System/X, dESiRE
|
||||
'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'),
|
||||
SXDOOR : 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE
|
||||
INFO : 'INFO.BBS', // Phoenix BBS
|
||||
}[self.fileType];
|
||||
}[this.fileType];
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'dropFileContents', {
|
||||
get : function() {
|
||||
isSupported() {
|
||||
return this.getHandler() ? true : false;
|
||||
}
|
||||
|
||||
getHandler() {
|
||||
return {
|
||||
DOOR : self.getDoorSysBuffer(),
|
||||
DOOR32 : self.getDoor32Buffer(),
|
||||
DORINFO : self.getDoorInfoDefBuffer(),
|
||||
}[self.fileType];
|
||||
DOOR : this.getDoorSysBuffer,
|
||||
DOOR32 : this.getDoor32Buffer,
|
||||
DORINFO : this.getDoorInfoDefBuffer,
|
||||
}[this.fileType];
|
||||
}
|
||||
});
|
||||
|
||||
this.getDoorInfoFileName = function() {
|
||||
var x;
|
||||
var node = self.client.node;
|
||||
getContents() {
|
||||
const handler = this.getHandler().bind(this);
|
||||
return handler();
|
||||
}
|
||||
|
||||
getDoorInfoFileName() {
|
||||
let x;
|
||||
const node = this.client.node;
|
||||
if(10 === node) {
|
||||
x = 0;
|
||||
} else if(node < 10) {
|
||||
|
@ -75,54 +81,60 @@ function DropFile(client, fileType) {
|
|||
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
|
||||
}
|
||||
return 'DORINFO' + x + '.DEF';
|
||||
};
|
||||
}
|
||||
|
||||
this.getDoorSysBuffer = function() {
|
||||
var up = self.client.user.properties;
|
||||
var now = moment();
|
||||
var secLevel = self.client.user.getLegacySecurityLevel().toString();
|
||||
getDoorSysBuffer() {
|
||||
const prop = this.client.user.properties;
|
||||
const now = moment();
|
||||
const secLevel = this.client.user.getLegacySecurityLevel().toString();
|
||||
const fullName = this.client.user.getSanitizedName('real');
|
||||
const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY');
|
||||
|
||||
const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024);
|
||||
const downK = Math.floor((parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024);
|
||||
|
||||
const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format('hh:mm');
|
||||
|
||||
// :TODO: fix time remaining
|
||||
// :TODO: fix default protocol -- user prop: transfer_protocol
|
||||
|
||||
return iconv.encode( [
|
||||
'COM1:', // "Comm Port - COM0: = LOCAL MODE"
|
||||
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
|
||||
'8', // "Parity - 7 or 8"
|
||||
self.client.node.toString(), // "Node Number - 1 to 99"
|
||||
this.client.node.toString(), // "Node Number - 1 to 99"
|
||||
'57600', // "DTE Rate. Actual BPS rate to use. (kg)"
|
||||
'Y', // "Screen Display - Y=On N=Off (Default to Y)"
|
||||
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
|
||||
'Y', // "Page Bell - Y=On N=Off (Default to Y)"
|
||||
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
|
||||
up.real_name || self.client.user.username, // "User Full Name"
|
||||
up.location || 'Anywhere', // "Calling From"
|
||||
fullName, // "User Full Name"
|
||||
prop[UserProps.Location]|| 'Anywhere', // "Calling From"
|
||||
'123-456-7890', // "Home Phone"
|
||||
'123-456-7890', // "Work/Data Phone"
|
||||
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
|
||||
secLevel, // "Security Level"
|
||||
up.login_count.toString(), // "Total Times On"
|
||||
prop[UserProps.LoginCount].toString(), // "Total Times On"
|
||||
now.format('MM/DD/YY'), // "Last Date Called"
|
||||
'15360', // "Seconds Remaining THIS call (for those that particular)"
|
||||
'256', // "Minutes Remaining THIS call"
|
||||
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
|
||||
self.client.term.termHeight.toString(), // "Page Length"
|
||||
this.client.term.termHeight.toString(), // "Page Length"
|
||||
'N', // "User Mode - Y = Expert, N = Novice"
|
||||
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
|
||||
'1', // "Conference Exited To DOOR From (G)"
|
||||
'01/01/99', // "User Expiration Date (mm/dd/yy)"
|
||||
self.client.user.userId.toString(), // "User File's Record Number"
|
||||
this.client.user.userId.toString(), // "User File's Record Number"
|
||||
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
|
||||
// :TODO: fix up, down, etc. form user properties
|
||||
'0', // "Total Uploads"
|
||||
'0', // "Total Downloads"
|
||||
'0', // "Daily Download "K" Total"
|
||||
'999999', // "Daily Download Max. "K" Limit"
|
||||
moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate"
|
||||
bd, // "Caller's Birthdate"
|
||||
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
|
||||
'X:\\GEN\\', // "Path to the GEN directory"
|
||||
StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)"
|
||||
self.client.user.username, // "Alias name"
|
||||
StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)"
|
||||
this.client.user.getSanitizedName(), // "Alias name"
|
||||
'00:05', // "Event time (hh:mm)" (note: wat?)
|
||||
'Y', // "If its an error correcting connection (Y/N)"
|
||||
'Y', // "ANSI supported & caller using NG mode (Y/N)"
|
||||
|
@ -131,45 +143,49 @@ function DropFile(client, fileType) {
|
|||
// :TODO: fix minutes here also:
|
||||
'256', // "Time Credits In Minutes (positive/negative)"
|
||||
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
|
||||
// :TODO: fix last vs now times:
|
||||
now.format('hh:mm'), // "Time of This Call"
|
||||
now.format('hh:mm'), // "Time of Last Call (hh:mm)"
|
||||
timeOfCall, // "Time of This Call"
|
||||
timeOfCall, // "Time of Last Call (hh:mm)"
|
||||
'9999', // "Maximum daily files available"
|
||||
// :TODO: fix these stats:
|
||||
'0', // "Files d/led so far today"
|
||||
'0', // "Total "K" Bytes Uploaded"
|
||||
'0', // "Total "K" Bytes Downloaded"
|
||||
up.user_comment || 'None', // "User Comment"
|
||||
upK.toString(), // "Total "K" Bytes Uploaded"
|
||||
downK.toString(), // "Total "K" Bytes Downloaded"
|
||||
prop[UserProps.UserComment] || 'None', // "User Comment"
|
||||
'0', // "Total Doors Opened"
|
||||
'0', // "Total Messages Left"
|
||||
|
||||
].join('\r\n') + '\r\n', 'cp437');
|
||||
};
|
||||
}
|
||||
|
||||
this.getDoor32Buffer = function() {
|
||||
getDoor32Buffer() {
|
||||
//
|
||||
// Resources:
|
||||
// * http://wiki.bbses.info/index.php/DOOR32.SYS
|
||||
// * https://github.com/NuSkooler/ansi-bbs/blob/master/docs/dropfile_formats/door32_sys.txt
|
||||
//
|
||||
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
|
||||
return iconv.encode([
|
||||
'2', // :TODO: This needs to be configurable!
|
||||
// :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely
|
||||
'-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows!
|
||||
'57600',
|
||||
Config.general.boardName,
|
||||
self.client.user.userId.toString(),
|
||||
self.client.user.properties.real_name || self.client.user.username,
|
||||
self.client.user.username,
|
||||
self.client.user.getLegacySecurityLevel().toString(),
|
||||
'546', // :TODO: Minutes left!
|
||||
'1', // ANSI
|
||||
self.client.node.toString(),
|
||||
].join('\r\n') + '\r\n', 'cp437');
|
||||
|
||||
const Door32CommTypes = {
|
||||
Local : 0,
|
||||
Serial : 1,
|
||||
Telnet : 2,
|
||||
};
|
||||
|
||||
this.getDoorInfoDefBuffer = function() {
|
||||
const commType = Door32CommTypes.Telnet;
|
||||
|
||||
return iconv.encode([
|
||||
commType.toString(),
|
||||
'-1',
|
||||
'115200',
|
||||
Config().general.boardName,
|
||||
this.client.user.userId.toString(),
|
||||
this.client.user.getSanitizedName('real'),
|
||||
this.client.user.getSanitizedName(),
|
||||
this.client.user.getLegacySecurityLevel().toString(),
|
||||
'546', // :TODO: Minutes left!
|
||||
'1', // ANSI
|
||||
this.client.node.toString(),
|
||||
].join('\r\n') + '\r\n', 'cp437');
|
||||
}
|
||||
|
||||
getDoorInfoDefBuffer() {
|
||||
// :TODO: fix time remaining
|
||||
|
||||
//
|
||||
|
@ -178,34 +194,34 @@ function DropFile(client, fileType) {
|
|||
//
|
||||
// Note that usernames are just used for first/last names here
|
||||
//
|
||||
var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
|
||||
var un = /[^\s]*/.exec(self.client.user.username)[0];
|
||||
var secLevel = self.client.user.getLegacySecurityLevel().toString();
|
||||
const opUserName = /[^\s]*/.exec(StatLog.getSystemStat(SysProps.SysOpUsername))[0];
|
||||
const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0];
|
||||
const secLevel = this.client.user.getLegacySecurityLevel().toString();
|
||||
const location = this.client.user.properties[UserProps.Location];
|
||||
|
||||
return iconv.encode( [
|
||||
Config.general.boardName, // "The name of the system."
|
||||
opUn, // "The sysop's name up to the first space."
|
||||
opUn, // "The sysop's name following the first space."
|
||||
Config().general.boardName, // "The name of the system."
|
||||
opUserName, // "The sysop's name up to the first space."
|
||||
opUserName, // "The sysop's name following the first space."
|
||||
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
|
||||
'57600', // "The current port (DTE) rate."
|
||||
'0', // "The number "0""
|
||||
un, // "The current user's name, up to the first space."
|
||||
un, // "The current user's name, following the first space."
|
||||
self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown."
|
||||
userName, // "The current user's name, up to the first space."
|
||||
userName, // "The current user's name, following the first space."
|
||||
location || '', // "Where the user lives, or a blank line if unknown."
|
||||
'1', // "The number "0" if TTY, or "1" if ANSI."
|
||||
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
|
||||
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
|
||||
'-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
|
||||
].join('\r\n') + '\r\n', 'cp437');
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DropFile.fileTypes = [ 'DORINFO' ];
|
||||
|
||||
DropFile.prototype.createFile = function(cb) {
|
||||
fs.writeFile(this.fullPath, this.dropFileContents, function written(err) {
|
||||
cb(err);
|
||||
createFile(cb) {
|
||||
mkdirs(paths.dirname(this.fullPath), err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
return fs.writeFile(this.fullPath, this.getContents(), cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ function EditTextView(options) {
|
|||
|
||||
TextView.call(this, options);
|
||||
|
||||
this.initDefaultWidth();
|
||||
|
||||
this.cursorPos = { row : 0, col : 0 };
|
||||
|
||||
this.clientBackspace = function() {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
|
@ -13,13 +13,14 @@ const nodeMailer = require('nodemailer');
|
|||
exports.sendMail = sendMail;
|
||||
|
||||
function sendMail(message, cb) {
|
||||
if(!_.has(Config, 'email.transport')) {
|
||||
const config = Config();
|
||||
if(!_.has(config, 'email.transport')) {
|
||||
return cb(Errors.MissingConfig('Email "email::transport" configuration missing'));
|
||||
}
|
||||
|
||||
message.from = message.from || Config.email.defaultFrom;
|
||||
message.from = message.from || config.email.defaultFrom;
|
||||
|
||||
const transportOptions = Object.assign( {}, Config.email.transport, {
|
||||
const transportOptions = Object.assign( {}, config.email.transport, {
|
||||
logger : Log,
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ class EnigError extends Error {
|
|||
this.reason = reason;
|
||||
this.reasonCode = reasonCode;
|
||||
|
||||
if(this.reason) {
|
||||
this.message += `: ${this.reason}`;
|
||||
}
|
||||
|
||||
if(typeof Error.captureStackTrace === 'function') {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
} else {
|
||||
|
@ -30,7 +34,9 @@ exports.Errors = {
|
|||
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
|
||||
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
|
||||
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode),
|
||||
MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode),
|
||||
MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode),
|
||||
MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode),
|
||||
BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode),
|
||||
};
|
||||
|
||||
exports.ErrorReasons = {
|
||||
|
@ -39,4 +45,10 @@ exports.ErrorReasons = {
|
|||
NoPreviousMenu : 'NOPREV',
|
||||
NoConditionMatch : 'NOCONDMATCH',
|
||||
NotEnabled : 'NOTENABLED',
|
||||
AlreadyLoggedIn : 'ALREADYLOGGEDIN',
|
||||
TooMany : 'TOOMANY',
|
||||
Disabled : 'DISABLED',
|
||||
Inactive : 'INACTIVE',
|
||||
Locked : 'LOCKED',
|
||||
NotAllowed : 'NOTALLOWED',
|
||||
};
|
|
@ -2,14 +2,14 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const assert = require('assert');
|
||||
|
||||
module.exports = function(condition, message) {
|
||||
if(Config.debug.assertsEnabled) {
|
||||
if(Config().debug.assertsEnabled) {
|
||||
assert.apply(this, arguments);
|
||||
} else if(!(condition)) {
|
||||
const stack = new Error().stack;
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const stringFormat = require('./string_format.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const net = require('net');
|
||||
|
||||
/*
|
||||
Expected configuration block example:
|
||||
|
||||
config: {
|
||||
host: 192.168.1.171
|
||||
port: 5001
|
||||
bbsTag: SOME_TAG
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
exports.getModule = ErcClientModule;
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'ENiGMA Relay Chat Client',
|
||||
desc : 'Chat with other ENiGMA BBSes',
|
||||
author : 'Andrew Pamment',
|
||||
};
|
||||
|
||||
var MciViewIds = {
|
||||
ChatDisplay : 1,
|
||||
InputArea : 3,
|
||||
};
|
||||
|
||||
// :TODO: needs converted to ES6 MenuModule subclass
|
||||
function ErcClientModule(options) {
|
||||
MenuModule.prototype.ctorShim.call(this, options);
|
||||
|
||||
const self = this;
|
||||
this.config = options.menuConfig.config;
|
||||
|
||||
this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}';
|
||||
this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}';
|
||||
|
||||
this.finishedLoading = function() {
|
||||
async.waterfall(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(_.isString(self.config.host) &&
|
||||
_.isNumber(self.config.port) &&
|
||||
_.isString(self.config.bbsTag))
|
||||
{
|
||||
return callback(null);
|
||||
} else {
|
||||
return callback(new Error('Configuration is missing required option(s)'));
|
||||
}
|
||||
},
|
||||
function connectToServer(callback) {
|
||||
const connectOpts = {
|
||||
port : self.config.port,
|
||||
host : self.config.host,
|
||||
};
|
||||
|
||||
const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
|
||||
|
||||
chatMessageView.setText('Connecting to server...');
|
||||
chatMessageView.redraw();
|
||||
|
||||
self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
|
||||
|
||||
// :TODO: Track actual client->enig connection for optional prevMenu @ final CB
|
||||
self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host);
|
||||
|
||||
self.chatConnection.on('data', data => {
|
||||
data = data.toString();
|
||||
|
||||
if(data.startsWith('ERCHANDSHAKE')) {
|
||||
self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`);
|
||||
} else if(data.startsWith('{')) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch(e) {
|
||||
return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server');
|
||||
}
|
||||
|
||||
let text;
|
||||
try {
|
||||
if(data.userName) {
|
||||
// user message
|
||||
text = stringFormat(self.chatEntryFormat, data);
|
||||
} else {
|
||||
// system message
|
||||
text = stringFormat(self.systemEntryFormat, data);
|
||||
}
|
||||
} catch(e) {
|
||||
return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error');
|
||||
}
|
||||
|
||||
chatMessageView.addText(text);
|
||||
|
||||
if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height?
|
||||
chatMessageView.deleteLine(0);
|
||||
chatMessageView.scrollDown();
|
||||
}
|
||||
|
||||
chatMessageView.redraw();
|
||||
self.viewControllers.menu.switchFocus(MciViewIds.InputArea);
|
||||
}
|
||||
});
|
||||
|
||||
self.chatConnection.once('end', () => {
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
self.chatConnection.once('error', err => {
|
||||
self.client.log.info(`ERC connection error: ${err.message}`);
|
||||
return callback(new Error('Failed connecting to ERC server!'));
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
self.client.log.warn( { error : err.message }, 'ERC error');
|
||||
}
|
||||
|
||||
self.prevMenu();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.scrollHandler = function(keyName) {
|
||||
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
|
||||
const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay);
|
||||
|
||||
if('up arrow' === keyName) {
|
||||
chatDisplayView.scrollUp();
|
||||
} else {
|
||||
chatDisplayView.scrollDown();
|
||||
}
|
||||
|
||||
chatDisplayView.redraw();
|
||||
inputAreaView.setFocus(true);
|
||||
};
|
||||
|
||||
|
||||
this.menuMethods = {
|
||||
inputAreaSubmit : function(formData, extraArgs, cb) {
|
||||
const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea);
|
||||
const inputData = inputAreaView.getData();
|
||||
|
||||
if('/quit' === inputData.toLowerCase()) {
|
||||
self.chatConnection.end();
|
||||
} else {
|
||||
try {
|
||||
self.chatConnection.write(`${inputData}\r\n`);
|
||||
} catch(e) {
|
||||
self.client.log.warn( { error : e.message }, 'ERC error');
|
||||
}
|
||||
inputAreaView.clearText();
|
||||
}
|
||||
return cb(null);
|
||||
},
|
||||
scrollUp : function(formData, extraArgs, cb) {
|
||||
self.scrollHandler(formData.key.name);
|
||||
return cb(null);
|
||||
},
|
||||
scrollDown : function(formData, extraArgs, cb) {
|
||||
self.scrollHandler(formData.key.name);
|
||||
return cb(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('util').inherits(ErcClientModule, MenuModule);
|
||||
|
||||
ErcClientModule.prototype.mciReady = function(mciData, cb) {
|
||||
this.standardMCIReadyHandler(mciData, cb);
|
||||
};
|
|
@ -3,13 +3,14 @@
|
|||
|
||||
// ENiGMA½
|
||||
const PluginModule = require('./plugin_module.js').PluginModule;
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const Log = require('./logger.js').log;
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
const _ = require('lodash');
|
||||
const later = require('later');
|
||||
const path = require('path');
|
||||
const pty = require('ptyw.js');
|
||||
const pty = require('node-pty');
|
||||
const sane = require('sane');
|
||||
const moment = require('moment');
|
||||
const paths = require('path');
|
||||
|
@ -24,8 +25,8 @@ exports.moduleInfo = {
|
|||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:)([^\0]+)?$/;
|
||||
const ACTION_REGEXP = /\@(method|execute)\:([^\0]+)?$/;
|
||||
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
|
||||
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
|
||||
|
||||
class ScheduledEvent {
|
||||
constructor(events, name) {
|
||||
|
@ -116,7 +117,7 @@ class ScheduledEvent {
|
|||
methodModule[this.action.what](this.action.args, err => {
|
||||
if(err) {
|
||||
Log.debug(
|
||||
{ error : err.toString(), eventName : this.name, action : this.action },
|
||||
{ error : err.message, eventName : this.name, action : this.action },
|
||||
'Error performing scheduled event action');
|
||||
}
|
||||
|
||||
|
@ -124,7 +125,7 @@ class ScheduledEvent {
|
|||
});
|
||||
} catch(e) {
|
||||
Log.warn(
|
||||
{ error : e.toString(), eventName : this.name, action : this.action },
|
||||
{ error : e.message, eventName : this.name, action : this.action },
|
||||
'Failed to perform scheduled event action');
|
||||
|
||||
return cb(e);
|
||||
|
@ -138,7 +139,22 @@ class ScheduledEvent {
|
|||
env : process.env,
|
||||
};
|
||||
|
||||
const proc = pty.spawn(this.action.what, this.action.args, opts);
|
||||
let proc;
|
||||
try {
|
||||
proc = pty.spawn(this.action.what, this.action.args, opts);
|
||||
} catch(e) {
|
||||
Log.warn(
|
||||
{
|
||||
error : 'Failed to spawn @execute process',
|
||||
reason : e.message,
|
||||
eventName : this.name,
|
||||
action : this.action,
|
||||
what : this.action.what,
|
||||
args : this.action.args
|
||||
}
|
||||
);
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
proc.once('exit', exitCode => {
|
||||
if(exitCode) {
|
||||
|
@ -146,7 +162,7 @@ class ScheduledEvent {
|
|||
{ eventName : this.name, action : this.action, exitCode : exitCode },
|
||||
'Bad exit code while performing scheduled event action');
|
||||
}
|
||||
return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null);
|
||||
return cb(exitCode ? Errors.ExternalProcess(`Bad exit code while performing scheduled event action: ${exitCode}`) : null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -155,8 +171,9 @@ class ScheduledEvent {
|
|||
function EventSchedulerModule(options) {
|
||||
PluginModule.call(this, options);
|
||||
|
||||
if(_.has(Config, 'eventScheduler')) {
|
||||
this.moduleConfig = Config.eventScheduler;
|
||||
const config = Config();
|
||||
if(_.has(config, 'eventScheduler')) {
|
||||
this.moduleConfig = config.eventScheduler;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const paths = require('path');
|
||||
const events = require('events');
|
||||
const Log = require('./logger.js').log;
|
||||
const SystemEvents = require('./system_events.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const glob = require('glob');
|
||||
|
||||
module.exports = new class Events extends events.EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.setMaxListeners(64); // :TODO: play with this...
|
||||
}
|
||||
|
||||
getSystemEvents() {
|
||||
return SystemEvents;
|
||||
}
|
||||
|
||||
addListener(event, listener) {
|
||||
|
@ -22,7 +25,7 @@ module.exports = new class Events extends events.EventEmitter {
|
|||
|
||||
emit(event, ...args) {
|
||||
Log.trace( { event : event }, 'Emitting event');
|
||||
return super.emit(event, args);
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
|
@ -35,39 +38,39 @@ module.exports = new class Events extends events.EventEmitter {
|
|||
return super.once(event, listener);
|
||||
}
|
||||
|
||||
//
|
||||
// Listen to multiple events for a single listener.
|
||||
// Called with: listener(event, eventName)
|
||||
//
|
||||
// The returned object must be used with removeMultipleEventListener()
|
||||
//
|
||||
addMultipleEventListener(events, listener) {
|
||||
Log.trace( { events }, 'Registering event listeners');
|
||||
|
||||
const listeners = [];
|
||||
|
||||
events.forEach(eventName => {
|
||||
const listenWrapper = _.partial(listener, _, eventName);
|
||||
this.on(eventName, listenWrapper);
|
||||
listeners.push( { eventName, listenWrapper } );
|
||||
});
|
||||
|
||||
return listeners;
|
||||
}
|
||||
|
||||
removeMultipleEventListener(listeners) {
|
||||
Log.trace( { events }, 'Removing listeners');
|
||||
listeners.forEach(listener => {
|
||||
this.removeListener(listener.eventName, listener.listenWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
removeListener(event, listener) {
|
||||
Log.trace( { event : event }, 'Removing listener');
|
||||
return super.removeListener(event, listener);
|
||||
}
|
||||
|
||||
startup(cb) {
|
||||
async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => {
|
||||
glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => {
|
||||
if(err) {
|
||||
return nextPath(err);
|
||||
}
|
||||
|
||||
async.each(files, (moduleName, nextModule) => {
|
||||
modulePath = paths.join(modulePath, moduleName);
|
||||
|
||||
try {
|
||||
const mod = require(modulePath);
|
||||
|
||||
if(_.isFunction(mod.registerEvents)) {
|
||||
// :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ?
|
||||
mod.registerEvents(this);
|
||||
}
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
|
||||
return nextModule(null);
|
||||
}, err => {
|
||||
return nextPath(err);
|
||||
});
|
||||
});
|
||||
}, err => {
|
||||
return cb(err);
|
||||
});
|
||||
return cb(null);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,12 +2,18 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||
const resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||
const Config = require('./config.js').config;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const { MenuModule } = require('./menu_module.js');
|
||||
const { resetScreen } = require('./ansi_term.js');
|
||||
const Config = require('./config.js').get;
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent;
|
||||
const {
|
||||
getEnigmaUserAgent
|
||||
} = require('./misc_util.js');
|
||||
const {
|
||||
trackDoorRunBegin,
|
||||
trackDoorRunEnd
|
||||
} = require('./door_util.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -17,7 +23,7 @@ const crypto = require('crypto');
|
|||
const moment = require('moment');
|
||||
const https = require('https');
|
||||
const querystring = require('querystring');
|
||||
const fs = require('fs');
|
||||
const fs = require('fs-extra');
|
||||
const SSHClient = require('ssh2').Client;
|
||||
|
||||
/*
|
||||
|
@ -66,7 +72,7 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||
this.config.sshHost = this.config.sshHost || this.config.ticketHost;
|
||||
this.config.sshPort = this.config.sshPort || 22;
|
||||
this.config.sshUser = this.config.sshUser || 'exodus_server';
|
||||
this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config.paths.misc, 'exodus.id_rsa');
|
||||
this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa');
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
|
@ -151,11 +157,16 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||
|
||||
let pipeRestored = false;
|
||||
let pipedStream;
|
||||
let doorTracking;
|
||||
|
||||
function restorePipe() {
|
||||
if(pipedStream && !pipeRestored && !clientTerminated) {
|
||||
self.client.term.output.unpipe(pipedStream);
|
||||
self.client.term.output.resume();
|
||||
|
||||
if(doorTracking) {
|
||||
trackDoorRunEnd(doorTracking);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +197,8 @@ exports.getModule = class ExodusModule extends MenuModule {
|
|||
});
|
||||
|
||||
sshClient.shell(window, options, (err, stream) => {
|
||||
doorTracking = trackDoorRunBegin(self.client, `exodus_${self.config.door}`);
|
||||
|
||||
pipedStream = stream; // :TODO: ewwwwwwwww hack
|
||||
self.client.term.output.pipe(stream);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ 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');
|
||||
const UserProps = require('./user_property.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -111,7 +112,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
|||
//
|
||||
// If the item was also the active filter, we need to make a new one active
|
||||
//
|
||||
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
|
||||
if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) {
|
||||
const newActive = this.filtersArray[this.currentFilterIndex];
|
||||
if(newActive) {
|
||||
filters.setActive(newActive.uuid);
|
||||
|
|
|
@ -12,7 +12,7 @@ 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 Config = require('./config.js').get;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const FileAreaWeb = require('./file_area_web.js');
|
||||
const FileBaseFilters = require('./file_base_filter.js');
|
||||
|
@ -24,6 +24,7 @@ const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi;
|
|||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const paths = require('path');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Area List',
|
||||
|
@ -75,6 +76,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
|
||||
this.filterCriteria = _.get(options, 'extraArgs.filterCriteria');
|
||||
this.fileList = _.get(options, 'extraArgs.fileList');
|
||||
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
|
||||
|
||||
if(this.fileList) {
|
||||
// we'll need to adjust position as well!
|
||||
|
@ -103,6 +105,10 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
return this.displayBrowsePage(true, cb); // true=clerarScreen
|
||||
}
|
||||
|
||||
if(this.lastFileNextExit) {
|
||||
return this.prevMenu(cb);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
prevFile : (formData, extraArgs, cb) => {
|
||||
|
@ -212,7 +218,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
const config = this.menuConfig.config;
|
||||
const currEntry = this.currentFileEntry;
|
||||
|
||||
const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD';
|
||||
const uploadTimestampFormat = config.uploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short');
|
||||
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
|
||||
const hashTagsSep = config.hashTagsSep || ', ';
|
||||
const isQueuedIndicator = config.isQueuedIndicator || 'Y';
|
||||
|
@ -247,12 +253,22 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
|
||||
if(entryInfo.archiveType) {
|
||||
const mimeType = resolveMimeType(entryInfo.archiveType);
|
||||
entryInfo.archiveTypeDesc = mimeType ? _.get(Config, [ 'fileTypes', mimeType, 'desc' ] ) || mimeType : entryInfo.archiveType;
|
||||
let desc;
|
||||
if(mimeType) {
|
||||
let fileType = _.get(Config(), [ 'fileTypes', mimeType ] );
|
||||
|
||||
if(Array.isArray(fileType)) {
|
||||
// further refine by extention
|
||||
fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext);
|
||||
}
|
||||
desc = fileType && fileType.desc;
|
||||
}
|
||||
entryInfo.archiveTypeDesc = desc || mimeType || entryInfo.archiveType;
|
||||
} else {
|
||||
entryInfo.archiveTypeDesc = 'N/A';
|
||||
}
|
||||
|
||||
entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported
|
||||
entryInfo.uploadByUsername = entryInfo.uploadByUserName = entryInfo.uploadByUsername || 'N/A'; // may be imported
|
||||
entryInfo.hashTags = entryInfo.hashTags || '(none)';
|
||||
|
||||
// create a rating string, e.g. "**---"
|
||||
|
@ -273,7 +289,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
|
||||
}
|
||||
} else {
|
||||
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat('short');
|
||||
|
||||
entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
|
||||
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
||||
|
@ -397,16 +413,23 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
//
|
||||
const desc = controlCodesToAnsi(self.currentFileEntry.desc);
|
||||
if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) {
|
||||
descView.setAnsi(
|
||||
desc,
|
||||
{
|
||||
const opts = {
|
||||
prepped : false,
|
||||
forceLineTerm : true
|
||||
},
|
||||
() => {
|
||||
return callback(null);
|
||||
};
|
||||
|
||||
//
|
||||
// if SAUCE states a term width, honor it else we may see
|
||||
// display corruption
|
||||
//
|
||||
const sauceTermWidth = _.get(self.currentFileEntry.meta, 'desc_sauce.Character.characterWidth');
|
||||
if(_.isNumber(sauceTermWidth)) {
|
||||
opts.termWidth = sauceTermWidth;
|
||||
}
|
||||
);
|
||||
|
||||
descView.setAnsi(desc, opts, () => {
|
||||
return callback(null);
|
||||
});
|
||||
} else {
|
||||
descView.setText(self.currentFileEntry.desc);
|
||||
return callback(null);
|
||||
|
@ -493,7 +516,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
return callback(null);
|
||||
}
|
||||
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempDownload(
|
||||
self.client,
|
||||
|
@ -567,34 +590,38 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
return cb(err);
|
||||
}
|
||||
|
||||
this.currentFileEntry.archiveEntries = entries;
|
||||
// assign and add standard "text" member for itemFormat
|
||||
this.currentFileEntry.archiveEntries = entries.map(e => Object.assign(e, { text : `${e.fileName} (${e.byteSize})` } ));
|
||||
return cb(null, 're-cached');
|
||||
});
|
||||
}
|
||||
|
||||
setFileListNoListing(text) {
|
||||
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
|
||||
if(fileListView) {
|
||||
fileListView.complexItems = false;
|
||||
fileListView.setItems( [ text ] );
|
||||
fileListView.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
populateFileListing() {
|
||||
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
|
||||
|
||||
if(this.currentFileEntry.entryInfo.archiveType) {
|
||||
this.cacheArchiveEntries( (err, cacheStatus) => {
|
||||
if(err) {
|
||||
// :TODO: Handle me!!!
|
||||
fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck
|
||||
return;
|
||||
return this.setFileListNoListing('Failed to get file listing');
|
||||
}
|
||||
|
||||
if('re-cached' === cacheStatus) {
|
||||
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here?
|
||||
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
|
||||
|
||||
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
|
||||
fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) );
|
||||
|
||||
fileListView.setItems(this.currentFileEntry.archiveEntries);
|
||||
fileListView.redraw();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] );
|
||||
const notAnArchiveFileName = stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } );
|
||||
this.setFileListNoListing(notAnArchiveFileName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const FileDb = require('./database.js').dbs.file;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
const FileEntry = require('./file_entry.js');
|
||||
|
@ -14,6 +14,9 @@ const User = require('./user.js');
|
|||
const Log = require('./logger.js').log;
|
||||
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
|
||||
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||
const Events = require('./events.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const SysProps = require('./system_menu_method.js');
|
||||
|
||||
// deps
|
||||
const hashids = require('hashids');
|
||||
|
@ -30,7 +33,7 @@ function notEnabledError() {
|
|||
|
||||
class FileAreaWebAccess {
|
||||
constructor() {
|
||||
this.hashids = new hashids(Config.general.boardName);
|
||||
this.hashids = new hashids(Config().general.boardName);
|
||||
this.expireTimers = {}; // hashId->timer
|
||||
}
|
||||
|
||||
|
@ -51,7 +54,7 @@ class FileAreaWebAccess {
|
|||
if(self.isEnabled()) {
|
||||
const routeAdded = self.webServer.instance.addRoute({
|
||||
method : 'GET',
|
||||
path : Config.fileBase.web.routePath,
|
||||
path : Config().fileBase.web.routePath,
|
||||
handler : self.routeWebRequest.bind(self),
|
||||
});
|
||||
return callback(routeAdded ? null : Errors.General('Failed adding route'));
|
||||
|
@ -183,11 +186,11 @@ class FileAreaWebAccess {
|
|||
buildSingleFileTempDownloadLink(client, fileEntry, hashId) {
|
||||
hashId = hashId || this.getSingleFileHashId(client, fileEntry);
|
||||
|
||||
return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`);
|
||||
return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`);
|
||||
}
|
||||
|
||||
buildBatchArchiveTempDownloadLink(client, hashId) {
|
||||
return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`);
|
||||
return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`);
|
||||
}
|
||||
|
||||
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||
|
@ -337,7 +340,7 @@ class FileAreaWebAccess {
|
|||
|
||||
resp.on('finish', () => {
|
||||
// transfer completed fully
|
||||
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size);
|
||||
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]);
|
||||
});
|
||||
|
||||
const headers = {
|
||||
|
@ -382,24 +385,21 @@ class FileAreaWebAccess {
|
|||
);
|
||||
},
|
||||
function loadFileEntries(fileIds, callback) {
|
||||
const filePaths = [];
|
||||
async.eachSeries(fileIds, (fileId, nextFileId) => {
|
||||
async.map(fileIds, (fileId, nextFileId) => {
|
||||
const fileEntry = new FileEntry();
|
||||
fileEntry.load(fileId, err => {
|
||||
if(!err) {
|
||||
filePaths.push(fileEntry.filePath);
|
||||
}
|
||||
return nextFileId(err);
|
||||
return nextFileId(err, fileEntry);
|
||||
});
|
||||
}, err => {
|
||||
}, (err, fileEntries) => {
|
||||
if(err) {
|
||||
return callback(Errors.DoesNotExist('Coudl not load file IDs for batch'));
|
||||
return callback(Errors.DoesNotExist('Could not load file IDs for batch'));
|
||||
}
|
||||
|
||||
return callback(null, filePaths);
|
||||
return callback(null, fileEntries);
|
||||
});
|
||||
},
|
||||
function createAndServeStream(filePaths, callback) {
|
||||
function createAndServeStream(fileEntries, callback) {
|
||||
const filePaths = fileEntries.map(fe => fe.filePath);
|
||||
Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request');
|
||||
|
||||
const zipFile = new yazl.ZipFile();
|
||||
|
@ -430,7 +430,7 @@ class FileAreaWebAccess {
|
|||
|
||||
resp.on('finish', () => {
|
||||
// transfer completed fully
|
||||
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize);
|
||||
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries);
|
||||
});
|
||||
|
||||
const batchFileName = `batch_${servedItem.hashId}.zip`;
|
||||
|
@ -457,7 +457,7 @@ class FileAreaWebAccess {
|
|||
);
|
||||
}
|
||||
|
||||
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, cb) {
|
||||
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
|
||||
async.waterfall(
|
||||
[
|
||||
function fetchActiveUser(callback) {
|
||||
|
@ -472,19 +472,25 @@ class FileAreaWebAccess {
|
|||
});
|
||||
},
|
||||
function updateStats(user, callback) {
|
||||
StatLog.incrementUserStat(user, 'dl_total_count', 1);
|
||||
StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes);
|
||||
StatLog.incrementSystemStat('dl_total_count', 1);
|
||||
StatLog.incrementSystemStat('dl_total_bytes', dlBytes);
|
||||
StatLog.incrementUserStat(user, UserProps.FileDlTotalCount, 1);
|
||||
StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes);
|
||||
|
||||
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
|
||||
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
|
||||
|
||||
return callback(null, user);
|
||||
},
|
||||
function sendEvent(user, callback) {
|
||||
Events.emit(
|
||||
Events.getSystemEvents().UserDownload,
|
||||
{
|
||||
user : user,
|
||||
files : fileEntries,
|
||||
}
|
||||
);
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
|
||||
const FileEntry = require('./file_entry.js');
|
||||
|
@ -14,6 +14,9 @@ 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');
|
||||
const UserProps = require('./user_property.js');
|
||||
const SysProps = require('./system_property.js');
|
||||
const SAUCE = require('./sauce.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
@ -26,6 +29,7 @@ const iconv = require('iconv-lite');
|
|||
const execFile = require('child_process').execFile;
|
||||
const moment = require('moment');
|
||||
|
||||
exports.startup = startup;
|
||||
exports.isInternalArea = isInternalArea;
|
||||
exports.getAvailableFileAreas = getAvailableFileAreas;
|
||||
exports.getAvailableFileAreaTags = getAvailableFileAreaTags;
|
||||
|
@ -42,6 +46,7 @@ exports.scanFile = scanFile;
|
|||
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||
exports.getDescFromFileName = getDescFromFileName;
|
||||
exports.getAreaStats = getAreaStats;
|
||||
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
|
||||
|
||||
// for scheduler:
|
||||
exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent;
|
||||
|
@ -52,6 +57,28 @@ const WellKnownAreaTags = exports.WellKnownAreaTags = {
|
|||
TempDownloads : 'system_temporary_download',
|
||||
};
|
||||
|
||||
function startup(cb) {
|
||||
async.series(
|
||||
[
|
||||
(callback) => {
|
||||
return cleanUpTempSessionItems(callback);
|
||||
},
|
||||
(callback) => {
|
||||
getAreaStats( (err, stats) => {
|
||||
if(!err) {
|
||||
StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats);
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function isInternalArea(areaTag) {
|
||||
return [ WellKnownAreaTags.MessageAreaAttach, WellKnownAreaTags.TempDownloads ].includes(areaTag);
|
||||
}
|
||||
|
@ -60,7 +87,7 @@ function getAvailableFileAreas(client, options) {
|
|||
options = options || { };
|
||||
|
||||
// perform ACS check per conf & omit internal if desired
|
||||
const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } ));
|
||||
const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } ));
|
||||
|
||||
return _.omitBy(allAreas, areaInfo => {
|
||||
if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) {
|
||||
|
@ -90,16 +117,17 @@ function getSortedAvailableFileAreas(client, options) {
|
|||
}
|
||||
|
||||
function getDefaultFileAreaTag(client, disableAcsCheck) {
|
||||
let defaultArea = _.findKey(Config.fileBase, o => o.default);
|
||||
const config = Config();
|
||||
let defaultArea = _.findKey(config.fileBase, o => o.default);
|
||||
if(defaultArea) {
|
||||
const area = Config.fileBase.areas[defaultArea];
|
||||
const area = config.fileBase.areas[defaultArea];
|
||||
if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) {
|
||||
return defaultArea;
|
||||
}
|
||||
}
|
||||
|
||||
// just use anything we can
|
||||
defaultArea = _.findKey(Config.fileBase.areas, (area, areaTag) => {
|
||||
defaultArea = _.findKey(config.fileBase.areas, (area, areaTag) => {
|
||||
return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area));
|
||||
});
|
||||
|
||||
|
@ -107,7 +135,7 @@ function getDefaultFileAreaTag(client, disableAcsCheck) {
|
|||
}
|
||||
|
||||
function getFileAreaByTag(areaTag) {
|
||||
const areaInfo = Config.fileBase.areas[areaTag];
|
||||
const areaInfo = Config().fileBase.areas[areaTag];
|
||||
if(areaInfo) {
|
||||
areaInfo.areaTag = areaTag; // convienence!
|
||||
areaInfo.storage = getAreaStorageLocations(areaInfo);
|
||||
|
@ -129,11 +157,11 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) {
|
|||
},
|
||||
function changeArea(area, callback) {
|
||||
if(true === options.persist) {
|
||||
client.user.persistProperty('file_area_tag', areaTag, err => {
|
||||
client.user.persistProperty(UserProps.FileAreaTag, areaTag, err => {
|
||||
return callback(err, area);
|
||||
});
|
||||
} else {
|
||||
client.user.properties['file_area_tag'] = areaTag;
|
||||
client.user.properties[UserProps.FileAreaTag] = areaTag;
|
||||
return callback(null, area);
|
||||
}
|
||||
}
|
||||
|
@ -151,13 +179,14 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) {
|
|||
}
|
||||
|
||||
function isValidStorageTag(storageTag) {
|
||||
return storageTag in Config.fileBase.storageTags;
|
||||
return storageTag in Config().fileBase.storageTags;
|
||||
}
|
||||
|
||||
function getAreaStorageDirectoryByTag(storageTag) {
|
||||
const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]);
|
||||
const config = Config();
|
||||
const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]);
|
||||
|
||||
return paths.resolve(Config.fileBase.areaStoragePrefix, storageLocation || '');
|
||||
return paths.resolve(config.fileBase.areaStoragePrefix, storageLocation || '');
|
||||
}
|
||||
|
||||
function getAreaDefaultStorageDirectory(areaInfo) {
|
||||
|
@ -170,7 +199,7 @@ function getAreaStorageLocations(areaInfo) {
|
|||
areaInfo.storageTags :
|
||||
[ areaInfo.storageTags || '' ];
|
||||
|
||||
const avail = Config.fileBase.storageTags;
|
||||
const avail = Config().fileBase.storageTags;
|
||||
|
||||
return _.compact(storageTags.map(storageTag => {
|
||||
if(avail[storageTag]) {
|
||||
|
@ -211,7 +240,7 @@ function getExistingFileEntriesBySha256(sha256, cb) {
|
|||
);
|
||||
}
|
||||
|
||||
// :TODO: This is bascially sliceAtEOF() from art.js .... DRY!
|
||||
// :TODO: This is basically sliceAtEOF() from art.js .... DRY!
|
||||
function sliceAtSauceMarker(data) {
|
||||
let eof = data.length;
|
||||
const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE)
|
||||
|
@ -227,7 +256,7 @@ function sliceAtSauceMarker(data) {
|
|||
|
||||
function attemptSetEstimatedReleaseDate(fileEntry) {
|
||||
// :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time
|
||||
const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi'));
|
||||
const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi'));
|
||||
|
||||
function getMatch(input) {
|
||||
if(input) {
|
||||
|
@ -272,11 +301,17 @@ function attemptSetEstimatedReleaseDate(fileEntry) {
|
|||
}
|
||||
|
||||
// a simple log proxy for when we call from oputil.js
|
||||
function logDebug(obj, msg) {
|
||||
const maybeLog = (obj, msg, level) => {
|
||||
if(Log) {
|
||||
Log.debug(obj, msg);
|
||||
Log[level](obj, msg);
|
||||
} else if ('error' === level) {
|
||||
console.error(`${msg}: ${JSON.stringify(obj)}`); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const logDebug = (obj, msg) => maybeLog(obj, msg, 'debug');
|
||||
const logTrace = (obj, msg) => maybeLog(obj, msg, 'trace');
|
||||
const logError = (obj, msg) => maybeLog(obj, msg, 'error');
|
||||
|
||||
function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
||||
async.waterfall(
|
||||
|
@ -284,11 +319,11 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
|||
function extractDescFiles(callback) {
|
||||
// :TODO: would be nice if these RegExp's were cached
|
||||
// :TODO: this is long winded...
|
||||
|
||||
const config = Config();
|
||||
const extractList = [];
|
||||
|
||||
const shortDescFile = archiveEntries.find( e => {
|
||||
return Config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) );
|
||||
return config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) );
|
||||
});
|
||||
|
||||
if(shortDescFile) {
|
||||
|
@ -296,7 +331,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
|||
}
|
||||
|
||||
const longDescFile = archiveEntries.find( e => {
|
||||
return Config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) );
|
||||
return config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) );
|
||||
});
|
||||
|
||||
if(longDescFile) {
|
||||
|
@ -319,8 +354,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
|||
}
|
||||
|
||||
const descFiles = {
|
||||
desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null,
|
||||
descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null,
|
||||
desc : shortDescFile ? paths.join(tempDir, paths.basename(shortDescFile.fileName)) : null,
|
||||
descLong : longDescFile ? paths.join(tempDir, paths.basename(longDescFile.fileName)) : null,
|
||||
};
|
||||
|
||||
return callback(null, descFiles);
|
||||
|
@ -328,6 +363,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
|||
});
|
||||
},
|
||||
function readDescFiles(descFiles, callback) {
|
||||
const config = Config();
|
||||
async.each(Object.keys(descFiles), (descType, next) => {
|
||||
const path = descFiles[descType];
|
||||
if(!path) {
|
||||
|
@ -341,9 +377,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
|||
|
||||
// skip entries that are too large
|
||||
const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`;
|
||||
|
||||
if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) {
|
||||
logDebug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` );
|
||||
if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) {
|
||||
logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` );
|
||||
return next(null);
|
||||
}
|
||||
|
||||
|
@ -352,20 +387,31 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) {
|
|||
return next(null);
|
||||
}
|
||||
|
||||
SAUCE.readSAUCE(data, (err, sauce) => {
|
||||
if(sauce) {
|
||||
// if we have SAUCE, this information will be kept as well,
|
||||
// but separate/pre-parsed.
|
||||
const metaKey = `desc${'descLong' === descType ? '_long' : ''}_sauce`;
|
||||
fileEntry.meta[metaKey] = JSON.stringify(sauce);
|
||||
}
|
||||
|
||||
//
|
||||
// Assume FILE_ID.DIZ, NFO files, etc. are CP437.
|
||||
// Assume FILE_ID.DIZ, NFO files, etc. are CP437; we need
|
||||
// to decode to a native format for storage
|
||||
//
|
||||
// :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');
|
||||
const decodedData = iconv.decode(data, 'cp437');
|
||||
fileEntry[descType] = sliceAtSauceMarker(decodedData, 0x1a);
|
||||
fileEntry[`${descType}Src`] = 'descFile';
|
||||
return next(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
// cleanup but don't wait
|
||||
temptmp.cleanup( paths => {
|
||||
// note: don't use client logger here - may not be avail
|
||||
logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' );
|
||||
logTrace( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' );
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
|
@ -481,13 +527,25 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c
|
|||
);
|
||||
}
|
||||
|
||||
function getInfoExtractUtilForDesc(mimeType, descType) {
|
||||
let util = _.get(Config, [ 'fileTypes', mimeType, `${descType}DescUtil` ]);
|
||||
function getInfoExtractUtilForDesc(mimeType, filePath, descType) {
|
||||
const config = Config();
|
||||
let fileType = _.get(config, [ 'fileTypes', mimeType ] );
|
||||
|
||||
if(Array.isArray(fileType)) {
|
||||
// further refine by extention
|
||||
fileType = fileType.find(ft => paths.extname(filePath) === ft.ext);
|
||||
}
|
||||
|
||||
if(!_.isObject(fileType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let util = _.get(fileType, `${descType}DescUtil`);
|
||||
if(!_.isString(util)) {
|
||||
return;
|
||||
}
|
||||
|
||||
util = _.get(Config, [ 'infoExtractUtils', util ]);
|
||||
util = _.get(config, [ 'infoExtractUtils', util ]);
|
||||
if(!util || !_.isString(util.cmd)) {
|
||||
return;
|
||||
}
|
||||
|
@ -502,7 +560,7 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) {
|
|||
}
|
||||
|
||||
async.eachSeries( [ 'short', 'long' ], (descType, nextDesc) => {
|
||||
const util = getInfoExtractUtilForDesc(mimeType, descType);
|
||||
const util = getInfoExtractUtilForDesc(mimeType, filePath, descType);
|
||||
if(!util) {
|
||||
return nextDesc(null);
|
||||
}
|
||||
|
@ -586,10 +644,6 @@ function addNewFileEntry(fileEntry, filePath, cb) {
|
|||
);
|
||||
}
|
||||
|
||||
function updateFileEntry(fileEntry, filePath, cb) {
|
||||
|
||||
}
|
||||
|
||||
const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ];
|
||||
|
||||
function scanFile(filePath, options, iterator, cb) {
|
||||
|
@ -617,23 +671,18 @@ function scanFile(filePath, options, iterator, cb) {
|
|||
fileName : paths.basename(filePath),
|
||||
};
|
||||
|
||||
function callIter(next) {
|
||||
if(iterator) {
|
||||
return iterator(stepInfo, next);
|
||||
} else {
|
||||
return next(null);
|
||||
}
|
||||
}
|
||||
const callIter = (next) => {
|
||||
return iterator ? iterator(stepInfo, next) : next(null);
|
||||
};
|
||||
|
||||
function readErrorCallIter(origError, next) {
|
||||
const readErrorCallIter = (origError, next) => {
|
||||
stepInfo.step = 'read_error';
|
||||
stepInfo.error = origError.message;
|
||||
|
||||
callIter( () => {
|
||||
return next(origError);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let lastCalcHashPercent;
|
||||
|
||||
|
@ -676,47 +725,42 @@ function scanFile(filePath, options, iterator, cb) {
|
|||
}
|
||||
});
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
function updateHashes(data) {
|
||||
async.each(hashesToCalc, (hashName, nextHash) => {
|
||||
hashes[hashName].update(data);
|
||||
return nextHash(null);
|
||||
}, () => {
|
||||
return stream.resume();
|
||||
});
|
||||
const updateHashes = (data) => {
|
||||
for(let i = 0; i < hashesToCalc.length; ++i) {
|
||||
hashes[hashesToCalc[i]].update(data);
|
||||
}
|
||||
|
||||
stream.on('data', data => {
|
||||
stream.pause(); // until iterator compeltes
|
||||
|
||||
stepInfo.bytesProcessed += data.length;
|
||||
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
|
||||
};
|
||||
|
||||
//
|
||||
// Only send 'hash_update' step update if we have a noticable percentage change in progress
|
||||
// Note that we are not using fs.createReadStream() here:
|
||||
// While convenient, it is quite a bit slower -- which adds
|
||||
// up to many seconds in time for larger files.
|
||||
//
|
||||
if(stepInfo.calcHashPercent === lastCalcHashPercent) {
|
||||
updateHashes(data);
|
||||
} else {
|
||||
lastCalcHashPercent = stepInfo.calcHashPercent;
|
||||
stepInfo.step = 'hash_update';
|
||||
const chunkSize = 1024 * 64;
|
||||
const buffer = Buffer.allocUnsafe(chunkSize);
|
||||
|
||||
callIter(err => {
|
||||
fs.open(filePath, 'r', (err, fd) => {
|
||||
if(err) {
|
||||
stream.destroy(); // cancel read
|
||||
return callback(err);
|
||||
return readErrorCallIter(err, callback);
|
||||
}
|
||||
|
||||
updateHashes(data);
|
||||
const nextChunk = () => {
|
||||
fs.read(fd, buffer, 0, chunkSize, null, (err, bytesRead) => {
|
||||
if(err) {
|
||||
return fs.close(fd, closeErr => {
|
||||
if(closeErr) {
|
||||
logError( { filePath, error : err.message }, 'Failed to close file');
|
||||
}
|
||||
return readErrorCallIter(err, callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if(0 === bytesRead) {
|
||||
// done - finalize
|
||||
fileEntry.meta.byte_size = stepInfo.bytesProcessed;
|
||||
|
||||
async.each(hashesToCalc, (hashName, nextHash) => {
|
||||
for(let i = 0; i < hashesToCalc.length; ++i) {
|
||||
const hashName = hashesToCalc[i];
|
||||
if('sha256' === hashName) {
|
||||
stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex');
|
||||
} else if('sha1' === hashName || 'md5' === hashName) {
|
||||
|
@ -724,16 +768,44 @@ function scanFile(filePath, options, iterator, cb) {
|
|||
} else if('crc32' === hashName) {
|
||||
stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16);
|
||||
}
|
||||
}
|
||||
|
||||
return nextHash(null);
|
||||
}, () => {
|
||||
stepInfo.step = 'hash_finish';
|
||||
return fs.close(fd, closeErr => {
|
||||
if(closeErr) {
|
||||
logError( { filePath, error : err.message }, 'Failed to close file');
|
||||
}
|
||||
return callIter(callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('error', err => {
|
||||
return readErrorCallIter(err, callback);
|
||||
stepInfo.bytesProcessed += bytesRead;
|
||||
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
|
||||
|
||||
//
|
||||
// Only send 'hash_update' step update if we have a noticable percentage change in progress
|
||||
//
|
||||
const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer;
|
||||
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
|
||||
updateHashes(data);
|
||||
return nextChunk();
|
||||
} else {
|
||||
lastCalcHashPercent = stepInfo.calcHashPercent;
|
||||
stepInfo.step = 'hash_update';
|
||||
|
||||
callIter(err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
updateHashes(data);
|
||||
return nextChunk();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
nextChunk();
|
||||
});
|
||||
},
|
||||
function processPhysicalFileByType(callback) {
|
||||
|
@ -747,7 +819,9 @@ function scanFile(filePath, options, iterator, cb) {
|
|||
populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => {
|
||||
if(err) {
|
||||
populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => {
|
||||
// :TODO: log err
|
||||
if(err) {
|
||||
logDebug( { error : err.message }, 'Non-archive file entry population failed');
|
||||
}
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
} else {
|
||||
|
@ -756,7 +830,9 @@ function scanFile(filePath, options, iterator, cb) {
|
|||
});
|
||||
} else {
|
||||
populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => {
|
||||
// :TODO: log err
|
||||
if(err) {
|
||||
logDebug( { error : err.message }, 'Non-archive file entry population failed');
|
||||
}
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
}
|
||||
|
@ -870,11 +946,48 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
|
|||
}
|
||||
|
||||
function getDescFromFileName(fileName) {
|
||||
// :TODO: this method could use some more logic to really be nice.
|
||||
//
|
||||
// Example filenames:
|
||||
//
|
||||
// input desired output
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Nintendo_Power_Issue_011_March-April_1990.cbr Nintendo Power Issue 011 March-April 1990
|
||||
// Atari User Issue 3 (July 1985).pdf Atari User Issue 3 (July 1985)
|
||||
// Out_Of_The_Shadows_010__1953_.cbz Out Of The Shadows 010 1953
|
||||
// ABC A Basic Compiler 1.03 [pro].atr ABC A Basic Compiler 1.03 [pro]
|
||||
// 221B Baker Street v1.0 (1987)(Datasoft)(Side B)[cr The Bounty].zip 221B Baker Street v1.0 (1987)(Datasoft)(Side B)[cr the Bounty]
|
||||
//
|
||||
// See also:
|
||||
// * https://scenerules.org/
|
||||
//
|
||||
|
||||
const ext = paths.extname(fileName);
|
||||
const name = paths.basename(fileName, ext);
|
||||
const asIsRe = /([vV]?(?:[0-9]{1,4})(?:\.[0-9]{1,4})+[-+]?(?:[a-z]{1,4})?)|(Incl\.)|(READ\.NFO)/g;
|
||||
|
||||
return _.upperFirst(name.replace(/[\-_.+]/g, ' ').replace(/\s+/g, ' '));
|
||||
const normalize = (s) => {
|
||||
return _.upperFirst(s.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' '));
|
||||
};
|
||||
|
||||
let out = '';
|
||||
let m;
|
||||
let pos;
|
||||
do {
|
||||
pos = asIsRe.lastIndex;
|
||||
m = asIsRe.exec(name);
|
||||
if(m) {
|
||||
if(m.index > pos) {
|
||||
out += normalize(name.slice(pos, m.index));
|
||||
}
|
||||
out += m[0]; // as-is
|
||||
}
|
||||
} while(0 != asIsRe.lastIndex);
|
||||
|
||||
if(pos < name.length) {
|
||||
out += normalize(name.slice(pos));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -930,9 +1043,47 @@ function getAreaStats(cb) {
|
|||
function updateAreaStatsScheduledEvent(args, cb) {
|
||||
getAreaStats( (err, stats) => {
|
||||
if(!err) {
|
||||
StatLog.setNonPeristentSystemStat('file_base_area_stats', stats);
|
||||
StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats);
|
||||
}
|
||||
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanUpTempSessionItems(cb) {
|
||||
// find (old) temporary session items and nuke 'em
|
||||
const filter = {
|
||||
areaTag : WellKnownAreaTags.TempDownloads,
|
||||
metaPairs : [
|
||||
{
|
||||
name : 'session_temp_dl',
|
||||
value : 1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
FileEntry.findFiles(filter, (err, fileIds) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
async.each(fileIds, (fileId, nextFileId) => {
|
||||
const fileEntry = new FileEntry();
|
||||
fileEntry.load(fileId, err => {
|
||||
if(err) {
|
||||
Log.warn( { fileId }, 'Failed loading temporary session download item for cleanup');
|
||||
return nextFileId(null);
|
||||
}
|
||||
|
||||
FileEntry.removeEntry(fileEntry, { removePhysFile : true }, err => {
|
||||
if(err) {
|
||||
Log.warn( { fileId : fileEntry.fileId, filePath : fileEntry.filePath }, 'Failed to clean up temporary session download item');
|
||||
}
|
||||
return nextFileId(null);
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
return cb(null);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
|
||||
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
|
||||
const StatLog = require('./stat_log.js');
|
||||
const SysProps = require('./system_property.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -24,23 +24,17 @@ 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,
|
||||
areaTag : formData.value.areaTag,
|
||||
};
|
||||
|
||||
const menuOpts = {
|
||||
extraArgs : {
|
||||
filterCriteria : filterCriteria,
|
||||
},
|
||||
menuFlags : [ 'popParent' ],
|
||||
menuFlags : [ 'popParent', 'mergeFlags' ],
|
||||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
|
||||
|
@ -48,10 +42,6 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
|||
};
|
||||
}
|
||||
|
||||
loadAvailAreas() {
|
||||
this.availAreas = getSortedAvailableFileAreas(this.client);
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
|
@ -60,35 +50,29 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
|||
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
async.waterfall(
|
||||
[
|
||||
function mergeAreaStats(callback) {
|
||||
const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} };
|
||||
const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || { areas : {} };
|
||||
|
||||
self.availAreas.forEach(area => {
|
||||
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
|
||||
const availAreas = getSortedAvailableFileAreas(self.client);
|
||||
availAreas.forEach(area => {
|
||||
const stats = areaStats.areas[area.areaTag];
|
||||
area.totalFiles = stats ? stats.files : 0;
|
||||
area.totalBytes = stats ? stats.bytes : 0;
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
return callback(null, availAreas);
|
||||
},
|
||||
function prepView(callback) {
|
||||
self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => {
|
||||
function prepView(availAreas, callback) {
|
||||
self.prepViewController('allViews', 0, 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.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
|
||||
areaListView.redraw();
|
||||
|
||||
return callback(null);
|
||||
|
|
|
@ -8,7 +8,6 @@ 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
|
||||
|
@ -59,8 +58,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||
|
||||
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
|
||||
},
|
||||
viewItemInfo : (formData, extraArgs, cb) => {
|
||||
},
|
||||
removeItem : (formData, extraArgs, cb) => {
|
||||
const selectedItem = this.dlQueue.items[formData.value.queueItem];
|
||||
if(!selectedItem) {
|
||||
|
@ -152,11 +149,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||
}
|
||||
|
||||
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}';
|
||||
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
|
||||
|
||||
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
|
||||
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
|
||||
queueView.setItems(this.dlQueue.items);
|
||||
|
||||
queueView.on('index update', idx => {
|
||||
const fileEntry = this.dlQueue.items[idx];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const UserProps = require('./user_property.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const uuidV4 = require('uuid/v4');
|
||||
|
@ -64,7 +66,7 @@ module.exports = class FileBaseFilters {
|
|||
}
|
||||
|
||||
load() {
|
||||
let filtersProperty = this.client.user.properties.file_base_filters;
|
||||
let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters];
|
||||
let defaulted;
|
||||
if(!filtersProperty) {
|
||||
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
|
||||
|
@ -90,11 +92,11 @@ module.exports = class FileBaseFilters {
|
|||
}
|
||||
|
||||
persist(cb) {
|
||||
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb);
|
||||
return this.client.user.persistProperty(UserProps.FileBaseFilters, JSON.stringify(this.filters), cb);
|
||||
}
|
||||
|
||||
cleanTags(tags) {
|
||||
return tags.toLowerCase().replace(/,?\s+|\,/g, ' ').trim();
|
||||
return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim();
|
||||
}
|
||||
|
||||
setActive(filterUuid) {
|
||||
|
@ -102,7 +104,7 @@ module.exports = class FileBaseFilters {
|
|||
|
||||
if(activeFilter) {
|
||||
this.activeFilter = activeFilter;
|
||||
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
|
||||
this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -129,11 +131,11 @@ module.exports = class FileBaseFilters {
|
|||
}
|
||||
|
||||
static getActiveFilter(client) {
|
||||
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
|
||||
return new FileBaseFilters(client).get(client.user.properties[UserProps.FileBaseFilterActiveUuid]);
|
||||
}
|
||||
|
||||
static getFileBaseLastViewedFileIdByUser(user) {
|
||||
return parseInt((user.properties.user_file_base_last_viewed || 0));
|
||||
return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0));
|
||||
}
|
||||
|
||||
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
|
||||
|
@ -150,6 +152,6 @@ module.exports = class FileBaseFilters {
|
|||
return;
|
||||
}
|
||||
|
||||
return user.persistProperty('user_file_base_last_viewed', fileId, cb);
|
||||
return user.persistProperty(UserProps.FileBaseLastViewedId, fileId, cb);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const stringFormat = require('./string_format.js');
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const FileArea = require('./file_base_area.js');
|
||||
const Config = require('./config.js').get;
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const {
|
||||
splitTextAtTerms,
|
||||
isAnsi,
|
||||
} = require('./string_util.js');
|
||||
const AnsiPrep = require('./ansi_prep.js');
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const fs = require('graceful-fs');
|
||||
const paths = require('path');
|
||||
const iconv = require('iconv-lite');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.exportFileList = exportFileList;
|
||||
exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent;
|
||||
|
||||
function exportFileList(filterCriteria, options, cb) {
|
||||
options.templateEncoding = options.templateEncoding || 'utf8';
|
||||
options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc';
|
||||
options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
|
||||
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
|
||||
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
|
||||
|
||||
if(true === options.escapeDesc) {
|
||||
options.escapeDesc = '\\n';
|
||||
}
|
||||
|
||||
const state = {
|
||||
total : 0,
|
||||
current : 0,
|
||||
step : 'preparing',
|
||||
status : 'Preparing',
|
||||
};
|
||||
|
||||
const updateProgress = _.isFunction(options.progress) ?
|
||||
progCb => {
|
||||
return options.progress(state, progCb);
|
||||
} :
|
||||
progCb => {
|
||||
return progCb(null);
|
||||
}
|
||||
;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function readTemplateFiles(callback) {
|
||||
updateProgress(err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const templateFiles = [
|
||||
{ name : options.headerTemplate, req : false },
|
||||
{ name : options.entryTemplate, req : true }
|
||||
];
|
||||
|
||||
const config = Config();
|
||||
async.map(templateFiles, (template, nextTemplate) => {
|
||||
if(!template.name && !template.req) {
|
||||
return nextTemplate(null, Buffer.from([]));
|
||||
}
|
||||
|
||||
template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name);
|
||||
fs.readFile(template.name, (err, data) => {
|
||||
return nextTemplate(err, data);
|
||||
});
|
||||
}, (err, templates) => {
|
||||
if(err) {
|
||||
return callback(Errors.General(err.message));
|
||||
}
|
||||
|
||||
// decode + ensure DOS style CRLF
|
||||
templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') );
|
||||
|
||||
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
|
||||
let descIndent = 0;
|
||||
if(!options.escapeDesc) {
|
||||
splitTextAtTerms(templates[1]).some(line => {
|
||||
const pos = line.indexOf('{fileDesc}');
|
||||
if(pos > -1) {
|
||||
descIndent = pos;
|
||||
return true; // found it!
|
||||
}
|
||||
return false; // keep looking
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, templates[0], templates[1], descIndent);
|
||||
});
|
||||
});
|
||||
},
|
||||
function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
|
||||
state.step = 'gathering';
|
||||
state.status = 'Gathering files for supplied criteria';
|
||||
updateProgress(err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
|
||||
if(0 === fileIds.length) {
|
||||
return callback(Errors.General('No results for criteria', 'NORESULTS'));
|
||||
}
|
||||
|
||||
return callback(err, headerTemplate, entryTemplate, descIndent, fileIds);
|
||||
});
|
||||
});
|
||||
},
|
||||
function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) {
|
||||
const formatObj = {
|
||||
totalFileCount : fileIds.length,
|
||||
};
|
||||
|
||||
let current = 0;
|
||||
let listBody = '';
|
||||
const totals = { fileCount : fileIds.length, bytes : 0 };
|
||||
state.total = fileIds.length;
|
||||
|
||||
state.step = 'file';
|
||||
|
||||
async.eachSeries(fileIds, (fileId, nextFileId) => {
|
||||
const fileInfo = new FileEntry();
|
||||
current += 1;
|
||||
|
||||
fileInfo.load(fileId, err => {
|
||||
if(err) {
|
||||
return nextFileId(null); // failed, but try the next
|
||||
}
|
||||
|
||||
totals.bytes += fileInfo.meta.byte_size;
|
||||
|
||||
const appendFileInfo = () => {
|
||||
if(options.escapeDesc) {
|
||||
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc);
|
||||
}
|
||||
|
||||
if(options.maxDescLen) {
|
||||
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen);
|
||||
}
|
||||
|
||||
listBody += stringFormat(entryTemplate, formatObj);
|
||||
|
||||
state.current = current;
|
||||
state.status = `Processing ${fileInfo.fileName}`;
|
||||
state.fileInfo = formatObj;
|
||||
|
||||
updateProgress(err => {
|
||||
return nextFileId(err);
|
||||
});
|
||||
};
|
||||
|
||||
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
|
||||
|
||||
formatObj.fileId = fileId;
|
||||
formatObj.areaName = _.get(area, 'name') || 'N/A';
|
||||
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
|
||||
formatObj.userRating = fileInfo.userRating || 0;
|
||||
formatObj.fileName = fileInfo.fileName;
|
||||
formatObj.fileSize = fileInfo.meta.byte_size;
|
||||
formatObj.fileDesc = fileInfo.desc || '';
|
||||
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth);
|
||||
formatObj.fileSha256 = fileInfo.fileSha256;
|
||||
formatObj.fileCrc32 = fileInfo.meta.file_crc32;
|
||||
formatObj.fileMd5 = fileInfo.meta.file_md5;
|
||||
formatObj.fileSha1 = fileInfo.meta.file_sha1;
|
||||
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A';
|
||||
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat);
|
||||
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A';
|
||||
formatObj.currentFile = current;
|
||||
formatObj.progress = Math.floor( (current / fileIds.length) * 100 );
|
||||
|
||||
if(isAnsi(fileInfo.desc)) {
|
||||
AnsiPrep(
|
||||
fileInfo.desc,
|
||||
{
|
||||
cols : Math.min(options.descWidth, 79 - descIndent),
|
||||
forceLineTerm : true, // ensure each line is term'd
|
||||
asciiMode : true, // export to ASCII
|
||||
fillLines : false, // don't fill up to |cols|
|
||||
indent : descIndent,
|
||||
},
|
||||
(err, desc) => {
|
||||
if(desc) {
|
||||
formatObj.fileDesc = desc;
|
||||
}
|
||||
return appendFileInfo();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : '';
|
||||
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n';
|
||||
return appendFileInfo();
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
return callback(err, listBody, headerTemplate, totals);
|
||||
});
|
||||
},
|
||||
function buildHeader(listBody, headerTemplate, totals, callback) {
|
||||
// header is built last such that we can have totals/etc.
|
||||
|
||||
let filterAreaName;
|
||||
let filterAreaDesc;
|
||||
if(filterCriteria.areaTag) {
|
||||
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
|
||||
filterAreaName = _.get(area, 'name') || 'N/A';
|
||||
filterAreaDesc = _.get(area, 'desc') || 'N/A';
|
||||
} else {
|
||||
filterAreaName = '-ALL-';
|
||||
filterAreaDesc = 'All areas';
|
||||
}
|
||||
|
||||
const headerFormatObj = {
|
||||
nowTs : moment().format(options.tsFormat),
|
||||
boardName : Config().general.boardName,
|
||||
totalFileCount : totals.fileCount,
|
||||
totalFileSize : totals.bytes,
|
||||
filterAreaTag : filterCriteria.areaTag || '-ALL-',
|
||||
filterAreaName : filterAreaName,
|
||||
filterAreaDesc : filterAreaDesc,
|
||||
filterTerms : filterCriteria.terms || '(none)',
|
||||
filterHashTags : filterCriteria.tags || '(none)',
|
||||
};
|
||||
|
||||
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
|
||||
return callback(null, listBody);
|
||||
},
|
||||
function done(listBody, callback) {
|
||||
delete state.fileInfo;
|
||||
state.step = 'finished';
|
||||
state.status = 'Finished processing';
|
||||
updateProgress( () => {
|
||||
return callback(null, listBody);
|
||||
});
|
||||
}
|
||||
], (err, listBody) => {
|
||||
return cb(err, listBody);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateFileBaseDescFilesScheduledEvent(args, cb) {
|
||||
//
|
||||
// For each area, loop over storage locations and build
|
||||
// DESCRIPT.ION file to store in the same directory.
|
||||
//
|
||||
// Standard-ish 4DOS spec is as such:
|
||||
// * Entry: <QUOTED_LFN> <DESC>[0x04<AppData>]\r\n
|
||||
// * Multi line descriptions are stored with *escaped* \r\n pairs
|
||||
// * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec
|
||||
//
|
||||
const entryTemplate = args[0];
|
||||
const headerTemplate = args[1];
|
||||
|
||||
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true });
|
||||
async.each(areas, (area, nextArea) => {
|
||||
const storageLocations = FileArea.getAreaStorageLocations(area);
|
||||
|
||||
async.each(storageLocations, (storageLoc, nextStorageLoc) => {
|
||||
const filterCriteria = {
|
||||
areaTag : area.areaTag,
|
||||
storageTag : storageLoc.storageTag,
|
||||
};
|
||||
|
||||
const exportOpts = {
|
||||
headerTemplate : headerTemplate,
|
||||
entryTemplate : entryTemplate,
|
||||
escapeDesc : true, // escape CRLF's
|
||||
maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes"
|
||||
};
|
||||
|
||||
exportFileList(filterCriteria, exportOpts, (err, listBody) => {
|
||||
|
||||
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION');
|
||||
fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => {
|
||||
if(err) {
|
||||
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION');
|
||||
} else {
|
||||
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION');
|
||||
}
|
||||
return nextStorageLoc(null);
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
return nextArea(null);
|
||||
});
|
||||
}, () => {
|
||||
return cb(null);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const { MenuModule } = require('./menu_module.js');
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const FileArea = require('./file_base_area.js');
|
||||
const { renderSubstr } = require('./string_util.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const Events = require('./events.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const { exportFileList } = require('./file_base_list_export.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const fs = require('graceful-fs');
|
||||
const fse = require('fs-extra');
|
||||
const paths = require('path');
|
||||
const moment = require('moment');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const yazl = require('yazl');
|
||||
|
||||
/*
|
||||
Module config block can contain the following:
|
||||
templateEncoding - encoding of template files (utf8)
|
||||
tsFormat - timestamp format (theme 'short')
|
||||
descWidth - max desc width (45)
|
||||
progBarChar - progress bar character (▒)
|
||||
compressThreshold - threshold to kick in comrpession for lists (1.44 MiB)
|
||||
templates - object containing:
|
||||
header - filename of header template (misc/file_list_header.asc)
|
||||
entry - filename of entry template (misc/file_list_entry.asc)
|
||||
|
||||
Header template variables:
|
||||
nowTs, boardName, totalFileCount, totalFileSize,
|
||||
filterAreaTag, filterAreaName, filterAreaDesc,
|
||||
filterTerms, filterHashTags
|
||||
|
||||
Entry template variables:
|
||||
fileId, areaName, areaDesc, userRating, fileName,
|
||||
fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32,
|
||||
fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags,
|
||||
currentFile, progress,
|
||||
*/
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'File Base List Export',
|
||||
desc : 'Exports file base listings for download',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
main : 0,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
main : {
|
||||
status : 1,
|
||||
progressBar : 2,
|
||||
|
||||
customRangeStart : 10,
|
||||
}
|
||||
};
|
||||
|
||||
exports.getModule = class FileBaseListExport extends MenuModule {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs);
|
||||
|
||||
this.config.templateEncoding = this.config.templateEncoding || 'utf8';
|
||||
this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short');
|
||||
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
|
||||
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1);
|
||||
this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :)
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
async.series(
|
||||
[
|
||||
(callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback),
|
||||
(callback) => this.prepareList(callback),
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
if('NORESULTS' === err.reasonCode) {
|
||||
return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults');
|
||||
}
|
||||
|
||||
return this.prevMenu();
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
finishedLoading() {
|
||||
this.prevMenu();
|
||||
}
|
||||
|
||||
prepareList(cb) {
|
||||
const self = this;
|
||||
|
||||
const statusView = self.viewControllers.main.getView(MciViewIds.main.status);
|
||||
const updateStatus = (status) => {
|
||||
if(statusView) {
|
||||
statusView.setText(status);
|
||||
}
|
||||
};
|
||||
|
||||
const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar);
|
||||
const updateProgressBar = (curr, total) => {
|
||||
if(progBarView) {
|
||||
const prog = Math.floor( (curr / total) * progBarView.dimens.width );
|
||||
progBarView.setText(self.config.progBarChar.repeat(prog));
|
||||
}
|
||||
};
|
||||
|
||||
let cancel = false;
|
||||
|
||||
const exportListProgress = (state, progNext) => {
|
||||
switch(state.step) {
|
||||
case 'preparing' :
|
||||
case 'gathering' :
|
||||
updateStatus(state.status);
|
||||
break;
|
||||
case 'file' :
|
||||
updateStatus(state.status);
|
||||
updateProgressBar(state.current, state.total);
|
||||
self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo);
|
||||
break;
|
||||
default :
|
||||
break;
|
||||
}
|
||||
|
||||
return progNext(cancel ? Errors.General('User canceled') : null);
|
||||
};
|
||||
|
||||
const keyPressHandler = (ch, key) => {
|
||||
if('escape' === key.name) {
|
||||
cancel = true;
|
||||
self.client.removeListener('key press', keyPressHandler);
|
||||
}
|
||||
};
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function buildList(callback) {
|
||||
// this may take quite a while; temp disable of idle monitor
|
||||
self.client.stopIdleMonitor();
|
||||
|
||||
self.client.on('key press', keyPressHandler);
|
||||
|
||||
const filterCriteria = Object.assign({}, self.config.filterCriteria);
|
||||
if(!filterCriteria.areaTag) {
|
||||
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client);
|
||||
}
|
||||
|
||||
const opts = {
|
||||
templateEncoding : self.config.templateEncoding,
|
||||
headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'),
|
||||
entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'),
|
||||
tsFormat : self.config.tsFormat,
|
||||
descWidth : self.config.descWidth,
|
||||
progress : exportListProgress,
|
||||
};
|
||||
|
||||
exportFileList(filterCriteria, opts, (err, listBody) => {
|
||||
return callback(err, listBody);
|
||||
});
|
||||
},
|
||||
function persistList(listBody, callback) {
|
||||
updateStatus('Persisting list');
|
||||
|
||||
const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads);
|
||||
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
|
||||
|
||||
fse.mkdirs(sysTempDownloadDir, err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const outputFileName = paths.join(
|
||||
sysTempDownloadDir,
|
||||
`file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt`
|
||||
);
|
||||
|
||||
fs.writeFile(outputFileName, listBody, 'utf8', err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => {
|
||||
return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) {
|
||||
const newEntry = new FileEntry({
|
||||
areaTag : sysTempDownloadArea.areaTag,
|
||||
fileName : paths.basename(outputFileName),
|
||||
storageTag : sysTempDownloadArea.storageTags[0],
|
||||
meta : {
|
||||
upload_by_username : self.client.user.username,
|
||||
upload_by_user_id : self.client.user.userId,
|
||||
byte_size : fileSize,
|
||||
session_temp_dl : 1, // download is valid until session is over
|
||||
}
|
||||
});
|
||||
|
||||
newEntry.desc = 'File List Export';
|
||||
|
||||
newEntry.persist(err => {
|
||||
if(!err) {
|
||||
// queue it!
|
||||
const dlQueue = new DownloadQueue(self.client);
|
||||
dlQueue.add(newEntry, true); // true=systemFile
|
||||
|
||||
// clean up after ourselves when the session ends
|
||||
const thisClientId = self.client.session.id;
|
||||
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
||||
if(thisClientId === _.get(evt, 'client.session.id')) {
|
||||
FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => {
|
||||
if(err) {
|
||||
Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' );
|
||||
} else {
|
||||
Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' );
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function done(callback) {
|
||||
// re-enable idle monitor
|
||||
self.client.startIdleMonitor();
|
||||
|
||||
updateStatus('Exported list has been added to your download queue');
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
self.client.removeListener('key press', keyPressHandler);
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) {
|
||||
fse.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if(stats.size < this.config.compressThreshold) {
|
||||
// small enough, keep orig
|
||||
return cb(null, filePath, stats.size);
|
||||
}
|
||||
|
||||
const zipFilePath = `${filePath}.zip`;
|
||||
|
||||
const zipFile = new yazl.ZipFile();
|
||||
zipFile.addFile(filePath, paths.basename(filePath));
|
||||
zipFile.end( () => {
|
||||
const outZipFile = fs.createWriteStream(zipFilePath);
|
||||
zipFile.outputStream.pipe(outZipFile);
|
||||
zipFile.outputStream.on('finish', () => {
|
||||
// delete the original
|
||||
fse.unlink(filePath, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// finally stat the new output
|
||||
fse.stat(zipFilePath, (err, stats) => {
|
||||
return cb(err, zipFilePath, stats ? stats.size : 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
|
@ -8,10 +8,9 @@ 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;
|
||||
const Config = require('./config.js').get;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -121,11 +120,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||
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.setItems(this.dlQueue.items);
|
||||
|
||||
queueView.on('index update', idx => {
|
||||
const fileEntry = this.dlQueue.items[idx];
|
||||
|
@ -139,7 +134,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||
}
|
||||
|
||||
generateAndDisplayBatchLink(cb) {
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempBatchDownload(
|
||||
this.client,
|
||||
|
@ -183,6 +178,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||
function prepareQueueDownloadLinks(callback) {
|
||||
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||
|
||||
const config = Config();
|
||||
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => {
|
||||
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => {
|
||||
if(err) {
|
||||
|
@ -190,7 +186,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||
return nextFileEntry(err); // we should have caught this prior
|
||||
}
|
||||
|
||||
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||
const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes');
|
||||
|
||||
FileAreaWeb.createAndServeTempDownload(
|
||||
self.client,
|
||||
|
@ -284,4 +280,3 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
const fileDb = require('./database.js').dbs.file;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
const Config = require('./config.js').config;
|
||||
const {
|
||||
getISOTimestampString,
|
||||
sanitizeString
|
||||
} = require('./database.js');
|
||||
const Config = require('./config.js').get;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
|
@ -35,6 +38,9 @@ const FILE_WELL_KNOWN_META = {
|
|||
tic_origin : null, // TIC "Origin"
|
||||
tic_desc : null, // TIC "Desc"
|
||||
tic_ldesc : null, // TIC "Ldesc" joined by '\n'
|
||||
session_temp_dl : (v) => parseInt(v) ? true : false,
|
||||
desc_sauce : (s) => JSON.parse(s) || {},
|
||||
desc_long_sauce : (s) => JSON.parse(s) || {},
|
||||
};
|
||||
|
||||
module.exports = class FileEntry {
|
||||
|
@ -43,11 +49,7 @@ module.exports = class FileEntry {
|
|||
|
||||
this.fileId = options.fileId || 0;
|
||||
this.areaTag = options.areaTag || '';
|
||||
this.meta = options.meta || {
|
||||
// values we always want
|
||||
dl_count : 0,
|
||||
};
|
||||
|
||||
this.meta = Object.assign( { dl_count : 0 }, options.meta);
|
||||
this.hashTags = options.hashTags || new Set();
|
||||
this.fileName = options.fileName;
|
||||
this.storageTag = options.storageTag;
|
||||
|
@ -202,7 +204,8 @@ module.exports = class FileEntry {
|
|||
}
|
||||
|
||||
static getAreaStorageDirectoryByTag(storageTag) {
|
||||
const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]);
|
||||
const config = Config();
|
||||
const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]);
|
||||
|
||||
// absolute paths as-is
|
||||
if(storageLocation && '/' === storageLocation.charAt(0)) {
|
||||
|
@ -210,7 +213,7 @@ module.exports = class FileEntry {
|
|||
}
|
||||
|
||||
// relative to |areaStoragePrefix|
|
||||
return paths.join(Config.fileBase.areaStoragePrefix, storageLocation || '');
|
||||
return paths.join(config.fileBase.areaStoragePrefix, storageLocation || '');
|
||||
}
|
||||
|
||||
get filePath() {
|
||||
|
@ -218,6 +221,19 @@ module.exports = class FileEntry {
|
|||
return paths.join(storageDir, this.fileName);
|
||||
}
|
||||
|
||||
static quickCheckExistsByPath(fullPath, cb) {
|
||||
fileDb.get(
|
||||
`SELECT COUNT() AS count
|
||||
FROM file
|
||||
WHERE file_name = ?
|
||||
LIMIT 1;`,
|
||||
[ paths.basename(fullPath) ],
|
||||
(err, rows) => {
|
||||
return err ? cb(err) : cb(null, rows.count > 0 ? true : false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static persistUserRating(fileId, userId, rating, cb) {
|
||||
return fileDb.run(
|
||||
`REPLACE INTO file_user_rating (file_id, user_id, rating)
|
||||
|
@ -355,7 +371,7 @@ module.exports = class FileEntry {
|
|||
return Object.keys(FILE_WELL_KNOWN_META);
|
||||
}
|
||||
|
||||
static findFileBySha(sha, cb) {
|
||||
static findBySha(sha, cb) {
|
||||
// full or partial SHA-256
|
||||
fileDb.all(
|
||||
`SELECT file_id
|
||||
|
@ -383,6 +399,29 @@ module.exports = class FileEntry {
|
|||
);
|
||||
}
|
||||
|
||||
// Attempt to fine a file by an *existing* full path.
|
||||
// Checkums may have changed and are not validated here.
|
||||
static findByFullPath(fullPath, cb) {
|
||||
// first, basic by-filename lookup.
|
||||
FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
if(!entries || !entries.length || entries.length > 1) {
|
||||
return cb(Errors.DoesNotExist('No matches'));
|
||||
}
|
||||
|
||||
// ensure the *full* path has not changed
|
||||
// :TODO: if FS is case-insensitive, we probably want a better check here
|
||||
const possibleMatch = entries[0];
|
||||
if(possibleMatch.fullPath === fullPath) {
|
||||
return cb(null, possibleMatch);
|
||||
}
|
||||
|
||||
return cb(Errors.DoesNotExist('No matches'));
|
||||
});
|
||||
}
|
||||
|
||||
static findByFileNameWildcard(wc, cb) {
|
||||
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
|
||||
wc = wc.replace(/\*/g, '%').replace(/\?/g, '_');
|
||||
|
@ -470,7 +509,7 @@ module.exports = class FileEntry {
|
|||
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
|
||||
} else {
|
||||
sql =
|
||||
`SELECT DISTINCT f.file_id, f.${filter.sort}
|
||||
`SELECT DISTINCT f.file_id
|
||||
FROM file f`;
|
||||
|
||||
sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
|
||||
|
@ -496,7 +535,7 @@ module.exports = class FileEntry {
|
|||
if(filter.metaPairs && filter.metaPairs.length > 0) {
|
||||
|
||||
filter.metaPairs.forEach(mp => {
|
||||
if(mp.wcValue) {
|
||||
if(mp.wildcards) {
|
||||
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
|
||||
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
|
||||
appendWhereClause(
|
||||
|
@ -523,18 +562,19 @@ module.exports = class FileEntry {
|
|||
}
|
||||
|
||||
if(filter.terms && filter.terms.length > 0) {
|
||||
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT rowid
|
||||
FROM file_fts
|
||||
WHERE file_fts MATCH "${filter.terms.replace(/"/g,'""')}"
|
||||
WHERE file_fts MATCH ":${sanitizeString(filter.terms)}"
|
||||
)`
|
||||
);
|
||||
}
|
||||
|
||||
if(filter.tags && filter.tags.length > 0) {
|
||||
// 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(',');
|
||||
const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanitizeString(tag)}"` ).join(',');
|
||||
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
|
@ -565,13 +605,14 @@ module.exports = class FileEntry {
|
|||
|
||||
sql += ';';
|
||||
|
||||
const matchingFileIds = [];
|
||||
fileDb.each(sql, (err, fileId) => {
|
||||
if(fileId) {
|
||||
matchingFileIds.push(fileId.file_id);
|
||||
fileDb.all(sql, (err, rows) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
}, err => {
|
||||
return cb(err, matchingFileIds);
|
||||
if(!rows || 0 === rows.length) {
|
||||
return cb(null, []); // no matches
|
||||
}
|
||||
return cb(null, rows.map(r => r.file_id));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -3,18 +3,21 @@
|
|||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const Config = require('./config.js').config;
|
||||
const Config = require('./config.js').get;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const DownloadQueue = require('./download_queue.js');
|
||||
const StatLog = require('./stat_log.js');
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const Events = require('./events.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const SysProps = require('./system_property.js');
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const pty = require('ptyw.js');
|
||||
const pty = require('node-pty');
|
||||
const temptmp = require('temptmp').createTrackedSession('transfer_file');
|
||||
const paths = require('path');
|
||||
const fs = require('graceful-fs');
|
||||
|
@ -55,9 +58,10 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
//
|
||||
// Most options can be set via extraArgs or config block
|
||||
//
|
||||
const config = Config();
|
||||
if(options.extraArgs) {
|
||||
if(options.extraArgs.protocol) {
|
||||
this.protocolConfig = Config.fileTransferProtocols[options.extraArgs.protocol];
|
||||
this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol];
|
||||
}
|
||||
|
||||
if(options.extraArgs.direction) {
|
||||
|
@ -77,7 +81,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
} else {
|
||||
if(this.config.protocol) {
|
||||
this.protocolConfig = Config.fileTransferProtocols[this.config.protocol];
|
||||
this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
|
||||
}
|
||||
|
||||
if(this.config.direction) {
|
||||
|
@ -97,7 +101,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
}
|
||||
|
||||
this.protocolConfig = this.protocolConfig || Config.fileTransferProtocols.zmodem8kSz; // try for *something*
|
||||
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
|
||||
this.direction = this.direction || 'send';
|
||||
this.sendQueue = this.sendQueue || [];
|
||||
|
||||
|
@ -312,11 +316,15 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
return callback(err); // failed to create it
|
||||
}
|
||||
|
||||
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL));
|
||||
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
fs.close(tempFileInfo.fd, err => {
|
||||
return callback(err, tempFileInfo.path);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
function createArgs(tempFileListPath, callback) {
|
||||
// initial args: ignore {filePaths} as we must break that into it's own sep array items
|
||||
|
@ -361,17 +369,20 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
'Executing external protocol'
|
||||
);
|
||||
|
||||
const externalProc = pty.spawn(cmd, args, {
|
||||
const spawnOpts = {
|
||||
cols : this.client.term.termWidth,
|
||||
rows : this.client.term.termHeight,
|
||||
cwd : this.recvDirectory,
|
||||
});
|
||||
encoding : null, // don't bork our data!
|
||||
};
|
||||
|
||||
const externalProc = pty.spawn(cmd, args, spawnOpts);
|
||||
|
||||
this.client.setTemporaryDirectDataHandler(data => {
|
||||
// needed for things like sz/rz
|
||||
if(external.escapeTelnet) {
|
||||
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
|
||||
externalProc.write(new Buffer(tmp, 'binary'));
|
||||
externalProc.write(Buffer.from(tmp, 'binary'));
|
||||
} else {
|
||||
externalProc.write(data);
|
||||
}
|
||||
|
@ -381,7 +392,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
// needed for things like sz/rz
|
||||
if(external.escapeTelnet) {
|
||||
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
|
||||
this.client.term.rawWrite(new Buffer(tmp, 'binary'));
|
||||
this.client.term.rawWrite(Buffer.from(tmp, 'binary'));
|
||||
} else {
|
||||
this.client.term.rawWrite(data);
|
||||
}
|
||||
|
@ -470,10 +481,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
});
|
||||
}, () => {
|
||||
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
|
||||
StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount);
|
||||
StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes);
|
||||
StatLog.incrementSystemStat('dl_total_count', downloadCount);
|
||||
StatLog.incrementSystemStat('dl_total_bytes', downloadBytes);
|
||||
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalCount, downloadCount);
|
||||
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalBytes, downloadBytes);
|
||||
|
||||
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount);
|
||||
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes);
|
||||
|
||||
fileIds.forEach(fileId => {
|
||||
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
|
||||
|
@ -500,10 +512,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
return next(null);
|
||||
});
|
||||
}, () => {
|
||||
StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount);
|
||||
StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes);
|
||||
StatLog.incrementSystemStat('ul_total_count', uploadCount);
|
||||
StatLog.incrementSystemStat('ul_total_bytes', uploadBytes);
|
||||
StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalCount, uploadCount);
|
||||
StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalBytes, uploadBytes);
|
||||
|
||||
StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount);
|
||||
StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes);
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
|
@ -542,7 +555,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
if(sentFileIds.length > 0) {
|
||||
// remove items we sent from the D/L queue
|
||||
const dlQueue = new DownloadQueue(self.client);
|
||||
dlQueue.removeItems(sentFileIds);
|
||||
const dlFileEntries = dlQueue.removeItems(sentFileIds);
|
||||
|
||||
// fire event for downloaded entries
|
||||
Events.emit(
|
||||
Events.getSystemEvents().UserDownload,
|
||||
{
|
||||
user : self.client.user,
|
||||
files : dlFileEntries
|
||||
}
|
||||
);
|
||||
|
||||
self.sentFileIds = sentFileIds;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
// enigma-bbs
|
||||
const MenuModule = require('./menu_module.js').MenuModule;
|
||||
const Config = require('./config.js').config;
|
||||
const stringFormat = require('./string_format.js');
|
||||
const Config = require('./config.js').get;
|
||||
const ViewController = require('./view_controller.js').ViewController;
|
||||
|
||||
// deps
|
||||
|
@ -110,12 +109,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||
function populateList(callback) {
|
||||
const protListView = vc.getView(MciViewIds.protList);
|
||||
|
||||
const protListFormat = self.config.protListFormat || '{name}';
|
||||
const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
|
||||
|
||||
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
|
||||
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
|
||||
|
||||
protListView.setItems(self.protocols);
|
||||
protListView.redraw();
|
||||
|
||||
return callback(null);
|
||||
|
@ -129,8 +123,9 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||
}
|
||||
|
||||
loadAvailProtocols() {
|
||||
this.protocols = _.map(Config.fileTransferProtocols, (protInfo, protocol) => {
|
||||
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
|
||||
return {
|
||||
text : protInfo.name, // standard
|
||||
protocol : protocol,
|
||||
name : protInfo.name,
|
||||
hasBatch : _.has(protInfo, 'external.recvArgs'),
|
||||
|
|
|
@ -51,7 +51,7 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
|
|||
if(err) {
|
||||
// for some reason fs-extra copy doesn't pass err.code
|
||||
// :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST
|
||||
if('EEXIST' === err.code || 'copy' === operation) {
|
||||
if('EEXIST' === err.code || 'dest already exists.' === err.message) {
|
||||
renameIndex += 1;
|
||||
return cb(null); // keep trying
|
||||
}
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
// deps
|
||||
const fs = require('graceful-fs');
|
||||
const iconv = require('iconv-lite');
|
||||
const moment = require('moment');
|
||||
|
||||
// Descriptions found in the wild that mean "no description" /facepalm.
|
||||
const IgnoredDescriptions = [
|
||||
'No description available',
|
||||
'No ID File Found For This Archive File.',
|
||||
];
|
||||
|
||||
module.exports = class FilesBBSFile {
|
||||
constructor() {
|
||||
this.entries = new Map();
|
||||
}
|
||||
|
||||
get(fileName) {
|
||||
return this.entries.get(fileName);
|
||||
}
|
||||
|
||||
getDescription(fileName) {
|
||||
const entry = this.get(fileName);
|
||||
if(entry) {
|
||||
return entry.desc;
|
||||
}
|
||||
}
|
||||
|
||||
static createFromFile(path, cb) {
|
||||
fs.readFile(path, (err, descData) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// :TODO: encoding should be default to CP437, but allowed to change - ie for Amiga/etc.
|
||||
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
|
||||
const filesBbs = new FilesBBSFile();
|
||||
|
||||
const isBadDescription = (desc) => {
|
||||
return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false;
|
||||
};
|
||||
|
||||
//
|
||||
// Contrary to popular belief, there is not a FILES.BBS standard. Instead,
|
||||
// many formats have been used over the years. We'll try to support as much
|
||||
// as we can within reason.
|
||||
//
|
||||
// Resources:
|
||||
// - Great info from Mystic @ http://wiki.mysticbbs.com/doku.php?id=mutil_import_files.bbs
|
||||
// - https://alt.bbs.synchronet.narkive.com/I6Vrxq6q/format-of-files-bbs
|
||||
//
|
||||
// Example files:
|
||||
// - https://github.com/NuSkooler/ansi-bbs/tree/master/ancient_formats/files_bbs
|
||||
//
|
||||
const detectDecoder = () => {
|
||||
// helpers
|
||||
const regExpTestUpTo = (n, re) => {
|
||||
return lines
|
||||
.slice(0, n)
|
||||
.some(l => re.test(l));
|
||||
};
|
||||
|
||||
//
|
||||
// Try to figure out which decoder to use
|
||||
//
|
||||
const decoders = [
|
||||
{
|
||||
// I've been told this is what Syncrhonet uses
|
||||
lineRegExp : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/,
|
||||
detect : function() {
|
||||
return regExpTestUpTo(10, this.lineRegExp);
|
||||
},
|
||||
extract : function() {
|
||||
for(let i = 0; i < lines.length; ++i) {
|
||||
let line = lines[i];
|
||||
const hdr = line.match(this.lineRegExp);
|
||||
if(!hdr) {
|
||||
continue;
|
||||
}
|
||||
const long = [];
|
||||
for(let j = i + 1; j < lines.length; ++j) {
|
||||
line = lines[j];
|
||||
if(!line.startsWith(' ')) {
|
||||
break;
|
||||
}
|
||||
long.push(line.trim());
|
||||
++i;
|
||||
}
|
||||
const desc = long.join('\r\n') || hdr[3] || '';
|
||||
const fileName = hdr[1];
|
||||
const timestamp = moment(hdr[2], 'MM/DD/YY');
|
||||
|
||||
if(isBadDescription(desc) || !timestamp.isValid()) {
|
||||
continue;
|
||||
}
|
||||
filesBbs.entries.set(fileName, { timestamp, desc } );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
//
|
||||
// Examples:
|
||||
// - Night Owl CD #7, 1992
|
||||
//
|
||||
lineRegExp : /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/,
|
||||
detect : function() {
|
||||
return regExpTestUpTo(10, this.lineRegExp);
|
||||
},
|
||||
extract : function() {
|
||||
for(let i = 0; i < lines.length; ++i) {
|
||||
let line = lines[i];
|
||||
const hdr = line.match(this.lineRegExp);
|
||||
if(!hdr) {
|
||||
continue;
|
||||
}
|
||||
const long = [ hdr[2].trim() ];
|
||||
for(let j = i + 1; j < lines.length; ++j) {
|
||||
line = lines[j];
|
||||
// -------------------------------------------------v 32
|
||||
if(!line.startsWith(' | ')) {
|
||||
break;
|
||||
}
|
||||
long.push(line.substr(33));
|
||||
++i;
|
||||
}
|
||||
const desc = long.join('\r\n');
|
||||
const fileName = hdr[1];
|
||||
|
||||
if(isBadDescription(desc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filesBbs.entries.set(fileName, { desc } );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
//
|
||||
// Simple first line with partial description,
|
||||
// secondary description lines tabbed out.
|
||||
//
|
||||
// Examples
|
||||
// - GUS archive @ dk.toastednet.org
|
||||
//
|
||||
lineRegExp : /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/,
|
||||
detect : function() {
|
||||
return regExpTestUpTo(10, this.lineRegExp);
|
||||
},
|
||||
extract : function() {
|
||||
for(let i = 0; i < lines.length; ++i) {
|
||||
let line = lines[i];
|
||||
const hdr = line.match(this.lineRegExp);
|
||||
if(!hdr) {
|
||||
continue;
|
||||
}
|
||||
const long = [ hdr[2].trimRight() ];
|
||||
for(let j = i + 1; j < lines.length; ++j) {
|
||||
line = lines[j];
|
||||
if(!line.startsWith('\t\t ')) {
|
||||
break;
|
||||
}
|
||||
long.push(line.substr(4));
|
||||
++i;
|
||||
}
|
||||
const desc = long.join('\r\n');
|
||||
const fileName = hdr[1];
|
||||
|
||||
if(isBadDescription(desc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filesBbs.entries.set(fileName, { desc } );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
//
|
||||
// <8.3FileName> <size> <MM-DD-YY> <desc first line>
|
||||
// <desc...>
|
||||
// Examples:
|
||||
// - Expanding Your BBS CD by David Wolfe, 1995
|
||||
//
|
||||
lineRegExp : /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/,
|
||||
detect : function() {
|
||||
return regExpTestUpTo(10, this.lineRegExp);
|
||||
},
|
||||
extract : function() {
|
||||
for(let i = 0; i < lines.length; ++i) {
|
||||
let line = lines[i];
|
||||
const hdr = line.match(this.lineRegExp);
|
||||
if(!hdr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstDescLine = hdr[4].trimRight();
|
||||
const long = [ firstDescLine ];
|
||||
for(let j = i + 1; j < lines.length; ++j) {
|
||||
line = lines[j];
|
||||
if(!line.startsWith(' '.repeat(34))) {
|
||||
break;
|
||||
}
|
||||
long.push(line.substr(34).trimRight());
|
||||
++i;
|
||||
}
|
||||
|
||||
const desc = long.join('\r\n');
|
||||
const fileName = hdr[1];
|
||||
const size = parseInt(hdr[2]);
|
||||
const timestamp = moment(hdr[3], 'MM-DD-YY');
|
||||
|
||||
if(isBadDescription(desc) || isNaN(size) || !timestamp.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filesBbs.entries.set(fileName, { desc, size, timestamp });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
//
|
||||
// Examples:
|
||||
// - Aminet Amiga CDROM, March 1994. Walnut Creek CDROM.
|
||||
// - CP/M CDROM, Sep. 1994. Walnut Creek CDROM.
|
||||
// - ...and many others.
|
||||
//
|
||||
// Basically: <8.3 filename> <description>
|
||||
//
|
||||
// May contain headers, but we'll just skip 'em.
|
||||
//
|
||||
lineRegExp : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/,
|
||||
detect : function() {
|
||||
return regExpTestUpTo(10, this.lineRegExp);
|
||||
},
|
||||
extract : function() {
|
||||
lines.forEach(line => {
|
||||
const hdr = line.match(this.lineRegExp);
|
||||
if(!hdr) {
|
||||
return; // forEach
|
||||
}
|
||||
|
||||
const fileName = hdr[1].trim();
|
||||
const desc = hdr[2].trim();
|
||||
|
||||
if(desc && !isBadDescription(desc)) {
|
||||
filesBbs.entries.set(fileName, { desc } );
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
//
|
||||
// Examples:
|
||||
// - AMINET CD's & similar
|
||||
//
|
||||
lineRegExp : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/,
|
||||
detect : function() {
|
||||
return regExpTestUpTo(10, this.lineRegExp);
|
||||
},
|
||||
extract : function() {
|
||||
lines.forEach(line => {
|
||||
const hdr = line.match(this.tester);
|
||||
if(!hdr) {
|
||||
return; // forEach
|
||||
}
|
||||
|
||||
const fileName = hdr[1].trim();
|
||||
let size = parseInt(hdr[2]);
|
||||
const desc = hdr[3].trim();
|
||||
|
||||
if(isNaN(size)) {
|
||||
return; // forEach
|
||||
}
|
||||
size *= 1024; // K->bytes.
|
||||
|
||||
if(desc) { // omit empty entries
|
||||
filesBbs.entries.set(fileName, { size, desc } );
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const decoder = decoders.find(d => d.detect());
|
||||
return decoder;
|
||||
};
|
||||
|
||||
const decoder = detectDecoder();
|
||||
if(!decoder) {
|
||||
return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format'));
|
||||
}
|
||||
|
||||
decoder.extract(decoder);
|
||||
|
||||
return cb(
|
||||
filesBbs.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized FILES.BBS format'),
|
||||
filesBbs
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue