Compare commits
213 Commits
various-de
...
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 | |
Bryan Ashby | 8c92f3cc49 | |
Bryan Ashby | c4518c7b94 | |
Bryan Ashby | 1626db3d52 | |
Bryan Ashby | 3155a0cd81 | |
Bryan Ashby | 5f6d70e460 | |
Bryan Ashby | 2cb0970a31 | |
Bryan Ashby | d2dafc4dbc | |
Bryan Ashby | e61be537aa | |
Bryan Ashby | 28a94f842c | |
Bryan Ashby | c105a2601a | |
Bryan Ashby | ad44495469 | |
Bryan Ashby | a1188fb90c | |
Bryan Ashby | 2a0ae05c45 | |
Bryan Ashby | 62bcb8055a | |
Bryan Ashby | 695bb40b17 | |
Bryan Ashby | 2d8c896ad4 | |
Bryan Ashby | 8e17897954 | |
Bryan Ashby | c06b777d34 | |
Bryan Ashby | edfc2f68a4 | |
Bryan Ashby | 4d6bc98a0f | |
Bryan Ashby | 7060bf270b | |
Bryan Ashby | 4a63bc577e | |
Bryan Ashby | 1f130347d5 | |
Bryan Ashby | 92ca571e40 | |
Bryan Ashby | c8df7f3d6b | |
Bryan Ashby | 8be8c21aa8 | |
Bryan Ashby | d4341bf85f | |
Bryan Ashby | 94145c043a | |
Bryan Ashby | 226c261505 | |
dependabot[bot] | e5a2044b88 | |
Bryan Ashby | b971876795 | |
Bryan Ashby | 5b1c11b1bc | |
Bryan Ashby | b48d133229 | |
Bryan Ashby | 56f03ff847 | |
Bryan Ashby | a48f2b4067 | |
Bryan Ashby | 21d0ad9e59 | |
Bryan Ashby | 2e159a0cc6 | |
Bryan Ashby | 1d14b187c2 | |
Bryan Ashby | 8552e2dd35 | |
Bryan Ashby | cc788056ea | |
Bryan Ashby | c9d9b5336c | |
Bryan Ashby | fdfd64976b | |
Bryan Ashby | 1402391234 | |
Bryan Ashby | ad2d5e379a | |
Bryan Ashby | 8adb0363e0 | |
Bryan Ashby | 2cae154977 | |
Bryan Ashby | d13d90e223 | |
Bryan Ashby | c1f5086db0 | |
Bryan Ashby | 56c92b7a86 | |
Bryan Ashby | 81595f87e1 | |
Bryan Ashby | 561f0464f2 | |
Bryan Ashby | ab87ec3936 | |
Bryan Ashby | 1b3933a9d2 | |
Bryan Ashby | f3d0da2075 | |
Bryan Ashby | 695dc3944e | |
Bryan Ashby | 8c68e51ecf |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
32
README.md
32
README.md
|
@ -4,35 +4,37 @@
|
|||
|
||||
ENiGMA½ is a modern BBS software with a nostalgic flair!
|
||||
|
||||
## Features Available Now
|
||||
* Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows)
|
||||
## Features
|
||||
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!
|
||||
* [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!
|
||||
* Support for **2-Factor Authentication** with One-Time-Passwords
|
||||
* [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/_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.
|
||||
* A built in achievement system. BBSing gamified!
|
||||
* Expandable **achievement system** — BBSing gamified!
|
||||
* 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:
|
||||
```
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash
|
||||
```
|
||||
|
||||
|
@ -47,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/)
|
||||
|
||||
|
@ -75,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)!
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# Troubleshooting
|
||||
|
||||
## Installation
|
||||
|
||||
### Compiler Issues
|
||||
Currently a number of ENiGMA½'s NPM dependencies include modules that require C bindings, and thus, may need compiled uf prebuilt binaries are not available on NPM for your system/architecture. This is often the case for older Linux systems, some ARM devices, etc.
|
||||
|
||||
**Example**: Compiling `sqlite3` from source with `npm`:
|
||||
```bash
|
||||
npm rebuild --build-from-source sqlite3
|
||||
```
|
||||
|
||||
With `yarn`:
|
||||
```bash
|
||||
env npm_config_build_from_source=true yarn install sqlite3
|
||||
```
|
||||
|
||||
If you get compiler errors when running `npm install` or `yarn`, you can try rebuilding with compiler overrides.
|
||||
|
||||
**Example**: Overriding compilers for `node-pty` compilation:
|
||||
|
||||
```bash
|
||||
env CC=gcc CXX=gcc npm rebuild --build-from-source node-pty
|
||||
```
|
||||
|
||||
## Upgrades
|
||||
|
||||
### Missing Menu & Theme Entries
|
||||
One thing to be sure and check after an update is your menu/prompt HJSON configurations as well as your theme(s). The default templates are updated alongside features, but you may need to merge in fragments missing from your own.
|
||||
|
||||
See also [Updating](./docs/_docs/admin/updating.md)
|
61
UPGRADE.md
61
UPGRADE.md
|
@ -1,7 +1,7 @@
|
|||
# Introduction
|
||||
This document covers basic upgrade notes for major ENiGMA½ version updates.
|
||||
|
||||
> :information_source: Be sure to read the version-to-version upgrade notes below for each upgrade!
|
||||
> :information_source: **Be sure to read the version-to-version upgrade notes below** for each upgrade!
|
||||
|
||||
# Before Upgrading
|
||||
* Always back up your system! (See [Administration](./docs/admin/administration.md))
|
||||
|
@ -27,38 +27,49 @@ npm install # or simply 'yarn'
|
|||
```
|
||||
|
||||
# Problems
|
||||
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).
|
||||
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.
|
||||
|
||||
# Version to Version Notes
|
||||
> :warning: Be sure to inspect these notes during any upgrades!
|
||||
|
||||
# 0.0.12-beta to 0.0.13-beta
|
||||
## 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)
|
||||
* All features and changes are backwards compatible. There are a few new configuration options in a new `term` section in the configuration. These are all optional, but include the following options in case you use them:
|
||||
|
||||
```hjson
|
||||
{
|
||||
|
||||
term: {
|
||||
// checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals.
|
||||
// Using this with a terminal that does not support cursor position reports results in a 2 second delay
|
||||
// during the connect process, but provides better autoconfiguration of utf-8
|
||||
// checkUtf8Encoding requires the use of cursor
|
||||
// position reports, which are not supported on all terminals.
|
||||
// Using this with a terminal that does not support cursor
|
||||
// position reports results in a 2 second delay during the
|
||||
// connect process, but provides better autoconfiguration of utf-8
|
||||
checkUtf8Encoding: true
|
||||
|
||||
|
||||
// Checking the ANSI home position also requires the use of cursor position reports, which are not
|
||||
// supported on all terminals. Using this with a terminal that does not support cursor position reports
|
||||
// results in a 3 second delay during the connect process, but works around positioning problems with
|
||||
// Checking the ANSI home position also requires the use of
|
||||
// cursor position reports, which are not supported on all
|
||||
/// terminals. Using this with a terminal that does not support
|
||||
// cursor position reports results in a 3 second delay during
|
||||
// the connect process, but works around positioning problems with
|
||||
// non-standard terminals.
|
||||
checkAnsiHomePosition: true
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In addition to these, there are also new options for `term.cp437TermList` and `term.utf8TermList`. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in `config_default.js`
|
||||
|
||||
# 0.0.11-beta to 0.0.12-beta
|
||||
## 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
|
||||
|
@ -77,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.
|
||||
|
@ -111,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`
|
||||
|
@ -128,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
|
||||
|
@ -148,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
|
||||
|
@ -157,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).
|
||||
|
|
17
WHATSNEW.md
17
WHATSNEW.md
|
@ -1,17 +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,51 +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, () => {
|
||||
trackDoorRunEnd(doorTracking);
|
||||
this.decrementActiveDoorNodeInstances();
|
||||
|
||||
// Clean up dropfile, if any
|
||||
if (exeInfo.dropFilePath) {
|
||||
fs.unlink(exeInfo.dropFilePath, err => {
|
||||
if (err) {
|
||||
Log.warn(
|
||||
{ error: err, path: exeInfo.dropFilePath },
|
||||
'Failed to remove drop file.'
|
||||
);
|
||||
}
|
||||
});
|
||||
this._makeDropDirs([exeInfo.dropFileDir, exeInfo.userAreaDir], err => {
|
||||
if (err) {
|
||||
Log.warn(
|
||||
`Failed creating directory ${exeInfo.dropFilePath}: ${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// client may have disconnected while process was active -
|
||||
// we're done here if so.
|
||||
if (!this.client.term.output) {
|
||||
return;
|
||||
}
|
||||
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
||||
|
||||
//
|
||||
// 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.doorInstance.run(exeInfo, err => {
|
||||
if (err) {
|
||||
Log.error(`Error running "${this.config.name}": ${err.message}`);
|
||||
}
|
||||
|
||||
this.autoNextMenu();
|
||||
trackDoorRunEnd(doorTracking);
|
||||
this.decrementActiveDoorNodeInstances();
|
||||
|
||||
// Clean up dropfile, if any
|
||||
if (exeInfo.dropFilePath) {
|
||||
fs.unlink(exeInfo.dropFilePath, err => {
|
||||
if (err) {
|
||||
Log.warn(
|
||||
{ error: err, path: exeInfo.dropFilePath },
|
||||
'Failed to remove drop file.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// client may have disconnected while process was active -
|
||||
// we're done here if so.
|
||||
if (!this.client.term.output) {
|
||||
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));
|
||||
|
||||
|
|
|
@ -513,7 +513,7 @@ Client.prototype.startIdleMonitor = function () {
|
|||
idleLogoutSeconds > 0 &&
|
||||
nowMs - this.lastActivityTime >= idleLogoutSeconds * 1000
|
||||
) {
|
||||
this.emit('idle timeout');
|
||||
this.emit('idle timeout', idleLogoutSeconds);
|
||||
}
|
||||
}, 1000 * 60);
|
||||
};
|
||||
|
@ -598,6 +598,10 @@ Client.prototype.isLocal = function () {
|
|||
};
|
||||
|
||||
Client.prototype.friendlyRemoteAddress = function () {
|
||||
if (!this.remoteAddress) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
// convert any :ffff: IPv4's to 32bit version
|
||||
return this.remoteAddress.replace(/^::ffff:/, '').replace(/^::1$/, 'localhost');
|
||||
};
|
||||
|
|
|
@ -85,13 +85,15 @@ function getActiveConnectionList(
|
|||
//
|
||||
// There may be a connection, but not a logged in user as of yet
|
||||
//
|
||||
if (ac.user.isAuthenticated()) {
|
||||
entry.text = ac.user.username;
|
||||
entry.userName = ac.user.username;
|
||||
entry.realName = ac.user.properties[UserProps.RealName];
|
||||
entry.location = ac.user.properties[UserProps.Location];
|
||||
entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations];
|
||||
entry.text = ac.user?.username || 'N/A';
|
||||
entry.userName = ac.user?.username || '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';
|
||||
|
||||
if (ac.user.isAuthenticated()) {
|
||||
// :TODO: track pre-auth time so we can properly track this
|
||||
const diff = now.diff(
|
||||
moment(ac.user.properties[UserProps.LastLoginTs]),
|
||||
'minutes'
|
||||
|
@ -153,7 +155,7 @@ function addNewClient(client, clientSock) {
|
|||
|
||||
client.log.info(
|
||||
connInfo,
|
||||
`Client connected (${connInfo.serverName}/${connInfo.port})`
|
||||
`Client connected on node ${nodeId} (${connInfo.serverName}/${connInfo.port})`
|
||||
);
|
||||
|
||||
Events.emit(Events.getSystemEvents().ClientConnected, {
|
||||
|
@ -176,7 +178,7 @@ function removeClient(client) {
|
|||
connectionCount: clientConnections.length,
|
||||
nodeId: client.node,
|
||||
},
|
||||
'Client disconnected'
|
||||
`Client disconnected from node ${client.node}`
|
||||
);
|
||||
|
||||
if (client.user && client.user.isValid()) {
|
||||
|
|
|
@ -44,7 +44,7 @@ function ClientTerminal(output) {
|
|||
if (iconv.encodingExists(enc)) {
|
||||
Log.info(
|
||||
{ encoding: enc, currentEncoding: outputEncoding },
|
||||
'Setting client encoding.'
|
||||
`Output encoding changed to ${enc}`
|
||||
);
|
||||
outputEncoding = enc;
|
||||
} else {
|
||||
|
@ -60,6 +60,11 @@ function ClientTerminal(output) {
|
|||
set: function (ttype) {
|
||||
termType = ttype.toLowerCase();
|
||||
|
||||
Log.debug(
|
||||
{ encoding: this.outputEncoding },
|
||||
`Terminal type changed to ${termType}; Adjusting output encoding`
|
||||
);
|
||||
|
||||
if (this.isNixTerm()) {
|
||||
this.outputEncoding = 'utf8';
|
||||
} else {
|
||||
|
@ -69,11 +74,6 @@ function ClientTerminal(output) {
|
|||
// :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification
|
||||
// Windows telnet will send "VTNT". If so, set termClient='windows'
|
||||
// there are some others on the page as well
|
||||
|
||||
Log.debug(
|
||||
{ encoding: this.outputEncoding },
|
||||
'Set output encoding due to terminal type change'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ const ansiQuerySyncTermFontSupport = (client, cb) => {
|
|||
const [_, w] = pos;
|
||||
if (w === 1) {
|
||||
// cursor didn't move
|
||||
client.log.info('Enabling SyncTERM font support');
|
||||
client.log.info(`SyncTERM font support enabled on node ${client.node}`);
|
||||
client.term.syncTermFontsEnabled = true;
|
||||
}
|
||||
},
|
||||
|
|
191
core/door.js
191
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,12 @@ 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!`));
|
||||
}
|
||||
|
||||
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
|
||||
|
@ -65,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;
|
||||
|
@ -345,14 +345,16 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
}
|
||||
|
||||
displayArtDataPrepCallback(name, artData, viewController) {
|
||||
if ('details' === name) {
|
||||
if (name === 'details') {
|
||||
try {
|
||||
this.detailsInfoArea = {
|
||||
top: artData.mciMap.XY2.position,
|
||||
bottom: artData.mciMap.XY3.position,
|
||||
};
|
||||
} catch (e) {
|
||||
throw Errors.DoesNotExist('Missing XY2 and XY3 position indicators!');
|
||||
throw Errors.DoesNotExist(
|
||||
'File listing details %XY2 and/or %XY3 MCI position indicators!'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -734,6 +736,10 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
clearScreen: false,
|
||||
noInput: true,
|
||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
||||
viewOffsets: {
|
||||
col: 0,
|
||||
row: self.detailsInfoArea.top[0] - 1,
|
||||
},
|
||||
},
|
||||
callback
|
||||
);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -428,34 +428,47 @@ module.exports = class FileEntry {
|
|||
return Object.keys(FILE_WELL_KNOWN_META);
|
||||
}
|
||||
|
||||
static findBySha(sha, cb) {
|
||||
static getFileIdsBySha(sha, options = {}, cb) {
|
||||
// full or partial SHA-256
|
||||
const limit = _.isNumber(options.limit) ? `LIMIT ${options.limit}` : '';
|
||||
fileDb.all(
|
||||
`SELECT file_id
|
||||
FROM file
|
||||
WHERE file_sha256 LIKE "${sha}%"
|
||||
LIMIT 2;`, // limit 2 such that we can find if there are dupes
|
||||
WHERE file_sha256 LIKE "${sha}%" ${limit};`,
|
||||
(err, fileIdRows) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (!fileIdRows || 0 === fileIdRows.length) {
|
||||
return cb(Errors.DoesNotExist('No matches'));
|
||||
}
|
||||
|
||||
if (fileIdRows.length > 1) {
|
||||
return cb(Errors.Invalid('SHA is ambiguous'));
|
||||
}
|
||||
|
||||
const fileEntry = new FileEntry();
|
||||
return fileEntry.load(fileIdRows[0].file_id, err => {
|
||||
return cb(err, fileEntry);
|
||||
});
|
||||
return cb(
|
||||
null,
|
||||
(fileIdRows || []).map(r => r.file_id)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static findBySha(sha, cb) {
|
||||
FileEntry.getFileIdsBySha(sha, { limit: 2 }, (err, fileIds) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (!fileIds || 0 === fileIds.length) {
|
||||
return cb(Errors.DoesNotExist('No matches'));
|
||||
}
|
||||
|
||||
if (fileIds.length > 1) {
|
||||
return cb(Errors.Invalid('SHA is ambiguous'));
|
||||
}
|
||||
|
||||
const fileEntry = new FileEntry();
|
||||
return fileEntry.load(fileIds[0], err => {
|
||||
return cb(err, fileEntry);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt to fine a file by an *existing* full path.
|
||||
// Checkums may have changed and are not validated here.
|
||||
static findByFullPath(fullPath, cb) {
|
||||
|
@ -651,6 +664,14 @@ module.exports = class FileEntry {
|
|||
}
|
||||
}
|
||||
|
||||
if (_.isString(filter.fileName) && filter.fileName.length > 0) {
|
||||
const caseSensitive = _.get(filter, 'filenameCaseSensitive', false);
|
||||
const collate = caseSensitive ? '' : 'COLLATE NOCASE';
|
||||
appendWhereClause(
|
||||
`(f.file_name = "${sanitizeString(filter.fileName)}" ${collate})`
|
||||
);
|
||||
}
|
||||
|
||||
// handle e.g. 1998 -> "1998"
|
||||
if (_.isNumber(filter.tags)) {
|
||||
filter.tags = filter.tags.toString();
|
||||
|
|
|
@ -150,7 +150,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
|
||||
this.client.log.info(
|
||||
{ sentFiles: sentFiles },
|
||||
`User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)`
|
||||
`User "${this.client.user.username}" downloaded ${sentFiles.length} file(s)`
|
||||
);
|
||||
}
|
||||
return cb(err);
|
||||
|
@ -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'
|
||||
);
|
||||
|
||||
|
|
20
core/fse.js
20
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());
|
||||
}
|
||||
|
@ -1299,6 +1303,10 @@ exports.FullScreenEditorModule =
|
|||
callingMenu: self,
|
||||
formId: formId,
|
||||
mciMap: artData.mciMap,
|
||||
viewOffsets: {
|
||||
col: 0,
|
||||
row: self.header.height,
|
||||
},
|
||||
};
|
||||
|
||||
self.addViewController(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
|
@ -10,6 +10,7 @@ const UserProps = require('./user_property.js');
|
|||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = class LoginServerModule extends ServerModule {
|
||||
constructor() {
|
||||
|
@ -87,8 +88,12 @@ module.exports = class LoginServerModule extends ServerModule {
|
|||
clientConns.removeClient(client);
|
||||
});
|
||||
|
||||
client.on('idle timeout', () => {
|
||||
client.log.info('User idle timeout expired');
|
||||
client.on('idle timeout', idleLogoutSeconds => {
|
||||
client.log.info(
|
||||
`Node ${client.node} idle timeout of ${moment
|
||||
.duration(idleLogoutSeconds, 'seconds')
|
||||
.humanize()} expired; Kicking`
|
||||
);
|
||||
|
||||
client.menuStack.goto('idleLogoff', err => {
|
||||
if (err) {
|
||||
|
|
|
@ -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',
|
||||
|
@ -663,6 +691,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
callingMenu: this,
|
||||
mciMap: artData.mciMap,
|
||||
formId: formId,
|
||||
viewOffsets: options.viewOffsets,
|
||||
};
|
||||
|
||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||
|
@ -696,17 +725,19 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
return form && form.getView(id);
|
||||
}
|
||||
|
||||
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
|
||||
getCustomViewsWithFilter(formName, startId, options) {
|
||||
options = options || {};
|
||||
|
||||
let textView;
|
||||
const views = [];
|
||||
|
||||
let view;
|
||||
let customMciId = startId;
|
||||
const config = this.menuConfig.config;
|
||||
const endId = options.endId || 99; // we'll fail to get a view before 99
|
||||
|
||||
while (
|
||||
customMciId <= endId &&
|
||||
(textView = this.viewControllers[formName].getView(customMciId))
|
||||
(view = this.viewControllers[formName].getView(customMciId))
|
||||
) {
|
||||
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
|
||||
const format = config[key];
|
||||
|
@ -715,20 +746,35 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
format &&
|
||||
(!options.filter || options.filter.find(f => format.indexOf(f) > -1))
|
||||
) {
|
||||
const text = stringFormat(format, fmtObj);
|
||||
|
||||
if (
|
||||
options.appendMultiLine &&
|
||||
textView instanceof MultiLineEditTextView
|
||||
) {
|
||||
textView.addText(text);
|
||||
} else if (textView.getData() != text) {
|
||||
textView.setText(text);
|
||||
}
|
||||
view.key = key; // cache
|
||||
views.push(view);
|
||||
}
|
||||
|
||||
++customMciId;
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
|
||||
options = options || {};
|
||||
const views = this.getCustomViewsWithFilter(formName, startId, options);
|
||||
const config = this.menuConfig.config;
|
||||
|
||||
views.forEach(view => {
|
||||
const format = config[view.key];
|
||||
const text = stringFormat(format, fmtObj);
|
||||
|
||||
if (options.appendMultiLine && view instanceof MultiLineEditTextView) {
|
||||
view.addText(text);
|
||||
} else {
|
||||
if (view.getData() != text) {
|
||||
view.setText(text);
|
||||
} else {
|
||||
view.redraw();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshPredefinedMciViewsByCode(formName, mciCodes) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ const ADDRESS_FLAVOR = {
|
|||
FTN: 'ftn', // FTN style
|
||||
Email: 'email', // From email
|
||||
QWK: 'qwk', // QWK packet
|
||||
NNTP: 'nntp', // NNTP article POST; often a email address
|
||||
};
|
||||
|
||||
const STATE_FLAGS0 = {
|
||||
|
@ -762,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!'));
|
||||
}
|
||||
|
|
|
@ -363,7 +363,7 @@ function changeMessageConference(client, confTag, cb) {
|
|||
if (!err) {
|
||||
client.log.info(
|
||||
{ confTag: confTag, confName: conf.name, areaTag: areaInfo.areaTag },
|
||||
'Current message conference changed'
|
||||
`${client.node} changed message conference to ${areaInfo.areaTag}`
|
||||
);
|
||||
} else {
|
||||
client.log.warn(
|
||||
|
@ -412,9 +412,9 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
|||
],
|
||||
function complete(err, area) {
|
||||
if (!err) {
|
||||
client.log.info(
|
||||
client.log.debug(
|
||||
{ areaTag: areaTag, area: area },
|
||||
'Current message area changed'
|
||||
`Node ${client.node} changed message area to ${areaTag}`
|
||||
);
|
||||
} else {
|
||||
client.log.warn(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -69,6 +69,18 @@ Actions:
|
|||
info arguments:
|
||||
--security Include security information in output
|
||||
|
||||
list arguments:
|
||||
--sort SORT_BY Specify field to sort by
|
||||
|
||||
Valid SORT_BY values:
|
||||
id : User ID
|
||||
username : Username
|
||||
realname : Real name
|
||||
status : Account status
|
||||
created : Account creation date
|
||||
lastlogin : Last login timestamp
|
||||
logins : Login count
|
||||
|
||||
2fa-otp arguments:
|
||||
--qr-type TYPE Specify QR code type
|
||||
|
||||
|
@ -170,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
|
||||
|
@ -182,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
|
||||
)();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ const async = require('async');
|
|||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const fs = require('fs-extra');
|
||||
const Table = require('easy-table');
|
||||
|
||||
exports.handleUserCommand = handleUserCommand;
|
||||
|
||||
|
@ -340,7 +341,7 @@ function showUserInfo(user) {
|
|||
|
||||
const statusDesc = () => {
|
||||
const status = user.properties[UserProps.AccountStatus];
|
||||
return _.invert(User.AccountStatus)[status] || 'unknown';
|
||||
return _.invert(User.AccountStatus)[status] || 'N/A';
|
||||
};
|
||||
|
||||
const created = () => {
|
||||
|
@ -508,7 +509,6 @@ function listUsers() {
|
|||
// :TODO: --created-since SPEC and --last-called SPEC
|
||||
// --created-since SPEC
|
||||
// SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days"
|
||||
// :TODO: --sort name|id
|
||||
let listWhat;
|
||||
if (argv._.length > 2) {
|
||||
listWhat = argv._[argv._.length - 1];
|
||||
|
@ -516,6 +516,8 @@ function listUsers() {
|
|||
listWhat = 'all';
|
||||
}
|
||||
|
||||
const sortBy = (argv.sort || 'id').toLowerCase();
|
||||
|
||||
const User = require('../../core/user');
|
||||
if (!['all'].concat(Object.keys(User.AccountStatus)).includes(listWhat)) {
|
||||
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
||||
|
@ -527,7 +529,13 @@ function listUsers() {
|
|||
const UserProps = require('../../core/user_property');
|
||||
|
||||
const userListOpts = {
|
||||
properties: [UserProps.AccountStatus],
|
||||
properties: [
|
||||
UserProps.RealName,
|
||||
UserProps.AccountStatus,
|
||||
UserProps.AccountCreated,
|
||||
UserProps.LastLoginTs,
|
||||
UserProps.LoginCount,
|
||||
],
|
||||
};
|
||||
|
||||
User.getUserList(userListOpts, (err, userList) => {
|
||||
|
@ -550,10 +558,94 @@ function listUsers() {
|
|||
});
|
||||
},
|
||||
(userList, callback) => {
|
||||
// default sort: by ID
|
||||
const sortById = (left, right) => {
|
||||
return left.userId - right.userId;
|
||||
};
|
||||
|
||||
const sortByLogin = prop => (left, right) => {
|
||||
return parseInt(right[prop]) - parseInt(left[prop]);
|
||||
};
|
||||
|
||||
const sortByString = prop => (left, right) => {
|
||||
return left[prop].localeCompare(right[prop], {
|
||||
sensitivity: false,
|
||||
numeric: true,
|
||||
});
|
||||
};
|
||||
|
||||
const sortByTimestamp = prop => (left, right) => {
|
||||
return moment(right[prop]) - moment(left[prop]);
|
||||
};
|
||||
|
||||
let sorter;
|
||||
switch (sortBy) {
|
||||
case 'username':
|
||||
sorter = sortByString('userName');
|
||||
break;
|
||||
case 'realname':
|
||||
sorter = sortByString(UserProps.RealName);
|
||||
break;
|
||||
case 'status':
|
||||
sorter = sortByString(UserProps.AccountStatus);
|
||||
break;
|
||||
case 'created':
|
||||
sorter = sortByTimestamp(UserProps.AccountCreated);
|
||||
break;
|
||||
case 'lastlogin':
|
||||
sorter = sortByTimestamp(UserProps.LastLoginTs);
|
||||
break;
|
||||
case 'logins':
|
||||
sorter = sortByLogin(UserProps.LoginCount);
|
||||
break;
|
||||
|
||||
case 'id':
|
||||
default:
|
||||
sorter = sortById;
|
||||
break;
|
||||
}
|
||||
|
||||
userList.sort(sorter);
|
||||
|
||||
const StatusNames = _.invert(User.AccountStatus);
|
||||
|
||||
const propOrNA = (user, prop) => {
|
||||
return user[prop] || 'N/A';
|
||||
};
|
||||
|
||||
const timestampOrNA = (user, prop, format) => {
|
||||
let ts = user[prop];
|
||||
return ts ? moment(ts).format(format) : 'N/A';
|
||||
};
|
||||
|
||||
const makeAccountStatus = status => {
|
||||
return StatusNames[status] || 'N/A';
|
||||
};
|
||||
|
||||
const table = new Table();
|
||||
userList.forEach(user => {
|
||||
console.info(`${user.userId}: ${user.userName}`);
|
||||
table.cell('ID', user.userId);
|
||||
table.cell('Username', user.userName);
|
||||
table.cell('Real Name', user[UserProps.RealName]);
|
||||
table.cell(
|
||||
'Status',
|
||||
makeAccountStatus(user[UserProps.AccountStatus])
|
||||
);
|
||||
table.cell(
|
||||
'Created',
|
||||
timestampOrNA(user, UserProps.AccountCreated, 'YYYY-MM-DD')
|
||||
);
|
||||
table.cell(
|
||||
'Last Login',
|
||||
timestampOrNA(user, UserProps.LastLoginTs, 'YYYY-MM-DD HH::mm')
|
||||
);
|
||||
table.cell('Logins', propOrNA(user, UserProps.LoginCount));
|
||||
|
||||
table.newRow();
|
||||
});
|
||||
|
||||
console.info(table.toString());
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1129,7 +1129,10 @@ function FTNMessageScanTossModule() {
|
|||
],
|
||||
err => {
|
||||
if (err) {
|
||||
Log.warn({ error: err.message }, 'Error exporting message');
|
||||
Log.warn(
|
||||
{ error: err.message },
|
||||
`Error exporting message: ${err.message}`
|
||||
);
|
||||
}
|
||||
return nextMessageOrUuid(null);
|
||||
}
|
||||
|
@ -1600,6 +1603,7 @@ function FTNMessageScanTossModule() {
|
|||
const packetOpts = { keepTearAndOrigin: false }; // needed so we can calc message UUID without these; we'll add later
|
||||
|
||||
let importStats = {
|
||||
packetPath,
|
||||
areaSuccess: {}, // areaTag->count
|
||||
areaFail: {}, // areaTag->count
|
||||
otherFail: 0,
|
||||
|
@ -1639,10 +1643,10 @@ function FTNMessageScanTossModule() {
|
|||
//
|
||||
// No local area configured for this import
|
||||
//
|
||||
// :TODO: Handle the "catch all" area bucket case if configured
|
||||
// :TODO: Handle the "catch all" area bucket case if configured -> email with area info/etc.? catchAll: enabled, areaTag, prefixMsg
|
||||
Log.warn(
|
||||
{ areaTag: areaTag },
|
||||
'No local area configured for this packet file!'
|
||||
`No local message area for "${areaTag}"`
|
||||
);
|
||||
|
||||
// bump generic failure
|
||||
|
@ -1675,9 +1679,7 @@ function FTNMessageScanTossModule() {
|
|||
|
||||
self.appendTearAndOrigin(message);
|
||||
|
||||
const importConfig = {
|
||||
localAreaTag: localAreaTag,
|
||||
};
|
||||
const importConfig = { localAreaTag };
|
||||
|
||||
self.importMailToArea(importConfig, packetHeader, message, err => {
|
||||
if (err) {
|
||||
|
@ -1699,7 +1701,7 @@ function FTNMessageScanTossModule() {
|
|||
uuid: message.messageUuid,
|
||||
MSGID: msgId,
|
||||
},
|
||||
'Not importing non-unique message'
|
||||
`Not importing non-unique message "${message.subject}" to ${localAreaTag}`
|
||||
);
|
||||
|
||||
return next(null);
|
||||
|
@ -1718,15 +1720,34 @@ function FTNMessageScanTossModule() {
|
|||
//
|
||||
// try to produce something helpful in the log
|
||||
//
|
||||
const finalStats = Object.assign(importStats, { packetPath: packetPath });
|
||||
if (err || Object.keys(finalStats.areaFail).length > 0) {
|
||||
if (err) {
|
||||
Object.assign(finalStats, { error: err.message });
|
||||
}
|
||||
const makeCount = obj => {
|
||||
return obj
|
||||
? _.reduce(
|
||||
obj,
|
||||
(sum, c) => {
|
||||
return sum + c;
|
||||
},
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
};
|
||||
|
||||
Log.warn(finalStats, 'Import completed with error(s)');
|
||||
const totalFail = makeCount(importStats.areaFail) + importStats.otherFail;
|
||||
const packetFileName = paths.basename(packetPath);
|
||||
if (err || totalFail > 0) {
|
||||
if (err) {
|
||||
Object.assign(importStats, { error: err.message });
|
||||
}
|
||||
Log.warn(
|
||||
importStats,
|
||||
`Packet ${packetFileName} import reported ${totalFail} error(s)`
|
||||
);
|
||||
} else {
|
||||
Log.info(finalStats, 'Import complete');
|
||||
const totalSuccess = makeCount(importStats.areaSuccess);
|
||||
Log.info(
|
||||
importStats,
|
||||
`Packet ${packetFileName} imported with ${totalSuccess} new message(s)`
|
||||
);
|
||||
}
|
||||
|
||||
cb(err);
|
||||
|
@ -1816,7 +1837,9 @@ function FTNMessageScanTossModule() {
|
|||
path: paths.join(importDir, packetFile),
|
||||
error: err.toString(),
|
||||
},
|
||||
'Failed to import packet file'
|
||||
`Failed to import packet file "${paths.basename(
|
||||
packetFile
|
||||
)}"`
|
||||
);
|
||||
|
||||
rejects.push(packetFile);
|
||||
|
@ -2360,7 +2383,9 @@ function FTNMessageScanTossModule() {
|
|||
reason: err.reason,
|
||||
tic: ticFileInfo.filePath,
|
||||
},
|
||||
'Failed to import/update TIC'
|
||||
`Failed to import/update TIC for "${paths.basename(
|
||||
ticFileInfo.filePath
|
||||
)}"`
|
||||
);
|
||||
} else {
|
||||
Log.info(
|
||||
|
@ -2369,7 +2394,9 @@ function FTNMessageScanTossModule() {
|
|||
file: ticFileInfo.filePath,
|
||||
area: localInfo.areaTag,
|
||||
},
|
||||
'TIC imported successfully'
|
||||
`TIC imported "${paths.basename(ticFileInfo.filePath)}" -> ${
|
||||
localInfo.areaTag
|
||||
}`
|
||||
);
|
||||
}
|
||||
return cb(err);
|
||||
|
@ -2738,6 +2765,7 @@ FTNMessageScanTossModule.prototype.startup = function (cb) {
|
|||
const importSchedule = this.parseScheduleString(
|
||||
this.moduleConfig.schedule.import
|
||||
);
|
||||
|
||||
if (importSchedule) {
|
||||
Log.debug(
|
||||
{
|
||||
|
@ -2766,14 +2794,17 @@ FTNMessageScanTossModule.prototype.startup = function (cb) {
|
|||
glob: `**/${paths.basename(importSchedule.watchFile)}`,
|
||||
});
|
||||
|
||||
const makeImportMsg = (e, path) => {
|
||||
return `Import/toss due to @watch[${e}] "${paths.basename(
|
||||
path
|
||||
)}"`;
|
||||
};
|
||||
|
||||
['change', 'add', 'delete'].forEach(event => {
|
||||
watcher.on(event, (fileName, fileRoot) => {
|
||||
const eventPath = paths.join(fileRoot, fileName);
|
||||
if (
|
||||
paths.join(fileRoot, fileName) ===
|
||||
importSchedule.watchFile
|
||||
) {
|
||||
tryImportNow('Performing import/toss due to @watch', {
|
||||
if (eventPath === importSchedule.watchFile) {
|
||||
tryImportNow(makeImportMsg(event, eventPath), {
|
||||
eventPath,
|
||||
event,
|
||||
});
|
||||
|
@ -2785,12 +2816,16 @@ FTNMessageScanTossModule.prototype.startup = function (cb) {
|
|||
// If the watch file already exists, kick off now
|
||||
// https://github.com/NuSkooler/enigma-bbs/issues/122
|
||||
//
|
||||
fse.exists(importSchedule.watchFile, exists => {
|
||||
if (exists) {
|
||||
tryImportNow('Performing import/toss due to @watch', {
|
||||
eventPath: importSchedule.watchFile,
|
||||
event: 'initial exists',
|
||||
});
|
||||
fse.access(importSchedule.watchFile, fse.constants.R_OK, err => {
|
||||
if (!err) {
|
||||
// exists and we can read
|
||||
tryImportNow(
|
||||
makeImportMsg('exists', importSchedule.watchFile),
|
||||
{
|
||||
eventPath: importSchedule.watchFile,
|
||||
event: 'exists',
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -10,11 +10,13 @@ const {
|
|||
splitTextAtTerms,
|
||||
isAnsi,
|
||||
stripAnsiControlCodes,
|
||||
wildcardMatch,
|
||||
} = require('../../string_util.js');
|
||||
const {
|
||||
getMessageConferenceByTag,
|
||||
getMessageAreaByTag,
|
||||
getMessageListForArea,
|
||||
getAvailableMessageAreasByConfTag,
|
||||
} = require('../../message_area.js');
|
||||
const { sortAreasOrConfs } = require('../../conf_area_util.js');
|
||||
const AnsiPrep = require('../../ansi_prep.js');
|
||||
|
@ -122,7 +124,7 @@ exports.getModule = class GopherModule extends ServerModule {
|
|||
if (isNaN(port)) {
|
||||
this.log.warn(
|
||||
{ port: config.contentServers.gopher.port, server: ModuleInfo.name },
|
||||
'Invalid port'
|
||||
'Invalid Gopher port'
|
||||
);
|
||||
return cb(
|
||||
Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)
|
||||
|
@ -243,14 +245,52 @@ exports.getModule = class GopherModule extends ServerModule {
|
|||
return cb('Not found');
|
||||
}
|
||||
|
||||
isAreaAndConfExposed(confTag, areaTag) {
|
||||
const conf = _.get(Config(), [
|
||||
_getConfigForConferenceTag(confTag) {
|
||||
const sysConfig = Config();
|
||||
let config = _.get(sysConfig, [
|
||||
'contentServers',
|
||||
'gopher',
|
||||
'messageConferences',
|
||||
'exposedConfAreas',
|
||||
confTag,
|
||||
]);
|
||||
return Array.isArray(conf) && conf.includes(areaTag);
|
||||
if (config) {
|
||||
return [config, false]; // new
|
||||
}
|
||||
|
||||
return [
|
||||
_.get(sysConfig, ['contentServers', 'gopher', 'messageConferences', confTag]),
|
||||
true,
|
||||
];
|
||||
}
|
||||
|
||||
isAreaAndConfExposed(confTag, areaTag) {
|
||||
const [confConfig, isLegacy] = this._getConfigForConferenceTag(confTag);
|
||||
|
||||
if (isLegacy) {
|
||||
return Array.isArray(confConfig) && confConfig.includes(areaTag);
|
||||
}
|
||||
|
||||
if (!Array.isArray(confConfig.include)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let exposed = false;
|
||||
for (let rule of confConfig.include) {
|
||||
if (wildcardMatch(areaTag, rule)) {
|
||||
exposed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// may still be excluded
|
||||
for (let rule of confConfig.exclude || []) {
|
||||
if (wildcardMatch(areaTag, rule)) {
|
||||
exposed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return exposed;
|
||||
}
|
||||
|
||||
prepareMessageBody(body, cb) {
|
||||
|
@ -304,7 +344,7 @@ exports.getModule = class GopherModule extends ServerModule {
|
|||
}
|
||||
|
||||
messageAreaGenerator(selectorMatch, cb) {
|
||||
this.log.debug({ selector: selectorMatch[0] }, 'Serving message area content');
|
||||
this.log.trace({ selector: selectorMatch[0] }, 'Message area request');
|
||||
//
|
||||
// Selector should be:
|
||||
// /msgarea - list confs
|
||||
|
@ -314,44 +354,267 @@ exports.getModule = class GopherModule extends ServerModule {
|
|||
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
|
||||
//
|
||||
if (selectorMatch[3] || selectorMatch[4]) {
|
||||
// message selector - display message
|
||||
// message
|
||||
//const raw = selectorMatch[4] ? true : false;
|
||||
// :TODO: support 'raw'
|
||||
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
|
||||
const confTag = selectorMatch[1].substr(1).split('/')[0];
|
||||
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
|
||||
const message = new Message();
|
||||
return this._displayMessage(selectorMatch, msgUuid, confTag, areaTag, cb);
|
||||
} else if (selectorMatch[2]) {
|
||||
// conf/area selector -- list messages in area
|
||||
const confTag = selectorMatch[1].substr(1).split('/')[0];
|
||||
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
|
||||
const area = getMessageAreaByTag(areaTag);
|
||||
return this._listMessagesInArea(selectorMatch, confTag, areaTag, area, cb);
|
||||
} else if (selectorMatch[1]) {
|
||||
// message conference selector -- list areas in this conference
|
||||
const confTag = selectorMatch[1].replace(/\r\n|\//g, '');
|
||||
return this._listExposedMessageConferenceAreas(selectorMatch, confTag, cb);
|
||||
} else {
|
||||
// message area base selector -- list exposed message conferences
|
||||
return this._listExposedMessageConferences(cb);
|
||||
}
|
||||
}
|
||||
|
||||
return message.load({ uuid: msgUuid }, err => {
|
||||
if (err) {
|
||||
this.log.debug(
|
||||
{ uuid: msgUuid },
|
||||
'Attempted access to non-existent message UUID!'
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
_makeAvailableMessageConferencesResponse(messageConferences, cb) {
|
||||
sortAreasOrConfs(messageConferences);
|
||||
|
||||
const response = [
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'),
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, ''),
|
||||
...messageConferences.map(conf =>
|
||||
this.makeItem(
|
||||
ItemTypes.SubMenu,
|
||||
`${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`,
|
||||
`/msgarea/${conf.confTag}`
|
||||
)
|
||||
),
|
||||
].join('');
|
||||
|
||||
this.log.debug('Gopher serving message conference list');
|
||||
return cb(response);
|
||||
}
|
||||
|
||||
_exposedMessageConferenceTags(obj) {
|
||||
return Object.keys(obj || {})
|
||||
.map(confTag =>
|
||||
Object.assign({ confTag }, getMessageConferenceByTag(confTag))
|
||||
)
|
||||
.filter(conf => conf); // remove any baddies
|
||||
}
|
||||
|
||||
_noExposedMessageConferences(cb) {
|
||||
return cb(
|
||||
this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')
|
||||
);
|
||||
}
|
||||
|
||||
// newer format
|
||||
_listExposedMessageConferences(cb) {
|
||||
let exposedConfs = _.get(Config(), 'contentServers.gopher.exposedConfAreas');
|
||||
if (!_.isObject(exposedConfs)) {
|
||||
return this._listExposedMessageConferencesLegacy(cb);
|
||||
}
|
||||
|
||||
exposedConfs = this._exposedMessageConferenceTags(exposedConfs);
|
||||
if (0 === exposedConfs.length) {
|
||||
return this._noExposedMessageConferences(cb);
|
||||
}
|
||||
|
||||
return this._makeAvailableMessageConferencesResponse(exposedConfs, cb);
|
||||
}
|
||||
|
||||
// older deprecated format
|
||||
_listExposedMessageConferencesLegacy(cb) {
|
||||
const exposedConfs = this._exposedMessageConferenceTags(
|
||||
_.get(Config(), 'contentServers.gopher.messageConferences')
|
||||
);
|
||||
|
||||
if (0 === exposedConfs.length) {
|
||||
return this._noExposedMessageConferences(cb);
|
||||
}
|
||||
|
||||
return this._makeAvailableMessageConferencesResponse(exposedConfs, cb);
|
||||
}
|
||||
|
||||
_makeAvailableMessageAreasResponse(exposedConf, exposedAreas, cb) {
|
||||
// ensure nothing private is present
|
||||
exposedAreas = exposedAreas.filter(
|
||||
area => area && !Message.isPrivateAreaTag(area.areaTag)
|
||||
);
|
||||
|
||||
if (0 === exposedAreas.length) {
|
||||
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available'));
|
||||
}
|
||||
|
||||
sortAreasOrConfs(exposedAreas);
|
||||
|
||||
const response = [
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, `Message areas in ${exposedConf.name}`),
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
...exposedAreas.map(area =>
|
||||
this.makeItem(
|
||||
ItemTypes.SubMenu,
|
||||
`${area.name} ${area.desc ? '- ' + area.desc : ''}`,
|
||||
`/msgarea/${exposedConf.confTag}/${area.areaTag}`
|
||||
)
|
||||
),
|
||||
].join('');
|
||||
|
||||
this.log.debug(
|
||||
{ confTag: exposedConf.confTag },
|
||||
'Gopher serving message area list'
|
||||
);
|
||||
return cb(response);
|
||||
}
|
||||
|
||||
_listExposedMessageConferenceAreas(selectorMatch, confTag, cb) {
|
||||
//
|
||||
// New system -- exposedConfAreas:
|
||||
// We have a required array |include| of area tags that may
|
||||
// contain wildcards and a _optional_ |exclude| array that
|
||||
// overrides any includes
|
||||
//
|
||||
// Deprecated -- messageConferences:
|
||||
// The key should point to an array of area tags
|
||||
//
|
||||
const [confConfig, isLegacy] = this._getConfigForConferenceTag(confTag);
|
||||
const messageConference = getMessageConferenceByTag(confTag); // we need the actual conf!
|
||||
|
||||
if (!messageConference) {
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
let areas;
|
||||
if (isLegacy) {
|
||||
areas = (confConfig || []).map(areaTag =>
|
||||
Object.assign({ areaTag }, getMessageAreaByTag(areaTag))
|
||||
);
|
||||
} else {
|
||||
// new system is more complex here, but nicer for the +op to manage
|
||||
areas = getAvailableMessageAreasByConfTag(confTag);
|
||||
if (!Array.isArray(confConfig.include)) {
|
||||
return cb(
|
||||
this.makeItem(ItemTypes.InfoMessage, 'No message areas available')
|
||||
);
|
||||
}
|
||||
|
||||
// filters |areas| down to what |includes| matches
|
||||
areas = _.filter(areas, (area, areaTag) => {
|
||||
for (let rule of confConfig.include) {
|
||||
if (wildcardMatch(areaTag, rule)) {
|
||||
area.areaTag = areaTag;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (
|
||||
message.areaTag !== areaTag ||
|
||||
!this.isAreaAndConfExposed(confTag, areaTag)
|
||||
) {
|
||||
this.log.warn(
|
||||
{ areaTag },
|
||||
'Attempted access to non-exposed conference and/or area!'
|
||||
// now filter out any excludes, if present
|
||||
if (Array.isArray(confConfig.exclude)) {
|
||||
areas = _.filter(areas, area => {
|
||||
for (let rule of confConfig.exclude) {
|
||||
if (wildcardMatch(area.areaTag, rule)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this._makeAvailableMessageAreasResponse(messageConference, areas, cb);
|
||||
}
|
||||
|
||||
_listMessagesInArea(selectorMatch, confTag, areaTag, area, cb) {
|
||||
if (Message.isPrivateAreaTag(areaTag)) {
|
||||
this.log.warn({ areaTag }, `Gopher attempted access to private "${areaTag}"`);
|
||||
return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private'));
|
||||
}
|
||||
|
||||
if (!area || !this.isAreaAndConfExposed(confTag, areaTag)) {
|
||||
this.log.warn(
|
||||
{ confTag, areaTag },
|
||||
`Gopher attempted access to non-exposed "${confTag}"/"${areaTag}"`
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
const filter = {
|
||||
resultType: 'messageList',
|
||||
sort: 'messageId',
|
||||
order: 'descending', // we want newest messages first for Gopher
|
||||
};
|
||||
|
||||
return getMessageListForArea(null, areaTag, filter, (err, msgList) => {
|
||||
const response = [
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`),
|
||||
this.makeItem(ItemTypes.InfoMessage, '(newest first)'),
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
...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,
|
||||
`${m.format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(
|
||||
msg.subject
|
||||
)} (${msg.fromUserName} to ${msg.toUserName})`,
|
||||
`/msgarea/${confTag}/${areaTag}/${msg.messageUuid}`
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
}),
|
||||
].join('');
|
||||
|
||||
if (Message.isPrivateAreaTag(areaTag)) {
|
||||
this.log.warn(
|
||||
{ areaTag },
|
||||
'Attempted access to message in private area!'
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
this.log.debug({ confTag, areaTag }, 'Gopher serving message list');
|
||||
return cb(response);
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareMessageBody(message.message, msgBody => {
|
||||
const response = `${'-'.repeat(70)}
|
||||
_displayMessage(selectorMatch, msgUuid, confTag, areaTag, cb) {
|
||||
const message = new Message();
|
||||
|
||||
return message.load({ uuid: msgUuid }, err => {
|
||||
if (err) {
|
||||
this.log.debug(
|
||||
{ uuid: msgUuid },
|
||||
'Attempted access to non-existent message UUID!'
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
if (
|
||||
message.areaTag !== areaTag ||
|
||||
!this.isAreaAndConfExposed(confTag, areaTag)
|
||||
) {
|
||||
this.log.warn(
|
||||
{ areaTag },
|
||||
`Gopher attempted access to non-exposed "${confTag}"/"${areaTag}"`
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
if (Message.isPrivateAreaTag(areaTag)) {
|
||||
this.log.warn(
|
||||
{ areaTag },
|
||||
`Gopher attempted access to message in private "${areaTag}"`
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
this.prepareMessageBody(message.message, msgBody => {
|
||||
const response = `${'-'.repeat(70)}
|
||||
To : ${message.toUserName}
|
||||
From : ${message.fromUserName}
|
||||
When : ${moment(message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')}
|
||||
|
@ -359,137 +622,17 @@ Subject: ${message.subject}
|
|||
ID : ${message.messageUuid} (${message.messageId})
|
||||
${'-'.repeat(70)}
|
||||
${msgBody}
|
||||
`;
|
||||
return cb(response);
|
||||
});
|
||||
});
|
||||
} else if (selectorMatch[2]) {
|
||||
// list messages in area
|
||||
const confTag = selectorMatch[1].substr(1).split('/')[0];
|
||||
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
|
||||
const area = getMessageAreaByTag(areaTag);
|
||||
|
||||
if (Message.isPrivateAreaTag(areaTag)) {
|
||||
this.log.warn({ areaTag }, 'Attempted access to private area!');
|
||||
return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private'));
|
||||
}
|
||||
|
||||
if (!area || !this.isAreaAndConfExposed(confTag, areaTag)) {
|
||||
this.log.warn(
|
||||
{ confTag, areaTag },
|
||||
'Attempted access to non-exposed conference and/or area!'
|
||||
`;
|
||||
this.log.debug(
|
||||
{
|
||||
confTag,
|
||||
areaTag,
|
||||
uuid: message.messageUuid,
|
||||
},
|
||||
`Gopher serving message "${message.subject}"`
|
||||
);
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
const filter = {
|
||||
resultType: 'messageList',
|
||||
sort: 'messageId',
|
||||
order: 'descending', // we want newest messages first for Gopher
|
||||
};
|
||||
|
||||
return getMessageListForArea(null, areaTag, filter, (err, msgList) => {
|
||||
const response = [
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
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(
|
||||
ItemTypes.TextFile,
|
||||
`${moment(msg.modTimestamp).format(
|
||||
'YYYY-MM-DD hh:mma'
|
||||
)}: ${this.shortenSubject(msg.subject)} (${
|
||||
msg.fromUserName
|
||||
} to ${msg.toUserName})`,
|
||||
`/msgarea/${confTag}/${areaTag}/${msg.messageUuid}`
|
||||
)
|
||||
),
|
||||
].join('');
|
||||
|
||||
return cb(response);
|
||||
});
|
||||
} else if (selectorMatch[1]) {
|
||||
// list areas in conf
|
||||
const sysConfig = Config();
|
||||
const confTag = selectorMatch[1].replace(/\r\n|\//g, '');
|
||||
const conf =
|
||||
_.get(sysConfig, [
|
||||
'contentServers',
|
||||
'gopher',
|
||||
'messageConferences',
|
||||
confTag,
|
||||
]) && getMessageConferenceByTag(confTag);
|
||||
if (!conf) {
|
||||
return this.notFoundGenerator(selectorMatch, cb);
|
||||
}
|
||||
|
||||
const areas = _.get(
|
||||
sysConfig,
|
||||
['contentServers', 'gopher', 'messageConferences', confTag],
|
||||
{}
|
||||
)
|
||||
.map(areaTag => Object.assign({ areaTag }, getMessageAreaByTag(areaTag)))
|
||||
.filter(area => area && !Message.isPrivateAreaTag(area.areaTag));
|
||||
|
||||
if (0 === areas.length) {
|
||||
return cb(
|
||||
this.makeItem(ItemTypes.InfoMessage, 'No message areas available')
|
||||
);
|
||||
}
|
||||
|
||||
sortAreasOrConfs(areas);
|
||||
|
||||
const response = [
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`),
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
...areas.map(area =>
|
||||
this.makeItem(
|
||||
ItemTypes.SubMenu,
|
||||
`${area.name} ${area.desc ? '- ' + area.desc : ''}`,
|
||||
`/msgarea/${confTag}/${area.areaTag}`
|
||||
)
|
||||
),
|
||||
].join('');
|
||||
|
||||
return cb(response);
|
||||
} else {
|
||||
// message area base (list confs)
|
||||
const confs = Object.keys(
|
||||
_.get(Config(), 'contentServers.gopher.messageConferences', {})
|
||||
)
|
||||
.map(confTag =>
|
||||
Object.assign({ confTag }, getMessageConferenceByTag(confTag))
|
||||
)
|
||||
.filter(conf => conf); // remove any baddies
|
||||
|
||||
if (0 === confs.length) {
|
||||
return cb(
|
||||
this.makeItem(
|
||||
ItemTypes.InfoMessage,
|
||||
'No message conferences available'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sortAreasOrConfs(confs);
|
||||
|
||||
const response = [
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'),
|
||||
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
|
||||
this.makeItem(ItemTypes.InfoMessage, ''),
|
||||
...confs.map(conf =>
|
||||
this.makeItem(
|
||||
ItemTypes.SubMenu,
|
||||
`${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`,
|
||||
`/msgarea/${conf.confTag}`
|
||||
)
|
||||
),
|
||||
].join('');
|
||||
|
||||
return cb(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ const { getTransactionDatabase, getModDatabasePath } = require('../../database.j
|
|||
const {
|
||||
getMessageAreaByTag,
|
||||
getMessageConferenceByTag,
|
||||
persistMessage,
|
||||
} = require('../../message_area.js');
|
||||
const User = require('../../user.js');
|
||||
const Errors = require('../../enig_error.js').Errors;
|
||||
|
@ -21,6 +22,7 @@ const {
|
|||
} = require('../../string_util.js');
|
||||
const AnsiPrep = require('../../ansi_prep.js');
|
||||
const { stripMciColorCodes } = require('../../color_codes.js');
|
||||
const ACS = require('../../acs');
|
||||
|
||||
// deps
|
||||
const NNTPServerBase = require('nntp-server');
|
||||
|
@ -111,6 +113,38 @@ class NNTPDatabase {
|
|||
|
||||
let nntpDatabase;
|
||||
|
||||
const AuthCommands = 'POST';
|
||||
|
||||
// these aren't exported by the NNTP module, unfortunantely
|
||||
const Responses = {
|
||||
ArticlePostedOk: '240 article posted ok',
|
||||
|
||||
SendArticle: '340 send article to be posted',
|
||||
|
||||
PostingNotAllowed: '440 posting not allowed',
|
||||
ArticlePostFailed: '441 posting failed',
|
||||
AuthRequired: '480 authentication required',
|
||||
};
|
||||
|
||||
const PostCommand = {
|
||||
head: 'POST',
|
||||
validate: /^POST$/i,
|
||||
|
||||
run(session, cmd) {
|
||||
if (!session.authenticated) {
|
||||
session.receivingPostArticle = false; // ensure reset
|
||||
return Responses.AuthRequired;
|
||||
}
|
||||
|
||||
session.receivingPostArticle = true;
|
||||
return Responses.SendArticle;
|
||||
},
|
||||
|
||||
capability(session, report) {
|
||||
report.push('POST');
|
||||
},
|
||||
};
|
||||
|
||||
class NNTPServer extends NNTPServerBase {
|
||||
constructor(options, serverName) {
|
||||
super(options);
|
||||
|
@ -125,14 +159,26 @@ class NNTPServer extends NNTPServerBase {
|
|||
}
|
||||
|
||||
_needAuth(session, command) {
|
||||
if (AuthCommands.includes(command)) {
|
||||
return !session.authenticated && !session.authUser;
|
||||
}
|
||||
|
||||
return super._needAuth(session, command);
|
||||
}
|
||||
|
||||
_address(session) {
|
||||
const addr = session.in_stream.remoteAddress;
|
||||
return addr ? addr.replace(/^::ffff:/, '').replace(/^::1$/, 'localhost') : 'N/A';
|
||||
}
|
||||
|
||||
_authenticate(session) {
|
||||
const username = session.authinfo_user;
|
||||
const password = session.authinfo_pass;
|
||||
|
||||
this.log.trace({ username }, 'Authentication request');
|
||||
this.log.debug(
|
||||
{ username, ip: this._address(session) },
|
||||
`NNTP authentication request for "${username}"`
|
||||
);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const user = new User();
|
||||
|
@ -140,17 +186,19 @@ class NNTPServer extends NNTPServerBase {
|
|||
{ type: User.AuthFactor1Types.Password, username, password },
|
||||
err => {
|
||||
if (err) {
|
||||
// :TODO: Log IP address
|
||||
this.log.debug(
|
||||
{ username, reason: err.message },
|
||||
'Authentication failure'
|
||||
this.log.warn(
|
||||
{ username, reason: err.message, ip: this._address(session) },
|
||||
`NNTP authentication failure for "${username}"`
|
||||
);
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
session.authUser = user;
|
||||
|
||||
this.log.debug({ username }, 'User authenticated successfully');
|
||||
this.log.info(
|
||||
{ username, ip: this._address(session) },
|
||||
`NTTP authentication success for "${username}"`
|
||||
);
|
||||
return resolve(true);
|
||||
}
|
||||
);
|
||||
|
@ -232,6 +280,7 @@ class NNTPServer extends NNTPServerBase {
|
|||
message.nntpHeaders = {
|
||||
From: this.getJAMStyleFrom(message, fromName),
|
||||
'X-Comment-To': toName,
|
||||
To: toName, // JAM-ish
|
||||
Newsgroups: session.group.name,
|
||||
Subject: message.subject,
|
||||
Date: this.getMessageDate(message),
|
||||
|
@ -343,7 +392,7 @@ class NNTPServer extends NNTPServerBase {
|
|||
messageUuid = msg && msg.messageUuid;
|
||||
} else {
|
||||
// <Message-ID> request
|
||||
[, messageUuid] = this.getMessageIdentifierParts(messageId);
|
||||
[, messageUuid] = NNTPServer.getMessageIdentifierParts(messageId);
|
||||
}
|
||||
|
||||
if (!_.isString(messageUuid)) {
|
||||
|
@ -394,7 +443,7 @@ class NNTPServer extends NNTPServerBase {
|
|||
)
|
||||
) {
|
||||
this.log.info(
|
||||
{ messageUuid, messageId },
|
||||
{ messageUuid, messageId, ip: this._address(session) },
|
||||
'Access denied for message'
|
||||
);
|
||||
return resolve(null);
|
||||
|
@ -592,15 +641,33 @@ class NNTPServer extends NNTPServerBase {
|
|||
if (!conf) {
|
||||
return false;
|
||||
}
|
||||
// :TODO: validate ACS
|
||||
|
||||
const area = getMessageAreaByTag(areaTag, confTag);
|
||||
if (!area) {
|
||||
return false;
|
||||
}
|
||||
// :TODO: validate ACS
|
||||
|
||||
return false;
|
||||
const acs = new ACS({ client: null, user: session.authUser });
|
||||
return acs.hasMessageConfRead(conf) && acs.hasMessageAreaRead(area);
|
||||
}
|
||||
|
||||
static hasConfAndAreaWriteAccess(session, confTag, areaTag) {
|
||||
if (Message.isPrivateAreaTag(areaTag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const conf = getMessageConferenceByTag(confTag);
|
||||
if (!conf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const area = getMessageAreaByTag(areaTag, confTag);
|
||||
if (!area) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const acs = new ACS({ client: null, user: session.authUser });
|
||||
return acs.hasMessageConfWrite(conf) && acs.hasMessageAreaWrite(area);
|
||||
}
|
||||
|
||||
getGroup(session, groupName, cb) {
|
||||
|
@ -861,7 +928,7 @@ class NNTPServer extends NNTPServerBase {
|
|||
return this.makeMessageIdentifier(message.messageId, message.messageUuid);
|
||||
}
|
||||
|
||||
getMessageIdentifierParts(messageId) {
|
||||
static getMessageIdentifierParts(messageId) {
|
||||
const m = messageId.match(
|
||||
/<([0-9]+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@enigma-bbs>/
|
||||
);
|
||||
|
@ -919,6 +986,240 @@ class NNTPServer extends NNTPServerBase {
|
|||
areaTag
|
||||
).replace(/\./g, '_')}`;
|
||||
}
|
||||
|
||||
static _importMessage(session, articleLines, cb) {
|
||||
const tidyFrom = f => {
|
||||
if (f) {
|
||||
// remove quotes around name, if present
|
||||
let m = /^"([^"]+)" <([^>]+)>$/.exec(f);
|
||||
if (m && m[1] && m[2]) {
|
||||
f = `${m[1]} <${m[2]}>`;
|
||||
}
|
||||
}
|
||||
return f;
|
||||
};
|
||||
|
||||
asyncWaterfall(
|
||||
[
|
||||
callback => {
|
||||
return NNTPServer._parseArticleLines(articleLines, callback);
|
||||
},
|
||||
(parsed, callback) => {
|
||||
// gather some initially important bits
|
||||
const subject = parsed.header.get('subject');
|
||||
const to = parsed.header.get('to') || parsed.header.get('x-jam-to'); // non-standard, may be missing
|
||||
const from = tidyFrom(
|
||||
parsed.header.get('from') ||
|
||||
parsed.header.get('sender') ||
|
||||
parsed.header.get('x-jam-from')
|
||||
);
|
||||
const date = parsed.header.get('date'); // if not present we'll use 'now'
|
||||
const newsgroups = parsed.header
|
||||
.get('newsgroups')
|
||||
.split(',')
|
||||
.map(ng => {
|
||||
const [confTag, areaTag] = ng.split('.');
|
||||
return { confTag, areaTag };
|
||||
});
|
||||
|
||||
// validate areaTag exists -- currently only a single area/post; no x-posts
|
||||
// :TODO: look into x-posting
|
||||
const area = getMessageAreaByTag(newsgroups[0].areaTag);
|
||||
if (!area) {
|
||||
return callback(
|
||||
Errors.DoesNotExist(
|
||||
`No area by tag "${newsgroups[0].areaTag}" exists!`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Not all ACS checks work with NNTP since we don't have a standard client;
|
||||
// If a particular ACS requires a |client|, it will return false!
|
||||
if (
|
||||
!NNTPServer.hasConfAndAreaWriteAccess(
|
||||
session,
|
||||
area.confTag,
|
||||
area.areaTag
|
||||
)
|
||||
) {
|
||||
return callback(
|
||||
Errors.AccessDenied(
|
||||
`No ACS to ${area.confTag}/${area.areaTag}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!_.isString(subject) ||
|
||||
!_.isString(from) ||
|
||||
!Array.isArray(newsgroups)
|
||||
) {
|
||||
return callback(
|
||||
Errors.Invalid('Missing one or more required article fields')
|
||||
);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
subject,
|
||||
from,
|
||||
date,
|
||||
newsgroups,
|
||||
to,
|
||||
parsed,
|
||||
});
|
||||
},
|
||||
(msgData, callback) => {
|
||||
if (msgData.to) {
|
||||
return callback(null, msgData);
|
||||
}
|
||||
|
||||
//
|
||||
// We don't have a 'to' field, try to derive if this is a
|
||||
// response to a message. If not, just fall back 'All'
|
||||
//
|
||||
// 'References'
|
||||
// - https://www.rfc-editor.org/rfc/rfc5536#section-3.2.10
|
||||
// - https://www.rfc-editor.org/rfc/rfc5322.html
|
||||
//
|
||||
// 'In-Reply-To'
|
||||
// - https://www.rfc-editor.org/rfc/rfc5322.html
|
||||
//
|
||||
// Both may contain 1:N, "optionally" separated by CFWS; by this
|
||||
// point in the code, they should be space separated at most.
|
||||
//
|
||||
// Each entry is in msg-id format. That is:
|
||||
// "<" id-left "@" id-right ">"
|
||||
//
|
||||
msgData.to = 'All'; // fallback
|
||||
let parentMessageId = (
|
||||
msgData.parsed.header.get('in-reply-to') ||
|
||||
msgData.parsed.header.get('references') ||
|
||||
''
|
||||
).split(' ')[0];
|
||||
|
||||
if (parentMessageId) {
|
||||
let [_, messageUuid] =
|
||||
NNTPServer.getMessageIdentifierParts(parentMessageId);
|
||||
if (messageUuid) {
|
||||
const filter = {
|
||||
resultType: 'messageList',
|
||||
uuids: messageUuid,
|
||||
limit: 1,
|
||||
};
|
||||
|
||||
return Message.findMessages(filter, (err, messageList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// current message/article is a reply to this message:
|
||||
msgData.to = messageList[0].fromUserName;
|
||||
msgData.replyToMsgId = messageList[0].replyToMsgId; // may not be present
|
||||
return callback(null, msgData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, msgData);
|
||||
},
|
||||
(msgData, callback) => {
|
||||
const message = new Message({
|
||||
toUserName: msgData.to,
|
||||
fromUserName: msgData.from,
|
||||
subject: msgData.subject,
|
||||
replyToMsgId: msgData.replyToMsgId || 0,
|
||||
modTimestamp: msgData.date, // moment can generally parse these
|
||||
// :TODO: inspect Content-Type 'charset' if present & attempt to properly decode if not UTF-8
|
||||
message: msgData.parsed.body.join('\n'),
|
||||
areaTag: msgData.newsgroups[0].areaTag,
|
||||
});
|
||||
|
||||
message.meta.System[Message.SystemMetaNames.ExternalFlavor] =
|
||||
Message.AddressFlavor.NNTP;
|
||||
|
||||
// :TODO: investigate JAMNTTP clients/etc.
|
||||
// :TODO: slurp in various X-XXXX kludges/etc. and bring them in
|
||||
|
||||
persistMessage(message, err => {
|
||||
if (!err) {
|
||||
Log.info(
|
||||
`NNTP post to "${message.areaTag}" by "${session.authUser.username}": "${message.subject}"`
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static _parseArticleLines(articleLines, cb) {
|
||||
//
|
||||
// Split articleLines into:
|
||||
// - Header split into N:V pairs
|
||||
// - Message Body lines
|
||||
// -
|
||||
const header = new Map();
|
||||
const body = [];
|
||||
let inHeader = true;
|
||||
let currentHeaderName;
|
||||
forEachSeries(
|
||||
articleLines,
|
||||
(line, nextLine) => {
|
||||
if (inHeader) {
|
||||
if (line === '.' || line === '') {
|
||||
inHeader = false;
|
||||
return nextLine(null);
|
||||
}
|
||||
|
||||
const sep = line.indexOf(':');
|
||||
if (sep < 1) {
|
||||
// at least a single char name
|
||||
// entries can split across lines -- they will be prefixed with a single space.
|
||||
if (
|
||||
currentHeaderName &&
|
||||
(line.startsWith(' ') || line.startsWith('\t'))
|
||||
) {
|
||||
let v = header.get(currentHeaderName);
|
||||
v += line
|
||||
.replace(/^\t/, ' ') // if we're dealign with a legacy tab
|
||||
.trimRight();
|
||||
header.set(currentHeaderName, v);
|
||||
return nextLine(null);
|
||||
}
|
||||
|
||||
return nextLine(
|
||||
Errors.Invalid(
|
||||
`"${line}" is not a valid NNTP message header!`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
currentHeaderName = line.slice(0, sep).trim().toLowerCase();
|
||||
const value = line.slice(sep + 1).trim();
|
||||
header.set(currentHeaderName, value);
|
||||
return nextLine(null);
|
||||
}
|
||||
|
||||
// body
|
||||
if (line !== '.') {
|
||||
// lines consisting of a single '.' are escaped to '..'
|
||||
if (line.startsWith('..')) {
|
||||
body.push(line.slice(1));
|
||||
} else {
|
||||
body.push(line);
|
||||
}
|
||||
}
|
||||
return nextLine(null);
|
||||
},
|
||||
err => {
|
||||
return cb(err, { header, body });
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exports.getModule = class NNTPServerModule extends ServerModule {
|
||||
|
@ -985,11 +1286,92 @@ exports.getModule = class NNTPServerModule extends ServerModule {
|
|||
|
||||
const config = Config();
|
||||
|
||||
// :TODO: nntp-server doesn't currently allow posting in a nice way, so this is kludged in. Fork+MR something cleaner at some point
|
||||
class ProxySession extends NNTPServerBase.Session {
|
||||
constructor(server, stream) {
|
||||
super(server, stream);
|
||||
this.articleLinesBuffer = [];
|
||||
}
|
||||
|
||||
parse(data) {
|
||||
if (this.receivingPostArticle) {
|
||||
return this.receivePostArticleData(data);
|
||||
}
|
||||
|
||||
super.parse(data);
|
||||
}
|
||||
|
||||
receivePostArticleData(data) {
|
||||
this.articleLinesBuffer.push(...data.split(/r?\n/));
|
||||
|
||||
const endOfPost = data.length === 1 && data[0] === '.';
|
||||
if (endOfPost) {
|
||||
this.receivingPostArticle = false;
|
||||
|
||||
// Command is not exported currently; maybe submit a MR to allow posting in a nicer way...
|
||||
function Command(runner, articleLines, session) {
|
||||
this.state = 0; // CMD_WAIT
|
||||
this.cmd_line = 'POST';
|
||||
this.resolved_value = null;
|
||||
this.rejected_value = null;
|
||||
this.run = runner;
|
||||
this.articleLines = articleLines;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
this.pipeline.push(
|
||||
new Command(
|
||||
this._processarticleLinesBuffer,
|
||||
this.articleLinesBuffer,
|
||||
this
|
||||
)
|
||||
);
|
||||
this.articleLinesBuffer = [];
|
||||
this.tick();
|
||||
}
|
||||
}
|
||||
|
||||
_processarticleLinesBuffer() {
|
||||
return new Promise(resolve => {
|
||||
NNTPServer._importMessage(this.session, this.articleLines, err => {
|
||||
if (err) {
|
||||
this.rejected_value = err; // will be serialized and 403 sent back currently; not really ideal as we want ArticlePostFailed
|
||||
// :TODO: tick() needs updated in session.js such that we can write back a proper code
|
||||
this.state = 3; // CMD_REJECTED
|
||||
|
||||
Log.error(
|
||||
{ error: err.message },
|
||||
`NNTP post failed: ${err.message}`
|
||||
);
|
||||
} else {
|
||||
this.resolved_value = Responses.ArticlePostedOk;
|
||||
this.state = 2; // CMD_RESOLVED
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static create(server, stream) {
|
||||
return new ProxySession(server, stream);
|
||||
}
|
||||
}
|
||||
|
||||
const commonOptions = {
|
||||
//requireAuth : true, // :TODO: re-enable!
|
||||
// :TODO: override |session| - use our own debug to Bunyan, etc.
|
||||
// :TODO: How to hook into debugging?!
|
||||
};
|
||||
|
||||
if (true === _.get(config, 'contentServers.nntp.allowPosts')) {
|
||||
// add in some additional supported commands
|
||||
const commands = Object.assign({}, NNTPServerBase.commands, {
|
||||
POST: PostCommand,
|
||||
});
|
||||
|
||||
commonOptions.commands = commands;
|
||||
commonOptions.session = ProxySession;
|
||||
}
|
||||
|
||||
if (this.enableNntp) {
|
||||
this.nntpServer = new NNTPServer(
|
||||
// :TODO: according to docs: if connection is non-tls, but behind proxy (assuming TLS termination?!!) then set this to true
|
||||
|
|
|
@ -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')) {
|
||||
|
@ -133,7 +135,7 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
|
||||
getSaveState() {
|
||||
// if no areas, we're falling back due to lack of access/areas avail to upload to
|
||||
if (this.availAreas.length > 0) {
|
||||
if (this.availAreas.length > 0 && this.viewControllers.options) {
|
||||
return {
|
||||
uploadType: this.uploadType,
|
||||
tempRecvDirectory: this.tempRecvDirectory,
|
||||
|
|
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,
|
||||
|
|
|
@ -50,7 +50,7 @@ function VerticalMenuView(options) {
|
|||
this.updateViewVisibleItems = function () {
|
||||
self.maxVisibleItems = Math.ceil(self.dimens.height / (self.itemSpacing + 1));
|
||||
|
||||
const topIndex = (this.focusItemAtTop ? throws.focusedItemIndex : 0) || 0;
|
||||
const topIndex = (this.focusItemAtTop ? this.focusedItemIndex : 0) || 0;
|
||||
|
||||
self.viewWindow = {
|
||||
top: topIndex,
|
||||
|
@ -195,7 +195,7 @@ VerticalMenuView.prototype.setFocusItemIndex = function (index) {
|
|||
? this.items.length - index
|
||||
: this.items.length;
|
||||
if (remainAfterFocus >= this.maxVisibleItems) {
|
||||
const topIndex = (this.focusItemAtTop ? throws.focusedItemIndex : 0) || 0;
|
||||
const topIndex = (this.focusItemAtTop ? this.focusedItemIndex : 0) || 0;
|
||||
|
||||
this.viewWindow = {
|
||||
top: topIndex,
|
||||
|
|
|
@ -49,6 +49,7 @@ function View(options) {
|
|||
this.position = { x: 0, y: 0 };
|
||||
this.textStyle = options.textStyle || 'normal';
|
||||
this.focusTextStyle = options.focusTextStyle || this.textStyle;
|
||||
this.offsetsApplied = false;
|
||||
|
||||
if (options.id) {
|
||||
this.setId(options.id);
|
||||
|
|
|
@ -150,6 +150,8 @@ function ViewController(options) {
|
|||
};
|
||||
|
||||
this.createViewsFromMCI = function (mciMap, cb) {
|
||||
const views = [];
|
||||
|
||||
async.each(
|
||||
Object.keys(mciMap),
|
||||
(name, nextItem) => {
|
||||
|
@ -161,6 +163,7 @@ function ViewController(options) {
|
|||
view.on('action', self.viewActionListener);
|
||||
}
|
||||
|
||||
views.push(view);
|
||||
self.addView(view);
|
||||
}
|
||||
|
||||
|
@ -168,7 +171,7 @@ function ViewController(options) {
|
|||
},
|
||||
err => {
|
||||
self.setViewOrder();
|
||||
return cb(err);
|
||||
return cb(err, views);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -491,6 +494,27 @@ ViewController.prototype.resetInitialFocus = function () {
|
|||
}
|
||||
};
|
||||
|
||||
ViewController.prototype.applyViewOffsets = function (
|
||||
views,
|
||||
offsetCol,
|
||||
offsetRow,
|
||||
force = false
|
||||
) {
|
||||
if (!Array.isArray(views)) {
|
||||
views = [views];
|
||||
}
|
||||
|
||||
views.forEach(view => {
|
||||
if (force || !view.offsetsApplied) {
|
||||
view.offsetsApplied = true;
|
||||
view.setPosition({
|
||||
col: view.position.col + offsetCol,
|
||||
row: view.position.row + offsetRow,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ViewController.prototype.switchFocus = function (id) {
|
||||
//
|
||||
// Perform focus switching validation now
|
||||
|
@ -589,7 +613,7 @@ ViewController.prototype.loadFromPromptConfig = function (options, cb) {
|
|||
});
|
||||
},
|
||||
function applyViewConfiguration(callback) {
|
||||
if (_.isObject(promptConfig.mci)) {
|
||||
if (promptConfig && _.isObject(promptConfig.mci)) {
|
||||
self.applyViewConfig(promptConfig, function configApplied(err, info) {
|
||||
initialFocusId = info.initialFocusId;
|
||||
callback(err);
|
||||
|
@ -759,7 +783,14 @@ ViewController.prototype.loadFromMenuConfig = function (options, cb) {
|
|||
);
|
||||
},
|
||||
function createViews(callback) {
|
||||
self.createViewsFromMCI(options.mciMap, function viewsCreated(err) {
|
||||
self.createViewsFromMCI(options.mciMap, (err, views) => {
|
||||
if (!err && _.isObject(options.viewOffsets)) {
|
||||
self.applyViewOffsets(
|
||||
views,
|
||||
options.viewOffsets.col,
|
||||
options.viewOffsets.row
|
||||
);
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
54
core/wfc.js
54
core/wfc.js
|
@ -1,6 +1,7 @@
|
|||
// ENiGMA½
|
||||
const { MenuModule } = require('./menu_module');
|
||||
const stringFormat = require('./string_format');
|
||||
const Events = require('./events');
|
||||
|
||||
const {
|
||||
getActiveConnectionList,
|
||||
|
@ -77,6 +78,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
|||
}
|
||||
|
||||
this.selectedNodeStatusIndex = -1; // no selection
|
||||
this.refreshing = false;
|
||||
|
||||
this.menuMethods = {
|
||||
toggleAvailable: (formData, extraArgs, cb) => {
|
||||
|
@ -87,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) => {
|
||||
|
@ -224,6 +227,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
|||
enter() {
|
||||
this.client.stopIdleMonitor();
|
||||
this._applyOpVisibility();
|
||||
Events.on(
|
||||
Events.getSystemEvents().ClientDisconnected,
|
||||
this._clientDisconnected.bind(this)
|
||||
);
|
||||
super.enter();
|
||||
}
|
||||
|
||||
|
@ -232,6 +239,11 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
|||
return stream.name === 'wfc-ringbuffer';
|
||||
});
|
||||
|
||||
Events.removeListener(
|
||||
Events.getSystemEvents().ClientDisconnected,
|
||||
this._clientDisconnected
|
||||
);
|
||||
|
||||
this._restoreOpVisibility();
|
||||
|
||||
this._stopRefreshing();
|
||||
|
@ -355,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() {
|
||||
|
@ -375,7 +389,38 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
|||
}
|
||||
}
|
||||
|
||||
_clientDisconnected() {
|
||||
const nodeStatusSelectionView = this.getView(
|
||||
'main',
|
||||
MciViewIds.main.selectedNodeStatusInfo
|
||||
);
|
||||
if (nodeStatusSelectionView) {
|
||||
nodeStatusSelectionView.setText('');
|
||||
}
|
||||
|
||||
this.selectedNodeStatusIndex = 0; // will select during refresh
|
||||
this._refreshAll();
|
||||
|
||||
// have to update the selection view here
|
||||
if (nodeStatusSelectionView) {
|
||||
const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus);
|
||||
if (nodeStatusView) {
|
||||
const item = nodeStatusView.getItems()[this.selectedNodeStatusIndex];
|
||||
this._updateNodeStatusSelection(nodeStatusSelectionView, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_refreshAll(cb) {
|
||||
if (this.refreshing) {
|
||||
if (cb) {
|
||||
return cb(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshing = true;
|
||||
|
||||
async.series(
|
||||
[
|
||||
callback => {
|
||||
|
@ -397,6 +442,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
|||
},
|
||||
],
|
||||
err => {
|
||||
this.refreshing = false;
|
||||
if (cb) {
|
||||
return cb(err);
|
||||
}
|
||||
|
@ -447,7 +493,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
|||
totalPosts: StatLog.getSystemStatNum(SysProps.MessageTotalCount),
|
||||
totalUsers: StatLog.getSystemStatNum(SysProps.TotalUserCount),
|
||||
totalFiles: fileAreaStats.totalFiles || 0,
|
||||
totalFileBytes: fileAreaStats.totalFileBytes || 0,
|
||||
totalFileBytes: fileAreaStats.totalBytes || 0,
|
||||
|
||||
// Today's Stats
|
||||
callsToday: StatLog.getSystemStatNum(SysProps.LoginsToday),
|
||||
|
@ -460,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
|
||||
|
@ -127,7 +127,9 @@ collections:
|
|||
- modding/autosig-edit.md
|
||||
- modding/menu-modules.md
|
||||
- admin/administration.md
|
||||
- modding/wfc.md
|
||||
- admin/oputil.md
|
||||
- admin/updating.md
|
||||
- troubleshooting/monitoring-logs.md
|
||||
- troubleshooting/ssh-troubleshooting.md
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue