Compare commits
158 Commits
449-nntp-w
...
master
Author | SHA1 | Date |
---|---|---|
Bryan Ashby | 6aa0c8679f | |
Nathan Byrd | 6b169de3ac | |
Nathan Byrd | 362f443290 | |
Nathan Byrd | 8a97f79d8c | |
Nathan Byrd | f17bbe7c5a | |
Bryan Ashby | 8c7df1fe75 | |
Bryan Ashby | b00d5d07dd | |
Bryan Ashby | bbfef536e0 | |
Bryan Ashby | 83636cb894 | |
Bryan Ashby | ca985dd2cb | |
Bryan Ashby | b87ff51160 | |
Bryan Ashby | 746233fa56 | |
Bryan Ashby | c84b3ee1c1 | |
Nathan Byrd | 270c09eb80 | |
Nathan Byrd | f5b0a8bb60 | |
Bryan Ashby | 1a99153431 | |
Nathan Byrd | 88a1b0ea69 | |
Nathan Byrd | 498c4a6082 | |
Nathan Byrd | 577992bbe5 | |
Bryan Ashby | db8bd2f80f | |
anthony | 0161c22401 | |
Nathan Byrd | 402c5d0156 | |
anthony | 5998144071 | |
anthony | d12b0789aa | |
Bryan Ashby | a644672de2 | |
Bryan Ashby | b0542cf51e | |
Nathan Byrd | 0ca22de6e9 | |
Nathan Byrd | 394ac465c8 | |
anthony | 2b6494cfc9 | |
Nathan Byrd | dddbad4541 | |
Nathan Byrd | 1d31b268aa | |
anthony | 0e09ce491c | |
anthony | a6196c38ec | |
Nathan Byrd | 450ba65565 | |
Bryan Ashby | fc107f1552 | |
anthony | f719499c65 | |
anthony | 75fa2026c0 | |
anthony | 5180447e87 | |
Nathan Byrd | 72a8546d74 | |
Bryan Ashby | 19ee4b0553 | |
dependabot[bot] | d3cf82b563 | |
Nathan Byrd | 79a3f4e1ce | |
Nathan Byrd | c0c262c971 | |
Nathan Byrd | 826db2d718 | |
Nathan Byrd | f3c9afc684 | |
Nathan Byrd | 372d84f572 | |
Nathan Byrd | 8029521399 | |
Bryan Ashby | 86049353a9 | |
Nathan Byrd | 855fabe34d | |
anthony | 6748b8966e | |
Nathan Byrd | 5712645299 | |
Nathan Byrd | a99b55abb5 | |
Nathan Byrd | d8f45f9147 | |
Nathan Byrd | 08841528e2 | |
Bryan Ashby | 5e120d1749 | |
Nathan Byrd | 06c3d100d7 | |
Nathan Byrd | 3adbb517e8 | |
Bryan Ashby | bfb6a30860 | |
Nathan Byrd | e87e7ad277 | |
Bryan Ashby | e2bb00b756 | |
Bryan Ashby | f4a90900ae | |
Bryan Ashby | b307e791c3 | |
Nathan Byrd | 6fb77f3a5f | |
Bryan Ashby | e8441408c0 | |
Nathan Byrd | 60ee842c71 | |
Bryan Ashby | de918dd338 | |
Bryan Ashby | 367931cbd6 | |
Bryan Ashby | 1af4a0432b | |
Nathan Byrd | 1bd328452b | |
Nathan Byrd | c2d7abc94e | |
Nathan Byrd | 602d693caf | |
Nathan Byrd | 5f0a03638e | |
Nathan Byrd | 8a6506c475 | |
Bryan Ashby | 97ae34a971 | |
Bryan Ashby | de339c9c52 | |
Nathan Byrd | 82c05c6e69 | |
Nathan Byrd | 5ba1ca1464 | |
Bryan Ashby | 3e1ec4477c | |
Nathan Byrd | 13ae7789a5 | |
Nathan Byrd | 80dcc14a50 | |
Nathan Byrd | 1d00482b02 | |
Nathan Byrd | adc2e1ba98 | |
Nathan Byrd | 8758722b6b | |
Nathan Byrd | 4d70f07fde | |
Nathan Byrd | bf0bf83053 | |
Nathan Byrd | 7d46607ddc | |
Nathan Byrd | ccaaa71e23 | |
Nathan Byrd | 0b157ddd1b | |
Bryan Ashby | 5a4563c799 | |
Bryan Ashby | 9205aaa9ee | |
Bryan Ashby | b11ddfc382 | |
Bryan Ashby | 993bdbe484 | |
Bryan Ashby | 7b4910c415 | |
Bryan Ashby | 18678d3e4d | |
Bryan Ashby | c87543c976 | |
Bryan Ashby | 25e4de5384 | |
Bryan Ashby | c6a6c06972 | |
Bryan Ashby | c12deb86ae | |
Bryan Ashby | 2d1cbac390 | |
Bryan Ashby | 08adf7ccb5 | |
dependabot[bot] | 5857f46d11 | |
dependabot[bot] | a1242b51aa | |
Bryan Ashby | f32bf090fb | |
dependabot[bot] | 8e904f589d | |
Bryan Ashby | 02f91b2621 | |
Bryan Ashby | 1cfbf4fb66 | |
Bryan Ashby | 614fa2d096 | |
Nathan Byrd | 67b5a8d99f | |
Bryan Ashby | f99de19792 | |
Bryan Ashby | 77160434e6 | |
Parker J. Banks | eca900f829 | |
Bryan Ashby | 9bac4f5e3b | |
Parker J. Banks | 8d6de56e91 | |
Bryan Ashby | 78af6fa522 | |
Bryan Ashby | 6ff7d1f545 | |
Bryan Ashby | 97cd0c3063 | |
Bryan Ashby | ac40f63e1f | |
dependabot[bot] | c39cd613ef | |
dependabot[bot] | 7cbe619235 | |
Bryan Ashby | 447be3552c | |
Bryan Ashby | a208d91d4b | |
Stack Fault | 9a4433ee03 | |
dependabot[bot] | ddf412d34c | |
dependabot[bot] | 1bf7617404 | |
Bryan Ashby | 3319b310d3 | |
dependabot[bot] | a5ef7d9da5 | |
Bryan Ashby | 330d7190ff | |
Bryan Ashby | 30f965d981 | |
Bryan Ashby | c8dfecbfcc | |
Bryan Ashby | 6578e55139 | |
Bryan Ashby | c265a44c1a | |
Bryan Ashby | 7f0254701c | |
Bryan Ashby | a4261d2d36 | |
Bryan Ashby | d1f0a12f77 | |
Bryan Ashby | b58c7e7cc6 | |
Bryan Ashby | 433ab17e9f | |
Bryan Ashby | ff616c384f | |
Bryan Ashby | 94da8798cf | |
Bryan Ashby | 32b4c344a8 | |
Bryan Ashby | a03b071256 | |
Bryan Ashby | 065658f6b8 | |
Bryan Ashby | 1021226020 | |
Bryan Ashby | 6710bf8c08 | |
Bryan Ashby | 091af5adea | |
Bryan Ashby | c7568ac897 | |
Bryan Ashby | 0ef3df047a | |
Bryan Ashby | a88cf6c34c | |
Bryan Ashby | b3cf535b9d | |
Bryan Ashby | 73dafdad9f | |
Bryan Ashby | fd01223578 | |
Bryan Ashby | 63514c1290 | |
Bryan Ashby | 8efc605a9e | |
Bryan Ashby | 5f7b81296e | |
Bryan Ashby | cee25a7104 | |
Bryan Ashby | 1025fef346 | |
Bryan Ashby | d8bc02ce46 | |
Bryan Ashby | 6e1c470b69 | |
dependabot[bot] | e5a2044b88 |
|
@ -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
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
filebase
|
||||
db
|
||||
drop
|
||||
file_base
|
||||
logs
|
||||
mail
|
||||
docs/_site
|
||||
docs/.sass-cache
|
|
@ -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
|
|
@ -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}}"
|
|
@ -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
|
|
@ -11,4 +11,5 @@ mail/
|
|||
node_modules/
|
||||
docs/_site/
|
||||
docs/.sass-cache/
|
||||
.vscode/
|
||||
|
||||
docs/.jekyll-cache/
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-vscode-remote.remote-containers",
|
||||
"laktak.hjson"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
20
README.md
20
README.md
|
@ -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)!
|
||||
|
|
37
UPGRADE.md
37
UPGRADE.md
|
@ -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).
|
||||
|
|
16
WHATSNEW.md
16
WHATSNEW.md
|
@ -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).
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
82
core/art.js
82
core/art.js
|
@ -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));
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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
|
||||
},
|
||||
|
||||
|
|
189
core/door.js
189
core/door.js
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -121,7 +121,6 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
|||
extraArgs: {
|
||||
filterCriteria: filterCriteria,
|
||||
},
|
||||
menuFlags: ['popParent'],
|
||||
};
|
||||
|
||||
return this.gotoMenu(
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
||||
|
|
16
core/fse.js
16
core/fse.js
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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!'));
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
54
core/mrc.js
54
core/mrc.js
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
15
core/nua.js
15
core/nua.js
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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('');
|
||||
};
|
||||
|
||||
|
|
|
@ -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')) {
|
||||
|
|
21
core/user.js
21
core/user.js
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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` |
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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 |
|
||||
|
|
|
@ -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_ |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue