diff --git a/.eslintrc.json b/.eslintrc.json index 53bd1287..fbe0b672 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,5 +28,8 @@ ], "comma-dangle": 0, "no-trailing-spaces" :"warn" + }, + "parserOptions": { + "ecmaVersion": 2020 } } \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..43e0e6cf --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,35 @@ +name: Docker + +on: + push: + branches: [ master ] + + workflow_dispatch: + +jobs: + buildx: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + tags: enigmabbs/enigma-bbs:latest + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true \ No newline at end of file diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml new file mode 100644 index 00000000..a17e17ef --- /dev/null +++ b/.github/workflows/jekyll.yml @@ -0,0 +1,75 @@ +name: Build and deploy jekyll site + +on: + push: + branches: + - master + # - source + # It is highly recommended that you only run this action on push to a + # specific branch, eg. master or source (if on *.github.io repo) + +jobs: + jekyll: + runs-on: ubuntu-latest # can change this to ubuntu-latest if you prefer + steps: + - name: 📂 setup + uses: actions/checkout@v2 + # include the lines below if you are using jekyll-last-modified-at + # or if you would otherwise need to fetch the full commit history + # however this may be very slow for large repositories! + # with: + # fetch-depth: '0' + + - name: 💎 setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 # can change this to 2.7 or whatever version you prefer + + - name: 🔨 install dependencies & build site + uses: limjh16/jekyll-action-ts@v2 + with: + enable_cache: true + ### Enables caching. Similar to https://github.com/actions/cache. + # + # format_output: true + ### Uses prettier https://prettier.io to format jekyll output HTML. + # + # prettier_opts: '{ "useTabs": true }' + ### Sets prettier options (in JSON) to format output HTML. For example, output tabs over spaces. + ### Possible options are outlined in https://prettier.io/docs/en/options.html + # + # prettier_ignore: 'about/*' + ### Ignore paths for prettier to not format those html files. + ### Useful if the file is exceptionally large, so formatting it takes a while. + ### Also useful if HTML compression is enabled for that file / formatting messes it up. + # + jekyll_src: docs + ### If the jekyll website source is not in root, specify the directory. (in this case, sample_site) + ### By default, this is not required as the action searches for a _config.yml automatically. + # + gem_src: docs + ### By default, this is not required as the action searches for a _config.yml automatically. + ### However, if there are multiple Gemfiles, the action may not be able to determine which to use. + ### In that case, specify the directory. (in this case, sample_site) + ### + ### If jekyll_src is set, the action would automatically choose the Gemfile in jekyll_src. + ### In that case this input may not be needed as well. + # + # key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + # restore-keys: ${{ runner.os }}-gems- + ### In cases where you want to specify the cache key, enable the above 2 inputs + ### Follows the format here https://github.com/actions/cache + # + # custom_opts: '--drafts --lsi' + ### If you need to specify any Jekyll build options, enable the above input + ### Flags accepted can be found here https://jekyllrb.com/docs/configuration/options/#build-command-options + + - name: 🚀 deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./_site + # if the repo you are deploying to is .github.io, uncomment the line below. + # if you are including the line below, make sure your source files are NOT in the master branch: + # publish_branch: master + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9eb2ef48 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +## Style +Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins. +There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise. +Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces. diff --git a/LICENSE.TXT b/LICENSE.TXT index af51c707..6fca7184 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,4 @@ -Copyright (c) 2015-2020, Bryan D. Ashby +Copyright (c) 2015-2022, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index ca3b7412..409b8332 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,9 @@ If you feel the urge to donate, [you can do so here](https://liberapay.com/NuSko Donate using Liberapay ## Support -* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) +* See [Discussions](https://github.com/NuSkooler/enigma-bbs/discussions) and [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) * **Discussion on a ENiGMA BBS!** (see Boards below) -* IRC: **#enigma-bbs** on **chat.freenode.net** ([webchat](https://webchat.freenode.net/?channels=enigma-bbs)) +* IRC: **#enigma-bbs** on **irc.libera.chat:6697(TLS)** ([webchat](https://web.libera.chat/gamja/?channels=#enigma-bbs)) * FSX_ENG on [fsxNet](http://bbs.geek.nz/#fsxNet) or ARK_ENIG on [ArakNet](https://www.araknet.xyz/) available on many fine boards * Email: bryan -at- l33t.codes * [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/) @@ -64,9 +64,9 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [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**) -* [Goblin Studio](https://goblin.strangled.net): (**ssh://goblin.strangled.net:8889**) ## Special Thanks +(in no particular order) * [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!) @@ -81,15 +81,16 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [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! * [Smooth](https://16colo.rs/tags/artist/smooth)/[fUEL](https://fuel.wtf/) for lots of dope art. Why not [snag a T-Shirt](https://www.redbubble.com/people/araknet/works/39126831-enigma-1-2-software-logo-design-by-smooth-of-fuel?p=t-shirt)? -* Al's Geek Lab for the [installation video](https://youtu.be/WnN-ucVi3ZU)! +* Al's Geek Lab for the [installation video](https://youtu.be/WnN-ucVi3ZU) and of course the [Back to the BBS - Part one: The return to being online](https://www.youtube.com/watch?reload=9&v=n0OwGSX2IiQ) documentary! * Alpha for the [FTN-style configuration guide](https://medium.com/@alpha_11845/setting-up-ftn-style-message-networks-with-enigma%C2%BD-bbs-709b22a1ae0d)! +* Huge shout out to [cognitivegears ](https://github.com/cognitivegears) for the various fixes, improvements, and **removing the need for cursor position reports** providing a much better terminal experience! ...and so many others! This project would be nothing without the BBS and artscene communities! ## License Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license: -Copyright (c) 2015-2020, Bryan D. Ashby +Copyright (c) 2015-2022, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/UPGRADE.md b/UPGRADE.md index 117b4bff..800e6fd2 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -28,6 +28,33 @@ npm install # or simply 'yarn' Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). + +# 0.0.12-beta to 0.0.13-beta +* :exclamation: The SSH server's `ssh2` module has gone through a major upgrade. Existing users will need to comment out two SSH KEX algorithms from their `config.hjson` if present else clients such as NetRunner will not be able to connect over SSH. Comment out `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` +* All features and changes are backwards compatible. There are a few new configuration options in a new `term` section in the configuration. These are all optional, but include the following options in case you use them: + +```hjson +{ + + term: { + // checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals. + // Using this with a terminal that does not support cursor position reports results in a 2 second delay + // during the connect process, but provides better autoconfiguration of utf-8 + checkUtf8Encoding: true + + + // Checking the ANSI home position also requires the use of cursor position reports, which are not + // supported on all terminals. Using this with a terminal that does not support cursor position reports + // results in a 3 second delay during the connect process, but works around positioning problems with + // non-standard terminals. + checkAnsiHomePosition: true + } +} + +``` + +In addition to these, there are also new options for `term.cp437TermList` and `term.utf8TermList`. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in `config_default.js` + # 0.0.11-beta to 0.0.12-beta * Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information. * **BREAKING CHANGE** There is no longer a `prompt.hjson` file. Prompts are now simply part of the menu set in the `prompts` section. If you have an existing system you will need to add your `prompt.hjson` to your `menu.hjson`'s `includes` section at a minimum. Example: @@ -39,6 +66,13 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or ] } ``` +* A set of database fixes were made that cause some records to be properly cleaned up when e.g. deleting a file. Existing `file.db` databases will need to be updated **manually**. Note that this applies to users upgrading within 0.0.12-beta as well: +1. **Make a backup of your file.db!** +2. Shut down ENiGMA. +3. From the enigma-bbs directory: +``` +sqlite3 db/file.sqlite3 < ./misc/update/tables_update_2020-11-29.sql +``` # 0.0.10-alpha to 0.0.11-beta * Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.: `nvm install 12 && nvm use 12`). diff --git a/WHATSNEW.md b/WHATSNEW.md index e15f1b8e..4ae304d6 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,8 +1,15 @@ # Whats New This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. +## 0.0.13-beta +* Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with Enigma 1/2. For more information, see [Issue #222](https://github.com/NuSkooler/enigma-bbs/issues/222). This may also resolve other issues, such as [Issue #365](https://github.com/NuSkooler/enigma-bbs/issues/365), and [Issue #320](https://github.com/NuSkooler/enigma-bbs/issues/320). Anyone that previously had terminal incompatibilities please re-check and let us know! +* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to V14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience. +* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in `UPGRADE.md` +* Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the [project repository](https://github.com/NuSkooler/enigma-bbs) for more information. + ## 0.0.12-beta * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](./docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). +* Development now occurs against [Node.js 14 LTS](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md). * The default configuration has been moved to [config_default.js](/core/config_default.js). * A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. * An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired. @@ -10,6 +17,17 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. * A number of new MCI codes (see [MCI](./docs/art/mci.md)) * Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system! +* The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information. +* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template. +* File base search has had an improvement to search term handling. +* `./oputil user group -group` to now accepts `~group` removing the need for special handling of the "-" character. #331 +* A fix has been made to clean up old `file.db` entries when a file is removed. Previously stale records could be left or even recycled into new entries. Please see [UPGRADE.md](UPGRADE.md) for details on applying this fix (look for `tables_update_2020-11-29.sql`). +* The [./docs/modding/onelinerz.md](onelinerz) module can have `dbSuffix` set in it's `config` block to specify a separate DB file. For example to use as a requests list. +* Default hash tags can now be set in file areas. Simply supply an array or list of values in a file area block via `hashTags`. +* Added ability to pass an `env` value (map) to `abracadabra` doors. See [Local Doors](./docs/modding/local-doors.md]). +* `dropFileType` is now optional when launching doors with `abracadabra`. It can also be explicitly set to `none`. +* FSE in *view* mode can now stylize quote indicators. Supply `quoteStyleLevel1` in the `config` block. This can be a single string or an array of two strings (one to style the quotee's initials, the next for the '>' character, and finally the quoted text). See the `messageAreaViewPost` menu `config` block in the default `luciano_blocktronics` `theme.hjson` file for an example. An additional level style (e.g. for nested quotes) may be added in the future. +* FSE in *view* mode can now stylize tear lines and origin lines via `tearLineStyle` and `originStyle` `config` values in the same manor as `quoteStyleLevel`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! @@ -116,4 +134,4 @@ submit: [ ...LOTS more! ## Pre 0.0.8-alpha -See GitHub \ No newline at end of file +See GitHub diff --git a/art/themes/luciano_blocktronics/FBHELP.ANS b/art/themes/luciano_blocktronics/FBHELP.ANS index 5dc95322..08ea5885 100644 Binary files a/art/themes/luciano_blocktronics/FBHELP.ANS and b/art/themes/luciano_blocktronics/FBHELP.ANS differ diff --git a/art/themes/luciano_blocktronics/FBRWSE.ANS b/art/themes/luciano_blocktronics/FBRWSE.ANS index 52db95e0..c2b617db 100644 Binary files a/art/themes/luciano_blocktronics/FBRWSE.ANS and b/art/themes/luciano_blocktronics/FBRWSE.ANS differ diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 407e8a1b..2ce388d0 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -538,6 +538,23 @@ // The 'msg_list' module looks for this entry by default messageAreaViewPost: { + config: { + quoteStyleLevel1: [ + "|00|11", + "|00|08", + "|00|03", + ] + tearLineStyle: [ + "|00|08", + "|00|02", + ] + originStyle: [ + "|00|08", + "|00|06", + "|00|03", + ] + } + 0: { mci: { TL1: { diff --git a/core/abracadabra.js b/core/abracadabra.js index 83aa376b..a8a72b1d 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -11,12 +11,14 @@ const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); +const Log = require('./logger').log; // deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); const paths = require('path'); +const fs = require('graceful-fs'); const activeDoorNodeInstances = {}; @@ -70,20 +72,12 @@ exports.getModule = class AbracadabraModule extends MenuModule { // :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')); this.config.nodeMax = this.config.nodeMax || 0; this.config.args = this.config.args || []; } - /* - :TODO: - * disconnecting while door is open leaves dosemu - * http://bbslink.net/sysop.php support - * Font support ala all other menus... or does this just work? - */ - incrementActiveDoorNodeInstances() { if(activeDoorNodeInstances[this.config.name]) { activeDoorNodeInstances[this.config.name] += 1; @@ -141,11 +135,15 @@ exports.getModule = class AbracadabraModule extends MenuModule { return self.doorInstance.prepare(self.config.io || 'stdio', callback); }, function generateDropfile(callback) { - const dropFileOpts = { - fileType : self.config.dropFileType, - }; + if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') { + return callback(null); + } + + self.dropFile = new DropFile( + self.client, + { fileType : self.config.dropFileType } + ); - self.dropFile = new DropFile(self.client, dropFileOpts); return self.dropFile.createFile(callback); } ], @@ -170,17 +168,30 @@ exports.getModule = class AbracadabraModule extends MenuModule { args : this.config.args, io : this.config.io || 'stdio', encoding : this.config.encoding || 'cp437', - dropFile : this.dropFile.fileName, - dropFilePath : this.dropFile.fullPath, node : this.client.node, + env : this.config.env, }; + if (this.dropFile) { + exeInfo.dropFile = this.dropFile.fileName; + exeInfo.dropFilePath = this.dropFile.fullPath; + } + const doorTracking = trackDoorRunBegin(this.client, this.config.name); this.doorInstance.run(exeInfo, () => { trackDoorRunEnd(doorTracking); this.decrementActiveDoorNodeInstances(); + // Clean up dropfile, if any + if (exeInfo.dropFilePath) { + fs.unlink(exeInfo.dropFilePath, err => { + if (err) { + Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.'); + } + }); + } + // client may have disconnected while process was active - // we're done here if so. if(!this.client.term.output) { @@ -199,7 +210,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { '\r\n\r\n' ); - this.prevMenu(); + this.autoNextMenu(); }); } diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 88063bef..438f6203 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -21,8 +21,6 @@ function ANSIEscapeParser(options) { events.EventEmitter.call(this); this.column = 1; - this.row = 1; - this.scrollBack = 0; this.graphicRendition = {}; this.parseState = { @@ -36,11 +34,15 @@ function ANSIEscapeParser(options) { trailingLF : 'default', // default|omit|no|yes, ... }); + this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); + + this.row = Math.min(options?.startRow ?? 1, this.termHeight); + self.moveCursor = function(cols, rows) { self.column += cols; self.row += rows; @@ -69,14 +71,11 @@ function ANSIEscapeParser(options) { }; self.clearScreen = function() { - // :TODO: should be doing something with row/column? + self.column = 1; + self.row = 1; self.emit('clear screen'); }; - /* - self.rowUpdated = function() { - self.emit('row update', self.row + self.scrollBack); - };*/ self.positionUpdated = function() { self.emit('position update', self.row, self.column); @@ -190,10 +189,11 @@ function ANSIEscapeParser(options) { self.emit('mci', { - mci : mciCode, - id : id ? parseInt(id, 10) : null, - args : args, - SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + position : [self.row, self.column], + mci : mciCode, + id : id ? parseInt(id, 10) : null, + args : args, + SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) }); if(self.mciReplaceChar.length > 0) { @@ -215,6 +215,9 @@ function ANSIEscapeParser(options) { } self.reset = function(input) { + self.column = 1; + self.row = Math.min(options?.startRow ?? 1, self.termHeight); + self.parseState = { // ignore anything past EOF marker, if any buffer : input.split(String.fromCharCode(0x1a), 1)[0], diff --git a/core/ansi_term.js b/core/ansi_term.js index cac29681..6a765da7 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -309,7 +309,8 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = { 'mo_soul' : 'mo_soul', 'mosoul' : 'mo_soul', - 'mO\'sOul' : 'mo_soul', + 'mo\'soul' : 'mo_soul', + 'amiga_mosoul' : 'mo_soul', 'amiga_microknight' : 'microknight', 'amiga_microknight+' : 'microknight_plus', diff --git a/core/art.js b/core/art.js index 0ff4835f..7385d08a 100644 --- a/core/art.js +++ b/core/art.js @@ -269,19 +269,16 @@ function display(client, art, options, cb) { termHeight : client.term.termHeight, termWidth : client.term.termWidth, trailingLF : options.trailingLF, + startRow : options.startRow, }); let parseComplete = false; - let cprListener; let mciMap; const mciCprQueue = []; let artHash; let mciMapFromCache; function completed() { - if(cprListener) { - client.removeListener('cursor position report', cprListener); - } if(!options.disableMciCache && !mciMapFromCache) { // cache our MCI findings... @@ -314,18 +311,6 @@ function display(client, art, options, cb) { // no cached MCI info mciMap = {}; - cprListener = function(pos) { - if(mciCprQueue.length > 0) { - mciMap[mciCprQueue.shift()].position = pos; - - if(parseComplete && 0 === mciCprQueue.length) { - return completed(); - } - } - }; - - client.on('cursor position report', cprListener); - let generatedId = 100; ansiParser.on('mci', mciInfo => { @@ -339,18 +324,16 @@ function display(client, art, options, cb) { mapEntry.focusArgs = mciInfo.args; } else { mciMap[mapKey] = { - args : mciInfo.args, - SGR : mciInfo.SGR, - code : mciInfo.mci, - id : id, + position : mciInfo.position, + args : mciInfo.args, + SGR : mciInfo.SGR, + code : mciInfo.mci, + id : id, }; if(!mciInfo.id) { ++generatedId; } - - mciCprQueue.push(mapKey); - client.term.rawWrite(ansi.queryPos()); } }); diff --git a/core/button_view.js b/core/button_view.js index edb32e12..2aebf347 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -21,7 +21,7 @@ function ButtonView(options) { util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(ch, key) { - if(this.isKeyMapped('accept', key.name) || ' ' === ch) { + if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); delete this.submitData; @@ -29,16 +29,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) { ButtonView.super_.prototype.onKeyPress.call(this, ch, key); } }; -/* -ButtonView.prototype.onKeyPress = function(ch, key) { - // allow space = submit - if(' ' === ch) { - this.emit('action', 'accept'); - } - - ButtonView.super_.prototype.onKeyPress.call(this, ch, key); -}; -*/ ButtonView.prototype.getData = function() { return this.submitData || null; diff --git a/core/client_term.js b/core/client_term.js index 4cbd603c..b19b4771 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -4,11 +4,12 @@ // ENiGMA½ var Log = require('./logger.js').log; var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; - +const Config = require('./config.js').get; var iconv = require('iconv-lite'); var assert = require('assert'); var _ = require('lodash'); + exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { @@ -115,7 +116,8 @@ ClientTerminal.prototype.isNixTerm = function() { return true; } - return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType); + const utf8TermList = Config().term.utf8TermList; + return utf8TermList.includes(this.termType); }; ClientTerminal.prototype.isANSI = function() { @@ -153,7 +155,8 @@ ClientTerminal.prototype.isANSI = function() { // linux: // * JuiceSSH (note: TERM=linux also) // - return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType); + const cp437TermList = Config().term.cp437TermList; + return cp437TermList.includes(this.termType); }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) diff --git a/core/color_codes.js b/core/color_codes.js index ff08275e..da6c8f5d 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -126,7 +126,7 @@ function renegadeToAnsi(s, client) { // // Converts various control codes popular in BBS packages -// to ANSI escape sequences. Additionaly supports ENiGMA style +// to ANSI escape sequences. Additionally supports ENiGMA style // MCI codes. // // Supported control code formats: @@ -134,16 +134,17 @@ function renegadeToAnsi(s, client) { // * 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 +// * CNET Control-Y: AKA Y-Style -- 0x19## where ## is a specific set of codes (older format) +// * CNET Control-Q: AKA Q-style -- 0x11##} where ## is a specific set of codes (newer format) // // TODO: Add Synchronet and Celerity format support // // Resources: // * http://wiki.synchro.net/custom:colors +// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt // function controlCodesToAnsi(s, client) { - 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 + 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|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex let m; let result = ''; diff --git a/core/config_default.js b/core/config_default.js index 82d3eb1d..25b14bb7 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -17,6 +17,24 @@ module.exports = () => { achievementFile : 'achievements.hjson', }, + term : { + // checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals. + // Using this with a terminal that does not support cursor position reports results in a 2 second delay + // during the connect process, but provides better autoconfiguration of utf-8 + checkUtf8Encoding : true, + + // Checking the ANSI home position also requires the use of cursor position reports, which are not + // supported on all terminals. Using this with a terminal that does not support cursor position reports + // results in a 3 second delay during the connect process, but works around positioning problems with + // non-standard terminals. + checkAnsiHomePosition: true, + + // List of terms that should be assumed to use cp437 encoding + cp437TermList : ['ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb'], + // List of terms that should be assumed to use utf8 encoding + utf8TermList : ['xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator'], + }, + users : { usernameMin : 2, usernameMax : 16, // Note that FidoNet wants 36 max @@ -166,10 +184,11 @@ module.exports = () => { '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', + // Group exchange not currnetly supported + // 'diffie-hellman-group-exchange-sha256', + // 'diffie-hellman-group-exchange-sha1', ], cipher : [ 'aes128-ctr', @@ -263,7 +282,7 @@ module.exports = () => { port : 8070, publicHostname : 'another-fine-enigma-bbs.org', publicPort : 8070, // adjust if behind NAT/etc. - bannerFile : 'gopher_banner.asc', + staticRoot : paths.join(__dirname, './../gopher'), // // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] @@ -492,7 +511,7 @@ module.exports = () => { }, decompress : { cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? + args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? }, list : { cmd : '7za', @@ -501,7 +520,7 @@ module.exports = () => { }, extract : { cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], + args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ], }, }, diff --git a/core/connect.js b/core/connect.js index 64c4ea3e..59cb4da9 100644 --- a/core/connect.js +++ b/core/connect.js @@ -4,6 +4,7 @@ // ENiGMA½ const ansi = require('./ansi_term.js'); const Events = require('./events.js'); +const Config = require('./config.js').get; const { Errors } = require('./enig_error.js'); // deps @@ -18,6 +19,13 @@ function ansiDiscoverHomePosition(client, cb) { // think of home as 0,0. If this is the case, we need to offset // our positioning to accommodate for such. // + + + if( !Config().term.checkAnsiHomePosition ) { + // Skip (and assume 1,1) if the home position check is disabled. + return cb(null); + } + const done = (err) => { client.removeListener('cursor position report', cprListener); clearTimeout(giveUpTimer); @@ -68,8 +76,9 @@ function ansiAttemptDetectUTF8(client, cb) { // // 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()) { + // Also skip this check if checkUtf8Encoding is disabled in the config + + if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) { return cb(null); } @@ -119,6 +128,8 @@ function ansiAttemptDetectUTF8(client, cb) { return giveUp(); }, 2000); + + client.once('cursor position report', cprListener); client.term.rawWrite(ansi.goHome() + ansi.queryPos()); } @@ -199,7 +210,7 @@ function displayBanner(term) { // note: intentional formatting: term.pipeWrite(` |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN -|06Copyright (c) 2014-2020 Bryan Ashby |14- |12http://l33t.codes/ +|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |00` ); @@ -216,7 +227,6 @@ function connectEntry(client, nextMenu) { }, function discoverHomePosition(callback) { ansiDiscoverHomePosition(client, () => { - // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required return callback(null); // we try to continue anyway }); }, diff --git a/core/database.js b/core/database.js index 17bc3844..55ed4e2e 100644 --- a/core/database.js +++ b/core/database.js @@ -421,7 +421,8 @@ const DB_INIT_TABLE = { hash_tag_id INTEGER NOT NULL, file_id INTEGER NOT NULL, - UNIQUE(hash_tag_id, file_id) + UNIQUE(hash_tag_id, file_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE );` ); @@ -431,7 +432,10 @@ const DB_INIT_TABLE = { user_id INTEGER NOT NULL, rating INTEGER NOT NULL, - UNIQUE(file_id, user_id) + UNIQUE(file_id, user_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE + -- Note that we cannot CASCADE if user_id is removed from user.db + -- See processing in oputil's removeUser() );` ); @@ -447,7 +451,8 @@ const DB_INIT_TABLE = { hash_id VARCHAR NOT NULL, file_id INTEGER NOT NULL, - UNIQUE(hash_id, file_id) + UNIQUE(hash_id, file_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE );` ); diff --git a/core/edit_text_view.js b/core/edit_text_view.js index db01b9f5..05c7224d 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -6,27 +6,45 @@ const TextView = require('./text_view.js').TextView; const miscUtil = require('./misc_util.js'); const strUtil = require('./string_util.js'); +const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT; + // deps const _ = require('lodash'); exports.EditTextView = EditTextView; +const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, { + delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ +}); + function EditTextView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.resizable = false; + if(!_.isObject(options.specialKeyMap)) { + options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP; + } + TextView.call(this, options); this.initDefaultWidth(); - this.cursorPos = { row : 0, col : 0 }; this.clientBackspace = function() { - const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); - this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); - }; + this.text = this.text.substr(0, this.text.length - 1); + + if(this.text.length >= this.dimens.width) { + this.redraw(); + } else { + this.cursorPos.col -= 1; + if(this.cursorPos.col >= 0) { + const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); + this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); + } + } + } } require('util').inherits(EditTextView, TextView); @@ -35,19 +53,16 @@ EditTextView.prototype.onKeyPress = function(ch, key) { if(key) { if(this.isKeyMapped('backspace', key.name)) { if(this.text.length > 0) { - this.text = this.text.substr(0, this.text.length - 1); - - if(this.text.length >= this.dimens.width) { - this.redraw(); - } else { - this.cursorPos.col -= 1; - if(this.cursorPos.col >= 0) { - this.clientBackspace(); - } - } + this.clientBackspace(); } return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); + } else if (this.isKeyMapped('delete', key.name)) { + // Some (mostly older) terms send 'delete' for Backspace. + // if we're at the end of the line, go ahead and treat them the same + if (this.text.length > 0 && this.cursorPos.col === this.text.length) { + this.clientBackspace(); + } } else if(this.isKeyMapped('clearLine', key.name)) { this.text = ''; this.cursorPos.col = 0; diff --git a/core/file_area_list.js b/core/file_area_list.js index a62271ca..637c3c3e 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -144,7 +144,10 @@ exports.getModule = class FileAreaList extends MenuModule { }, displayHelp : (formData, extraArgs, cb) => { return this.displayHelpPage(cb); - } + }, + movementKeyPressed : (formData, extraArgs, cb) => { + return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb); + }, }; } @@ -201,7 +204,7 @@ exports.getModule = class FileAreaList extends MenuModule { }, function display(callback) { return self.displayBrowsePage(false, err => { - if(err && 'NORESULTS' === err.reasonCode) { + if(err) { self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); } return callback(err); @@ -505,6 +508,23 @@ exports.getModule = class FileAreaList extends MenuModule { ); } + _handleMovementKeyPress(keyName, cb) { + const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc); + if (!descView) { + return cb(null); + } + + switch (keyName) { + case 'down arrow' : descView.scrollDocumentUp(); break; + case 'up arrow' : descView.scrollDocumentDown(); break; + case 'page up' : descView.keyPressPageUp(); break; + case 'page down' : descView.keyPressPageDown(); break; + } + + this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu); + return cb(null); + } + fetchAndDisplayWebDownloadLink(cb) { const self = this; @@ -720,7 +740,7 @@ exports.getModule = class FileAreaList extends MenuModule { } FileEntry.findFiles(filterCriteria, (err, fileIds) => { - this.fileList = fileIds; + this.fileList = fileIds || []; return cb(err); }); } diff --git a/core/file_base_area.js b/core/file_base_area.js index e9926111..57cf9cc7 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -45,7 +45,7 @@ exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule; exports.getFileEntryPath = getFileEntryPath; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; exports.scanFile = scanFile; -exports.scanFileAreaForChanges = scanFileAreaForChanges; +//exports.scanFileAreaForChanges = scanFileAreaForChanges; exports.getDescFromFileName = getDescFromFileName; exports.getAreaStats = getAreaStats; exports.cleanUpTempSessionItems = cleanUpTempSessionItems; @@ -139,7 +139,14 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { function getFileAreaByTag(areaTag) { const areaInfo = Config().fileBase.areas[areaTag]; if(areaInfo) { - areaInfo.areaTag = areaTag; // convienence! + // normalize |hashTags| + if (_.isString(areaInfo.hashTags)) { + areaInfo.hashTags = areaInfo.hashTags.trim().split(','); + } + if (Array.isArray(areaInfo.hashTags)) { + areaInfo.hashTags = new Set(areaInfo.hashTags.map(t => t.trim())); + } + areaInfo.areaTag = areaTag; // convenience! areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } @@ -794,7 +801,7 @@ function scanFile(filePath, options, iterator, cb) { stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); // - // Only send 'hash_update' step update if we have a noticable percentage change in progress + // Only send 'hash_update' step update if we have a noticeable percentage change in progress // const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { @@ -871,90 +878,91 @@ function scanFile(filePath, options, iterator, cb) { ); } -function scanFileAreaForChanges(areaInfo, options, iterator, cb) { - if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; - } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; - } +// :TODO: this stuff needs cleaned up +// function scanFileAreaForChanges(areaInfo, options, iterator, cb) { +// if(3 === arguments.length && _.isFunction(iterator)) { +// cb = iterator; +// iterator = null; +// } else if(2 === arguments.length && _.isFunction(options)) { +// cb = options; +// iterator = null; +// options = {}; +// } - const storageLocations = getAreaStorageLocations(areaInfo); +// const storageLocations = getAreaStorageLocations(areaInfo); - async.eachSeries(storageLocations, (storageLoc, nextLocation) => { - async.series( - [ - function scanPhysFiles(callback) { - const physDir = storageLoc.dir; +// async.eachSeries(storageLocations, (storageLoc, nextLocation) => { +// async.series( +// [ +// function scanPhysFiles(callback) { +// const physDir = storageLoc.dir; - fs.readdir(physDir, (err, files) => { - if(err) { - return callback(err); - } +// fs.readdir(physDir, (err, files) => { +// if(err) { +// return callback(err); +// } - async.eachSeries(files, (fileName, nextFile) => { - const fullPath = paths.join(physDir, fileName); +// async.eachSeries(files, (fileName, nextFile) => { +// const fullPath = paths.join(physDir, fileName); - fs.stat(fullPath, (err, stats) => { - if(err) { - // :TODO: Log me! - return nextFile(null); // always try next file - } +// fs.stat(fullPath, (err, stats) => { +// if(err) { +// // :TODO: Log me! +// return nextFile(null); // always try next file +// } - if(!stats.isFile()) { - return nextFile(null); - } +// if(!stats.isFile()) { +// return nextFile(null); +// } - scanFile( - fullPath, - { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag - }, - iterator, - (err, fileEntry, dupeEntries) => { - if(err) { - // :TODO: Log me!!! - return nextFile(null); // try next anyway - } +// scanFile( +// fullPath, +// { +// areaTag : areaInfo.areaTag, +// storageTag : storageLoc.storageTag +// }, +// iterator, +// (err, fileEntry, dupeEntries) => { +// if(err) { +// // :TODO: Log me!!! +// return nextFile(null); // try next anyway +// } - if(dupeEntries.length > 0) { - // :TODO: Handle duplidates -- what to do here??? - } else { - if(Array.isArray(options.tags)) { - options.tags.forEach(tag => { - fileEntry.hashTags.add(tag); - }); - } - addNewFileEntry(fileEntry, fullPath, err => { - // pass along error; we failed to insert a record in our DB or something else bad - return nextFile(err); - }); - } - } - ); - }); - }, err => { - return callback(err); - }); - }); - }, - function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above - return callback(null); - } - ], - err => { - return nextLocation(err); - } - ); - }, - err => { - return cb(err); - }); -} +// if(dupeEntries.length > 0) { +// // :TODO: Handle duplicates -- what to do here??? +// } else { +// if(Array.isArray(options.tags)) { +// options.tags.forEach(tag => { +// fileEntry.hashTags.add(tag); +// }); +// } +// addNewFileEntry(fileEntry, fullPath, err => { +// // pass along error; we failed to insert a record in our DB or something else bad +// return nextFile(err); +// }); +// } +// } +// ); +// }); +// }, err => { +// return callback(err); +// }); +// }); +// }, +// function scanDbEntries(callback) { +// // :TODO: Look @ db entries for area that were *not* processed above +// return callback(null); +// } +// ], +// err => { +// return nextLocation(err); +// } +// ); +// }, +// err => { +// return cb(err); +// }); +// } function getDescFromFileName(fileName) { // diff --git a/core/file_entry.js b/core/file_entry.js index 0539f137..476864ca 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -243,6 +243,15 @@ module.exports = class FileEntry { ); } + static removeUserRatings(userId, cb) { + return fileDb.run( + `DELETE FROM file_user_rating + WHERE user_id = ?;`, + [ userId ], + cb + ); + } + static persistMetaValue(fileId, name, value, transOrDb, cb) { if(!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; @@ -457,6 +466,16 @@ module.exports = class FileEntry { ); } + // + // Find file(s) by |filter| + // + // - sort: sort results by any well known name, file_id, or user_rating + // - terms: one or more search terms to search within filenames as well + // as short and long descriptions. We attempt to use the FTS ability when + // possible, but want to allow users to search for wildcard matches in + // which some cases we'll use multiple LIKE queries. + // See _normalizeFileSearchTerms() + // static findFiles(filter, cb) { filter = filter || {}; @@ -500,8 +519,8 @@ module.exports = class FileEntry { if('user_rating' === filter.sort) { sql = `SELECT DISTINCT f.file_id, - (SELECT IFNULL(AVG(rating), 0) rating - FROM file_user_rating + (SELECT IFNULL(AVG(rating), 0) rating + FROM file_user_rating WHERE file_id = f.file_id) AS avg_rating FROM file f`; @@ -540,16 +559,16 @@ module.exports = class FileEntry { mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_meta + SELECT file_id + FROM file_meta WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}" )` ); } else { appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_meta + SELECT file_id + FROM file_meta WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}" )` ); @@ -562,14 +581,29 @@ 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 ":${sanitizeString(filter.terms)}" - )` - ); + const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms); + + if ('fts_match' === queryType) { + // 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 ":${terms}" + )` + ); + } else { + appendWhereClause( + `(f.file_name LIKE "${terms}" OR + f.desc LIKE "${terms}" OR + f.desc_long LIKE "${terms}")` + ); + } + } + + // handle e.g. 1998 -> "1998" + if (_.isNumber(filter.tags)) { + filter.tags = filter.tags.toString(); } if(filter.tags && filter.tags.length > 0) { @@ -693,4 +727,46 @@ module.exports = class FileEntry { } ); } + + static _normalizeFileSearchTerms(terms) { + // ensure we have reasonable input to start with + terms = sanitizeString(terms.toString()); + + // No wildcards? + const hasSingleCharWC = terms.indexOf('?') > -1; + if (terms.indexOf('*') === -1 && !hasSingleCharWC) { + return [ terms, 'fts_match' ]; + } + + const prepareLike = () => { + // Convert * and ? to SQL LIKE style + terms = terms.replace(/\*/g, '%').replace(/\?/g, '_'); + return terms; + }; + + // Any ? wildcards? + if (hasSingleCharWC) { + return [ prepareLike(terms), 'like' ]; + } + + const split = terms.replace(/\s+/g, ' ').split(' '); + const useLike = split.some(term => { + if (term.indexOf('?') > -1) { + return true; + } + + const wcPos = term.indexOf('*'); + if (wcPos > -1 && wcPos !== term.length - 1) { + return true; + } + + return false; + }); + + if (useLike) { + return [ prepareLike(terms), 'like' ]; + } + + return [ terms, 'fts_match' ]; + } }; diff --git a/core/fse.js b/core/fse.js index ba278a01..a0d8750b 100644 --- a/core/fse.js +++ b/core/fse.js @@ -21,7 +21,10 @@ const { isAnsi, stripAnsiControlCodes, insert } = require('./string_util.js'); -const { stripMciColorCodes } = require('./color_codes.js'); +const { + stripMciColorCodes, + controlCodesToAnsi, +} = require('./color_codes.js'); const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); const Events = require('./events.js'); @@ -418,7 +421,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // // Find tearline - we want to color it differently. // - const tearLinePos = this.message.getTearLinePosition(msg); + const tearLinePos = Message.getTearLinePosition(msg); if(tearLinePos > -1) { msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text')); @@ -432,7 +435,55 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } ); } else { - bodyMessageView.setText(stripAnsiControlCodes(msg)); + msg = stripAnsiControlCodes(msg); // start clean + + const styleToArray = (style, len) => { + if (!Array.isArray(style)) { + style = [ style ]; + } + while (style.length < len) { + style.push(style[0]); + } + return style; + }; + + // + // In *View* mode, if enabled, do a little prep work so we can stylize: + // - Quote indicators + // - Tear lines + // - Origins + // + if (this.menuConfig.config.quoteStyleLevel1) { + // can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT + // Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed + const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3); + + const QuoteRegex = /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm; + msg = msg.replace(QuoteRegex, (m, spc1, initials, spc2, text) => { + return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + if (this.menuConfig.config.tearLineStyle) { + // '---' and TEXT + const style = styleToArray(this.menuConfig.config.tearLineStyle, 2); + + const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m; + msg = msg.replace(TearLineRegex, (m, text) => { + return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + if (this.menuConfig.config.originStyle) { + const style = styleToArray(this.menuConfig.config.originStyle, 3); + + const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m; + msg = msg.replace(OriginRegex, (m, spc, text) => { + return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + bodyMessageView.setText(controlCodesToAnsi(msg)); } } } @@ -552,7 +603,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul theme.displayThemedAsset( footerArt, self.client, - { font : self.menuConfig.font }, + { font : self.menuConfig.font, startRow: self.header.height + self.body.height }, function displayed(err, artData) { callback(err, artData); } @@ -575,19 +626,34 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul async.series( [ function displayHeaderAndBody(callback) { - async.eachSeries( comps, function dispArt(n, next) { - theme.displayThemedAsset( - art[n], - self.client, - { font : self.menuConfig.font }, - function displayed(err) { - next(err); + async.waterfall( + [ + function displayHeader(callback) { + theme.displayThemedAsset( + art['header'], + self.client, + { font : self.menuConfig.font }, + function displayed(err, artInfo) { + return callback(err, artInfo); + } + ); + }, + function displayBody(artInfo, callback) { + theme.displayThemedAsset( + art['header'], + self.client, + { font : self.menuConfig.font, startRow: artInfo.height + 1 }, + function displayed(err, artInfo) { + return callback(err, artInfo); + } + ); } - ); - }, function complete(err) { - //self.body.height = self.client.term.termHeight - self.header.height - 1; - callback(err); - }); + ], + function complete(err) { + //self.body.height = self.client.term.termHeight - self.header.height - 1; + callback(err); + } + ); }, function displayFooter(callback) { // we have to treat the footer special @@ -649,31 +715,39 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul assert(_.isObject(art)); - async.series( + async.waterfall( [ function beforeDisplayArt(callback) { self.beforeArt(callback); }, - function displayHeaderAndBodyArt(callback) { - async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) { - theme.displayThemedAsset( - art[n], - self.client, - { font : self.menuConfig.font }, - function displayed(err, artData) { - if(artData) { - mciData[n] = artData; - self[n] = { height : artData.height }; - } - - next(err); + function displayHeader(callback) { + theme.displayThemedAsset( + art.header, + self.client, + { font : self.menuConfig.font }, + function displayed(err, artInfo) { + if(artInfo) { + mciData['header'] = artInfo; + self.header = {height: artInfo.height}; } - ); - }, function complete(err) { - callback(err); - }); + return callback(err, artInfo); + } + ); }, - function displayFooter(callback) { + function displayBody(artInfo, callback) { + theme.displayThemedAsset( + art.body, + self.client, + { font : self.menuConfig.font, startRow: artInfo.height + 1 }, + function displayed(err, artInfo) { + if(artInfo) { + mciData['body'] = artInfo; + self.body = {height: artInfo.height - self.header.height}; + } + return callback(err, artInfo); + }); + }, + function displayFooter(artInfo, callback) { self.setInitialFooterMode(); var footerName = self.getFooterName(); @@ -740,10 +814,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }); }, function prepareViewStates(callback) { - var header = self.viewControllers.header; - var from = header.getView(MciViewIds.header.from); - from.acceptsFocus = false; - //from.setText(self.client.user.username); + let from = self.viewControllers.header.getView(MciViewIds.header.from); + if (from) { + from.acceptsFocus = false; + } // :TODO: make this a method var body = self.viewControllers.body.getView(MciViewIds.body.message); @@ -774,10 +848,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul { const fromView = self.viewControllers.header.getView(MciViewIds.header.from); const area = getMessageAreaByTag(self.messageAreaTag); - if(area && area.realNames) { - fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username); - } else { - fromView.setText(self.client.user.username); + if(fromView !== undefined) { + if(area && area.realNames) { + fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username); + } else { + fromView.setText(self.client.user.username); + } } if(self.replyToMessage) { @@ -863,7 +939,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } initHeaderViewMode() { - this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + // Only set header text for from view if it is on the form + if (this.viewControllers.header.getView(MciViewIds.header.from) !== undefined) { + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + } this.setHeaderText(MciViewIds.header.to, this.message.toUserName); this.setHeaderText(MciViewIds.header.subject, this.message.subject); diff --git a/core/full_menu_view.js b/core/full_menu_view.js new file mode 100644 index 00000000..212b4d15 --- /dev/null +++ b/core/full_menu_view.js @@ -0,0 +1,511 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; + +// deps +const util = require('util'); +const _ = require('lodash'); + +exports.FullMenuView = FullMenuView; + +function FullMenuView(options) { + options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; + + + MenuView.call(this, options); + + + // Initialize paging + this.pages = []; + this.currentPage = 0; + + this.initDefaultWidth(); + + // we want page up/page down by default + if (!_.isObject(options.specialKeyMap)) { + Object.assign(this.specialKeyMap, { + 'page up': ['page up'], + 'page down': ['page down'], + }); + } + + this.autoAdjustHeightIfEnabled = () => { + if (this.autoAdjustHeight) { + this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); + this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); + } + + this.positionCacheExpired = true; + }; + + this.autoAdjustHeightIfEnabled(); + + this.clearPage = () => { + let width = this.dimens.width; + if (this.oldDimens) { + if (this.oldDimens.width > width) { + width = this.oldDimens.width; + } + delete this.oldDimens; + } + + for (let i = 0; i < this.dimens.height; i++) { + const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); + } + } + + this.cachePositions = () => { + if (this.positionCacheExpired) { + // first, clear the page + this.clearPage(); + + + this.autoAdjustHeightIfEnabled(); + + this.pages = []; // reset + + // Calculate number of items visible per column + this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); + // handle case where one can fit at the end + if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { + this.itemsPerRow++; + } + + // Final check to make sure we don't try to display more than we have + if (this.itemsPerRow > this.items.length) { + this.itemsPerRow = this.items.length; + } + + let col = this.position.col; + let row = this.position.row; + const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar); + + let itemInRow = 0; + let itemInCol = 0; + + let pageStart = 0; + + for (let i = 0; i < this.items.length; ++i) { + itemInRow++; + this.items[i].row = row; + this.items[i].col = col; + this.items[i].itemInRow = itemInRow; + + row += this.itemSpacing + 1; + + // have to calculate the max length on the last entry + if (i == this.items.length - 1) { + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + const itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + this.items[i - j].fixedLength = maxLength; + } + + + // Check if we have room for this column + // skip for column 0, we need at least one + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { + // save previous page + this.pages.push({ start: pageStart, end: i - itemInRow }); + + // fix the last column processed + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != col) { + break; + } + this.items[i - j].col = this.position.col; + pageStart = i - j; + } + + } + + // Since this is the last page, save the current page as well + this.pages.push({ start: pageStart, end: i }); + + } + // also handle going to next column + else if (itemInRow == this.itemsPerRow) { + itemInRow = 0; + + // restart row for next column + row = this.position.row; + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { + // TODO: handle complex items + let itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (let j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].fixedLength = maxLength; + } + + // Check if we have room for this column in the current page + // skip for first column, we need at least one + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { + // save previous page + this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + + // restart page start for next page + pageStart = i - this.itemsPerRow + 1; + + // reset + col = this.position.col; + itemInRow = 0; + + // fix the last column processed + for (let j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].col = col; + } + + } + + // increment the column + col += maxLength + spacer.length; + itemInCol++; + } + + + // Set the current page if the current item is focused. + if (this.focusedItemIndex === i) { + this.currentPage = this.pages.length; + } + } + } + + this.positionCacheExpired = false; + }; + + this.drawItem = (index) => { + const item = this.items[index]; + if (!item) { + return; + } + + const cached = this.getRenderCacheItem(index, item.focused); + if (cached) { + return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + } + + let text; + let sgr; + if (item.focused && this.hasFocusItems()) { + const focusItem = this.focusItems[index]; + text = focusItem ? focusItem.text : item.text; + sgr = ''; + } else if (this.complexItems) { + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); + sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); + } + + let renderLength = strUtil.renderStringLength(text); + if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) { + text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; + } + + let padLength = Math.min(item.fixedLength + 1, this.dimens.width); + + text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; + this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); + }; +} + +util.inherits(FullMenuView, MenuView); + +FullMenuView.prototype.redraw = function() { + FullMenuView.super_.prototype.redraw.call(this); + + this.cachePositions(); + + if (this.items.length) { + for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) { + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } + } +}; + +FullMenuView.prototype.setHeight = function(height) { + this.oldDimens = Object.assign({}, this.dimens); + + FullMenuView.super_.prototype.setHeight.call(this, height); + + this.positionCacheExpired = true; + this.autoAdjustHeight = false; +}; + +FullMenuView.prototype.setWidth = function(width) { + this.oldDimens = Object.assign({}, this.dimens); + + FullMenuView.super_.prototype.setWidth.call(this, width); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setTextOverflow = function(overflow) { + FullMenuView.super_.prototype.setTextOverflow.call(this, overflow); + + this.positionCacheExpired = true; + +} + +FullMenuView.prototype.setPosition = function(pos) { + FullMenuView.super_.prototype.setPosition.call(this, pos); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setFocus = function(focused) { + FullMenuView.super_.prototype.setFocus.call(this, focused); + this.positionCacheExpired = true; + this.autoAdjustHeight = false; + + this.redraw(); +}; + +FullMenuView.prototype.setFocusItemIndex = function(index) { + FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex +}; + +FullMenuView.prototype.onKeyPress = function(ch, key) { + if (key) { + if (this.isKeyMapped('up', key.name)) { + this.focusPrevious(); + } else if (this.isKeyMapped('down', key.name)) { + this.focusNext(); + } else if (this.isKeyMapped('left', key.name)) { + this.focusPreviousColumn(); + } else if (this.isKeyMapped('right', key.name)) { + this.focusNextColumn(); + } else if (this.isKeyMapped('page up', key.name)) { + this.focusPreviousPageItem(); + } else if (this.isKeyMapped('page down', key.name)) { + this.focusNextPageItem(); + } else if (this.isKeyMapped('home', key.name)) { + this.focusFirst(); + } else if (this.isKeyMapped('end', key.name)) { + this.focusLast(); + } + } + + FullMenuView.super_.prototype.onKeyPress.call(this, ch, key); +}; + +FullMenuView.prototype.getData = function() { + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; +}; + +FullMenuView.prototype.setItems = function(items) { + // if we have items already, save off their drawing area so we don't leave fragments at redraw + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.setItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.removeItem = function(index) { + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.removeItem.call(this, index); + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.focusNext = function() { + if (this.items.length - 1 === this.focusedItemIndex) { + this.clearPage(); + this.focusedItemIndex = 0; + this.currentPage = 0; + } + else { + this.focusedItemIndex++; + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } + } + + this.redraw(); + + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPrevious = function() { + if (0 === this.focusedItemIndex) { + this.clearPage(); + this.focusedItemIndex = this.items.length - 1; + this.currentPage = this.pages.length - 1; + } + else { + this.focusedItemIndex--; + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } + } + + this.redraw(); + + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusPreviousColumn = function() { + + const currentRow = this.items[this.focusedItemIndex].itemInRow; + this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; + if (this.focusedItemIndex < 0) { + this.clearPage(); + const lastItemRow = this.items[this.items.length - 1].itemInRow; + if (lastItemRow > currentRow) { + this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; + } + else { + // can't go to same column, so go to last item + this.focusedItemIndex = this.items.length - 1; + } + // set to last page + this.currentPage = this.pages.length - 1; + } + else { + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } + } + + this.redraw(); + + // TODO: This isn't specific to Previous, may want to replace in the future + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusNextColumn = function() { + + const currentRow = this.items[this.focusedItemIndex].itemInRow; + this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; + if (this.focusedItemIndex > this.items.length - 1) { + this.focusedItemIndex = currentRow - 1; + this.currentPage = 0; + this.clearPage(); + } + else if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } + + this.redraw(); + + // TODO: This isn't specific to Next, may want to replace in the future + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPreviousPageItem = function() { + + // handle first page + if (this.currentPage == 0) { + // Do nothing, page up shouldn't go down on last page + return; + } + + this.currentPage--; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); + + this.redraw(); + + return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); +}; + +FullMenuView.prototype.focusNextPageItem = function() { + + // handle last page + if (this.currentPage == this.pages.length - 1) { + // Do nothing, page up shouldn't go down on last page + return; + } + + this.currentPage++; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); + + this.redraw(); + + return FullMenuView.super_.prototype.focusNextPageItem.call(this); +}; + +FullMenuView.prototype.focusFirst = function() { + + this.currentPage = 0; + this.focusedItemIndex = 0; + this.clearPage(); + + this.redraw(); + return FullMenuView.super_.prototype.focusFirst.call(this); +}; + +FullMenuView.prototype.focusLast = function() { + + this.currentPage = this.pages.length - 1; + this.focusedItemIndex = this.pages[this.currentPage].end; + this.clearPage(); + + this.redraw(); + return FullMenuView.super_.prototype.focusLast.call(this); +}; + +FullMenuView.prototype.setFocusItems = function(items) { + FullMenuView.super_.prototype.setFocusItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setItemSpacing = function(itemSpacing) { + FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setJustify = function(justify) { + FullMenuView.super_.prototype.setJustify.call(this, justify); + this.positionCacheExpired = true; +}; + + +FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); + + this.positionCacheExpired = true; +}; diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index abd04cb1..c9163273 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -132,7 +132,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.text = this.text.substr(0, this.text.length - 1); this.clientBackspace(); } else { - while(this.patternArrayPos > 0) { + while(this.patternArrayPos >= 0) { if(_.isRegExp(this.patternArray[this.patternArrayPos])) { this.text = this.text.substr(0, this.text.length - 1); this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 037121e5..d6c37865 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView; const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const FullMenuView = require('./full_menu_view.js').FullMenuView; const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; @@ -27,7 +28,7 @@ function MCIViewFactory(client) { } MCIViewFactory.UserViewCodes = [ - 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', + 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE', // // XY is a special MCI code that allows finding positions @@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new HorizontalMenuView(options); break; + // Full Menu + case 'FM' : + setOption(0, 'itemSpacing'); + setOption(1, 'itemHorizSpacing'); + setOption(2, 'justify'); + setOption(3, 'textStyle'); + + setFocusOption(0, 'focusTextStyle'); + + view = new FullMenuView(options); + break; + case 'SM' : setOption(0, 'textStyle'); setOption(1, 'justify'); diff --git a/core/menu_module.js b/core/menu_module.js index 8d325378..64fd56b5 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -56,14 +56,14 @@ exports.MenuModule = class MenuModule extends PluginModule { initSequence() { const self = this; const mciData = {}; - let pausePosition; + let pausePosition = {row: 0, column: 0}; const hasArt = () => { return _.isString(self.menuConfig.art) || (Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs')); }; - async.series( + async.waterfall( [ function beforeArtInterrupt(callback) { return self.displayQueuedInterruptions(callback); @@ -73,7 +73,7 @@ exports.MenuModule = class MenuModule extends PluginModule { }, function displayMenuArt(callback) { if(!hasArt()) { - return callback(null); + return callback(null, null); } self.displayAsset( @@ -86,18 +86,15 @@ exports.MenuModule = class MenuModule extends PluginModule { mciData.menu = artData.mciMap; } - return callback(null); // any errors are non-fatal + if(artData) { + pausePosition.row = artData.height + 1; + } + + return callback(null, artData); // any errors are non-fatal } ); }, - function moveToPromptLocation(callback) { - if(self.menuConfig.prompt) { - // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements - } - - return callback(null); - }, - function displayPromptArt(callback) { + function displayPromptArt(artData, callback) { if(!_.isString(self.menuConfig.prompt)) { return callback(null); } @@ -106,41 +103,41 @@ exports.MenuModule = class MenuModule extends PluginModule { return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); } + const options = Object.assign({}, self.menuConfig.config); + + if(_.isNumber(artData?.height)) { + options.startRow = artData.height + 1; + } + self.displayAsset( self.menuConfig.promptConfig.art, - self.menuConfig.config, + options, (err, artData) => { if(artData) { mciData.prompt = artData.mciMap; + pausePosition.row = artData.height + 1; } + return callback(err); // pass err here; prompts *must* have art } ); }, - function recordCursorPosition(callback) { - if(!self.shouldPause()) { - return callback(null); // cursor position not needed - } - - self.client.once('cursor position report', pos => { - pausePosition = { row : pos[0], col : 1 }; - self.client.log.trace('After art position recorded', pausePosition ); - return callback(null); - }); - - self.client.term.rawWrite(ansi.queryPos()); - }, function afterArtDisplayed(callback) { return self.mciReady(mciData, callback); }, function displayPauseIfRequested(callback) { if(!self.shouldPause()) { - return callback(null); + return callback(null, null); + } + + if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) { + // If this scrolled, the prompt will go to the bottom of the screen + pausePosition.row = self.client.termHeight; } return self.pausePrompt(pausePosition, callback); }, - function finishAndNext(callback) { + function finishAndNext(artInfo, callback) { self.finishedLoading(); self.realTimeInterrupt = 'allowed'; return self.autoNextMenu(callback); @@ -512,7 +509,7 @@ exports.MenuModule = class MenuModule extends PluginModule { this.optionalMoveToPosition(position); - return theme.displayThemedPause(this.client, cb); + return theme.displayThemedPause(this.client, {position}, cb); } promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) { diff --git a/core/menu_view.js b/core/menu_view.js index d9016153..9c750aba 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -38,14 +38,14 @@ function MenuView(options) { this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization this.focusPrefix = options.focusPrefix || ''; this.focusSuffix = options.focusSuffix || ''; this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; this.hasFocusItems = function() { return !_.isUndefined(self.focusItems); @@ -68,6 +68,15 @@ function MenuView(options) { util.inherits(MenuView, View); +MenuView.prototype.setTextOverflow = function(overflow) { + this.textOverflow = overflow; + this.invalidateRenderCache(); +} + +MenuView.prototype.hasTextOverflow = function() { + return this.textOverflow != undefined; +} + MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { this.sorted = false; @@ -253,19 +262,32 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { this.positionCacheExpired = true; }; +MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + itemHorizSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); + + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; +}; + MenuView.prototype.setPropertyValue = function(propName, value) { switch(propName) { case 'itemSpacing' : this.setItemSpacing(value); break; + case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; case 'items' : this.setItems(value); break; case 'focusItems' : this.setFocusItems(value); break; case 'hotKeys' : this.setHotKeys(value); break; + case 'textOverflow' : this.setTextOverflow(value); break; case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; + case 'justify' : this.setJustify(value); break; + case 'fillChar' : this.setFillChar(value); break; case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'itemFormat' : case 'focusItemFormat' : this[propName] = value; + // if there is a cache currently, invalidate it + this.invalidateRenderCache(); break; case 'sort' : this.setSort(value); break; @@ -274,6 +296,17 @@ MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; +MenuView.prototype.setFillChar = function(fillChar) { + this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); + this.invalidateRenderCache(); +} + +MenuView.prototype.setJustify = function(justify) { + this.justify = justify; + this.invalidateRenderCache(); + this.positionCacheExpired = true; +} + MenuView.prototype.setHotKeys = function(hotKeys) { if(_.isObject(hotKeys)) { if(this.caseInsensitiveHotKeys) { diff --git a/core/message.js b/core/message.js index c5ad490b..e98baec2 100644 --- a/core/message.js +++ b/core/message.js @@ -790,7 +790,7 @@ module.exports = class Message { return ftnUtil.getQuotePrefix(this[source]); } - getTearLinePosition(input) { + static getTearLinePosition(input) { const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); return m ? m.index : -1; } @@ -886,12 +886,12 @@ module.exports = class Message { } ); } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */; const quoted = []; const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex // find *last* tearline - let tearLinePos = this.getTearLinePosition(input); + let tearLinePos = Message.getTearLinePosition(input); tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { @@ -910,7 +910,7 @@ module.exports = class Message { if(quoted.length > 0) { // - // Preserve paragraph seperation. + // Preserve paragraph separation. // // FSC-0032 states something about leaving blank lines fully blank // (without a prefix) but it seems nicer (and more consistent with other systems) diff --git a/core/mrc.js b/core/mrc.js index 1e0791dd..28e0e3c3 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -426,7 +426,8 @@ exports.getModule = class mrcModule extends MenuModule { switch (cmd[0]) { case 'pm': - this.processOutgoingMessage(cmd[2], cmd[1]); + const newmsg = cmd.slice(2).join(' '); + this.processOutgoingMessage(newmsg, cmd[1]); break; case 'rainbow': { diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 9f099b16..ce6cf10b 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -65,7 +65,7 @@ const _ = require('lodash'); const SPECIAL_KEY_MAP_DEFAULT = { 'line feed' : [ 'return' ], exit : [ 'esc' ], - backspace : [ 'backspace' ], + backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ delete : [ 'delete' ], tab : [ 'tab' ], up : [ 'up arrow' ], @@ -74,7 +74,7 @@ const SPECIAL_KEY_MAP_DEFAULT = { home : [ 'home' ], left : [ 'left arrow' ], right : [ 'right arrow' ], - 'delete line' : [ 'ctrl + y' ], + 'delete line' : [ 'ctrl + y', 'ctrl + u' ], // https://en.wikipedia.org/wiki/Backspace 'page up' : [ 'page up' ], 'page down' : [ 'page down' ], insert : [ 'insert', 'ctrl + v' ], @@ -265,11 +265,10 @@ function MultiLineEditTextView(options) { this.getRenderText = function(index) { let text = self.getVisibleText(index); - const remain = self.dimens.width - text.length; + const remain = self.dimens.width - strUtil.renderStringLength(text); if(remain > 0) { - text += ' '.repeat(remain + 1); - // text += new Array(remain + 1).join(' '); + text += ' '.repeat(remain);// + 1); } return text; diff --git a/core/onelinerz.js b/core/onelinerz.js index d840f731..679f5fec 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -135,7 +135,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { self.db.each( `SELECT * FROM ( - SELECT * + SELECT * FROM onelinerz ORDER BY timestamp DESC LIMIT ${limit} @@ -248,8 +248,9 @@ exports.getModule = class OnelinerzModule extends MenuModule { async.series( [ function openDatabase(callback) { + const dbSuffix = self.menuConfig.config.dbSuffix; self.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo), + getModDatabasePath(exports.moduleInfo, dbSuffix), err => { return callback(err); } @@ -260,7 +261,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { `CREATE TABLE IF NOT EXISTS onelinerz ( id INTEGER PRIMARY KEY, user_id INTEGER_NOT NULL, - user_name VARCHAR NOT NULL, + user_name VARCHAR NOT NULL, oneliner VARCHAR NOT NULL, timestamp DATETIME NOT NULL );` diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 308d3581..5531695d 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -153,6 +153,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { function updateTags(fe) { if(Array.isArray(options.tags)) { fe.hashTags = new Set(options.tags); + } else if (areaInfo.hashTags) { // no explicit tags; merge in defaults, if any + fe.hashTags = areaInfo.hashTags; } } @@ -227,7 +229,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { fullPath, { areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag + storageTag : storageLoc.storageTag, + hashTags : areaInfo.hashTags, }, (stepInfo, next) => { if(argv.verbose) { @@ -549,7 +552,7 @@ function scanFileAreas() { console.info(`Processing area "${areaInfo.name}":`); scanFileAreaForChanges(areaInfo, options, err => { - return callback(err); + return nextAreaTag(err); }); }, err => { return callback(err); diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 50bc3b5e..05025cfe 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -57,7 +57,7 @@ Actions: lock USERNAME Set a user's status to "locked" - group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + group USERNAME [+|~]GROUP Adds (+) or removes (~) user from a group list [FILTER] List users with optional FILTER. diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 403c5076..d3a13931 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -172,6 +172,7 @@ function removeUser(user) { message : [ 'user_message_area_last_read' ], system : [ 'user_event_log', ], user : [ 'user_group_member', 'user' ], + file : [ 'file_user_rating'] }; async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => { @@ -275,7 +276,7 @@ function modUserGroups(user) { let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" let action = groupName[0]; // + or - - if('-' === action || '+' === action) { + if('-' === action || '+' === action || '~' === action) { groupName = groupName.substr(1); } @@ -286,7 +287,7 @@ function modUserGroups(user) { } // - // Groups are currently arbritary, so do a slight validation + // Groups are currently arbitrary, so do a slight validation // if(!/[A-Za-z0-9]+/.test(groupName)) { process.exitCode = ExitCodes.BAD_ARGS; @@ -303,7 +304,7 @@ function modUserGroups(user) { } const UserGroup = require('../../core/user_group.js'); - if('-' === action) { + if('-' === action || '~' === action) { UserGroup.removeUserFromGroup(user.userId, groupName, done); } else { UserGroup.addUserToGroup(user.userId, groupName, done); diff --git a/core/qwk_mail_packet.js b/core/qwk_mail_packet.js index eab57eb7..26d7bef2 100644 --- a/core/qwk_mail_packet.js +++ b/core/qwk_mail_packet.js @@ -941,7 +941,7 @@ class QWKPacketWriter extends EventEmitter { } // First block is a space padded ID - const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2020 Bryan Ashby`; + const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2022 Bryan Ashby`; this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii'); this.currentMessageOffset = QWKMessageBlockSize; diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 4e51c889..a817bd22 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -81,14 +81,22 @@ exports.getModule = class GopherModule extends ServerModule { this.publicHostname = config.contentServers.gopher.publicHostname; this.publicPort = config.contentServers.gopher.publicPort; - this.addRoute(/^\/?\r\n$/, this.defaultGenerator); - this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); + this.addRoute(/^\/?msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); + this.addRoute(/^(\/?[^\t\r\n]*)\r\n$/, this.staticGenerator); this.server = net.createServer( socket => { socket.setEncoding('ascii'); socket.on('data', data => { - this.routeRequest(data, socket); + // sanitize a bit - bots like to inject garbage + data = data.replace(/[^ -~\t\r\n]/g, ''); + if (data) { + this.routeRequest(data, socket); + } else { + this.notFoundGenerator('**invalid selector**', res => { + return socket.end(`${res}`); + }); + } }); socket.on('error', err => { @@ -161,22 +169,56 @@ exports.getModule = class GopherModule extends ServerModule { return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; } - defaultGenerator(selectorMatch, cb) { - this.log.debug( { selector : selectorMatch[0] }, 'Serving default content'); + staticGenerator(selectorMatch, cb) { + this.log.debug( { selector : selectorMatch[1] || '(gophermap)' }, 'Serving static content'); - let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'gopher_banner.asc'); - bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); - fs.readFile(bannerFile, 'utf8', (err, banner) => { - if(err) { - return cb('You have reached an ENiGMA½ Gopher server!'); + const requestedPath = selectorMatch[1]; + let path = this.resolveContentPath(requestedPath); + if (!path) { + return cb('Not found'); + } + + fs.stat(path, (err, stats) => { + if (err) { + return cb('Not found'); } - banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join(''); - banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); - return cb(banner); + let isGopherMap = false; + if (stats.isDirectory()) { + path = paths.join(path, 'gophermap'); + isGopherMap = true; + } + + fs.readFile(path, isGopherMap ? 'utf8' : null, (err, content) => { + if (err) { + let content = 'You have reached an ENiGMA½ Gopher server!\r\n'; + content += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); + return cb(content); + } + + if (isGopherMap) { + // Convert any UNIX style LF's to DOS CRLF's + content = content.replace(/\r?\n/g, '\r\n'); + + // variable support + content = content + .replace(/{publicHostname}/g, this.publicHostname) + .replace(/{publicPort}/g, this.publicPort); + } + + return cb(content); + }); }); } + resolveContentPath(requestPath) { + const staticRoot = _.get(Config(), 'contentServers.gopher.staticRoot'); + const path = paths.resolve(staticRoot, `.${requestPath}`); + if (path.startsWith(staticRoot)) { + return path; + } + } + notFoundGenerator(selector, cb) { this.log.debug( { selector }, 'Serving not found content'); return cb('Not found'); diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 237c0994..7c1a263b 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -125,7 +125,7 @@ class NNTPServer extends NNTPServerBase { const config = Config(); this.groupCache = new LRU({ max : _.get(config, 'contentServers.nntp.cache.maxItems', 200), - maxAge : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s + ttl : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s }); } diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 04b9ccdd..70ede1c1 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -215,20 +215,22 @@ exports.getModule = class WebServerModule extends ServerModule { routeIndex(req, resp) { const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html'); - return this.returnStaticPage(filePath, resp); } routeStaticFile(req, resp) { const fileName = req.url.substr(req.url.indexOf('/', 1)); - const filePath = paths.join(Config().contentServers.web.staticRoot, fileName); - + const filePath = this.resolveStaticPath(fileName); return this.returnStaticPage(filePath, resp); } returnStaticPage(filePath, resp) { const self = this; + if (!filePath) { + return this.fileNotFound(resp); + } + fs.stat(filePath, (err, stats) => { if(err || !stats.isFile()) { return self.fileNotFound(resp); @@ -245,6 +247,14 @@ exports.getModule = class WebServerModule extends ServerModule { }); } + resolveStaticPath(requestPath) { + const staticRoot = _.get(Config(), 'contentServers.web.staticRoot'); + const path = paths.resolve(staticRoot, `.${requestPath}`); + if (path.startsWith(staticRoot)) { + return path; + } + } + routeTemplateFilePage(templatePath, preprocessCallback, resp) { const self = this; diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index ddb1ebfd..d1d8f19b 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -357,9 +357,11 @@ exports.getModule = class SSHServerModule extends LoginServerModule { // However, as of this writing, NetRunner and SyncTERM both // fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com) // - ssh2.Server.KEEPALIVE_INTERVAL = 0; + // See also #399 + // + ssh2.Server.KEEPALIVE_CLIENT_INTERVAL = 0; - this.server = ssh2.Server(serverConf); + this.server = new ssh2.Server(serverConf); this.server.on('connection', (conn, info) => { Log.info(info, 'New SSH connection'); this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 12a3ed36..b3e13a8b 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -103,14 +103,16 @@ class TelnetClient { case Options.NEW_ENVIRON : { this._logDebug( - { vars : command.optionData.vars, userVars : command.optionData.userVars }, + { vars : command.optionData.vars, uservars : command.optionData.uservars }, 'New environment received' ); // get a value from vars with fallback of user vars const getValue = (name) => { - return command.optionData.vars.find(nv => nv.name === name) || - command.optionData.userVars.find(nv => nv.name === name); + return command.optionData.vars && + (command.optionData.vars.find(nv => nv.name === name) || + command.optionData.uservars.find(nv => nv.name === name) + ); }; if ('unknown' === this.term.termType) { diff --git a/core/show_art.js b/core/show_art.js index 7e53ca60..4236760c 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -171,22 +171,15 @@ exports.getModule = class ShowArtModule extends MenuModule { return callback(err); } const mciData = { menu : artData.mciMap }; - return callback(null, mciData); + if(self.client.term.termHeight > 0 && artData.height > self.client.term.termHeight) { + // We must have scrolled, adjust the positioning for pause + artData.height = self.client.term.termHeight; + } + const pausePosition = { row: artData.height + 1, col: 1}; + return callback(null, mciData, pausePosition); } ); }, - function recordCursorPosition(mciData, callback) { - if(!options.pause) { - return callback(null, mciData, null); // cursor position not needed - } - - self.client.once('cursor position report', pos => { - const pausePosition = { row : pos[0], col : 1 }; - return callback(null, mciData, pausePosition); - }); - - self.client.term.rawWrite(ANSI.queryPos()); - }, function afterArtDisplayed(mciData, pausePosition, callback) { self.mciReady(mciData, err => { return callback(err, pausePosition); diff --git a/core/string_util.js b/core/string_util.js index 6b88ec40..673a128f 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -20,6 +20,7 @@ exports.stringFromNullTermBuffer = stringFromNullTermBuffer; exports.stringToNullTermBuffer = stringToNullTermBuffer; exports.renderSubstr = renderSubstr; exports.renderStringLength = renderStringLength; +exports.ansiRenderStringLength = ansiRenderStringLength; exports.formatByteSizeAbbr = formatByteSizeAbbr; exports.formatByteSize = formatByteSize; exports.formatCountAbbr = formatCountAbbr; @@ -297,7 +298,7 @@ function renderStringLength(s) { let len = 0; const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the rege; reset + re.lastIndex = 0; // we recycle the regex; reset // // Loop counting only literal (non-control) sequences @@ -312,7 +313,41 @@ function renderStringLength(s) { len += s.slice(pos, m.index).length; } - if('C' === m[3]) { // ESC[C is foward/right + if('C' === m[3]) { // ESC[C is forward/right + len += parseInt(m[2], 10) || 0; + } + } + } while(0 !== re.lastIndex); + + if(pos < s.length) { + len += s.slice(pos).length; + } + + return len; +} + +// Like renderStringLength() but ANSI only (no pipe codes accounted for) +function ansiRenderStringLength(s) { + let m; + let pos; + let len = 0; + + const re = ANSI.getFullMatchRegExp(); + + // + // Loop counting only literal (non-control) sequences + // paying special attention to ESC[C which means forward + // + do { + pos = re.lastIndex; + m = re.exec(s); + + if(m) { + if(m.index > pos) { + len += s.slice(pos, m.index).length; + } + + if('C' === m[3]) { // ESC[C is forward/right len += parseInt(m[2], 10) || 0; } } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index ba9bb699..75e14dae 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -26,6 +26,7 @@ exports.nextConf = nextConf; exports.prevArea = prevArea; exports.nextArea = nextArea; exports.sendForgotPasswordEmail = sendForgotPasswordEmail; +exports.optimizeDatabases = optimizeDatabases; const handleAuthFailures = (callingMenu, err, cb) => { // already logged in with this user? @@ -205,3 +206,25 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { return logoff(callingMenu, formData, extraArgs, cb); }); } + +function optimizeDatabases(callingMenu, formData, extraArgs, cb) { + const dbs = require('./database').dbs; + const client = callingMenu.client; + + client.term.write('\r\n\r\n'); + + Object.keys(dbs).forEach(dbName => { + client.log.info({ dbName }, 'Optimizing database'); + + client.term.write(`Optimizing ${dbName}. Please wait...\r\n`); + + // https://www.sqlite.org/pragma.html#pragma_optimize + dbs[dbName].run('PRAGMA optimize;', err => { + if (err) { + client.log.error({ error : err, dbName }, 'Error attempting to optimize database'); + } + }); + }); + + return callingMenu.prevMenu(cb); +} \ No newline at end of file diff --git a/core/text_view.js b/core/text_view.js index 2a5c93c5..cbecb54f 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -44,49 +44,6 @@ function TextView(options) { this.textMaskChar = options.textMaskChar; } - /* - this.drawText = function(s) { - - // - // |<- this.maxLength - // ABCDEFGHIJK - // |ABCDEFG| ^_ this.text.length - // ^-- this.dimens.width - // - let textToDraw = _.isString(this.textMaskChar) ? - new Array(s.length + 1).join(this.textMaskChar) : - stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - - if(textToDraw.length > this.dimens.width) { - if(this.hasFocus) { - if(this.horizScroll) { - textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length); - } - } else { - if(textToDraw.length > this.dimens.width) { - if(this.textOverflow && - this.dimens.width > this.textOverflow.length && - textToDraw.length - this.textOverflow.length >= this.textOverflow.length) - { - textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow; - } else { - textToDraw = textToDraw.substr(0, this.dimens.width); - } - } - } - } - - this.client.term.write(padStr( - textToDraw, - this.dimens.width + 1, - this.fillChar, - this.justify, - this.hasFocus ? this.getFocusSGR() : this.getSGR(), - this.getStyleSGR(1) || this.getSGR() - ), false); - }; -*/ - this.drawText = function(s) { // @@ -125,7 +82,7 @@ function TextView(options) { this.client.term.write( padStr( textToDraw, - this.dimens.width + 1, + this.dimens.width, renderedFillChar, //this.fillChar, this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), diff --git a/core/theme.js b/core/theme.js index f9cf5792..a511ba33 100644 --- a/core/theme.js +++ b/core/theme.js @@ -495,6 +495,7 @@ function displayPreparedArt(options, artInfo, cb) { sauce : artInfo.sauce, font : options.font, trailingLF : options.trailingLF, + startRow : options.startRow, }; art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); @@ -551,6 +552,7 @@ function displayThemedPrompt(name, client, options, cb) { if(options.clearScreen) { client.term.rawWrite(ansi.resetScreen()); + options.position = {row: 1, column: 1}; } // @@ -560,9 +562,9 @@ function displayThemedPrompt(name, client, options, cb) { // const dispOptions = Object.assign( {}, options, promptConfig.config ); // :TODO: We can use term detection to do nifty things like avoid this kind of kludge: - if(!options.clearScreen) { - dispOptions.font = 'not_really_a_font!'; // kludge :) - } + // if(!options.clearScreen) { + // dispOptions.font = 'not_really_a_font!'; // kludge :) + // } displayThemedAsset( promptConfig.art, @@ -583,12 +585,15 @@ function displayThemedPrompt(name, client, options, cb) { return callback(null, promptConfig, artInfo); } - client.once('cursor position report', pos => { - artInfo.startRow = pos[0] - artInfo.height; - return callback(null, promptConfig, artInfo); - }); + if(_.isNumber(options?.position?.row)) { + artInfo.startRow = options.position.row; + if(client.term.termHeight > 0 && artInfo.startRow + artInfo.height > client.term.termHeight) { + // in this case, we will have scrolled + artInfo.startRow = client.term.termHeight - artInfo.height; + } + } - client.term.rawWrite(ansi.queryPos()); + return callback(null, promptConfig, artInfo); }, function createMCIViews(promptConfig, artInfo, callback) { const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController; @@ -614,7 +619,9 @@ function displayThemedPrompt(name, client, options, cb) { }); }, function clearPauseArt(artInfo, assocViewController, callback) { - if(options.clearPrompt) { + // Only clear with height if clearPrompt is true and if we were able + // to determine the row + if(options.clearPrompt && artInfo.startRow) { if(artInfo.startRow && artInfo.height) { client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); diff --git a/core/upload.js b/core/upload.js index b451ac9a..7ab79c48 100644 --- a/core/upload.js +++ b/core/upload.js @@ -332,6 +332,7 @@ exports.getModule = class UploadModule extends MenuModule { const scanOpts = { areaTag : self.areaInfo.areaTag, storageTag : self.areaInfo.storageTags[0], + hashTags : self.areaInfo.hashTags, }; function handleScanStep(stepInfo, nextScanStep) { diff --git a/core/view.js b/core/view.js index fdf78916..1a44d830 100644 --- a/core/view.js +++ b/core/view.js @@ -17,7 +17,7 @@ exports.View = View; const VIEW_SPECIAL_KEY_MAP_DEFAULT = { accept : [ 'return' ], exit : [ 'esc' ], - backspace : [ 'backspace', 'del' ], + backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ del : [ 'del' ], next : [ 'tab' ], up : [ 'up arrow' ], @@ -154,7 +154,7 @@ View.prototype.setHeight = function(height) { View.prototype.setWidth = function(width) { width = parseInt(width) || 1; - width = Math.min(width, this.client.term.termWidth); + width = Math.min(width, this.client.term.termWidth - this.position.col); this.dimens.width = width; }; @@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) { case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocus(value); break; + case 'focus' : this.setFocusProperty(value); break; case 'text' : if('setText' in this) { @@ -252,10 +252,16 @@ View.prototype.redraw = function() { this.client.term.write(ansi.goto(this.position.row, this.position.col)); }; -View.prototype.setFocus = function(focused) { - enigAssert(this.acceptsFocus, 'View does not accept focus'); - +View.prototype.setFocusProperty = function(focused) { + // Either this should accept focus, or the focus should be false + enigAssert(this.acceptsFocus || !focused, 'View does not accept focus'); this.hasFocus = focused; +}; + +View.prototype.setFocus = function(focused) { + // Call separate method to differentiate between a value set as a + // property vs focus programmatically called. + this.setFocusProperty(focused); this.restoreCursor(); }; diff --git a/core/word_wrap.js b/core/word_wrap.js index 94773283..afeca1f8 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,7 +1,9 @@ /* jslint node: true */ 'use strict'; -const renderStringLength = require('./string_util.js').renderStringLength; +const { + ansiRenderStringLength, +} = require('./string_util'); // deps const assert = require('assert'); @@ -28,7 +30,7 @@ function wordWrapText(text, options) { //const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g'); // - // For a given word, match 0->options.width chars -- alwasy include a full trailing ESC + // For a given word, match 0->options.width chars -- always include a full trailing ESC // sequence if present! // // :TODO: Need to create ansi.getMatchRegex or something - this is used all over @@ -49,7 +51,7 @@ function wordWrapText(text, options) { function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { - renderLen = renderStringLength(w); + renderLen = ansiRenderStringLength(w); if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { @@ -70,7 +72,7 @@ function wordWrapText(text, options) { // // * Sublime Text 3 for example considers spaces after a word // part of said word. For example, "word " would be wraped - // in it's entirity. + // in it's entirety. // // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. // "\t" may resolve to " " and must fit within the space. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..1e5b1711 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,56 @@ +FROM node:12-buster-slim + +LABEL maintainer="dave@force9.org" + +ENV NVM_DIR /root/.nvm +ENV DEBIAN_FRONTEND noninteractive +COPY . /enigma-bbs + +# Do some installing! (and alot of cleaning up) keeping it in one step for less docker layers +# - if you need to debug i recommend to break the steps with individual RUNs) +RUN apt-get update \ + && apt-get install -y \ + git \ + curl \ + build-essential \ + python \ + python3 \ + libssl-dev \ + lrzsz \ + arj \ + lhasa \ + unrar-free \ + p7zip-full \ + && npm install -g pm2 \ + && cd /enigma-bbs && npm install --only=production \ + && pm2 start main.js \ + && mkdir -p /enigma-bbs-pre/art \ + && mkdir /enigma-bbs-pre/mods \ + && mkdir /enigma-bbs-pre/config \ + && cp -rp art/* ../enigma-bbs-pre/art/ \ + && cp -rp mods/* ../enigma-bbs-pre/mods/ \ + && cp -rp config/* ../enigma-bbs-pre/config/ \ + && apt-get remove build-essential python python3 libssl-dev git curl -y \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && apt-get clean + +# sexyz +COPY docker/bin/sexyz /usr/local/bin +RUN chmod +x /enigma-bbs/docker/bin/docker-entrypoint.sh + +# enigma storage mounts +VOLUME /enigma-bbs/art +VOLUME /enigma-bbs/config +VOLUME /enigma-bbs/db +VOLUME /enigma-bbs/filebase +VOLUME /enigma-bbs/logs +VOLUME /enigma-bbs/mods +VOLUME /mail + +# Enigma default port +EXPOSE 8888 + +WORKDIR /enigma-bbs + +ENTRYPOINT ["/enigma-bbs/docker/bin/docker-entrypoint.sh"] \ No newline at end of file diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh new file mode 100644 index 00000000..e96eeed9 --- /dev/null +++ b/docker/bin/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e + +# Set some vars +PRE_POPULATED_VOLUMES=("config" "mods" "art") # These are folders which contain runtime needed files, and need to be represented in the host +BBS_ROOT_DIR=/enigma-bbs # Install location +BBS_STAGING_PATH=/enigma-bbs-pre # Staging location for pre populated volumes (PRE_POPULATED_VOLUMES) +CONFIG_NAME=config.hjson # This is the default name, this script is intended for easy get-go - make changes as needed + +# Setup happens when there is no existing config file +if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then + for VOLUME in "${PRE_POPULATED_VOLUMES[@]}" + do + if [ -n "$(find "$BBS_ROOT_DIR/$VOLUME" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then + cp -rp $BBS_STAGING_PATH/$VOLUME/* $BBS_ROOT_DIR/$VOLUME/ + else + printf "WARN: skipped $BBS_ROOT_DIR/$VOLUME: Volume not empty or not a new setup; Files required to run ENiGMA 1/2 may be missing.\n Possible bad state\n" + printf "INFO: You have mounted folders with existing data - but no existing config json.\n\nPossible solutions:\n1. Make sure all volumes are set correctly specifically config volume... \n2. Check your configuration name if non-default\n\n\n" + fi + done + ./oputil.js config new +fi +if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then # Make sure once more, otherwise pm2-runtime will loop if missing the config + printf "ERROR: Missing configuration - ENiGMA 1/2 will not work. please run config\n" + + exit 1 +else + exec pm2-runtime main.js +fi diff --git a/docker/bin/sexyz b/docker/bin/sexyz new file mode 100755 index 00000000..36924822 Binary files /dev/null and b/docker/bin/sexyz differ diff --git a/docs/Gemfile b/docs/Gemfile index 1a0104dd..ec96435d 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -8,10 +8,10 @@ source "https://rubygems.org" # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! -gem "jekyll", "~> 3.7.0" +gem "jekyll", "~> 4.2.1" # This is the default theme for new Jekyll sites. You may change this to anything you like. -gem "hacker" +# gem "hacker" # If you want to use GitHub Pages, remove the "gem "jekyll"" above and # uncomment the line below. To upgrade, run `bundle update github-pages`. @@ -19,11 +19,12 @@ gem "hacker" # If you have any plugins, put them here! group :jekyll_plugins do - gem "jekyll-feed", "~> 0.6" - gem 'jekyll-seo-tag' - gem 'jekyll-theme-hacker' - gem 'jekyll-sitemap' - gem 'jemoji' + gem 'jekyll-seo-tag', '~> 2.7.1' + gem 'jekyll-theme-hacker', '~>0.2.0' + gem 'jekyll-sitemap', '~>1.4.0' + gem 'jemoji', '~>0.12.0' + gem 'jekyll-relative-links', '~>0.6.1' + gem 'jekyll-minifier' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 55838d2b..3cdf8097 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,101 +1,116 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.2.9) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + activesupport (7.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) colorator (1.1.0) - concurrent-ruby (1.0.5) - em-websocket (0.5.1) + concurrent-ruby (1.1.9) + cssminify2 (2.0.1) + em-websocket (0.5.3) eventmachine (>= 0.12.9) - http_parser.rb (~> 0.6.0) - eventmachine (1.2.5) - ffi (1.9.24) + http_parser.rb (~> 0) + eventmachine (1.2.7) + execjs (2.8.1) + ffi (1.15.5) forwardable-extended (2.6.0) - gemoji (3.0.0) - hacker (0.0.1) - html-pipeline (2.7.1) + gemoji (3.0.1) + html-pipeline (2.14.0) activesupport (>= 2) - nokogiri (>= 1.8.5) - http_parser.rb (0.6.0) - i18n (0.9.1) + nokogiri (>= 1.4) + htmlcompressor (0.4.0) + http_parser.rb (0.8.0) + i18n (1.9.1) concurrent-ruby (~> 1.0) - jekyll (3.7.4) + jekyll (4.2.1) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (~> 0.7) - jekyll-sass-converter (~> 1.0) + i18n (~> 1.0) + jekyll-sass-converter (~> 2.0) jekyll-watch (~> 2.0) - kramdown (~> 1.14) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (~> 0.3.3) + mercenary (~> 0.4.0) pathutil (~> 0.9) - rouge (>= 1.7, < 4) + rouge (~> 3.0) safe_yaml (~> 1.0) - jekyll-feed (0.9.2) - jekyll (~> 3.3) - jekyll-sass-converter (1.5.1) - sass (~> 3.4) - jekyll-seo-tag (2.4.0) - jekyll (~> 3.3) - jekyll-sitemap (1.1.1) - jekyll (~> 3.3) - jekyll-theme-hacker (0.1.0) - jekyll (~> 3.5) + terminal-table (~> 2.0) + jekyll-minifier (0.1.10) + cssminify2 (~> 2.0) + htmlcompressor (~> 0.4) + jekyll (>= 3.5) + json-minify (~> 0.0.3) + uglifier (~> 4.1) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (2.1.0) + sassc (> 2.0.1, < 3.0) + jekyll-seo-tag (2.7.1) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-theme-hacker (0.2.0) + jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) - jekyll-watch (2.0.0) + jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.8.1) - activesupport (~> 4.0, >= 4.2.9) + jemoji (0.12.0) gemoji (~> 3.0) html-pipeline (~> 2.2) - jekyll (>= 3.0) - kramdown (1.16.2) - liquid (4.0.0) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - mercenary (0.3.6) - mini_portile2 (2.4.0) - minitest (5.11.1) - nokogiri (1.10.8) - mini_portile2 (~> 2.4.0) - pathutil (0.16.1) + jekyll (>= 3.0, < 5.0) + json (2.6.1) + json-minify (0.0.3) + json (> 0) + kramdown (2.3.1) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.3) + listen (3.7.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + minitest (5.15.0) + nokogiri (1.13.1-x86_64-linux) + racc (~> 1.4) + pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (3.0.1) - rb-fsevent (0.10.2) - rb-inotify (0.9.10) - ffi (>= 1.9.24, < 2) - rouge (3.1.0) - ruby_dep (1.5.0) - safe_yaml (1.0.4) - sass (3.5.5) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - thread_safe (0.3.6) - tzinfo (1.2.4) - thread_safe (~> 0.1) + public_suffix (4.0.6) + racc (1.6.0) + rb-fsevent (0.11.0) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.5) + rouge (3.28.0) + safe_yaml (1.0.5) + sassc (2.4.0) + ffi (~> 1.9) + terminal-table (2.0.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + uglifier (4.2.0) + execjs (>= 0.3.0, < 3) + unicode-display_width (1.8.0) PLATFORMS - ruby + x86_64-linux DEPENDENCIES - hacker - jekyll (~> 3.7.0) - jekyll-feed (~> 0.6) - jekyll-seo-tag - jekyll-sitemap - jekyll-theme-hacker - jemoji + jekyll (~> 4.2.1) + jekyll-minifier + jekyll-relative-links (~> 0.6.1) + jekyll-seo-tag (~> 2.7.1) + jekyll-sitemap (~> 1.4.0) + jekyll-theme-hacker (~> 0.2.0) + jemoji (~> 0.12.0) tzinfo-data BUNDLED WITH - 1.16.1 + 2.3.5 diff --git a/docs/_config.yml b/docs/_config.yml index c41690bf..244f6ca4 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -9,13 +9,17 @@ logo: /assets/images/enigma-logo.png markdown: kramdown theme: jekyll-theme-hacker plugins: - - jekyll-feed - jekyll-seo-tag + - jekyll-relative-links - jekyll-sitemap - jemoji baseurl: /enigma-bbs +relative_links: + enabled: true + collections: true + # Exclude from processing. # The following items will not be processed, by default. Create a custom list # to override the default setting. @@ -28,3 +32,102 @@ exclude: - vendor/gems/ - vendor/ruby/ - .idea + + +# New documents that are not included below under order will display at the +# end of the list. Section names for directories and subdirectories are +# setup in _data/sections.yml. Change there in order to update the name of +# one of the subdirectories or to add a new one. + + +collections: + docs: + output: true + permalink: /:path:output_ext + order: + - installation/installation-methods.md + - installation/install-script.md + - installation/docker.md + - installation/manual.md + - installation/hardware/rpi.md + - installation/hardware/windows.md + - installation/network.md + - installation/testing.md + - installation/production.md + - configuration/creating-config.md + - configuration/sysop-setup.md + - configuration/config-files.md + - configuration/config-hjson.md + - configuration/hjson.md + - configuration/menu-hjson.md + - configuration/directory-structure.md + - configuration/external-binaries.md + - configuration/archivers.md + - configuration/file-transfer-protocols.md + - configuration/email.md + - configuration/colour-codes.md + - configuration/event-scheduler.md + - configuration/acs.md + - configuration/security.md + - misc/user-interrupt.md + - filebase/index.md + - filebase/first-file-area.md + - filebase/acs.md + - filebase/uploads.md + - filebase/web-access.md + - filebase/tic-support.md + - filebase/network-mounts-and-symlinks.md + - messageareas/configuring-a-message-area.md + - messageareas/message-networks.md + - messageareas/bso-import-export.md + - messageareas/netmail.md + - messageareas/qwk.md + - messageareas/ftn.md + - art/general.md + - art/themes.md + - art/mci.md + - art/views/button_view.md + - art/views/edit_text_view.md + - art/views/full_menu_view.md + - art/views/horizontal_menu_view.md + - art/views/mask_edit_text_view.md + - art/views/multi_line_edit_text_view.md + - art/views/predefined_label_view.md + - art/views/spinner_menu_view.md + - art/views/text_view.md + - art/views/toggle_menu_view.md + - art/views/vertical_menu_view.md + - servers/loginservers/telnet.md + - servers/loginservers/ssh.md + - servers/loginservers/websocket.md + - servers/contentservers/web-server.md + - servers/contentservers/gopher.md + - servers/contentservers/nntp.md + - modding/local-doors.md + - modding/door-servers.md + - modding/telnet-bridge.md + - modding/existing-mods.md + - modding/file-area-list.md + - modding/last-callers.md + - modding/whos-online.md + - modding/user-list.md + - modding/msg-conf-list.md + - modding/msg-area-list.md + - modding/bbs-list.md + - modding/rumorz.md + - modding/file-transfer-protocol-select.md + - modding/onelinerz.md + - modding/show-art.md + - modding/file-base-download-manager.md + - modding/file-base-web-download-manager.md + - modding/set-newscan-date.md + - modding/node-msg.md + - modding/top-x.md + - modding/user-2fa-otp-config.md + - modding/autosig-edit.md + - modding/menu-modules.md + - admin/administration.md + - admin/oputil.md + - admin/updating.md + - troubleshooting/monitoring-logs.md + diff --git a/docs/_data/sections.yml b/docs/_data/sections.yml new file mode 100644 index 00000000..994698e9 --- /dev/null +++ b/docs/_data/sections.yml @@ -0,0 +1,28 @@ +installation: + title: Installation +configuration: + title: Configuration +filebase: + title: File Base +messageareas: + title: Message Areas +art: + title: Art +servers: + title: Servers +modding: + title: Modding +admin: + title: Administration +troubleshooting: + title: Troubleshooting +misc: + title: Miscellaneous +views: + title: Views +hardware: + title: OS / Hardware Specific +loginservers: + title: Login Servers +contentservers: + title: Content Servers diff --git a/docs/admin/administration.md b/docs/_docs/admin/administration.md similarity index 99% rename from docs/admin/administration.md rename to docs/_docs/admin/administration.md index 0b960246..b5dc7f0c 100644 --- a/docs/admin/administration.md +++ b/docs/_docs/admin/administration.md @@ -40,4 +40,4 @@ SQLite database files become less performant over time and waste space. It is re Example: ```bash sqlite3 ./db/message.sqlite3 "vacuum;" -``` \ No newline at end of file +``` diff --git a/docs/admin/oputil.md b/docs/_docs/admin/oputil.md similarity index 97% rename from docs/admin/oputil.md rename to docs/_docs/admin/oputil.md index 49e7039c..b2ee2892 100644 --- a/docs/admin/oputil.md +++ b/docs/_docs/admin/oputil.md @@ -76,7 +76,16 @@ Actions: lock USERNAME Set a user's status to "locked" - group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + group USERNAME [+|~]GROUP Adds (+) or removes (~) user from a group + + list [FILTER] List users with optional FILTER. + + Valid filters: + all : All users (default). + disabled : Disabled users. + inactive : Inactive users. + active : Active (regular) users. + locked : Locked users. info arguments: --security Include security information in output @@ -104,7 +113,7 @@ info arguments: | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | | `lock` | Locks the user account (prevents logins) | `./oputil.js user lock joeuser` | N/A | -| `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser -derp` | N/A | +| `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser ~derp` | N/A | #### Manage 2FA/OTP While `oputil.js` can be used to manage a user's 2FA/OTP, it is highly recommended to require users to opt-in themselves. See [Security](../configuration/security.md) for details. diff --git a/docs/admin/updating.md b/docs/_docs/admin/updating.md similarity index 100% rename from docs/admin/updating.md rename to docs/_docs/admin/updating.md diff --git a/docs/art/general.md b/docs/_docs/art/general.md similarity index 100% rename from docs/art/general.md rename to docs/_docs/art/general.md diff --git a/docs/art/mci.md b/docs/_docs/art/mci.md similarity index 93% rename from docs/art/mci.md rename to docs/_docs/art/mci.md index 235a92a6..0dd1e5eb 100644 --- a/docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -126,15 +126,17 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | Code | Name | Description | Notes | |------|----------------------|------------------|-------| -| `TL` | Text Label | Displays text | Static content | -| `ET` | Edit Text | Collect user input | Single line entry | -| `ME` | Masked Edit Text | Collect user input using a *mask* | See **Mask Edits** below | -| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. | -| `BT` | Button | A button | ...it's a button | -| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists | -| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar | -| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options | -| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input | +| `TL` | Text Label | Displays text | Static content. See [Text View](views/text_view.md) | +| `ET` | Edit Text | Collect user input | Single line entry. See [Edit Text](views/edit_text_view.md) | +| `ME` | Masked Edit Text | Collect user input using a *mask* | See [Masked Edit](views/mask_edit_text_view.md) and **Mask Edits** below. | +| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. See [Multiline Text Edit](views/multi_line_edit_text_view.md) | +| `BT` | Button | A button | ...it's a button. See [Button](views/button_view.md) | +| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists. See [Vertical Menu](views/vertical_menu_view.md) | +| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar. See [Horizontal Menu](views/horizontal_menu_view.md) | +| `FM` | Full Menu | A menu that can go both vertical and horizontal. | See [Full Menu](views/full_menu_view.md) | +| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options. See [Spinner Menu](views/spinner_menu_view.md) | +| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input. See [Toggle Menu](views/toggle_menu_view.md)| +| `PL` | Predefined Label | Show environment information | See [Predefined Label](views/predefined_label_view.md)| | `KE` | Key Entry | A *single* key input control | Think hotkeys | :information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. @@ -243,4 +245,4 @@ Suppose a format object contains the following elements: `userName` and `affils` ![Example](../assets/images/text-format-example1.png "Text Format") -:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". \ No newline at end of file +:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". diff --git a/docs/art/themes.md b/docs/_docs/art/themes.md similarity index 100% rename from docs/art/themes.md rename to docs/_docs/art/themes.md diff --git a/docs/_docs/art/views/button_view.md b/docs/_docs/art/views/button_view.md new file mode 100644 index 00000000..65a753ca --- /dev/null +++ b/docs/_docs/art/views/button_view.md @@ -0,0 +1,57 @@ +--- +layout: page +title: Button View +--- +## Button View +A button view supports displaying a button on a screen. + +## General Information + +:information_source: A button view is defined with a percent (%) and the characters BT, followed by the view number. For example: `%BT1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `text` | Sets the text to display on the button | +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)| +| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `fillChar` | Specifies a character to fill extra space longer than the text length. Defaults to an empty space | +| `textOverflow` | If the button text cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below | + +### Text Overflow + +The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. + +:information_source: If `textOverflow` is not specified at all, a button can become wider than the `width` if needed to display the text value. + +:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` + +## Example + +![Example](../../assets/images/button_view_example1.gif "Button") + +
+Configuration fragment (expand to view) +
+``` +BT1: { + submit: true + justify: center + argName: btnSelect + width: 17 + focusTextStyle: upper + text: Centered button +} +``` +
+
diff --git a/docs/_docs/art/views/edit_text_view.md b/docs/_docs/art/views/edit_text_view.md new file mode 100644 index 00000000..c372246d --- /dev/null +++ b/docs/_docs/art/views/edit_text_view.md @@ -0,0 +1,42 @@ +--- +layout: page +title: Edit Text View +--- +## Edit Text View +An edit text view supports editing form values on a screen. This can be for new entry as well as editing existing values defined by the module. + +## General Information + +:information_source: An edit text view is defined with a percent (%) and the characters ET, followed by the view number. For example: `%ET1`. This is generally used on a form in order to allow a user to enter or edit a text value. + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets the focus text style. See **Text Styles** in [MCI](../mci.md) | +| `width` | Sets the width of a view for the text edit (default 15)| +| `argName` | Sets the argument name for this value in the form | +| `maxLength` | Sets the maximum number of characters that can be entered | +| `focus` | Set to true to capture initial focus | +| `justify` | Sets the justification of the text entry. Options: left (default), right, center | +| `fillChar` | Specifies a character to fill extra space in the text entry with. Defaults to an empty space | + +## Example + +![Example](../../assets/images/edit_text_view_example1.gif "Edit Text View") + +
+Configuration fragment (expand to view) +
+``` +ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true +} +``` +
+
diff --git a/docs/_docs/art/views/full_menu_view.md b/docs/_docs/art/views/full_menu_view.md new file mode 100644 index 00000000..19ff365a --- /dev/null +++ b/docs/_docs/art/views/full_menu_view.md @@ -0,0 +1,240 @@ +--- +layout: page +title: Full Menu View +--- +## Full Menu View +A full menu view supports displaying a list of times on a screen in a very configurable manner. A full menu view supports either a single row or column of values, similar to Horizontal Menu (HM) and Vertical Menu (VM), or in multiple columns. + +## General Information + +Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A full menu view is defined with a percent (%) and the characters FM, followed by the view number. For example: `%FM1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)| +| `itemSpacing` | Used to separate items vertically in the menu | +| `itemHorizSpacing` | Used to separate items horizontally in the menu | +| `height` | Sets the height of views to display multiple items vertically (default 1) | +| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `textOverflow` | If a single column cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below | +| `items` | List of items to show in the menu. See **Items** below. +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, C: 2, D: 3 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A full menu, similar to other menus, take a list of items to display in the menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list: + +``` +["First item", "Second item", "Third Item"] +``` + +### Text Overflow + +The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. Note, because columns are automatically calculated, this can only occur when the text is too long to fit the `width` using a single column. + +:information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column. + +:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` + +## Examples + +### A simple vertical menu - similar to VM + +![Example](../../assets/images/full_menu_view_example1.gif "Vertical menu") + +
+Configuration fragment (expand to view) +
+``` +FM1: { + submit: true + argName: navSelect + width: 1 + items: [ + { + text: login + data: login + } + { + text: apply + data: new user + } + { + text: about + data: about + } + { + text: log off + data: logoff + } + ] +} + +``` +
+
+ +### A simple horizontal menu - similar to HM + +![Example](../../assets/images/full_menu_view_example2.gif "Horizontal menu") + +
+Configuration fragment (expand to view) +
+``` +FM2: { + focus: true + height: 1 + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` +
+
+ +### A multi-column navigation menu with hotkeys + + +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") + +
+Configuration fragment (expand to view) +
+``` +FM1: { + focus: true + height: 6 + width: 60 + submit: true + argName: navSelect + hotKeys: { M: 0, E: 1, D: 2 ,F: 3,!: 4, A: 5, C: 6, Y: 7, S: 8, R: 9, O: 10, L:11, U:12, W: 13, B:14, G:15, T: 16, Q:17 } + hotKeySubmit: true + items: [ + { + text: M) message area + data: message + } + { + text: E) private email + data: email + } + { + text: D) doors + data: doors + } + { + text: F) file base + data: files + } + { + text: !) global newscan + data: newscan + } + { + text: A) achievements + data: achievements + } + { + text: C) configuration + data: config + } + { + text: Y) user stats + data: userstats + } + { + text: S) system stats + data: systemstats + } + { + text: R) rumorz + data: rumorz + } + { + text: O) onelinerz + data: onelinerz + } + { + text: L) last callers + data: callers + } + { + text: U) user list + data: userlist + } + { + text: W) whos online + data: who + } + { + text: B) bbs list + data: bbslist + } + { + text: G) node-to-node messages + data: nodemessages + } + { + text: T) multi relay chat + data: mrc + } + { + text: Q) quit + data: quit + } + ] +} +``` +
+
+ diff --git a/docs/_docs/art/views/horizontal_menu_view.md b/docs/_docs/art/views/horizontal_menu_view.md new file mode 100644 index 00000000..90dc4438 --- /dev/null +++ b/docs/_docs/art/views/horizontal_menu_view.md @@ -0,0 +1,91 @@ +--- +layout: page +title: Horizontal Menu View +--- +## Horizontal Menu View +A horizontal menu view supports displaying a list of times on a screen horizontally (side to side, in a single row) similar to a lightbox. + +## General Information + +Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A horizontal menu view is defined with a percent (%) and the characters HM, followed by the view number (if used.) For example: `%HM1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)| +| `itemSpacing` | Used to separate items horizontally in the menu | +| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `items` | List of items to show in the menu. See **Items** below. +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, C: 2, D: 3 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A horizontal menu, similar to other menus, take a list of items to display in the menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list: + +``` +["First item", "Second item", "Third Item"] +``` + +## Example + +![Example](../../assets/images/horizontal_menu_view_example1.gif "Horizontal menu") + +
+Configuration fragment (expand to view) +
+``` +HM2: { + focus: true + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` +
+
diff --git a/docs/_docs/art/views/mask_edit_text_view.md b/docs/_docs/art/views/mask_edit_text_view.md new file mode 100644 index 00000000..a03e83c5 --- /dev/null +++ b/docs/_docs/art/views/mask_edit_text_view.md @@ -0,0 +1,64 @@ +--- +layout: page +title: Mask Edit Text View +--- +## Mask Edit Text View +A mask edit text view supports editing form values on a screen. This can be for new entry as well as editing existing values. Unlike a edit text view, the mask edit text view uses a mask pattern to specify what format the values should be entered in. + +## General Information + +:information_source: A mask edit text view is defined with a percent (%) and the characters ME, followed by the view number. For example: `%ME1`. This is generally used on a form in order to allow a user to enter or edit a text value. + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets the focus text style. See **Text Styles** in [MCI](../mci.md) | +| `argName` | Sets the argument name for this value in the form | +| `maxLength` | Sets the maximum number of characters that can be entered. *Not normally useful, set the mask pattern as needed instead* | +| `focus` | Set to true to capture initial focus | +| `maskPattern` | Sets the mask pattern. See **Mask Pattern** below | +| `fillChar` | Specifies a character to fill extra space in the text entry with. Defaults to an empty space | + +### Mask Pattern + +A `maskPattern` must be set on a mask edit text view (not doing so will cause the view to be focusable, but no text can be input). The `maskPattern` is a set of characters used to define input, as well as optional literal characters that can be entered into the pattern that will always be entered into the input. The following mask characters are supported: + +| Mask Character | Description | +|----------------|--------------| +| # | Numeric input, one of 0 through 9 | +| A | Alphabetic, one of a through z or A through Z | +| @ | Alphanumeric, matches one of either Numeric or Alphabetic above | +| & | Printable, matches one printable character including spaces | + +Any value other than the entries above is treated like a literal value to be displayed in the patter. Multiple pattern characters are combined for longer inputs. Some examples could include: + +| Pattern | Description | +|---------|--------------| +| `AA` | Matches up to two alphabetic characters, for example a state name (i.e. "CA") | +| `###` | Matches up to three numeric characters, for example an age (i.e. 25) | +| `###-###-####` | A pattern matching a phone number with area code | +| `##/##/####` | Matches a date of type month/day/year or day/month/year (i.e. 01/01/2000) | +| `##-AAA-####` | Matches a date of type day-month-year (i.e. 01-MAR-2010) | +| `# foot ## inches`| Matches a height in feet and inches (i.e. 6 foot 2 inches) | + + +## Example + +![Example](../../assets/images/mask_edit_text_view_example1.gif "Masked Text Edit View") + +
+Configuration fragment (expand to view) +
+``` +ME1: { + argName: height + fillChar: "#" + maskPattern: "# ft. ## in." +} +``` +
+
diff --git a/docs/_docs/art/views/multi_line_edit_text_view.md b/docs/_docs/art/views/multi_line_edit_text_view.md new file mode 100644 index 00000000..870360ba --- /dev/null +++ b/docs/_docs/art/views/multi_line_edit_text_view.md @@ -0,0 +1,53 @@ +--- +layout: page +title: Multi Line Edit Text View +--- +## Multi Line Edit Text View +A text display / editor designed to edit or display a message. + +## General Information + +:information_source: A multi line edit text view is defined with a percent (%) and the characters MT, followed by the view number. For example: `%MT1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `text` | Sets the text to display - only useful for read-only and preview, otherwise use a specific module | +| `width` | Sets the width of a view to display horizontally (default 15) | +| `height` | Sets the height of a view to display vertically | +| `argName` | Sets the argument name for the form | +| `mode` | One of edit, preview, or read-only. See **Mode** below | + +### Mode + +The mode of a multi line edit text view controls how the view behaves. The following modes are allowed: + +| Mode | Description | +|-------------|--------------| +| edit | edit the contents of the view | +| preview | preview the text, including scrolling | +| read-only | No scrolling or editing the view | + +:information_source: If `mode` is not set, the default mode is "edit" + +:information_source: With mode preview, scrolling the contents is allowed, but is not with read-only. + +## Example + +![Example](../../assets/images/multi_line_edit_text_view_example1.gif "Multi Line Edit Text View") + +
+Configuration fragment (expand to view) +
+``` +ML1: { + width: 79 + argName: message + mode: edit +} +``` +
+
diff --git a/docs/_docs/art/views/predefined_label_view.md b/docs/_docs/art/views/predefined_label_view.md new file mode 100644 index 00000000..cae23f55 --- /dev/null +++ b/docs/_docs/art/views/predefined_label_view.md @@ -0,0 +1,49 @@ +--- +layout: page +title: Predefined Label View +--- +## Predefined Label View +A predefined label view supports displaying a predefined MCI label on a screen. + +## General Information + +:information_source: A predefined label view is defined with a percent (%) and the characters PL, followed by the view number and then the predefined MCI value in parenthesis. For example: `%PL1(VL)` to display the Version Label. *NOTE*: this is an alternate way of placing MCI codes, as the MCI can also be placed on the art page directly with the code. For example `%VL`. The difference between these is that the PL version can have additional formatting options applied to it. + +:information_source: See *Predefined Codes* in [MCI](../mci.md) for the list of available MCI codes. + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `justify` | Sets the justification of the MCI value text. Options: left (default), right, center | +| `fillChar` | Specifies a character to fill extra space in the view. Defaults to an empty space | +| `width` | Specifies the width that the value should be displayed in (default 3) | +| `textOverflow` | If the MCI is wider than width, set overflow characters. See **Text Overflow** below | + +### Text Overflow + +The `textOverflow` option is used to specify what happens when a predefined MCI string is too long to fit in the `width` defined. + +:information_source: If `textOverflow` is not specified at all, a predefined label view can become wider than the `width` if needed to display the MCI value. + +:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` + +## Example + +![Example](../../assets/images/predefined_label_view_example1.png "Predefined label") + +
+Configuration fragment (expand to view) +
+``` +PL1: { + textStyle: upper +} +``` +
+
diff --git a/docs/_docs/art/views/spinner_menu_view.md b/docs/_docs/art/views/spinner_menu_view.md new file mode 100644 index 00000000..0f7139f8 --- /dev/null +++ b/docs/_docs/art/views/spinner_menu_view.md @@ -0,0 +1,104 @@ +--- +layout: page +title: Spinner Menu View +--- +## Spinner Menu View +A spinner menu view supports displaying a set of times on a screen as a list, with one item displayed at a time. This is generally used to pick one option from a list. Some examples could include selecting from a list of states, themes, etc. + +## General Information + +Items can be selected on a menu via the cursor keys or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A spinner menu view is defined with a percent (%) and the characters SM, followed by the view number (if used.) For example: `%SM1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)| +| `focus` | If set to `true`, establishes initial focus | +| `width` | Sets the width of a view on the display (default 15)| +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `items` | List of items to show in the menu. See **Items** below. +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, C: 2, D: 3 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A spinner menu, similar to other menus, take a list of items to display in the menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list: + +``` +["First item", "Second item", "Third Item"] +``` + +## Example + +![Example](../../assets/images/spinner_menu_view_example1.gif "Spinner menu") + +
+Configuration fragment (expand to view) +
+``` +SM1: { + submit: true + argName: themeSelect + items: [ + { + text: Light + data: light + } + { + text: Dark + data: dark + } + { + text: Rainbow + data: rainbow + } + { + text: Gruvbox + data: gruvbox + } + ] +} + +``` +
+
diff --git a/docs/_docs/art/views/text_view.md b/docs/_docs/art/views/text_view.md new file mode 100644 index 00000000..3bec8ed8 --- /dev/null +++ b/docs/_docs/art/views/text_view.md @@ -0,0 +1,48 @@ +--- +layout: page +title: Text View +--- +## Text View +A text label view supports displaying simple text on a screen. + +## General Information + +:information_source: A text label view is defined with a percent (%) and the characters TL, followed by the view number. For example: `%TL1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `text` | Sets the text to display on the label | +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `width` | Sets the width of a view to display horizontally (default 15)| +| `justify` | Sets the justification of the text in the view. Options: left (default), right, center | +| `fillChar` | Specifies a character to fill extra space in the view with. Defaults to an empty space | +| `textOverflow` | Set overflow characters to display in case the text length is less than the width. See **Text Overflow** below | + +### Text Overflow + +The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. + +:information_source: If `textOverflow` is not specified at all, a text label can become wider than the `width` if needed to display the text value. + +:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` + +## Example + +![Example](../../assets/images/text_label_view_example1.png "Text label") + +
+Configuration fragment (expand to view) +
+``` +TL1: { + text: Text label +} +``` +
+
diff --git a/docs/_docs/art/views/toggle_menu_view.md b/docs/_docs/art/views/toggle_menu_view.md new file mode 100644 index 00000000..65c1eabd --- /dev/null +++ b/docs/_docs/art/views/toggle_menu_view.md @@ -0,0 +1,83 @@ +--- +layout: page +title: Toggle Menu View +--- +## Toggle Menu View +A toggle menu view supports displaying a list of options on a screen horizontally (side to side, in a single row) similar to a [Horizontal Menu](horizontal_menu_view.md). It is designed to present one of two choices easily. + +## General Information + +Items can be selected on a menu via the left and right cursor keys, or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A toggle menu view is defined with a percent (%) and the characters TM, followed by the view number (if used.) For example: `%TM1` + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)| +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `items` | List of items to show in the menu. Must include exactly two (2) items. See **Items** below. | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, Q: 1 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A toggle menu, similar to other menus, take a list of items to display in the menu. Unlike other menus, however, there must be exactly two items in a toggle menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list: + +``` +["First item", "Second item"] +``` + +## Example + +![Example](../../assets/images/toggle_menu_view_example1.gif "Toggle menu") + +
+Configuration fragment (expand to view) +
+``` +TM2: { + focus: true + submit: true + argName: navSelect + focusTextStyle: upper + items: [ "yes", "no" ] +} +``` +
+
diff --git a/docs/_docs/art/views/vertical_menu_view.md b/docs/_docs/art/views/vertical_menu_view.md new file mode 100644 index 00000000..e46f92ae --- /dev/null +++ b/docs/_docs/art/views/vertical_menu_view.md @@ -0,0 +1,106 @@ +--- +layout: page +title: Vertical Menu View +--- +## Vertical Menu View +A vertical menu view supports displaying a list of times on a screen vertically in a single column, similar to a lightbar. This type of control is often useful for lists of items or menu controls. + +## General Information + +Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A vertical menu view is defined with a percent (%) and the characters VM, followed by the view number (if used.) For example: `%VM1`. + +:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)| +| `itemSpacing` | Used to separate items vertically in the menu | +| `height` | Sets the height of views to display multiple items vertically (default 1) | +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `items` | List of items to show in the menu. See **Items** below. +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, C: 2, D: 3 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A vertical menu, similar to other menus, take a list of items to display in the menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list: + +``` +["First item", "Second item", "Third Item"] +``` + + +## Example + +![Example](../../assets/images/vertical_menu_view_example1.gif "Vertical menu") + +
+Configuration fragment (expand to view) +
+``` +VM1: { + submit: true + argName: navSelect + items: [ + { + text: login + data: login + } + { + text: apply + data: new user + } + { + text: about + data: about + } + { + text: log off + data: logoff + } + ] +} + +``` +
+
diff --git a/docs/configuration/acs.md b/docs/_docs/configuration/acs.md similarity index 100% rename from docs/configuration/acs.md rename to docs/_docs/configuration/acs.md diff --git a/docs/configuration/archivers.md b/docs/_docs/configuration/archivers.md similarity index 100% rename from docs/configuration/archivers.md rename to docs/_docs/configuration/archivers.md diff --git a/docs/configuration/colour-codes.md b/docs/_docs/configuration/colour-codes.md similarity index 100% rename from docs/configuration/colour-codes.md rename to docs/_docs/configuration/colour-codes.md diff --git a/docs/configuration/config-files.md b/docs/_docs/configuration/config-files.md similarity index 100% rename from docs/configuration/config-files.md rename to docs/_docs/configuration/config-files.md diff --git a/docs/configuration/config-hjson.md b/docs/_docs/configuration/config-hjson.md similarity index 100% rename from docs/configuration/config-hjson.md rename to docs/_docs/configuration/config-hjson.md diff --git a/docs/configuration/creating-config.md b/docs/_docs/configuration/creating-config.md similarity index 100% rename from docs/configuration/creating-config.md rename to docs/_docs/configuration/creating-config.md diff --git a/docs/configuration/directory-structure.md b/docs/_docs/configuration/directory-structure.md similarity index 69% rename from docs/configuration/directory-structure.md rename to docs/_docs/configuration/directory-structure.md index 4060991c..8f318ac4 100644 --- a/docs/configuration/directory-structure.md +++ b/docs/_docs/configuration/directory-structure.md @@ -6,17 +6,17 @@ All paths mentioned here are relative to the ENiGMA½ checkout directory. | Directory | Description | |---------------------|-----------------------------------------------------------------------------------------------------------| -| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art]({{ site.baseurl }}{% link art/general.md %}). -| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes]({{ site.baseurl }}{% link art/themes.md %}). +| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art](../art/general.md). +| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes](../art/themes.md). | `/config` | [config.hjson](config-hjson.md) system configuration. | `/config/menus` | [menu.hjson](menu-hjson.md) storage. | `/config/security` | SSL certificates and public/private keys. | `/db` | All ENiGMA½ databases in SQLite3 format. | `/docs` | These docs ;-) -| `/dropfiles` | Dropfiles created for [local doors]({{ site.baseurl }}{% link modding/local-doors.md %}) -| `/logs` | Logs. See [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) +| `/dropfiles` | Dropfiles created for [local doors](../modding/local-doors.md) +| `/logs` | Logs. See [Monitoring Logs](../troubleshooting/monitoring-logs.md) | `/misc` | Stuff with no other home; reset password templates, common password lists, other random bits -| `/mods` | User mods. See [Modding]({{ site.baseurl }}{% link modding/existing-mods.md %}) +| `/mods` | User mods. See [Modding](../modding/existing-mods.md) | `/node_modules` | External libraries required by ENiGMA½, installed when you run `npm install` | `/util` | Various tools used in running/debugging ENiGMA½ -| `/www` | ENiGMA½'s built in webserver root directory \ No newline at end of file +| `/www` | ENiGMA½'s built in webserver root directory diff --git a/docs/configuration/email.md b/docs/_docs/configuration/email.md similarity index 86% rename from docs/configuration/email.md rename to docs/_docs/configuration/email.md index eb13ef71..b8418181 100644 --- a/docs/configuration/email.md +++ b/docs/_docs/configuration/email.md @@ -3,7 +3,7 @@ layout: page title: Email --- ## Email Support -ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible. +ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson](config-hjson.md). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible. Additional email support will come in the near future. diff --git a/docs/configuration/event-scheduler.md b/docs/_docs/configuration/event-scheduler.md similarity index 100% rename from docs/configuration/event-scheduler.md rename to docs/_docs/configuration/event-scheduler.md diff --git a/docs/configuration/external-binaries.md b/docs/_docs/configuration/external-binaries.md similarity index 100% rename from docs/configuration/external-binaries.md rename to docs/_docs/configuration/external-binaries.md diff --git a/docs/configuration/file-transfer-protocols.md b/docs/_docs/configuration/file-transfer-protocols.md similarity index 100% rename from docs/configuration/file-transfer-protocols.md rename to docs/_docs/configuration/file-transfer-protocols.md diff --git a/docs/configuration/hjson.md b/docs/_docs/configuration/hjson.md similarity index 100% rename from docs/configuration/hjson.md rename to docs/_docs/configuration/hjson.md diff --git a/docs/configuration/menu-hjson.md b/docs/_docs/configuration/menu-hjson.md similarity index 84% rename from docs/configuration/menu-hjson.md rename to docs/_docs/configuration/menu-hjson.md index 0c409d2f..ab935222 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/_docs/configuration/menu-hjson.md @@ -3,13 +3,15 @@ layout: page title: Menu HSJON --- ## Menu HJSON -The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determines the menus (or screens) a user can see, the order they come in, how they interact with each other, ACS configuration, and so on. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information. +The core of a ENiGMA½ based BBS is it's menus driven by what will be referred to as `menu.hjson`. Throughout ENiGMA½ documentation, when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determine the menus (or screens) a user can see, the order they come in, how they interact with each other, ACS configuration, and so on. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. -Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: +:information_source: See also [HJSON General Information](hjson.md) for more information on the HJSON file format. -* Classical Main, Messages, and File menus -* Art file display -* Module driven menus such as door launchers and other custom mods +:bulb: Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: + +* Classical navigation and menus such as Main, Messages, and Files. +* Art file display. +* Module driven menus such as [door launchers](../modding/local-doors.md), [Onelinerz](../modding/onelinzerz.md), and other custom mods. Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system. @@ -25,24 +27,25 @@ As you can see a menu can be very simple. :information_source: Remember that the top level menu may include additional files using the `includes` directive. See [Configuration Files](config-files.md) for more information on this. ## Common Menu Entry Members -Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. See documentation for the module in question for particulars. +Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. Menus that use their own module contain a `module` declaration: + +```hjson +module: some_fancy_module +``` + +See documentation for the module in question for particulars. | Item | Description | |--------|--------------| | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | | `art` | An art file *spec*. See [General Art Information](../art/general.md). | -| `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. Note that special built in methods such as `@systemMethod:logoff` can also be utilized here. | +| `next` | Specifies the menu to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. Note that special built in methods such as `@systemMethod:logoff` can also be utilized here. | | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in the `prompts` section. See **Prompts** for more information. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | -| `module` | Sets the module name to use for this menu. See **Menu Modules** below. | +| `module` | Sets the module name to use for this menu. The system ships with many build in modules or you can build your own! | | `config` | An object containing additional configuration. See **Config Block** below. | -### Menu Modules -A given menu entry is backed by a *menu module*. That is, the code behind it. Menus are considered "standard" if the `module` member is not specified (and therefore backed by `core/standard_menu.js`). - -See [Menu Modules](../modding/menu-modules.md) for more information. - ### Config Block The `config` block for a menu entry can contain common members as well as a per-module (when `module` is used) settings. @@ -267,6 +270,31 @@ someMenu: { } ``` +## Case Study: Adding a Sub Menu to Main +A very common task: You want to add a new menu accessible from "Main". First, let's create a new menu called "Snazzy Town"! Perhaps under the `mainMenu` entry somewhere, create a new menu: + +```hjson +snazzyTown: { + desc: Snazzy Town + art: snazzy + config: { + cls: true + pause: true + } +} +``` + +Now let's make it accessible by "S" from the main menu. By default the main menu entry is named `mainMenu`. Within the `mainMenu`'s `submit` block you will see some existing action matches to "command". Simply add a new one pointing to `snazzyTown`: + +```hjson +{ + value: { command: "S" } + action: @menu:snazzyTown +} +``` + +That's it! When users type "S" at the main menu, they'll be sent to the Snazzy Town menu. Since we did not supply additional flow logic when they exit, they will fall back to main. + ## Case Study: Adding a New User Password (NUP) You've got a super 31337 board and want to prevent lamerz! Let's run through adding a NUP to your application flow. diff --git a/docs/configuration/security.md b/docs/_docs/configuration/security.md similarity index 100% rename from docs/configuration/security.md rename to docs/_docs/configuration/security.md diff --git a/docs/configuration/sysop-setup.md b/docs/_docs/configuration/sysop-setup.md similarity index 100% rename from docs/configuration/sysop-setup.md rename to docs/_docs/configuration/sysop-setup.md diff --git a/docs/filebase/acs.md b/docs/_docs/filebase/acs.md similarity index 100% rename from docs/filebase/acs.md rename to docs/_docs/filebase/acs.md diff --git a/docs/filebase/first-file-area.md b/docs/_docs/filebase/first-file-area.md similarity index 96% rename from docs/filebase/first-file-area.md rename to docs/_docs/filebase/first-file-area.md index ae9e0fd2..db1efed8 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/_docs/filebase/first-file-area.md @@ -36,6 +36,7 @@ File base *Areas* are configured using the `fileBase.areas` configuration block | `desc` | :-1: | Friendly area description. | | `storageTags` | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first** storage tag location is utilized! | | `sort` | :-1: | If present, provides the sort key for ordering. `name` is used otherwise. | +| `hashTags` | :-1: | Set to an array of strings or comma separated list to provide _default_ hash tags for this area. | Example areas section: @@ -45,6 +46,7 @@ areas: { name: Retro PC desc: Oldschool PC/DOS storageTags: [ "retro_pc_dos", "retro_pc_bbs" ] + hashTags: ["retro", "pc", "dos" ] } } ``` diff --git a/docs/filebase/index.md b/docs/_docs/filebase/index.md similarity index 100% rename from docs/filebase/index.md rename to docs/_docs/filebase/index.md diff --git a/docs/filebase/network-mounts-and-symlinks.md b/docs/_docs/filebase/network-mounts-and-symlinks.md similarity index 100% rename from docs/filebase/network-mounts-and-symlinks.md rename to docs/_docs/filebase/network-mounts-and-symlinks.md diff --git a/docs/filebase/tic-support.md b/docs/_docs/filebase/tic-support.md similarity index 100% rename from docs/filebase/tic-support.md rename to docs/_docs/filebase/tic-support.md diff --git a/docs/filebase/uploads.md b/docs/_docs/filebase/uploads.md similarity index 100% rename from docs/filebase/uploads.md rename to docs/_docs/filebase/uploads.md diff --git a/docs/filebase/web-access.md b/docs/_docs/filebase/web-access.md similarity index 100% rename from docs/filebase/web-access.md rename to docs/_docs/filebase/web-access.md diff --git a/docs/_docs/installation/docker.md b/docs/_docs/installation/docker.md new file mode 100644 index 00000000..1fa1de49 --- /dev/null +++ b/docs/_docs/installation/docker.md @@ -0,0 +1,72 @@ +--- +layout: page +title: Docker +--- +**You'll need Docker installed before going any further. How to do so are out of scope of these docs, but you can find full instructions +for every operating system on the [Docker website](https://docs.docker.com/engine/install/).** + +## Quick Start +prepare a folder where you are going to save your bbs files. +- Generate some config for your BBS: \ +you can perform this step from anywhere - but make sure to consistently run it from the same place to retain your config inside the docker guest +``` +docker run -it -p 8888:8888 \ +--name "ENiGMABBS" \ +-v "$(pwd)/config:/enigma-bbs/config" \ +-v "$(pwd)/db:/enigma-bbs/db" \ +-v "$(pwd)/logs:/enigma-bbs/logs" \ +-v "$(pwd)/filebase:/enigma-bbs/filebase" \ +-v "$(pwd)/art:/enigma-bbs/art" \ +-v "$(pwd)/mods:/enigma-bbs/mods" \ +-v "$(pwd)/mail:/mail" \ +enigmabbs/enigma-bbs:latest +``` +- Run it: \ +you can use the same command as above, just daemonize and drop interactiveness (we needed it for config but most of the time docker will run in the background) +```` +docker run -d -p 8888:8888 \ +--name "ENiGMABBS" \ +-v "$(pwd)/config:/enigma-bbs/config" \ +-v "$(pwd)/db:/enigma-bbs/db" \ +-v "$(pwd)/logs:/enigma-bbs/logs" \ +-v "$(pwd)/filebase:/enigma-bbs/filebase" \ +-v "$(pwd)/art:/enigma-bbs/art" \ +-v "$(pwd)/mods:/enigma-bbs/mods" \ +-v "$(pwd)/mail:/mail" \ +enigmabbs/enigma-bbs:latest +```` +- Restarting and Making changes\ +if you make any changes to your host config folder they will persist, and you can just restart ENiGMABBS container to load any changes you've made. + +```docker restart ENiGMABBS``` + +:bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`. + +:bulb: Windows users - you'll need to switch out `$(pwd)/enigma-bbs/config` for a Windows-style path. + +## Volumes + +Containers by their nature are ephermeral. Meaning, stuff you want to keep (config, database, mail) needs +to be stored outside of the running container. As such, the following volumes are mountable: + +| Volume | Usage | +|:------------------------|:---------------------------------------------------------------------| +| /enigma-bbs/art | Art, themes, etc | +| /enigma-bbs/config | Config such as config.hjson, menu.hjson, prompt.hjson, SSL certs etc | +| /enigma-bbs/db | ENiGMA databases | +| /enigma-bbs/filebase | Filebase | +| /enigma-bbs/logs | Logs | +| /enigma-bbs/mods | ENiGMA mods | +| /mail | FTN mail (for use with an external mailer) | + + +## Building your own image + +Customising the Docker image is easy! + +1. Clone the ENiGMA-BBS source. +2. Build the image + + ``` + docker build -f ./docker/Dockerfile . + ``` diff --git a/docs/installation/rpi.md b/docs/_docs/installation/hardware/rpi.md similarity index 100% rename from docs/installation/rpi.md rename to docs/_docs/installation/hardware/rpi.md diff --git a/docs/installation/windows.md b/docs/_docs/installation/hardware/windows.md similarity index 100% rename from docs/installation/windows.md rename to docs/_docs/installation/hardware/windows.md diff --git a/docs/installation/install-script.md b/docs/_docs/installation/install-script.md similarity index 100% rename from docs/installation/install-script.md rename to docs/_docs/installation/install-script.md diff --git a/docs/installation/installation-methods.md b/docs/_docs/installation/installation-methods.md similarity index 100% rename from docs/installation/installation-methods.md rename to docs/_docs/installation/installation-methods.md diff --git a/docs/installation/manual.md b/docs/_docs/installation/manual.md similarity index 93% rename from docs/installation/manual.md rename to docs/_docs/installation/manual.md index cb463d81..eb8101aa 100644 --- a/docs/installation/manual.md +++ b/docs/_docs/installation/manual.md @@ -6,7 +6,7 @@ For Linux environments it's recommended you run the [install script](install-scr do things manually, read on... ## Prerequisites -* [Node.js](https://nodejs.org/) version **v12.x LTS or higher** (Other versions may work but are not supported). +* [Node.js](https://nodejs.org/) version **v14.x LTS or higher**. Versions under v14 are known not to work due to language level changes. * :bulb: It is **highly** recommended to use [Node Version Manager (NVM)](https://github.com/creationix/nvm) to manage your Node.js installation if you're on a Linux/Unix environment. * [Python](https://www.python.org/downloads/) for compiling Node.js packages with native extensions via `node-gyp`. @@ -57,7 +57,7 @@ ENiGMA BBS makes use of a few packages for archive and legacy protocol support. :information_source: Additional information in [Archivers](../configuration/archivers.md) and [File Transfer Protocols](../configuration/file-transfer-protocols.md) ## Config Files -You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/) for more information. +You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/hjson.md) for more information. Use `oputil.js` to generate your **initial** configuration: diff --git a/docs/installation/network.md b/docs/_docs/installation/network.md similarity index 100% rename from docs/installation/network.md rename to docs/_docs/installation/network.md diff --git a/docs/installation/production.md b/docs/_docs/installation/production.md similarity index 100% rename from docs/installation/production.md rename to docs/_docs/installation/production.md diff --git a/docs/installation/testing.md b/docs/_docs/installation/testing.md similarity index 97% rename from docs/installation/testing.md rename to docs/_docs/installation/testing.md index 2e5dbea1..1efc47cc 100644 --- a/docs/installation/testing.md +++ b/docs/_docs/installation/testing.md @@ -13,7 +13,7 @@ _Note that if you've used the [Docker](docker.md) installation method, you've al If everything went OK: ```bash -ENiGMA½ Copyright (c) 2014-2020, Bryan Ashby +ENiGMA½ Copyright (c) 2014-2022, Bryan Ashby _____________________ _____ ____________________ __________\_ / \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! // __|___// | \// |// | \// | | \// \ /___ /_____ diff --git a/docs/messageareas/bso-import-export.md b/docs/_docs/messageareas/bso-import-export.md similarity index 100% rename from docs/messageareas/bso-import-export.md rename to docs/_docs/messageareas/bso-import-export.md diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/_docs/messageareas/configuring-a-message-area.md similarity index 100% rename from docs/messageareas/configuring-a-message-area.md rename to docs/_docs/messageareas/configuring-a-message-area.md diff --git a/docs/messageareas/ftn.md b/docs/_docs/messageareas/ftn.md similarity index 100% rename from docs/messageareas/ftn.md rename to docs/_docs/messageareas/ftn.md diff --git a/docs/messageareas/message-networks.md b/docs/_docs/messageareas/message-networks.md similarity index 100% rename from docs/messageareas/message-networks.md rename to docs/_docs/messageareas/message-networks.md diff --git a/docs/messageareas/netmail.md b/docs/_docs/messageareas/netmail.md similarity index 100% rename from docs/messageareas/netmail.md rename to docs/_docs/messageareas/netmail.md diff --git a/docs/messageareas/qwk.md b/docs/_docs/messageareas/qwk.md similarity index 100% rename from docs/messageareas/qwk.md rename to docs/_docs/messageareas/qwk.md diff --git a/docs/misc/user-interrupt.md b/docs/_docs/misc/user-interrupt.md similarity index 100% rename from docs/misc/user-interrupt.md rename to docs/_docs/misc/user-interrupt.md diff --git a/docs/modding/autosig-edit.md b/docs/_docs/modding/autosig-edit.md similarity index 100% rename from docs/modding/autosig-edit.md rename to docs/_docs/modding/autosig-edit.md diff --git a/docs/modding/bbs-list.md b/docs/_docs/modding/bbs-list.md similarity index 100% rename from docs/modding/bbs-list.md rename to docs/_docs/modding/bbs-list.md diff --git a/docs/modding/door-servers.md b/docs/_docs/modding/door-servers.md similarity index 100% rename from docs/modding/door-servers.md rename to docs/_docs/modding/door-servers.md diff --git a/docs/modding/existing-mods.md b/docs/_docs/modding/existing-mods.md similarity index 100% rename from docs/modding/existing-mods.md rename to docs/_docs/modding/existing-mods.md diff --git a/docs/modding/file-area-list.md b/docs/_docs/modding/file-area-list.md similarity index 100% rename from docs/modding/file-area-list.md rename to docs/_docs/modding/file-area-list.md diff --git a/docs/modding/file-base-download-manager.md b/docs/_docs/modding/file-base-download-manager.md similarity index 100% rename from docs/modding/file-base-download-manager.md rename to docs/_docs/modding/file-base-download-manager.md diff --git a/docs/modding/file-base-web-download-manager.md b/docs/_docs/modding/file-base-web-download-manager.md similarity index 100% rename from docs/modding/file-base-web-download-manager.md rename to docs/_docs/modding/file-base-web-download-manager.md diff --git a/docs/modding/file-transfer-protocol-select.md b/docs/_docs/modding/file-transfer-protocol-select.md similarity index 100% rename from docs/modding/file-transfer-protocol-select.md rename to docs/_docs/modding/file-transfer-protocol-select.md diff --git a/docs/modding/last-callers.md b/docs/_docs/modding/last-callers.md similarity index 100% rename from docs/modding/last-callers.md rename to docs/_docs/modding/last-callers.md diff --git a/docs/modding/local-doors.md b/docs/_docs/modding/local-doors.md similarity index 95% rename from docs/modding/local-doors.md rename to docs/_docs/modding/local-doors.md index 3e3e59ad..ce948875 100644 --- a/docs/modding/local-doors.md +++ b/docs/_docs/modding/local-doors.md @@ -5,6 +5,8 @@ title: Local Doors ## Local Doors ENiGMA½ has many ways to add doors to your system. In addition to the [many built in door server modules](door-servers.md), local doors are of course also supported using the ! The `abracadabra` module! +:information_source: See also [Let’s add a DOS door to Enigma½ BBS](https://medium.com/retro-future/lets-add-a-dos-game-to-enigma-1-2-41f257deaa3c) by Robbie Whiting for a great writeup on adding doors! + ## The abracadabra Module The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and perform I/O through standard I/O (stdio) or a temporary TCP server. @@ -14,10 +16,11 @@ The `abracadabra` `config` block can contain the following members: | Item | Required | Description | |------|----------|-------------| | `name` | :+1: | Used as a key for tracking number of clients using a particular door. | -| `dropFileType` | :+1: | Specifies the type of dropfile to generate (See **Dropfile Types** below). | +| `dropFileType` | :-1: | Specifies the type of dropfile to generate (See **Dropfile Types** below). Can be omitted or set to `none`. | | `cmd` | :+1: | Path to executable to launch. | | `args` | :-1: | Array of argument(s) to pass to `cmd`. See **Argument Variables** below for information on variables that can be used here. | `cwd` | :-1: | Sets the Current Working Directory (CWD) for `cmd`. Defaults to the directory of `cmd`. | +| `env` | :-1: | Sets the environment. Supplied in the form of an map: `{ SOME_VAR: "value" }` | `nodeMax` | :-1: | Max number of nodes that can access this door at once. Uses `name` as a tracking key. | | `tooManyArt` | :-1: | Art spec to display if too many instances are already in use. | | `io` | :-1: | How to process input/output (I/O). Can be `stdio` or `socket`. When using `stdio`, I/O is handled via standard stdin/stdout. When using `socket` a temporary socket server is spawned that can be connected back to. The server listens on localhost on `{srvPort}` (See **Argument Variables** below for more information). Default value is `stdio`. | diff --git a/docs/modding/menu-modules.md b/docs/_docs/modding/menu-modules.md similarity index 88% rename from docs/modding/menu-modules.md rename to docs/_docs/modding/menu-modules.md index 1a2a9133..0ab7baea 100644 --- a/docs/modding/menu-modules.md +++ b/docs/_docs/modding/menu-modules.md @@ -1,6 +1,6 @@ --- layout: page -title: Local Doors +title: Menu Modules --- ## Menu Modules Menu entries found within `menu.hjson` are backed by *menu modules*. diff --git a/docs/modding/msg-area-list.md b/docs/_docs/modding/msg-area-list.md similarity index 100% rename from docs/modding/msg-area-list.md rename to docs/_docs/modding/msg-area-list.md diff --git a/docs/modding/msg-conf-list.md b/docs/_docs/modding/msg-conf-list.md similarity index 100% rename from docs/modding/msg-conf-list.md rename to docs/_docs/modding/msg-conf-list.md diff --git a/docs/modding/node-msg.md b/docs/_docs/modding/node-msg.md similarity index 100% rename from docs/modding/node-msg.md rename to docs/_docs/modding/node-msg.md diff --git a/docs/modding/onelinerz.md b/docs/_docs/modding/onelinerz.md similarity index 84% rename from docs/modding/onelinerz.md rename to docs/_docs/modding/onelinerz.md index 92515617..001c4e04 100644 --- a/docs/modding/onelinerz.md +++ b/docs/_docs/modding/onelinerz.md @@ -9,6 +9,7 @@ The built in `onelinerz` module provides a retro onelinerz system. ### Config Block Available `config` block entries: * `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` date format. +* `dbSuffix`: Provide a suffix that will be appended to the DB name to use onelinerz for more than one purpose (separate lists). ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): diff --git a/docs/modding/rumorz.md b/docs/_docs/modding/rumorz.md similarity index 100% rename from docs/modding/rumorz.md rename to docs/_docs/modding/rumorz.md diff --git a/docs/modding/set-newscan-date.md b/docs/_docs/modding/set-newscan-date.md similarity index 100% rename from docs/modding/set-newscan-date.md rename to docs/_docs/modding/set-newscan-date.md diff --git a/docs/modding/show-art.md b/docs/_docs/modding/show-art.md similarity index 100% rename from docs/modding/show-art.md rename to docs/_docs/modding/show-art.md diff --git a/docs/modding/telnet-bridge.md b/docs/_docs/modding/telnet-bridge.md similarity index 100% rename from docs/modding/telnet-bridge.md rename to docs/_docs/modding/telnet-bridge.md diff --git a/docs/modding/top-x.md b/docs/_docs/modding/top-x.md similarity index 100% rename from docs/modding/top-x.md rename to docs/_docs/modding/top-x.md diff --git a/docs/modding/user-2fa-otp-config.md b/docs/_docs/modding/user-2fa-otp-config.md similarity index 99% rename from docs/modding/user-2fa-otp-config.md rename to docs/_docs/modding/user-2fa-otp-config.md index 4ef12687..f2e5f945 100644 --- a/docs/modding/user-2fa-otp-config.md +++ b/docs/_docs/modding/user-2fa-otp-config.md @@ -1,6 +1,6 @@ --- layout: page -title: TopX +title: 2FA/OTP Config --- ## The 2FA/OTP Config Module The `user_2fa_otp_config` module provides opt-in, configuration, and viewing of Two-Factor Authentication via One-Time-Password (2FA/OTP) settings. In order to allow users access to 2FA/OTP, the system must be properly configured. See [Security](../configuration/security.md) for more information. diff --git a/docs/modding/user-list.md b/docs/_docs/modding/user-list.md similarity index 100% rename from docs/modding/user-list.md rename to docs/_docs/modding/user-list.md diff --git a/docs/modding/whos-online.md b/docs/_docs/modding/whos-online.md similarity index 100% rename from docs/modding/whos-online.md rename to docs/_docs/modding/whos-online.md diff --git a/docs/_docs/servers/contentservers/gopher.md b/docs/_docs/servers/contentservers/gopher.md new file mode 100644 index 00000000..03c34bed --- /dev/null +++ b/docs/_docs/servers/contentservers/gopher.md @@ -0,0 +1,65 @@ +--- +layout: page +title: Gopher Server +--- +## The Gopher Content Server +The Gopher *content server* provides access to publicly exposed message conferences and areas over Gopher (gopher://) as well as any other content you wish to serve in your Gopher Hole! + +## Configuration +Gopher configuration is found in `contentServers.gopher` in `config.hjson`. + +| Item | Required | Description | +|------|----------|-------------| +| `enabled` | :+1: | Set to `true` to enable Gopher | +| `staticRoot` | :+1: | Sets the path serving as the static root path for all Gopher content. Defaults to `enigma-bbs/gopher`.
See also **Gophermap's** below | +| `port` | :-1: | Override the default port of `8070` | +| `publicHostname` | :+1: | Set the **public** hostname/domain that Gopher will serve to the outside world. Example: `myfancybbs.com` | +| `publicPort` | :+1: | Set the **public** port that Gopher will serve to the outside world. | +| `messageConferences` | :-1: | An map of *conference tags* to *area tags* that are publicly exposed via Gopher. See example below. | + +Notes on `publicHostname` and `publicPort`: +The Gopher protocol serves content that contains host/domain and port even when referencing it's own documents. Due to this, these members must be set to your publicly addressable Gopher server! + +## Gophermap's +[Gophermap's](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) are how to build menus for your Gopher Hole. Each map is a simple text file named `gophermap` (all lowercase, no extension) with DOS style CRLF endings. + +Within any directory nested within your `staticRoot` may live a `gophermap`. A template may be found in the `enigma-bbsmisc` directory. + +ENiGMA will pre-process `gophermap` files replacing in following variables: +* `{publicHostname}`: The public hostname from your config. +* `{publicPort}`: The public port from your config. + +:information_source: See [Wikipedia](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) for more information on the `gophermap` format. + +:information_source: See [RFC 1436](https://tools.ietf.org/html/rfc1436) for the original Gopher spec. + +:bulb: Tools such as [gfu](https://rawtext.club/~sloum/gfu.html) may help you with `gophermap`'s + +### Example Gophermap +An example `gophermap` living in `enigma-bbs/gopher`: +``` +iWelcome to a Gopher server! {publicHostname} {publicPort} +1Public Message Area /msgarea {publicHostname} {publicPort} +. +``` + +### Example +Let's suppose you are serving Gopher for your BBS at `myfancybbs.com`. Your ENiGMA½ system is listening on the default Gopher `port` of 8070 but you're behind a firewall and want port 70 exposed to the public. Lastly, you want to expose some fsxNet areas: + +```hjson +contentServers: { + gopher: { + enabled: true + publicHostname: myfancybbs.com + publicPort: 70 + + // Expose some public message conferences/areas + messageConferences: { + fsxnet: { // fsxNet's conf tag + // Areas of fsxNet we want to expose: + "fsx_gen", "fsx_bbs" + } + } + } +} +``` diff --git a/docs/servers/nntp.md b/docs/_docs/servers/contentservers/nntp.md similarity index 100% rename from docs/servers/nntp.md rename to docs/_docs/servers/contentservers/nntp.md diff --git a/docs/servers/web-server.md b/docs/_docs/servers/contentservers/web-server.md similarity index 100% rename from docs/servers/web-server.md rename to docs/_docs/servers/contentservers/web-server.md diff --git a/docs/servers/ssh.md b/docs/_docs/servers/loginservers/ssh.md similarity index 100% rename from docs/servers/ssh.md rename to docs/_docs/servers/loginservers/ssh.md diff --git a/docs/servers/telnet.md b/docs/_docs/servers/loginservers/telnet.md similarity index 100% rename from docs/servers/telnet.md rename to docs/_docs/servers/loginservers/telnet.md diff --git a/docs/servers/websocket.md b/docs/_docs/servers/loginservers/websocket.md similarity index 94% rename from docs/servers/websocket.md rename to docs/_docs/servers/loginservers/websocket.md index 1bfa0583..a2c26754 100644 --- a/docs/servers/websocket.md +++ b/docs/_docs/servers/loginservers/websocket.md @@ -6,7 +6,7 @@ title: Web Socket / Web Interface Server The WebSocket Login Server provides **secure** (wss://) as well as non-secure (ws://) WebSocket login access. This is often combined with a browser based WebSocket client such as VTX or fTelnet. # VTX Web Client -ENiGMA supports the VTX websocket client for connecting to your BBS from a web page. Example usage can be found at [Xibalba](https://xibalba.l33t.codes) and [fORCE9](https://bbs.force9.org/vtx/force9.html) amongst others. +ENiGMA supports the VTX WebSocket client for connecting to your BBS from a web page. Example usage can be found at [Xibalba](https://xibalba.l33t.codes) and [fORCE9](https://bbs.force9.org/vtx/force9.html) amongst others. ## Before You Start There are a few things out of scope of this document: @@ -62,7 +62,7 @@ following: 3. Download the [VTX_ClientServer](https://github.com/codewar65/VTX_ClientServer/archive/master.zip) to your webserver, and unpack it to a temporary directory. -4. Download the example [VTX client HTML file](/misc/vtx/vtx.html) and save it to your webserver root. +4. Download the example [VTX client HTML file](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/vtx/vtx.html) and save it to your webserver root. 5. Create an `assets/vtx` directory within your webserver root, so you have a structure like the following: diff --git a/docs/troubleshooting/monitoring-logs.md b/docs/_docs/troubleshooting/monitoring-logs.md similarity index 100% rename from docs/troubleshooting/monitoring-logs.md rename to docs/_docs/troubleshooting/monitoring-logs.md diff --git a/docs/_includes/nav.html b/docs/_includes/nav.html new file mode 100644 index 00000000..e3272d9c --- /dev/null +++ b/docs/_includes/nav.html @@ -0,0 +1,70 @@ +
    +{% for doc in site.docs %} + {% assign pathparts = doc.path | split: '/' %} + {% assign dir = pathparts[1] %} + + {% if pathparts.size > 3 %} + {% assign subdir = pathparts[2] %} + {% unless site.data.sections[subdir] %} + {% assign subsection = subdir %} + {% else %} + {% assign subsection = site.data.sections[subdir].title %} + {% endunless %} + {% else %} + {% assign subdir = "NONE" %} + {% endif %} + + {% assign section = site.data.sections[dir].title %} + {% unless section %} + {% assign section = dir %} + {% endunless %} + + {% if doc.previous %} + {% assign prevpathparts = doc.previous.path | split: '/' %} + {% assign prevdir = prevpathparts[1] %} + + {% if prevpathparts.size > 3 %} + {% assign prevsubdir = prevpathparts[2] %} + {% else %} + {% assign prevsubdir = "NONE" %} + {% endif %} + {% else %} + {% assign prevdir = "NONE" %} + {% assign prevsubdir = "NONE" %} + {% endif %} + + {% if subdir != prevsubdir and prevsubdir != "NONE" %} +
+ {% endif %} + + {% if dir != prevdir %} + {% if prevdir != "NONE" %} + + {% endif %} +
  • {{section}}
  • +
      + + {% endif %} + + {% if subdir != "NONE" and subdir != prevsubdir %} +
    • {{subsection}}
    • +
        + {% endif %} + + + {% if doc.url != page.url %} +
      • {{doc.title}}
      • + {% else %} +
      • {{doc.title}}
      • + {% endif %} + + + + {% unless doc.next %} +
      + {% if prevsubdir != "NONE" %} +
    + {% endif %} + {% endunless %} +{% endfor %} + diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md deleted file mode 100644 index 42cc7bff..00000000 --- a/docs/_includes/nav.md +++ /dev/null @@ -1,98 +0,0 @@ - - Installation - - [Installation Methods]({{ site.baseurl }}{% link installation/installation-methods.md %}) - - [Install script]({{ site.baseurl }}{% link installation/install-script.md %}) - - [Docker]({{ site.baseurl }}{% link installation/docker.md %}) - - [Manual installation]({{ site.baseurl }}{% link installation/manual.md %}) - - [OS / Hardware Specific]({{ site.baseurl }}{% link installation/os-hardware.md %}) - - [Raspberry Pi]({{ site.baseurl }}{% link installation/rpi.md %}) - - [Windows]({{ site.baseurl }}{% link installation/windows.md %}) - - [Your Network Setup]({{ site.baseurl }}{% link installation/network.md %}) - - [Testing Your Installation]({{ site.baseurl }}{% link installation/testing.md %}) - - [Production Installation]({{ site.baseurl }}{% link installation/production.md %}) - - - Configuration - - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) - - [Configuration Files]({{ site.baseurl }}{% link configuration/config-files.md %}) - - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - - [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %}) - - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) - - [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %}) - - [External Binaries]({{ site.baseurl }}{% link configuration/external-binaries.md %}) - - [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %}) - - [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %}) - - [Email]({{ site.baseurl }}{% link configuration/email.md %}) - - [Colour Codes]({{ site.baseurl }}{% link configuration/colour-codes.md %}) - - [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %}) - - [Event Scheduler]({{ site.baseurl }}{% link configuration/event-scheduler.md %}) - - [Security]({{ site.baseurl }}{% link configuration/security.md %}) - - - File Base - - [About]({{ site.baseurl }}{% link filebase/index.md %}) - - [Configuring a File Area]({{ site.baseurl }}{% link filebase/first-file-area.md %}) - - [ACS model]({{ site.baseurl }}{% link filebase/acs.md %}) - - [Uploads]({{ site.baseurl }}{% link filebase/uploads.md %}) - - [Web Access]({{ site.baseurl }}{% link filebase/web-access.md %}) - - [TIC Support]({{ site.baseurl }}{% link filebase/tic-support.md %}) (Importing from FTN networks) - - Tips and tricks - - [Network mounts and symlinks]({{ site.baseurl }}{% link filebase/network-mounts-and-symlinks.md %}) - - - Message Areas - - [Configuring a Message Area]({{ site.baseurl }}{% link messageareas/configuring-a-message-area.md %}) - - [Message networks]({{ site.baseurl }}{% link messageareas/message-networks.md %}) - - [BSO Import & Export]({{ site.baseurl }}{% link messageareas/bso-import-export.md %}) - - [Netmail]({{ site.baseurl }}{% link messageareas/netmail.md %}) - - [QWK]({{ site.baseurl }}{% link messageareas/qwk.md %}) - - [FTN]({{ site.baseurl }}{% link messageareas/ftn.md %}) - - - Art - - [General]({{ site.baseurl }}{% link art/general.md %}) - - [Themes]({{ site.baseurl }}{% link art/themes.md %}) - - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) - - - Servers - - Login Servers - - [Telnet]({{ site.baseurl }}{% link servers/telnet.md %}) - - [SSH]({{ site.baseurl }}{% link servers/ssh.md %}) - - [WebSocket]({{ site.baseurl }}{% link servers/websocket.md %}) - - Build your own - - Content Servers - - [Web]({{ site.baseurl }}{% link servers/web-server.md %}) - - [Gopher]({{ site.baseurl }}{% link servers/gopher.md %}) - - [NNTP]({{ site.baseurl }}{% link servers/nntp.md %}) - - - Modding - - [Local Doors]({{ site.baseurl }}{% link modding/local-doors.md %}) - - [Door Servers]({{ site.baseurl }}{% link modding/door-servers.md %}) - - DoorParty - - BBSLink - - Combatnet - - Exodus - - [Telnet Bridge]({{ site.baseurl }}{% link modding/telnet-bridge.md %}) - - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) - - [File Area List]({{ site.baseurl }}{% link modding/file-area-list.md %}) - - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) - - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) - - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) - - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) - - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) - - [BBS List]({{ site.baseurl }}{% link modding/bbs-list.md %}) - - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) - - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) - - [Show Art]({{ site.baseurl }}{% link modding/show-art.md %}) - - [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %}) - - [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %}) - - [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %}) - - [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %}) - - [Top X]({{ site.baseurl }}{% link modding/top-x.md %}) - - [2FA/OTP Config]({{ site.baseurl }}{% link modding/user-2fa-otp-config.md %}) - - [Auto Signature Editor]({{ site.baseurl }}{% link modding/autosig-edit.md %}) - - - Administration - - [Administration]({{ site.baseurl }}{% link admin/administration.md %}) - - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) - - [Updating]({{ site.baseurl }}{% link admin/updating.md %}) - - - Troubleshooting - - [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 60f87db8..2066d4c9 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -4,26 +4,55 @@ + + + {% seo %} - Fork me on GitHub + {% if page.include-banner %} + Fork me on GitHub + {% endif %} +
    -
    -
    +
    +
    + + {{ content }} + +
    +
    -
    +
    {% if site.google_analytics %}