Compare commits
No commits in common. "master" and "cp437-unless-nix-explicit" have entirely different histories.
master
...
cp437-unle
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
node_modules
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
filebase
|
|
||||||
db
|
|
||||||
drop
|
|
||||||
file_base
|
|
||||||
logs
|
|
||||||
mail
|
|
||||||
docs/_site
|
|
||||||
docs/.sass-cache
|
|
|
@ -6,13 +6,3 @@
|
||||||
*.TXT eol=crlf
|
*.TXT eol=crlf
|
||||||
*.diz eol=crlf
|
*.diz 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
|
|
|
@ -1,74 +0,0 @@
|
||||||
# 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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
tags: enigmabbs/enigma-bbs:latest
|
tags: enigmabbs/enigma-bbs:latest
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
|
@ -11,5 +11,4 @@ mail/
|
||||||
node_modules/
|
node_modules/
|
||||||
docs/_site/
|
docs/_site/
|
||||||
docs/.sass-cache/
|
docs/.sass-cache/
|
||||||
|
.vscode/
|
||||||
docs/.jekyll-cache/
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"ms-vscode-remote.remote-containers",
|
|
||||||
"laktak.hjson"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"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,37 +4,35 @@
|
||||||
|
|
||||||
ENiGMA½ is a modern BBS software with a nostalgic flair!
|
ENiGMA½ is a modern BBS software with a nostalgic flair!
|
||||||
|
|
||||||
## Features
|
## Features Available Now
|
||||||
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)
|
||||||
* **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"!)
|
* 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/_docs/modding/existing-mods.md)
|
* **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/_docs/art/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles
|
* [MCI support](docs/art/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles
|
||||||
* Telnet, **SSH**, and both secure and non-secure [WebSocket](https://en.wikipedia.org/wiki/WebSocket) access built in! Additional servers are easy to implement
|
* 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
|
* [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.
|
* [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.
|
* Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support.
|
||||||
* Renegade style [pipe color codes](./docs/_docs/configuration/colour-codes.md).
|
* Renegade style [pipe color codes](./docs/configuration/colour-codes.md).
|
||||||
* [SQLite](http://sqlite.org/) storage of users, message areas, etc.
|
* [SQLite](http://sqlite.org/) storage of users, message areas, etc.
|
||||||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption.
|
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption.
|
||||||
* Support for **2-Factor Authentication** with One-Time-Passwords
|
* 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/)!
|
* [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!
|
||||||
* Structured [Bunyan](https://github.com/trentm/node-bunyan) logging!
|
* [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)!
|
* [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/_docs/servers/contentservers/web-server.md). Legacy X/Y/Z modem also supported!
|
* [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/web-server.md). Legacy X/Y/Z modem also supported!
|
||||||
* Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more!
|
* 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.
|
* ANSI support in the Full Screen Editor (FSE), file descriptions, etc.
|
||||||
* Expandable **achievement system** — BBSing gamified!
|
* A built in 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!
|
...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
|
## 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.
|
[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.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
On most *nix systems simply run the following from your terminal:
|
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
|
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -49,7 +47,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)
|
* 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)
|
* **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))
|
* IRC: **#enigma-bbs** on **irc.libera.chat:6697(TLS)** ([webchat](https://web.libera.chat/gamja/?channels=#enigma-bbs))
|
||||||
* `FSX_ENG` on [fsxNet](https://fsxnet.nz) or `ARK_ENIG` on [ArakNet](https://www.araknet.xyz/) available on many fine boards
|
* FSX_ENG on [fsxNet](http://bbs.geek.nz/#fsxNet) or ARK_ENIG on [ArakNet](https://www.araknet.xyz/) available on many fine boards
|
||||||
* Email: bryan -at- l33t.codes
|
* Email: bryan -at- l33t.codes
|
||||||
* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/)
|
* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/)
|
||||||
|
|
||||||
|
@ -77,7 +75,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!
|
* [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!
|
* Sudndeath for Xibalba ANSI work!
|
||||||
* Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!)
|
* Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!)
|
||||||
* 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!
|
* 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!
|
||||||
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
|
* Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)!
|
||||||
* [Apam](https://github.com/apamment) of [Magicka](https://magickabbs.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)!
|
* [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)!
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# 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)
|
|
62
UPGRADE.md
62
UPGRADE.md
|
@ -1,8 +1,6 @@
|
||||||
# Introduction
|
# Introduction
|
||||||
This document covers basic upgrade notes for major ENiGMA½ version updates.
|
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!
|
|
||||||
|
|
||||||
# Before Upgrading
|
# Before Upgrading
|
||||||
* Always back up your system! (See [Administration](./docs/admin/administration.md))
|
* Always back up your system! (See [Administration](./docs/admin/administration.md))
|
||||||
* Seriously, always back up your system!
|
* Seriously, always back up your system!
|
||||||
|
@ -27,49 +25,37 @@ npm install # or simply 'yarn'
|
||||||
```
|
```
|
||||||
|
|
||||||
# Problems
|
# Problems
|
||||||
1. Check [TROUBLESHOOTING](TROUBLESHOOTING.md) first.
|
Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or
|
||||||
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.
|
[file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues).
|
||||||
|
|
||||||
# Version to Version Notes
|
|
||||||
> :warning: Be sure to inspect these notes during any upgrades!
|
|
||||||
|
|
||||||
## 0.0.13-beta to 0.0.14-beta
|
# 0.0.12-beta to 0.0.13-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`
|
* :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:
|
* 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
|
```hjson
|
||||||
{
|
{
|
||||||
|
|
||||||
term: {
|
term: {
|
||||||
// checkUtf8Encoding requires the use of cursor
|
// checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals.
|
||||||
// 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
|
||||||
// Using this with a terminal that does not support cursor
|
// during the connect process, but provides better autoconfiguration of utf-8
|
||||||
// position reports results in a 2 second delay during the
|
|
||||||
// connect process, but provides better autoconfiguration of utf-8
|
|
||||||
checkUtf8Encoding: true
|
checkUtf8Encoding: true
|
||||||
|
|
||||||
|
|
||||||
// Checking the ANSI home position also requires the use of
|
// Checking the ANSI home position also requires the use of cursor position reports, which are not
|
||||||
// cursor position reports, which are not supported on all
|
// supported on all terminals. Using this with a terminal that does not support cursor position reports
|
||||||
/// terminals. Using this with a terminal that does not support
|
// results in a 3 second delay during the connect process, but works around positioning problems with
|
||||||
// cursor position reports results in a 3 second delay during
|
|
||||||
// the connect process, but works around positioning problems with
|
|
||||||
// non-standard terminals.
|
// non-standard terminals.
|
||||||
checkAnsiHomePosition: true
|
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`
|
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.
|
* 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:
|
* **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
|
```hjson
|
||||||
|
@ -88,14 +74,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
|
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`).
|
* 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.
|
* 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`.
|
* 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!
|
* 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.
|
* 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.
|
* 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.
|
||||||
|
@ -122,7 +108,7 @@ webSocket: {
|
||||||
* Similar to the last item, `defaults.general.passwordChar` in `theme.hjson` is now just `defaults.passwordChar`.
|
* 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:
|
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**
|
* Configuration files are defaulted to `./config`. Related, the `--config` option now points to a configuration **directory**
|
||||||
* `./mods/art` has been moved to `./art/general`
|
* `./mods/art` has been moved to `./art/general`
|
||||||
|
@ -139,17 +125,17 @@ With the above changes, you'll need to to at least:
|
||||||
* Move any certificates, pub/private keys, etc. from `./misc` to `./config`
|
* Move any certificates, pub/private keys, etc. from `./misc` to `./config`
|
||||||
* Specify user modules as `@userModule:my_module_name`
|
* 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
|
No issues
|
||||||
|
|
||||||
## 0.0.5-alpha to 0.0.6-alpha
|
# 0.0.5-alpha to 0.0.6-alpha
|
||||||
No issues
|
No issues
|
||||||
|
|
||||||
## 0.0.4-alpha to 0.0.5-alpha
|
# 0.0.4-alpha to 0.0.5-alpha
|
||||||
No issues
|
No issues
|
||||||
|
|
||||||
## 0.0.1-alpha to 0.0.4-alpha
|
# 0.0.1-alpha to 0.0.4-alpha
|
||||||
### Node.js 6.x+ LTS is now **required**
|
## 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:
|
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
|
```bash
|
||||||
nvm install 6
|
nvm install 6
|
||||||
|
@ -159,7 +145,7 @@ nvm alias default 6
|
||||||
### ES6
|
### 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.
|
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:
|
A few upgrades need to be made to your SQLite databases:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -168,8 +154,8 @@ sqlite3 db/message.sqlite
|
||||||
sqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild');
|
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`
|
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).
|
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).
|
||||||
|
|
23
WHATSNEW.md
23
WHATSNEW.md
|
@ -1,32 +1,12 @@
|
||||||
# Whats New
|
# Whats New
|
||||||
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
|
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
|
## 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.
|
* **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!
|
* 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.
|
* 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.
|
* 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
|
## 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).
|
* 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).
|
||||||
|
@ -36,7 +16,6 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
|
||||||
* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired.
|
* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired.
|
||||||
* New `PV` ACS check for arbitrary user properties. See [ACS](./docs/configuration/acs.md) for details.
|
* New `PV` ACS check for arbitrary user properties. See [ACS](./docs/configuration/acs.md) for details.
|
||||||
* The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future.
|
* The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future.
|
||||||
* A number of new MCI codes (see [MCI](./docs/art/mci.md))
|
|
||||||
* Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system!
|
* Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system!
|
||||||
* The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information.
|
* The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information.
|
||||||
* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template.
|
* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template.
|
||||||
|
|
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: {
|
mci: {
|
||||||
VM1: {
|
VM1: {
|
||||||
height: 14
|
height: 15,
|
||||||
width: 50
|
width: 50
|
||||||
itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{location:<19.19}|03{lastLoginTs}"
|
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}"
|
focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{location:<19.19}{lastLoginTs}"
|
||||||
|
@ -246,96 +246,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainMenuWaitingForCaller: {
|
|
||||||
config: {
|
|
||||||
// formats
|
|
||||||
quickLogTimestampFormat: "|01|03MM|08/|03DD hh:mm:ssa"
|
|
||||||
nowDateTimeFormat: "|00|10ddd|08, |10MMMM Do YYYY|08, |10h|08:|10mm|02a"
|
|
||||||
lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a"
|
|
||||||
|
|
||||||
// header
|
|
||||||
mainInfoFormat10: "|00|10{now} |10{currentUserName} |08- |02Prv|08:|10{newPrivateMail} |02Addr|08:|10{newMessagesAddrTo} |08- |02Avail|08:|10{availIndicator} |02Vis|07:|10{visIndicator}"
|
|
||||||
|
|
||||||
// today
|
|
||||||
mainInfoFormat11: "|00|15{callsToday:>5}"
|
|
||||||
mainInfoFormat12: "|00|15{postsToday:>5}"
|
|
||||||
mainInfoFormat13: "|00|15{newUsersToday:>5}"
|
|
||||||
mainInfoFormat14: "|00|15{uploadsToday:<4}"
|
|
||||||
mainInfoFormat15: "|00|15{downloadsToday:<4}"
|
|
||||||
mainInfoFormat16: "|00|15{uploadBytesToday!sizeWithoutAbbr:<5} |07{uploadBytesToday!sizeAbbr}"
|
|
||||||
mainInfoFormat17: "|00|15{downloadBytesToday!sizeWithoutAbbr:<5} |07{downloadBytesToday!sizeAbbr}"
|
|
||||||
|
|
||||||
|
|
||||||
// last login
|
|
||||||
mainInfoFormat18: "|00|15{lastLoginUserName:<26} |07{lastLogin}"
|
|
||||||
|
|
||||||
// system stats
|
|
||||||
mainInfoFormat20: "|00|15{freeMemoryBytes!sizeWithoutAbbr} |07{freeMemoryBytes!sizeAbbr} free |08/ |15{totalMemoryBytes!sizeWithoutAbbr} |07{totalMemoryBytes!sizeAbbr}"
|
|
||||||
mainInfoFormat22: "|00|15{systemCurrentLoad} |07% |08/ |15{systemAvgLoad} |07load avg|08."
|
|
||||||
mainInfoFormat24: "|00|15{processUptimeSeconds!durationSeconds} |08/ |15{processBytesIngress!sizeWithoutAbbr:>4} |07{processBytesIngress!sizeAbbr}|08/|15{processBytesEgress!sizeWithoutAbbr:>4} |07{processBytesEgress!sizeAbbr}"
|
|
||||||
|
|
||||||
// totals
|
|
||||||
mainInfoFormat19: "|00|15{totalCalls:>5}"
|
|
||||||
mainInfoFormat21: "|00|15{totalPosts:>7}"
|
|
||||||
mainInfoFormat23: "|00|15{totalUsers:>5}"
|
|
||||||
mainInfoFormat25: "|00|15{totalFiles:>4} |08/ |15{totalFileBytes!sizeWithoutAbbr:>4} |07{totalFileBytes!sizeAbbr}"
|
|
||||||
|
|
||||||
quickLogLevel: info
|
|
||||||
quickLogLevelIndicators: {
|
|
||||||
trace : |00|02T
|
|
||||||
debug: |00|03D
|
|
||||||
info: |00|15I
|
|
||||||
warn: |00|14W
|
|
||||||
error: |00|12E
|
|
||||||
fatal: |00|28F
|
|
||||||
}
|
|
||||||
quickLogLevelMessagePrefixes: {
|
|
||||||
trace : |00|02
|
|
||||||
debug: |00|03
|
|
||||||
info: |00|07
|
|
||||||
warn: |00|14
|
|
||||||
error: |00|12
|
|
||||||
fatal: |00|28
|
|
||||||
}
|
|
||||||
statusAvailableIndicators: [ "N", "Y" ]
|
|
||||||
statusVisibleIndicators: [ "N", "Y" ]
|
|
||||||
|
|
||||||
nodeStatusSelectionFormat: "|00|07{realName:<12}\n|08- |07{serverName:<10}\n|08- |07{remoteAddress:<10}"
|
|
||||||
}
|
|
||||||
0: {
|
|
||||||
mci: {
|
|
||||||
TL16: {
|
|
||||||
fillChar: .
|
|
||||||
}
|
|
||||||
TL20: { width: 30 }
|
|
||||||
TL22: { width: 30 }
|
|
||||||
TL24: { width: 30 }
|
|
||||||
|
|
||||||
// node status
|
|
||||||
VM1: {
|
|
||||||
height: 5
|
|
||||||
width: 37
|
|
||||||
itemFormat: "|00 |15{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}"
|
|
||||||
focusItemFormat: "|00|10> |15{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}"
|
|
||||||
focusItemAtTop: false
|
|
||||||
}
|
|
||||||
// quick log
|
|
||||||
VM2: {
|
|
||||||
height: 5
|
|
||||||
width: 73
|
|
||||||
itemFormat: "|00|07{nodeId} {levelIndicator} |02{timestamp} {message:<51.50}"
|
|
||||||
}
|
|
||||||
|
|
||||||
MT3: {
|
|
||||||
mode: preview
|
|
||||||
autoScroll: false
|
|
||||||
height: 5
|
|
||||||
width: 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messageBaseMessageList: {
|
messageBaseMessageList: {
|
||||||
config: {
|
config: {
|
||||||
dateTimeFormat: ddd MMM Do
|
dateTimeFormat: ddd MMM Do
|
||||||
|
@ -343,7 +253,7 @@
|
||||||
}
|
}
|
||||||
mci: {
|
mci: {
|
||||||
VM1: {
|
VM1: {
|
||||||
height: 13
|
height: 14
|
||||||
width: 70
|
width: 70
|
||||||
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts:<15.16} |15{newIndicator}"
|
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}"
|
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts:<15.16} {newIndicator}"
|
||||||
|
@ -416,7 +326,7 @@
|
||||||
}
|
}
|
||||||
mci: {
|
mci: {
|
||||||
VM1: {
|
VM1: {
|
||||||
height: 12
|
height: 14
|
||||||
width: 70
|
width: 70
|
||||||
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}"
|
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}"
|
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
|
||||||
|
@ -595,7 +505,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageBaseSearchResultsMessageList: {
|
messageBaseSearchMessageList: {
|
||||||
config: {
|
config: {
|
||||||
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
|
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
|
||||||
// Fri Sep 25th
|
// Fri Sep 25th
|
||||||
|
@ -618,7 +528,7 @@
|
||||||
}
|
}
|
||||||
mci: {
|
mci: {
|
||||||
VM1: {
|
VM1: {
|
||||||
height: 12
|
height: 16
|
||||||
width: 71
|
width: 71
|
||||||
itemFormat: "|00|15 {msgNum:<4.4} |03{subject:<34.33} {fromUserName:<19.18} |03{ts:<12.12}"
|
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}"
|
focusItemFormat: "|00|19> |15{msgNum:<4.4} {subject:<34.33} {fromUserName:<19.18} {ts:<12.12}"
|
||||||
|
@ -783,7 +693,7 @@
|
||||||
}
|
}
|
||||||
mci: {
|
mci: {
|
||||||
VM1: {
|
VM1: {
|
||||||
height: 12
|
height: 14
|
||||||
width: 70
|
width: 70
|
||||||
itemFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}"
|
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}"
|
focusItemFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}"
|
||||||
|
@ -1214,7 +1124,7 @@
|
||||||
2: {
|
2: {
|
||||||
mci: {
|
mci: {
|
||||||
MT1: {
|
MT1: {
|
||||||
height: 13
|
height: 14
|
||||||
width: 45
|
width: 45
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -9,7 +9,6 @@ const ansi = require('./ansi_term.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
|
||||||
const Log = require('./logger').log;
|
const Log = require('./logger').log;
|
||||||
const Config = require('./config.js').get;
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -109,7 +108,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
name: self.config.name,
|
name: self.config.name,
|
||||||
activeCount: activeDoorNodeInstances[self.config.name],
|
activeCount: activeDoorNodeInstances[self.config.name],
|
||||||
},
|
},
|
||||||
`Too many active instances of door "${self.config.name}"`
|
'Too many active instances'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_.isString(self.config.tooManyArt)) {
|
if (_.isString(self.config.tooManyArt)) {
|
||||||
|
@ -180,10 +179,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
this.client.term.write(ansi.resetScreen());
|
this.client.term.write(ansi.resetScreen());
|
||||||
|
|
||||||
const exeInfo = {
|
const exeInfo = {
|
||||||
name: this.config.name,
|
|
||||||
cmd: this.config.cmd,
|
cmd: this.config.cmd,
|
||||||
preCmd: this.config.preCmd,
|
|
||||||
preCmdArgs: this.config.preCmdArgs,
|
|
||||||
cwd: this.config.cwd || paths.dirname(this.config.cmd),
|
cwd: this.config.cwd || paths.dirname(this.config.cmd),
|
||||||
args: this.config.args,
|
args: this.config.args,
|
||||||
io: this.config.io || 'stdio',
|
io: this.config.io || 'stdio',
|
||||||
|
@ -192,35 +188,14 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
env: this.config.env,
|
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) {
|
if (this.dropFile) {
|
||||||
exeInfo.dropFile = this.dropFile.fileName;
|
exeInfo.dropFile = this.dropFile.fileName;
|
||||||
exeInfo.dropFilePath = this.dropFile.fullPath;
|
exeInfo.dropFilePath = this.dropFile.fullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._makeDropDirs([exeInfo.dropFileDir, exeInfo.userAreaDir], err => {
|
|
||||||
if (err) {
|
|
||||||
Log.warn(
|
|
||||||
`Failed creating directory ${exeInfo.dropFilePath}: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
||||||
|
|
||||||
this.doorInstance.run(exeInfo, err => {
|
this.doorInstance.run(exeInfo, () => {
|
||||||
if (err) {
|
|
||||||
Log.error(`Error running "${this.config.name}": ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackDoorRunEnd(doorTracking);
|
trackDoorRunEnd(doorTracking);
|
||||||
this.decrementActiveDoorNodeInstances();
|
this.decrementActiveDoorNodeInstances();
|
||||||
|
|
||||||
|
@ -248,10 +223,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
//
|
//
|
||||||
this.client.term.rawWrite(
|
this.client.term.rawWrite(
|
||||||
ansi.normal() +
|
ansi.normal() +
|
||||||
ansi.goto(
|
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
|
||||||
this.client.term.termHeight,
|
|
||||||
this.client.term.termWidth
|
|
||||||
) +
|
|
||||||
ansi.setScrollRegion() +
|
ansi.setScrollRegion() +
|
||||||
ansi.goto(this.client.term.termHeight, 0) +
|
ansi.goto(this.client.term.termHeight, 0) +
|
||||||
'\r\n\r\n'
|
'\r\n\r\n'
|
||||||
|
@ -259,17 +231,6 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
|
|
||||||
this.autoNextMenu();
|
this.autoNextMenu();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_makeDropDirs(dirs, cb) {
|
|
||||||
async.forEach(
|
|
||||||
dirs,
|
|
||||||
(dir, nextDir) => {
|
|
||||||
fs.mkdir(dir, { recursive: true }, nextDir);
|
|
||||||
},
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leave() {
|
leave() {
|
||||||
|
|
|
@ -505,9 +505,9 @@ class Achievements {
|
||||||
getFormatObject(info) {
|
getFormatObject(info) {
|
||||||
return {
|
return {
|
||||||
userName: info.user.username,
|
userName: info.user.username,
|
||||||
userRealName: info.user.realName(false) || 'N/A',
|
userRealName: info.user.properties[UserProps.RealName],
|
||||||
userLocation: info.user.properties[UserProps.Location] || 'N/A',
|
userLocation: info.user.properties[UserProps.Location],
|
||||||
userAffils: info.user.properties[UserProps.Affiliations] || 'N/A',
|
userAffils: info.user.properties[UserProps.Affiliations],
|
||||||
nodeId: info.client.node,
|
nodeId: info.client.node,
|
||||||
title: info.details.title,
|
title: info.details.title,
|
||||||
//text : info.global ? info.details.globalText : info.details.text,
|
//text : info.global ? info.details.globalText : info.details.text,
|
||||||
|
|
|
@ -24,7 +24,7 @@ function ANSIEscapeParser(options) {
|
||||||
this.graphicRendition = {};
|
this.graphicRendition = {};
|
||||||
|
|
||||||
this.parseState = {
|
this.parseState = {
|
||||||
re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
|
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||||
};
|
};
|
||||||
|
|
||||||
options = miscUtil.valueWithDefault(options, {
|
options = miscUtil.valueWithDefault(options, {
|
||||||
|
@ -37,12 +37,6 @@ function ANSIEscapeParser(options) {
|
||||||
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||||
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
||||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
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.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
||||||
|
|
||||||
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
||||||
|
@ -77,25 +71,10 @@ function ANSIEscapeParser(options) {
|
||||||
self.clearScreen = function () {
|
self.clearScreen = function () {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row = 1;
|
self.row = 1;
|
||||||
self.positionUpdated();
|
|
||||||
self.emit('clear screen');
|
self.emit('clear screen');
|
||||||
};
|
};
|
||||||
|
|
||||||
self.positionUpdated = function () {
|
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);
|
self.emit('position update', self.row, self.column);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,8 +90,8 @@ function ANSIEscapeParser(options) {
|
||||||
|
|
||||||
switch (charCode) {
|
switch (charCode) {
|
||||||
case CR:
|
case CR:
|
||||||
self.emit('literal', text.slice(start, pos + 1));
|
self.emit('literal', text.slice(start, pos));
|
||||||
start = pos + 1;
|
start = pos;
|
||||||
|
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
|
|
||||||
|
@ -126,8 +105,8 @@ function ANSIEscapeParser(options) {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit('literal', text.slice(start, pos + 1));
|
self.emit('literal', text.slice(start, pos));
|
||||||
start = pos + 1;
|
start = pos;
|
||||||
|
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
|
|
||||||
|
@ -135,16 +114,13 @@ function ANSIEscapeParser(options) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (self.column === self.breakWidth) {
|
if (self.column === self.termWidth) {
|
||||||
self.emit('literal', text.slice(start, pos + 1));
|
self.emit('literal', text.slice(start, pos + 1));
|
||||||
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.column = 1;
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
|
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
} else {
|
} else {
|
||||||
self.column += 1;
|
self.column += 1;
|
||||||
|
@ -159,7 +135,7 @@ function ANSIEscapeParser(options) {
|
||||||
//
|
//
|
||||||
// Finalize this chunk
|
// Finalize this chunk
|
||||||
//
|
//
|
||||||
if (self.column > self.breakWidth) {
|
if (self.column > self.termWidth) {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
|
|
||||||
|
@ -246,7 +222,7 @@ function ANSIEscapeParser(options) {
|
||||||
self.parseState = {
|
self.parseState = {
|
||||||
// ignore anything past EOF marker, if any
|
// ignore anything past EOF marker, if any
|
||||||
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
|
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
|
||||||
re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
|
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
|
||||||
stop: false,
|
stop: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -286,47 +262,9 @@ function ANSIEscapeParser(options) {
|
||||||
opCode = match[2];
|
opCode = match[2];
|
||||||
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
|
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
|
||||||
|
|
||||||
// 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);
|
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);
|
self.emit('control', match[0], opCode, args);
|
||||||
}
|
}
|
||||||
} while (0 !== re.lastIndex);
|
} while (0 !== re.lastIndex);
|
||||||
|
@ -334,8 +272,8 @@ function ANSIEscapeParser(options) {
|
||||||
if (pos < buffer.length) {
|
if (pos < buffer.length) {
|
||||||
var lastBit = buffer.slice(pos);
|
var lastBit = buffer.slice(pos);
|
||||||
|
|
||||||
// handles either \r\n or \n
|
// :TODO: check for various ending LF's, not just DOS \r\n
|
||||||
if ('\n' === lastBit.slice(-1).toString()) {
|
if ('\r\n' === lastBit.slice(-2).toString()) {
|
||||||
switch (self.trailingLF) {
|
switch (self.trailingLF) {
|
||||||
case 'default':
|
case 'default':
|
||||||
//
|
//
|
||||||
|
@ -343,14 +281,14 @@ function ANSIEscapeParser(options) {
|
||||||
// if we're going to end on termHeight
|
// if we're going to end on termHeight
|
||||||
//
|
//
|
||||||
if (this.termHeight === self.row) {
|
if (this.termHeight === self.row) {
|
||||||
lastBit = lastBit.slice(0, -1);
|
lastBit = lastBit.slice(0, -2);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'omit':
|
case 'omit':
|
||||||
case 'no':
|
case 'no':
|
||||||
case false:
|
case false:
|
||||||
lastBit = lastBit.slice(0, -1);
|
lastBit = lastBit.slice(0, -2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -361,6 +299,48 @@ function ANSIEscapeParser(options) {
|
||||||
self.emit('complete');
|
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) {
|
function escape(opCode, args) {
|
||||||
let arg;
|
let arg;
|
||||||
|
|
||||||
|
@ -393,37 +373,6 @@ function ANSIEscapeParser(options) {
|
||||||
self.moveCursor(-arg, 0);
|
self.moveCursor(-arg, 0);
|
||||||
break;
|
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 'f': // horiz & vertical
|
||||||
case 'H': // cursor position
|
case 'H': // cursor position
|
||||||
//self.row = args[0] || 1;
|
//self.row = args[0] || 1;
|
||||||
|
@ -434,37 +383,14 @@ function ANSIEscapeParser(options) {
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// save position
|
||||||
// erase display/screen
|
case 's':
|
||||||
case 'J':
|
self.saveCursorPosition();
|
||||||
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;
|
break;
|
||||||
|
|
||||||
// erase text in line
|
// restore position
|
||||||
case 'K':
|
case 'u':
|
||||||
if(isNaN(args[0]) || 0 === args[0]) {
|
self.restoreCursorPosition();
|
||||||
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;
|
break;
|
||||||
|
|
||||||
// set graphic rendition
|
// set graphic rendition
|
||||||
|
@ -536,52 +462,15 @@ function ANSIEscapeParser(options) {
|
||||||
self.emit('sgr update', self.graphicRendition);
|
self.emit('sgr update', self.graphicRendition);
|
||||||
break; // m
|
break; // m
|
||||||
|
|
||||||
// save position
|
// :TODO: s, u, K
|
||||||
case 's':
|
|
||||||
self.saveCursorPosition();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Scroll up
|
// erase display/screen
|
||||||
case 'S':
|
case 'J':
|
||||||
arg = isNaN(args[0]) ? 1 : args[0];
|
// :TODO: Handle other 'J' types!
|
||||||
self.emit('scroll', arg);
|
if (2 === args[0]) {
|
||||||
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();
|
self.clearScreen();
|
||||||
|
}
|
||||||
break;
|
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,
|
// pty.js doesn't currently give us a error when things fail,
|
||||||
// so we have this horrible, horrible hack:
|
// so we have this horrible, horrible hack:
|
||||||
let err;
|
let err;
|
||||||
proc.onData(d => {
|
proc.once('data', d => {
|
||||||
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
|
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
|
||||||
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
|
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.onExit(exitEvent => {
|
proc.once('exit', exitCode => {
|
||||||
return cb(
|
return cb(
|
||||||
exitEvent.exitCode
|
exitCode
|
||||||
? Errors.ExternalProcess(
|
? Errors.ExternalProcess(
|
||||||
`${action} failed with exit code: ${exitEvent.exitCode}`
|
`${action} failed with exit code: ${exitCode}`
|
||||||
)
|
)
|
||||||
: err
|
: err
|
||||||
);
|
);
|
||||||
|
@ -358,10 +358,10 @@ module.exports = class ArchiveUtil {
|
||||||
output += data;
|
output += data;
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.onExit(exitEvent => {
|
proc.once('exit', exitCode => {
|
||||||
if (exitEvent.exitCode) {
|
if (exitCode) {
|
||||||
return cb(
|
return cb(
|
||||||
Errors.ExternalProcess(`List failed with exit code: ${exitEvent.exitCode}`)
|
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
82
core/art.js
82
core/art.js
|
@ -41,21 +41,11 @@ const SUPPORTED_ART_TYPES = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFontNameFromSAUCE(sauce) {
|
function getFontNameFromSAUCE(sauce) {
|
||||||
if (sauce && sauce.Character) {
|
if (sauce.Character) {
|
||||||
return sauce.Character.fontName;
|
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) {
|
function sliceAtEOF(data, eofMarker) {
|
||||||
let eof = data.length;
|
let eof = data.length;
|
||||||
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
|
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
|
||||||
|
@ -284,7 +274,6 @@ function display(client, art, options, cb) {
|
||||||
mciReplaceChar: options.mciReplaceChar,
|
mciReplaceChar: options.mciReplaceChar,
|
||||||
termHeight: client.term.termHeight,
|
termHeight: client.term.termHeight,
|
||||||
termWidth: client.term.termWidth,
|
termWidth: client.term.termWidth,
|
||||||
artWidth: getWidthFromSAUCE(options.sauce),
|
|
||||||
trailingLF: options.trailingLF,
|
trailingLF: options.trailingLF,
|
||||||
startRow: options.startRow,
|
startRow: options.startRow,
|
||||||
});
|
});
|
||||||
|
@ -316,75 +305,6 @@ 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('literal', literal => client.term.write(literal, false));
|
||||||
ansiParser.on('control', control => client.term.rawWrite(control));
|
ansiParser.on('control', control => client.term.rawWrite(control));
|
||||||
|
|
||||||
|
|
98
core/bbs.js
98
core/bbs.js
|
@ -2,6 +2,9 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
//var SegfaultHandler = require('segfault-handler');
|
||||||
|
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const conf = require('./config.js');
|
const conf = require('./config.js');
|
||||||
const logger = require('./logger.js');
|
const logger = require('./logger.js');
|
||||||
|
@ -10,7 +13,6 @@ const resolvePath = require('./misc_util.js').resolvePath;
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
const SysLogKeys = require('./system_log.js');
|
const SysLogKeys = require('./system_log.js');
|
||||||
const UserLogNames = require('./user_log_name');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -149,9 +151,7 @@ function shutdownSystem() {
|
||||||
[
|
[
|
||||||
function closeConnections(callback) {
|
function closeConnections(callback) {
|
||||||
const ClientConns = require('./client_connections.js');
|
const ClientConns = require('./client_connections.js');
|
||||||
const activeConnections = ClientConns.getActiveConnections(
|
const activeConnections = ClientConns.getActiveConnections();
|
||||||
ClientConns.AllConnections
|
|
||||||
);
|
|
||||||
let i = activeConnections.length;
|
let i = activeConnections.length;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
const activeTerm = activeConnections[i].term;
|
const activeTerm = activeConnections[i].term;
|
||||||
|
@ -257,8 +257,6 @@ function initialize(cb) {
|
||||||
//
|
//
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
|
|
||||||
// :TODO: use User.getUserInfo() for this!
|
|
||||||
|
|
||||||
const propLoadOpts = {
|
const propLoadOpts = {
|
||||||
names: [
|
names: [
|
||||||
UserProps.RealName,
|
UserProps.RealName,
|
||||||
|
@ -272,7 +270,7 @@ function initialize(cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function getOpUserName(next) {
|
function getOpUserName(next) {
|
||||||
return User.getUserName(User.RootUserID, next);
|
return User.getUserName(1, next);
|
||||||
},
|
},
|
||||||
function getOpProps(opUserName, next) {
|
function getOpProps(opUserName, next) {
|
||||||
User.loadProperties(
|
User.loadProperties(
|
||||||
|
@ -303,9 +301,8 @@ function initialize(cb) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function initSystemLogStats(callback) {
|
function initCallsToday(callback) {
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
logName: SysLogKeys.UserLoginHistory,
|
logName: SysLogKeys.UserLoginHistory,
|
||||||
resultType: 'count',
|
resultType: 'count',
|
||||||
|
@ -322,89 +319,6 @@ function initialize(cb) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function initUserLogStats(callback) {
|
|
||||||
const StatLog = require('./stat_log');
|
|
||||||
|
|
||||||
const entries = [
|
|
||||||
[UserLogNames.UlFiles, [SysProps.FileUlTodayCount, 'count']],
|
|
||||||
[UserLogNames.UlFileBytes, [SysProps.FileUlTodayBytes, 'obj']],
|
|
||||||
[UserLogNames.DlFiles, [SysProps.FileDlTodayCount, 'count']],
|
|
||||||
[UserLogNames.DlFileBytes, [SysProps.FileDlTodayBytes, 'obj']],
|
|
||||||
[UserLogNames.NewUser, [SysProps.NewUsersTodayCount, 'count']],
|
|
||||||
];
|
|
||||||
|
|
||||||
async.each(
|
|
||||||
entries,
|
|
||||||
(entry, nextEntry) => {
|
|
||||||
const [logName, [sysPropName, resultType]] = entry;
|
|
||||||
|
|
||||||
const filter = {
|
|
||||||
logName,
|
|
||||||
resultType,
|
|
||||||
date: moment(),
|
|
||||||
};
|
|
||||||
|
|
||||||
StatLog.findUserLogEntries(filter, (err, stat) => {
|
|
||||||
if (!err) {
|
|
||||||
if (resultType === 'obj') {
|
|
||||||
stat = stat.reduce(
|
|
||||||
(bytes, entry) =>
|
|
||||||
bytes + parseInt(entry.log_value) || 0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StatLog.setNonPersistentSystemStat(sysPropName, stat);
|
|
||||||
}
|
|
||||||
return nextEntry(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return callback(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function initLastLogin(callback) {
|
|
||||||
const StatLog = require('./stat_log');
|
|
||||||
StatLog.getSystemLogEntries(
|
|
||||||
SysLogKeys.UserLoginHistory,
|
|
||||||
'timestamp_desc',
|
|
||||||
1,
|
|
||||||
(err, lastLogin) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
let loginObj;
|
|
||||||
try {
|
|
||||||
loginObj = JSON.parse(lastLogin[0].log_value);
|
|
||||||
loginObj.timestamp = moment(lastLogin[0].timestamp);
|
|
||||||
} catch (e) {
|
|
||||||
return callback(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For live stats we want to resolve user ID -> name, etc.
|
|
||||||
const User = require('./user');
|
|
||||||
User.getUserInfo(loginObj.userId, (err, props) => {
|
|
||||||
const stat = Object.assign({}, props, loginObj);
|
|
||||||
StatLog.setNonPersistentSystemStat(SysProps.LastLogin, stat);
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function initUserCount(callback) {
|
|
||||||
const User = require('./user.js');
|
|
||||||
User.getUserCount((err, count) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatLog = require('./stat_log');
|
|
||||||
StatLog.setNonPersistentSystemStat(SysProps.TotalUserCount, count);
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function initMessageStats(callback) {
|
function initMessageStats(callback) {
|
||||||
return require('./message_area.js').startup(callback);
|
return require('./message_area.js').startup(callback);
|
||||||
},
|
},
|
||||||
|
|
|
@ -129,7 +129,7 @@ exports.getModule = class BBSLinkModule extends MenuModule {
|
||||||
'/auth.php?key=' + randomKey,
|
'/auth.php?key=' + randomKey,
|
||||||
headers,
|
headers,
|
||||||
function resp(err, body) {
|
function resp(err, body) {
|
||||||
const status = body.trim();
|
var status = body.trim();
|
||||||
|
|
||||||
if ('complete' === status) {
|
if ('complete' === status) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
|
|
|
@ -97,12 +97,7 @@ function Client(/*input, output*/) {
|
||||||
Object.defineProperty(this, 'currentTheme', {
|
Object.defineProperty(this, 'currentTheme', {
|
||||||
get: () => {
|
get: () => {
|
||||||
if (this.currentThemeConfig) {
|
if (this.currentThemeConfig) {
|
||||||
// :TODO: clean this up: We have a ugly transition state in which we have a pure raw config vs a ConfigLoader in which get() must be called
|
|
||||||
try {
|
|
||||||
return this.currentThemeConfig.get();
|
return this.currentThemeConfig.get();
|
||||||
} catch (e) {
|
|
||||||
return this.currentThemeConfig;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
info: {
|
info: {
|
||||||
|
@ -513,7 +508,7 @@ Client.prototype.startIdleMonitor = function () {
|
||||||
idleLogoutSeconds > 0 &&
|
idleLogoutSeconds > 0 &&
|
||||||
nowMs - this.lastActivityTime >= idleLogoutSeconds * 1000
|
nowMs - this.lastActivityTime >= idleLogoutSeconds * 1000
|
||||||
) {
|
) {
|
||||||
this.emit('idle timeout', idleLogoutSeconds);
|
this.emit('idle timeout');
|
||||||
}
|
}
|
||||||
}, 1000 * 60);
|
}, 1000 * 60);
|
||||||
};
|
};
|
||||||
|
@ -597,15 +592,6 @@ Client.prototype.isLocal = function () {
|
||||||
return ['127.0.0.1', '::ffff:127.0.0.1'].includes(this.remoteAddress);
|
return ['127.0.0.1', '::ffff:127.0.0.1'].includes(this.remoteAddress);
|
||||||
};
|
};
|
||||||
|
|
||||||
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');
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Default error handlers
|
// Default error handlers
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -21,86 +21,42 @@ exports.getConnectionByNodeId = getConnectionByNodeId;
|
||||||
const clientConnections = [];
|
const clientConnections = [];
|
||||||
exports.clientConnections = clientConnections;
|
exports.clientConnections = clientConnections;
|
||||||
|
|
||||||
const AllConnections = { authUsersOnly: false, visibleOnly: false, availOnly: false };
|
function getActiveConnections(authUsersOnly = false) {
|
||||||
exports.AllConnections = AllConnections;
|
|
||||||
|
|
||||||
const UserVisibleConnections = {
|
|
||||||
authUsersOnly: false,
|
|
||||||
visibleOnly: true,
|
|
||||||
availOnly: false,
|
|
||||||
};
|
|
||||||
exports.UserVisibleConnections = UserVisibleConnections;
|
|
||||||
|
|
||||||
const UserMessageableConnections = {
|
|
||||||
authUsersOnly: true,
|
|
||||||
visibleOnly: true,
|
|
||||||
availOnly: true,
|
|
||||||
};
|
|
||||||
exports.UserMessageableConnections = UserMessageableConnections;
|
|
||||||
|
|
||||||
function getActiveConnections(
|
|
||||||
options = { authUsersOnly: true, visibleOnly: true, availOnly: false }
|
|
||||||
) {
|
|
||||||
return clientConnections.filter(conn => {
|
return clientConnections.filter(conn => {
|
||||||
if (options.authUsersOnly && !conn.user.isAuthenticated()) {
|
return (authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (options.visibleOnly && !conn.user.isVisible()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (options.availOnly && !conn.user.isAvailable()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveConnectionList(
|
function getActiveConnectionList(authUsersOnly) {
|
||||||
options = { authUsersOnly: true, visibleOnly: true, availOnly: false }
|
if (!_.isBoolean(authUsersOnly)) {
|
||||||
) {
|
authUsersOnly = true;
|
||||||
const now = moment();
|
|
||||||
|
|
||||||
return _.map(getActiveConnections(options), ac => {
|
|
||||||
let action;
|
|
||||||
try {
|
|
||||||
// attempting to fetch a bad menu stack item can blow up/assert
|
|
||||||
action = _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown');
|
|
||||||
} catch (e) {
|
|
||||||
action = 'Unknown';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = moment();
|
||||||
|
|
||||||
|
return _.map(getActiveConnections(authUsersOnly), ac => {
|
||||||
const entry = {
|
const entry = {
|
||||||
node: ac.node,
|
node: ac.node,
|
||||||
authenticated: ac.user.isAuthenticated(),
|
authenticated: ac.user.isAuthenticated(),
|
||||||
userId: ac.user.userId,
|
userId: ac.user.userId,
|
||||||
action: action,
|
action: _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'),
|
||||||
serverName: ac.session.serverName,
|
|
||||||
isSecure: ac.session.isSecure,
|
|
||||||
isVisible: ac.user.isVisible(),
|
|
||||||
isAvailable: ac.user.isAvailable(),
|
|
||||||
remoteAddress: ac.friendlyRemoteAddress(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// There may be a connection, but not a logged in user as of yet
|
// There may be a connection, but not a logged in user as of yet
|
||||||
//
|
//
|
||||||
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()) {
|
if (ac.user.isAuthenticated()) {
|
||||||
// :TODO: track pre-auth time so we can properly track this
|
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];
|
||||||
|
|
||||||
const diff = now.diff(
|
const diff = now.diff(
|
||||||
moment(ac.user.properties[UserProps.LastLoginTs]),
|
moment(ac.user.properties[UserProps.LastLoginTs]),
|
||||||
'minutes'
|
'minutes'
|
||||||
);
|
);
|
||||||
entry.timeOn = moment.duration(diff, 'minutes');
|
entry.timeOn = moment.duration(diff, 'minutes');
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -125,15 +81,6 @@ function addNewClient(client, clientSock) {
|
||||||
moment().valueOf(),
|
moment().valueOf(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// kludge to refresh process update stats at first client
|
|
||||||
if (clientConnections.length < 1) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const StatLog = require('./stat_log');
|
|
||||||
const SysProps = require('./system_property');
|
|
||||||
StatLog.getSystemStat(SysProps.ProcessTrafficStats);
|
|
||||||
}, 3000); // slight pause to wait for updates
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConnections.push(client);
|
clientConnections.push(client);
|
||||||
clientConnections.sort((c1, c2) => c1.session.id - c2.session.id);
|
clientConnections.sort((c1, c2) => c1.session.id - c2.session.id);
|
||||||
|
|
||||||
|
@ -143,7 +90,6 @@ function addNewClient(client, clientSock) {
|
||||||
|
|
||||||
const connInfo = {
|
const connInfo = {
|
||||||
remoteAddress: remoteAddress,
|
remoteAddress: remoteAddress,
|
||||||
friendlyRemoteAddress: client.friendlyRemoteAddress(),
|
|
||||||
serverName: client.session.serverName,
|
serverName: client.session.serverName,
|
||||||
isSecure: client.session.isSecure,
|
isSecure: client.session.isSecure,
|
||||||
};
|
};
|
||||||
|
@ -153,10 +99,7 @@ function addNewClient(client, clientSock) {
|
||||||
connInfo.family = clientSock.localFamily;
|
connInfo.family = clientSock.localFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.log.info(
|
client.log.info(connInfo, 'Client connected');
|
||||||
connInfo,
|
|
||||||
`Client connected on node ${nodeId} (${connInfo.serverName}/${connInfo.port})`
|
|
||||||
);
|
|
||||||
|
|
||||||
Events.emit(Events.getSystemEvents().ClientConnected, {
|
Events.emit(Events.getSystemEvents().ClientConnected, {
|
||||||
client: client,
|
client: client,
|
||||||
|
@ -178,7 +121,7 @@ function removeClient(client) {
|
||||||
connectionCount: clientConnections.length,
|
connectionCount: clientConnections.length,
|
||||||
nodeId: client.node,
|
nodeId: client.node,
|
||||||
},
|
},
|
||||||
`Client disconnected from node ${client.node}`
|
'Client disconnected'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (client.user && client.user.isValid()) {
|
if (client.user && client.user.isValid()) {
|
||||||
|
@ -200,9 +143,9 @@ function removeClient(client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionByUserId(userId) {
|
function getConnectionByUserId(userId) {
|
||||||
return getActiveConnections(AllConnections).find(ac => userId === ac.user.userId);
|
return getActiveConnections().find(ac => userId === ac.user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionByNodeId(nodeId) {
|
function getConnectionByNodeId(nodeId) {
|
||||||
return getActiveConnections(AllConnections).find(ac => nodeId == ac.node);
|
return getActiveConnections().find(ac => nodeId == ac.node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,19 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Log = require('./logger.js').log;
|
var Log = require('./logger.js').log;
|
||||||
const renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const iconv = require('iconv-lite');
|
var iconv = require('iconv-lite');
|
||||||
const assert = require('assert');
|
var assert = require('assert');
|
||||||
const _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
exports.ClientTerminal = ClientTerminal;
|
exports.ClientTerminal = ClientTerminal;
|
||||||
|
|
||||||
function ClientTerminal(output) {
|
function ClientTerminal(output) {
|
||||||
this.output = output;
|
this.output = output;
|
||||||
|
|
||||||
let outputEncoding = 'cp437';
|
var outputEncoding = 'cp437';
|
||||||
assert(iconv.encodingExists(outputEncoding));
|
assert(iconv.encodingExists(outputEncoding));
|
||||||
|
|
||||||
// convert line feeds such as \n -> \r\n
|
// convert line feeds such as \n -> \r\n
|
||||||
|
@ -26,10 +26,10 @@ function ClientTerminal(output) {
|
||||||
// Some terminal we handle specially
|
// Some terminal we handle specially
|
||||||
// They can also be found in this.env{}
|
// They can also be found in this.env{}
|
||||||
//
|
//
|
||||||
let termType = 'unknown';
|
var termType = 'unknown';
|
||||||
let termHeight = 0;
|
var termHeight = 0;
|
||||||
let termWidth = 0;
|
var termWidth = 0;
|
||||||
let termClient = 'unknown';
|
var termClient = 'unknown';
|
||||||
|
|
||||||
this.currentSyncFont = 'not_set';
|
this.currentSyncFont = 'not_set';
|
||||||
|
|
||||||
|
@ -42,10 +42,6 @@ function ClientTerminal(output) {
|
||||||
},
|
},
|
||||||
set: function (enc) {
|
set: function (enc) {
|
||||||
if (iconv.encodingExists(enc)) {
|
if (iconv.encodingExists(enc)) {
|
||||||
Log.info(
|
|
||||||
{ encoding: enc, currentEncoding: outputEncoding },
|
|
||||||
`Output encoding changed to ${enc}`
|
|
||||||
);
|
|
||||||
outputEncoding = enc;
|
outputEncoding = enc;
|
||||||
} else {
|
} else {
|
||||||
Log.warn({ encoding: enc }, 'Unknown encoding');
|
Log.warn({ encoding: enc }, 'Unknown encoding');
|
||||||
|
@ -60,11 +56,6 @@ function ClientTerminal(output) {
|
||||||
set: function (ttype) {
|
set: function (ttype) {
|
||||||
termType = ttype.toLowerCase();
|
termType = ttype.toLowerCase();
|
||||||
|
|
||||||
Log.debug(
|
|
||||||
{ encoding: this.outputEncoding },
|
|
||||||
`Terminal type changed to ${termType}; Adjusting output encoding`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.isNixTerm()) {
|
if (this.isNixTerm()) {
|
||||||
this.outputEncoding = 'utf8';
|
this.outputEncoding = 'utf8';
|
||||||
} else {
|
} else {
|
||||||
|
@ -74,6 +65,11 @@ function ClientTerminal(output) {
|
||||||
// :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification
|
// :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification
|
||||||
// Windows telnet will send "VTNT". If so, set termClient='windows'
|
// Windows telnet will send "VTNT". If so, set termClient='windows'
|
||||||
// there are some others on the page as well
|
// there are some others on the page as well
|
||||||
|
|
||||||
|
Log.debug(
|
||||||
|
{ encoding: this.outputEncoding },
|
||||||
|
'Set output encoding due to terminal type change'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
/* 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,15 +188,22 @@ module.exports = () => {
|
||||||
//
|
//
|
||||||
// 1 - Generate a Private Key (PK):
|
// 1 - Generate a Private Key (PK):
|
||||||
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
|
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
|
||||||
// For information on generating a key, see:
|
// To generate a secure PK, issue the following command:
|
||||||
// https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html#generate-a-ssh-private-key
|
//
|
||||||
|
// > 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
|
||||||
//
|
//
|
||||||
// 2 - Set 'privateKeyPass' to the password you used in step #1
|
// 2 - Set 'privateKeyPass' to the password you used in step #1
|
||||||
//
|
//
|
||||||
// 3 - Finally, set 'enabled' to 'true'
|
// 3 - Finally, set 'enabled' to 'true'
|
||||||
//
|
//
|
||||||
// Additional reading:
|
// Additional reading:
|
||||||
// - https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html
|
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
|
||||||
|
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
|
||||||
//
|
//
|
||||||
privateKeyPem: paths.join(
|
privateKeyPem: paths.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -215,18 +222,14 @@ module.exports = () => {
|
||||||
//
|
//
|
||||||
algorithms: {
|
algorithms: {
|
||||||
kex: [
|
kex: [
|
||||||
'curve25519-sha256',
|
|
||||||
'curve25519-sha256@libssh.org',
|
|
||||||
'ecdh-sha2-nistp256',
|
'ecdh-sha2-nistp256',
|
||||||
'ecdh-sha2-nistp384',
|
'ecdh-sha2-nistp384',
|
||||||
'ecdh-sha2-nistp521',
|
'ecdh-sha2-nistp521',
|
||||||
'diffie-hellman-group14-sha1',
|
'diffie-hellman-group14-sha1',
|
||||||
'diffie-hellman-group1-sha1',
|
'diffie-hellman-group1-sha1',
|
||||||
'curve25519-sha256',
|
// Group exchange not currnetly supported
|
||||||
'curve25519-sha256@libssh.org',
|
// 'diffie-hellman-group-exchange-sha256',
|
||||||
'ecdh-sha2-nistp256',
|
// 'diffie-hellman-group-exchange-sha1',
|
||||||
'ecdh-sha2-nistp384',
|
|
||||||
'ecdh-sha2-nistp521',
|
|
||||||
],
|
],
|
||||||
cipher: [
|
cipher: [
|
||||||
'aes128-ctr',
|
'aes128-ctr',
|
||||||
|
@ -239,7 +242,12 @@ module.exports = () => {
|
||||||
'aes256-cbc',
|
'aes256-cbc',
|
||||||
'aes192-cbc',
|
'aes192-cbc',
|
||||||
'aes128-cbc',
|
'aes128-cbc',
|
||||||
|
'blowfish-cbc',
|
||||||
'3des-cbc',
|
'3des-cbc',
|
||||||
|
'arcfour256',
|
||||||
|
'arcfour128',
|
||||||
|
'cast128-cbc',
|
||||||
|
'arcfour',
|
||||||
],
|
],
|
||||||
hmac: [
|
hmac: [
|
||||||
'hmac-sha2-256',
|
'hmac-sha2-256',
|
||||||
|
@ -937,10 +945,8 @@ module.exports = () => {
|
||||||
],
|
],
|
||||||
|
|
||||||
web: {
|
web: {
|
||||||
// if you change the /_f/ prefix here, ensure something
|
path: '/f/',
|
||||||
// non-colliding with other routes is utilized
|
routePath: '/f/[a-zA-Z0-9]+$',
|
||||||
path: '/_f/',
|
|
||||||
routePath: '^/_f/[a-zA-Z0-9]+$',
|
|
||||||
expireMinutes: 1440, // 1 day
|
expireMinutes: 1440, // 1 day
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ module.exports = class ConfigLoader {
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
config,
|
config,
|
||||||
(defaultVal, configVal, key, target, source) => {
|
(defaultVal, configVal, key, target, source) => {
|
||||||
let path;
|
var path;
|
||||||
while (true) {
|
while (true) {
|
||||||
// eslint-disable-line no-constant-condition
|
// eslint-disable-line no-constant-condition
|
||||||
if (!stack.length) {
|
if (!stack.length) {
|
||||||
|
|
|
@ -157,7 +157,9 @@ const ansiQuerySyncTermFontSupport = (client, cb) => {
|
||||||
const [_, w] = pos;
|
const [_, w] = pos;
|
||||||
if (w === 1) {
|
if (w === 1) {
|
||||||
// cursor didn't move
|
// cursor didn't move
|
||||||
client.log.info(`SyncTERM font support enabled on node ${client.node}`);
|
client.log.info(
|
||||||
|
'Client supports SyncTERM fonts or properly ignores unknown ESC sequence'
|
||||||
|
);
|
||||||
client.term.syncTermFontsEnabled = true;
|
client.term.syncTermFontsEnabled = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
83
core/door.js
83
core/door.js
|
@ -11,7 +11,6 @@ const decode = require('iconv-lite').decode;
|
||||||
const createServer = require('net').createServer;
|
const createServer = require('net').createServer;
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
|
||||||
|
|
||||||
module.exports = class Door {
|
module.exports = class Door {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
|
@ -33,7 +32,7 @@ module.exports = class Door {
|
||||||
});
|
});
|
||||||
|
|
||||||
conn.once('error', err => {
|
conn.once('error', err => {
|
||||||
this.client.log.warn(
|
this.client.log.info(
|
||||||
{ error: err.message },
|
{ error: err.message },
|
||||||
'Door socket server connection'
|
'Door socket server connection'
|
||||||
);
|
);
|
||||||
|
@ -57,84 +56,38 @@ module.exports = class Door {
|
||||||
run(exeInfo, cb) {
|
run(exeInfo, cb) {
|
||||||
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
||||||
|
|
||||||
if ('socket' === this.io) {
|
if ('socket' === this.io && !this.sockServer) {
|
||||||
if(!this.sockServer) {
|
|
||||||
return cb(Errors.UnexpectedState('Socket server is not running'));
|
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);
|
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
|
||||||
|
|
||||||
const formatObj = {
|
const formatObj = {
|
||||||
dropFile: exeInfo.dropFile,
|
dropFile: exeInfo.dropFile,
|
||||||
dropFilePath: exeInfo.dropFilePath,
|
dropFilePath: exeInfo.dropFilePath,
|
||||||
dropFileDir: exeInfo.dropFileDir,
|
|
||||||
userAreaDir: exeInfo.userAreaDir,
|
|
||||||
node: exeInfo.node.toString(),
|
node: exeInfo.node.toString(),
|
||||||
srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1',
|
srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1',
|
||||||
userId: this.client.user.userId.toString(),
|
userId: this.client.user.userId.toString(),
|
||||||
userName: this.client.user.getSanitizedName(),
|
userName: this.client.user.getSanitizedName(),
|
||||||
userNameRaw: this.client.user.username,
|
userNameRaw: this.client.user.username,
|
||||||
termWidth: this.client.term.termWidth,
|
|
||||||
termHeight: this.client.term.termHeight,
|
|
||||||
cwd: cwd,
|
cwd: cwd,
|
||||||
};
|
};
|
||||||
|
|
||||||
const args = exeInfo.args.map(arg => stringFormat(arg, formatObj));
|
const args = exeInfo.args.map(arg => stringFormat(arg, formatObj));
|
||||||
|
|
||||||
const spawnOptions = {
|
this.client.log.info(
|
||||||
|
{ cmd: exeInfo.cmd, args, io: this.io },
|
||||||
|
'Executing external door process'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.doorPty = pty.spawn(exeInfo.cmd, args, {
|
||||||
cols: this.client.term.termWidth,
|
cols: this.client.term.termWidth,
|
||||||
rows: this.client.term.termHeight,
|
rows: this.client.term.termHeight,
|
||||||
cwd: cwd,
|
cwd: cwd,
|
||||||
env: exeInfo.env,
|
env: exeInfo.env,
|
||||||
encoding: null, // we want to handle all encoding ourself
|
encoding: null, // we want to handle all encoding ourself
|
||||||
};
|
|
||||||
|
|
||||||
async.series(
|
|
||||||
[
|
|
||||||
callback => {
|
|
||||||
if (!_.isString(exeInfo.preCmd)) {
|
|
||||||
return callback(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const preCmdArgs = (exeInfo.preCmdArgs || []).map(arg =>
|
|
||||||
stringFormat(arg, formatObj)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.client.log.info(
|
|
||||||
{ cmd: exeInfo.preCmd, args: preCmdArgs },
|
|
||||||
`Executing external door pre-command (${exeInfo.name})`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const prePty = pty.spawn(
|
|
||||||
exeInfo.preCmd,
|
|
||||||
preCmdArgs,
|
|
||||||
spawnOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
callback => {
|
|
||||||
this.client.log.info(
|
|
||||||
{ cmd: exeInfo.cmd, args, io: this.io },
|
|
||||||
`Executing external door (${exeInfo.name})`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.doorPty = pty.spawn(exeInfo.cmd, args, spawnOptions);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
|
@ -145,8 +98,7 @@ module.exports = class Door {
|
||||||
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
|
||||||
if (
|
if (
|
||||||
this.doorPty &&
|
this.doorPty &&
|
||||||
this.client.session.uniqueId ===
|
this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')
|
||||||
_.get(evt, 'client.session.uniqueId')
|
|
||||||
) {
|
) {
|
||||||
this.client.log.info(
|
this.client.log.info(
|
||||||
{ pid: this.doorPty.pid },
|
{ pid: this.doorPty.pid },
|
||||||
|
@ -168,7 +120,7 @@ module.exports = class Door {
|
||||||
|
|
||||||
this.doorPty.onData(this.doorDataHandler.bind(this));
|
this.doorPty.onData(this.doorDataHandler.bind(this));
|
||||||
|
|
||||||
this.doorPty.onExit( (/*exitEvent*/) => {
|
this.doorPty.once('close', () => {
|
||||||
return this.restoreIo(this.doorPty);
|
return this.restoreIo(this.doorPty);
|
||||||
});
|
});
|
||||||
} else if ('socket' === this.io) {
|
} else if ('socket' === this.io) {
|
||||||
|
@ -181,9 +133,8 @@ module.exports = class Door {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.doorPty.onExit(exitEvent => {
|
this.doorPty.once('exit', exitCode => {
|
||||||
const {exitCode, signal} = exitEvent;
|
this.client.log.info({ exitCode: exitCode }, 'Door exited');
|
||||||
this.client.log.info({ exitCode, signal }, 'Door exited');
|
|
||||||
|
|
||||||
if (this.sockServer) {
|
if (this.sockServer) {
|
||||||
this.sockServer.close();
|
this.sockServer.close();
|
||||||
|
@ -197,14 +148,8 @@ module.exports = class Door {
|
||||||
this.doorPty.removeAllListeners();
|
this.doorPty.removeAllListeners();
|
||||||
delete this.doorPty;
|
delete this.doorPty;
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
],
|
|
||||||
() => {
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doorDataHandler(data) {
|
doorDataHandler(data) {
|
||||||
|
|
|
@ -34,15 +34,8 @@ module.exports = class DropFile {
|
||||||
this.baseDir = baseDir;
|
this.baseDir = baseDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
static dropFileDirectory(baseDir, client) {
|
|
||||||
return paths.join(baseDir, 'node' + client.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
get fullPath() {
|
get fullPath() {
|
||||||
return paths.join(
|
return paths.join(this.baseDir, 'node' + this.client.node, this.fileName);
|
||||||
DropFile.dropFileDirectory(this.baseDir, this.client),
|
|
||||||
this.fileName
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get fileName() {
|
get fileName() {
|
||||||
|
|
|
@ -114,7 +114,7 @@ class ScheduledEvent {
|
||||||
executeAction(reason, cb) {
|
executeAction(reason, cb) {
|
||||||
Log.info(
|
Log.info(
|
||||||
{ eventName: this.name, action: this.action, reason: reason },
|
{ eventName: this.name, action: this.action, reason: reason },
|
||||||
`Executing scheduled event "${this.name}"...`
|
'Executing scheduled event action...'
|
||||||
);
|
);
|
||||||
|
|
||||||
if ('method' === this.action.type) {
|
if ('method' === this.action.type) {
|
||||||
|
@ -167,17 +167,17 @@ class ScheduledEvent {
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.onExit(exitEvent => {
|
proc.once('exit', exitCode => {
|
||||||
if (exitEvent.exitCode) {
|
if (exitCode) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
{ eventName: this.name, action: this.action, exitCode: exitEvent.exitCode },
|
{ eventName: this.name, action: this.action, exitCode: exitCode },
|
||||||
'Bad exit code while performing scheduled event action'
|
'Bad exit code while performing scheduled event action'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return cb(
|
return cb(
|
||||||
exitEvent.exitCode
|
exitCode
|
||||||
? Errors.ExternalProcess(
|
? Errors.ExternalProcess(
|
||||||
`Bad exit code while performing scheduled event action: ${exitEvent.exitCode}`
|
`Bad exit code while performing scheduled event action: ${exitCode}`
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
|
const theme = require('./theme.js');
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
const FileArea = require('./file_base_area.js');
|
const FileArea = require('./file_base_area.js');
|
||||||
|
@ -75,8 +77,6 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
this.fileList = _.get(options, 'extraArgs.fileList');
|
this.fileList = _.get(options, 'extraArgs.fileList');
|
||||||
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
|
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
|
||||||
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
if (this.fileList) {
|
if (this.fileList) {
|
||||||
// we'll need to adjust position as well!
|
// we'll need to adjust position as well!
|
||||||
this.fileListPosition = 0;
|
this.fileListPosition = 0;
|
||||||
|
@ -344,19 +344,74 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
displayArtDataPrepCallback(name, artData, viewController) {
|
displayArtAndPrepViewController(name, options, cb) {
|
||||||
if (name === 'details') {
|
const self = this;
|
||||||
|
const config = this.menuConfig.config;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function readyAndDisplayArt(callback) {
|
||||||
|
if (options.clearScreen) {
|
||||||
|
self.client.term.rawWrite(ansi.resetScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.displayThemedAsset(
|
||||||
|
config.art[name],
|
||||||
|
self.client,
|
||||||
|
{ font: self.menuConfig.font, trailingLF: false },
|
||||||
|
(err, artData) => {
|
||||||
|
return callback(err, artData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function prepeareViewController(artData, callback) {
|
||||||
|
if (_.isUndefined(self.viewControllers[name])) {
|
||||||
|
const vcOpts = {
|
||||||
|
client: self.client,
|
||||||
|
formId: FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_.isUndefined(options.noInput)) {
|
||||||
|
vcOpts.noInput = options.noInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vc = self.addViewController(
|
||||||
|
name,
|
||||||
|
new ViewController(vcOpts)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ('details' === name) {
|
||||||
try {
|
try {
|
||||||
this.detailsInfoArea = {
|
self.detailsInfoArea = {
|
||||||
top: artData.mciMap.XY2.position,
|
top: artData.mciMap.XY2.position,
|
||||||
bottom: artData.mciMap.XY3.position,
|
bottom: artData.mciMap.XY3.position,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Errors.DoesNotExist(
|
return callback(
|
||||||
'File listing details %XY2 and/or %XY3 MCI position indicators!'
|
Errors.DoesNotExist(
|
||||||
|
'Missing XY2 and XY3 position indicators!'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadOpts = {
|
||||||
|
callingMenu: self,
|
||||||
|
mciMap: artData.mciMap,
|
||||||
|
formId: FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.viewControllers[name].setFocus(true);
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
displayBrowsePage(clearScreen, cb) {
|
displayBrowsePage(clearScreen, cb) {
|
||||||
|
@ -381,11 +436,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
function prepArtAndViewController(callback) {
|
function prepArtAndViewController(callback) {
|
||||||
return self.displayArtAndPrepViewController(
|
return self.displayArtAndPrepViewController(
|
||||||
'browse',
|
'browse',
|
||||||
FormIds.browse,
|
{ clearScreen: clearScreen },
|
||||||
{
|
|
||||||
clearScreen: clearScreen,
|
|
||||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
|
||||||
},
|
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -477,11 +528,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
function prepArtAndViewController(callback) {
|
function prepArtAndViewController(callback) {
|
||||||
return self.displayArtAndPrepViewController(
|
return self.displayArtAndPrepViewController(
|
||||||
'details',
|
'details',
|
||||||
FormIds.details,
|
{ clearScreen: true },
|
||||||
{
|
|
||||||
clearScreen: true,
|
|
||||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
|
||||||
},
|
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -731,16 +778,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
|
|
||||||
return self.displayArtAndPrepViewController(
|
return self.displayArtAndPrepViewController(
|
||||||
name,
|
name,
|
||||||
FormIds[name],
|
{ clearScreen: false, noInput: true },
|
||||||
{
|
|
||||||
clearScreen: false,
|
|
||||||
noInput: true,
|
|
||||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
|
||||||
viewOffsets: {
|
|
||||||
col: 0,
|
|
||||||
row: self.detailsInfoArea.top[0] - 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -534,12 +534,6 @@ class FileAreaWebAccess {
|
||||||
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
|
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
|
||||||
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
|
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
|
||||||
|
|
||||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1);
|
|
||||||
StatLog.incrementNonPersistentSystemStat(
|
|
||||||
SysProps.FileDlTodayBytes,
|
|
||||||
dlBytes
|
|
||||||
);
|
|
||||||
|
|
||||||
return callback(null, user);
|
return callback(null, user);
|
||||||
},
|
},
|
||||||
function sendEvent(user, callback) {
|
function sendEvent(user, callback) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
|
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const SysProps = require('./system_property.js');
|
const SysProps = require('./system_property.js');
|
||||||
|
@ -24,8 +24,6 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
selectArea: (formData, extraArgs, cb) => {
|
selectArea: (formData, extraArgs, cb) => {
|
||||||
const filterCriteria = {
|
const filterCriteria = {
|
||||||
|
@ -36,7 +34,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
|
||||||
extraArgs: {
|
extraArgs: {
|
||||||
filterCriteria: filterCriteria,
|
filterCriteria: filterCriteria,
|
||||||
},
|
},
|
||||||
menuFlags: [ MenuFlags.NoHistory ],
|
menuFlags: ['popParent', 'mergeFlags'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
|
const theme = require('./theme.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const FileAreaWeb = require('./file_area_web.js');
|
const FileAreaWeb = require('./file_area_web.js');
|
||||||
|
@ -36,8 +38,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
this.dlQueue = new DownloadQueue(this.client);
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
if (_.has(options, 'lastMenuResult.sentFileIds')) {
|
if (_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||||
|
@ -194,7 +194,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
function prepArtAndViewController(callback) {
|
function prepArtAndViewController(callback) {
|
||||||
return self.displayArtAndPrepViewController(
|
return self.displayArtAndPrepViewController(
|
||||||
'queueManager',
|
'queueManager',
|
||||||
FormIds.queueManager,
|
|
||||||
{ clearScreen: clearScreen },
|
{ clearScreen: clearScreen },
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
|
@ -210,4 +209,59 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayArtAndPrepViewController(name, options, cb) {
|
||||||
|
const self = this;
|
||||||
|
const config = this.menuConfig.config;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function readyAndDisplayArt(callback) {
|
||||||
|
if (options.clearScreen) {
|
||||||
|
self.client.term.rawWrite(ansi.resetScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.displayThemedAsset(
|
||||||
|
config.art[name],
|
||||||
|
self.client,
|
||||||
|
{ font: self.menuConfig.font, trailingLF: false },
|
||||||
|
(err, artData) => {
|
||||||
|
return callback(err, artData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function prepeareViewController(artData, callback) {
|
||||||
|
if (_.isUndefined(self.viewControllers[name])) {
|
||||||
|
const vcOpts = {
|
||||||
|
client: self.client,
|
||||||
|
formId: FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_.isUndefined(options.noInput)) {
|
||||||
|
vcOpts.noInput = options.noInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vc = self.addViewController(
|
||||||
|
name,
|
||||||
|
new ViewController(vcOpts)
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadOpts = {
|
||||||
|
callingMenu: self,
|
||||||
|
mciMap: artData.mciMap,
|
||||||
|
formId: FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.viewControllers[name].setFocus(true);
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -121,6 +121,7 @@ exports.getModule = class FileBaseSearch extends MenuModule {
|
||||||
extraArgs: {
|
extraArgs: {
|
||||||
filterCriteria: filterCriteria,
|
filterCriteria: filterCriteria,
|
||||||
},
|
},
|
||||||
|
menuFlags: ['popParent'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
const FileArea = require('./file_base_area.js');
|
const FileArea = require('./file_base_area.js');
|
||||||
const { renderSubstr } = require('./string_util.js');
|
const { renderSubstr } = require('./string_util.js');
|
||||||
|
@ -65,9 +65,6 @@ const MciViewIds = {
|
||||||
exports.getModule = class FileBaseListExport extends MenuModule {
|
exports.getModule = class FileBaseListExport extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
this.config = Object.assign(
|
this.config = Object.assign(
|
||||||
{},
|
{},
|
||||||
_.get(options, 'menuConfig.config'),
|
_.get(options, 'menuConfig.config'),
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const DownloadQueue = require('./download_queue.js');
|
const DownloadQueue = require('./download_queue.js');
|
||||||
|
const theme = require('./theme.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const FileAreaWeb = require('./file_area_web.js');
|
const FileAreaWeb = require('./file_area_web.js');
|
||||||
|
@ -38,8 +40,6 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
this.dlQueue = new DownloadQueue(this.client);
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
|
@ -187,7 +187,6 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
||||||
function prepArtAndViewController(callback) {
|
function prepArtAndViewController(callback) {
|
||||||
return self.displayArtAndPrepViewController(
|
return self.displayArtAndPrepViewController(
|
||||||
'queueManager',
|
'queueManager',
|
||||||
FormIds.queueManager,
|
|
||||||
{ clearScreen: clearScreen },
|
{ clearScreen: clearScreen },
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
|
@ -267,4 +266,59 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayArtAndPrepViewController(name, options, cb) {
|
||||||
|
const self = this;
|
||||||
|
const config = this.menuConfig.config;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function readyAndDisplayArt(callback) {
|
||||||
|
if (options.clearScreen) {
|
||||||
|
self.client.term.rawWrite(ansi.resetScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.displayThemedAsset(
|
||||||
|
config.art[name],
|
||||||
|
self.client,
|
||||||
|
{ font: self.menuConfig.font, trailingLF: false },
|
||||||
|
(err, artData) => {
|
||||||
|
return callback(err, artData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function prepeareViewController(artData, callback) {
|
||||||
|
if (_.isUndefined(self.viewControllers[name])) {
|
||||||
|
const vcOpts = {
|
||||||
|
client: self.client,
|
||||||
|
formId: FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_.isUndefined(options.noInput)) {
|
||||||
|
vcOpts.noInput = options.noInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vc = self.addViewController(
|
||||||
|
name,
|
||||||
|
new ViewController(vcOpts)
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadOpts = {
|
||||||
|
callingMenu: self,
|
||||||
|
mciMap: artData.mciMap,
|
||||||
|
formId: FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.viewControllers[name].setFocus(true);
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -428,45 +428,32 @@ module.exports = class FileEntry {
|
||||||
return Object.keys(FILE_WELL_KNOWN_META);
|
return Object.keys(FILE_WELL_KNOWN_META);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getFileIdsBySha(sha, options = {}, cb) {
|
static findBySha(sha, cb) {
|
||||||
// full or partial SHA-256
|
// full or partial SHA-256
|
||||||
const limit = _.isNumber(options.limit) ? `LIMIT ${options.limit}` : '';
|
|
||||||
fileDb.all(
|
fileDb.all(
|
||||||
`SELECT file_id
|
`SELECT file_id
|
||||||
FROM file
|
FROM file
|
||||||
WHERE file_sha256 LIKE "${sha}%" ${limit};`,
|
WHERE file_sha256 LIKE "${sha}%"
|
||||||
|
LIMIT 2;`, // limit 2 such that we can find if there are dupes
|
||||||
(err, fileIdRows) => {
|
(err, fileIdRows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(
|
if (!fileIdRows || 0 === fileIdRows.length) {
|
||||||
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'));
|
return cb(Errors.DoesNotExist('No matches'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileIds.length > 1) {
|
if (fileIdRows.length > 1) {
|
||||||
return cb(Errors.Invalid('SHA is ambiguous'));
|
return cb(Errors.Invalid('SHA is ambiguous'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileEntry = new FileEntry();
|
const fileEntry = new FileEntry();
|
||||||
return fileEntry.load(fileIds[0], err => {
|
return fileEntry.load(fileIdRows[0].file_id, err => {
|
||||||
return cb(err, fileEntry);
|
return cb(err, fileEntry);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to fine a file by an *existing* full path.
|
// Attempt to fine a file by an *existing* full path.
|
||||||
|
@ -664,14 +651,6 @@ 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"
|
// handle e.g. 1998 -> "1998"
|
||||||
if (_.isNumber(filter.tags)) {
|
if (_.isNumber(filter.tags)) {
|
||||||
filter.tags = filter.tags.toString();
|
filter.tags = filter.tags.toString();
|
||||||
|
|
|
@ -150,7 +150,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
|
|
||||||
this.client.log.info(
|
this.client.log.info(
|
||||||
{ sentFiles: sentFiles },
|
{ sentFiles: sentFiles },
|
||||||
`User "${this.client.user.username}" downloaded ${sentFiles.length} file(s)`
|
`Successfully sent ${sentFiles.length} file(s)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -485,10 +485,13 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
externalProc.onExit(exitEvent => {
|
externalProc.once('close', () => {
|
||||||
const {exitCode, signal} = exitEvent;
|
return this.restorePipeAfterExternalProc();
|
||||||
|
});
|
||||||
|
|
||||||
|
externalProc.once('exit', exitCode => {
|
||||||
this.client.log.debug(
|
this.client.log.debug(
|
||||||
{ cmd: cmd, args: args, exitCode, signal },
|
{ cmd: cmd, args: args, exitCode: exitCode },
|
||||||
'Process exited'
|
'Process exited'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
20
core/fse.js
20
core/fse.js
|
@ -167,7 +167,6 @@ exports.FullScreenEditorModule =
|
||||||
var newFocusViewId;
|
var newFocusViewId;
|
||||||
if (errMsgView) {
|
if (errMsgView) {
|
||||||
if (err) {
|
if (err) {
|
||||||
errMsgView.clearText();
|
|
||||||
errMsgView.setText(err.message);
|
errMsgView.setText(err.message);
|
||||||
|
|
||||||
if (MciViewIds.header.subject === err.view.getId()) {
|
if (MciViewIds.header.subject === err.view.getId()) {
|
||||||
|
@ -184,13 +183,6 @@ exports.FullScreenEditorModule =
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
editModeEscPressed: function (formData, extraArgs, cb) {
|
editModeEscPressed: function (formData, extraArgs, cb) {
|
||||||
const errMsgView = self.viewControllers.header.getView(
|
|
||||||
MciViewIds.header.errorMsg
|
|
||||||
);
|
|
||||||
if (errMsgView) {
|
|
||||||
errMsgView.clearText();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.footerMode =
|
self.footerMode =
|
||||||
'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
||||||
|
|
||||||
|
@ -990,7 +982,11 @@ exports.FullScreenEditorModule =
|
||||||
const area = getMessageAreaByTag(self.messageAreaTag);
|
const area = getMessageAreaByTag(self.messageAreaTag);
|
||||||
if (fromView !== undefined) {
|
if (fromView !== undefined) {
|
||||||
if (area && area.realNames) {
|
if (area && area.realNames) {
|
||||||
fromView.setText(self.client.user.realName());
|
fromView.setText(
|
||||||
|
self.client.user.properties[
|
||||||
|
UserProps.RealName
|
||||||
|
] || self.client.user.username
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
fromView.setText(self.client.user.username);
|
fromView.setText(self.client.user.username);
|
||||||
}
|
}
|
||||||
|
@ -1058,7 +1054,7 @@ exports.FullScreenEditorModule =
|
||||||
posView.setText(
|
posView.setText(
|
||||||
_.padStart(String(pos.row + 1), 2, '0') +
|
_.padStart(String(pos.row + 1), 2, '0') +
|
||||||
',' +
|
',' +
|
||||||
_.padStart(String(pos.col + 1), 2, '0')
|
_.padEnd(String(pos.col + 1), 2, '0')
|
||||||
);
|
);
|
||||||
this.client.term.rawWrite(ansi.restorePos());
|
this.client.term.rawWrite(ansi.restorePos());
|
||||||
}
|
}
|
||||||
|
@ -1303,10 +1299,6 @@ exports.FullScreenEditorModule =
|
||||||
callingMenu: self,
|
callingMenu: self,
|
||||||
formId: formId,
|
formId: formId,
|
||||||
mciMap: artData.mciMap,
|
mciMap: artData.mciMap,
|
||||||
viewOffsets: {
|
|
||||||
col: 0,
|
|
||||||
row: self.header.height,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.addViewController(
|
self.addViewController(
|
||||||
|
|
|
@ -135,7 +135,7 @@ module.exports = class Address {
|
||||||
static fromString(addrStr) {
|
static fromString(addrStr) {
|
||||||
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
||||||
|
|
||||||
if (m && m[2] && m[3]) {
|
if (m) {
|
||||||
// start with a 2D
|
// start with a 2D
|
||||||
let addr = {
|
let addr = {
|
||||||
net: parseInt(m[2]),
|
net: parseInt(m[2]),
|
||||||
|
|
141
core/goldmine.js
141
core/goldmine.js
|
@ -1,141 +0,0 @@
|
||||||
/* 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,7 +10,6 @@ const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
|
||||||
|
|
||||||
module.exports = class LoginServerModule extends ServerModule {
|
module.exports = class LoginServerModule extends ServerModule {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -53,7 +52,6 @@ module.exports = class LoginServerModule extends ServerModule {
|
||||||
client.session = {};
|
client.session = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
client.rawSocket = clientSock;
|
|
||||||
client.session.serverName = modInfo.name;
|
client.session.serverName = modInfo.name;
|
||||||
client.session.isSecure = _.isBoolean(client.isSecure)
|
client.session.isSecure = _.isBoolean(client.isSecure)
|
||||||
? client.isSecure
|
? client.isSecure
|
||||||
|
@ -88,12 +86,8 @@ module.exports = class LoginServerModule extends ServerModule {
|
||||||
clientConns.removeClient(client);
|
clientConns.removeClient(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('idle timeout', idleLogoutSeconds => {
|
client.on('idle timeout', () => {
|
||||||
client.log.info(
|
client.log.info('User idle timeout expired');
|
||||||
`Node ${client.node} idle timeout of ${moment
|
|
||||||
.duration(idleLogoutSeconds, 'seconds')
|
|
||||||
.humanize()} expired; Kicking`
|
|
||||||
);
|
|
||||||
|
|
||||||
client.menuStack.goto('idleLogoff', err => {
|
client.menuStack.goto('idleLogoff', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ MCIViewFactory.UserViewCodes = [
|
||||||
'ET',
|
'ET',
|
||||||
'ME',
|
'ME',
|
||||||
'MT',
|
'MT',
|
||||||
|
'PL',
|
||||||
'BT',
|
'BT',
|
||||||
'VM',
|
'VM',
|
||||||
'HM',
|
'HM',
|
||||||
|
@ -128,6 +129,21 @@ MCIViewFactory.prototype.createFromMCI = function (mci) {
|
||||||
view = new MultiLineEditTextView(options);
|
view = new MultiLineEditTextView(options);
|
||||||
break;
|
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
|
// Button
|
||||||
case 'BT':
|
case 'BT':
|
||||||
if (mci.args.length > 0) {
|
if (mci.args.length > 0) {
|
||||||
|
|
|
@ -12,7 +12,6 @@ const MultiLineEditTextView =
|
||||||
require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||||
const Errors = require('../core/enig_error.js').Errors;
|
const Errors = require('../core/enig_error.js').Errors;
|
||||||
const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
|
const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
|
||||||
const EnigAssert = require('./enigma_assert');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -20,23 +19,6 @@ const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const iconvDecode = require('iconv-lite').decode;
|
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 {
|
exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
@ -59,17 +41,6 @@ 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() {
|
static get InterruptTypes() {
|
||||||
return {
|
return {
|
||||||
Never: 'never',
|
Never: 'never',
|
||||||
|
@ -603,13 +574,8 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalSubmitNotify = options.submitNotify;
|
//let artHeight;
|
||||||
|
|
||||||
options.submitNotify = () => {
|
options.submitNotify = () => {
|
||||||
if (_.isFunction(originalSubmitNotify)) {
|
|
||||||
originalSubmitNotify();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevVc) {
|
if (prevVc) {
|
||||||
prevVc.setFocus(true);
|
prevVc.setFocus(true);
|
||||||
}
|
}
|
||||||
|
@ -630,9 +596,6 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
options.viewController.setFocus(true);
|
options.viewController.setFocus(true);
|
||||||
|
|
||||||
this.optionalMoveToPosition(position);
|
this.optionalMoveToPosition(position);
|
||||||
if (!options.position) {
|
|
||||||
options.position = position;
|
|
||||||
}
|
|
||||||
theme.displayThemedPrompt(promptName, this.client, options, (err, artInfo) => {
|
theme.displayThemedPrompt(promptName, this.client, options, (err, artInfo) => {
|
||||||
/*
|
/*
|
||||||
if(artInfo) {
|
if(artInfo) {
|
||||||
|
@ -643,70 +606,6 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
displayArtAndPrepViewController(name, formId, options, cb) {
|
|
||||||
const config = this.menuConfig.config;
|
|
||||||
EnigAssert(_.isObject(config));
|
|
||||||
|
|
||||||
async.waterfall(
|
|
||||||
[
|
|
||||||
callback => {
|
|
||||||
if (options.clearScreen) {
|
|
||||||
this.client.term.rawWrite(ansi.resetScreen());
|
|
||||||
}
|
|
||||||
|
|
||||||
theme.displayThemedAsset(
|
|
||||||
config.art[name],
|
|
||||||
this.client,
|
|
||||||
{ font: this.menuConfig.font, trailingLF: false },
|
|
||||||
(err, artData) => {
|
|
||||||
return callback(err, artData);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(artData, callback) => {
|
|
||||||
if (_.isUndefined(this.viewControllers[name])) {
|
|
||||||
const vcOpts = {
|
|
||||||
client: this.client,
|
|
||||||
formId: formId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!_.isUndefined(options.noInput)) {
|
|
||||||
vcOpts.noInput = options.noInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vc = this.addViewController(
|
|
||||||
name,
|
|
||||||
new ViewController(vcOpts)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_.isFunction(options.artDataPrep)) {
|
|
||||||
try {
|
|
||||||
options.artDataPrep(name, artData, vc);
|
|
||||||
} catch (e) {
|
|
||||||
return callback(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadOpts = {
|
|
||||||
callingMenu: this,
|
|
||||||
mciMap: artData.mciMap,
|
|
||||||
formId: formId,
|
|
||||||
viewOffsets: options.viewOffsets,
|
|
||||||
};
|
|
||||||
|
|
||||||
return vc.loadFromMenuConfig(loadOpts, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.viewControllers[name].setFocus(true);
|
|
||||||
return callback(null);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
err => {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewText(formName, mciId, text, appendMultiLine) {
|
setViewText(formName, mciId, text, appendMultiLine) {
|
||||||
const view = this.getView(formName, mciId);
|
const view = this.getView(formName, mciId);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
|
@ -714,7 +613,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appendMultiLine && view instanceof MultiLineEditTextView) {
|
if (appendMultiLine && view instanceof MultiLineEditTextView) {
|
||||||
view.setAnsi(text);
|
view.addText(text);
|
||||||
} else {
|
} else {
|
||||||
view.setText(text);
|
view.setText(text);
|
||||||
}
|
}
|
||||||
|
@ -725,19 +624,17 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
return form && form.getView(id);
|
return form && form.getView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCustomViewsWithFilter(formName, startId, options) {
|
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
const views = [];
|
let textView;
|
||||||
|
|
||||||
let view;
|
|
||||||
let customMciId = startId;
|
let customMciId = startId;
|
||||||
const config = this.menuConfig.config;
|
const config = this.menuConfig.config;
|
||||||
const endId = options.endId || 99; // we'll fail to get a view before 99
|
const endId = options.endId || 99; // we'll fail to get a view before 99
|
||||||
|
|
||||||
while (
|
while (
|
||||||
customMciId <= endId &&
|
customMciId <= endId &&
|
||||||
(view = this.viewControllers[formName].getView(customMciId))
|
(textView = this.viewControllers[formName].getView(customMciId))
|
||||||
) {
|
) {
|
||||||
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
|
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
|
||||||
const format = config[key];
|
const format = config[key];
|
||||||
|
@ -746,35 +643,20 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
format &&
|
format &&
|
||||||
(!options.filter || options.filter.find(f => format.indexOf(f) > -1))
|
(!options.filter || options.filter.find(f => format.indexOf(f) > -1))
|
||||||
) {
|
) {
|
||||||
view.key = key; // cache
|
const text = stringFormat(format, fmtObj);
|
||||||
views.push(view);
|
|
||||||
|
if (
|
||||||
|
options.appendMultiLine &&
|
||||||
|
textView instanceof MultiLineEditTextView
|
||||||
|
) {
|
||||||
|
textView.addText(text);
|
||||||
|
} else {
|
||||||
|
textView.setText(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
++customMciId;
|
++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) {
|
refreshPredefinedMciViewsByCode(formName, mciCodes) {
|
||||||
|
@ -870,26 +752,4 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Various common helpers
|
|
||||||
getDateFormat(defaultStyle = 'short') {
|
|
||||||
return (
|
|
||||||
this.config.dateFormat ||
|
|
||||||
this.client.currentTheme.helpers.getDateFormat(defaultStyle)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeFormat(defaultStyle = 'short') {
|
|
||||||
return (
|
|
||||||
this.config.timeFormat ||
|
|
||||||
this.client.currentTheme.helpers.getTimeFormat(defaultStyle)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDateTimeFormat(defaultStyle = 'short') {
|
|
||||||
return (
|
|
||||||
this.config.dateTimeFormat ||
|
|
||||||
this.client.currentTheme.helpers.getDateTimeFormat(defaultStyle)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
const loadMenu = require('./menu_util.js').loadMenu;
|
const loadMenu = require('./menu_util.js').loadMenu;
|
||||||
const { Errors, ErrorReasons } = require('./enig_error.js');
|
const { Errors, ErrorReasons } = require('./enig_error.js');
|
||||||
const { getResolvedSpec } = require('./menu_util.js');
|
const { getResolvedSpec } = require('./menu_util.js');
|
||||||
const { MenuFlags } = require('./menu_module.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const bunyan = require('bunyan');
|
|
||||||
|
// :TODO: Stack is backwards.... top should be most recent! :)
|
||||||
|
|
||||||
module.exports = class MenuStack {
|
module.exports = class MenuStack {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
|
@ -27,12 +27,20 @@ module.exports = class MenuStack {
|
||||||
}
|
}
|
||||||
|
|
||||||
peekPrev() {
|
peekPrev() {
|
||||||
|
if (this.stackSize > 1) {
|
||||||
return this.stack[this.stack.length - 2];
|
return this.stack[this.stack.length - 2];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
top() {
|
top() {
|
||||||
|
if (this.stackSize > 0) {
|
||||||
return this.stack[this.stack.length - 1];
|
return this.stack[this.stack.length - 1];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get stackSize() {
|
||||||
|
return this.stack.length;
|
||||||
|
}
|
||||||
|
|
||||||
get currentModule() {
|
get currentModule() {
|
||||||
const top = this.top();
|
const top = this.top();
|
||||||
|
@ -73,6 +81,7 @@ module.exports = class MenuStack {
|
||||||
prev(cb) {
|
prev(cb) {
|
||||||
const menuResult = this.top().instance.getMenuResult();
|
const menuResult = this.top().instance.getMenuResult();
|
||||||
|
|
||||||
|
// :TODO: leave() should really take a cb...
|
||||||
this.pop().instance.leave(); // leave & remove current
|
this.pop().instance.leave(); // leave & remove current
|
||||||
|
|
||||||
const previousModuleInfo = this.pop(); // get previous
|
const previousModuleInfo = this.pop(); // get previous
|
||||||
|
@ -120,7 +129,7 @@ module.exports = class MenuStack {
|
||||||
client: self.client,
|
client: self.client,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentModuleInfo && currentModuleInfo.menuFlags.includes(MenuFlags.ForwardArgs)) {
|
if (currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
|
||||||
loadOpts.extraArgs = currentModuleInfo.extraArgs;
|
loadOpts.extraArgs = currentModuleInfo.extraArgs;
|
||||||
} else {
|
} else {
|
||||||
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
|
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
|
||||||
|
@ -129,6 +138,7 @@ module.exports = class MenuStack {
|
||||||
|
|
||||||
loadMenu(loadOpts, (err, modInst) => {
|
loadMenu(loadOpts, (err, modInst) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
// :TODO: probably should just require a cb...
|
||||||
const errCb = cb || self.client.defaultHandlerMissingMod();
|
const errCb = cb || self.client.defaultHandlerMissingMod();
|
||||||
errCb(err);
|
errCb(err);
|
||||||
} else {
|
} else {
|
||||||
|
@ -141,6 +151,22 @@ module.exports = class MenuStack {
|
||||||
return;
|
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
|
// If menuFlags were supplied in menu.hjson, they should win over
|
||||||
// anything supplied in code.
|
// anything supplied in code.
|
||||||
|
@ -154,9 +180,9 @@ module.exports = class MenuStack {
|
||||||
// in code we can ask to merge in
|
// in code we can ask to merge in
|
||||||
if (
|
if (
|
||||||
Array.isArray(options.menuFlags) &&
|
Array.isArray(options.menuFlags) &&
|
||||||
options.menuFlags.includes(MenuFlags.MergeFlags)
|
options.menuFlags.includes('mergeFlags')
|
||||||
) {
|
) {
|
||||||
menuFlags = [...new Set(options.menuFlags)]; // make unique
|
menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,8 +193,12 @@ module.exports = class MenuStack {
|
||||||
|
|
||||||
currentModuleInfo.instance.leave();
|
currentModuleInfo.instance.leave();
|
||||||
|
|
||||||
if (currentModuleInfo.menuFlags.includes(MenuFlags.NoHistory)) {
|
if (currentModuleInfo.menuFlags.includes('noHistory')) {
|
||||||
this.pop().instance.leave(); // leave & remove current from stack
|
this.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuFlags.includes('popParent')) {
|
||||||
|
this.pop().instance.leave(); // leave & remove current
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +214,6 @@ module.exports = class MenuStack {
|
||||||
modInst.restoreSavedState(options.savedState);
|
modInst.restoreSavedState(options.savedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.client.log.level() <= bunyan.TRACE) {
|
|
||||||
const stackEntries = self.stack.map(stackEntry => {
|
const stackEntries = self.stack.map(stackEntry => {
|
||||||
let name = stackEntry.name;
|
let name = stackEntry.name;
|
||||||
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
|
if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
|
||||||
|
@ -196,7 +225,6 @@ module.exports = class MenuStack {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
|
self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
|
||||||
}
|
|
||||||
|
|
||||||
modInst.enter();
|
modInst.enter();
|
||||||
|
|
||||||
|
|
|
@ -198,10 +198,6 @@ MenuView.prototype.getItems = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.getItem = function (index) {
|
MenuView.prototype.getItem = function (index) {
|
||||||
if (index > this.items.length - 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.complexItems) {
|
if (this.complexItems) {
|
||||||
return this.items[index];
|
return this.items[index];
|
||||||
}
|
}
|
||||||
|
@ -237,10 +233,6 @@ MenuView.prototype.setFocusItemIndex = function (index) {
|
||||||
this.focusedItemIndex = index;
|
this.focusedItemIndex = index;
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuView.prototype.getFocusItemIndex = function () {
|
|
||||||
return this.focusedItemIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
MenuView.prototype.onKeyPress = function (ch, key) {
|
MenuView.prototype.onKeyPress = function (ch, key) {
|
||||||
const itemIndex = this.getHotKeyItemIndex(ch);
|
const itemIndex = this.getHotKeyItemIndex(ch);
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
|
|
|
@ -55,7 +55,6 @@ const ADDRESS_FLAVOR = {
|
||||||
FTN: 'ftn', // FTN style
|
FTN: 'ftn', // FTN style
|
||||||
Email: 'email', // From email
|
Email: 'email', // From email
|
||||||
QWK: 'qwk', // QWK packet
|
QWK: 'qwk', // QWK packet
|
||||||
NNTP: 'nntp', // NNTP article POST; often a email address
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATE_FLAGS0 = {
|
const STATE_FLAGS0 = {
|
||||||
|
@ -763,11 +762,6 @@ module.exports = class Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
persist(cb) {
|
persist(cb) {
|
||||||
const containsNonWhitespaceCharacterRegEx = /\S/;
|
|
||||||
if (!containsNonWhitespaceCharacterRegEx.test(this.message)) {
|
|
||||||
return cb(Errors.Invalid('Empty message'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isValid()) {
|
if (!this.isValid()) {
|
||||||
return cb(Errors.Invalid('Cannot persist invalid message!'));
|
return cb(Errors.Invalid('Cannot persist invalid message!'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,6 @@ exports.filterMessageListByReadACS = filterMessageListByReadACS;
|
||||||
exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea;
|
exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea;
|
||||||
exports.getMessageListForArea = getMessageListForArea;
|
exports.getMessageListForArea = getMessageListForArea;
|
||||||
exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser;
|
exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser;
|
||||||
exports.getNewMessageCountAddressedToUser = getNewMessageCountAddressedToUser;
|
|
||||||
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
|
||||||
exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea;
|
exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea;
|
||||||
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
|
||||||
|
@ -363,7 +362,7 @@ function changeMessageConference(client, confTag, cb) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
client.log.info(
|
client.log.info(
|
||||||
{ confTag: confTag, confName: conf.name, areaTag: areaInfo.areaTag },
|
{ confTag: confTag, confName: conf.name, areaTag: areaInfo.areaTag },
|
||||||
`${client.node} changed message conference to ${areaInfo.areaTag}`
|
'Current message conference changed'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
client.log.warn(
|
client.log.warn(
|
||||||
|
@ -412,9 +411,9 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
|
||||||
],
|
],
|
||||||
function complete(err, area) {
|
function complete(err, area) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
client.log.debug(
|
client.log.info(
|
||||||
{ areaTag: areaTag, area: area },
|
{ areaTag: areaTag, area: area },
|
||||||
`Node ${client.node} changed message area to ${areaTag}`
|
'Current message area changed'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
client.log.warn(
|
client.log.warn(
|
||||||
|
@ -532,36 +531,6 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// New message count -- for all areas available to the user
|
|
||||||
// that are addressed to that user (ie: matching username)
|
|
||||||
// Does NOT Include private messages.
|
|
||||||
function getNewMessageCountAddressedToUser(client, cb) {
|
|
||||||
const areaTags = getAllAvailableMessageAreaTags(client).filter(
|
|
||||||
areaTag => areaTag !== Message.WellKnownAreaTags.Private
|
|
||||||
);
|
|
||||||
|
|
||||||
let newMessageCount = 0;
|
|
||||||
async.forEach(
|
|
||||||
areaTags,
|
|
||||||
(areaTag, nextAreaTag) => {
|
|
||||||
getMessageAreaLastReadId(client.user.userId, areaTag, (_, lastMessageId) => {
|
|
||||||
lastMessageId = lastMessageId || 0;
|
|
||||||
getNewMessageCountInAreaForUser(
|
|
||||||
client.user.userId,
|
|
||||||
areaTag,
|
|
||||||
(err, count) => {
|
|
||||||
newMessageCount += count;
|
|
||||||
return nextAreaTag(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return cb(null, newMessageCount);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
function getNewMessagesInAreaForUser(userId, areaTag, cb) {
|
||||||
getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => {
|
getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => {
|
||||||
lastMessageId = lastMessageId || 0;
|
lastMessageId = lastMessageId || 0;
|
||||||
|
|
|
@ -332,10 +332,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
||||||
publicExportAreas,
|
publicExportAreas,
|
||||||
(exportArea, nextExportArea) => {
|
(exportArea, nextExportArea) => {
|
||||||
const area = getMessageAreaByTag(exportArea.areaTag);
|
const area = getMessageAreaByTag(exportArea.areaTag);
|
||||||
let conf;
|
const conf = getMessageConferenceByTag(area.confTag);
|
||||||
if (area) {
|
|
||||||
conf = getMessageConferenceByTag(area.confTag);
|
|
||||||
}
|
|
||||||
if (!area || !conf) {
|
if (!area || !conf) {
|
||||||
// :TODO: remove from user properties - this area does not exist
|
// :TODO: remove from user properties - this area does not exist
|
||||||
this.client.log.warn(
|
this.client.log.warn(
|
||||||
|
|
|
@ -113,6 +113,7 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
|
||||||
const returnNoResults = () => {
|
const returnNoResults = () => {
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
|
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
|
||||||
|
{ menuFlags: ['popParent'] },
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -159,6 +160,7 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
|
||||||
messageList,
|
messageList,
|
||||||
noUpdateLastReadId: true,
|
noUpdateLastReadId: true,
|
||||||
},
|
},
|
||||||
|
menuFlags: ['popParent'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|
|
@ -10,14 +10,7 @@ function dailyMaintenanceScheduledEvent(args, cb) {
|
||||||
//
|
//
|
||||||
// Various stats need reset daily
|
// Various stats need reset daily
|
||||||
//
|
//
|
||||||
// :TODO: files/etc. here
|
[SysProps.LoginsToday, SysProps.MessagesToday].forEach(prop => {
|
||||||
const resetProps = [
|
|
||||||
SysProps.LoginsToday,
|
|
||||||
SysProps.MessagesToday,
|
|
||||||
SysProps.NewUsersTodayCount,
|
|
||||||
];
|
|
||||||
|
|
||||||
resetProps.forEach(prop => {
|
|
||||||
StatLog.setNonPersistentSystemStat(prop, 0);
|
StatLog.setNonPersistentSystemStat(prop, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
54
core/mrc.js
54
core/mrc.js
|
@ -55,8 +55,6 @@ const helpText = `
|
||||||
|03/|11topic |03<message> |08- |07Set the room topic
|
|03/|11topic |03<message> |08- |07Set the room topic
|
||||||
|03/|11bbses |08& |03/|11info <id> |08- |07Info about BBS's connected
|
|03/|11bbses |08& |03/|11info <id> |08- |07Info about BBS's connected
|
||||||
|03/|11meetups |08- |07Info about MRC MeetUps
|
|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/|11l33t |03<your message> |08- |07l337 5p34k
|
||||||
|03/|11kewl |03<your message> |08- |07BBS KeWL SPeaK
|
|03/|11kewl |03<your message> |08- |07BBS KeWL SPeaK
|
||||||
|
@ -377,18 +375,6 @@ exports.getModule = class mrcModule extends MenuModule {
|
||||||
'|08' + currentTime + '|00 ' + message.body + '|00'
|
'|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);
|
this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea);
|
||||||
|
@ -554,46 +540,6 @@ exports.getModule = class mrcModule extends MenuModule {
|
||||||
this.sendServerMessage('LIST');
|
this.sendServerMessage('LIST');
|
||||||
break;
|
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':
|
case 'quit':
|
||||||
return this.prevMenu();
|
return this.prevMenu();
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const messageArea = require('./message_area.js');
|
const messageArea = require('./message_area.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
@ -29,9 +29,6 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
// always include noHistory flag
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
this.initList();
|
this.initList();
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
|
@ -52,7 +49,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
|
||||||
extraArgs: {
|
extraArgs: {
|
||||||
areaTag: area.areaTag,
|
areaTag: area.areaTag,
|
||||||
},
|
},
|
||||||
menuFlags: [ MenuFlags.NoHistory ],
|
menuFlags: ['popParent', 'noHistory'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|
|
@ -14,39 +14,6 @@ exports.moduleInfo = {
|
||||||
author: 'NuSkooler',
|
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 {
|
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
@ -75,15 +42,8 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
const errMsgView = self.viewControllers.header.getView(
|
// :TODO:... sooooo now what?
|
||||||
MciViewIds.header.errorMsg
|
} else {
|
||||||
);
|
|
||||||
if (errMsgView) {
|
|
||||||
errMsgView.setText(err.message);
|
|
||||||
}
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: not logging 'from' here as it's part of client.log.xxxx()
|
// note: not logging 'from' here as it's part of client.log.xxxx()
|
||||||
self.client.log.info(
|
self.client.log.info(
|
||||||
{
|
{
|
||||||
|
@ -91,8 +51,9 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||||
subject: msg.subject,
|
subject: msg.subject,
|
||||||
uuid: msg.messageUuid,
|
uuid: msg.messageUuid,
|
||||||
},
|
},
|
||||||
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
|
'Message persisted'
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return self.nextMenu(cb);
|
return self.nextMenu(cb);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const messageArea = require('./message_area.js');
|
const messageArea = require('./message_area.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
|
@ -26,9 +26,6 @@ exports.getModule = class MessageConfListModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
// always include noHistory flag
|
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
|
|
||||||
this.initList();
|
this.initList();
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
|
@ -52,7 +49,7 @@ exports.getModule = class MessageConfListModule extends MenuModule {
|
||||||
extraArgs: {
|
extraArgs: {
|
||||||
confTag: conf.confTag,
|
confTag: conf.confTag,
|
||||||
},
|
},
|
||||||
menuFlags: [ MenuFlags.NoHistory ],
|
menuFlags: ['popParent', 'noHistory'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|
|
@ -70,14 +70,6 @@ exports.getModule = class MessageListModule extends (
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
selectMessage: (formData, extraArgs, cb) => {
|
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) {
|
if (MciViewIds.allViews.msgList === formData.submitId) {
|
||||||
// 'messageIndex' or older deprecated 'message' member
|
// 'messageIndex' or older deprecated 'message' member
|
||||||
this.initialFocusIndex = _.get(
|
this.initialFocusIndex = _.get(
|
||||||
|
@ -323,16 +315,9 @@ exports.getModule = class MessageListModule extends (
|
||||||
let msgNum = 1;
|
let msgNum = 1;
|
||||||
self.config.messageList.forEach((listItem, index) => {
|
self.config.messageList.forEach((listItem, index) => {
|
||||||
listItem.msgNum = msgNum++;
|
listItem.msgNum = msgNum++;
|
||||||
try {
|
|
||||||
listItem.ts = moment(listItem.modTimestamp).format(
|
listItem.ts = moment(listItem.modTimestamp).format(
|
||||||
dateTimeFormat
|
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)
|
const isNew = _.isBoolean(listItem.isNew)
|
||||||
? listItem.isNew
|
? listItem.isNew
|
||||||
: listItem.messageId > self.lastReadId;
|
: listItem.messageId > self.lastReadId;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule, MenuFlags } = require('./menu_module');
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const Message = require('./message.js');
|
const Message = require('./message.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const { filterMessageListByReadACS } = require('./message_area.js');
|
const { filterMessageListByReadACS } = require('./message_area.js');
|
||||||
|
@ -16,12 +16,14 @@ exports.moduleInfo = {
|
||||||
exports.getModule = class MyMessagesModule extends MenuModule {
|
exports.getModule = class MyMessagesModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.setMergedFlag(MenuFlags.NoHistory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
const filter = {
|
const filter = {
|
||||||
toUserName: [this.client.user.username, this.client.user.realName()],
|
toUserName: [
|
||||||
|
this.client.user.username,
|
||||||
|
this.client.user.getProperty(UserProps.RealName),
|
||||||
|
],
|
||||||
sort: 'modTimestamp',
|
sort: 'modTimestamp',
|
||||||
resultType: 'messageList',
|
resultType: 'messageList',
|
||||||
limit: 1024 * 16, // we want some sort of limit...
|
limit: 1024 * 16, // we want some sort of limit...
|
||||||
|
@ -47,6 +49,7 @@ exports.getModule = class MyMessagesModule extends MenuModule {
|
||||||
if (!this.messageList || 0 === this.messageList.length) {
|
if (!this.messageList || 0 === this.messageList.length) {
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
|
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
|
||||||
|
{ menuFlags: ['popParent'] }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +58,7 @@ exports.getModule = class MyMessagesModule extends MenuModule {
|
||||||
messageList: this.messageList,
|
messageList: this.messageList,
|
||||||
noUpdateLastReadId: true,
|
noUpdateLastReadId: true,
|
||||||
},
|
},
|
||||||
|
menuFlags: ['popParent'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.gotoMenu(
|
return this.gotoMenu(
|
||||||
|
|
|
@ -6,7 +6,6 @@ const { MenuModule } = require('./menu_module.js');
|
||||||
const {
|
const {
|
||||||
getActiveConnectionList,
|
getActiveConnectionList,
|
||||||
getConnectionByNodeId,
|
getConnectionByNodeId,
|
||||||
UserMessageableConnections,
|
|
||||||
} = require('./client_connections.js');
|
} = require('./client_connections.js');
|
||||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
const { getThemeArt } = require('./theme.js');
|
const { getThemeArt } = require('./theme.js');
|
||||||
|
@ -237,7 +236,7 @@ exports.getModule = class NodeMessageModule extends MenuModule {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
.concat(
|
.concat(
|
||||||
getActiveConnectionList(UserMessageableConnections).map(node =>
|
getActiveConnectionList(true).map(node =>
|
||||||
Object.assign(node, {
|
Object.assign(node, {
|
||||||
text: -1 == node.node ? '-ALL-' : node.node.toString(),
|
text: -1 == node.node ? '-ALL-' : node.node.toString(),
|
||||||
})
|
})
|
||||||
|
|
19
core/nua.js
19
core/nua.js
|
@ -13,7 +13,6 @@ const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name: 'NUA',
|
name: 'NUA',
|
||||||
|
@ -96,15 +95,15 @@ exports.getModule = class NewUserAppModule extends MenuModule {
|
||||||
areaTag = areaTag || '';
|
areaTag = areaTag || '';
|
||||||
|
|
||||||
newUser.properties = {
|
newUser.properties = {
|
||||||
[UserProps.RealName]: formData.value.realName || '',
|
[UserProps.RealName]: formData.value.realName,
|
||||||
[UserProps.Birthdate]: getISOTimestampString(
|
[UserProps.Birthdate]: getISOTimestampString(
|
||||||
formData.value.birthdate || moment()
|
formData.value.birthdate
|
||||||
),
|
),
|
||||||
[UserProps.Sex]: formData.value.sex || '',
|
[UserProps.Sex]: formData.value.sex,
|
||||||
[UserProps.Location]: formData.value.location || '',
|
[UserProps.Location]: formData.value.location,
|
||||||
[UserProps.Affiliations]: formData.value.affils || '',
|
[UserProps.Affiliations]: formData.value.affils,
|
||||||
[UserProps.EmailAddress]: formData.value.email || '',
|
[UserProps.EmailAddress]: formData.value.email,
|
||||||
[UserProps.WebAddress]: formData.value.web || '',
|
[UserProps.WebAddress]: formData.value.web,
|
||||||
[UserProps.AccountCreated]: getISOTimestampString(),
|
[UserProps.AccountCreated]: getISOTimestampString(),
|
||||||
|
|
||||||
[UserProps.MessageConfTag]: confTag,
|
[UserProps.MessageConfTag]: confTag,
|
||||||
|
@ -131,7 +130,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
|
||||||
};
|
};
|
||||||
newUser.create(createUserInfo, err => {
|
newUser.create(createUserInfo, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
self.client.log.warn(
|
self.client.log.info(
|
||||||
{ error: err, username: formData.value.username },
|
{ error: err, username: formData.value.username },
|
||||||
'New user creation failed'
|
'New user creation failed'
|
||||||
);
|
);
|
||||||
|
@ -145,7 +144,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
|
||||||
} else {
|
} else {
|
||||||
self.client.log.info(
|
self.client.log.info(
|
||||||
{ username: formData.value.username, userId: newUser.userId },
|
{ username: formData.value.username, userId: newUser.userId },
|
||||||
`New user "${formData.value.username}" created`
|
'New user created'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cache SysOp information now
|
// Cache SysOp information now
|
||||||
|
|
|
@ -69,18 +69,6 @@ Actions:
|
||||||
info arguments:
|
info arguments:
|
||||||
--security Include security information in output
|
--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:
|
2fa-otp arguments:
|
||||||
--qr-type TYPE Specify QR code type
|
--qr-type TYPE Specify QR code type
|
||||||
|
|
||||||
|
@ -182,14 +170,6 @@ General Information:
|
||||||
MessageBase: `usage: oputil.js mb <action> [<arguments>]
|
MessageBase: `usage: oputil.js mb <action> [<arguments>]
|
||||||
|
|
||||||
Actions:
|
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
|
areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail
|
||||||
|
|
||||||
NetMail is sent to supplied address with the supplied command(s). Multi-part commands
|
NetMail is sent to supplied address with the supplied command(s). Multi-part commands
|
||||||
|
@ -202,9 +182,6 @@ Actions:
|
||||||
packet in the directory specified by PATH. The QWK
|
packet in the directory specified by PATH. The QWK
|
||||||
BBS ID will be obtained by the final component of PATH.
|
BBS ID will be obtained by the final component of PATH.
|
||||||
|
|
||||||
list-confs arguments:
|
|
||||||
--areas Include areas within each message conference.
|
|
||||||
|
|
||||||
import-areas arguments:
|
import-areas arguments:
|
||||||
--conf CONF_TAG Conference tag in which to import areas
|
--conf CONF_TAG Conference tag in which to import areas
|
||||||
--network NETWORK Network name/key to associate FTN areas
|
--network NETWORK Network name/key to associate FTN areas
|
||||||
|
|
|
@ -692,162 +692,6 @@ 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 handleMessageBaseCommand() {
|
||||||
function errUsage() {
|
function errUsage() {
|
||||||
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR);
|
||||||
|
@ -865,8 +709,6 @@ function handleMessageBaseCommand() {
|
||||||
'import-areas': importAreas,
|
'import-areas': importAreas,
|
||||||
'qwk-dump': dumpQWKPacket,
|
'qwk-dump': dumpQWKPacket,
|
||||||
'qwk-export': exportQWKPacket,
|
'qwk-export': exportQWKPacket,
|
||||||
'list-confs': listConferences,
|
|
||||||
post: postMessage,
|
|
||||||
}[action] || errUsage
|
}[action] || errUsage
|
||||||
)();
|
)();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const Table = require('easy-table');
|
|
||||||
|
|
||||||
exports.handleUserCommand = handleUserCommand;
|
exports.handleUserCommand = handleUserCommand;
|
||||||
|
|
||||||
|
@ -341,7 +340,7 @@ function showUserInfo(user) {
|
||||||
|
|
||||||
const statusDesc = () => {
|
const statusDesc = () => {
|
||||||
const status = user.properties[UserProps.AccountStatus];
|
const status = user.properties[UserProps.AccountStatus];
|
||||||
return _.invert(User.AccountStatus)[status] || 'N/A';
|
return _.invert(User.AccountStatus)[status] || 'unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
const created = () => {
|
const created = () => {
|
||||||
|
@ -509,6 +508,7 @@ function listUsers() {
|
||||||
// :TODO: --created-since SPEC and --last-called SPEC
|
// :TODO: --created-since SPEC and --last-called SPEC
|
||||||
// --created-since SPEC
|
// --created-since SPEC
|
||||||
// SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days"
|
// SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days"
|
||||||
|
// :TODO: --sort name|id
|
||||||
let listWhat;
|
let listWhat;
|
||||||
if (argv._.length > 2) {
|
if (argv._.length > 2) {
|
||||||
listWhat = argv._[argv._.length - 1];
|
listWhat = argv._[argv._.length - 1];
|
||||||
|
@ -516,8 +516,6 @@ function listUsers() {
|
||||||
listWhat = 'all';
|
listWhat = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortBy = (argv.sort || 'id').toLowerCase();
|
|
||||||
|
|
||||||
const User = require('../../core/user');
|
const User = require('../../core/user');
|
||||||
if (!['all'].concat(Object.keys(User.AccountStatus)).includes(listWhat)) {
|
if (!['all'].concat(Object.keys(User.AccountStatus)).includes(listWhat)) {
|
||||||
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
||||||
|
@ -529,13 +527,7 @@ function listUsers() {
|
||||||
const UserProps = require('../../core/user_property');
|
const UserProps = require('../../core/user_property');
|
||||||
|
|
||||||
const userListOpts = {
|
const userListOpts = {
|
||||||
properties: [
|
properties: [UserProps.AccountStatus],
|
||||||
UserProps.RealName,
|
|
||||||
UserProps.AccountStatus,
|
|
||||||
UserProps.AccountCreated,
|
|
||||||
UserProps.LastLoginTs,
|
|
||||||
UserProps.LoginCount,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
User.getUserList(userListOpts, (err, userList) => {
|
User.getUserList(userListOpts, (err, userList) => {
|
||||||
|
@ -558,94 +550,10 @@ function listUsers() {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(userList, callback) => {
|
(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 => {
|
userList.forEach(user => {
|
||||||
table.cell('ID', user.userId);
|
console.info(`${user.userId}: ${user.userName}`);
|
||||||
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);
|
return callback(null);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,31 +19,14 @@ const packageJson = require('../package.json');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const async = require('async');
|
|
||||||
|
|
||||||
exports.getPredefinedMCIValue = getPredefinedMCIValue;
|
exports.getPredefinedMCIValue = getPredefinedMCIValue;
|
||||||
exports.getPredefinedMCIFormatObject = getPredefinedMCIFormatObject;
|
|
||||||
exports.init = init;
|
exports.init = init;
|
||||||
|
|
||||||
function init(cb) {
|
function init(cb) {
|
||||||
async.series(
|
setNextRandomRumor(cb);
|
||||||
[
|
|
||||||
callback => {
|
|
||||||
return setNextRandomRumor(callback);
|
|
||||||
},
|
|
||||||
callback => {
|
|
||||||
// by fetching a memory or load we'll force a refresh now
|
|
||||||
StatLog.getSystemStat(SysProps.SystemMemoryStats);
|
|
||||||
return callback(null);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
err => {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: move this to stat_log.js like system memory is handled
|
|
||||||
function setNextRandomRumor(cb) {
|
function setNextRandomRumor(cb) {
|
||||||
StatLog.getSystemLogEntries(
|
StatLog.getSystemLogEntries(
|
||||||
SysLogKeys.UserAddedRumorz,
|
SysLogKeys.UserAddedRumorz,
|
||||||
|
@ -82,6 +65,10 @@ function userStatAsCountString(client, statName, defaultValue) {
|
||||||
return toNumberWithCommas(value);
|
return toNumberWithCommas(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sysStatAsString(statName, defaultValue) {
|
||||||
|
return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
const PREDEFINED_MCI_GENERATORS = {
|
const PREDEFINED_MCI_GENERATORS = {
|
||||||
//
|
//
|
||||||
// Board
|
// Board
|
||||||
|
@ -117,6 +104,7 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
SE: function opEmail() {
|
SE: function opEmail() {
|
||||||
return StatLog.getSystemStat(SysProps.SysOpEmailAddress);
|
return StatLog.getSystemStat(SysProps.SysOpEmailAddress);
|
||||||
},
|
},
|
||||||
|
// :TODO: op age, web, ?????
|
||||||
|
|
||||||
//
|
//
|
||||||
// Current user / session
|
// Current user / session
|
||||||
|
@ -174,8 +162,8 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
return client.node.toString();
|
return client.node.toString();
|
||||||
},
|
},
|
||||||
IP: function clientIpAddress(client) {
|
IP: function clientIpAddress(client) {
|
||||||
return client.friendlyRemoteAddress();
|
return client.remoteAddress.replace(/^::ffff:/, '');
|
||||||
},
|
}, // convert any :ffff: IPv4's to 32bit version
|
||||||
ST: function serverName(client) {
|
ST: function serverName(client) {
|
||||||
return client.session.serverName;
|
return client.session.serverName;
|
||||||
},
|
},
|
||||||
|
@ -284,23 +272,6 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0;
|
const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0;
|
||||||
return moment.duration(minutes, 'minutes').humanize();
|
return moment.duration(minutes, 'minutes').humanize();
|
||||||
},
|
},
|
||||||
NM: function userNewMessagesAddressedToCount(client) {
|
|
||||||
return StatLog.getUserStatNumByClient(
|
|
||||||
client,
|
|
||||||
UserProps.NewAddressedToMessageCount
|
|
||||||
);
|
|
||||||
},
|
|
||||||
NP: function userNewPrivateMailCount(client) {
|
|
||||||
return StatLog.getUserStatNumByClient(client, UserProps.NewPrivateMailCount);
|
|
||||||
},
|
|
||||||
IA: function userStatusAvailableIndicator(client) {
|
|
||||||
const indicators = client.currentTheme.helpers.getStatusAvailIndicators();
|
|
||||||
return client.user.isAvailable() ? indicators[0] || 'Y' : indicators[1] || 'N';
|
|
||||||
},
|
|
||||||
IV: function userStatusVisibleIndicator(client) {
|
|
||||||
const indicators = client.currentTheme.helpers.getStatusVisibleIndicators();
|
|
||||||
return client.user.isVisible() ? indicators[0] || 'Y' : indicators[1] || 'N';
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Date/Time
|
// Date/Time
|
||||||
|
@ -347,36 +318,15 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
.trim();
|
.trim();
|
||||||
},
|
},
|
||||||
|
|
||||||
MB: function totalMemoryBytes() {
|
// :TODO: MCI for core count, e.g. os.cpus().length
|
||||||
const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {
|
|
||||||
totalBytes: 0,
|
// :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage
|
||||||
};
|
|
||||||
return formatByteSize(stats.totalBytes, true); // true=withAbbr
|
|
||||||
},
|
|
||||||
MF: function totalMemoryFreeBytes() {
|
|
||||||
const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {
|
|
||||||
freeBytes: 0,
|
|
||||||
};
|
|
||||||
return formatByteSize(stats.freeBytes, true); // true=withAbbr
|
|
||||||
},
|
|
||||||
LA: function systemLoadAverage() {
|
|
||||||
const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { average: 0.0 };
|
|
||||||
return stats.average.toLocaleString();
|
|
||||||
},
|
|
||||||
CL: function systemCurrentLoad() {
|
|
||||||
const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { current: 0 };
|
|
||||||
return `${stats.current}%`;
|
|
||||||
},
|
|
||||||
UU: function systemUptime() {
|
|
||||||
return moment.duration(process.uptime(), 'seconds').humanize();
|
|
||||||
},
|
|
||||||
NV: function nodeVersion() {
|
NV: function nodeVersion() {
|
||||||
return process.version;
|
return process.version;
|
||||||
},
|
},
|
||||||
|
|
||||||
AN: function activeNodes() {
|
AN: function activeNodes() {
|
||||||
return clientConnections
|
return clientConnections.getActiveConnections().length.toString();
|
||||||
.getActiveConnections(clientConnections.UserVisibleConnections)
|
|
||||||
.length.toString();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
TC: function totalCalls() {
|
TC: function totalCalls() {
|
||||||
|
@ -386,19 +336,6 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
return StatLog.getSystemStat(SysProps.LoginsToday).toLocaleString();
|
return StatLog.getSystemStat(SysProps.LoginsToday).toLocaleString();
|
||||||
},
|
},
|
||||||
|
|
||||||
PI: function processBytesIngress() {
|
|
||||||
const stats = StatLog.getSystemStat(SysProps.ProcessTrafficStats) || {
|
|
||||||
ingress: 0,
|
|
||||||
};
|
|
||||||
return stats.ingress.toLocaleString();
|
|
||||||
},
|
|
||||||
PE: function processBytesEgress() {
|
|
||||||
const stats = StatLog.getSystemStat(SysProps.ProcessTrafficStats) || {
|
|
||||||
egress: 0,
|
|
||||||
};
|
|
||||||
return stats.ingress.toLocaleString();
|
|
||||||
},
|
|
||||||
|
|
||||||
RR: function randomRumor() {
|
RR: function randomRumor() {
|
||||||
// start the process of picking another random one
|
// start the process of picking another random one
|
||||||
setNextRandomRumor();
|
setNextRandomRumor();
|
||||||
|
@ -409,15 +346,17 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
//
|
//
|
||||||
// System File Base, Up/Download Info
|
// System File Base, Up/Download Info
|
||||||
//
|
//
|
||||||
|
// :TODO: DD - Today's # of downloads (iNiQUiTY)
|
||||||
|
//
|
||||||
SD: function systemNumDownloads() {
|
SD: function systemNumDownloads() {
|
||||||
return StatLog.getFriendlySystemStat(SysProps.FileDlTotalCount, 0);
|
return sysStatAsString(SysProps.FileDlTotalCount, 0);
|
||||||
},
|
},
|
||||||
SO: function systemByteDownload() {
|
SO: function systemByteDownload() {
|
||||||
const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes);
|
const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes);
|
||||||
return formatByteSize(byteSize, true); // true=withAbbr
|
return formatByteSize(byteSize, true); // true=withAbbr
|
||||||
},
|
},
|
||||||
SU: function systemNumUploads() {
|
SU: function systemNumUploads() {
|
||||||
return StatLog.getFriendlySystemStat(SysProps.FileUlTotalCount, 0);
|
return sysStatAsString(SysProps.FileUlTotalCount, 0);
|
||||||
},
|
},
|
||||||
SP: function systemByteUpload() {
|
SP: function systemByteUpload() {
|
||||||
const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes);
|
const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes);
|
||||||
|
@ -434,59 +373,18 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
},
|
},
|
||||||
PT: function messagesPostedToday() {
|
PT: function messagesPostedToday() {
|
||||||
// Obv/2
|
// Obv/2
|
||||||
return StatLog.getFriendlySystemStat(SysProps.MessagesToday, 0);
|
return sysStatAsString(SysProps.MessagesToday, 0);
|
||||||
},
|
},
|
||||||
TP: function totalMessagesOnSystem() {
|
TP: function totalMessagesOnSystem() {
|
||||||
// Obv/2
|
// Obv/2
|
||||||
return StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0);
|
return sysStatAsString(SysProps.MessageTotalCount, 0);
|
||||||
},
|
|
||||||
FT: function totalUploadsToday() {
|
|
||||||
// Obv/2
|
|
||||||
return StatLog.getFriendlySystemStat(SysProps.FileUlTodayCount, 0);
|
|
||||||
},
|
|
||||||
FB: function totalUploadBytesToday() {
|
|
||||||
const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTodayBytes);
|
|
||||||
return formatByteSize(byteSize, true); // true=withAbbr
|
|
||||||
},
|
|
||||||
DD: function totalDownloadsToday() {
|
|
||||||
// iNiQUiTY
|
|
||||||
return StatLog.getFriendlySystemStat(SysProps.FileDlTodayCount, 0);
|
|
||||||
},
|
|
||||||
DB: function totalDownloadBytesToday() {
|
|
||||||
const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTodayBytes);
|
|
||||||
return formatByteSize(byteSize, true); // true=withAbbr
|
|
||||||
},
|
|
||||||
NT: function totalNewUsersToday() {
|
|
||||||
// Obv/2
|
|
||||||
return StatLog.getSystemStatNum(SysProps.NewUsersTodayCount);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// :TODO: NT - New users today (Obv/2)
|
||||||
|
// :TODO: FT - Files uploaded/added *today* (Obv/2)
|
||||||
|
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
|
||||||
|
// :TODO: LC - name of last caller to system (Obv/2)
|
||||||
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
|
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
|
||||||
// :TODO: ?? - Total users on system
|
|
||||||
|
|
||||||
TU: function totalSystemUsers() {
|
|
||||||
return StatLog.getSystemStatNum(SysProps.TotalUserCount) || 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
LC: function lastCallerUserName() {
|
|
||||||
// Obv/2
|
|
||||||
const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {};
|
|
||||||
return lastLogin.userName || 'N/A';
|
|
||||||
},
|
|
||||||
LD: function lastCallerDate(client) {
|
|
||||||
const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {};
|
|
||||||
if (!lastLogin.timestamp) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
return lastLogin.timestamp.format(client.currentTheme.helpers.getDateFormat());
|
|
||||||
},
|
|
||||||
LT: function lastCallerTime(client) {
|
|
||||||
const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {};
|
|
||||||
if (!lastLogin.timestamp) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
return lastLogin.timestamp.format(client.currentTheme.helpers.getTimeFormat());
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Special handling for XY
|
// Special handling for XY
|
||||||
|
@ -526,28 +424,10 @@ function getPredefinedMCIValue(client, code, extra) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(
|
Log.error(
|
||||||
{ code: code, exception: e.message },
|
{ code: code, exception: e.message },
|
||||||
`Failed generating predefined MCI value (${code})`
|
'Exception caught generating predefined MCI value'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
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(
|
StatLog.getSystemLogEntries(
|
||||||
SystemLogKeys.UserAddedRumorz,
|
SystemLogKeys.UserAddedRumorz,
|
||||||
StatLog.Order.TimestampDesc,
|
StatLog.Order.Timestamp,
|
||||||
(err, entries) => {
|
(err, entries) => {
|
||||||
return callback(err, entriesView, entries);
|
return callback(err, entriesView, entries);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1129,10 +1129,7 @@ function FTNMessageScanTossModule() {
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
Log.warn(
|
Log.warn({ error: err.message }, 'Error exporting message');
|
||||||
{ error: err.message },
|
|
||||||
`Error exporting message: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return nextMessageOrUuid(null);
|
return nextMessageOrUuid(null);
|
||||||
}
|
}
|
||||||
|
@ -1603,7 +1600,6 @@ function FTNMessageScanTossModule() {
|
||||||
const packetOpts = { keepTearAndOrigin: false }; // needed so we can calc message UUID without these; we'll add later
|
const packetOpts = { keepTearAndOrigin: false }; // needed so we can calc message UUID without these; we'll add later
|
||||||
|
|
||||||
let importStats = {
|
let importStats = {
|
||||||
packetPath,
|
|
||||||
areaSuccess: {}, // areaTag->count
|
areaSuccess: {}, // areaTag->count
|
||||||
areaFail: {}, // areaTag->count
|
areaFail: {}, // areaTag->count
|
||||||
otherFail: 0,
|
otherFail: 0,
|
||||||
|
@ -1643,10 +1639,10 @@ function FTNMessageScanTossModule() {
|
||||||
//
|
//
|
||||||
// No local area configured for this import
|
// No local area configured for this import
|
||||||
//
|
//
|
||||||
// :TODO: Handle the "catch all" area bucket case if configured -> email with area info/etc.? catchAll: enabled, areaTag, prefixMsg
|
// :TODO: Handle the "catch all" area bucket case if configured
|
||||||
Log.warn(
|
Log.warn(
|
||||||
{ areaTag: areaTag },
|
{ areaTag: areaTag },
|
||||||
`No local message area for "${areaTag}"`
|
'No local area configured for this packet file!'
|
||||||
);
|
);
|
||||||
|
|
||||||
// bump generic failure
|
// bump generic failure
|
||||||
|
@ -1679,7 +1675,9 @@ function FTNMessageScanTossModule() {
|
||||||
|
|
||||||
self.appendTearAndOrigin(message);
|
self.appendTearAndOrigin(message);
|
||||||
|
|
||||||
const importConfig = { localAreaTag };
|
const importConfig = {
|
||||||
|
localAreaTag: localAreaTag,
|
||||||
|
};
|
||||||
|
|
||||||
self.importMailToArea(importConfig, packetHeader, message, err => {
|
self.importMailToArea(importConfig, packetHeader, message, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -1701,7 +1699,7 @@ function FTNMessageScanTossModule() {
|
||||||
uuid: message.messageUuid,
|
uuid: message.messageUuid,
|
||||||
MSGID: msgId,
|
MSGID: msgId,
|
||||||
},
|
},
|
||||||
`Not importing non-unique message "${message.subject}" to ${localAreaTag}`
|
'Not importing non-unique message'
|
||||||
);
|
);
|
||||||
|
|
||||||
return next(null);
|
return next(null);
|
||||||
|
@ -1720,34 +1718,15 @@ function FTNMessageScanTossModule() {
|
||||||
//
|
//
|
||||||
// try to produce something helpful in the log
|
// try to produce something helpful in the log
|
||||||
//
|
//
|
||||||
const makeCount = obj => {
|
const finalStats = Object.assign(importStats, { packetPath: packetPath });
|
||||||
return obj
|
if (err || Object.keys(finalStats.areaFail).length > 0) {
|
||||||
? _.reduce(
|
|
||||||
obj,
|
|
||||||
(sum, c) => {
|
|
||||||
return sum + c;
|
|
||||||
},
|
|
||||||
0
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalFail = makeCount(importStats.areaFail) + importStats.otherFail;
|
|
||||||
const packetFileName = paths.basename(packetPath);
|
|
||||||
if (err || totalFail > 0) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
Object.assign(importStats, { error: err.message });
|
Object.assign(finalStats, { error: err.message });
|
||||||
}
|
}
|
||||||
Log.warn(
|
|
||||||
importStats,
|
Log.warn(finalStats, 'Import completed with error(s)');
|
||||||
`Packet ${packetFileName} import reported ${totalFail} error(s)`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const totalSuccess = makeCount(importStats.areaSuccess);
|
Log.info(finalStats, 'Import complete');
|
||||||
Log.info(
|
|
||||||
importStats,
|
|
||||||
`Packet ${packetFileName} imported with ${totalSuccess} new message(s)`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(err);
|
cb(err);
|
||||||
|
@ -1837,9 +1816,7 @@ function FTNMessageScanTossModule() {
|
||||||
path: paths.join(importDir, packetFile),
|
path: paths.join(importDir, packetFile),
|
||||||
error: err.toString(),
|
error: err.toString(),
|
||||||
},
|
},
|
||||||
`Failed to import packet file "${paths.basename(
|
'Failed to import packet file'
|
||||||
packetFile
|
|
||||||
)}"`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
rejects.push(packetFile);
|
rejects.push(packetFile);
|
||||||
|
@ -2383,9 +2360,7 @@ function FTNMessageScanTossModule() {
|
||||||
reason: err.reason,
|
reason: err.reason,
|
||||||
tic: ticFileInfo.filePath,
|
tic: ticFileInfo.filePath,
|
||||||
},
|
},
|
||||||
`Failed to import/update TIC for "${paths.basename(
|
'Failed to import/update TIC'
|
||||||
ticFileInfo.filePath
|
|
||||||
)}"`
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.info(
|
Log.info(
|
||||||
|
@ -2394,9 +2369,7 @@ function FTNMessageScanTossModule() {
|
||||||
file: ticFileInfo.filePath,
|
file: ticFileInfo.filePath,
|
||||||
area: localInfo.areaTag,
|
area: localInfo.areaTag,
|
||||||
},
|
},
|
||||||
`TIC imported "${paths.basename(ticFileInfo.filePath)}" -> ${
|
'TIC imported successfully'
|
||||||
localInfo.areaTag
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -2765,7 +2738,6 @@ FTNMessageScanTossModule.prototype.startup = function (cb) {
|
||||||
const importSchedule = this.parseScheduleString(
|
const importSchedule = this.parseScheduleString(
|
||||||
this.moduleConfig.schedule.import
|
this.moduleConfig.schedule.import
|
||||||
);
|
);
|
||||||
|
|
||||||
if (importSchedule) {
|
if (importSchedule) {
|
||||||
Log.debug(
|
Log.debug(
|
||||||
{
|
{
|
||||||
|
@ -2794,17 +2766,14 @@ FTNMessageScanTossModule.prototype.startup = function (cb) {
|
||||||
glob: `**/${paths.basename(importSchedule.watchFile)}`,
|
glob: `**/${paths.basename(importSchedule.watchFile)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeImportMsg = (e, path) => {
|
|
||||||
return `Import/toss due to @watch[${e}] "${paths.basename(
|
|
||||||
path
|
|
||||||
)}"`;
|
|
||||||
};
|
|
||||||
|
|
||||||
['change', 'add', 'delete'].forEach(event => {
|
['change', 'add', 'delete'].forEach(event => {
|
||||||
watcher.on(event, (fileName, fileRoot) => {
|
watcher.on(event, (fileName, fileRoot) => {
|
||||||
const eventPath = paths.join(fileRoot, fileName);
|
const eventPath = paths.join(fileRoot, fileName);
|
||||||
if (eventPath === importSchedule.watchFile) {
|
if (
|
||||||
tryImportNow(makeImportMsg(event, eventPath), {
|
paths.join(fileRoot, fileName) ===
|
||||||
|
importSchedule.watchFile
|
||||||
|
) {
|
||||||
|
tryImportNow('Performing import/toss due to @watch', {
|
||||||
eventPath,
|
eventPath,
|
||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
|
@ -2816,16 +2785,12 @@ FTNMessageScanTossModule.prototype.startup = function (cb) {
|
||||||
// If the watch file already exists, kick off now
|
// If the watch file already exists, kick off now
|
||||||
// https://github.com/NuSkooler/enigma-bbs/issues/122
|
// https://github.com/NuSkooler/enigma-bbs/issues/122
|
||||||
//
|
//
|
||||||
fse.access(importSchedule.watchFile, fse.constants.R_OK, err => {
|
fse.exists(importSchedule.watchFile, exists => {
|
||||||
if (!err) {
|
if (exists) {
|
||||||
// exists and we can read
|
tryImportNow('Performing import/toss due to @watch', {
|
||||||
tryImportNow(
|
|
||||||
makeImportMsg('exists', importSchedule.watchFile),
|
|
||||||
{
|
|
||||||
eventPath: importSchedule.watchFile,
|
eventPath: importSchedule.watchFile,
|
||||||
event: 'exists',
|
event: 'initial exists',
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,8 +219,7 @@ exports.getModule = class MrcModule extends ServerModule {
|
||||||
connectedSockets.forEach(client => {
|
connectedSockets.forEach(client => {
|
||||||
if (
|
if (
|
||||||
message.to_user == '' ||
|
message.to_user == '' ||
|
||||||
// Fix PrivMSG delivery on case mismatch
|
message.to_user == client.username ||
|
||||||
message.to_user.toUpperCase() == client.username.toUpperCase() ||
|
|
||||||
message.to_user == 'CLIENT' ||
|
message.to_user == 'CLIENT' ||
|
||||||
message.from_user == client.username ||
|
message.from_user == client.username ||
|
||||||
message.to_user == 'NOTME'
|
message.to_user == 'NOTME'
|
||||||
|
|
|
@ -10,13 +10,11 @@ const {
|
||||||
splitTextAtTerms,
|
splitTextAtTerms,
|
||||||
isAnsi,
|
isAnsi,
|
||||||
stripAnsiControlCodes,
|
stripAnsiControlCodes,
|
||||||
wildcardMatch,
|
|
||||||
} = require('../../string_util.js');
|
} = require('../../string_util.js');
|
||||||
const {
|
const {
|
||||||
getMessageConferenceByTag,
|
getMessageConferenceByTag,
|
||||||
getMessageAreaByTag,
|
getMessageAreaByTag,
|
||||||
getMessageListForArea,
|
getMessageListForArea,
|
||||||
getAvailableMessageAreasByConfTag,
|
|
||||||
} = require('../../message_area.js');
|
} = require('../../message_area.js');
|
||||||
const { sortAreasOrConfs } = require('../../conf_area_util.js');
|
const { sortAreasOrConfs } = require('../../conf_area_util.js');
|
||||||
const AnsiPrep = require('../../ansi_prep.js');
|
const AnsiPrep = require('../../ansi_prep.js');
|
||||||
|
@ -124,7 +122,7 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
if (isNaN(port)) {
|
if (isNaN(port)) {
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ port: config.contentServers.gopher.port, server: ModuleInfo.name },
|
{ port: config.contentServers.gopher.port, server: ModuleInfo.name },
|
||||||
'Invalid Gopher port'
|
'Invalid port'
|
||||||
);
|
);
|
||||||
return cb(
|
return cb(
|
||||||
Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)
|
Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)
|
||||||
|
@ -245,52 +243,14 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
return cb('Not found');
|
return cb('Not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
_getConfigForConferenceTag(confTag) {
|
isAreaAndConfExposed(confTag, areaTag) {
|
||||||
const sysConfig = Config();
|
const conf = _.get(Config(), [
|
||||||
let config = _.get(sysConfig, [
|
|
||||||
'contentServers',
|
'contentServers',
|
||||||
'gopher',
|
'gopher',
|
||||||
'exposedConfAreas',
|
'messageConferences',
|
||||||
confTag,
|
confTag,
|
||||||
]);
|
]);
|
||||||
if (config) {
|
return Array.isArray(conf) && conf.includes(areaTag);
|
||||||
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) {
|
prepareMessageBody(body, cb) {
|
||||||
|
@ -344,7 +304,7 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
messageAreaGenerator(selectorMatch, cb) {
|
messageAreaGenerator(selectorMatch, cb) {
|
||||||
this.log.trace({ selector: selectorMatch[0] }, 'Message area request');
|
this.log.debug({ selector: selectorMatch[0] }, 'Serving message area content');
|
||||||
//
|
//
|
||||||
// Selector should be:
|
// Selector should be:
|
||||||
// /msgarea - list confs
|
// /msgarea - list confs
|
||||||
|
@ -354,235 +314,12 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
|
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
|
||||||
//
|
//
|
||||||
if (selectorMatch[3] || selectorMatch[4]) {
|
if (selectorMatch[3] || selectorMatch[4]) {
|
||||||
// message selector - display message
|
|
||||||
// message
|
// message
|
||||||
//const raw = selectorMatch[4] ? true : false;
|
//const raw = selectorMatch[4] ? true : false;
|
||||||
// :TODO: support 'raw'
|
// :TODO: support 'raw'
|
||||||
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
|
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
|
||||||
const confTag = selectorMatch[1].substr(1).split('/')[0];
|
const confTag = selectorMatch[1].substr(1).split('/')[0];
|
||||||
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
|
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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}`
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
].join('');
|
|
||||||
|
|
||||||
this.log.debug({ confTag, areaTag }, 'Gopher serving message list');
|
|
||||||
return cb(response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_displayMessage(selectorMatch, msgUuid, confTag, areaTag, cb) {
|
|
||||||
const message = new Message();
|
const message = new Message();
|
||||||
|
|
||||||
return message.load({ uuid: msgUuid }, err => {
|
return message.load({ uuid: msgUuid }, err => {
|
||||||
|
@ -600,7 +337,7 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
) {
|
) {
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ areaTag },
|
{ areaTag },
|
||||||
`Gopher attempted access to non-exposed "${confTag}"/"${areaTag}"`
|
'Attempted access to non-exposed conference and/or area!'
|
||||||
);
|
);
|
||||||
return this.notFoundGenerator(selectorMatch, cb);
|
return this.notFoundGenerator(selectorMatch, cb);
|
||||||
}
|
}
|
||||||
|
@ -608,7 +345,7 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
if (Message.isPrivateAreaTag(areaTag)) {
|
if (Message.isPrivateAreaTag(areaTag)) {
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ areaTag },
|
{ areaTag },
|
||||||
`Gopher attempted access to message in private "${areaTag}"`
|
'Attempted access to message in private area!'
|
||||||
);
|
);
|
||||||
return this.notFoundGenerator(selectorMatch, cb);
|
return this.notFoundGenerator(selectorMatch, cb);
|
||||||
}
|
}
|
||||||
|
@ -622,17 +359,137 @@ Subject: ${message.subject}
|
||||||
ID : ${message.messageUuid} (${message.messageId})
|
ID : ${message.messageUuid} (${message.messageId})
|
||||||
${'-'.repeat(70)}
|
${'-'.repeat(70)}
|
||||||
${msgBody}
|
${msgBody}
|
||||||
`;
|
`;
|
||||||
this.log.debug(
|
|
||||||
{
|
|
||||||
confTag,
|
|
||||||
areaTag,
|
|
||||||
uuid: message.messageUuid,
|
|
||||||
},
|
|
||||||
`Gopher serving message "${message.subject}"`
|
|
||||||
);
|
|
||||||
return cb(response);
|
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!'
|
||||||
|
);
|
||||||
|
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,7 +9,6 @@ const { getTransactionDatabase, getModDatabasePath } = require('../../database.j
|
||||||
const {
|
const {
|
||||||
getMessageAreaByTag,
|
getMessageAreaByTag,
|
||||||
getMessageConferenceByTag,
|
getMessageConferenceByTag,
|
||||||
persistMessage,
|
|
||||||
} = require('../../message_area.js');
|
} = require('../../message_area.js');
|
||||||
const User = require('../../user.js');
|
const User = require('../../user.js');
|
||||||
const Errors = require('../../enig_error.js').Errors;
|
const Errors = require('../../enig_error.js').Errors;
|
||||||
|
@ -22,7 +21,6 @@ const {
|
||||||
} = require('../../string_util.js');
|
} = require('../../string_util.js');
|
||||||
const AnsiPrep = require('../../ansi_prep.js');
|
const AnsiPrep = require('../../ansi_prep.js');
|
||||||
const { stripMciColorCodes } = require('../../color_codes.js');
|
const { stripMciColorCodes } = require('../../color_codes.js');
|
||||||
const ACS = require('../../acs');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const NNTPServerBase = require('nntp-server');
|
const NNTPServerBase = require('nntp-server');
|
||||||
|
@ -113,38 +111,6 @@ class NNTPDatabase {
|
||||||
|
|
||||||
let 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 {
|
class NNTPServer extends NNTPServerBase {
|
||||||
constructor(options, serverName) {
|
constructor(options, serverName) {
|
||||||
super(options);
|
super(options);
|
||||||
|
@ -159,26 +125,14 @@ class NNTPServer extends NNTPServerBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
_needAuth(session, command) {
|
_needAuth(session, command) {
|
||||||
if (AuthCommands.includes(command)) {
|
|
||||||
return !session.authenticated && !session.authUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super._needAuth(session, command);
|
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) {
|
_authenticate(session) {
|
||||||
const username = session.authinfo_user;
|
const username = session.authinfo_user;
|
||||||
const password = session.authinfo_pass;
|
const password = session.authinfo_pass;
|
||||||
|
|
||||||
this.log.debug(
|
this.log.trace({ username }, 'Authentication request');
|
||||||
{ username, ip: this._address(session) },
|
|
||||||
`NNTP authentication request for "${username}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const user = new User();
|
const user = new User();
|
||||||
|
@ -186,19 +140,17 @@ class NNTPServer extends NNTPServerBase {
|
||||||
{ type: User.AuthFactor1Types.Password, username, password },
|
{ type: User.AuthFactor1Types.Password, username, password },
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.warn(
|
// :TODO: Log IP address
|
||||||
{ username, reason: err.message, ip: this._address(session) },
|
this.log.debug(
|
||||||
`NNTP authentication failure for "${username}"`
|
{ username, reason: err.message },
|
||||||
|
'Authentication failure'
|
||||||
);
|
);
|
||||||
return resolve(false);
|
return resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.authUser = user;
|
session.authUser = user;
|
||||||
|
|
||||||
this.log.info(
|
this.log.debug({ username }, 'User authenticated successfully');
|
||||||
{ username, ip: this._address(session) },
|
|
||||||
`NTTP authentication success for "${username}"`
|
|
||||||
);
|
|
||||||
return resolve(true);
|
return resolve(true);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -280,7 +232,6 @@ class NNTPServer extends NNTPServerBase {
|
||||||
message.nntpHeaders = {
|
message.nntpHeaders = {
|
||||||
From: this.getJAMStyleFrom(message, fromName),
|
From: this.getJAMStyleFrom(message, fromName),
|
||||||
'X-Comment-To': toName,
|
'X-Comment-To': toName,
|
||||||
To: toName, // JAM-ish
|
|
||||||
Newsgroups: session.group.name,
|
Newsgroups: session.group.name,
|
||||||
Subject: message.subject,
|
Subject: message.subject,
|
||||||
Date: this.getMessageDate(message),
|
Date: this.getMessageDate(message),
|
||||||
|
@ -392,7 +343,7 @@ class NNTPServer extends NNTPServerBase {
|
||||||
messageUuid = msg && msg.messageUuid;
|
messageUuid = msg && msg.messageUuid;
|
||||||
} else {
|
} else {
|
||||||
// <Message-ID> request
|
// <Message-ID> request
|
||||||
[, messageUuid] = NNTPServer.getMessageIdentifierParts(messageId);
|
[, messageUuid] = this.getMessageIdentifierParts(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isString(messageUuid)) {
|
if (!_.isString(messageUuid)) {
|
||||||
|
@ -443,7 +394,7 @@ class NNTPServer extends NNTPServerBase {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
{ messageUuid, messageId, ip: this._address(session) },
|
{ messageUuid, messageId },
|
||||||
'Access denied for message'
|
'Access denied for message'
|
||||||
);
|
);
|
||||||
return resolve(null);
|
return resolve(null);
|
||||||
|
@ -641,35 +592,17 @@ class NNTPServer extends NNTPServerBase {
|
||||||
if (!conf) {
|
if (!conf) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// :TODO: validate ACS
|
||||||
|
|
||||||
const area = getMessageAreaByTag(areaTag, confTag);
|
const area = getMessageAreaByTag(areaTag, confTag);
|
||||||
if (!area) {
|
if (!area) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// :TODO: validate ACS
|
||||||
|
|
||||||
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;
|
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) {
|
getGroup(session, groupName, cb) {
|
||||||
let group = this.groupCache.get(groupName);
|
let group = this.groupCache.get(groupName);
|
||||||
if (group) {
|
if (group) {
|
||||||
|
@ -928,7 +861,7 @@ class NNTPServer extends NNTPServerBase {
|
||||||
return this.makeMessageIdentifier(message.messageId, message.messageUuid);
|
return this.makeMessageIdentifier(message.messageId, message.messageUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getMessageIdentifierParts(messageId) {
|
getMessageIdentifierParts(messageId) {
|
||||||
const m = messageId.match(
|
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>/
|
/<([0-9]+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@enigma-bbs>/
|
||||||
);
|
);
|
||||||
|
@ -986,240 +919,6 @@ class NNTPServer extends NNTPServerBase {
|
||||||
areaTag
|
areaTag
|
||||||
).replace(/\./g, '_')}`;
|
).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 {
|
exports.getModule = class NNTPServerModule extends ServerModule {
|
||||||
|
@ -1286,92 +985,11 @@ exports.getModule = class NNTPServerModule extends ServerModule {
|
||||||
|
|
||||||
const config = Config();
|
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 = {
|
const commonOptions = {
|
||||||
// :TODO: How to hook into debugging?!
|
//requireAuth : true, // :TODO: re-enable!
|
||||||
|
// :TODO: override |session| - use our own debug to Bunyan, etc.
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
if (this.enableNntp) {
|
||||||
this.nntpServer = new NNTPServer(
|
this.nntpServer = new NNTPServer(
|
||||||
// :TODO: according to docs: if connection is non-tls, but behind proxy (assuming TLS termination?!!) then set this to true
|
// :TODO: according to docs: if connection is non-tls, but behind proxy (assuming TLS termination?!!) then set this to true
|
||||||
|
|
|
@ -15,7 +15,6 @@ const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const mimeTypes = require('mime-types');
|
const mimeTypes = require('mime-types');
|
||||||
const forEachSeries = require('async/forEachSeries');
|
const forEachSeries = require('async/forEachSeries');
|
||||||
const findSeries = require('async/findSeries');
|
|
||||||
|
|
||||||
const ModuleInfo = (exports.moduleInfo = {
|
const ModuleInfo = (exports.moduleInfo = {
|
||||||
name: 'Web',
|
name: 'Web',
|
||||||
|
@ -75,6 +74,14 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
this.enableHttps = config.contentServers.web.https.enabled || false;
|
this.enableHttps = config.contentServers.web.https.enabled || false;
|
||||||
|
|
||||||
this.routes = {};
|
this.routes = {};
|
||||||
|
|
||||||
|
if (this.isEnabled() && config.contentServers.web.staticRoot) {
|
||||||
|
this.addRoute({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/static/.*$',
|
||||||
|
handler: this.routeStaticFile.bind(this),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildUrl(pathAndQuery) {
|
buildUrl(pathAndQuery) {
|
||||||
|
@ -203,21 +210,13 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
routeRequest(req, resp) {
|
routeRequest(req, resp) {
|
||||||
let route = _.find(this.routes, r => r.matchesRequest(req));
|
const route = _.find(this.routes, r => r.matchesRequest(req));
|
||||||
|
|
||||||
if (route) {
|
if (!route && '/' === req.url) {
|
||||||
return route.handler(req, resp);
|
return this.routeIndex(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) {
|
respondWithError(resp, code, bodyText, title) {
|
||||||
|
@ -257,57 +256,27 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
||||||
}
|
}
|
||||||
|
|
||||||
tryRouteIndex(req, resp, cb) {
|
routeIndex(req, resp) {
|
||||||
const tryFiles = Config().contentServers.web.tryFiles || [
|
const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html');
|
||||||
'index.html',
|
return this.returnStaticPage(filePath, resp);
|
||||||
'index.htm',
|
}
|
||||||
];
|
|
||||||
|
|
||||||
findSeries(
|
routeStaticFile(req, resp) {
|
||||||
tryFiles,
|
const fileName = req.url.substr(req.url.indexOf('/', 1));
|
||||||
(tryFile, nextTryFile) => {
|
|
||||||
const fileName = paths.join(
|
|
||||||
req.url.substr(req.url.lastIndexOf('/', 1)),
|
|
||||||
tryFile
|
|
||||||
);
|
|
||||||
const filePath = this.resolveStaticPath(fileName);
|
const filePath = this.resolveStaticPath(fileName);
|
||||||
|
return this.returnStaticPage(filePath, resp);
|
||||||
fs.stat(filePath, (err, stats) => {
|
|
||||||
if (err || !stats.isFile()) {
|
|
||||||
return nextTryFile(null, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
returnStaticPage(filePath, resp) {
|
||||||
'Content-Type':
|
const self = this;
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tryStaticRoute(req, resp, cb) {
|
|
||||||
const fileName = req.url.substr(req.url.lastIndexOf('/', 1));
|
|
||||||
const filePath = this.resolveStaticPath(fileName);
|
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return cb(false);
|
return this.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.stat(filePath, (err, stats) => {
|
fs.stat(filePath, (err, stats) => {
|
||||||
if (err || !stats.isFile()) {
|
if (err || !stats.isFile()) {
|
||||||
return cb(false);
|
return self.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -319,9 +288,7 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
|
|
||||||
const readStream = fs.createReadStream(filePath);
|
const readStream = fs.createReadStream(filePath);
|
||||||
resp.writeHead(200, headers);
|
resp.writeHead(200, headers);
|
||||||
readStream.pipe(resp);
|
return readStream.pipe(resp);
|
||||||
|
|
||||||
return cb(true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const Errors = require('../core/enig_error.js').Errors;
|
const Errors = require('../core/enig_error.js').Errors;
|
||||||
|
const ANSI = require('./ansi_term.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const { getMessageAreaByTag } = require('./message_area.js');
|
const { getMessageAreaByTag } = require('./message_area.js');
|
||||||
|
|
||||||
|
@ -20,7 +21,6 @@ exports.moduleInfo = {
|
||||||
exports.getModule = class ShowArtModule extends MenuModule {
|
exports.getModule = class ShowArtModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
|
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
|
||||||
extraArgs: options.extraArgs,
|
extraArgs: options.extraArgs,
|
||||||
});
|
});
|
||||||
|
|
316
core/stat_log.js
316
core/stat_log.js
|
@ -4,15 +4,10 @@
|
||||||
const sysDb = require('./database.js').dbs.system;
|
const sysDb = require('./database.js').dbs.system;
|
||||||
const { getISOTimestampString } = require('./database.js');
|
const { getISOTimestampString } = require('./database.js');
|
||||||
const Errors = require('./enig_error.js');
|
const Errors = require('./enig_error.js');
|
||||||
const SysProps = require('./system_property.js');
|
|
||||||
const UserProps = require('./user_property');
|
|
||||||
const Message = require('./message');
|
|
||||||
const { getActiveConnections, AllConnections } = require('./client_connections');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const SysInfo = require('systeminformation');
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
System Event Log & Stats
|
System Event Log & Stats
|
||||||
|
@ -29,7 +24,6 @@ const SysInfo = require('systeminformation');
|
||||||
class StatLog {
|
class StatLog {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.systemStats = {};
|
this.systemStats = {};
|
||||||
this.lastSysInfoStatsRefresh = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(cb) {
|
init(cb) {
|
||||||
|
@ -112,17 +106,7 @@ class StatLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSystemStat(statName) {
|
getSystemStat(statName) {
|
||||||
const stat = this.systemStats[statName];
|
return this.systemStats[statName];
|
||||||
|
|
||||||
// Some stats are refreshed periodically when they are
|
|
||||||
// being accessed (e.g. "looked at"). This is handled async.
|
|
||||||
this._refreshSystemStat(statName);
|
|
||||||
|
|
||||||
return stat;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFriendlySystemStat(statName, defaultValue) {
|
|
||||||
return (this.getSystemStat(statName) || defaultValue).toLocaleString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSystemStatNum(statName) {
|
getSystemStatNum(statName) {
|
||||||
|
@ -157,25 +141,13 @@ class StatLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserStat(user, statName) {
|
getUserStat(user, statName) {
|
||||||
return user.getProperty(statName);
|
return user.properties[statName];
|
||||||
}
|
|
||||||
|
|
||||||
getUserStatByClient(client, statName) {
|
|
||||||
const stat = this.getUserStat(client.user, statName);
|
|
||||||
this._refreshUserStat(client, statName);
|
|
||||||
return stat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserStatNum(user, statName) {
|
getUserStatNum(user, statName) {
|
||||||
return parseInt(this.getUserStat(user, statName)) || 0;
|
return parseInt(this.getUserStat(user, statName)) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserStatNumByClient(client, statName, ttlSeconds = 10) {
|
|
||||||
const stat = this.getUserStatNum(client.user, statName);
|
|
||||||
this._refreshUserStat(client, statName, ttlSeconds);
|
|
||||||
return stat;
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementUserStat(user, statName, incrementBy, cb) {
|
incrementUserStat(user, statName, incrementBy, cb) {
|
||||||
incrementBy = incrementBy || 1;
|
incrementBy = incrementBy || 1;
|
||||||
|
|
||||||
|
@ -267,18 +239,75 @@ class StatLog {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
/*
|
||||||
// Find System Log entry(s) by |filter|:
|
Find System Log entries by |filter|:
|
||||||
//
|
|
||||||
// - logName: Name of log (required)
|
filter.logName (required)
|
||||||
// - resultType: 'obj' | 'count' (default='obj')
|
filter.resultType = (obj) | count
|
||||||
// - limit: Limit returned results
|
where obj contains timestamp and log_value
|
||||||
// - date: exact date to filter against
|
filter.limit
|
||||||
// - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random'
|
filter.date - exact date to filter against
|
||||||
// (default='timestamp')
|
filter.order = (timestamp) | timestamp_asc | timestamp_desc | random
|
||||||
//
|
*/
|
||||||
findSystemLogEntries(filter, cb) {
|
findSystemLogEntries(filter, cb) {
|
||||||
return this._findLogEntries('system_event_log', filter, cb);
|
filter = filter || {};
|
||||||
|
if (!_.isString(filter.logName)) {
|
||||||
|
return cb(Errors.MissingParam('filter.logName is required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.resultType = filter.resultType || 'obj';
|
||||||
|
filter.order = filter.order || 'timestamp';
|
||||||
|
|
||||||
|
let sql;
|
||||||
|
if ('count' === filter.resultType) {
|
||||||
|
sql = `SELECT COUNT() AS count
|
||||||
|
FROM system_event_log`;
|
||||||
|
} else {
|
||||||
|
sql = `SELECT timestamp, log_value
|
||||||
|
FROM system_event_log`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' WHERE log_name = ?';
|
||||||
|
|
||||||
|
if (filter.date) {
|
||||||
|
filter.date = moment(filter.date);
|
||||||
|
sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format(
|
||||||
|
'YYYY-MM-DD'
|
||||||
|
)}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('count' !== filter.resultType) {
|
||||||
|
switch (filter.order) {
|
||||||
|
case 'timestamp':
|
||||||
|
case 'timestamp_asc':
|
||||||
|
sql += ' ORDER BY timestamp ASC';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'timestamp_desc':
|
||||||
|
sql += ' ORDER BY timestamp DESC';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'random':
|
||||||
|
sql += ' ORDER BY RANDOM()';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isNumber(filter.limit) && 0 !== filter.limit) {
|
||||||
|
sql += ` LIMIT ${filter.limit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ';';
|
||||||
|
|
||||||
|
if ('count' === filter.resultType) {
|
||||||
|
sysDb.get(sql, [filter.logName], (err, row) => {
|
||||||
|
return cb(err, row ? row.count : 0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sysDb.all(sql, [filter.logName], (err, rows) => {
|
||||||
|
return cb(err, rows);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSystemLogEntries(logName, order, limit, cb) {
|
getSystemLogEntries(logName, order, limit, cb) {
|
||||||
|
@ -339,211 +368,6 @@ class StatLog {
|
||||||
systemEventUserLogInit(this);
|
systemEventUserLogInit(this);
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Find User Log entry(s) by |filter|:
|
|
||||||
//
|
|
||||||
// - logName: Name of log (required)
|
|
||||||
// - userId: User ID in which to restrict entries to (missing=all)
|
|
||||||
// - sessionId: Session ID in which to restrict entries to (missing=any)
|
|
||||||
// - resultType: 'obj' | 'count' (default='obj')
|
|
||||||
// - limit: Limit returned results
|
|
||||||
// - date: exact date to filter against
|
|
||||||
// - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random'
|
|
||||||
// (default='timestamp')
|
|
||||||
//
|
|
||||||
findUserLogEntries(filter, cb) {
|
|
||||||
return this._findLogEntries('user_event_log', filter, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshSystemStat(statName) {
|
|
||||||
switch (statName) {
|
|
||||||
case SysProps.SystemLoadStats:
|
|
||||||
case SysProps.SystemMemoryStats:
|
|
||||||
return this._refreshSysInfoStats();
|
|
||||||
|
|
||||||
case SysProps.ProcessTrafficStats:
|
|
||||||
return this._refreshProcessTrafficStats();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshSysInfoStats() {
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
if (now < this.lastSysInfoStatsRefresh + 5) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastSysInfoStatsRefresh = now;
|
|
||||||
|
|
||||||
const basicSysInfo = {
|
|
||||||
mem: 'total, free',
|
|
||||||
currentLoad: 'avgLoad, currentLoad',
|
|
||||||
};
|
|
||||||
|
|
||||||
SysInfo.get(basicSysInfo)
|
|
||||||
.then(sysInfo => {
|
|
||||||
const memStats = {
|
|
||||||
totalBytes: sysInfo.mem.total,
|
|
||||||
freeBytes: sysInfo.mem.free,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setNonPersistentSystemStat(SysProps.SystemMemoryStats, memStats);
|
|
||||||
|
|
||||||
const loadStats = {
|
|
||||||
// Not avail on BSD, yet.
|
|
||||||
average: parseFloat(
|
|
||||||
_.get(sysInfo, 'currentLoad.avgLoad', 0).toFixed(2)
|
|
||||||
),
|
|
||||||
current: parseFloat(
|
|
||||||
_.get(sysInfo, 'currentLoad.currentLoad', 0).toFixed(2)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setNonPersistentSystemStat(SysProps.SystemLoadStats, loadStats);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
// :TODO: log me
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshProcessTrafficStats() {
|
|
||||||
const trafficStats = getActiveConnections(AllConnections).reduce(
|
|
||||||
(stats, conn) => {
|
|
||||||
stats.ingress += conn.rawSocket.bytesRead;
|
|
||||||
stats.egress += conn.rawSocket.bytesWritten;
|
|
||||||
return stats;
|
|
||||||
},
|
|
||||||
{ ingress: 0, egress: 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setNonPersistentSystemStat(SysProps.ProcessTrafficStats, trafficStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshUserStat(client, statName, ttlSeconds) {
|
|
||||||
switch (statName) {
|
|
||||||
case UserProps.NewPrivateMailCount:
|
|
||||||
this._wrapUserRefreshWithCachedTTL(
|
|
||||||
client,
|
|
||||||
statName,
|
|
||||||
this._refreshUserPrivateMailCount,
|
|
||||||
ttlSeconds
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UserProps.NewAddressedToMessageCount:
|
|
||||||
this._wrapUserRefreshWithCachedTTL(
|
|
||||||
client,
|
|
||||||
statName,
|
|
||||||
this._refreshUserNewAddressedToMessageCount,
|
|
||||||
ttlSeconds
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_wrapUserRefreshWithCachedTTL(client, statName, updateMethod, ttlSeconds) {
|
|
||||||
client.statLogRefreshCache = client.statLogRefreshCache || new Map();
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const old = client.statLogRefreshCache.get(statName) || 0;
|
|
||||||
if (now < old + ttlSeconds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMethod(client);
|
|
||||||
client.statLogRefreshCache.set(statName, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshUserPrivateMailCount(client) {
|
|
||||||
const MsgArea = require('./message_area');
|
|
||||||
MsgArea.getNewMessageCountInAreaForUser(
|
|
||||||
client.user.userId,
|
|
||||||
Message.WellKnownAreaTags.Private,
|
|
||||||
(err, count) => {
|
|
||||||
if (!err) {
|
|
||||||
client.user.setProperty(UserProps.NewPrivateMailCount, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshUserNewAddressedToMessageCount(client) {
|
|
||||||
const MsgArea = require('./message_area');
|
|
||||||
MsgArea.getNewMessageCountAddressedToUser(client, (err, count) => {
|
|
||||||
if (!err) {
|
|
||||||
client.user.setProperty(UserProps.NewAddressedToMessageCount, count);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_findLogEntries(logTable, filter, cb) {
|
|
||||||
filter = filter || {};
|
|
||||||
if (!_.isString(filter.logName)) {
|
|
||||||
return cb(Errors.MissingParam('filter.logName is required'));
|
|
||||||
}
|
|
||||||
|
|
||||||
filter.resultType = filter.resultType || 'obj';
|
|
||||||
filter.order = filter.order || 'timestamp';
|
|
||||||
|
|
||||||
let sql;
|
|
||||||
if ('count' === filter.resultType) {
|
|
||||||
sql = `SELECT COUNT() AS count
|
|
||||||
FROM ${logTable}`;
|
|
||||||
} else {
|
|
||||||
sql = `SELECT timestamp, log_value
|
|
||||||
FROM ${logTable}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ' WHERE log_name = ?';
|
|
||||||
|
|
||||||
if (_.isNumber(filter.userId)) {
|
|
||||||
sql += ` AND user_id = ${filter.userId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.sessionId) {
|
|
||||||
sql += ` AND session_id = ${filter.sessionId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.date) {
|
|
||||||
filter.date = moment(filter.date);
|
|
||||||
sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format(
|
|
||||||
'YYYY-MM-DD'
|
|
||||||
)}")`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('count' !== filter.resultType) {
|
|
||||||
switch (filter.order) {
|
|
||||||
case 'timestamp':
|
|
||||||
case 'timestamp_asc':
|
|
||||||
sql += ' ORDER BY timestamp ASC';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'timestamp_desc':
|
|
||||||
sql += ' ORDER BY timestamp DESC';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'random':
|
|
||||||
sql += ' ORDER BY RANDOM()';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isNumber(filter.limit) && 0 !== filter.limit) {
|
|
||||||
sql += ` LIMIT ${filter.limit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ';';
|
|
||||||
|
|
||||||
if ('count' === filter.resultType) {
|
|
||||||
sysDb.get(sql, [filter.logName], (err, row) => {
|
|
||||||
return cb(err, row ? row.count : 0);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sysDb.all(sql, [filter.logName], (err, rows) => {
|
|
||||||
return cb(err, rows);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new StatLog();
|
module.exports = new StatLog();
|
||||||
|
|
|
@ -380,7 +380,7 @@ function formatByteSizeAbbr(byteSize) {
|
||||||
|
|
||||||
function formatByteSize(byteSize, withAbbr = false, decimals = 2) {
|
function formatByteSize(byteSize, withAbbr = false, decimals = 2) {
|
||||||
const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024));
|
const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024));
|
||||||
let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)).toString();
|
let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals));
|
||||||
if (withAbbr) {
|
if (withAbbr) {
|
||||||
result += ` ${BYTE_SIZE_ABBRS[i]}`;
|
result += ` ${BYTE_SIZE_ABBRS[i]}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const LogNames = require('./user_log_name.js');
|
const LogNames = require('./user_log_name.js');
|
||||||
const SysProps = require('./system_property.js');
|
|
||||||
|
|
||||||
const DefaultKeepForDays = 365;
|
const DefaultKeepForDays = 365;
|
||||||
|
|
||||||
|
@ -31,7 +30,6 @@ module.exports = function systemEventUserLogInit(statLog) {
|
||||||
const detailHandler = {
|
const detailHandler = {
|
||||||
[systemEvents.NewUser]: e => {
|
[systemEvents.NewUser]: e => {
|
||||||
append(e, LogNames.NewUser, 1);
|
append(e, LogNames.NewUser, 1);
|
||||||
statLog.incrementNonPersistentSystemStat(SysProps.NewUsersTodayCount, 1);
|
|
||||||
},
|
},
|
||||||
[systemEvents.UserLogin]: e => {
|
[systemEvents.UserLogin]: e => {
|
||||||
append(e, LogNames.Login, 1);
|
append(e, LogNames.Login, 1);
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
//
|
//
|
||||||
module.exports = {
|
module.exports = {
|
||||||
UserAddedRumorz: 'system_rumorz',
|
UserAddedRumorz: 'system_rumorz',
|
||||||
UserLoginHistory: 'user_login_history', // JSON object
|
UserLoginHistory: 'user_login_history',
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,6 @@ const iconv = require('iconv-lite');
|
||||||
|
|
||||||
exports.login = login;
|
exports.login = login;
|
||||||
exports.login2FA_OTP = login2FA_OTP;
|
exports.login2FA_OTP = login2FA_OTP;
|
||||||
exports.setClientEncoding = setClientEncoding;
|
|
||||||
exports.logoff = logoff;
|
exports.logoff = logoff;
|
||||||
exports.prevMenu = prevMenu;
|
exports.prevMenu = prevMenu;
|
||||||
exports.nextMenu = nextMenu;
|
exports.nextMenu = nextMenu;
|
||||||
|
@ -242,16 +241,6 @@ function nextArea(callingMenu, formData, extraArgs, cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setClientEncoding(callingMenu, formData, extraArgs, cb) {
|
|
||||||
// TODO: Also add other encoding types
|
|
||||||
const client = callingMenu.client;
|
|
||||||
let encoding = formData.value.encoding;
|
|
||||||
|
|
||||||
client.term.outputEncoding = encoding;
|
|
||||||
|
|
||||||
return callingMenu.nextMenu(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
|
function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
|
||||||
const username = formData.value.username || callingMenu.client.user.username;
|
const username = formData.value.username || callingMenu.client.user.username;
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
LoginCount: 'login_count',
|
LoginCount: 'login_count',
|
||||||
LoginsToday: 'logins_today', // non-persistent
|
LoginsToday: 'logins_today', // non-persistent
|
||||||
LastLogin: 'last_login', // object { userId, sessionId, userName, userRealName, timestamp }; non-persistent
|
|
||||||
|
|
||||||
FileBaseAreaStats: 'file_base_area_stats', // object - see file_base_area.js::getAreaStats
|
FileBaseAreaStats: 'file_base_area_stats', // object - see file_base_area.js::getAreaStats
|
||||||
FileUlTotalCount: 'ul_total_count',
|
FileUlTotalCount: 'ul_total_count',
|
||||||
|
@ -18,27 +17,17 @@ module.exports = {
|
||||||
FileDlTotalCount: 'dl_total_count',
|
FileDlTotalCount: 'dl_total_count',
|
||||||
FileDlTotalBytes: 'dl_total_bytes',
|
FileDlTotalBytes: 'dl_total_bytes',
|
||||||
|
|
||||||
FileUlTodayCount: 'ul_today_count', // non-persistent
|
|
||||||
FileUlTodayBytes: 'ul_today_bytes', // non-persistent
|
|
||||||
FileDlTodayCount: 'dl_today_count', // non-persistent
|
|
||||||
FileDlTodayBytes: 'dl_today_bytes', // non-persistent
|
|
||||||
|
|
||||||
MessageTotalCount: 'message_post_total_count', // total non-private messages on the system; non-persistent
|
MessageTotalCount: 'message_post_total_count', // total non-private messages on the system; non-persistent
|
||||||
MessagesToday: 'message_post_today', // non-private messages posted/imported today; non-persistent
|
MessagesToday: 'message_post_today', // non-private messages posted/imported today; non-persistent
|
||||||
|
|
||||||
SysOpUsername: 'sysop_username', // non-persistent
|
// begin +op non-persistent...
|
||||||
SysOpRealName: 'sysop_real_name', // non-persistent
|
SysOpUsername: 'sysop_username',
|
||||||
SysOpLocation: 'sysop_location', // non-persistent
|
SysOpRealName: 'sysop_real_name',
|
||||||
SysOpAffiliations: 'sysop_affiliation', // non-persistent
|
SysOpLocation: 'sysop_location',
|
||||||
SysOpSex: 'sysop_sex', // non-persistent
|
SysOpAffiliations: 'sysop_affiliation',
|
||||||
SysOpEmailAddress: 'sysop_email_address', // non-persistent
|
SysOpSex: 'sysop_sex',
|
||||||
|
SysOpEmailAddress: 'sysop_email_address',
|
||||||
|
// end +op non-persistent
|
||||||
|
|
||||||
NextRandomRumor: 'random_rumor',
|
NextRandomRumor: 'random_rumor',
|
||||||
|
|
||||||
SystemMemoryStats: 'system_memory_stats', // object { totalBytes, freeBytes }; non-persistent
|
|
||||||
SystemLoadStats: 'system_load_stats', // object { average, current }; non-persistent
|
|
||||||
ProcessTrafficStats: 'system_traffic_bytes_ingress', // object { ingress, egress }; non-persistent
|
|
||||||
|
|
||||||
TotalUserCount: 'user_total_count', // non-persistent
|
|
||||||
NewUsersTodayCount: 'user_new_today_count', // non-persistent
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,10 +21,8 @@ exports.validateEmailAvail = validateEmailAvail;
|
||||||
exports.validateBirthdate = validateBirthdate;
|
exports.validateBirthdate = validateBirthdate;
|
||||||
exports.validatePasswordSpec = validatePasswordSpec;
|
exports.validatePasswordSpec = validatePasswordSpec;
|
||||||
|
|
||||||
const emptyFieldError = () => new Error('Field cannot be empty');
|
|
||||||
|
|
||||||
function validateNonEmpty(data, cb) {
|
function validateNonEmpty(data, cb) {
|
||||||
return cb(data && data.length > 0 ? null : emptyFieldError);
|
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateMessageSubject(data, cb) {
|
function validateMessageSubject(data, cb) {
|
||||||
|
@ -93,11 +91,7 @@ function validateGeneralMailAddressedTo(data, cb) {
|
||||||
// :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
|
// :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
|
||||||
const addressedToInfo = getAddressedToInfo(data);
|
const addressedToInfo = getAddressedToInfo(data);
|
||||||
|
|
||||||
if (addressedToInfo.name.length === 0) {
|
if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
|
||||||
return cb(emptyFieldError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -223,7 +223,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
self.client.log.warn(
|
self.client.log.info(
|
||||||
`Telnet bridge connection error: ${err.message}`
|
`Telnet bridge connection error: ${err.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue