Compare commits

...

158 Commits

Author SHA1 Message Date
Bryan Ashby 6aa0c8679f
Merge pull request #520 from NuSkooler/feature/multiarch_docker
Feature/multiarch docker
2023-10-15 17:34:09 -06:00
Nathan Byrd 6b169de3ac Added missing change directory 2023-10-15 20:43:31 +00:00
Nathan Byrd 362f443290 Changed copy to directory location 2023-10-15 20:37:35 +00:00
Nathan Byrd 8a97f79d8c Removed old python package 2023-10-15 20:22:16 +00:00
Nathan Byrd f17bbe7c5a Updated docker support 2023-10-15 20:17:47 +00:00
Bryan Ashby 8c7df1fe75 Oops! 2023-10-12 20:41:59 -06:00
Bryan Ashby b00d5d07dd
Merge pull request #510 from NuSkooler/bugfix/scrolling_updates
Bugfix/scrolling updates
2023-10-12 20:36:54 -06:00
Bryan Ashby bbfef536e0
Merge pull request #489 from NuSkooler/bugfix/node_v20
Updated node-pty to support new node versions
2023-10-12 20:28:00 -06:00
Bryan Ashby 83636cb894 Sync up with master 2023-10-12 20:25:01 -06:00
Bryan Ashby ca985dd2cb Fix onExit(): emits a object containing exitCode and signal 2023-10-12 20:21:49 -06:00
Bryan Ashby b87ff51160 Merge branch 'bugfix/node_v20' of github.com:NuSkooler/enigma-bbs into bugfix/node_v20 2023-10-12 20:06:10 -06:00
Bryan Ashby 746233fa56 Updated lock 2023-10-12 20:05:45 -06:00
Bryan Ashby c84b3ee1c1
Merge pull request #515 from NuSkooler/bugfix/ssh_doc_updates
Updated the SSH version, config, and documentation
2023-10-11 19:43:21 -06:00
Nathan Byrd 270c09eb80 Added a sentence 2023-10-11 00:28:13 +00:00
Nathan Byrd f5b0a8bb60 Updated the SSH version, config, and documentation 2023-10-11 00:19:43 +00:00
Bryan Ashby 1a99153431
Merge pull request #514 from NuSkooler/bugfix/dev_env_python
Small bugfix on dev container startup to lock python version
2023-10-10 16:52:42 -06:00
Nathan Byrd 88a1b0ea69 locked python version to fix startup issue 2023-10-10 20:42:15 +00:00
Nathan Byrd 498c4a6082 Additional API changes 2023-10-10 19:43:58 +00:00
Nathan Byrd 577992bbe5 Changes to API for node-pty 2023-10-10 19:33:25 +00:00
Bryan Ashby db8bd2f80f
Merge pull request #511 from AnthonyHarwood/fse_err_msg_display_tweak
fse err msg display tweak
2023-09-29 12:01:05 -06:00
anthony 0161c22401 clear prior message; msg tweak for empty field 2023-09-28 11:05:10 -05:00
Nathan Byrd 402c5d0156 Merge branch 'master' into bugfix/scrolling_updates 2023-09-27 20:56:04 +00:00
anthony 5998144071 Merge branch 'bugfix/ftn_address_fromString' of https://github.com/AnthonyHarwood/enigma-bbs into bugfix/ftn_address_fromString 2023-09-27 15:52:00 -05:00
anthony d12b0789aa Added checks for undefined 2023-09-27 15:51:33 -05:00
Bryan Ashby a644672de2
Merge pull request #504 from AnthonyHarwood/disallow_posting_empty_msg
Disallow posting empty message
2023-09-26 17:17:18 -06:00
Bryan Ashby b0542cf51e
Merge pull request #508 from NuSkooler/bugfix/doors_socket
Fixed door logic per issue #506
2023-09-26 17:15:04 -06:00
Nathan Byrd 0ca22de6e9 Added back in unused escape codes for backwards compat 2023-09-26 22:02:49 +00:00
Nathan Byrd 394ac465c8 Merge remote-tracking branch 'origin/master' into bugfix/scrolling_updates 2023-09-26 21:50:04 +00:00
anthony 2b6494cfc9 Added checks for undefined 2023-09-26 16:14:10 -05:00
Nathan Byrd dddbad4541
Merge pull request #507 from AnthonyHarwood/bugfix/private_mail_menu_template
Removed extraneous space from private mail config template
2023-09-26 09:09:18 -05:00
Nathan Byrd 1d31b268aa Fixed door logic per issue #506 2023-09-26 14:00:05 +00:00
anthony 0e09ce491c Removed extraneous space from config template 2023-09-26 08:43:23 -05:00
anthony a6196c38ec Disallow posting empty message 2023-09-25 20:25:01 -05:00
Nathan Byrd 450ba65565 Fixed missing ansi codes causing formatting misses 2023-09-24 20:39:57 +00:00
Bryan Ashby fc107f1552
Merge pull request #503 from AnthonyHarwood/bugfix/TextView_clearText
Added code to clear the text
2023-09-22 15:57:19 -06:00
anthony f719499c65 Merge branch 'bugfix/TextView_clearText' of https://github.com/AnthonyHarwood/enigma-bbs into bugfix/TextView_clearText 2023-09-22 14:07:50 -05:00
anthony 75fa2026c0 Added code to clear the text 2023-09-22 14:05:16 -05:00
anthony 5180447e87 Added code to clear the text 2023-09-22 10:33:42 -05:00
Nathan Byrd 72a8546d74 Removed special handling of backspace and form feed 2023-09-22 01:18:38 +00:00
Bryan Ashby 19ee4b0553
Merge pull request #502 from NuSkooler/dependabot/npm_and_yarn/systeminformation-5.21.7
Bump systeminformation from 5.12.3 to 5.21.7
2023-09-21 11:50:26 -06:00
dependabot[bot] d3cf82b563
Bump systeminformation from 5.12.3 to 5.21.7
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.12.3 to 5.21.7.
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.12.3...v5.21.7)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-21 17:24:32 +00:00
Nathan Byrd 79a3f4e1ce Handle deleting rows / columns 2023-09-20 23:52:34 +00:00
Nathan Byrd c0c262c971 Added insert and delete rows 2023-09-20 23:30:01 +00:00
Nathan Byrd 826db2d718 Added scrolling. 2023-09-20 23:00:32 +00:00
Nathan Byrd f3c9afc684 Added F and G 2023-09-20 21:40:59 +00:00
Nathan Byrd 372d84f572 Added backspace 2023-09-20 21:23:00 +00:00
Nathan Byrd 8029521399 changed handling of regex 2023-09-20 21:19:03 +00:00
Bryan Ashby 86049353a9
Merge pull request #501 from AnthonyHarwood/fse_cursor_position
fixed incorrect reporting of cursor position
2023-09-20 13:49:35 -06:00
Nathan Byrd 855fabe34d Added additional vt100 codes 2023-09-20 13:44:54 +00:00
anthony 6748b8966e fixed incorrect reporting of cursor position 2023-09-19 13:14:24 -05:00
Nathan Byrd 5712645299 Added additional characters that change position 2023-09-19 01:39:04 +00:00
Nathan Byrd a99b55abb5 Added support for some non-bracket escape sequences 2023-09-19 01:04:36 +00:00
Nathan Byrd d8f45f9147 Removed unused code 2023-09-18 23:36:27 +00:00
Nathan Byrd 08841528e2 Initial versions of new bansi codes 2023-09-18 23:35:44 +00:00
Bryan Ashby 5e120d1749
Merge pull request #498 from NuSkooler/bugfix/fix_art_length
Bugfix/fix art length
2023-09-05 22:50:57 -06:00
Nathan Byrd 06c3d100d7 Updated length for message list 2023-09-05 20:41:40 +00:00
Nathan Byrd 3adbb517e8 Updated art lengths to be 24 lines 2023-09-05 14:55:57 -05:00
Bryan Ashby bfb6a30860
Merge pull request #497 from NuSkooler/bugfix/github_action
Updated Docker action
2023-09-03 21:21:13 -06:00
Nathan Byrd e87e7ad277 Updated Docker action 2023-09-03 21:35:50 -05:00
Bryan Ashby e2bb00b756 Merge branch 'master' of github.com:NuSkooler/enigma-bbs 2023-09-03 11:02:29 -06:00
Bryan Ashby f4a90900ae TIC attachment doc updates 2023-09-03 11:01:43 -06:00
Bryan Ashby b307e791c3
Merge pull request #495 from NuSkooler/bugfix/pablodraw_ansi
Added additional escape control sequences for PabloDraw
2023-09-02 23:02:34 -06:00
Nathan Byrd 6fb77f3a5f Added additional escape control sequences 2023-09-02 23:50:41 -05:00
Bryan Ashby e8441408c0
Merge pull request #493 from NuSkooler/bugfix/Gemfile_lock
Updated Gemfile.lock to fix Github Actions failure
2023-08-30 15:36:48 -06:00
Nathan Byrd 60ee842c71 Updated Gemfile.lock to fix Github Actions failure 2023-08-30 19:14:20 +00:00
Bryan Ashby de918dd338
Merge pull request #480 from NuSkooler/479-nua-optional-fields
Make real name and other properties optional - pass 1
2023-08-29 20:43:14 -06:00
Bryan Ashby 367931cbd6
Merge branch 'master' into 479-nua-optional-fields 2023-08-29 20:43:00 -06:00
Bryan Ashby 1af4a0432b
Merge pull request #492 from NuSkooler/feature/docker_dev
Setup Docker development environment
2023-08-29 20:42:03 -06:00
Nathan Byrd 1bd328452b Added documentation for development setup 2023-08-28 15:39:19 -05:00
Nathan Byrd c2d7abc94e Removed browse task as it won't run without debugging 2023-08-28 14:17:47 -05:00
Nathan Byrd 602d693caf Added additional local extensions 2023-08-28 13:10:38 -05:00
Nathan Byrd 5f0a03638e Added some handy extensions and tasks 2023-08-28 13:08:54 -05:00
Nathan Byrd 8a6506c475 Additional changes needed to run 2023-08-27 19:04:17 -05:00
Bryan Ashby 97ae34a971 Quick fix on SAUCE 2023-08-27 17:31:35 -06:00
Bryan Ashby de339c9c52
Merge pull request #488 from NuSkooler/bugfix/art_width
Fix to display ANSI files with implicit new lines on wide terminals
2023-08-27 17:24:27 -06:00
Nathan Byrd 82c05c6e69 Merge branch 'master' into feature/docker_dev 2023-08-27 12:27:07 -05:00
Nathan Byrd 5ba1ca1464 Initial devcontainer setup 2023-08-27 12:25:36 -05:00
Bryan Ashby 3e1ec4477c
Merge pull request #491 from NuSkooler/feature/docker_dev
Docker Update
2023-08-26 21:11:26 -06:00
Nathan Byrd 13ae7789a5 No need to copy the ephemeral directories 2023-08-26 20:03:48 -05:00
Nathan Byrd 80dcc14a50 Small doc change to add running custom container 2023-08-26 11:05:39 -05:00
Nathan Byrd 1d00482b02 Updated node & added .dockerignore 2023-08-26 10:54:39 -05:00
Nathan Byrd adc2e1ba98 Fixed Dockerfile to run on Windows 2023-08-26 10:37:12 -05:00
Nathan Byrd 8758722b6b Fixed PR review comments 2023-08-25 18:34:45 -05:00
Nathan Byrd 4d70f07fde Updated node-pty to support new node versions 2023-08-25 15:59:35 -05:00
Nathan Byrd bf0bf83053 Small logic change to improve SAUCE handling 2023-08-25 14:38:32 -05:00
Nathan Byrd 7d46607ddc Updated the WHATSNEW to include info about the art change. 2023-08-25 14:33:28 -05:00
Nathan Byrd ccaaa71e23 Make the terminal go to new line if the width of the term is greater than the art width 2023-08-25 14:24:47 -05:00
Nathan Byrd 0b157ddd1b Changed ansi parser to use SAUCE width when available 2023-08-24 18:00:02 -05:00
Bryan Ashby 5a4563c799 PR feedback: Better handling of real name and email 2023-08-24 09:13:29 -06:00
Bryan Ashby 9205aaa9ee Merge branch 'master' of github.com:NuSkooler/enigma-bbs into 479-nua-optional-fields 2023-08-24 08:53:03 -06:00
Bryan Ashby b11ddfc382
Merge pull request #466 from NuSkooler/dependabot/npm_and_yarn/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1
2023-08-23 21:49:25 -06:00
Bryan Ashby 993bdbe484
Merge branch 'master' into dependabot/npm_and_yarn/http-cache-semantics-4.1.1 2023-08-23 21:49:10 -06:00
Bryan Ashby 7b4910c415
Merge pull request #467 from NuSkooler/dependabot/npm_and_yarn/sqlite3-5.1.5
Bump sqlite3 from 5.0.11 to 5.1.5
2023-08-23 21:48:20 -06:00
Bryan Ashby 18678d3e4d
Merge branch 'master' into dependabot/npm_and_yarn/sqlite3-5.1.5 2023-08-23 21:47:47 -06:00
Bryan Ashby c87543c976 Update SQLite to latest 2023-08-23 21:45:08 -06:00
Bryan Ashby 25e4de5384
Merge pull request #484 from NuSkooler/dependabot/npm_and_yarn/word-wrap-1.2.5
Bump word-wrap from 1.2.3 to 1.2.5
2023-08-23 21:38:46 -06:00
Bryan Ashby c6a6c06972
Merge pull request #487 from NuSkooler/menu-stack-and-flags-revamp
Update MenuFlags to work as expected
2023-08-23 19:07:56 -06:00
Bryan Ashby c12deb86ae
Merge pull request #485 from NuSkooler/dependabot/bundler/docs/activesupport-7.0.7.2
Bump activesupport from 7.0.4.1 to 7.0.7.2 in /docs
2023-08-23 19:07:35 -06:00
Bryan Ashby 2d1cbac390
Merge branch 'master' into dependabot/bundler/docs/activesupport-7.0.7.2 2023-08-23 19:07:10 -06:00
Bryan Ashby 08adf7ccb5
Merge pull request #486 from NuSkooler/dependabot/npm_and_yarn/semver-6.3.1
Bump semver from 6.3.0 to 6.3.1
2023-08-23 19:05:38 -06:00
dependabot[bot] 5857f46d11
Bump semver from 6.3.0 to 6.3.1
Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-24 01:03:10 +00:00
dependabot[bot] a1242b51aa
Bump activesupport from 7.0.4.1 to 7.0.7.2 in /docs
Bumps [activesupport](https://github.com/rails/rails) from 7.0.4.1 to 7.0.7.2.
- [Release notes](https://github.com/rails/rails/releases)
- [Changelog](https://github.com/rails/rails/blob/v7.0.7.2/activesupport/CHANGELOG.md)
- [Commits](https://github.com/rails/rails/compare/v7.0.4.1...v7.0.7.2)

---
updated-dependencies:
- dependency-name: activesupport
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-24 01:03:06 +00:00
Bryan Ashby f32bf090fb
Merge pull request #472 from NuSkooler/dependabot/bundler/docs/nokogiri-1.14.3
Bump nokogiri from 1.13.6 to 1.14.3 in /docs
2023-08-23 19:03:04 -06:00
dependabot[bot] 8e904f589d
Bump word-wrap from 1.2.3 to 1.2.5
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-24 01:03:03 +00:00
Bryan Ashby 02f91b2621
Merge pull request #473 from NuSkooler/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2023-08-23 19:02:40 -06:00
Bryan Ashby 1cfbf4fb66 Update MenuFlags to work as expected
* 'popParent' has been removed
* 'noHistory' now works as expected
* Mods that explicitly want noHistory can state such in their constructor()
2023-08-23 18:06:48 -06:00
Bryan Ashby 614fa2d096
Merge pull request #482 from NuSkooler/bugfix/missing_image
Fixed bad image path
2023-08-21 19:17:52 -06:00
Nathan Byrd 67b5a8d99f Fixed bad image path 2023-08-21 19:58:16 -05:00
Bryan Ashby f99de19792 Make real name and other properties optional - pass 1 2023-07-24 12:17:47 -06:00
Bryan Ashby 77160434e6
Merge pull request #477 from parkbanks/master
Bugfix to theme.hjson
2023-06-19 11:56:08 -06:00
Parker J. Banks eca900f829
Update theme.hjson
Bugfix, change messageBaseSearchMessageList to messageBaseSearchResultsMessageList to match message_base.in.hjson
2023-06-19 10:43:25 -06:00
Bryan Ashby 9bac4f5e3b
Merge pull request #476 from parkbanks/master
Rumorz display bugfix
2023-05-22 09:09:43 -06:00
Parker J. Banks 8d6de56e91
Rumorz display bugfix
Updates rumorz module to display the N latest rumors, rather than the first N rumors.
2023-05-21 12:09:54 -06:00
Bryan Ashby 78af6fa522 Add oputil mb post command 2023-05-11 12:07:29 -06:00
Bryan Ashby 6ff7d1f545 Add oputil mb list-confs, update with prettier 2023-05-11 08:40:53 -06:00
Bryan Ashby 97cd0c3063 Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs 2023-04-25 10:24:51 -06:00
Bryan Ashby ac40f63e1f Clone data so it's not invalidated 2023-04-25 10:24:11 -06:00
dependabot[bot] c39cd613ef
Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-16 20:09:15 +00:00
dependabot[bot] 7cbe619235
Bump nokogiri from 1.13.6 to 1.14.3 in /docs
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.6 to 1.14.3.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.6...v1.14.3)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-12 06:15:16 +00:00
Bryan Ashby 447be3552c Better error handling 2023-04-08 21:04:40 -06:00
Bryan Ashby a208d91d4b
Merge pull request #470 from stack-fault/master
Fixed PrivMsg delivery + added new server commands
2023-03-31 12:11:45 -06:00
Stack Fault 9a4433ee03 Fixed PrivMsg delivery + added new server commands 2023-03-31 13:46:07 -04:00
dependabot[bot] ddf412d34c
Bump sqlite3 from 5.0.11 to 5.1.5
Bumps [sqlite3](https://github.com/TryGhost/node-sqlite3) from 5.0.11 to 5.1.5.
- [Release notes](https://github.com/TryGhost/node-sqlite3/releases)
- [Commits](https://github.com/TryGhost/node-sqlite3/compare/v5.0.11...v5.1.5)

---
updated-dependencies:
- dependency-name: sqlite3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 20:33:51 +00:00
dependabot[bot] 1bf7617404
Bump http-cache-semantics from 4.1.0 to 4.1.1
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-04 15:21:17 +00:00
Bryan Ashby 3319b310d3
Merge pull request #464 from NuSkooler/dependabot/bundler/docs/activesupport-7.0.4.1
Bump activesupport from 7.0.1 to 7.0.4.1 in /docs
2023-01-18 16:05:52 -07:00
dependabot[bot] a5ef7d9da5
Bump activesupport from 7.0.1 to 7.0.4.1 in /docs
Bumps [activesupport](https://github.com/rails/rails) from 7.0.1 to 7.0.4.1.
- [Release notes](https://github.com/rails/rails/releases)
- [Changelog](https://github.com/rails/rails/blob/v7.0.4.1/activesupport/CHANGELOG.md)
- [Commits](https://github.com/rails/rails/compare/v7.0.1...v7.0.4.1)

---
updated-dependencies:
- dependency-name: activesupport
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-18 18:29:34 +00:00
Bryan Ashby 330d7190ff
Merge pull request #463 from NuSkooler/462-remove-no-longer-supported-combatnet
Remove no longer supported CombatNet
2023-01-16 11:07:11 -07:00
Bryan Ashby 30f965d981
Remove no longer supported CombatNet 2023-01-16 11:04:56 -07:00
Bryan Ashby c8dfecbfcc
Merge pull request #458 from NuSkooler/webserver-improvements
Bump version to 0.0.14-beta + major web server updates
2022-12-29 01:08:44 -07:00
Bryan Ashby 6578e55139
Update Web Server docs, remove 'static' requirement 2022-12-29 01:01:09 -07:00
Bryan Ashby c265a44c1a
Bump version to 0.0.14-beta + major web server updates 2022-12-29 00:56:10 -07:00
Bryan Ashby 7f0254701c
Ensure we have a message list 2022-12-14 19:56:43 -07:00
Bryan Ashby a4261d2d36
Fix bad check for missing areas (by tag) 2022-11-27 21:07:56 -07:00
Bryan Ashby d1f0a12f77
New options for launching local doors via the abracadabra module 2022-10-25 10:59:56 -06:00
Bryan Ashby b58c7e7cc6
Fix fsxNet link 2022-10-25 10:49:20 -06:00
Bryan Ashby 433ab17e9f
Update fsxNet links 2022-10-24 12:59:23 -06:00
Bryan Ashby ff616c384f
MenuModule.setConfigWithExtraArgs() 2022-10-13 22:43:41 -06:00
Bryan Ashby 94da8798cf
Merge pull request #454 from NuSkooler/text-label-with-mci-codes
Advanced formatting for TextView / %TL MCI code
2022-10-12 02:28:50 +00:00
Bryan Ashby 32b4c344a8
Fix docs, use node 14 2022-10-10 11:24:16 -06:00
Bryan Ashby a03b071256
Docs on new MCI formatting 2022-10-05 23:40:59 -06:00
Bryan Ashby 065658f6b8
First commit of "advanced" MCI formatting via theme.hjson entries allowing mini-formatting langauge to apply
Example: {BN!styleFirstLower} in 'text' property
2022-10-05 21:46:13 -06:00
Bryan Ashby 1021226020
Nuke %PL - never used, not needed 2022-10-05 21:07:56 -06:00
Bryan Ashby 6710bf8c08
Update docs 2022-10-04 11:19:27 -06:00
Bryan Ashby 091af5adea
More info on WFC in docs 2022-10-02 11:33:44 -06:00
Bryan Ashby c7568ac897
Fix visibility restore at WFC exit 2022-10-02 11:22:23 -06:00
Bryan Ashby 0ef3df047a
Merge branch 'master' of github.com:NuSkooler/enigma-bbs 2022-10-01 17:21:09 -06:00
Bryan Ashby a88cf6c34c
Fix logo 2022-10-01 17:20:55 -06:00
Bryan Ashby b3cf535b9d
Merge pull request #445 from NuSkooler/dependabot/npm_and_yarn/ansi-regex-4.1.1
Bump ansi-regex from 4.1.0 to 4.1.1
2022-10-01 18:29:55 +00:00
Bryan Ashby 73dafdad9f
Test out code scanning 2022-10-01 18:28:06 +00:00
Bryan Ashby fd01223578
Diff paths for README.md is annoying 2022-10-01 12:23:28 -06:00
Bryan Ashby 63514c1290
Le sigh 2022-10-01 12:20:54 -06:00
Bryan Ashby 8efc605a9e
Fix paths... 2022-10-01 12:14:44 -06:00
Bryan Ashby 5f7b81296e
Fix paths 2022-10-01 11:53:40 -06:00
Bryan Ashby cee25a7104
More docs on MenuModule 2022-10-01 11:43:56 -06:00
Bryan Ashby 1025fef346
Doc updates, tidy, group member info 2022-09-30 23:55:40 -06:00
Bryan Ashby d8bc02ce46
Catch timestamp error 2022-09-30 23:55:28 -06:00
Bryan Ashby 6e1c470b69
Merge pull request #451 from NuSkooler/449-nntp-write-access
Initial NNTP write access support

This will now be beta tested on Xibalba
2022-09-26 00:32:33 +00:00
dependabot[bot] e5a2044b88
Bump ansi-regex from 4.1.0 to 4.1.1
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-16 18:20:48 +00:00
116 changed files with 2128 additions and 1291 deletions

9
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM library/node:lts-bookworm
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update \
&& apt install -y --no-install-recommends sudo telnet \
&& apt autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& echo "node ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/node \
&& chmod 0440 /etc/sudoers.d/node

View File

@ -0,0 +1,24 @@
{
"name": "Basic Node.js",
"build": { "dockerfile": "Dockerfile" },
"remoteUser": "root",
"forwardPorts": [8888, 4000],
"postCreateCommand": "gem install jekyll bundler && /bin/rm -rf node_modules && npm install && cd docs && bundle install && cd ..",
"features": {
"ghcr.io/devcontainers/features/python:1": {
"installTools": true,
"version": "3.11"
},
"ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {},
"ghcr.io/jungaretti/features/ripgrep:1": {},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {},
"ghcr.io/devcontainers/features/ruby:1": {
"version": "3.1"
}
},
"customizations": {
"vscode": {
"extensions": ["ms-azuretools.vscode-docker","alexcvzz.vscode-sqlite","yzhang.markdown-all-in-one", "DavidAnson.vscode-markdownlint", "christian-kohler.npm-intellisense", "dbaeumer.vscode-eslint", "bierner.markdown-yaml-preamble"]
}
}
}

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
node_modules
package-lock.json
yarn.lock
filebase
db
drop
file_base
logs
mail
docs/_site
docs/.sass-cache

10
.gitattributes vendored
View File

@ -6,3 +6,13 @@
*.TXT eol=crlf
*.diz eol=crlf
*.DIZ eol=crlf
# Don't mess with shell script line endings
*.sh text eol=lf
# Same thing for optutil.js which functions as a shell script
optutil.js text eol=lf
# The devcontainer is also unix
.devcontainer/Dockerfile text eol=lf
.devcontainer/devcontainer.json text eol=lf

74
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,74 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '41 0 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@ -11,25 +11,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
tags: enigmabbs/enigma-bbs:latest
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true

3
.gitignore vendored
View File

@ -11,4 +11,5 @@ mail/
node_modules/
docs/_site/
docs/.sass-cache/
.vscode/
docs/.jekyll-cache/

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"ms-vscode-remote.remote-containers",
"laktak.hjson"
]
}

26
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Jekyll (ENiGMA½ documentation server)",
"command": "cd docs && bundle exec jekyll serve",
"isBackground": true,
"type": "shell"
},
{
"label": "(re)build Jekyll bundles",
"command": "cd docs && bundle install",
"type": "shell"
},
{
"label": "(re)build node modules",
"command": "/bin/rm -rf node_modules && npm install",
"type": "shell"
},
{
"label": "ENiGMA½ new configuration",
"command": "./oputil.js config new",
"type": "shell"
}
]
}

View File

@ -8,29 +8,29 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
Below are just some of the features ENiGMA½ supports out of the box:
* **Multi platform** — Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows)
* Unlimited multi node support (for all those BBS "callers"!)
* **Highly customizable** via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based [mods](docs/modding/existing-mods.md)
* [MCI support](docs/art/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles
* **Highly customizable** via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based [mods](./docs/_docs/modding/existing-mods.md)
* [MCI support](./docs/_docs/art/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles
* Telnet, **SSH**, and both secure and non-secure [WebSocket](https://en.wikipedia.org/wiki/WebSocket) access built in! Additional servers are easy to implement
* [CP437](http://www.ascii-codes.com/) and UTF-8 output
* [SyncTERM](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior.
* Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support.
* Renegade style [pipe color codes](./docs/configuration/colour-codes.md).
* Renegade style [pipe color codes](./docs/_docs/configuration/colour-codes.md).
* [SQLite](http://sqlite.org/) storage of users, message areas, etc.
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption.
* Support for **2-Factor Authentication** with One-Time-Passwords
* [Door support](docs/modding/door-servers.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support!
* [Door support](./docs/_docs/modding/door-servers.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), and [Exodus](https://oddnetwork.org/exodus/)!
* Structured [Bunyan](https://github.com/trentm/node-bunyan) logging!
* [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be exposed via [Gopher](docs/servers/gopher.md), or [NNTP](docs/servers/nntp.md)!
* [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/web-server.md). Legacy X/Y/Z modem also supported!
* [Message networks](./docs/_docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be exposed via [Gopher](./docs/_docs/servers/contentservers/gopher.md), or [NNTP](./docs/_docs/servers/contentservers/nntp.md)!
* [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](./docs/_docs/servers/contentservers/web-server.md). Legacy X/Y/Z modem also supported!
* Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more!
* ANSI support in the Full Screen Editor (FSE), file descriptions, etc.
* Expandable **achievement system** — BBSing gamified!
* A remote accessible Waiting For Caller (WFC)!
* A remote accessible [Waiting For Caller (WFC)](./docs/_docs/modding/wfc.md)!
...and much much more. Please check out [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) and feel free to request features (or contribute!) features!
## Documentation
[Browse the docs online](https://nuskooler.github.io/enigma-bbs/). Be sure to checkout the [/docs/](./docs/) folder as well for the latest and greatest documentation.
[Browse the docs online](https://nuskooler.github.io/enigma-bbs/). Be sure to checkout the [/docs/](./docs/_docs/) folder as well for the latest and greatest documentation.
## Installation
On most *nix systems simply run the following from your terminal:
@ -49,7 +49,7 @@ If you feel the urge to donate, [you can do so here](https://liberapay.com/NuSko
* 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 **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
* `FSX_ENG` on [fsxNet](https://fsxnet.nz) 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/)
@ -77,7 +77,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested
* [Luciano Ayres](http://www.lucianoayres.com.br/) of [Blocktronics](http://blocktronics.org/), creator of the "Mystery Skulls" default ENiGMA½ theme!
* Sudndeath for Xibalba ANSI work!
* Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!)
* Avon of [Agency BBS](http://bbs.geek.nz/) and [fsxNet](http://bbs.geek.nz/#fsxNet) for putting up with my experiments to his system and for FSX_ENG!
* Avon of [Agency BBS](http://bbs.nz/) and [fsxNet](https://fsxnet.nz) for putting up with my experiments to his system and for FSX_ENG!
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
* [Apam](https://github.com/apamment) of [Magicka](https://magickabbs.com/)
* [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)!

View File

@ -30,7 +30,16 @@ npm install # or simply 'yarn'
1. Check [TROUBLESHOOTING](TROUBLESHOOTING.md) first.
2. 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) if you believe you've found a bug or missing feature.
# 0.0.12-beta to 0.0.13-beta
# Version to Version Notes
> :warning: Be sure to inspect these notes during any upgrades!
## 0.0.13-beta to 0.0.14-beta
* Due to changes to supported algorithms in newer versions of openssl, the default list of supported algorithms for the ssh login server has changed. There are both removed ciphers as well as optional new kex algorithms available now. ***NOTE:*** Changes to supported algorithms are only needed to support keys generated with new versions of openssl, if you already have a ssl key in use you should not have to make any changes to your config.
* Removed ciphers: 'blowfish-cbc', 'arcfour256', 'arcfour128', and 'cast128-cbc'
* Added kex: 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521'
## 0.0.12-beta to 0.0.13-beta
* To enable the new Waiting for Caller (WFC) support, please see [WFC](docs/modding/wfc.md).
* :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`
* Gopher configuration change. See [WHATSNEW](WHATSNEW.md)
@ -60,7 +69,7 @@ npm install # or simply 'yarn'
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
## 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:
```hjson
@ -79,14 +88,14 @@ In addition to these, there are also new options for `term.cp437TermList` and `t
sqlite3 db/file.sqlite3 < ./misc/update/tables_update_2020-11-29.sql
```
# 0.0.10-alpha to 0.0.11-beta
## 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`).
# 0.0.9-alpha to 0.0.10-alpha
## 0.0.9-alpha to 0.0.10-alpha
* Security related files such as private keys and certs are now looked for in `config/security` by default.
* Default archive handler for zip files has switched to InfoZip due to a bug in the latest p7Zip packages causing "volume not found" errors. Ensure you have the InfoZip `zip` and `unzip` commands in ENiGMA's path. You can switch back to 7Zip by overriding `archiveHandler` for `application/zip` in your `config.hjson` under `fileTypes` to `7Zip`.
# 0.0.8-alpha to 0.0.9-alpha
## 0.0.8-alpha to 0.0.9-alpha
* Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha!
* The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well.
* Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork.
@ -113,7 +122,7 @@ webSocket: {
* Similar to the last item, `defaults.general.passwordChar` in `theme.hjson` is now just `defaults.passwordChar`.
# 0.0.7-alpha to 0.0.8-alpha
## 0.0.7-alpha to 0.0.8-alpha
ENiGMA 0.0.8-alpha comes with some structure changes:
* Configuration files are defaulted to `./config`. Related, the `--config` option now points to a configuration **directory**
* `./mods/art` has been moved to `./art/general`
@ -130,17 +139,17 @@ With the above changes, you'll need to to at least:
* Move any certificates, pub/private keys, etc. from `./misc` to `./config`
* Specify user modules as `@userModule:my_module_name`
# 0.0.6-alpha to 0.0.7-alpha
## 0.0.6-alpha to 0.0.7-alpha
No issues
# 0.0.5-alpha to 0.0.6-alpha
## 0.0.5-alpha to 0.0.6-alpha
No issues
# 0.0.4-alpha to 0.0.5-alpha
## 0.0.4-alpha to 0.0.5-alpha
No issues
# 0.0.1-alpha to 0.0.4-alpha
## Node.js 6.x+ LTS is now **required**
## 0.0.1-alpha to 0.0.4-alpha
### Node.js 6.x+ LTS is now **required**
You will need to upgrade Node.js to [6.x+](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V6.md). If using [nvm](https://github.com/creationix/nvm) (you should be!) the process will go something like this:
```bash
nvm install 6
@ -150,7 +159,7 @@ nvm alias default 6
### ES6
Newly written code will use ES6 and a lot of code has started the migration process. Of note is the `MenuModule` class. If you have created a mod that inherits from `MenuModule`, you will need to upgrade your class to ES6.
## Manual Database Upgrade
### Manual Database Upgrade
A few upgrades need to be made to your SQLite databases:
```bash
@ -159,8 +168,8 @@ sqlite3 db/message.sqlite
sqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild');
```
## Archiver Changes
### Archiver Changes
If you have overridden or made additions to archivers in your `config.hjson` you will need to update them. See [Archive Configuration](docs/archive.md) and `core/config.js`
## File Base Configuration
### File Base Configuration
As 0.0.4-alpha contains file bases, you'll want to create a suitable configuration if you wish to use the feature. See [File Base Configuration](docs/file_base.md).

View File

@ -1,18 +1,32 @@
# Whats New
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
## 0.0.14-beta
* The [Web Server](/docs/_docs/servers/contentservers/web-server.md) has made some possibly breaking changes:
* `/static/` prefixes are no longer required. This was a ugly hack.
* Some internal routes such as those used for password resets live within `/_internal/`.
* Routes for the file base now default to `/_f/` prefixed instead of just `/f/`. If `/f/` is in your `config.hjson` you are encouraged to update it!
* Finally, the system will search for `index.html` and `index.htm` in that order, if another suitable route cannot be established.
* CombatNet has shut down, so the module (`combatnet.js`) has been removed.
* The Menu Flag `popParent` has been removed and `noHistory` has been updated to work as expected. In general things should "Just Work", but check your `menu.hjson` entries if you see menu stack issues.
* Various New User Application (NUA) properties are now optional. If you would like to reduce the information users are required, remove optional fields from NUA artwork and collect less. These properties will be stored as "" (empty). Optional properties are as follows: Real name, Birth date, Sex, Location, Affiliations (Affils), Email, and Web address.
* Art handling has been changed to respect the art width contained in SAUCE when present in the case where the terminal width is greater than the art width. This fixes art files that assume wrapping at 80 columns on wide (mostly new utf8) terminals.
## 0.0.13-beta
* **Note for contributors**: ENiGMA has switched to [Prettier](https://prettier.io) for formatting/style. Please see [CONTRIBUTING](CONTRIBUTING.md) and the Prettier website for more information.
* 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.
* **New Waiting For Caller (WFC)** support via the `wfc.js` module.
* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in [UPGRADE](UPGRADE.md).
* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePosition`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in [UPGRADE](UPGRADE.md).
* Many new system statistics available via the StatLog such as current and average load, memory, etc.
* Many new MCI codes: `MB`, `MF`, `LA`, `CL`, `UU`, `FT`, `DD`, `FB`, `DB`, `LC`, `LT`, `LD`, and more. See [MCI](./docs/art/mci.md).
* SyncTERM style font support detection.
* Added a system method to support setting the client encoding from menus, `@systemMethod:setClientEncoding`.
* 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.
* Deprecated Gopher's `messageConferences` configuration key in favor of a easier to deal with `exposedConfAreas` allowing wildcards and exclusions. See [Gopher](./docs/servers/contentservers/gopher.md).
* NNTP write (aka POST) access support for authenticated users over TLS.
* [Advanced MCI formatting](./docs/art/mci.md#mci-formatting)!
* Additional options in the `abracadabra` module for launching doors. See [Local Doors](./docs/modding/local-doors.md)
## 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).

View File

@ -187,7 +187,7 @@
}
mci: {
VM1: {
height: 15,
height: 14
width: 50
itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{location:<19.19}|03{lastLoginTs}"
focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{location:<19.19}{lastLoginTs}"
@ -343,7 +343,7 @@
}
mci: {
VM1: {
height: 14
height: 13
width: 70
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts:<15.16} |15{newIndicator}"
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts:<15.16} {newIndicator}"
@ -416,7 +416,7 @@
}
mci: {
VM1: {
height: 14
height: 12
width: 70
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}"
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
@ -595,7 +595,7 @@
}
}
messageBaseSearchMessageList: {
messageBaseSearchResultsMessageList: {
config: {
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
// Fri Sep 25th
@ -618,7 +618,7 @@
}
mci: {
VM1: {
height: 16
height: 12
width: 71
itemFormat: "|00|15 {msgNum:<4.4} |03{subject:<34.33} {fromUserName:<19.18} |03{ts:<12.12}"
focusItemFormat: "|00|19> |15{msgNum:<4.4} {subject:<34.33} {fromUserName:<19.18} {ts:<12.12}"
@ -783,7 +783,7 @@
}
mci: {
VM1: {
height: 14
height: 12
width: 70
itemFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}"
focusItemFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}"
@ -1214,7 +1214,7 @@
2: {
mci: {
MT1: {
height: 14
height: 13
width: 45
}
@ -1364,4 +1364,4 @@
}
}
}
}
}

View File

@ -9,6 +9,7 @@ const ansi = require('./ansi_term.js');
const { Errors } = require('./enig_error.js');
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
const Log = require('./logger').log;
const Config = require('./config.js').get;
// deps
const async = require('async');
@ -181,6 +182,8 @@ exports.getModule = class AbracadabraModule extends MenuModule {
const exeInfo = {
name: this.config.name,
cmd: this.config.cmd,
preCmd: this.config.preCmd,
preCmdArgs: this.config.preCmdArgs,
cwd: this.config.cwd || paths.dirname(this.config.cmd),
args: this.config.args,
io: this.config.io || 'stdio',
@ -189,55 +192,86 @@ exports.getModule = class AbracadabraModule extends MenuModule {
env: this.config.env,
};
exeInfo.dropFileDir = DropFile.dropFileDirectory(
Config().paths.dropFiles,
this.client
);
exeInfo.userAreaDir = paths.join(
exeInfo.dropFileDir,
this.client.user.getSanitizedName(),
this.config.name.toLowerCase()
);
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, err => {
this._makeDropDirs([exeInfo.dropFileDir, exeInfo.userAreaDir], err => {
if (err) {
Log.error(`Error running "${this.config.name}": ${err.message}`);
Log.warn(
`Failed creating directory ${exeInfo.dropFilePath}: ${err.message}`
);
}
trackDoorRunEnd(doorTracking);
this.decrementActiveDoorNodeInstances();
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
// 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.'
);
}
});
}
this.doorInstance.run(exeInfo, err => {
if (err) {
Log.error(`Error running "${this.config.name}": ${err.message}`);
}
// client may have disconnected while process was active -
// we're done here if so.
if (!this.client.term.output) {
return;
}
trackDoorRunEnd(doorTracking);
this.decrementActiveDoorNodeInstances();
//
// Try to clean up various settings such as scroll regions that may
// have been set within the door
//
this.client.term.rawWrite(
ansi.normal() +
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
ansi.setScrollRegion() +
ansi.goto(this.client.term.termHeight, 0) +
'\r\n\r\n'
);
// 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.'
);
}
});
}
this.autoNextMenu();
// client may have disconnected while process was active -
// we're done here if so.
if (!this.client.term.output) {
return;
}
//
// Try to clean up various settings such as scroll regions that may
// have been set within the door
//
this.client.term.rawWrite(
ansi.normal() +
ansi.goto(
this.client.term.termHeight,
this.client.term.termWidth
) +
ansi.setScrollRegion() +
ansi.goto(this.client.term.termHeight, 0) +
'\r\n\r\n'
);
this.autoNextMenu();
});
});
}
_makeDropDirs(dirs, cb) {
async.forEach(
dirs,
(dir, nextDir) => {
fs.mkdir(dir, { recursive: true }, nextDir);
},
cb
);
}
leave() {
super.leave();
this.decrementActiveDoorNodeInstances();

View File

@ -505,9 +505,9 @@ class Achievements {
getFormatObject(info) {
return {
userName: info.user.username,
userRealName: info.user.properties[UserProps.RealName],
userLocation: info.user.properties[UserProps.Location],
userAffils: info.user.properties[UserProps.Affiliations],
userRealName: info.user.realName(false) || 'N/A',
userLocation: info.user.properties[UserProps.Location] || 'N/A',
userAffils: info.user.properties[UserProps.Affiliations] || 'N/A',
nodeId: info.client.node,
title: info.details.title,
//text : info.global ? info.details.globalText : info.details.text,

View File

@ -24,7 +24,7 @@ function ANSIEscapeParser(options) {
this.graphicRendition = {};
this.parseState = {
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
};
options = miscUtil.valueWithDefault(options, {
@ -37,6 +37,12 @@ function ANSIEscapeParser(options) {
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
this.breakWidth = this.termWidth;
// toNumber takes care of null, undefined etc as well.
let artWidth = _.toNumber(options.artWidth);
if(!(_.isNaN(artWidth)) && artWidth > 0 && artWidth < this.breakWidth) {
this.breakWidth = options.artWidth;
}
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
@ -71,10 +77,25 @@ function ANSIEscapeParser(options) {
self.clearScreen = function () {
self.column = 1;
self.row = 1;
self.positionUpdated();
self.emit('clear screen');
};
self.positionUpdated = function () {
if(self.row > self.termHeight) {
if(this.savedPosition) {
this.savedPosition.row -= self.row - self.termHeight;
}
self.emit('scroll', self.row - self.termHeight);
self.row = self.termHeight;
}
else if(self.row < 1) {
if(this.savedPosition) {
this.savedPosition.row -= self.row - 1;
}
self.emit('scroll', -(self.row - 1));
self.row = 1;
}
self.emit('position update', self.row, self.column);
};
@ -90,8 +111,8 @@ function ANSIEscapeParser(options) {
switch (charCode) {
case CR:
self.emit('literal', text.slice(start, pos));
start = pos;
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
self.column = 1;
@ -105,8 +126,8 @@ function ANSIEscapeParser(options) {
self.column = 1;
}
self.emit('literal', text.slice(start, pos));
start = pos;
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
self.row += 1;
@ -114,13 +135,16 @@ function ANSIEscapeParser(options) {
break;
default:
if (self.column === self.termWidth) {
if (self.column === self.breakWidth) {
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
// If we hit breakWidth before termWidth then we need to force the terminal to go to the next line.
if(self.column < self.termWidth) {
self.emit('literal', '\r\n');
}
self.column = 1;
self.row += 1;
self.positionUpdated();
} else {
self.column += 1;
@ -135,7 +159,7 @@ function ANSIEscapeParser(options) {
//
// Finalize this chunk
//
if (self.column > self.termWidth) {
if (self.column > self.breakWidth) {
self.column = 1;
self.row += 1;
@ -222,7 +246,7 @@ function ANSIEscapeParser(options) {
self.parseState = {
// ignore anything past EOF marker, if any
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
stop: false,
};
};
@ -262,9 +286,47 @@ function ANSIEscapeParser(options) {
opCode = match[2];
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
escape(opCode, args);
// Handle the case where there is no bracket
if(!(_.isNil(match[3]))) {
opCode = match[3];
args = [];
// no bracket
switch(opCode) {
// save cursor position
case '7':
escape('s', args);
break;
// restore cursor position
case '8':
escape('u', args);
break;
// scroll up
case 'D':
escape('S', args);
break;
// move to next line
case 'E':
// functonality is the same as ESC [ E
escape(opCode, args);
break;
// create a tab at current cursor position
case 'H':
literal('\t');
break;
// scroll down
case 'M':
escape('T', args);
break;
}
}
else {
escape(opCode, args);
}
//self.emit('chunk', match[0]);
self.emit('control', match[0], opCode, args);
}
} while (0 !== re.lastIndex);
@ -272,8 +334,8 @@ function ANSIEscapeParser(options) {
if (pos < buffer.length) {
var lastBit = buffer.slice(pos);
// :TODO: check for various ending LF's, not just DOS \r\n
if ('\r\n' === lastBit.slice(-2).toString()) {
// handles either \r\n or \n
if ('\n' === lastBit.slice(-1).toString()) {
switch (self.trailingLF) {
case 'default':
//
@ -281,14 +343,14 @@ function ANSIEscapeParser(options) {
// if we're going to end on termHeight
//
if (this.termHeight === self.row) {
lastBit = lastBit.slice(0, -2);
lastBit = lastBit.slice(0, -1);
}
break;
case 'omit':
case 'no':
case false:
lastBit = lastBit.slice(0, -2);
lastBit = lastBit.slice(0, -1);
break;
}
}
@ -299,48 +361,6 @@ function ANSIEscapeParser(options) {
self.emit('complete');
};
/*
self.parse = function(buffer, savedRe) {
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
// :TODO: move this to "constants" section @ top
var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g;
var pos = 0;
var match;
var opCode;
var args;
// ignore anything past EOF marker, if any
buffer = buffer.split(String.fromCharCode(0x1a), 1)[0];
do {
pos = re.lastIndex;
match = re.exec(buffer);
if(null !== match) {
if(match.index > pos) {
parseMCI(buffer.slice(pos, match.index));
}
opCode = match[2];
args = getArgArray(match[1].split(';'));
escape(opCode, args);
self.emit('chunk', match[0]);
}
} while(0 !== re.lastIndex);
if(pos < buffer.length) {
parseMCI(buffer.slice(pos));
}
self.emit('complete');
};
*/
function escape(opCode, args) {
let arg;
@ -373,6 +393,37 @@ function ANSIEscapeParser(options) {
self.moveCursor(-arg, 0);
break;
// line feed
case 'E':
arg = isNaN(args[0]) ? 1 : args[0];
if(this.row + arg > this.termHeight) {
this.emit('scroll', arg - (this.termHeight - this.row));
self.moveCursor(0, this.termHeight);
}
else {
self.moveCursor(0, arg);
}
break;
// reverse line feed
case 'F':
arg = isNaN(args[0]) ? 1 : args[0];
if(this.row - arg < 1) {
this.emit('scroll', -(arg - this.row));
self.moveCursor(0, 1 - this.row);
}
else {
self.moveCursor(0, -arg);
}
break;
// absolute horizontal cursor position
case 'G':
arg = isNaN(args[0]) ? 1 : args[0];
self.column = Math.max(1, arg);
self.positionUpdated();
break;
case 'f': // horiz & vertical
case 'H': // cursor position
//self.row = args[0] || 1;
@ -383,14 +434,37 @@ function ANSIEscapeParser(options) {
self.positionUpdated();
break;
// save position
case 's':
self.saveCursorPosition();
// erase display/screen
case 'J':
if(isNaN(args[0]) || 0 === args[0]) {
self.emit('erase rows', self.row, self.termHeight);
}
else if (1 === args[0]) {
self.emit('erase rows', 1, self.row);
}
else if (2 === args[0]) {
self.clearScreen();
}
break;
// restore position
case 'u':
self.restoreCursorPosition();
// erase text in line
case 'K':
if(isNaN(args[0]) || 0 === args[0]) {
self.emit('erase columns', self.row, self.column, self.termWidth);
}
else if (1 === args[0]) {
self.emit('erase columns', self.row, 1, self.column);
}
else if (2 === args[0]) {
self.emit('erase columns', self.row, 1, self.termWidth);
}
break;
// insert line
case 'L':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('insert line', self.row, arg);
break;
// set graphic rendition
@ -462,15 +536,52 @@ function ANSIEscapeParser(options) {
self.emit('sgr update', self.graphicRendition);
break; // m
// :TODO: s, u, K
// erase display/screen
case 'J':
// :TODO: Handle other 'J' types!
if (2 === args[0]) {
self.clearScreen();
}
// save position
case 's':
self.saveCursorPosition();
break;
// Scroll up
case 'S':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('scroll', arg);
break;
// Scroll down
case 'T':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('scroll', -arg);
break;
// restore position
case 'u':
self.restoreCursorPosition();
break;
// clear
case 'U':
self.clearScreen();
break;
// delete line
// TODO: how should we handle 'M'?
case 'Y':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('delete line', self.row, arg);
break;
// back tab
case 'Z':
// calculate previous tabstop
self.column = Math.max( 1, self.column - (self.column % 8 || 8) );
self.positionUpdated();
break;
case '@':
// insert column(s)
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('insert columns', self.row, self.column, arg);
break;
}
}
}

View File

@ -208,17 +208,17 @@ module.exports = class ArchiveUtil {
// pty.js doesn't currently give us a error when things fail,
// so we have this horrible, horrible hack:
let err;
proc.once('data', d => {
proc.onData(d => {
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
}
});
proc.once('exit', exitCode => {
proc.onExit(exitEvent => {
return cb(
exitCode
exitEvent.exitCode
? Errors.ExternalProcess(
`${action} failed with exit code: ${exitCode}`
`${action} failed with exit code: ${exitEvent.exitCode}`
)
: err
);
@ -358,10 +358,10 @@ module.exports = class ArchiveUtil {
output += data;
});
proc.once('exit', exitCode => {
if (exitCode) {
proc.onExit(exitEvent => {
if (exitEvent.exitCode) {
return cb(
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
Errors.ExternalProcess(`List failed with exit code: ${exitEvent.exitCode}`)
);
}

View File

@ -41,11 +41,21 @@ const SUPPORTED_ART_TYPES = {
};
function getFontNameFromSAUCE(sauce) {
if (sauce.Character) {
if (sauce && sauce.Character) {
return sauce.Character.fontName;
}
}
function getWidthFromSAUCE(sauce) {
if (sauce && sauce.Character) {
let sauceWidth = _.toNumber(sauce.Character.characterWidth);
if(!(_.isNaN(sauceWidth)) && sauceWidth > 0) {
return sauceWidth;
}
}
return null;
}
function sliceAtEOF(data, eofMarker) {
let eof = data.length;
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
@ -274,6 +284,7 @@ function display(client, art, options, cb) {
mciReplaceChar: options.mciReplaceChar,
termHeight: client.term.termHeight,
termWidth: client.term.termWidth,
artWidth: getWidthFromSAUCE(options.sauce),
trailingLF: options.trailingLF,
startRow: options.startRow,
});
@ -305,6 +316,75 @@ function display(client, art, options, cb) {
}
});
// Remove any MCI's that are in erased rows
ansiParser.on('erase row', (startRow, endRow) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (mciInfo.position[0] >= startRow && mciInfo.position[0] <= endRow) {
delete mciMap[mapKey];
}
});
});
// Remove any MCI's that are in erased columns
ansiParser.on('erase columns', (row, startCol, endCol) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (
mciInfo.position[0] === row &&
mciInfo.position[1] >= startCol &&
mciInfo.position[1] <= endCol
) {
delete mciMap[mapKey];
}
});
});
ansiParser.on('insert columns', (row, startCol, numCols) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (mciInfo.position[0] === row && mciInfo.position[1] >= startCol) {
mciInfo.position[1] += numCols;
if(mciInfo.position[1] > client.term.termWidth) {
delete mciMap[mapKey];
}
}
});
});
// Clear the screen, removing any MCI's
ansiParser.on('clear screen', () => {
_.forEach(mciMap, (mciInfo, mapKey) => {
delete mciMap[mapKey];
});
});
ansiParser.on('scroll', (scrollY) => {
_.forEach(mciMap, (mciInfo) => {
mciInfo.position[0] -= scrollY;
});
});
ansiParser.on('insert line', (row, numLines) => {
_.forEach(mciMap, (mciInfo) => {
if (mciInfo.position[0] >= row) {
mciInfo.position[0] += numLines;
}
});
});
ansiParser.on('delete line', (row, numLines) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (mciInfo.position[0] >= row) {
if(mciInfo.position[0] < row + numLines) {
// unlike scrolling, the rows are actually gone,
// so we need to delete any MCI's that are in them
delete mciMap[mapKey];
}
else {
mciInfo.position[0] -= numLines;
}
}
});
});
ansiParser.on('literal', literal => client.term.write(literal, false));
ansiParser.on('control', control => client.term.rawWrite(control));

View File

@ -87,7 +87,7 @@ function getActiveConnectionList(
//
entry.text = ac.user?.username || 'N/A';
entry.userName = ac.user?.username || 'N/A';
entry.realName = ac.user?.getProperty(UserProps.RealName) || 'N/A';
entry.realName = ac.user?.realName(false) || 'N/A';
entry.location = ac.user?.getProperty(UserProps.Location) || 'N/A';
entry.affils = entry.affiliation =
ac.user?.getProperty(UserProps.Affiliations) || 'N/A';

View File

@ -1,136 +0,0 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const { MenuModule } = require('../core/menu_module.js');
const { resetScreen } = require('../core/ansi_term.js');
const { Errors } = require('./enig_error.js');
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
// deps
const async = require('async');
const RLogin = require('rlogin');
exports.moduleInfo = {
name: 'CombatNet',
desc: 'CombatNet Access Module',
author: 'Dave Stephens',
};
exports.getModule = class CombatNetModule extends MenuModule {
constructor(options) {
super(options);
// establish defaults
this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513;
}
initSequence() {
const self = this;
async.series(
[
function validateConfig(callback) {
return self.validateConfigFields(
{
host: 'string',
password: 'string',
bbsTag: 'string',
rloginPort: 'number',
},
callback
);
},
function establishRloginConnection(callback) {
self.client.term.write(resetScreen());
self.client.term.write('Connecting to CombatNet, please wait...\n');
let doorTracking;
const restorePipeToNormal = function () {
if (self.client.term.output) {
self.client.term.output.removeListener(
'data',
sendToRloginBuffer
);
if (doorTracking) {
trackDoorRunEnd(doorTracking);
}
}
};
const rlogin = new RLogin({
clientUsername: self.config.password,
serverUsername: `${self.config.bbsTag}${self.client.user.username}`,
host: self.config.host,
port: self.config.rloginPort,
terminalType: self.client.term.termClient,
terminalSpeed: 57600,
});
// If there was an error ...
rlogin.on('error', err => {
self.client.log.info(
`CombatNet rlogin client error: ${err.message}`
);
restorePipeToNormal();
return callback(err);
});
// If we've been disconnected ...
rlogin.on('disconnect', () => {
self.client.log.info('Disconnected from CombatNet');
restorePipeToNormal();
return callback(null);
});
function sendToRloginBuffer(buffer) {
rlogin.send(buffer);
}
rlogin.on(
'connect',
/* The 'connect' event handler will be supplied with one argument,
a boolean indicating whether or not the connection was established. */
function (state) {
if (state) {
self.client.log.info('Connected to CombatNet');
self.client.term.output.on('data', sendToRloginBuffer);
doorTracking = trackDoorRunBegin(self.client);
} else {
return callback(
Errors.General(
'Failed to establish establish CombatNet connection'
)
);
}
}
);
// If data (a Buffer) has been received from the server ...
rlogin.on('data', data => {
self.client.term.rawWrite(data);
});
// connect...
rlogin.connect();
// note: no explicit callback() until we're finished!
},
],
err => {
if (err) {
self.client.log.warn({ error: err.message }, 'CombatNet error');
}
// if the client is still here, go to previous
self.prevMenu();
}
);
}
};

View File

@ -188,22 +188,15 @@ module.exports = () => {
//
// 1 - Generate a Private Key (PK):
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
// To generate a secure PK, issue the following command:
//
// > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
// -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \
// -out ./config/security/ssh_private_key.pem -aes128
//
// (The above is a more modern equivalent of the following):
// > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048
// For information on generating a key, see:
// https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html#generate-a-ssh-private-key
//
// 2 - Set 'privateKeyPass' to the password you used in step #1
//
// 3 - Finally, set 'enabled' to 'true'
//
// Additional reading:
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
// - https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html
//
privateKeyPem: paths.join(
__dirname,
@ -222,14 +215,18 @@ module.exports = () => {
//
algorithms: {
kex: [
'curve25519-sha256',
'curve25519-sha256@libssh.org',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group14-sha1',
'diffie-hellman-group1-sha1',
// Group exchange not currnetly supported
// 'diffie-hellman-group-exchange-sha256',
// 'diffie-hellman-group-exchange-sha1',
'curve25519-sha256',
'curve25519-sha256@libssh.org',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
],
cipher: [
'aes128-ctr',
@ -242,12 +239,7 @@ module.exports = () => {
'aes256-cbc',
'aes192-cbc',
'aes128-cbc',
'blowfish-cbc',
'3des-cbc',
'arcfour256',
'arcfour128',
'cast128-cbc',
'arcfour',
],
hmac: [
'hmac-sha2-256',
@ -945,8 +937,10 @@ module.exports = () => {
],
web: {
path: '/f/',
routePath: '/f/[a-zA-Z0-9]+$',
// if you change the /_f/ prefix here, ensure something
// non-colliding with other routes is utilized
path: '/_f/',
routePath: '^/_f/[a-zA-Z0-9]+$',
expireMinutes: 1440, // 1 day
},

View File

@ -11,6 +11,7 @@ const decode = require('iconv-lite').decode;
const createServer = require('net').createServer;
const paths = require('path');
const _ = require('lodash');
const async = require('async');
module.exports = class Door {
constructor(client) {
@ -56,8 +57,10 @@ module.exports = class Door {
run(exeInfo, cb) {
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
if ('socket' === this.io && !this.sockServer) {
return cb(Errors.UnexpectedState('Socket server is not running'));
if ('socket' === this.io) {
if(!this.sockServer) {
return cb(Errors.UnexpectedState('Socket server is not running'));
}
} else if ('stdio' !== this.io) {
return cb(Errors.Invalid(`"${this.io}" is not a valid io type!`));
}
@ -67,91 +70,141 @@ module.exports = class Door {
const formatObj = {
dropFile: exeInfo.dropFile,
dropFilePath: exeInfo.dropFilePath,
dropFileDir: exeInfo.dropFileDir,
userAreaDir: exeInfo.userAreaDir,
node: exeInfo.node.toString(),
srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1',
userId: this.client.user.userId.toString(),
userName: this.client.user.getSanitizedName(),
userNameRaw: this.client.user.username,
termWidth: this.client.term.termWidth,
termHeight: this.client.term.termHeight,
cwd: cwd,
};
const args = exeInfo.args.map(arg => stringFormat(arg, formatObj));
this.client.log.info(
{ cmd: exeInfo.cmd, args, io: this.io },
`Executing external door (${exeInfo.name})`
);
const spawnOptions = {
cols: this.client.term.termWidth,
rows: this.client.term.termHeight,
cwd: cwd,
env: exeInfo.env,
encoding: null, // we want to handle all encoding ourself
};
try {
this.doorPty = pty.spawn(exeInfo.cmd, args, {
cols: this.client.term.termWidth,
rows: this.client.term.termHeight,
cwd: cwd,
env: exeInfo.env,
encoding: null, // we want to handle all encoding ourself
});
} catch (e) {
return cb(e);
}
async.series(
[
callback => {
if (!_.isString(exeInfo.preCmd)) {
return callback(null);
}
//
// PID is launched. Make sure it's killed off if the user disconnects.
//
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
if (
this.doorPty &&
this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')
) {
this.client.log.info(
{ pid: this.doorPty.pid },
'User has disconnected; Killing door process.'
);
this.doorPty.kill();
}
});
const preCmdArgs = (exeInfo.preCmdArgs || []).map(arg =>
stringFormat(arg, formatObj)
);
this.client.log.debug(
{ processId: this.doorPty.pid },
'External door process spawned'
);
this.client.log.info(
{ cmd: exeInfo.preCmd, args: preCmdArgs },
`Executing external door pre-command (${exeInfo.name})`
);
if ('stdio' === this.io) {
this.client.log.debug('Using stdio for door I/O');
try {
const prePty = pty.spawn(
exeInfo.preCmd,
preCmdArgs,
spawnOptions
);
this.client.term.output.pipe(this.doorPty);
this.doorPty.onData(this.doorDataHandler.bind(this));
this.doorPty.once('close', () => {
return this.restoreIo(this.doorPty);
});
} else if ('socket' === this.io) {
this.client.log.debug(
{
srvPort: this.sockServer.address().port,
srvSocket: this.sockServerSocket,
prePty.onExit(exitEvent => {
const {exitCode, signal} = exitEvent;
this.client.log.info(
{ exitCode, signal },
'Door pre-command exited'
);
return callback(null);
});
} catch (e) {
return callback(e);
}
},
'Using temporary socket server for door I/O'
);
}
callback => {
this.client.log.info(
{ cmd: exeInfo.cmd, args, io: this.io },
`Executing external door (${exeInfo.name})`
);
this.doorPty.once('exit', exitCode => {
this.client.log.info({ exitCode: exitCode }, 'Door exited');
try {
this.doorPty = pty.spawn(exeInfo.cmd, args, spawnOptions);
} catch (e) {
return cb(e);
}
if (this.sockServer) {
this.sockServer.close();
//
// PID is launched. Make sure it's killed off if the user disconnects.
//
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
if (
this.doorPty &&
this.client.session.uniqueId ===
_.get(evt, 'client.session.uniqueId')
) {
this.client.log.info(
{ pid: this.doorPty.pid },
'User has disconnected; Killing door process.'
);
this.doorPty.kill();
}
});
this.client.log.debug(
{ processId: this.doorPty.pid },
'External door process spawned'
);
if ('stdio' === this.io) {
this.client.log.debug('Using stdio for door I/O');
this.client.term.output.pipe(this.doorPty);
this.doorPty.onData(this.doorDataHandler.bind(this));
this.doorPty.onExit( (/*exitEvent*/) => {
return this.restoreIo(this.doorPty);
});
} else if ('socket' === this.io) {
this.client.log.debug(
{
srvPort: this.sockServer.address().port,
srvSocket: this.sockServerSocket,
},
'Using temporary socket server for door I/O'
);
}
this.doorPty.onExit(exitEvent => {
const {exitCode, signal} = exitEvent;
this.client.log.info({ exitCode, signal }, 'Door exited');
if (this.sockServer) {
this.sockServer.close();
}
// we may not get a close
if ('stdio' === this.io) {
this.restoreIo(this.doorPty);
}
this.doorPty.removeAllListeners();
delete this.doorPty;
return callback(null);
});
},
],
() => {
return cb(null);
}
// we may not get a close
if ('stdio' === this.io) {
this.restoreIo(this.doorPty);
}
this.doorPty.removeAllListeners();
delete this.doorPty;
return cb(null);
});
);
}
doorDataHandler(data) {

View File

@ -34,8 +34,15 @@ module.exports = class DropFile {
this.baseDir = baseDir;
}
static dropFileDirectory(baseDir, client) {
return paths.join(baseDir, 'node' + client.node);
}
get fullPath() {
return paths.join(this.baseDir, 'node' + this.client.node, this.fileName);
return paths.join(
DropFile.dropFileDirectory(this.baseDir, this.client),
this.fileName
);
}
get fileName() {

View File

@ -167,17 +167,17 @@ class ScheduledEvent {
return cb(e);
}
proc.once('exit', exitCode => {
if (exitCode) {
proc.onExit(exitEvent => {
if (exitEvent.exitCode) {
Log.warn(
{ eventName: this.name, action: this.action, exitCode: exitCode },
{ eventName: this.name, action: this.action, exitCode: exitEvent.exitCode },
'Bad exit code while performing scheduled event action'
);
}
return cb(
exitCode
exitEvent.exitCode
? Errors.ExternalProcess(
`Bad exit code while performing scheduled event action: ${exitCode}`
`Bad exit code while performing scheduled event action: ${exitEvent.exitCode}`
)
: null
);

View File

@ -2,10 +2,8 @@
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const { MenuModule, MenuFlags } = require('./menu_module.js');
const ansi = require('./ansi_term.js');
const theme = require('./theme.js');
const FileEntry = require('./file_entry.js');
const stringFormat = require('./string_format.js');
const FileArea = require('./file_base_area.js');
@ -77,6 +75,8 @@ exports.getModule = class FileAreaList extends MenuModule {
this.fileList = _.get(options, 'extraArgs.fileList');
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
this.setMergedFlag(MenuFlags.NoHistory);
if (this.fileList) {
// we'll need to adjust position as well!
this.fileListPosition = 0;

View File

@ -2,7 +2,7 @@
'use strict';
// enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule;
const { MenuModule, MenuFlags } = require('./menu_module.js');
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js');
@ -24,6 +24,8 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
constructor(options) {
super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.menuMethods = {
selectArea: (formData, extraArgs, cb) => {
const filterCriteria = {
@ -34,7 +36,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
extraArgs: {
filterCriteria: filterCriteria,
},
menuFlags: ['popParent', 'mergeFlags'],
menuFlags: [ MenuFlags.NoHistory ],
};
return this.gotoMenu(

View File

@ -2,10 +2,8 @@
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const { MenuModule, MenuFlags } = require('./menu_module.js');
const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js');
const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors;
const FileAreaWeb = require('./file_area_web.js');
@ -38,6 +36,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
constructor(options) {
super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.dlQueue = new DownloadQueue(this.client);
if (_.has(options, 'lastMenuResult.sentFileIds')) {

View File

@ -121,7 +121,6 @@ exports.getModule = class FileBaseSearch extends MenuModule {
extraArgs: {
filterCriteria: filterCriteria,
},
menuFlags: ['popParent'],
};
return this.gotoMenu(

View File

@ -2,7 +2,7 @@
'use strict';
// ENiGMA½
const { MenuModule } = require('./menu_module.js');
const { MenuModule, MenuFlags } = require('./menu_module.js');
const FileEntry = require('./file_entry.js');
const FileArea = require('./file_base_area.js');
const { renderSubstr } = require('./string_util.js');
@ -65,6 +65,9 @@ const MciViewIds = {
exports.getModule = class FileBaseListExport extends MenuModule {
constructor(options) {
super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.config = Object.assign(
{},
_.get(options, 'menuConfig.config'),

View File

@ -2,10 +2,8 @@
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const { MenuModule, MenuFlags } = require('./menu_module.js');
const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js');
const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors;
const FileAreaWeb = require('./file_area_web.js');
@ -40,6 +38,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
constructor(options) {
super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.dlQueue = new DownloadQueue(this.client);
this.menuMethods = {

View File

@ -485,13 +485,10 @@ exports.getModule = class TransferFileModule extends MenuModule {
}
});
externalProc.once('close', () => {
return this.restorePipeAfterExternalProc();
});
externalProc.once('exit', exitCode => {
externalProc.onExit(exitEvent => {
const {exitCode, signal} = exitEvent;
this.client.log.debug(
{ cmd: cmd, args: args, exitCode: exitCode },
{ cmd: cmd, args: args, exitCode, signal },
'Process exited'
);

View File

@ -167,6 +167,7 @@ exports.FullScreenEditorModule =
var newFocusViewId;
if (errMsgView) {
if (err) {
errMsgView.clearText();
errMsgView.setText(err.message);
if (MciViewIds.header.subject === err.view.getId()) {
@ -183,6 +184,13 @@ exports.FullScreenEditorModule =
return cb(null);
},
editModeEscPressed: function (formData, extraArgs, cb) {
const errMsgView = self.viewControllers.header.getView(
MciViewIds.header.errorMsg
);
if (errMsgView) {
errMsgView.clearText();
}
self.footerMode =
'editor' === self.footerMode ? 'editorMenu' : 'editor';
@ -982,11 +990,7 @@ exports.FullScreenEditorModule =
const area = getMessageAreaByTag(self.messageAreaTag);
if (fromView !== undefined) {
if (area && area.realNames) {
fromView.setText(
self.client.user.properties[
UserProps.RealName
] || self.client.user.username
);
fromView.setText(self.client.user.realName());
} else {
fromView.setText(self.client.user.username);
}
@ -1054,7 +1058,7 @@ exports.FullScreenEditorModule =
posView.setText(
_.padStart(String(pos.row + 1), 2, '0') +
',' +
_.padEnd(String(pos.col + 1), 2, '0')
_.padStart(String(pos.col + 1), 2, '0')
);
this.client.term.rawWrite(ansi.restorePos());
}

View File

@ -135,7 +135,7 @@ module.exports = class Address {
static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
if (m) {
if (m && m[2] && m[3]) {
// start with a 2D
let addr = {
net: parseInt(m[2]),

141
core/goldmine.js Normal file
View File

@ -0,0 +1,141 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const { MenuModule } = require('../core/menu_module');
const { resetScreen } = require('../core/ansi_term');
const { Errors } = require('../core/enig_error');
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util');
// deps
const async = require('async');
const _ = require('lodash');
const RLogin = require('rlogin');
exports.moduleInfo = {
name: 'gOLD mINE',
desc: 'gOLD mINE Community Door Server Module',
author: 'NuSkooler',
};
exports.getModule = class GoldmineModule extends MenuModule {
constructor(options) {
super(options);
this.setConfigWithExtraArgs(options);
// http://goldminebbs.com/
this.config.host = this.config.host || '165.232.153.209';
this.config.rloginPort = this.config.rloginPort || 513;
}
initSequence() {
let clientTerminated = false;
async.series(
[
callback => {
return this.validateConfigFields(
{
host: 'string',
rloginPort: 'number',
bbsTag: 'string',
},
callback
);
},
callback => {
this.client.term.write(resetScreen());
this.client.term.write('Connecting to gOLD mINE, please wait...\n');
const username = this.client.user.getSanitizedName();
let doorTracking;
const rlogin = new RLogin({
clientUsername: username,
serverUsername: `${this.config.bbsTag}${username}`,
host: this.config.host,
port: this.config.rloginPort,
terminalType: '',
terminalSpeed: '',
});
if (
_.isString(this.config.directDoorCode) &&
this.config.directDoorCode.length > 0
) {
rlogin.terminalType = `xtrn=${this.config.directDoorCode}`;
}
const rloginSend = buffer => {
return rlogin.send(buffer);
};
let pipeRestored = false;
const restorePipeAndFinish = err => {
if (pipeRestored) {
return;
}
pipeRestored = true;
if (this.client.term.output) {
this.client.term.output.removeListener('data', rloginSend);
}
if (doorTracking) {
trackDoorRunEnd(doorTracking);
}
return callback(err);
};
rlogin.on('error', err => {
// Eat up RLogin error with terminalSpeed not being a number
if (err === 'RLogin: invalid terminalSpeed argument.') {
return;
}
this.client.log.info(
`gOLD mINE rlogin client error: ${err.message || err}`
);
return restorePipeAndFinish(err);
});
rlogin.on('disconnect', () => {
this.client.log.info('Disconnected from gOLD mINE');
return restorePipeAndFinish(null);
});
rlogin.on('connect', connected => {
if (!connected) {
return callback(
Errors.General(
'Failed to establish connection to gOLD mINE'
)
);
}
this.client.log.info('Connected to gOLD mINE');
this.client.term.output.on('data', rloginSend);
doorTracking = trackDoorRunBegin(this.client);
});
rlogin.on('data', data => {
this.client.term.rawWrite(data);
});
// connect...
rlogin.connect();
},
],
err => {
if (err) {
this.client.log.warn({ error: err.message }, 'gOLD mINE error');
}
this.prevMenu();
}
);
}
};

View File

@ -33,7 +33,6 @@ MCIViewFactory.UserViewCodes = [
'ET',
'ME',
'MT',
'PL',
'BT',
'VM',
'HM',
@ -129,21 +128,6 @@ MCIViewFactory.prototype.createFromMCI = function (mci) {
view = new MultiLineEditTextView(options);
break;
// Pre-defined Label (Text View)
// :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove
case 'PL':
if (mci.args.length > 0) {
options.text = getPredefinedMCIValue(this.client, mci.args[0]);
if (options.text) {
setOption(1, 'textStyle');
setOption(2, 'justify');
setWidth(3);
view = new TextView(options);
}
}
break;
// Button
case 'BT':
if (mci.args.length > 0) {

View File

@ -20,6 +20,23 @@ const assert = require('assert');
const _ = require('lodash');
const iconvDecode = require('iconv-lite').decode;
const MenuFlags = {
// When leaving this menu to load/chain to another, remove this
// menu from history. In other words, the fallback from
// the next menu would *not* be this one, but the previous.
NoHistory: 'noHistory',
// Generally used in code only: Request that any flags from menu.hjson
// are merged in to the total set of flags vs overriding the default.
MergeFlags: 'mergeFlags',
// Forward this menu's 'extraArgs' to the next.
ForwardArgs: 'forwardArgs',
};
exports.MenuFlags = MenuFlags;
exports.MenuModule = class MenuModule extends PluginModule {
constructor(options) {
super(options);
@ -42,6 +59,17 @@ exports.MenuModule = class MenuModule extends PluginModule {
}
}
setConfigWithExtraArgs(options) {
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
extraArgs: options.extraArgs,
});
}
setMergedFlag(flag) {
this.menuConfig.config.menuFlags.push(flag);
this.menuConfig.config.menuFlags = [...new Set([...this.menuConfig.config.menuFlags, MenuFlags.MergeFlags])];
}
static get InterruptTypes() {
return {
Never: 'never',

View File

@ -5,12 +5,12 @@
const loadMenu = require('./menu_util.js').loadMenu;
const { Errors, ErrorReasons } = require('./enig_error.js');
const { getResolvedSpec } = require('./menu_util.js');
const { MenuFlags } = require('./menu_module.js');
// deps
const _ = require('lodash');
const assert = require('assert');
// :TODO: Stack is backwards.... top should be most recent! :)
const bunyan = require('bunyan');
module.exports = class MenuStack {
constructor(client) {
@ -27,19 +27,11 @@ module.exports = class MenuStack {
}
peekPrev() {
if (this.stackSize > 1) {
return this.stack[this.stack.length - 2];
}
return this.stack[this.stack.length - 2];
}
top() {
if (this.stackSize > 0) {
return this.stack[this.stack.length - 1];
}
}
get stackSize() {
return this.stack.length;
return this.stack[this.stack.length - 1];
}
get currentModule() {
@ -56,13 +48,13 @@ module.exports = class MenuStack {
return cb(
Array.isArray(menuConfig.next)
? Errors.MenuStack(
'No matching condition for "next"',
ErrorReasons.NoConditionMatch
)
'No matching condition for "next"',
ErrorReasons.NoConditionMatch
)
: Errors.MenuStack(
'Invalid or missing "next" member in menu config',
ErrorReasons.InvalidNextMenu
)
'Invalid or missing "next" member in menu config',
ErrorReasons.InvalidNextMenu
)
);
}
@ -81,7 +73,6 @@ module.exports = class MenuStack {
prev(cb) {
const menuResult = this.top().instance.getMenuResult();
// :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current
const previousModuleInfo = this.pop(); // get previous
@ -129,7 +120,7 @@ module.exports = class MenuStack {
client: self.client,
};
if (currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
if (currentModuleInfo && currentModuleInfo.menuFlags.includes(MenuFlags.ForwardArgs)) {
loadOpts.extraArgs = currentModuleInfo.extraArgs;
} else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
@ -138,7 +129,6 @@ module.exports = class MenuStack {
loadMenu(loadOpts, (err, modInst) => {
if (err) {
// :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err);
} else {
@ -151,22 +141,6 @@ module.exports = class MenuStack {
return;
}
//
// Handle deprecated 'options' block by merging to config and warning user.
// :TODO: Remove in 0.0.10+
//
if (modInst.menuConfig.options) {
self.client.log.warn(
{ options: modInst.menuConfig.options },
'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions'
);
Object.assign(
modInst.menuConfig.config || {},
modInst.menuConfig.options
);
delete modInst.menuConfig.options;
}
//
// If menuFlags were supplied in menu.hjson, they should win over
// anything supplied in code.
@ -180,9 +154,9 @@ module.exports = class MenuStack {
// in code we can ask to merge in
if (
Array.isArray(options.menuFlags) &&
options.menuFlags.includes('mergeFlags')
options.menuFlags.includes(MenuFlags.MergeFlags)
) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
menuFlags = [...new Set(options.menuFlags)]; // make unique
}
}
@ -193,12 +167,8 @@ module.exports = class MenuStack {
currentModuleInfo.instance.leave();
if (currentModuleInfo.menuFlags.includes('noHistory')) {
this.pop();
}
if (menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current
if (currentModuleInfo.menuFlags.includes(MenuFlags.NoHistory)) {
this.pop().instance.leave(); // leave & remove current from stack
}
}
@ -214,17 +184,19 @@ module.exports = class MenuStack {
modInst.restoreSavedState(options.savedState);
}
const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name;
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
', '
)})`;
}
return name;
});
if (self.client.log.level() <= bunyan.TRACE) {
const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name;
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
', '
)})`;
}
return name;
});
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
}
modInst.enter();

View File

@ -763,6 +763,11 @@ module.exports = class Message {
}
persist(cb) {
const containsNonWhitespaceCharacterRegEx = /\S/;
if (!containsNonWhitespaceCharacterRegEx.test(this.message)) {
return cb(Errors.Invalid('Empty message'));
}
if (!this.isValid()) {
return cb(Errors.Invalid('Cannot persist invalid message!'));
}

View File

@ -332,7 +332,10 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
publicExportAreas,
(exportArea, nextExportArea) => {
const area = getMessageAreaByTag(exportArea.areaTag);
const conf = getMessageConferenceByTag(area.confTag);
let conf;
if (area) {
conf = getMessageConferenceByTag(area.confTag);
}
if (!area || !conf) {
// :TODO: remove from user properties - this area does not exist
this.client.log.warn(

View File

@ -113,7 +113,6 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
const returnNoResults = () => {
return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags: ['popParent'] },
cb
);
};
@ -160,7 +159,6 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
messageList,
noUpdateLastReadId: true,
},
menuFlags: ['popParent'],
};
return this.gotoMenu(

View File

@ -55,6 +55,8 @@ const helpText = `
|03/|11topic |03<message> |08- |07Set the room topic
|03/|11bbses |08& |03/|11info <id> |08- |07Info about BBS's connected
|03/|11meetups |08- |07Info about MRC MeetUps
|03/|11quote |08- |07Send raw command to server
|03/|11help |08- |07Server-side commands help
---
|03/|11l33t |03<your message> |08- |07l337 5p34k
|03/|11kewl |03<your message> |08- |07BBS KeWL SPeaK
@ -375,6 +377,18 @@ exports.getModule = class mrcModule extends MenuModule {
'|08' + currentTime + '|00 ' + message.body + '|00'
);
}
// Deliver PrivMsg
else if (
message.to_user.toLowerCase() == this.state.alias.toLowerCase()
) {
const currentTime = moment().format(
this.client.currentTheme.helpers.getTimeFormat()
);
this.addMessageToChatLog(
'|08' + currentTime + '|00 ' + message.body + '|00'
);
}
}
this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea);
@ -540,6 +554,46 @@ exports.getModule = class mrcModule extends MenuModule {
this.sendServerMessage('LIST');
break;
// Allow support for new server commands without change to client
case 'quote':
this.sendServerMessage(`${message.substr(7)}`);
break;
/**
* Process known additional server commands directly
*/
case 'afk':
this.sendServerMessage(`AFK ${message.substr(5)}`);
break;
case 'roomconfig':
this.sendServerMessage(`ROOMCONFIG ${message.substr(12)}`);
break;
case 'roompass':
this.sendServerMessage(`ROOMPASS ${message.substr(12)}`);
break;
case 'status':
this.sendServerMessage(`STATUS ${message.substr(8)}`);
break;
case 'lastseen':
this.sendServerMessage(`LASTSEEN ${message.substr(10)}`);
break;
case 'help':
this.sendServerMessage(`HELP ${message.substr(6)}`);
break;
case 'statistics':
case 'changelog':
case 'listbans':
case 'listmutes':
case 'routing':
this.sendServerMessage(cmd[0].toUpperCase());
break;
case 'quit':
return this.prevMenu();

View File

@ -2,7 +2,7 @@
'use strict';
// ENiGMA½
const { MenuModule } = require('./menu_module.js');
const { MenuModule, MenuFlags } = require('./menu_module.js');
const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js');
@ -29,6 +29,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
constructor(options) {
super(options);
// always include noHistory flag
this.setMergedFlag(MenuFlags.NoHistory);
this.initList();
this.menuMethods = {
@ -49,7 +52,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
extraArgs: {
areaTag: area.areaTag,
},
menuFlags: ['popParent', 'noHistory'],
menuFlags: [ MenuFlags.NoHistory ],
};
return this.gotoMenu(

View File

@ -14,6 +14,39 @@ exports.moduleInfo = {
author: 'NuSkooler',
};
const MciViewIds = {
header: {
from: 1,
to: 2,
subject: 3,
errorMsg: 4,
modTimestamp: 5,
msgNum: 6,
msgTotal: 7,
customRangeStart: 10, // 10+ = customs
},
body: {
message: 1,
},
// :TODO: quote builder MCIs - remove all magic #'s
// :TODO: consolidate all footer MCI's - remove all magic #'s
ViewModeFooter: {
MsgNum: 6,
MsgTotal: 7,
// :TODO: Just use custom ranges
},
quoteBuilder: {
quotedMsg: 1,
// 2 NYI
quoteLines: 3,
},
};
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
constructor(options) {
super(options);
@ -42,19 +75,25 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
],
function complete(err) {
if (err) {
// :TODO:... sooooo now what?
} else {
// note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info(
{
to: msg.toUserName,
subject: msg.subject,
uuid: msg.messageUuid,
},
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
const errMsgView = self.viewControllers.header.getView(
MciViewIds.header.errorMsg
);
if (errMsgView) {
errMsgView.setText(err.message);
}
return cb(err);
}
// note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info(
{
to: msg.toUserName,
subject: msg.subject,
uuid: msg.messageUuid,
},
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
);
return self.nextMenu(cb);
}
);

View File

@ -2,7 +2,7 @@
'use strict';
// ENiGMA½
const { MenuModule } = require('./menu_module.js');
const { MenuModule, MenuFlags } = require('./menu_module.js');
const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js');
@ -26,6 +26,9 @@ exports.getModule = class MessageConfListModule extends MenuModule {
constructor(options) {
super(options);
// always include noHistory flag
this.setMergedFlag(MenuFlags.NoHistory);
this.initList();
this.menuMethods = {
@ -49,7 +52,7 @@ exports.getModule = class MessageConfListModule extends MenuModule {
extraArgs: {
confTag: conf.confTag,
},
menuFlags: ['popParent', 'noHistory'],
menuFlags: [ MenuFlags.NoHistory ],
};
return this.gotoMenu(

View File

@ -70,6 +70,14 @@ exports.getModule = class MessageListModule extends (
this.menuMethods = {
selectMessage: (formData, extraArgs, cb) => {
if (!Array.isArray(this.config?.messageList)) {
this.client.log.error(
{ formData },
'No message list is available to select from!'
);
return cb(null);
}
if (MciViewIds.allViews.msgList === formData.submitId) {
// 'messageIndex' or older deprecated 'message' member
this.initialFocusIndex = _.get(
@ -315,9 +323,16 @@ exports.getModule = class MessageListModule extends (
let msgNum = 1;
self.config.messageList.forEach((listItem, index) => {
listItem.msgNum = msgNum++;
listItem.ts = moment(listItem.modTimestamp).format(
dateTimeFormat
);
try {
listItem.ts = moment(listItem.modTimestamp).format(
dateTimeFormat
);
} catch (e) {
self.client.log.warn(
`Error parsing "${listItem.modTimestamp}"; expected timestamp: ${e.message}`
);
listItem.ts = moment().format(dateTimeFormat);
}
const isNew = _.isBoolean(listItem.isNew)
? listItem.isNew
: listItem.messageId > self.lastReadId;

View File

@ -2,7 +2,7 @@
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const { MenuModule, MenuFlags } = require('./menu_module');
const Message = require('./message.js');
const UserProps = require('./user_property.js');
const { filterMessageListByReadACS } = require('./message_area.js');
@ -16,14 +16,12 @@ exports.moduleInfo = {
exports.getModule = class MyMessagesModule extends MenuModule {
constructor(options) {
super(options);
this.setMergedFlag(MenuFlags.NoHistory);
}
initSequence() {
const filter = {
toUserName: [
this.client.user.username,
this.client.user.getProperty(UserProps.RealName),
],
toUserName: [this.client.user.username, this.client.user.realName()],
sort: 'modTimestamp',
resultType: 'messageList',
limit: 1024 * 16, // we want some sort of limit...
@ -49,7 +47,6 @@ exports.getModule = class MyMessagesModule extends MenuModule {
if (!this.messageList || 0 === this.messageList.length) {
return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags: ['popParent'] }
);
}
@ -58,7 +55,6 @@ exports.getModule = class MyMessagesModule extends MenuModule {
messageList: this.messageList,
noUpdateLastReadId: true,
},
menuFlags: ['popParent'],
};
return this.gotoMenu(

View File

@ -13,6 +13,7 @@ const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name: 'NUA',
@ -95,15 +96,15 @@ exports.getModule = class NewUserAppModule extends MenuModule {
areaTag = areaTag || '';
newUser.properties = {
[UserProps.RealName]: formData.value.realName,
[UserProps.RealName]: formData.value.realName || '',
[UserProps.Birthdate]: getISOTimestampString(
formData.value.birthdate
formData.value.birthdate || moment()
),
[UserProps.Sex]: formData.value.sex,
[UserProps.Location]: formData.value.location,
[UserProps.Affiliations]: formData.value.affils,
[UserProps.EmailAddress]: formData.value.email,
[UserProps.WebAddress]: formData.value.web,
[UserProps.Sex]: formData.value.sex || '',
[UserProps.Location]: formData.value.location || '',
[UserProps.Affiliations]: formData.value.affils || '',
[UserProps.EmailAddress]: formData.value.email || '',
[UserProps.WebAddress]: formData.value.web || '',
[UserProps.AccountCreated]: getISOTimestampString(),
[UserProps.MessageConfTag]: confTag,

View File

@ -182,6 +182,14 @@ General Information:
MessageBase: `usage: oputil.js mb <action> [<arguments>]
Actions:
list-confs List conferences and areas
post PATH Posts a message file specified in PATH.
PATH must point to a UTF-8 encoded JSON file
containing 'to', 'from', 'subject', 'areaTag', and
'body'. If 'timestamp' is present, the system will
attempt to use it.
areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail
NetMail is sent to supplied address with the supplied command(s). Multi-part commands
@ -194,6 +202,9 @@ Actions:
packet in the directory specified by PATH. The QWK
BBS ID will be obtained by the final component of PATH.
list-confs arguments:
--areas Include areas within each message conference.
import-areas arguments:
--conf CONF_TAG Conference tag in which to import areas
--network NETWORK Network name/key to associate FTN areas

View File

@ -692,6 +692,162 @@ function exportQWKPacket() {
);
}
const listConferences = () => {
initConfigAndDatabases(err => {
if (err) {
return console.error(err.reason ? err.reason : err.message);
}
const { getSortedAvailMessageConferences } = require('../../core/message_area');
const conferences = getSortedAvailMessageConferences(null, { noClient: true });
for (let conf of conferences) {
console.info(`${conf.confTag} - ${conf.conf.name}`);
if (!argv.areas) {
continue;
}
for (let areaTag of Object.keys(conf.conf.areas)) {
console.info(` ${areaTag} - ${conf.conf.areas[areaTag].name}`);
}
}
});
};
const postMessage = () => {
const inputFile = argv._[argv._.length - 1];
if (argv._.length < 3 || !inputFile || 0 === inputFile.length) {
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
}
async.waterfall(
[
callback => {
return initConfigAndDatabases(callback);
},
callback => {
fs.readFile(inputFile, { encoding: 'utf-8' }, (err, jsonData) => {
if (err) {
return callback(err);
}
let messageJson;
try {
messageJson = JSON.parse(jsonData);
} catch (e) {
return callback(e);
}
for (let f of ['to', 'from', 'subject', 'body', 'areaTag']) {
if (!_.isString(messageJson[f])) {
return callback(
Errors.MissingConfig(
`Missing "${f}" field in message JSON`
)
);
}
messageJson[f] = messageJson[f].trim();
if (messageJson[f].length === 0 && f !== 'subject') {
return callback(
Errors.Invalid(
`"${messageJson[f]}" is not a valid value for the "${f}" field`
)
);
}
}
const { getMessageAreaByTag } = require('../../core/message_area');
const area = getMessageAreaByTag(messageJson.areaTag);
if (!area) {
return callback(
Errors.DoesNotExist(
`Area "${messageJson.areaTag}" does not exist`
)
);
}
const { getAddressedToInfo } = require('../../core/mail_util');
const Message = require('../../core/message');
const toInfo = getAddressedToInfo(messageJson.to);
const fromInfo = getAddressedToInfo(messageJson.from);
if (fromInfo.flavor !== Message.AddressFlavor.Local) {
return callback(
Errors.Invalid(
'Only local "from" users are currently supported'
)
);
}
let modTimestamp;
if (_.isString(messageJson.timestamp)) {
modTimestamp = moment(messageJson.timestamp);
}
if (!modTimestamp || !modTimestamp.isValid()) {
modTimestamp = moment();
}
const message = new Message({
toUserName: messageJson.to,
fromUserName: messageJson.from,
subject: messageJson.subject,
message: messageJson.body,
areaTag: messageJson.areaTag,
modTimestamp,
});
if (toInfo.flavor !== Message.AddressFlavor.Local) {
message.setExternalFlavor(toInfo.flavor);
message.setRemoteToUser(toInfo.remote);
return callback(null, area, message);
}
const User = require('../../core/user');
User.getUserIdAndNameByLookup(
message.toUserName,
(err, toUserId, toUserName) => {
if (err) {
return callback(
Errors.DoesNotExist(
`User "${message.toUserName}" does not exist.`
)
);
}
message.to = toUserName; // adjust case/etc.
message.setLocalToUserId(toUserId);
return callback(null, area, message);
}
);
});
},
(area, message, callback) => {
message.persist(err => {
if (!err) {
console.info(
`Message from ${message.fromUserName} to ${message.toUserName}: "${message.subject}" in ${area.name}`
);
}
return callback(err);
});
},
],
err => {
if (err) {
return console.error(err.reason ? err.reason : err.message);
}
}
);
};
function handleMessageBaseCommand() {
function errUsage() {
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
@ -709,6 +865,8 @@ function handleMessageBaseCommand() {
'import-areas': importAreas,
'qwk-dump': dumpQWKPacket,
'qwk-export': exportQWKPacket,
'list-confs': listConferences,
post: postMessage,
}[action] || errUsage
)();
}

View File

@ -22,6 +22,7 @@ const moment = require('moment');
const async = require('async');
exports.getPredefinedMCIValue = getPredefinedMCIValue;
exports.getPredefinedMCIFormatObject = getPredefinedMCIFormatObject;
exports.init = init;
function init(cb) {
@ -532,3 +533,21 @@ function getPredefinedMCIValue(client, code, extra) {
return value;
}
}
function getPredefinedMCIFormatObject(client, text) {
const re = /\{([A-Z]{2})(?:[!:][^}]+)?\}/g;
let m;
const formatObj = {};
while ((m = re.exec(text))) {
const v = getPredefinedMCIValue(client, m[1]);
if (v) {
if (!isNaN(v)) {
formatObj[m[1]] = parseInt(v);
} else {
formatObj[m[1]] = v;
}
}
}
return _.isEmpty(formatObj) ? null : formatObj;
}

View File

@ -172,7 +172,7 @@ exports.getModule = class RumorzModule extends MenuModule {
StatLog.getSystemLogEntries(
SystemLogKeys.UserAddedRumorz,
StatLog.Order.Timestamp,
StatLog.Order.TimestampDesc,
(err, entries) => {
return callback(err, entriesView, entries);
}

View File

@ -219,7 +219,8 @@ exports.getModule = class MrcModule extends ServerModule {
connectedSockets.forEach(client => {
if (
message.to_user == '' ||
message.to_user == client.username ||
// Fix PrivMSG delivery on case mismatch
message.to_user.toUpperCase() == client.username.toUpperCase() ||
message.to_user == 'CLIENT' ||
message.from_user == client.username ||
message.to_user == 'NOTME'

View File

@ -557,17 +557,24 @@ exports.getModule = class GopherModule extends ServerModule {
this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`),
this.makeItem(ItemTypes.InfoMessage, '(newest first)'),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...msgList.map(msg =>
this.makeItem(
...msgList.map(msg => {
let m;
try {
m = moment(msg.modTimestamp);
} catch (e) {
this.log.warn(
`Error parsing "${msg.modTimestamp}"; expected timestamp: ${e.message}`
);
m = moment();
}
return this.makeItem(
ItemTypes.TextFile,
`${moment(msg.modTimestamp).format(
'YYYY-MM-DD hh:mma'
)}: ${this.shortenSubject(msg.subject)} (${
msg.fromUserName
} to ${msg.toUserName})`,
`${m.format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(
msg.subject
)} (${msg.fromUserName} to ${msg.toUserName})`,
`/msgarea/${confTag}/${areaTag}/${msg.messageUuid}`
)
),
);
}),
].join('');
this.log.debug({ confTag, areaTag }, 'Gopher serving message list');

View File

@ -15,6 +15,7 @@ const fs = require('graceful-fs');
const paths = require('path');
const mimeTypes = require('mime-types');
const forEachSeries = require('async/forEachSeries');
const findSeries = require('async/findSeries');
const ModuleInfo = (exports.moduleInfo = {
name: 'Web',
@ -74,14 +75,6 @@ exports.getModule = class WebServerModule extends ServerModule {
this.enableHttps = config.contentServers.web.https.enabled || false;
this.routes = {};
if (this.isEnabled() && config.contentServers.web.staticRoot) {
this.addRoute({
method: 'GET',
path: '/static/.*$',
handler: this.routeStaticFile.bind(this),
});
}
}
buildUrl(pathAndQuery) {
@ -210,13 +203,21 @@ exports.getModule = class WebServerModule extends ServerModule {
}
routeRequest(req, resp) {
const route = _.find(this.routes, r => r.matchesRequest(req));
let route = _.find(this.routes, r => r.matchesRequest(req));
if (!route && '/' === req.url) {
return this.routeIndex(req, resp);
if (route) {
return route.handler(req, resp);
} else {
this.tryStaticRoute(req, resp, wasHandled => {
if (!wasHandled) {
this.tryRouteIndex(req, resp, wasHandled => {
if (!wasHandled) {
return this.fileNotFound(resp);
}
});
}
});
}
return route ? route.handler(req, resp) : this.accessDenied(resp);
}
respondWithError(resp, code, bodyText, title) {
@ -256,27 +257,57 @@ exports.getModule = class WebServerModule extends ServerModule {
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
}
routeIndex(req, resp) {
const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html');
return this.returnStaticPage(filePath, resp);
tryRouteIndex(req, resp, cb) {
const tryFiles = Config().contentServers.web.tryFiles || [
'index.html',
'index.htm',
];
findSeries(
tryFiles,
(tryFile, nextTryFile) => {
const fileName = paths.join(
req.url.substr(req.url.lastIndexOf('/', 1)),
tryFile
);
const filePath = this.resolveStaticPath(fileName);
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
return nextTryFile(null, false);
}
const headers = {
'Content-Type':
mimeTypes.contentType(paths.basename(filePath)) ||
mimeTypes.contentType('.bin'),
'Content-Length': stats.size,
};
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
readStream.pipe(resp);
return nextTryFile(null, true);
});
},
(_, wasHandled) => {
return cb(wasHandled);
}
);
}
routeStaticFile(req, resp) {
const fileName = req.url.substr(req.url.indexOf('/', 1));
tryStaticRoute(req, resp, cb) {
const fileName = req.url.substr(req.url.lastIndexOf('/', 1));
const filePath = this.resolveStaticPath(fileName);
return this.returnStaticPage(filePath, resp);
}
returnStaticPage(filePath, resp) {
const self = this;
if (!filePath) {
return this.fileNotFound(resp);
return cb(false);
}
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
return self.fileNotFound(resp);
return cb(false);
}
const headers = {
@ -288,7 +319,9 @@ exports.getModule = class WebServerModule extends ServerModule {
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
return readStream.pipe(resp);
readStream.pipe(resp);
return cb(true);
});
}

View File

@ -4,7 +4,6 @@
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const Errors = require('../core/enig_error.js').Errors;
const ANSI = require('./ansi_term.js');
const Config = require('./config.js').get;
const { getMessageAreaByTag } = require('./message_area.js');
@ -21,6 +20,7 @@ exports.moduleInfo = {
exports.getModule = class ShowArtModule extends MenuModule {
constructor(options) {
super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
extraArgs: options.extraArgs,
});

View File

@ -21,8 +21,10 @@ exports.validateEmailAvail = validateEmailAvail;
exports.validateBirthdate = validateBirthdate;
exports.validatePasswordSpec = validatePasswordSpec;
const emptyFieldError = () => new Error('Field cannot be empty');
function validateNonEmpty(data, cb) {
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
return cb(data && data.length > 0 ? null : emptyFieldError);
}
function validateMessageSubject(data, cb) {
@ -91,7 +93,11 @@ function validateGeneralMailAddressedTo(data, cb) {
// :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
const addressedToInfo = getAddressedToInfo(data);
if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
if (addressedToInfo.name.length === 0) {
return cb(emptyFieldError());
}
if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
return cb(null);
}

View File

@ -11,6 +11,9 @@ const renderSubstr = require('./string_util.js').renderSubstr;
const renderStringLength = require('./string_util.js').renderStringLength;
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds;
const getPredefinedMCIFormatObject =
require('./predefined_mci').getPredefinedMCIFormatObject;
const stringFormat = require('./string_format');
// deps
const util = require('util');
@ -153,6 +156,12 @@ TextView.prototype.setText = function (text, redraw) {
text = text.toString();
}
const formatObj = getPredefinedMCIFormatObject(this.client, text);
if (formatObj) {
// expand before converting
text = stringFormat(text, formatObj);
}
this.text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc.
if (this.maxLength > 0) {
this.text = renderSubstr(this.text, 0, this.maxLength);
@ -170,6 +179,10 @@ TextView.prototype.setText = function (text, redraw) {
};
TextView.prototype.clearText = function () {
if (this.text) {
this.setText(this.fillChar.repeat(this.text.length));
}
this.setText('');
};

View File

@ -2,7 +2,7 @@
'use strict';
// enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule;
const { MenuModule, MenuFlags } = require('./menu_module');
const stringFormat = require('./string_format.js');
const getSortedAvailableFileAreas =
require('./file_base_area.js').getSortedAvailableFileAreas;
@ -76,6 +76,8 @@ exports.getModule = class UploadModule extends MenuModule {
constructor(options) {
super(options);
this.setMergedFlag(MenuFlags.NoHistory);
this.interrupt = MenuModule.InterruptTypes.Never;
if (_.has(options, 'lastMenuResult.recvFilePaths')) {

View File

@ -124,12 +124,29 @@ module.exports = class User {
return isMember;
}
realName(withUsernameFallback = true) {
const realName = this.getProperty(UserProps.RealName);
if (realName) {
return realName;
}
if (withUsernameFallback) {
return this.username;
}
}
getSanitizedName(type = 'username') {
const name =
'real' === type ? this.getProperty(UserProps.RealName) : this.username;
const name = 'real' === type ? this.realName(true) : this.username;
return sanatizeFilename(name) || `user${this.userId.toString()}`;
}
emailAddress() {
const email = this.getProperty(UserProps.EmailAddress);
if (email) {
const realName = this.realName(false);
return realName ? `${realName} <${email}>` : email;
}
}
isAvailable() {
return (this.statusFlags & User.StatusFlags.NotAvailable) == 0;
}

View File

@ -76,7 +76,7 @@ module.exports = class User2FA_OTPWebRegister {
(token, textTemplate, htmlTemplate, callback) => {
const webServer = getWebServer();
const registerUrl = webServer.instance.buildUrl(
`/enable_2fa_otp?token=${token}&otpType=${otpType}`
`/_internal/enable_2fa_otp?token=${token}&otpType=${otpType}`
);
const replaceTokens = s => {
@ -93,9 +93,7 @@ module.exports = class User2FA_OTPWebRegister {
}
const message = {
to: `${
user.getProperty(UserProps.RealName) || user.username
} <${user.getProperty(UserProps.EmailAddress)}>`,
to: user.emailAddress(),
// from will be filled in
subject: '2-Factor Authentication Registration',
text: textTemplate,
@ -170,7 +168,7 @@ module.exports = class User2FA_OTPWebRegister {
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
}
const postUrl = webServer.instance.buildUrl('/enable_2fa_otp');
const postUrl = webServer.instance.buildUrl('/_internal/enable_2fa_otp');
const config = Config();
return webServer.instance.routeTemplateFilePage(
_.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'),
@ -296,12 +294,12 @@ ${backupCodes}
[
{
method: 'GET',
path: '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9_]+$',
path: /^\/_internal\/enable_2fa_otp\?token=[a-f0-9]+&otpType=[a-zA-Z0-9_]+$/,
handler: User2FA_OTPWebRegister.routeRegisterGet,
},
{
method: 'POST',
path: '^\\/enable_2fa_otp$',
path: /^\/_internal\/enable_2fa_otp$/,
handler: User2FA_OTPWebRegister.routeRegisterPost,
},
].forEach(r => {

View File

@ -8,10 +8,10 @@ const theme = require('./theme.js');
const sysValidate = require('./system_view_validate.js');
const UserProps = require('./user_property.js');
const { getISOTimestampString } = require('./database.js');
const EnigAssert = require('./enigma_assert');
// deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const moment = require('moment');
@ -109,24 +109,27 @@ exports.getModule = class UserConfigModule extends MenuModule {
// Handlers
//
saveChanges: function (formData, extraArgs, cb) {
assert(formData.value.password === formData.value.passwordConfirm);
EnigAssert(formData.value.password === formData.value.passwordConfirm);
// cache a copy of |formData| as changing a theme below can invalidate it
formData = _.clone(formData);
const newProperties = {
[UserProps.RealName]: formData.value.realName,
[UserProps.RealName]: formData.value.realName || '',
[UserProps.Birthdate]: getISOTimestampString(
formData.value.birthdate
formData.value.birthdate || moment()
),
[UserProps.Sex]: formData.value.sex,
[UserProps.Location]: formData.value.location,
[UserProps.Affiliations]: formData.value.affils,
[UserProps.EmailAddress]: formData.value.email,
[UserProps.WebAddress]: formData.value.web,
[UserProps.Sex]: formData.value.sex || '',
[UserProps.Location]: formData.value.location || '',
[UserProps.Affiliations]: formData.value.affils || '',
[UserProps.EmailAddress]: formData.value.email || '',
[UserProps.WebAddress]: formData.value.web || '',
[UserProps.TermHeight]: formData.value.termHeight.toString(),
[UserProps.ThemeId]:
self.availThemeInfo[formData.value.theme].themeId,
};
// runtime set theme
// Runtime set theme
theme.setClientTheme(self.client, newProperties.theme_id);
// persist all changes
@ -230,11 +233,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
function populateViews(callback) {
const user = self.client.user;
self.setViewText(
'menu',
MciCodeIds.RealName,
user.properties[UserProps.RealName]
);
self.setViewText('menu', MciCodeIds.RealName, user.realName(false) || '');
self.setViewText(
'menu',
MciCodeIds.BirthDate,

View File

@ -22,7 +22,7 @@ const _ = require('lodash');
const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = `%USERNAME%:
A password reset has been requested for your account on %BOARDNAME%.
* If this was not you, please ignore this email.
* Otherwise, follow this link: %RESET_URL%
`;
@ -121,7 +121,7 @@ class WebPasswordReset {
const sendMail = require('./email.js').sendMail;
const resetUrl = webServer.instance.buildUrl(
`/reset_password?token=${
`/_internal/reset_password?token=${
user.properties[UserProps.EmailPwResetToken]
}`
);
@ -143,9 +143,7 @@ class WebPasswordReset {
}
const message = {
to: `${user.properties[UserProps.RealName] || user.username} <${
user.properties[UserProps.EmailAddress]
}>`,
to: user.emailAddress(),
// from will be filled in
subject: 'Forgot Password',
text: textTemplate,
@ -194,13 +192,13 @@ class WebPasswordReset {
{
// this is the page displayed to user when they GET it
method: 'GET',
path: '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate
path: /^\/_internal\/reset_password\?token=[a-f0-9]+$/,
handler: WebPasswordReset.routeResetPasswordGet,
},
// POST handler for performing the actual reset
{
method: 'POST',
path: '^\\/reset_password$',
path: /^\/_internal\/reset_password$/,
handler: WebPasswordReset.routeResetPasswordPost,
},
].forEach(r => {
@ -269,7 +267,7 @@ class WebPasswordReset {
);
}
const postResetUrl = webServer.instance.buildUrl('/reset_password');
const postResetUrl = webServer.instance.buildUrl('/_internal/reset_password');
const config = Config();
return webServer.instance.routeTemplateFilePage(

View File

@ -89,6 +89,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
toggleVisible: (formData, extraArgs, cb) => {
const visible = this.client.user.isVisible();
this.client.user.setVisibility(!visible);
this.visibilityToggled = true; // we won't restore it in this case
return this._refreshAll(cb);
},
displayHelp: (formData, extraArgs, cb) => {
@ -366,7 +367,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
}
_restoreOpVisibility() {
this.client.user.setVisibility(this.restoreUserIsVisible);
if (!this.visibilityToggled) {
this.client.user.setVisibility(this.restoreUserIsVisible);
}
}
_startRefreshing() {
@ -503,9 +506,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
// Current
currentUserName: this.client.user.username,
currentUserRealName:
this.client.user.getProperty(UserProps.RealName) ||
this.client.user.username,
currentUserRealName: this.client.user.realName(false) || 'N/A',
availIndicator: availIndicator,
visIndicator: visIndicator,
lastLoginUserName: lastLoginStats.userName,

View File

@ -1,19 +1,25 @@
FROM node:14-buster-slim
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:20-bookworm-slim
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETBRANCH
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)
# Just copy the package.json so it only needs to build once
COPY package.json /enigma-bbs/
# Install APT and NPM packages
RUN apt-get update \
&& apt-get install -y \
git \
curl \
build-essential \
python \
python3 \
libssl-dev \
lrzsz \
@ -21,8 +27,22 @@ RUN apt-get update \
lhasa \
unrar-free \
p7zip-full \
dos2unix \
&& npm set progress=false && npm config set depth 0 \
&& npm install -g npm@latest \
&& npm install -g pm2 \
&& cd /enigma-bbs && npm install --only=production \
&& cd /enigma-bbs && npm install
# Do this after npm install to avoid cache-miss on every code change
COPY . /enigma-bbs
# Then run post source copy steps that have to happen every time
RUN dos2unix /enigma-bbs/docker/bin/docker-entrypoint.sh \
&& apt-get remove dos2unix -y \
&& chmod +x /enigma-bbs/docker/bin/docker-entrypoint.sh \
&& cp -f /enigma-bbs/docker/bin/sexyz /usr/local/bin \
&& cd /enigma-bbs \
&& pm2 start main.js \
&& mkdir -p /enigma-bbs-pre/art \
&& mkdir /enigma-bbs-pre/mods \
@ -30,15 +50,11 @@ RUN apt-get update \
&& 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 remove build-essential 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

View File

@ -30,3 +30,4 @@ end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem "webrick"

View File

@ -1,15 +1,15 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (7.0.1)
activesupport (7.0.7.2)
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)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
colorator (1.1.0)
concurrent-ruby (1.1.9)
concurrent-ruby (1.2.2)
cssminify2 (2.0.1)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
@ -19,14 +19,14 @@ GEM
ffi (1.15.5)
forwardable-extended (2.6.0)
gemoji (3.0.1)
html-pipeline (2.14.0)
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
htmlcompressor (0.4.0)
http_parser.rb (0.8.0)
i18n (1.9.1)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jekyll (4.2.1)
jekyll (4.2.2)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@ -49,7 +49,7 @@ GEM
uglifier (~> 4.1)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-sass-converter (2.1.0)
jekyll-sass-converter (2.2.0)
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0)
@ -64,42 +64,46 @@ GEM
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
json (2.6.1)
json (2.6.3)
json-minify (0.0.3)
json (> 0)
kramdown (2.3.1)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.7.1)
liquid (4.0.4)
listen (3.8.0)
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.6-x86_64-linux)
minitest (5.19.0)
nokogiri (1.15.4-aarch64-linux)
racc (~> 1.4)
nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (4.0.6)
racc (1.6.0)
rb-fsevent (0.11.0)
public_suffix (5.0.3)
racc (1.7.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.28.0)
rexml (3.2.6)
rouge (3.30.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)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.8.0)
webrick (1.8.1)
PLATFORMS
aarch64-linux
x86_64-linux
DEPENDENCIES
@ -111,6 +115,7 @@ DEPENDENCIES
jekyll-theme-hacker (~> 0.2.0)
jemoji (~> 0.12.0)
tzinfo-data
webrick
BUNDLED WITH
2.3.5
2.4.19

View File

@ -54,6 +54,7 @@ collections:
- installation/network.md
- installation/testing.md
- installation/production.md
- installation/development.md
- configuration/creating-config.md
- configuration/sysop-setup.md
- configuration/config-files.md
@ -92,7 +93,6 @@ collections:
- 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
@ -131,4 +131,5 @@ collections:
- admin/oputil.md
- admin/updating.md
- troubleshooting/monitoring-logs.md
- troubleshooting/ssh-troubleshooting.md

View File

@ -323,7 +323,7 @@ qwk-export arguments:
| Action | Description | Examples |
|-----------|-------------------|---------------------------------------|
| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file. Optionally maps areas to FTN networks. | `./oputil.js config import-areas /some/path/l33tnet.na` |
| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file. Optionally maps areas to FTN networks. | `./oputil.js mb import-areas /some/path/l33tnet.na` |
| `areafix` | Utility for sending AreaFix mails without logging into the system | |
| `qwk-dump` | Dump a QWK packet to stdout | `./oputil.js mb qwk-dump /path/to/XIBALBA.QWK` |
| `qwk-export` | Export messages to a QWK packet | `./oputil.js mb qwk-export /path/to/XIBALBA.QWK` |

View File

@ -108,7 +108,7 @@ There are many predefined MCI codes that can be used anywhere on the system (pla
| `NM` | Count of new messages **address to the current user** across all message areas in which they have access |
| `NP` | Count of new private mail to the current user |
| `IA` | Indicator as to rather the current user is **available** or not. See also `getStatusAvailIndicators()` in [Themes](themes.md) |
| `IV` | Indicator as to rather the curent user is **visible** or not. See also `getStatusVisibleIndicators()` in [Themes](themes.md) |
| `IV` | Indicator as to rather the current user is **visible** or not. See also `getStatusVisibleIndicators()` in [Themes](themes.md) |
| `PI` | Ingress bytes for the current process (since ENiGMA started up) |
| `PE` | Egress bytes for the current process (since ENiGMA started up) |
@ -123,11 +123,9 @@ Some additional special case codes also exist:
| `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. |
> :information_source: More are added all
the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js)
for a full listing.
> :information_source: More are added all the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) for a full listing.
:memo: Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc.
> :memo: Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc.
## Views
@ -146,7 +144,6 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu
| `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.
@ -183,21 +180,21 @@ Predefined MCI codes and other Views can have properties set via `menu.hjson` an
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** below |
| `focusTextStyle` | Sets focus text style. See **Text Styles** below. |
| `textStyle` | Sets the standard (non-focus) text style. See [Text Styles](#text-styles) below |
| `focusTextStyle` | Sets focus text style. See [Text Styles](#text-styles) below |
| `itemSpacing` | Used to separate items in menus such as Vertical Menu and Horizontal Menu Views. |
| `height` | Sets the height of views such as menus that may be > 1 character in height |
| `width` | Sets the width of a view |
| `focus` | If set to `true`, establishes initial focus |
| `text` | (initial) text of a view |
| `text` | Set's the view's text if applicable, such as a [TextView](./views/text_view.md) or [EditTextView](./views/edit_text_view.md) amongst others. See [MCI Formatting](#mci-formatting) below for advanced formatting options using the |
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** below |
| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** below |
| `itemFormat` | Sets the format for a list entry. See [Entry Formatting](#entry-formatting) below |
| `focusItemFormat` | Sets the format for a focused list entry. See [Entry Formatting](#entry-formatting) below |
These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files!
### Custom Properties
Often a module will provide custom properties that receive format objects (See **Entry Formatting** below). Custom property formatting can be declared in the `config` block. For example, `browseInfoFormat10`..._N_ (where _N_ is up to 99) in the `file_area_list` module received a fairly extensive format object that contains `{fileName}`, `{estReleaseYear}`, etc.
Often a module will provide custom properties that receive format objects (See [Entry Formatting](#entry-formatting) below). Custom property formatting can be declared in the `config` block. For example, `browseInfoFormat10`..._N_ (where _N_ is up to 99) in the `file_area_list` module received a fairly extensive format object that contains `{fileName}`, `{estReleaseYear}`, etc.
### Text Styles
@ -219,6 +216,21 @@ Standard style types available for `textStyle` and `focusTextStyle`:
### Entry Formatting
Various strings can be formatted using a syntax that allows width & precision specifiers, text styling, etc. Depending on the context, various elements can be referenced by `{name}`. Additional text styles can be supplied as well. The syntax is largely modeled after Python's [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language).
#### MCI Formatting
For more advanced layouts, you may want to apply formatting to MCI codes. In this case, an alternative syntax is supported similar to standard [Entry Formatting](#entry-formatting).
MCI codes can be surrounded by `{` and `}` in the `text` field in your `theme.hjson` for a Text Label (`%TL` aka [Text View](./views/text_view.md)). Some examples:
```hjson
text: "|00|07{BN!stylel33t}" // render board name in "l33t" text
// MCI codes that produce a number can use appropriate stylers as well
text: "|00|07{SD:,}"
// ...or perhaps
text: "|00:07{SD!countWithAbbr}"
```
> :bulb: MCI formatting also applies when programmatically calling [setText()](https://github.com/NuSkooler/enigma-bbs/blob/6710bf8c084487be2ee1d46d72a05d17a7b166f4/core/text_view.js#L148) of [TextView's](./views/text_view.md) and derived views.
### Additional Text Styles
Some of the text styles mentioned above are also available in the mini format language:

View File

@ -1,49 +0,0 @@
---
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")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
PL1: {
textStyle: upper
}
```
</div>
</details>

View File

@ -15,7 +15,7 @@ A text label view supports displaying simple text on a screen.
| Property | Description |
|-------------|--------------|
| `text` | Sets the text to display on the label |
| `text` | Sets the text to display on the label. See [MCI Formatting](../mci.md#mci-formatting) for information for advanced formatting. |
| `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 |

View File

@ -6,6 +6,16 @@ title: Access Condition System (ACS)
## Access Condition System (ACS)
ENiGMA½ uses an Access Condition System (ACS) that is both familiar to oldschool BBS operators and has it's own style. With ACS, SysOp's are able to control access to various areas of the system based on various conditions such as group membership, connection type, etc. Various touch points in the system are configured to allow for `acs` checks. In some cases ACS is a simple boolean check while others (via ACS blocks) allow to define what conditions must be true for certain _rights_ such as `read` and `write` (though others exist as well).
## Group Membership
ENiGMA½ does not utilize legacy "security levels" (see note below) but instead utilizes a group system. Users may be long to one or more groups which can be checked by the `GM` ACS (See [ACS Codes](#acs-codes) below). Two special groups exist out of the box:
1. `users`: Any regular user
2. `sysops`: System Operators. The first user (your root, or admin) will alwasy belong to this group.
You do not need to explicitly create groups: By checking for them via ACS, and adding members to a group, they implicitly exist within the system. You may use as many groups within your system as you would like. See ['optuil user group'](../admin/oputil.md#user) for information adding and removing users to groups.
> :information_source: Many dropfile formats require a security level. As such, the following apply: Root user or users in `sysops` group receive a security level of `100` while standard `users` receive `30`.
## ACS Codes
The following are ACS codes available as of this writing:
@ -24,7 +34,7 @@ The following are ACS codes available as of this writing:
| TW<i>width</i> | Terminal width is >= _width_ |
| TM[_themeId_,...] | User's current theme ID is one of [_themeId_,...] (e.g. `luciano_blocktronics`) |
| TT[_termType_,...] | User's current terminal type is one of [_termType_,...] (`ANSI-BBS`, `utf8`, `xterm`, etc.) |
| ID<i>id</i>, ID[_id_,...] | User's ID is _id_ or oen of [_id_,...] |
| ID<i>id</i>, ID[_id_,...] | User's ID is _id_ or one of [_id_,...] |
| WD<i>weekDay</i>, WD[_weekDay_,...] | Current day of week is _weekDay_ or one of [_weekDay_,...] where `0` is Sunday, `1` is Monday, and so on. |
| AA<i>days</i> | Account is >= _days_ old |
| BU<i>bytes</i> | User has uploaded >= _bytes_ |

View File

@ -59,13 +59,15 @@ The `config` block for a menu entry can contain common members as well as a per-
| `menuFlags` | An array of menu flag(s) controlling menu behavior. See **Menu Flags** below.
#### Menu Flags
The `menuFlags` field of a `config` block can change default behavior of a particular menu.
The `menuFlags` field of a `config` block can change default behavior of a particular menu:
| Flag | Description |
|------|-------------|
| `noHistory` | Prevents the menu from remaining in the menu stack / history. When this flag is set, when the **next** menu falls back, this menu will be skipped and the previous menu again displayed instead. Example: menuA -> menuB(noHistory) -> menuC: Exiting menuC returns the user to menuA. |
| `popParent` | When *this* menu is exited, fall back beyond the parent as well. Often used in combination with `noHistory`. |
| `forwardArgs` | If set, when the next menu is entered, forward any `extraArgs` arguments to *this* menu on to it. |
| `noHistory` | When leaving the current menu to load/chain to another, remove this menu from history. In other words, the fallback from the next menu would *not* be this one, but the previous. |
| `mergeFlags` | Generally used in code only: Request that any flags from `menu.hjson` |
| `forwardArgs` | Forward this menu's `extraArgs` to the next. |
> 💡 In JavaScript code, `MenuFlags` from `menu_module.js` contains constants for these flags.
## Forms

View File

@ -3,7 +3,7 @@ layout: page
title: TIC Support
---
## TIC Support
ENiGMA½ supports FidoNet-Style TIC file attachments by mapping TIC areas to local file areas.
ENiGMA½ supports FidoNet-Style TIC file attachments by mapping external TIC area tags to local file areas.
Under a given node defined in the `ftn_bso` config section in `config.hjson` (see
[BSO Import/Export](../messageareas/bso-import-export.md)), TIC configuration may be supplied:
@ -17,9 +17,9 @@ Under a given node defined in the `ftn_bso` config section in `config.hjson` (se
packetPassword: mypass
encoding: cp437
archiveType: zip
tic: {
tic: { // <--- General TIC config for 46:*
password: TESTY-TEST
uploadBy: Agoranet TIC
uploadBy: AgoraNet TIC
allowReplace: true
}
}
@ -29,7 +29,15 @@ Under a given node defined in the `ftn_bso` config section in `config.hjson` (se
}
```
You then need to configure the mapping between TIC areas you want to carry, and the file base area and storage tag for them to be tossed to. Optionally you can also add hashtags to the tossed files to assist users in searching for files:
Valid `tic` members:
| Item | Required | Description |
|--------|---------------|------------------|
| `password` | :-1: | TIC packet password, if required |
| `uploadedBy` | :-1: | Sets the "uploaded by" field for TIC attachments, for example "AgoraNet TIC" |
| `allowReplace` | :-1: | Set to `true` to allow TIC attachments to replace each other. This is especially handy for things like weekly node list attachments |
Next, we need to configure the mapping between TIC areas you want to carry, and the file base area (and, optionally, specific storage tag) for them to be tossed to. You can also add hashtags to the tossed files to assist users in searching for files:
```hjson
ticAreas: {
@ -41,10 +49,22 @@ ticAreas: {
}
```
Multiple TIC areas can be mapped to a single file base area.
> :information_source: Note that in the example above `agn_node` represents the **external** network area tag, usually represented in all caps. In this case, `AGN_NODE`.
Valid `ticAreas` members under a given node mapping are as follows:
| Item | Required | Description |
|--------|---------------|------------------|
| `areaTag` | :+1: | Specifies the local areaTag in which to place TIC attachments |
| `storageTag` | :-1: | Optionally, set a specific storageTag. If not set, the default for this area will be used. |
| `hashTags` | :-1: | One or more optional hash tags to assign TIC attachments in this area. |
💡 Multiple TIC areas can be mapped to a single file base area.
### Example Configuration
An example configuration linking file base areas, FTN BSO node configuration and TIC area configuration.
Example configuration fragments mapping file base areas, FTN BSO node configuration and TIC area configuration.
```hjson
fileBase: {
@ -79,22 +99,24 @@ scannerTossers: {
}
}
}
ticAreas: {
// here we map AgoraNet AGN_NODE -> local msgNetworks file area
agn_node: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,nodelist
}
agn_info: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,infopack
}
}
}
}
ticAreas: {
agn_node: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,nodelist
}
agn_info: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,infopack
}
}
```
## See Also

View File

@ -0,0 +1,38 @@
---
layout: page
title: Development Environment Setup
---
_Note:_ This is only useful for people who are looking to contribute to the ENiGMA½ source base itself. Those that are just setting up a new BBS system do not need this section.
The easiest way to get started with development on ENiGMA½ is via the pre-configured Visual Studio Code remote docker container environment. This setup will download and configure everything needed with minimal interaction. It also works cross-platform.
* Install [Visual Studio Code](https://code.visualstudio.com/download)
* Install [Docker](https://docs.docker.com/engine/install/)
* Clone the [ENiGMA½](https://github.com/NuSkooler/enigma-bbs) repository.
* Choose "Open Folder" from Visual Studio Code and open the location where you cloned the repository.
That's it! Visual Studio Code should prompt you for everything else that is needed, including some useful extensions for development.
## Tasks
Once it completes, there are a few tasks and run-configs that are useful. Open up the command pallete and search/choose "Tasks> Run Task". From there you can run the following tasks:
### Start Jekyll (ENiGMA½ documentation server)
This task will start the Jekyll server to perform local testing of changes to documentation. After running this task, open a browser to (http://localhost:4000/enigma-bbs/) to see the documentation.
### (re)build Jekyll bundles
When the image is created the Jekyll bundles are installed, so in general there shouldn't be much need to run this task. This is available however in case soemthing goes wrong or you are working on the Jekyll setup itself.
### (re)build node modules
Used to re-generate the node modules. Generally shouldn't be necessary unless something is broken or you are adding/changing versions of dependencies.
### ENiGMA½ new configuration
This task executes `oputil.js` in order to create a new BBS configuration (useful if you have just checked out the code and haven't setup any configuration yet.)
## Run / Debug config
There is also a default "Launch Program" config (hotkey access via F5 / Ctrl-Shift-D.) This will launch ENiGMA½. Once it has launched, access the system via telnet, port 8888 as usual.

View File

@ -71,5 +71,9 @@ Customising the Docker image is easy!
1. Clone the ENiGMA-BBS source.
2. Build the image
```bash
docker build -f ./docker/Dockerfile .
docker build -t enigmabbs -f ./docker/Dockerfile .
```
3. Run the image
```bash
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
```

View File

@ -28,9 +28,9 @@ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh |
Next, install Node.js with NVM:
```bash
nvm install 12
nvm use 12
nvm alias default 12
nvm install 14
nvm use 14
nvm alias default 14
```
If the above steps completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment.

View File

@ -179,7 +179,7 @@ kill-old-partial-files 86400
prescan
# fsxNet - Agency HUB
node 21:1/100@fsxnet -md agency.bbs.nz:24556 SOMEPASS c
node 21:1/100@fsxnet -md fsxnet.nz:24556 SOMEPASS c
# ArakNet
node 10:101/0@araknet -md whq.araknet.xyz:24556 SOMEPASS c

View File

@ -40,22 +40,5 @@ doorParty: {
Fill in `username`, `password`, and `bbsTag` with credentials provided to you and you should be in business!
## The CombatNet Module
The `combatnet` module provides native support for [CombatNet](http://combatnet.us/). Add the following to your menu config:
````hjson
combatNet: {
desc: Using CombatNet
module: combatnet
config: {
bbsTag: CBNxxx
password: XXXXXXXXX
}
}
````
Update `bbsTag` (in the format CBNxxx) and `password` with the details provided when you register, then
you should be ready to rock!
## The Exodus Module
TBC

View File

@ -16,14 +16,16 @@ 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). Can be omitted or set to `none`. |
| `dropFileType` | :-1: | Specifies the type of dropfile to generate (See [Dropfile Types](#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.
| `args` | :-1: | Array of argument(s) to pass to `cmd`. See [Argument Variables](#argument-variables) below for information on variables that can be utilized here. |
| `preCmd` | :-1: | Path to a pre-command executable or script to launch. Executes before `cmd`. |
| `preCmdArgs` | :-1: | Array of argument(s) to pass to `preCmd`. See [Argument Variables](#argument-variables) below for information on variables that can be utilized 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`. |
| `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](#argument-variables) below for more information). Default value is `stdio`. |
| `encoding` | :-1: | Sets the **door's** encoding. Defaults to `cp437`. Linux binaries often produce `utf8`. |
#### Dropfile Types
@ -31,23 +33,28 @@ Dropfile types specified by `dropFileType`:
| Value | Description |
|-------|-------------|
| `none` | No door file is needed |
| `DOOR` | [DOOR.SYS](https://web.archive.org/web/20160325192739/http://goldfndr.home.mindspring.com/dropfile/doorsys.htm)
| `DOOR32` | [DOOR32.SYS](https://raw.githubusercontent.com/NuSkooler/ansi-bbs/master/docs/dropfile_formats/door32_sys.txt)
| `DORINFO` | [DORINFOx.DEF](https://web.archive.org/web/20160321190038/http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm)
#### Argument Variables
The following variables may be used in `args` entries:
The following variables may be used in `args` and `preCmdArgs` entries:
| Variable | Description | Example |
|----------|-------------|---------|
| `{node}` | Current node number. | `1` |
| `{dropFile}` | Dropfile _filename_ only. | `DOOR.SYS` |
| `{dropFilePath}` | Full path to generated dropfile. The system places dropfiles in the path set by `paths.dropFiles` in `config.hjson`. | `C:\enigma-bbs\drop\node1\DOOR.SYS` |
| `{dropFileDir}` | Full path to **directory** containing the generated dropfile. | `/home/enigma-bbs/drop/node1/` |
| `{userAreaDir}` | Full path to a **directory** safe for user-specific save files/etc. | `/home/enigma-bbs/drop/node1/NuSkooler/lord/` |
| `{userId}` | Current user ID. | `420` |
| `{userName}` | [Sanitized](https://www.npmjs.com/package/sanitize-filename) username. Safe for filenames, etc. If the full username is sanitized away, this will resolve to something like "user_1234". | `izard` |
| `{userNameRaw}` | _Raw_ username. May not be safe for filenames! | `\/\/izard` |
| `{srvPort}` | Temporary server port when `io` is set to `socket`. | `1234` |
| `{cwd}` | Current Working Directory. | `/home/enigma-bbs/doors/foo/` |
| `{termHeight}` | Current client term height | `25` |
| `{termWidth}` | Current client term width | `80` |
Example `args` member using some variables described above:
```hjson

Some files were not shown because too many files have changed in this diff Show More