Compare commits
22 Commits
Author | SHA1 | Date |
---|---|---|
Hri7566 | e65159830f | |
Hri7566 | a82839085f | |
Hri7566 | 617c8c4bc5 | |
Hri7566 | b7911c5cfa | |
Hri7566 | ceab9310b5 | |
Hri7566 | dd6f841b60 | |
Hri7566 | 0e21bdc687 | |
Hri7566 | 163a01bf8a | |
Hri7566 | fd65fc57dd | |
Hri7566 | bac593dcef | |
Hri7566 | ad083a7196 | |
Hri7566 | e5fb941bb7 | |
Hri7566 | 937082694e | |
Hri7566 | 9fcc3864c0 | |
Hri7566 | 8c8af2edcc | |
Hri7566 | fb693d3b02 | |
Hri7566 | a5d3bd7670 | |
Hri7566 | e8b5c24714 | |
Hri7566 | 134b978d07 | |
Hri7566 | 2133c21d22 | |
Hri7566 | 754629feae | |
Hri7566 | 2332aa5810 |
|
@ -1,4 +1,3 @@
|
||||||
[submodule "public"]
|
[submodule "public"]
|
||||||
path = public
|
path = public
|
||||||
url = https://github.com/Hri7566/mpp-frontend-dev
|
url = git@git.hri7566.info:Hri7566/mpp-frontend-dev
|
||||||
branch = dev2
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
142
README.md
142
README.md
|
@ -1,8 +1,8 @@
|
||||||
![MPP](https://github.com/multiplayerpiano/mpp-frontend-v1/blob/master/static/128-piano.png?raw=true)
|
<img src="./.resources/images/128-piano-dev2.png" alt="MPP" width="128" />
|
||||||
|
|
||||||
# mpp-server-dev2
|
# mpp-server-dev2
|
||||||
|
|
||||||
This is an MPP server currently in development for [MPP.dev](https://www.multiplayerpiano.dev). The original server is old and the site desperately needs a new one.
|
This is an MPP server currently in development for [MPP.dev](https://www.multiplayerpiano.dev). It is roughly feature complete, supporting many of the planned ideas already, but certain features that some users might want to use for another site are currently unimplemented, namely a more complicated scheme for the antibot system.
|
||||||
|
|
||||||
This server uses Bun - not just the runtime, but the libraries as well. This is because Bun provides easy access to uWebSockets.js, a speedy implementation of WebSockets that heavily outperforms the old `ws` module that is used so frequently.
|
This server uses Bun - not just the runtime, but the libraries as well. This is because Bun provides easy access to uWebSockets.js, a speedy implementation of WebSockets that heavily outperforms the old `ws` module that is used so frequently.
|
||||||
|
|
||||||
|
@ -63,30 +63,38 @@ This has always been the future intention of this project.
|
||||||
- Ability to rename channels
|
- Ability to rename channels
|
||||||
- Chat clearing similar to MPP.net
|
- Chat clearing similar to MPP.net
|
||||||
- Channel forceloading message
|
- Channel forceloading message
|
||||||
|
- YAML configs
|
||||||
|
- Automatic reloading of configs during runtime via file watching
|
||||||
|
- Interfacing handled by JS Proxy objects
|
||||||
|
- Templating on frontend
|
||||||
|
- Handles changing things on page based on config
|
||||||
|
- Requires the use of `mpp-frontend-dev` to function properly
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Change `socketsBySocketID` to `socketsByUUID`
|
- [x] Token generation
|
||||||
- Channel data saving
|
- [ ] Frontend implementation
|
||||||
- Permission groups and permissions
|
- [ ] Permission groups and permissions
|
||||||
- Probable permission groups: owner, admin, mod, trialmod, default
|
- [x] Probable permission groups: owner, admin, mod, trialmod, default
|
||||||
- Setup tags for each permission group
|
- [x] Setup tags for each permission group
|
||||||
- MPP.com data message
|
- [ ] Implement permissions into rest of server
|
||||||
|
- [ ] MPP.com data message
|
||||||
- Implement based on `spooky.js` given there is no official documentation
|
- Implement based on `spooky.js` given there is no official documentation
|
||||||
- No cussing setting
|
- [ ] No cussing setting
|
||||||
- Full server-wide event bus
|
- badwords.txt
|
||||||
- Channel events
|
- [x] Full server-wide event bus
|
||||||
- Socket events
|
- [ ] Channel events
|
||||||
- User data events
|
- [ ] Socket events
|
||||||
- Permission-related events
|
- [ ] User data events
|
||||||
- Redo ratelimits
|
- [ ] Permission-related events
|
||||||
- Redo all of the validations with Zod
|
- [ ] Redo ratelimits
|
||||||
- This probably means making Zod schemas for every single message type
|
- [ ] Test every frontend
|
||||||
- Also user and channel data
|
- [ ] Test fishing bot
|
||||||
- Test every frontend
|
- [ ] Remote console
|
||||||
- Test fishing bot
|
- [x] Modify frontend to use templating
|
||||||
- Remote console
|
- [x] index.html
|
||||||
- Modify frontend to use templating
|
- [ ] js files
|
||||||
|
- [ ] Completley reorganize script.js
|
||||||
|
|
||||||
## Backlog/Notes
|
## Backlog/Notes
|
||||||
|
|
||||||
|
@ -102,77 +110,85 @@ This has always been the future intention of this project.
|
||||||
- Check for different messages?
|
- Check for different messages?
|
||||||
- Check for URL?
|
- Check for URL?
|
||||||
- Notifications for server-generated XSS?
|
- Notifications for server-generated XSS?
|
||||||
|
- Somehow check for templating, maybe with the existing httpIPCache?
|
||||||
- Migrate to PostgreSQL instead of SQLite
|
- Migrate to PostgreSQL instead of SQLite
|
||||||
- Likely a low priority, we use prisma anyway, but it would be nice to have a server
|
- Likely a low priority, we use prisma anyway, but it would be nice to have a non-blocking database
|
||||||
- Implement user caching
|
- Implement user caching
|
||||||
- Skip redis due to the infamous licensing issues
|
- Skip redis due to the infamous licensing issues
|
||||||
|
- fork?
|
||||||
- Probably use a simple in-memory cache
|
- Probably use a simple in-memory cache
|
||||||
- Likely store with leveldb or JSON
|
- Likely store with leveldb or JSON
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
Don't expect these instructions to stay the same. They might not even be up to date already! This is due to frequent changes in this repository, as this project is still in active development.
|
This might seem like a lot of reading, but it's worth reading through everything here. There's a lot of info that you could miss, so take your time!
|
||||||
|
Also, don't expect these instructions to stay the same forever. Because this server is in an early pre-release state and is in active development, there will be frequent changes in this repository.
|
||||||
|
|
||||||
0. Install bun
|
0. Setup
|
||||||
|
|
||||||
|
- Install bun
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl -fsSL https://bun.sh/install | bash
|
$ curl -fsSL https://bun.sh/install | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Clone the repo and setup Git submodules
|
- Clone the repository and setup Git submodules
|
||||||
|
|
||||||
This step is subject to change, due to the necessity of testing different frontends, where the frontend may or may not be a git submodule.
|
If you are forking this repository, you can just setup a new submodule for the frontend (instructions not included), **however, templating will likely not function properly with this approach unless you implement it yourself.**
|
||||||
This will probably be updated in the near future. Expect a step asking to download the frontend manually.
|
|
||||||
If you are forking this repository, you can just setup a new submodule for the frontend.
|
|
||||||
The frontend files go in the `public` folder.
|
|
||||||
|
|
||||||
I am also considering using handlebars or something similar for templating, where the frontend will require completely different code.
|
If you would like to use a different repository for the frontend, the files go in the `public` folder.
|
||||||
The reason behind this decision is that I would like different things to change on the frontend based on the server's config files,
|
|
||||||
such as enabling the color changing option in the userset modal menu, or sending separate code to server admins/mods/webmasters.
|
|
||||||
|
|
||||||
|
In any case, if you would like the templating features and want the frontend to change based on the server's configuration, setting up git submodules is practically required for full compatability.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://git.hri7566.info/Hri7566/mpp-server-dev2
|
$ git clone --recursive https://git.hri7566.info/Hri7566/mpp-server-dev2
|
||||||
$ cd mpp-server-dev2
|
```
|
||||||
$ git submodule update --init --recursive
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Configure
|
2. Configure
|
||||||
|
|
||||||
- Copy environment variables
|
- Copy default environment variables
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cp .env.template .env
|
$ cp .env.template .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env` to your needs. Some variables are required for certain features to work.
|
Edit `.env` to your needs. Some variables are required for certain features to work. Most of this is self-explanatory if you have set up other large projects.
|
||||||
|
- `DATABASE_URL`: Database URI for prisma to connect to (as of right now, this is required to be a sqlite path)
|
||||||
|
- `PORT`: TCP port the HTTP/WS server will run on
|
||||||
|
- `ADMIN_PASS`: Admin password for the server
|
||||||
|
- `SALT`: Hashing salt for creating general-purpose IDs/user IDs
|
||||||
|
- `COLOR_SALT`: Hashing salt for creating user colors
|
||||||
|
|
||||||
|
Obviously, you can also set those in your shell environment instead, if need be.
|
||||||
|
|
||||||
- Edit the files in the `config` folder to match your needs
|
- Edit the files in the `config` folder to match your needs
|
||||||
|
|
||||||
For token auth, there are a few options to consider. In `config/users.yml`, you can set `tokenAuth` to a few different values:
|
For token authentication, there are a few options to consider. In `config/users.yml`, you can set `tokenAuth` to a few different values:
|
||||||
|
|
||||||
- `jwt`: Use JWT token authentication
|
- `jwt`: Use JWT token authentication
|
||||||
- `uuid`: Use UUID token authentication
|
- `uuid`: Use UUID token authentication
|
||||||
- `none`: Disable token authentication
|
- `none`: Disable token authentication
|
||||||
|
|
||||||
If you are using UUID token authentication, the server will generate a UUID token for each user when they first connect.
|
If you are using UUID token authentication, the server will generate a UUID token for each user when they first connect. This option is relatively simple and could be considered less secure.
|
||||||
|
|
||||||
If you are using JWT token authentication, you will need to generate a key for the server to use.
|
If you are using JWT token authentication, the server will generate a JSON Web Token for each user when they first connect.
|
||||||
This can be done by running the following command:
|
You will need to generate a key in the file `mppkey` for the server to use.
|
||||||
|
This can be done by running the following command, given `openssl` is installed:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ openssl genrsa -out mppkey 2048
|
$ openssl genrsa -out mppkey 2048
|
||||||
```
|
```
|
||||||
|
|
||||||
For antibot/browser detection there are also a few options to consider. In `config/users.yml`, you can set `browserChallenge` to a few different values:
|
For antibot/browser detection there are also a few options to consider.
|
||||||
|
In `config/users.yml`, you can set `browserChallenge` to a few different values:
|
||||||
|
|
||||||
- `none`: Disable browser challenge
|
- `none`: Disable browser challenge
|
||||||
- `basic`: Use a simple function to detect browsers
|
- `basic`: Use a simple function to detect browsers
|
||||||
- `obf`: Use an obfuscated function to detect browsers - TODO: implement this
|
- `obf`: Use an obfuscated function to detect browsers - **this is not implemented yet**
|
||||||
|
|
||||||
The `basic` option only sends a simple function to the client, and the `obf` option sends an obfuscated mess to the client.
|
The `basic` option only sends a simple function to the client, and the `obf` option sends an obfuscated mess to the client.
|
||||||
|
|
||||||
This option requires the newer-style (MPP.net) frontend to be used.
|
Token authentication is only supported on most frontends newer than 2020.
|
||||||
|
|
||||||
3. Install packages
|
3. Install packages
|
||||||
|
|
||||||
|
@ -188,27 +204,38 @@ such as enabling the color changing option in the userset modal menu, or sending
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Run
|
5. Run
|
||||||
|
|
||||||
|
The main entrypoint is in `src/start.ts`.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ bun .
|
$ bun src/start.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
If you would like to run in development mode:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ bun dev
|
||||||
|
# or
|
||||||
|
$ bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Background Info on Feature Implementation Decisions
|
## Background Info on Feature Implementation Decisions
|
||||||
|
|
||||||
To avoid various controversies or confusion, I will attempt to explain why certain features were implemented in this section.
|
To avoid various controversies or mass confusion, I will attempt to explain why certain features were implemented any why certain things may be missing in this section.
|
||||||
|
|
||||||
### General Explanation
|
### General Explanation
|
||||||
|
|
||||||
Multiplayer Piano was originally developed by Brandon Lockaby from 2012-2020, and this server was the original MPP server and was written in JavaScript for Node.js.
|
Multiplayer Piano (MPP) was originally developed by Brandon Lockaby from 2012-2020, and this server was the original MPP server and was written in JavaScript for the Node.js runtime.
|
||||||
It had many unknown features and basically has no documentation, so most of the admin features based on this server are guesswork or based on tiny snippets of code acquired from various sources.
|
Brandon didn't share details about it often, and it had many unknown features and basically has no documentation, so most of the admin features based on this server are guesswork or based on tiny snippets of code acquired from various sources.
|
||||||
This server was hosted on `www.multiplayerpiano.com:80` until some time in 2019-2020, when it was upgraded to https and moved to `www.multiplayerpiano.dev:443`.
|
The only other people known to be associated in the development were chacha and
|
||||||
|
This server was hosted on Linode under the domain `www.multiplayerpiano.com:80` until some time in 2019-2020, when it was upgraded to https and moved to `www.multiplayerpiano.com:443`.
|
||||||
|
|
||||||
After that point, in late 2020, the rights to the site were sold to some user who later revealed themselves as "jacored", but they have been unhelpful in all regards.
|
After that point, in late 2020, the rights to the site were sold to some user who later revealed themselves as "jacored", but they have been unhelpful in all regards.
|
||||||
As much as I would like to make peace with them, I have decided they are simply not worth it due to neglecting my help in the past, and threatening to sue me for alleged DDOSing or copyright infringement,
|
As much as I would like to make peace with them, I have decided they are simply not worth it due to neglecting my help in the past, and threatening to sue me for alleged DDOSing or copyright infringement,
|
||||||
call the police, and even get some of my good friends arrested. So... past 2020, we don't have much information about the server's changes.
|
call my local police, and even threatened to get some of my good developer friends arrested. So... past 2020, we don't have much information about the server's changes.
|
||||||
|
|
||||||
Due to those reasons, I have decided to deem the original server 2021-onwards as "jacored's server" or similar.
|
Due to those reasons, I have decided to deem the original server 2021-onwards as "jacored's server" or similar.
|
||||||
This is because it is a lot less stable and less like Brandon's original efforts to keep things running smoothly.
|
This is because it is a lot less stable and less like Brandon's original efforts to keep things running smoothly, and there is a transition point where it was no longer running on Linode.
|
||||||
|
|
||||||
Somewhere around 2015-2017, there was another server developed by nagalun (aka Ming) called `multiplayerpian-server` on GitHub.
|
Somewhere around 2015-2017, there was another server developed by nagalun (aka Ming) called `multiplayerpian-server` on GitHub.
|
||||||
This server was written in C++ and was largely based on Brandon's server.
|
This server was written in C++ and was largely based on Brandon's server.
|
||||||
|
@ -228,7 +255,7 @@ My fork was hosted at `mpp.hri7566.info`.
|
||||||
Also around 2019-2020, I helped Foonix create a server known then as multiplayerpiano.net, hosted at `multiplayerpiano.net`.
|
Also around 2019-2020, I helped Foonix create a server known then as multiplayerpiano.net, hosted at `multiplayerpiano.net`.
|
||||||
This server was heavily based on my fork of BopItFreak's server, but it slightly diverged when I added features to each site.
|
This server was heavily based on my fork of BopItFreak's server, but it slightly diverged when I added features to each site.
|
||||||
The site was renamed to `multiplayerpiano.dev` due to lack of care for domain maintenance on Foonix's part.
|
The site was renamed to `multiplayerpiano.dev` due to lack of care for domain maintenance on Foonix's part.
|
||||||
**This is where the `dev` in the name of this project comes from.**
|
**Since the original server for this site was called `mpp-server-dev`, this is where the `dev2` in the name of this project comes from.**
|
||||||
|
|
||||||
In August 2020, a server was developed by a user named aeiou (now known as LapisHusky) called MPPClone, hosted at `mppclone.com`.
|
In August 2020, a server was developed by a user named aeiou (now known as LapisHusky) called MPPClone, hosted at `mppclone.com`.
|
||||||
This server was eventually handed off to multiple other users, and is still up and running to this day at `multiplayerpiano.net`.
|
This server was eventually handed off to multiple other users, and is still up and running to this day at `multiplayerpiano.net`.
|
||||||
|
@ -238,7 +265,7 @@ server and the frontend to keep moderation and user experience in check.
|
||||||
Due to this, I have decided to implement most features from the MPP.net server here.
|
Due to this, I have decided to implement most features from the MPP.net server here.
|
||||||
**None of the source code from MPP.net is used in this repository.**
|
**None of the source code from MPP.net is used in this repository.**
|
||||||
|
|
||||||
Around 2021-2024, multiple servers were developed by Someone8448 called `smnmpp` or similar.
|
Around 2021-2024, multiple servers in direct correlation with each other were developed by Someone8448 called `smnmpp` or similar.
|
||||||
These servers were closed source and roughly based on MPP.net.
|
These servers were closed source and roughly based on MPP.net.
|
||||||
|
|
||||||
Other servers were developed and forked by other users, but none of them are necessarily popular enough to be used as a reference.
|
Other servers were developed and forked by other users, but none of them are necessarily popular enough to be used as a reference.
|
||||||
|
@ -297,10 +324,11 @@ Someone8448 also implemented their own antibot system, but there are no plans to
|
||||||
|
|
||||||
### Color changing
|
### Color changing
|
||||||
|
|
||||||
Although this feature is likely self-explanatory, it is worth mentioning due to the fact it isn't enabled on MPP.com.
|
Although this feature is likely self-explanatory, this is worth mentioning due to the fact this isn't enabled on MPP.com's server.
|
||||||
Most other servers including MPP.net allow color changing.
|
Most other servers including MPP.net allow color changing.
|
||||||
|
|
||||||
Color changing is simply whether clients have the ability to change their color in the `userset` message.
|
Color changing is simply whether clients have the ability to change their color in the `userset` message.
|
||||||
|
The original server was capable of controlling the served HTML for the modal UI related to changing user settings.
|
||||||
|
|
||||||
### Chat filtering
|
### Chat filtering
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,13 @@
|
||||||
|
owner:
|
||||||
|
- clearChat
|
||||||
|
- vanish
|
||||||
|
- chsetAnywhere
|
||||||
|
- chownAnywhere
|
||||||
|
- usersetOthers
|
||||||
|
- siteBan
|
||||||
|
- siteBanAnyReason
|
||||||
|
- siteBanAnyDuration
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
- clearChat
|
- clearChat
|
||||||
- vanish
|
- vanish
|
||||||
|
@ -7,3 +17,19 @@ admin:
|
||||||
- siteBan
|
- siteBan
|
||||||
- siteBanAnyReason
|
- siteBanAnyReason
|
||||||
- siteBanAnyDuration
|
- siteBanAnyDuration
|
||||||
|
|
||||||
|
mod:
|
||||||
|
- clearChat
|
||||||
|
- vanish
|
||||||
|
- chsetAnywhere
|
||||||
|
- chownAnywhere
|
||||||
|
- usersetOthers
|
||||||
|
- siteBan
|
||||||
|
- siteBanAnyReason
|
||||||
|
|
||||||
|
trialmod:
|
||||||
|
- clearChat
|
||||||
|
- vanish
|
||||||
|
- chsetAnywhere
|
||||||
|
- chownAnywhere
|
||||||
|
- siteBan
|
||||||
|
|
|
@ -1,44 +1,33 @@
|
||||||
# Rate limit config file
|
|
||||||
|
|
||||||
# Difference between rate limits and rate limit chains:
|
|
||||||
# Rate limits will not allow anything to be sent until the rate limit interval has passed.
|
|
||||||
# Rate limit chains, on the other hand, will allow messages to be sent until the rate limit chain's limit has been reached.
|
|
||||||
# This is useful for rate limiting messages that are sent in rapid succession, like note messages.
|
|
||||||
# This is also the basis for note quota, however that is handled in a separate way due to the way it is implemented.
|
|
||||||
|
|
||||||
# Rate limits for normal users.
|
|
||||||
user:
|
user:
|
||||||
# Rate limits
|
|
||||||
normal:
|
normal:
|
||||||
a: 1500 # Chat messages
|
a: 1500
|
||||||
m: 50 # Cursor messages
|
m: 50
|
||||||
ch: 1000 # Channel join messages
|
ch: 1000
|
||||||
kickban: 125 # Kickban messages
|
kickban: 125
|
||||||
unban: 125 # Unban messages
|
unban: 125
|
||||||
t: 7.8125 # Ping messages
|
t: 7.8125
|
||||||
+ls: 16.666666666666668 # Channel list subscription messages
|
+ls: 16.666666666666668
|
||||||
-ls: 16.666666666666668 # Channel list unsubscription messages
|
-ls: 16.666666666666668
|
||||||
chown: 2000 # Channel ownership messages
|
chown: 2000
|
||||||
hi: 50 # Handshake messages
|
hi: 50
|
||||||
bye: 50 # Disconnection messages
|
bye: 50
|
||||||
devices: 50 # MIDI device messages
|
devices: 50
|
||||||
admin message: 50 # Admin passthrough messages
|
admin message: 50
|
||||||
|
+custom: 16.666666666666668
|
||||||
# Rate limit chains
|
-custom: 16.666666666666668
|
||||||
chains:
|
chains:
|
||||||
userset: # Username/color update messages
|
userset:
|
||||||
interval: 1800000
|
interval: 1800000
|
||||||
num: 1000
|
num: 1000
|
||||||
chset: # Channel settings messages
|
chset:
|
||||||
interval: 1800000
|
interval: 1800000
|
||||||
num: 1024
|
num: 1024
|
||||||
n: # Note messages
|
n:
|
||||||
# TODO is this correct?
|
interval: 1000
|
||||||
|
num: 512
|
||||||
|
custom:
|
||||||
interval: 1000
|
interval: 1000
|
||||||
num: 512
|
num: 512
|
||||||
|
|
||||||
# The other rate limits are like the above messages, but for other types of users.
|
|
||||||
# Rate limits for users with a crown.
|
|
||||||
crown:
|
crown:
|
||||||
normal:
|
normal:
|
||||||
a: 600
|
a: 600
|
||||||
|
@ -54,6 +43,8 @@ crown:
|
||||||
bye: 50
|
bye: 50
|
||||||
devices: 50
|
devices: 50
|
||||||
admin message: 50
|
admin message: 50
|
||||||
|
+custom: 16.666666666666668
|
||||||
|
-custom: 16.666666666666668
|
||||||
chains:
|
chains:
|
||||||
userset:
|
userset:
|
||||||
interval: 1800000
|
interval: 1800000
|
||||||
|
@ -64,8 +55,9 @@ crown:
|
||||||
n:
|
n:
|
||||||
interval: 1000
|
interval: 1000
|
||||||
num: 512
|
num: 512
|
||||||
|
custom:
|
||||||
# Rate limits for admins.
|
interval: 1000
|
||||||
|
num: 512
|
||||||
admin:
|
admin:
|
||||||
normal:
|
normal:
|
||||||
a: 120
|
a: 120
|
||||||
|
@ -81,6 +73,8 @@ admin:
|
||||||
bye: 50
|
bye: 50
|
||||||
devices: 50
|
devices: 50
|
||||||
admin message: 16.666666666666668
|
admin message: 16.666666666666668
|
||||||
|
+custom: 8.333333333333334
|
||||||
|
-custom: 8.333333333333334
|
||||||
chains:
|
chains:
|
||||||
userset:
|
userset:
|
||||||
interval: 500
|
interval: 500
|
||||||
|
@ -91,3 +85,6 @@ admin:
|
||||||
n:
|
n:
|
||||||
interval: 50
|
interval: 50
|
||||||
num: 512
|
num: 512
|
||||||
|
custom:
|
||||||
|
interval: 60000
|
||||||
|
num: 20000
|
||||||
|
|
|
@ -11,7 +11,7 @@ defaultFlags:
|
||||||
|
|
||||||
# Whether or not to allow users to change their color.
|
# Whether or not to allow users to change their color.
|
||||||
# Based on some reports, the MPP.com server stopped allowing this around 2016.
|
# Based on some reports, the MPP.com server stopped allowing this around 2016.
|
||||||
enableColorChanging: false
|
enableColorChanging: true
|
||||||
|
|
||||||
# Whether to allow custom data inside note messages.
|
# Whether to allow custom data inside note messages.
|
||||||
# This was in the original server, but not in MPP.net's server do to stricter sanitization.
|
# This was in the original server, but not in MPP.net's server do to stricter sanitization.
|
||||||
|
@ -20,7 +20,7 @@ enableCustomNoteData: true
|
||||||
|
|
||||||
# Whether or not to enable tags that are sent publicly.
|
# Whether or not to enable tags that are sent publicly.
|
||||||
# This won't prevent admins from changing tags internally, but they will not be sent to clients if set to false.
|
# This won't prevent admins from changing tags internally, but they will not be sent to clients if set to false.
|
||||||
enableTags: true
|
enableTags: false
|
||||||
|
|
||||||
# This is the user data that the server will use to send admin chat messages with.
|
# This is the user data that the server will use to send admin chat messages with.
|
||||||
# This is a feature available on MPP.com, but was unknown to the MPP.net developers, therefore not implemented on MPP.net.
|
# This is a feature available on MPP.com, but was unknown to the MPP.net developers, therefore not implemented on MPP.net.
|
||||||
|
@ -37,7 +37,7 @@ enableAdminEval: true
|
||||||
# The token validation scheme. Valid values are "none", "jwt" and "uuid".
|
# The token validation scheme. Valid values are "none", "jwt" and "uuid".
|
||||||
# This server will still validate existing tokens generated with other schemes if not set to "none", mimicking MPP.net's server.
|
# This server will still validate existing tokens generated with other schemes if not set to "none", mimicking MPP.net's server.
|
||||||
# This is set to "none" by default because MPP.com does not have a token system.
|
# This is set to "none" by default because MPP.com does not have a token system.
|
||||||
tokenAuth: jwt
|
tokenAuth: none
|
||||||
|
|
||||||
# The browser challenge scheme. Valid options are "none", "obf" and "basic".
|
# The browser challenge scheme. Valid options are "none", "obf" and "basic".
|
||||||
# This is to change what is sent in the "b" message.
|
# This is to change what is sent in the "b" message.
|
||||||
|
@ -45,7 +45,7 @@ tokenAuth: jwt
|
||||||
# "obf" will sent an obfuscated function to the client,
|
# "obf" will sent an obfuscated function to the client,
|
||||||
# and "basic" will just send a simple function that expects a boolean.
|
# and "basic" will just send a simple function that expects a boolean.
|
||||||
# FIXME Note that "obf" is not implemented yet, and has undefined behavior.
|
# FIXME Note that "obf" is not implemented yet, and has undefined behavior.
|
||||||
browserChallenge: basic
|
browserChallenge: none
|
||||||
|
|
||||||
# Scheme for generating user IDs.
|
# Scheme for generating user IDs.
|
||||||
# Valid options are "random", "sha256", "mpp" and "uuid".
|
# Valid options are "random", "sha256", "mpp" and "uuid".
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
"author": "Hri7566",
|
"author": "Hri7566",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/start.ts",
|
||||||
"dev": "bun run src/index.ts --watch"
|
"dev": "bun run src/start.ts --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "5.17.0",
|
"@prisma/client": "5.17.0",
|
||||||
"@t3-oss/env-core": "^0.6.1",
|
"@t3-oss/env-core": "^0.6.1",
|
||||||
"bun-types": "^1.1.27",
|
"bun-types": "^1.1.28",
|
||||||
"commander": "^11.1.0",
|
"commander": "^11.1.0",
|
||||||
"date-holidays": "^3.23.12",
|
"date-holidays": "^3.23.12",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/node": "^20.16.5",
|
"@types/node": "^20.16.5",
|
||||||
"@types/nunjucks": "^3.2.6",
|
"@types/nunjucks": "^3.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
|
2
public
2
public
|
@ -1 +1 @@
|
||||||
Subproject commit 7f58dd80a3cc72a4f141c3a94e561e3f2bf3c5fe
|
Subproject commit 1dc00c7f885ac919a1bda7d4c749d33bd594c42f
|
|
@ -1,16 +1,15 @@
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { Logger } from "../util/Logger";
|
import { Logger } from "../util/Logger";
|
||||||
import type {
|
import type {
|
||||||
ChannelSettingValue,
|
|
||||||
IChannelSettings,
|
IChannelSettings,
|
||||||
ClientEvents,
|
OutgoingSocketEvents,
|
||||||
Participant,
|
IncomingSocketEvents,
|
||||||
ServerEvents,
|
IParticipant,
|
||||||
IChannelInfo,
|
IChannelInfo,
|
||||||
Notification,
|
Notification,
|
||||||
UserFlags,
|
UserFlags,
|
||||||
Tag,
|
Tag,
|
||||||
ChannelFlags as TChannelFlags
|
TChannelFlags
|
||||||
} from "../util/types";
|
} from "../util/types";
|
||||||
import type { Socket } from "../ws/Socket";
|
import type { Socket } from "../ws/Socket";
|
||||||
import { validateChannelSettings } from "./settings";
|
import { validateChannelSettings } from "./settings";
|
||||||
|
@ -32,7 +31,7 @@ import {
|
||||||
getSavedChannel,
|
getSavedChannel,
|
||||||
saveChannel
|
saveChannel
|
||||||
} from "../data/channel";
|
} from "../data/channel";
|
||||||
import { forceloadChannel } from "./forceLoad";
|
import { forceloadChannel } from "./forceload";
|
||||||
|
|
||||||
interface CachedKickban {
|
interface CachedKickban {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -51,13 +50,13 @@ interface ExtraPartData {
|
||||||
flags: UserFlags;
|
flags: UserFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtraPart = Participant & ExtraPartData;
|
type ExtraPart = IParticipant & ExtraPartData;
|
||||||
|
|
||||||
export class Channel extends EventEmitter {
|
export class Channel extends EventEmitter {
|
||||||
private settings: Partial<IChannelSettings>;
|
private settings: Partial<IChannelSettings>;
|
||||||
private ppl = new Array<ExtraPart>();
|
private ppl = new Array<ExtraPart>();
|
||||||
|
|
||||||
public chatHistory = new Array<ClientEvents["a"]>();
|
public chatHistory = new Array<OutgoingSocketEvents["a"]>();
|
||||||
|
|
||||||
private async loadChatHistory() {
|
private async loadChatHistory() {
|
||||||
try {
|
try {
|
||||||
|
@ -92,7 +91,8 @@ export class Channel extends EventEmitter {
|
||||||
const data = {
|
const data = {
|
||||||
id: info._id,
|
id: info._id,
|
||||||
settings: JSON.stringify(info.settings),
|
settings: JSON.stringify(info.settings),
|
||||||
flags: JSON.stringify(this.flags)
|
flags: JSON.stringify(this.flags),
|
||||||
|
forceload: this.stays
|
||||||
};
|
};
|
||||||
|
|
||||||
//this.logger.debug("Channel data to save:", data);
|
//this.logger.debug("Channel data to save:", data);
|
||||||
|
@ -150,6 +150,8 @@ export class Channel extends EventEmitter {
|
||||||
// Copy default settings
|
// Copy default settings
|
||||||
mixin(this.settings, config.defaultSettings);
|
mixin(this.settings, config.defaultSettings);
|
||||||
|
|
||||||
|
if (owner_id) this.settings.owner_id = owner_id;
|
||||||
|
|
||||||
if (!this.isLobby()) {
|
if (!this.isLobby()) {
|
||||||
if (set) {
|
if (set) {
|
||||||
// Copied from changeSettings below
|
// Copied from changeSettings below
|
||||||
|
@ -174,7 +176,8 @@ export class Channel extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are not a lobby, so we must have a crown
|
// We are not a lobby, so we probably have a crown
|
||||||
|
// this.getFlag("no_crown");
|
||||||
this.crown = new Crown();
|
this.crown = new Crown();
|
||||||
|
|
||||||
// ...and, possibly, an owner, too
|
// ...and, possibly, an owner, too
|
||||||
|
@ -203,6 +206,7 @@ export class Channel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private alreadyBound = false;
|
private alreadyBound = false;
|
||||||
|
private destroyTimeout: Timer | undefined;
|
||||||
|
|
||||||
private bindEventListeners() {
|
private bindEventListeners() {
|
||||||
if (this.alreadyBound) return;
|
if (this.alreadyBound) return;
|
||||||
|
@ -212,6 +216,12 @@ export class Channel extends EventEmitter {
|
||||||
this.logger.info("Loaded chat history");
|
this.logger.info("Loaded chat history");
|
||||||
|
|
||||||
this.on("update", (self, uuid) => {
|
this.on("update", (self, uuid) => {
|
||||||
|
// Propogate channel flags intended to be updated
|
||||||
|
if (typeof this.flags.owner_id === "string") {
|
||||||
|
this.settings.owner_id = this.flags.owner_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.logger.debug("update");
|
||||||
// Send updated info
|
// Send updated info
|
||||||
for (const socket of socketsByUUID.values()) {
|
for (const socket of socketsByUUID.values()) {
|
||||||
for (const p of this.ppl) {
|
for (const p of this.ppl) {
|
||||||
|
@ -231,31 +241,41 @@ export class Channel extends EventEmitter {
|
||||||
|
|
||||||
if (this.ppl.length === 0 && !this.stays) {
|
if (this.ppl.length === 0 && !this.stays) {
|
||||||
if (config.channelDestroyTimeout) {
|
if (config.channelDestroyTimeout) {
|
||||||
setTimeout(() => {
|
this.destroyTimeout = setTimeout(() => {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}, config.channelDestroyTimeout);
|
}, config.channelDestroyTimeout);
|
||||||
} else {
|
} else {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.ppl.length > 0 &&
|
||||||
|
typeof this.destroyTimeout !== "undefined"
|
||||||
|
) {
|
||||||
|
clearTimeout(this.destroyTimeout);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const BANNED_WORDS = ["AMIGHTYWIND", "CHECKLYHQ"];
|
const BANNED_WORDS = ["AMIGHTYWIND", "CHECKLYHQ"];
|
||||||
|
|
||||||
this.on("a", async (msg: ServerEvents["a"], socket: Socket) => {
|
this.on("a", async (msg: IncomingSocketEvents["a"], socket: Socket) => {
|
||||||
try {
|
try {
|
||||||
if (typeof msg.message !== "string") return;
|
if (typeof msg.message !== "string") return;
|
||||||
|
|
||||||
const userFlags = socket.getUserFlags();
|
const userFlags = socket.getUserFlags();
|
||||||
|
let overrideColor: string | undefined;
|
||||||
|
|
||||||
if (userFlags) {
|
if (userFlags) {
|
||||||
if (userFlags.cant_chat == 1) return;
|
if (userFlags.cant_chat === 1) return;
|
||||||
if (userFlags.chat_curse_1 == 1)
|
if (userFlags.chat_curse_1 === 1)
|
||||||
msg.message = msg.message
|
msg.message = msg.message
|
||||||
.replace(/[aeiu]/g, "o")
|
.replace(/[aeiu]/g, "o")
|
||||||
.replace(/[AEIU]/g, "O");
|
.replace(/[AEIU]/g, "O");
|
||||||
if (userFlags.chat_curse_2 == 1)
|
if (userFlags.chat_curse_2 === 1)
|
||||||
msg.message = spoop_text(msg.message);
|
msg.message = spoop_text(msg.message);
|
||||||
|
if (typeof userFlags.chat_color === "string")
|
||||||
|
overrideColor = userFlags.chat_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.settings.chat) return;
|
if (!this.settings.chat) return;
|
||||||
|
@ -282,15 +302,18 @@ export class Channel extends EventEmitter {
|
||||||
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
|
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const part = socket.getParticipant() as Participant;
|
const part = socket.getParticipant() as IParticipant;
|
||||||
|
|
||||||
const outgoing: ClientEvents["a"] = {
|
const outgoing: OutgoingSocketEvents["a"] = {
|
||||||
m: "a",
|
m: "a",
|
||||||
a: msg.message,
|
a: msg.message,
|
||||||
t: Date.now(),
|
t: Date.now(),
|
||||||
p: part
|
p: part
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (typeof overrideColor !== "undefined")
|
||||||
|
outgoing.p.color = overrideColor;
|
||||||
|
|
||||||
this.sendArray([outgoing]);
|
this.sendArray([outgoing]);
|
||||||
this.chatHistory.push(outgoing);
|
this.chatHistory.push(outgoing);
|
||||||
await saveChatHistory(this.getID(), this.chatHistory);
|
await saveChatHistory(this.getID(), this.chatHistory);
|
||||||
|
@ -548,7 +571,7 @@ export class Channel extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public join(socket: Socket, force = false): void {
|
public join(socket: Socket, force = false): void {
|
||||||
if (this.isDestroyed()) return;
|
if (this.isDestroyed()) return;
|
||||||
const part = socket.getParticipant() as Participant;
|
const part = socket.getParticipant() as IParticipant;
|
||||||
|
|
||||||
let hasChangedChannel = false;
|
let hasChangedChannel = false;
|
||||||
|
|
||||||
|
@ -662,7 +685,7 @@ export class Channel extends EventEmitter {
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
// Give the crown back
|
// Give the crown back
|
||||||
this.giveCrown(p, true, false);
|
this.giveCrown(p, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -708,9 +731,13 @@ export class Channel extends EventEmitter {
|
||||||
// Broadcast a channel update so everyone subscribed to the channel list can see the new user count
|
// Broadcast a channel update so everyone subscribed to the channel list can see the new user count
|
||||||
//this.emit("update", this, socket.getUUID());
|
//this.emit("update", this, socket.getUUID());
|
||||||
//this.logger.debug("Update from join");
|
//this.logger.debug("Update from join");
|
||||||
this.emit("update", this);
|
// this.emit("update", this);
|
||||||
|
socket.sendChannelUpdate(this.getInfo(), this.getParticipantList());
|
||||||
|
|
||||||
//this.logger.debug("Settings:", this.settings);
|
//this.logger.debug("Settings:", this.settings);
|
||||||
|
if (this.settings.owner_id === part._id) {
|
||||||
|
this.giveCrown(part, true, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -719,7 +746,8 @@ export class Channel extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public leave(socket: Socket) {
|
public leave(socket: Socket) {
|
||||||
// this.logger.debug("Leave called");
|
// this.logger.debug("Leave called");
|
||||||
const part = socket.getParticipant() as Participant;
|
const part = socket.getParticipant();
|
||||||
|
if (!part) return;
|
||||||
|
|
||||||
let dupeCount = 0;
|
let dupeCount = 0;
|
||||||
for (const s of socketsByUUID.values()) {
|
for (const s of socketsByUUID.values()) {
|
||||||
|
@ -735,27 +763,31 @@ export class Channel extends EventEmitter {
|
||||||
if (dupeCount === 1) {
|
if (dupeCount === 1) {
|
||||||
const p = this.ppl.find(p => p.id === socket.getParticipantID());
|
const p = this.ppl.find(p => p.id === socket.getParticipantID());
|
||||||
|
|
||||||
|
let hadCrown = false;
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
this.ppl.splice(this.ppl.indexOf(p), 1);
|
this.ppl.splice(this.ppl.indexOf(p), 1);
|
||||||
|
|
||||||
if (this.crown) {
|
if (this.crown) {
|
||||||
if (this.crown.participantId === p.id) {
|
if (this.crown.participantId === p.id) {
|
||||||
// Channel owner left, reset crown timeout
|
// Channel owner left, reset crown timeout
|
||||||
|
hadCrown = true;
|
||||||
this.chown();
|
this.chown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast bye
|
// Broadcast bye
|
||||||
this.sendArray([
|
if (!hadCrown)
|
||||||
{
|
this.sendArray([
|
||||||
m: "bye",
|
{
|
||||||
p: part.id
|
m: "bye",
|
||||||
}
|
p: part.id
|
||||||
]);
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
//this.logger.debug("Update from leave");
|
//this.logger.debug("Update from leave");
|
||||||
this.emit("update", this);
|
// this.emit("update", this);
|
||||||
} else {
|
} else {
|
||||||
for (const p of this.ppl) {
|
for (const p of this.ppl) {
|
||||||
if (!p.uuids.includes(socket.getUUID())) continue;
|
if (!p.uuids.includes(socket.getUUID())) continue;
|
||||||
|
@ -847,8 +879,8 @@ export class Channel extends EventEmitter {
|
||||||
* Send messages to everyone in this channel
|
* Send messages to everyone in this channel
|
||||||
* @param arr List of events to send to clients
|
* @param arr List of events to send to clients
|
||||||
*/
|
*/
|
||||||
public sendArray<EventID extends keyof ClientEvents>(
|
public sendArray<EventID extends keyof OutgoingSocketEvents>(
|
||||||
arr: ClientEvents[EventID][],
|
arr: OutgoingSocketEvents[EventID][],
|
||||||
blockPartID?: string
|
blockPartID?: string
|
||||||
) {
|
) {
|
||||||
const sentSocketIDs = new Array<string>();
|
const sentSocketIDs = new Array<string>();
|
||||||
|
@ -875,7 +907,7 @@ export class Channel extends EventEmitter {
|
||||||
* @param socket Socket that is sending notes
|
* @param socket Socket that is sending notes
|
||||||
* @returns undefined
|
* @returns undefined
|
||||||
*/
|
*/
|
||||||
public playNotes(msg: ServerEvents["n"], socket?: Socket) {
|
public playNotes(msg: IncomingSocketEvents["n"], socket?: Socket) {
|
||||||
if (this.isDestroyed()) return;
|
if (this.isDestroyed()) return;
|
||||||
let pianoPartID = usersConfig.adminParticipant.id;
|
let pianoPartID = usersConfig.adminParticipant.id;
|
||||||
|
|
||||||
|
@ -883,9 +915,23 @@ export class Channel extends EventEmitter {
|
||||||
const part = socket.getParticipant();
|
const part = socket.getParticipant();
|
||||||
if (!part) return;
|
if (!part) return;
|
||||||
pianoPartID = part.id;
|
pianoPartID = part.id;
|
||||||
|
|
||||||
|
const flags = socket.getUserFlags();
|
||||||
|
|
||||||
|
if (flags) {
|
||||||
|
// Is crownsolo on?
|
||||||
|
if (
|
||||||
|
this.settings.crownsolo &&
|
||||||
|
this.crown &&
|
||||||
|
!(flags.admin || flags.mod)
|
||||||
|
) {
|
||||||
|
// Do they have the crown?
|
||||||
|
if (part.id !== this.crown.participantId) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientMsg: ClientEvents["n"] = {
|
const clientMsg: OutgoingSocketEvents["n"] = {
|
||||||
m: "n",
|
m: "n",
|
||||||
n: msg.n,
|
n: msg.n,
|
||||||
t: msg.t,
|
t: msg.t,
|
||||||
|
@ -921,6 +967,7 @@ export class Channel extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public destroy() {
|
public destroy() {
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
|
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
|
|
||||||
if (this.ppl.length > 0) {
|
if (this.ppl.length > 0) {
|
||||||
|
@ -948,7 +995,7 @@ export class Channel extends EventEmitter {
|
||||||
* Change ownership (don't forget to use crown.canBeSetBy if you're letting a user call this)
|
* Change ownership (don't forget to use crown.canBeSetBy if you're letting a user call this)
|
||||||
* @param part Participant to give crown to (or undefined to drop crown)
|
* @param part Participant to give crown to (or undefined to drop crown)
|
||||||
*/
|
*/
|
||||||
public chown(part?: Participant) {
|
public chown(part?: IParticipant) {
|
||||||
if (this.crown) {
|
if (this.crown) {
|
||||||
if (part) {
|
if (part) {
|
||||||
this.giveCrown(part);
|
this.giveCrown(part);
|
||||||
|
@ -963,7 +1010,7 @@ export class Channel extends EventEmitter {
|
||||||
* @param part Participant to give crown to
|
* @param part Participant to give crown to
|
||||||
* @param force Whether or not to force-create a crown (useful for lobbies)
|
* @param force Whether or not to force-create a crown (useful for lobbies)
|
||||||
*/
|
*/
|
||||||
public giveCrown(part: Participant, force = false, update = true) {
|
public giveCrown(part: IParticipant, force = false, update = true) {
|
||||||
if (force) {
|
if (force) {
|
||||||
if (!this.crown) this.crown = new Crown();
|
if (!this.crown) this.crown = new Crown();
|
||||||
}
|
}
|
||||||
|
@ -974,7 +1021,7 @@ export class Channel extends EventEmitter {
|
||||||
this.crown.time = Date.now();
|
this.crown.time = Date.now();
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
//this.logger.debug("Update from giveCrown");
|
// this.logger.debug("Update from giveCrown");
|
||||||
this.emit("update", this);
|
this.emit("update", this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1040,7 +1087,7 @@ export class Channel extends EventEmitter {
|
||||||
if (!banChannel) return;
|
if (!banChannel) return;
|
||||||
|
|
||||||
// Check if they are on the server at all
|
// Check if they are on the server at all
|
||||||
let bannedPart: Participant | undefined;
|
let bannedPart: IParticipant | undefined;
|
||||||
const bannedUUIDs: string[] = [];
|
const bannedUUIDs: string[] = [];
|
||||||
for (const sock of socketsByUUID.values()) {
|
for (const sock of socketsByUUID.values()) {
|
||||||
if (sock.getUserID() === _id) {
|
if (sock.getUserID() === _id) {
|
||||||
|
@ -1230,7 +1277,7 @@ export class Channel extends EventEmitter {
|
||||||
* @param msg Chat message event to send
|
* @param msg Chat message event to send
|
||||||
* @param p Participant who is "sending the message"
|
* @param p Participant who is "sending the message"
|
||||||
**/
|
**/
|
||||||
public async sendChat(msg: ServerEvents["a"], p: Participant) {
|
public async sendChat(msg: IncomingSocketEvents["a"], p: IParticipant) {
|
||||||
if (!msg.message) return;
|
if (!msg.message) return;
|
||||||
|
|
||||||
if (msg.message.length > 512) return;
|
if (msg.message.length > 512) return;
|
||||||
|
@ -1241,7 +1288,7 @@ export class Channel extends EventEmitter {
|
||||||
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
|
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const outgoing: ClientEvents["a"] = {
|
const outgoing: OutgoingSocketEvents["a"] = {
|
||||||
m: "a",
|
m: "a",
|
||||||
a: msg.message,
|
a: msg.message,
|
||||||
t: Date.now(),
|
t: Date.now(),
|
||||||
|
@ -1283,6 +1330,8 @@ export class Channel extends EventEmitter {
|
||||||
val: TChannelFlags[K]
|
val: TChannelFlags[K]
|
||||||
) {
|
) {
|
||||||
this.flags[key] = val;
|
this.flags[key] = val;
|
||||||
|
this.logger.debug("Updating channel flag " + key + " to", val);
|
||||||
|
this.emit("update", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1345,6 +1394,11 @@ export class Channel extends EventEmitter {
|
||||||
).toFixed(2)}M`
|
).toFixed(2)}M`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setForceload(enable: boolean) {
|
||||||
|
this.stays = enable;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Channel;
|
export default Channel;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Participant, Vector2 } from "../util/types";
|
import { IParticipant, Vector2 } from "../util/types";
|
||||||
import { Socket } from "../ws/Socket";
|
import { Socket } from "../ws/Socket";
|
||||||
|
|
||||||
// shiny hat
|
// shiny hat
|
||||||
|
|
|
@ -14,9 +14,15 @@ const logger = new Logger("Channel Forceloader");
|
||||||
*/
|
*/
|
||||||
export function forceloadChannel(id: string) {
|
export function forceloadChannel(id: string) {
|
||||||
try {
|
try {
|
||||||
logger.info("Forceloading", id);
|
const existing = ChannelList.getChannel(id);
|
||||||
new Channel(id, undefined, undefined, undefined, true);
|
if (existing) {
|
||||||
return true;
|
logger.info("Keeping", id, "forceloaded");
|
||||||
|
existing.setForceload(true);
|
||||||
|
} else {
|
||||||
|
logger.info("Forceloading", id);
|
||||||
|
new Channel(id, undefined, undefined, undefined, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { ChannelList } from "~/channel/ChannelList";
|
||||||
|
import { bus } from "./bus";
|
||||||
|
import type { OutgoingSocketEvents, IncomingSocketEvents } from "~/util/types";
|
||||||
|
import { socketsByUUID, type Socket } from "~/ws/Socket";
|
||||||
|
|
||||||
|
export function loadBehaviors() {
|
||||||
|
bus.on("hamburger", () => {
|
||||||
|
for (const ch of ChannelList.getList()) {
|
||||||
|
ch.sendChatAdmin("🍔");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.on("ls", () => {});
|
||||||
|
|
||||||
|
bus.on("custom", (msg: IncomingSocketEvents["custom"], sender: Socket) => {
|
||||||
|
if (typeof msg !== "object") return;
|
||||||
|
if (typeof msg.data === "undefined") return;
|
||||||
|
if (typeof msg.target !== "object") return;
|
||||||
|
if (typeof msg.target.mode !== "string") return;
|
||||||
|
if (
|
||||||
|
typeof msg.target.global !== "undefined" &&
|
||||||
|
typeof msg.target.global !== "boolean"
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (const receiver of socketsByUUID.values()) {
|
||||||
|
if (receiver.isDestroyed()) return;
|
||||||
|
if (!receiver.isCustomSubbed()) return;
|
||||||
|
|
||||||
|
if (sender.isDestroyed()) return;
|
||||||
|
if (!sender.isCustomSubbed()) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
msg.target.global !== true ||
|
||||||
|
typeof msg.target.global === "undefined"
|
||||||
|
) {
|
||||||
|
const ch = sender.getCurrentChannel();
|
||||||
|
if (!ch) return;
|
||||||
|
|
||||||
|
const ch2 = receiver.getCurrentChannel();
|
||||||
|
if (!ch2) return;
|
||||||
|
|
||||||
|
if (ch.getID() !== ch2.getID()) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.target.mode === "id") {
|
||||||
|
if (typeof msg.target.id !== "string") return;
|
||||||
|
|
||||||
|
if (receiver.getUserID() === msg.target.id) {
|
||||||
|
receiver.sendArray([
|
||||||
|
{
|
||||||
|
m: "custom",
|
||||||
|
data: msg.data,
|
||||||
|
p: sender.getUserID()
|
||||||
|
} as OutgoingSocketEvents["custom"]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if (msg.target.mode === "ids") {
|
||||||
|
if (typeof msg.target.ids !== "object") return;
|
||||||
|
if (!Array.isArray(msg.target.ids)) return;
|
||||||
|
|
||||||
|
if (msg.target.ids.includes(receiver.getUserID())) {
|
||||||
|
receiver.sendArray([
|
||||||
|
{
|
||||||
|
m: "custom",
|
||||||
|
data: msg.data,
|
||||||
|
p: sender.getUserID()
|
||||||
|
} as OutgoingSocketEvents["custom"]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if (msg.target.mode === "subscribed") {
|
||||||
|
receiver.sendArray([
|
||||||
|
{
|
||||||
|
m: "custom",
|
||||||
|
data: msg.data,
|
||||||
|
p: sender.getUserID()
|
||||||
|
} as OutgoingSocketEvents["custom"]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.emit("ready");
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
class EventBus extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bus = new EventBus();
|
18
src/index.ts
18
src/index.ts
|
@ -13,29 +13,33 @@
|
||||||
// There are a lot of unhinged bs comments in this repo
|
// There are a lot of unhinged bs comments in this repo
|
||||||
// Pay no attention to the ones that cuss you out
|
// Pay no attention to the ones that cuss you out
|
||||||
|
|
||||||
// If you don't load the server first, bun will literally segfault
|
import { loadForcedStartupChannels } from "./channel/forceload";
|
||||||
import "./ws/server";
|
|
||||||
import { loadForcedStartupChannels } from "./channel/forceLoad";
|
|
||||||
import { Logger } from "./util/Logger";
|
import { Logger } from "./util/Logger";
|
||||||
// docker hates this next one
|
// docker hates this next one
|
||||||
import { startReadline } from "./util/readline";
|
import { startReadline } from "./util/readline";
|
||||||
import { loadDefaultPermissions } from "./data/permissions";
|
import { loadDefaultPermissions } from "./data/permissions";
|
||||||
|
import { loadBehaviors } from "./event/behaviors";
|
||||||
|
import { startHTTPServer } from "./ws/server";
|
||||||
|
|
||||||
// wrapper for some reason
|
// wrapper for some reason
|
||||||
export function startServer() {
|
export function startServer() {
|
||||||
// Let's construct an entire object just for one thing to be printed
|
|
||||||
// and then keep it in memory for the entirety of runtime
|
|
||||||
const logger = new Logger("Main");
|
const logger = new Logger("Main");
|
||||||
logger.info("Forceloading startup channels...");
|
logger.info("Forceloading startup channels...");
|
||||||
loadForcedStartupChannels();
|
loadForcedStartupChannels();
|
||||||
|
logger.info("Finished forceloading");
|
||||||
|
|
||||||
|
logger.info("Loading behaviors...");
|
||||||
|
loadBehaviors();
|
||||||
|
logger.info("Finished loading behaviors");
|
||||||
|
|
||||||
loadDefaultPermissions();
|
loadDefaultPermissions();
|
||||||
|
|
||||||
// Break the console
|
// Break the console
|
||||||
|
logger.info("Starting REPL");
|
||||||
startReadline();
|
startReadline();
|
||||||
|
|
||||||
// Nevermind, two things are printed
|
startHTTPServer();
|
||||||
logger.info("Ready");
|
logger.info("Ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer();
|
// startServer();
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { startServer } from ".";
|
||||||
|
|
||||||
|
startServer();
|
|
@ -13,7 +13,7 @@ import { Logger } from "./Logger";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class ConfigManager {
|
export class ConfigManager {
|
||||||
// public static configCache = new Map<string, unknown>();
|
public static configCache = new Map<string, unknown>();
|
||||||
public static logger: Logger;
|
public static logger: Logger;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
@ -32,10 +32,15 @@ export class ConfigManager {
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
* @param configPath Path to load config from
|
* @param configPath Path to load config from
|
||||||
* @param defaultConfig Config to use if none is present (will save to path if used)
|
* @param defaultConfig Config to use if none is present (will save to path if used, see saveDefault)
|
||||||
|
* @param saveDefault Whether to save the default config if none is present
|
||||||
* @returns Parsed YAML config
|
* @returns Parsed YAML config
|
||||||
*/
|
*/
|
||||||
public static loadConfig<T>(configPath: string, defaultConfig: T): T {
|
public static loadConfig<T>(
|
||||||
|
configPath: string,
|
||||||
|
defaultConfig: T,
|
||||||
|
saveDefault = true
|
||||||
|
): T {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
// Config exists?
|
// Config exists?
|
||||||
|
@ -73,38 +78,43 @@ export class ConfigManager {
|
||||||
mix(config, defRecord);
|
mix(config, defRecord);
|
||||||
|
|
||||||
// Save config if modified
|
// Save config if modified
|
||||||
if (changed) this.writeConfig(configPath, config);
|
if (saveDefault && changed) this.writeConfig(configPath, config);
|
||||||
|
|
||||||
// File contents changed callback
|
if (!this.configCache.has(configPath)) {
|
||||||
// const watcher = watchFile(configPath, () => {
|
// File contents changed callback
|
||||||
// this.logger.info(
|
const watcher = watchFile(configPath, () => {
|
||||||
// "Reloading config due to changes:",
|
this.logger.info(
|
||||||
// configPath
|
"Reloading config due to changes:",
|
||||||
// );
|
configPath
|
||||||
// this.loadConfig(configPath, defaultConfig);
|
);
|
||||||
// });
|
|
||||||
|
|
||||||
// this.configCache.set(configPath, config);
|
this.loadConfig(configPath, defaultConfig, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// return this.getConfigProxy<T>(configPath);
|
this.configCache.set(configPath, config);
|
||||||
return config;
|
|
||||||
|
return this.getConfigProxy<T>(configPath);
|
||||||
|
// return config;
|
||||||
} else {
|
} else {
|
||||||
// Write default config to disk and use that
|
// Write default config to disk and use that
|
||||||
//logger.warn(`Config file "${configPath}" not found, writing default config to disk`);
|
//logger.warn(`Config file "${configPath}" not found, writing default config to disk`);
|
||||||
this.writeConfig(configPath, defaultConfig);
|
if (saveDefault) this.writeConfig(configPath, defaultConfig);
|
||||||
|
|
||||||
// File contents changed callback
|
if (!this.configCache.has(configPath)) {
|
||||||
// const watcher = watchFile(configPath, () => {
|
// File contents changed callback
|
||||||
// this.logger.info(
|
const watcher = watchFile(configPath, () => {
|
||||||
// "Reloading config due to changes:",
|
this.logger.info(
|
||||||
// configPath
|
"Reloading config due to changes:",
|
||||||
// );
|
configPath
|
||||||
// this.loadConfig(configPath, defaultConfig);
|
);
|
||||||
// });
|
this.loadConfig(configPath, defaultConfig, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// this.configCache.set(configPath, defaultConfig);
|
this.configCache.set(configPath, defaultConfig);
|
||||||
// return this.getConfigProxy<T>(configPath);
|
return this.getConfigProxy<T>(configPath);
|
||||||
return defaultConfig;
|
// return defaultConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,24 +138,24 @@ export class ConfigManager {
|
||||||
* @param configPath Path to config file
|
* @param configPath Path to config file
|
||||||
* @returns Config proxy object
|
* @returns Config proxy object
|
||||||
*/
|
*/
|
||||||
// protected static getConfigProxy<T>(configPath: string) {
|
protected static getConfigProxy<T>(configPath: string) {
|
||||||
// const self = this;
|
const self = this;
|
||||||
|
|
||||||
// return new Proxy(
|
return new Proxy(
|
||||||
// {},
|
{},
|
||||||
// {
|
{
|
||||||
// get(_target: unknown, name: string) {
|
get(_target: unknown, name: string) {
|
||||||
// // Get the updated in-memory version of the config
|
// Get the updated in-memory version of the config
|
||||||
// const config = self.configCache.get(configPath) as T;
|
const config = self.configCache.get(configPath) as T;
|
||||||
|
|
||||||
// if (config) {
|
if (config) {
|
||||||
// if (config.hasOwnProperty(name))
|
if (config.hasOwnProperty(name))
|
||||||
// return (config as Record<string, unknown>)[
|
return (config as Record<string, unknown>)[
|
||||||
// name
|
name
|
||||||
// ] as T[keyof T];
|
] as T[keyof T];
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// ) as T;
|
) as T;
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,13 @@ declare type UserFlags = Partial<{
|
||||||
mod: number;
|
mod: number;
|
||||||
admin: number;
|
admin: number;
|
||||||
vanish: number;
|
vanish: number;
|
||||||
|
chat_color: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ChannelFlags = Partial<{
|
type TChannelFlags = Partial<{
|
||||||
limit: number;
|
limit: number;
|
||||||
owner_id: string;
|
owner_id: string;
|
||||||
|
no_crown: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
declare interface Tag {
|
declare interface Tag {
|
||||||
|
@ -40,7 +42,7 @@ declare interface User {
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Participant extends User {
|
declare interface IParticipant extends User {
|
||||||
id: string; // participant id (same as user id on mppclone)
|
id: string; // participant id (same as user id on mppclone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +109,7 @@ declare interface Crown {
|
||||||
endPos: Vector2;
|
endPos: Vector2;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface ServerEvents {
|
declare interface IncomingSocketEvents {
|
||||||
hi: {
|
hi: {
|
||||||
m: "hi";
|
m: "hi";
|
||||||
token?: string;
|
token?: string;
|
||||||
|
@ -209,7 +211,7 @@ declare interface ServerEvents {
|
||||||
"admin message": {
|
"admin message": {
|
||||||
m: "admin message";
|
m: "admin message";
|
||||||
password: string;
|
password: string;
|
||||||
msg: ServerEvents[keyof ServerEvents];
|
msg: IncomingSocketEvents[keyof IncomingSocketEvents];
|
||||||
};
|
};
|
||||||
|
|
||||||
b: {
|
b: {
|
||||||
|
@ -279,6 +281,11 @@ declare interface ServerEvents {
|
||||||
_id: string;
|
_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
unforceload: {
|
||||||
|
m: "unforceload";
|
||||||
|
_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
ch_flag: {
|
ch_flag: {
|
||||||
m: "ch_flag";
|
m: "ch_flag";
|
||||||
_id?: string;
|
_id?: string;
|
||||||
|
@ -310,7 +317,7 @@ declare interface ServerEvents {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface ClientEvents {
|
declare interface OutgoingSocketEvents {
|
||||||
a: {
|
a: {
|
||||||
m: "a";
|
m: "a";
|
||||||
a: string;
|
a: string;
|
||||||
|
@ -407,11 +414,11 @@ declare interface ClientEvents {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventID = ServerEvents[keyof ServerEvents]["m"];
|
type EventID = IncomingSocketEvents[keyof IncomingSocketEvents]["m"];
|
||||||
|
|
||||||
declare type ServerEventListener<E extends EventID> = {
|
declare type ServerEventListener<E extends EventID> = {
|
||||||
id: E;
|
id: E;
|
||||||
callback: (msg: ServerEvents[E], socket: Socket) => Promise<void>;
|
callback: (msg: IncomingSocketEvents[E], socket: Socket) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type Vector2<T = number> = {
|
declare type Vector2<T = number> = {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class Gateway {
|
||||||
public isTokenValid = false; // implemented
|
public isTokenValid = false; // implemented
|
||||||
|
|
||||||
// Their user agent, if sent
|
// Their user agent, if sent
|
||||||
public userAgent = ""; // TODO
|
public userAgent = ""; // partially implemented
|
||||||
|
|
||||||
// Whether they have moved their cursor
|
// Whether they have moved their cursor
|
||||||
public hasCursorMoved = false; // implemented
|
public hasCursorMoved = false; // implemented
|
||||||
|
@ -65,6 +65,12 @@ export class Gateway {
|
||||||
// Whether the user has sent a channel list subscription request, a.k.a. opened the channel list
|
// Whether the user has sent a channel list subscription request, a.k.a. opened the channel list
|
||||||
public hasOpenedChannelList = false; // implemented
|
public hasOpenedChannelList = false; // implemented
|
||||||
|
|
||||||
|
// Whether the user has sent a custom message subscription request (+custom)
|
||||||
|
public hasSentCustomSub = false; // implemented
|
||||||
|
|
||||||
|
// Whether the user has sent a custom message unsubscription request (-custom)
|
||||||
|
public hasSentCustomUnsub = false; // implemented
|
||||||
|
|
||||||
// Whether the user has changed their name/color this session (not just changed from default)
|
// Whether the user has changed their name/color this session (not just changed from default)
|
||||||
public hasChangedName = false; // implemented
|
public hasChangedName = false; // implemented
|
||||||
public hasChangedColor = false; // implemented
|
public hasChangedColor = false; // implemented
|
||||||
|
|
|
@ -9,9 +9,9 @@ import EventEmitter from "events";
|
||||||
import type {
|
import type {
|
||||||
IChannelInfo,
|
IChannelInfo,
|
||||||
IChannelSettings,
|
IChannelSettings,
|
||||||
ClientEvents,
|
OutgoingSocketEvents,
|
||||||
Participant,
|
IParticipant,
|
||||||
ServerEvents,
|
IncomingSocketEvents,
|
||||||
UserFlags,
|
UserFlags,
|
||||||
Vector2,
|
Vector2,
|
||||||
Notification,
|
Notification,
|
||||||
|
@ -44,6 +44,7 @@ import {
|
||||||
} from "~/data/permissions";
|
} from "~/data/permissions";
|
||||||
import { getRoles } from "~/data/role";
|
import { getRoles } from "~/data/role";
|
||||||
import { setTag } from "~/util/tags";
|
import { setTag } from "~/util/tags";
|
||||||
|
import { bus } from "~/event/bus";
|
||||||
|
|
||||||
const logger = new Logger("Sockets");
|
const logger = new Logger("Sockets");
|
||||||
|
|
||||||
|
@ -295,8 +296,8 @@ export class Socket extends EventEmitter {
|
||||||
* Send this socket an array of messages
|
* Send this socket an array of messages
|
||||||
* @param arr Array of messages to send
|
* @param arr Array of messages to send
|
||||||
**/
|
**/
|
||||||
public sendArray<EventID extends keyof ClientEvents>(
|
public sendArray<EventID extends keyof OutgoingSocketEvents>(
|
||||||
arr: ClientEvents[EventID][]
|
arr: OutgoingSocketEvents[EventID][]
|
||||||
) {
|
) {
|
||||||
if (this.isDestroyed() || !this.ws) return;
|
if (this.isDestroyed() || !this.ws) return;
|
||||||
this.ws.send(JSON.stringify(arr));
|
this.ws.send(JSON.stringify(arr));
|
||||||
|
@ -506,7 +507,7 @@ export class Socket extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Send this socket a channel update message
|
* Send this socket a channel update message
|
||||||
**/
|
**/
|
||||||
public sendChannelUpdate(ch: IChannelInfo, ppl: Participant[]) {
|
public sendChannelUpdate(ch: IChannelInfo, ppl: IParticipant[]) {
|
||||||
this.sendArray([
|
this.sendArray([
|
||||||
{
|
{
|
||||||
m: "ch",
|
m: "ch",
|
||||||
|
@ -545,7 +546,7 @@ export class Socket extends EventEmitter {
|
||||||
const ch = this.getCurrentChannel();
|
const ch = this.getCurrentChannel();
|
||||||
|
|
||||||
if (ch) {
|
if (ch) {
|
||||||
const part = this.getParticipant() as Participant;
|
const part = this.getParticipant() as IParticipant;
|
||||||
const cursorPos = this.getCursorPos();
|
const cursorPos = this.getCursorPos();
|
||||||
|
|
||||||
ch.sendArray([
|
ch.sendArray([
|
||||||
|
@ -646,7 +647,7 @@ export class Socket extends EventEmitter {
|
||||||
* Make this socket play a note in the channel they are in
|
* Make this socket play a note in the channel they are in
|
||||||
* @param msg Note message from client
|
* @param msg Note message from client
|
||||||
**/
|
**/
|
||||||
public playNotes(msg: ServerEvents["n"]) {
|
public playNotes(msg: IncomingSocketEvents["n"]) {
|
||||||
const ch = this.getCurrentChannel();
|
const ch = this.getCurrentChannel();
|
||||||
if (!ch) return;
|
if (!ch) return;
|
||||||
ch.playNotes(msg, this);
|
ch.playNotes(msg, this);
|
||||||
|
@ -852,6 +853,28 @@ export class Socket extends EventEmitter {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isSubscribedToCustom = false;
|
||||||
|
|
||||||
|
public isCustomSubbed() {
|
||||||
|
return this.isSubscribedToCustom === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow custom messages to be sent and received from this socket
|
||||||
|
**/
|
||||||
|
public subscribeToCustom() {
|
||||||
|
if (this.isSubscribedToCustom) return;
|
||||||
|
this.isSubscribedToCustom = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disallow custom messages to be sent and received from this socket
|
||||||
|
**/
|
||||||
|
public unsubscribeFromCustom() {
|
||||||
|
if (!this.isSubscribedToCustom) return;
|
||||||
|
this.isSubscribedToCustom = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socketsByUUID = new Map<Socket["uuid"], Socket>();
|
export const socketsByUUID = new Map<Socket["uuid"], Socket>();
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import type { EventID, ServerEventListener, ServerEvents } from "../util/types";
|
import type {
|
||||||
|
EventID,
|
||||||
|
ServerEventListener,
|
||||||
|
IncomingSocketEvents
|
||||||
|
} from "../util/types";
|
||||||
|
|
||||||
export class EventGroup {
|
export class EventGroup {
|
||||||
public eventList = new Array<ServerEventListener<any>>();
|
public eventList = new Array<ServerEventListener<any>>();
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { Logger } from "~/util/Logger";
|
||||||
import { ChannelList } from "../../../../channel/ChannelList";
|
import { ChannelList } from "../../../../channel/ChannelList";
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { ServerEventListener, TChannelFlags } from "../../../../util/types";
|
||||||
|
|
||||||
|
const logger = new Logger("Channel flag input");
|
||||||
|
|
||||||
export const ch_flag: ServerEventListener<"ch_flag"> = {
|
export const ch_flag: ServerEventListener<"ch_flag"> = {
|
||||||
id: "ch_flag",
|
id: "ch_flag",
|
||||||
|
@ -14,9 +17,15 @@ export const ch_flag: ServerEventListener<"ch_flag"> = {
|
||||||
chid = ch.getID();
|
chid = ch.getID();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ch = ChannelList.getList().find(c => c.getID() == chid);
|
if (typeof msg.key !== "string") return;
|
||||||
|
if (typeof msg.value === "undefined") return;
|
||||||
|
|
||||||
|
const ch = ChannelList.getChannel(chid);
|
||||||
if (!ch) return;
|
if (!ch) return;
|
||||||
|
|
||||||
ch.setFlag(msg.key, msg.value);
|
ch.setFlag(
|
||||||
|
msg.key as keyof TChannelFlags,
|
||||||
|
msg.value as TChannelFlags[keyof TChannelFlags]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { forceloadChannel } from "../../../../channel/forceLoad";
|
import { forceloadChannel } from "~/channel/forceload";
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { ServerEventListener } from "~/util/types";
|
||||||
|
|
||||||
export const forceload: ServerEventListener<"forceload"> = {
|
export const forceload: ServerEventListener<"forceload"> = {
|
||||||
id: "forceload",
|
id: "forceload",
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { forceloadChannel, unforceloadChannel } from "~/channel/forceload";
|
||||||
|
import { ServerEventListener } from "~/util/types";
|
||||||
|
|
||||||
|
export const unforceload: ServerEventListener<"unforceload"> = {
|
||||||
|
id: "unforceload",
|
||||||
|
callback: async (msg, socket) => {
|
||||||
|
// Unforceload channel
|
||||||
|
if (typeof msg._id !== "string") return;
|
||||||
|
|
||||||
|
unforceloadChannel(msg._id);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,17 +1,20 @@
|
||||||
import { EventGroup, eventGroups } from "../../events";
|
import { EventGroup, eventGroups } from "../../events";
|
||||||
import { admin_chat } from "./handlers/admin_chat";
|
import { admin_chat } from "./handlers/admin_chat";
|
||||||
|
import { ch_flag } from "./handlers/ch_flag";
|
||||||
import { clear_chat } from "./handlers/clear_chat";
|
import { clear_chat } from "./handlers/clear_chat";
|
||||||
|
|
||||||
export const EVENT_GROUP_ADMIN = new EventGroup("admin");
|
export const EVENT_GROUP_ADMIN = new EventGroup("admin");
|
||||||
|
|
||||||
import { color } from "./handlers/color";
|
import { color } from "./handlers/color";
|
||||||
import { eval_msg } from "./handlers/eval";
|
import { eval_msg } from "./handlers/eval";
|
||||||
|
import { forceload } from "./handlers/forceload";
|
||||||
import { move } from "./handlers/move";
|
import { move } from "./handlers/move";
|
||||||
import { name } from "./handlers/name";
|
import { name } from "./handlers/name";
|
||||||
import { notification } from "./handlers/notification";
|
import { notification } from "./handlers/notification";
|
||||||
import { rename_channel } from "./handlers/rename_channel";
|
import { rename_channel } from "./handlers/rename_channel";
|
||||||
import { restart } from "./handlers/restart";
|
import { restart } from "./handlers/restart";
|
||||||
import { tag } from "./handlers/tag";
|
import { tag } from "./handlers/tag";
|
||||||
|
import { unforceload } from "./handlers/unforceload";
|
||||||
import { user_flag } from "./handlers/user_flag";
|
import { user_flag } from "./handlers/user_flag";
|
||||||
|
|
||||||
// EVENT_GROUP_ADMIN.add(color);
|
// EVENT_GROUP_ADMIN.add(color);
|
||||||
|
@ -19,17 +22,20 @@ import { user_flag } from "./handlers/user_flag";
|
||||||
// EVENT_GROUP_ADMIN.add(user_flag);
|
// EVENT_GROUP_ADMIN.add(user_flag);
|
||||||
|
|
||||||
EVENT_GROUP_ADMIN.addMany(
|
EVENT_GROUP_ADMIN.addMany(
|
||||||
color,
|
color,
|
||||||
name,
|
name,
|
||||||
user_flag,
|
user_flag,
|
||||||
clear_chat,
|
clear_chat,
|
||||||
notification,
|
notification,
|
||||||
restart,
|
restart,
|
||||||
move,
|
move,
|
||||||
rename_channel,
|
rename_channel,
|
||||||
admin_chat,
|
admin_chat,
|
||||||
eval_msg,
|
eval_msg,
|
||||||
tag
|
tag,
|
||||||
|
ch_flag,
|
||||||
|
forceload,
|
||||||
|
unforceload
|
||||||
);
|
);
|
||||||
|
|
||||||
eventGroups.push(EVENT_GROUP_ADMIN);
|
eventGroups.push(EVENT_GROUP_ADMIN);
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
|
||||||
|
export const plus_custom: ServerEventListener<"+custom"> = {
|
||||||
|
id: "+custom",
|
||||||
|
callback: async (msg, socket) => {
|
||||||
|
// Custom message subscribe
|
||||||
|
if (socket.rateLimits) {
|
||||||
|
if (!socket.rateLimits.normal["+custom"].attempt()) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.gateway.hasSentCustomSub = true;
|
||||||
|
|
||||||
|
socket.subscribeToCustom();
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
|
||||||
|
export const minus_custom: ServerEventListener<"-ls"> = {
|
||||||
|
id: "-ls",
|
||||||
|
callback: async (msg, socket) => {
|
||||||
|
// Unsubscribe from custom messages
|
||||||
|
if (socket.rateLimits) {
|
||||||
|
if (!socket.rateLimits.normal["-custom"].attempt()) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.gateway.hasSentCustomUnsub = true;
|
||||||
|
|
||||||
|
socket.unsubscribeFromCustom();
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,10 +1,17 @@
|
||||||
import { Socket } from "../../../Socket";
|
import { Socket } from "../../../Socket";
|
||||||
import { ServerEventListener, ServerEvents } from "../../../../util/types";
|
import {
|
||||||
|
ServerEventListener,
|
||||||
|
IncomingSocketEvents
|
||||||
|
} from "../../../../util/types";
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/64509631/is-there-a-regex-to-match-all-unicode-emojis
|
// https://stackoverflow.com/questions/64509631/is-there-a-regex-to-match-all-unicode-emojis
|
||||||
const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g;
|
const emojiRegex =
|
||||||
|
/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g;
|
||||||
|
|
||||||
function populateSocketChatGatewayFlags(msg: ServerEvents["a"], socket: Socket) {
|
function populateSocketChatGatewayFlags(
|
||||||
|
msg: IncomingSocketEvents["a"],
|
||||||
|
socket: Socket
|
||||||
|
) {
|
||||||
socket.gateway.hasSentChatMessage = true;
|
socket.gateway.hasSentChatMessage = true;
|
||||||
|
|
||||||
if (msg.message.toUpperCase() == msg.message) {
|
if (msg.message.toUpperCase() == msg.message) {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { ServerEventListener } from "~/util/types";
|
||||||
|
|
||||||
|
export const custom: ServerEventListener<"custom"> = {
|
||||||
|
id: "custom",
|
||||||
|
callback: async (msg, socket) => {
|
||||||
|
// Custom message
|
||||||
|
if (!socket.isCustomSubbed()) return;
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,7 +2,7 @@ import { getUserPermissions } from "~/data/permissions";
|
||||||
import { Logger } from "~/util/Logger";
|
import { Logger } from "~/util/Logger";
|
||||||
import { getMOTD } from "~/util/motd";
|
import { getMOTD } from "~/util/motd";
|
||||||
import { createToken, getToken, validateToken } from "~/util/token";
|
import { createToken, getToken, validateToken } from "~/util/token";
|
||||||
import type { ServerEventListener, ServerEvents } from "~/util/types";
|
import type { ServerEventListener, IncomingSocketEvents } from "~/util/types";
|
||||||
import type { Socket } from "~/ws/Socket";
|
import type { Socket } from "~/ws/Socket";
|
||||||
import { config, usersConfigPath } from "~/ws/usersConfig";
|
import { config, usersConfigPath } from "~/ws/usersConfig";
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,24 @@ export const userset: ServerEventListener<"userset"> = {
|
||||||
callback: async (msg, socket) => {
|
callback: async (msg, socket) => {
|
||||||
// Change username/color
|
// Change username/color
|
||||||
if (!socket.rateLimits?.chains.userset.attempt()) return;
|
if (!socket.rateLimits?.chains.userset.attempt()) return;
|
||||||
if (typeof msg.set.name !== "string" && typeof msg.set.color !== "string") return;
|
if (
|
||||||
|
typeof msg.set.name !== "string" &&
|
||||||
|
typeof msg.set.color !== "string"
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
if (typeof msg.set.name == "string") {
|
const user = socket.getUser();
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
if (typeof msg.set.name == "string" && msg.set.name !== user.name) {
|
||||||
socket.gateway.hasChangedName = true;
|
socket.gateway.hasChangedName = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof msg.set.color == "string" && config.enableColorChanging) {
|
if (
|
||||||
|
typeof msg.set.color == "string" &&
|
||||||
|
msg.set.color !== user.color &&
|
||||||
|
config.enableColorChanging
|
||||||
|
) {
|
||||||
socket.gateway.hasChangedColor = true;
|
socket.gateway.hasChangedColor = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ import { kickban } from "./handlers/kickban";
|
||||||
import { bye } from "./handlers/bye";
|
import { bye } from "./handlers/bye";
|
||||||
import { chown } from "./handlers/chown";
|
import { chown } from "./handlers/chown";
|
||||||
import { unban } from "./handlers/unban";
|
import { unban } from "./handlers/unban";
|
||||||
|
import { plus_custom } from "./handlers/+custom";
|
||||||
|
import { minus_custom } from "./handlers/-custom";
|
||||||
|
import { custom } from "./handlers/custom";
|
||||||
|
|
||||||
// Imagine not having an "addMany" function...
|
// Imagine not having an "addMany" function...
|
||||||
|
|
||||||
|
@ -48,7 +51,10 @@ EVENTGROUP_USER.addMany(
|
||||||
kickban,
|
kickban,
|
||||||
unban,
|
unban,
|
||||||
bye,
|
bye,
|
||||||
chown
|
chown,
|
||||||
|
plus_custom,
|
||||||
|
minus_custom,
|
||||||
|
custom
|
||||||
);
|
);
|
||||||
|
|
||||||
eventGroups.push(EVENTGROUP_USER);
|
eventGroups.push(EVENTGROUP_USER);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// This is some convoluted dark magic I copied from some old mpp server I wrote
|
// Replicated note quota class from client
|
||||||
// No fucking clue where it came from or how it works internally, but I typedefized it
|
// with types!
|
||||||
// It's just a bunch of rate limits in a chain... like a RateLimitChain...... hmmmm.......
|
|
||||||
export class NoteQuota {
|
export class NoteQuota {
|
||||||
public allowance = 8000;
|
public allowance = 8000;
|
||||||
public max = 24000;
|
public max = 24000;
|
||||||
|
|
|
@ -17,6 +17,9 @@ export interface RateLimitConfigList<
|
||||||
"-ls": RL;
|
"-ls": RL;
|
||||||
chown: RL;
|
chown: RL;
|
||||||
|
|
||||||
|
"+custom": RL;
|
||||||
|
"-custom": RL;
|
||||||
|
|
||||||
// weird limits
|
// weird limits
|
||||||
hi: RL;
|
hi: RL;
|
||||||
bye: RL;
|
bye: RL;
|
||||||
|
@ -28,6 +31,7 @@ export interface RateLimitConfigList<
|
||||||
userset: RLC;
|
userset: RLC;
|
||||||
chset: RLC;
|
chset: RLC;
|
||||||
n: RLC; // not to be confused with NoteQuota
|
n: RLC; // not to be confused with NoteQuota
|
||||||
|
custom: RLC;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,37 +63,8 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
|
||||||
"-ls": 1000 / 60,
|
"-ls": 1000 / 60,
|
||||||
chown: 2000,
|
chown: 2000,
|
||||||
|
|
||||||
hi: 1000 / 20,
|
"+custom": 1000 / 60,
|
||||||
bye: 1000 / 20,
|
"-custom": 1000 / 60,
|
||||||
devices: 1000 / 20,
|
|
||||||
"admin message": 1000 / 20
|
|
||||||
},
|
|
||||||
chains: {
|
|
||||||
userset: {
|
|
||||||
interval: 1000 * 60 * 30,
|
|
||||||
num: 1000
|
|
||||||
},
|
|
||||||
chset: {
|
|
||||||
interval: 1000 * 60 * 30,
|
|
||||||
num: 1024
|
|
||||||
},
|
|
||||||
n: {
|
|
||||||
interval: 1000,
|
|
||||||
num: 512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crown: {
|
|
||||||
normal: {
|
|
||||||
a: 6000 / 10,
|
|
||||||
m: 1000 / 20,
|
|
||||||
ch: 1000 / 1,
|
|
||||||
kickban: 1000 / 8,
|
|
||||||
unban: 1000 / 8,
|
|
||||||
t: 1000 / 128,
|
|
||||||
"+ls": 1000 / 60,
|
|
||||||
"-ls": 1000 / 60,
|
|
||||||
chown: 2000,
|
|
||||||
|
|
||||||
hi: 1000 / 20,
|
hi: 1000 / 20,
|
||||||
bye: 1000 / 20,
|
bye: 1000 / 20,
|
||||||
|
@ -108,6 +83,49 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
|
||||||
n: {
|
n: {
|
||||||
interval: 1000,
|
interval: 1000,
|
||||||
num: 512
|
num: 512
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
interval: 1000,
|
||||||
|
num: 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crown: {
|
||||||
|
normal: {
|
||||||
|
a: 6000 / 10,
|
||||||
|
m: 1000 / 20,
|
||||||
|
ch: 1000 / 1,
|
||||||
|
kickban: 1000 / 8,
|
||||||
|
unban: 1000 / 8,
|
||||||
|
t: 1000 / 128,
|
||||||
|
"+ls": 1000 / 60,
|
||||||
|
"-ls": 1000 / 60,
|
||||||
|
chown: 2000,
|
||||||
|
|
||||||
|
"+custom": 1000 / 60,
|
||||||
|
"-custom": 1000 / 60,
|
||||||
|
|
||||||
|
hi: 1000 / 20,
|
||||||
|
bye: 1000 / 20,
|
||||||
|
devices: 1000 / 20,
|
||||||
|
"admin message": 1000 / 20
|
||||||
|
},
|
||||||
|
chains: {
|
||||||
|
userset: {
|
||||||
|
interval: 1000 * 60 * 30,
|
||||||
|
num: 1000
|
||||||
|
},
|
||||||
|
chset: {
|
||||||
|
interval: 1000 * 60 * 30,
|
||||||
|
num: 1024
|
||||||
|
},
|
||||||
|
n: {
|
||||||
|
interval: 1000,
|
||||||
|
num: 512
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
interval: 1000,
|
||||||
|
num: 512
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -123,6 +141,9 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
|
||||||
"-ls": 1000 / 60,
|
"-ls": 1000 / 60,
|
||||||
chown: 500,
|
chown: 500,
|
||||||
|
|
||||||
|
"+custom": 1000 / 120,
|
||||||
|
"-custom": 1000 / 120,
|
||||||
|
|
||||||
hi: 1000 / 20,
|
hi: 1000 / 20,
|
||||||
bye: 1000 / 20,
|
bye: 1000 / 20,
|
||||||
devices: 1000 / 20,
|
devices: 1000 / 20,
|
||||||
|
@ -140,6 +161,10 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
|
||||||
n: {
|
n: {
|
||||||
interval: 50,
|
interval: 50,
|
||||||
num: 512
|
num: 512
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
interval: 1000 * 60,
|
||||||
|
num: 20000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,14 @@ export const adminLimits: RateLimitConstructorList = {
|
||||||
"-ls": () => new RateLimit(config.admin.normal["-ls"]),
|
"-ls": () => new RateLimit(config.admin.normal["-ls"]),
|
||||||
chown: () => new RateLimit(config.admin.normal.chown),
|
chown: () => new RateLimit(config.admin.normal.chown),
|
||||||
|
|
||||||
|
"+custom": () => new RateLimit(config.admin.normal["+custom"]),
|
||||||
|
"-custom": () => new RateLimit(config.admin.normal["-custom"]),
|
||||||
|
|
||||||
hi: () => new RateLimit(config.admin.normal.hi),
|
hi: () => new RateLimit(config.admin.normal.hi),
|
||||||
bye: () => new RateLimit(config.admin.normal.bye),
|
bye: () => new RateLimit(config.admin.normal.bye),
|
||||||
devices: () => new RateLimit(config.admin.normal.devices),
|
devices: () => new RateLimit(config.admin.normal.devices),
|
||||||
"admin message": () => new RateLimit(config.admin.normal["admin message"])
|
"admin message": () =>
|
||||||
|
new RateLimit(config.admin.normal["admin message"])
|
||||||
},
|
},
|
||||||
chains: {
|
chains: {
|
||||||
userset: () =>
|
userset: () =>
|
||||||
|
@ -28,12 +32,17 @@ export const adminLimits: RateLimitConstructorList = {
|
||||||
chset: () =>
|
chset: () =>
|
||||||
new RateLimitChain(
|
new RateLimitChain(
|
||||||
config.admin.chains.chset.num,
|
config.admin.chains.chset.num,
|
||||||
config.admin.chains.userset.interval
|
config.admin.chains.chset.interval
|
||||||
),
|
),
|
||||||
n: () =>
|
n: () =>
|
||||||
new RateLimitChain(
|
new RateLimitChain(
|
||||||
config.admin.chains.n.num,
|
config.admin.chains.n.num,
|
||||||
config.admin.chains.userset.interval
|
config.admin.chains.n.interval
|
||||||
|
),
|
||||||
|
custom: () =>
|
||||||
|
new RateLimitChain(
|
||||||
|
config.admin.chains.custom.num,
|
||||||
|
config.admin.chains.custom.interval
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,14 @@ export const crownLimits: RateLimitConstructorList = {
|
||||||
"-ls": () => new RateLimit(config.crown.normal["-ls"]),
|
"-ls": () => new RateLimit(config.crown.normal["-ls"]),
|
||||||
chown: () => new RateLimit(config.crown.normal.chown),
|
chown: () => new RateLimit(config.crown.normal.chown),
|
||||||
|
|
||||||
|
"+custom": () => new RateLimit(config.crown.normal["+custom"]),
|
||||||
|
"-custom": () => new RateLimit(config.crown.normal["-custom"]),
|
||||||
|
|
||||||
hi: () => new RateLimit(config.crown.normal.hi),
|
hi: () => new RateLimit(config.crown.normal.hi),
|
||||||
bye: () => new RateLimit(config.crown.normal.bye),
|
bye: () => new RateLimit(config.crown.normal.bye),
|
||||||
devices: () => new RateLimit(config.crown.normal.devices),
|
devices: () => new RateLimit(config.crown.normal.devices),
|
||||||
"admin message": () => new RateLimit(config.crown.normal["admin message"])
|
"admin message": () =>
|
||||||
|
new RateLimit(config.crown.normal["admin message"])
|
||||||
},
|
},
|
||||||
chains: {
|
chains: {
|
||||||
userset: () =>
|
userset: () =>
|
||||||
|
@ -28,12 +32,17 @@ export const crownLimits: RateLimitConstructorList = {
|
||||||
chset: () =>
|
chset: () =>
|
||||||
new RateLimitChain(
|
new RateLimitChain(
|
||||||
config.crown.chains.chset.num,
|
config.crown.chains.chset.num,
|
||||||
config.crown.chains.userset.interval
|
config.crown.chains.chset.interval
|
||||||
),
|
),
|
||||||
n: () =>
|
n: () =>
|
||||||
new RateLimitChain(
|
new RateLimitChain(
|
||||||
config.crown.chains.n.num,
|
config.crown.chains.n.num,
|
||||||
config.crown.chains.userset.interval
|
config.crown.chains.n.interval
|
||||||
|
),
|
||||||
|
custom: () =>
|
||||||
|
new RateLimitChain(
|
||||||
|
config.crown.chains.custom.num,
|
||||||
|
config.crown.chains.custom.interval
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,14 @@ export const userLimits: RateLimitConstructorList = {
|
||||||
"-ls": () => new RateLimit(config.user.normal["-ls"]),
|
"-ls": () => new RateLimit(config.user.normal["-ls"]),
|
||||||
chown: () => new RateLimit(config.user.normal.chown),
|
chown: () => new RateLimit(config.user.normal.chown),
|
||||||
|
|
||||||
|
"+custom": () => new RateLimit(config.user.normal["+custom"]),
|
||||||
|
"-custom": () => new RateLimit(config.user.normal["-custom"]),
|
||||||
|
|
||||||
hi: () => new RateLimit(config.user.normal.hi),
|
hi: () => new RateLimit(config.user.normal.hi),
|
||||||
bye: () => new RateLimit(config.user.normal.bye),
|
bye: () => new RateLimit(config.user.normal.bye),
|
||||||
devices: () => new RateLimit(config.user.normal.devices),
|
devices: () => new RateLimit(config.user.normal.devices),
|
||||||
"admin message": () => new RateLimit(config.user.normal["admin message"])
|
"admin message": () =>
|
||||||
|
new RateLimit(config.user.normal["admin message"])
|
||||||
},
|
},
|
||||||
chains: {
|
chains: {
|
||||||
userset: () =>
|
userset: () =>
|
||||||
|
@ -28,12 +32,17 @@ export const userLimits: RateLimitConstructorList = {
|
||||||
chset: () =>
|
chset: () =>
|
||||||
new RateLimitChain(
|
new RateLimitChain(
|
||||||
config.user.chains.chset.num,
|
config.user.chains.chset.num,
|
||||||
config.user.chains.userset.interval
|
config.user.chains.chset.interval
|
||||||
),
|
),
|
||||||
n: () =>
|
n: () =>
|
||||||
new RateLimitChain(
|
new RateLimitChain(
|
||||||
config.user.chains.n.num,
|
config.user.chains.n.num,
|
||||||
config.user.chains.userset.interval
|
config.user.chains.n.interval
|
||||||
|
),
|
||||||
|
custom: () =>
|
||||||
|
new RateLimitChain(
|
||||||
|
config.user.chains.custom.num,
|
||||||
|
config.user.chains.custom.num
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
174
src/ws/server.ts
174
src/ws/server.ts
|
@ -7,7 +7,7 @@ import { Socket, socketsByUUID } from "./Socket";
|
||||||
import env from "../util/env";
|
import env from "../util/env";
|
||||||
import { getMOTD } from "../util/motd";
|
import { getMOTD } from "../util/motd";
|
||||||
import nunjucks from "nunjucks";
|
import nunjucks from "nunjucks";
|
||||||
import type { ServerWebSocket } from "bun";
|
import type { Server, ServerWebSocket } from "bun";
|
||||||
import { ConfigManager } from "~/util/config";
|
import { ConfigManager } from "~/util/config";
|
||||||
import { config as usersConfig } from "./usersConfig";
|
import { config as usersConfig } from "./usersConfig";
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const logger = new Logger("WebSocket Server");
|
||||||
|
|
||||||
// ip -> timestamp
|
// ip -> timestamp
|
||||||
// for checking if they visited the site and are also connected to the websocket
|
// for checking if they visited the site and are also connected to the websocket
|
||||||
const httpIpCache = new Map<string, number>();
|
export const httpIpCache = new Map<string, number>();
|
||||||
|
|
||||||
interface IFrontendConfig {
|
interface IFrontendConfig {
|
||||||
topButtons: "original" | "none";
|
topButtons: "original" | "none";
|
||||||
|
@ -59,117 +59,121 @@ async function getIndex() {
|
||||||
|
|
||||||
type ServerWebSocketMPP = ServerWebSocket<{ ip: string; socket: Socket }>;
|
type ServerWebSocketMPP = ServerWebSocket<{ ip: string; socket: Socket }>;
|
||||||
|
|
||||||
export const app = Bun.serve<{ ip: string }>({
|
export let app: Server;
|
||||||
port: env.PORT,
|
|
||||||
hostname: "0.0.0.0",
|
|
||||||
fetch: (req, server) => {
|
|
||||||
const reqip = server.requestIP(req);
|
|
||||||
if (!reqip) return;
|
|
||||||
|
|
||||||
const ip = req.headers.get("x-forwarded-for") || reqip.address;
|
export function startHTTPServer() {
|
||||||
|
app = Bun.serve<{ ip: string }>({
|
||||||
|
port: env.PORT,
|
||||||
|
hostname: "0.0.0.0",
|
||||||
|
fetch: (req, server) => {
|
||||||
|
const reqip = server.requestIP(req);
|
||||||
|
if (!reqip) return;
|
||||||
|
|
||||||
// Upgrade websocket connections
|
const ip = req.headers.get("x-forwarded-for") || reqip.address;
|
||||||
if (server.upgrade(req, { data: { ip } })) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpIpCache.set(ip, Date.now());
|
// Upgrade websocket connections
|
||||||
const url = new URL(req.url).pathname;
|
if (server.upgrade(req, { data: { ip } })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// lol
|
httpIpCache.set(ip, Date.now());
|
||||||
// const ip = decoder.decode(res.getRemoteAddressAsText());
|
const url = new URL(req.url).pathname;
|
||||||
// logger.debug(`${req.getMethod()} ${url} ${ip}`);
|
|
||||||
// res.writeStatus(`200 OK`).end("HI!");
|
|
||||||
|
|
||||||
// I have no clue if this is even safe...
|
// lol
|
||||||
// wtf do I do when the user types "/../.env" in the URL?
|
// const ip = decoder.decode(res.getRemoteAddressAsText());
|
||||||
// From my testing, nothing out of the ordinary happens...
|
// logger.debug(`${req.getMethod()} ${url} ${ip}`);
|
||||||
// but just in case, if you find something wrong with URLs,
|
// res.writeStatus(`200 OK`).end("HI!");
|
||||||
// this is the most likely culprit
|
|
||||||
|
|
||||||
const file = path.join("./public/", url);
|
// I have no clue if this is even safe...
|
||||||
|
// wtf do I do when the user types "/../.env" in the URL?
|
||||||
|
// From my testing, nothing out of the ordinary happens...
|
||||||
|
// but just in case, if you find something wrong with URLs,
|
||||||
|
// this is the most likely culprit
|
||||||
|
|
||||||
// Time for unreadable blocks of confusion
|
const file = path.join("./public/", url);
|
||||||
try {
|
|
||||||
// Is it a file?
|
|
||||||
if (fs.lstatSync(file).isFile()) {
|
|
||||||
// Read the file
|
|
||||||
const data = Bun.file(file);
|
|
||||||
|
|
||||||
// Return the file
|
// Time for unreadable blocks of confusion
|
||||||
if (data) {
|
try {
|
||||||
return new Response(data);
|
// Is it a file?
|
||||||
|
if (fs.lstatSync(file).isFile()) {
|
||||||
|
// Read the file
|
||||||
|
const data = Bun.file(file);
|
||||||
|
|
||||||
|
// Return the file
|
||||||
|
if (data) {
|
||||||
|
return new Response(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the index file, since it's a channel name or something
|
||||||
|
return getIndex();
|
||||||
|
} catch (err) {
|
||||||
|
// Return the index file as a coverup of our extreme failure
|
||||||
return getIndex();
|
return getIndex();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
open: (ws: ServerWebSocketMPP) => {
|
||||||
|
// swimming in the pool
|
||||||
|
const socket = new Socket(ws, createSocketID());
|
||||||
|
|
||||||
// Return the index file, since it's a channel name or something
|
ws.data.socket = socket;
|
||||||
return getIndex();
|
// logger.debug("Connection at " + socket.getIP());
|
||||||
} catch (err) {
|
|
||||||
// Return the index file as a coverup of our extreme failure
|
|
||||||
return getIndex();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
websocket: {
|
|
||||||
open: (ws: ServerWebSocketMPP) => {
|
|
||||||
// swimming in the pool
|
|
||||||
const socket = new Socket(ws, createSocketID());
|
|
||||||
|
|
||||||
ws.data.socket = socket;
|
if (socket.socketID === undefined) {
|
||||||
// logger.debug("Connection at " + socket.getIP());
|
socket.socketID = createSocketID();
|
||||||
|
}
|
||||||
|
|
||||||
if (socket.socketID === undefined) {
|
socketsByUUID.set(socket.getUUID(), socket);
|
||||||
socket.socketID = createSocketID();
|
|
||||||
}
|
|
||||||
|
|
||||||
socketsByUUID.set(socket.getUUID(), socket);
|
const ip = socket.getIP();
|
||||||
|
|
||||||
const ip = socket.getIP();
|
if (httpIpCache.has(ip)) {
|
||||||
|
const date = httpIpCache.get(ip);
|
||||||
|
|
||||||
if (httpIpCache.has(ip)) {
|
if (date) {
|
||||||
const date = httpIpCache.get(ip);
|
if (Date.now() - date < 1000 * 60) {
|
||||||
|
// They got the page and we were connected in under a minute
|
||||||
if (date) {
|
socket.gateway.hasConnectedToHTTPServer = true;
|
||||||
if (Date.now() - date < 1000 * 60) {
|
} else {
|
||||||
// They got the page and we were connected in under a minute
|
// They got the page and a long time has passed
|
||||||
socket.gateway.hasConnectedToHTTPServer = true;
|
httpIpCache.delete(ip);
|
||||||
} else {
|
}
|
||||||
// They got the page and a long time has passed
|
|
||||||
httpIpCache.delete(ip);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
|
||||||
message: (ws: ServerWebSocketMPP, message: string) => {
|
message: (ws: ServerWebSocketMPP, message: string) => {
|
||||||
// Fucking string
|
// Fucking string
|
||||||
const msg = message.toString();
|
const msg = message.toString();
|
||||||
|
|
||||||
// Let's find out wtf they even sent
|
// Let's find out wtf they even sent
|
||||||
handleMessage(ws.data.socket, msg);
|
handleMessage(ws.data.socket, msg);
|
||||||
},
|
},
|
||||||
|
|
||||||
close: (ws: ServerWebSocketMPP, code, message) => {
|
close: (ws: ServerWebSocketMPP, code, message) => {
|
||||||
// This usually gets called when someone leaves,
|
// This usually gets called when someone leaves,
|
||||||
// but it's also used internally just in case
|
// but it's also used internally just in case
|
||||||
// some dickhead can't close their tab like a
|
// some dickhead can't close their tab like a
|
||||||
// normal person.
|
// normal person.
|
||||||
|
|
||||||
const socket = ws.data.socket as Socket;
|
const socket = ws.data.socket as Socket;
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
|
||||||
for (const sockID of socketsByUUID.keys()) {
|
for (const sockID of socketsByUUID.keys()) {
|
||||||
const sock = socketsByUUID.get(sockID);
|
const sock = socketsByUUID.get(sockID);
|
||||||
|
|
||||||
if (sock === socket) {
|
if (sock === socket) {
|
||||||
socketsByUUID.delete(sockID);
|
socketsByUUID.delete(sockID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("Listening on port", env.PORT);
|
logger.info("Listening on port", env.PORT);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ConfigManager } from "../util/config";
|
import { ConfigManager } from "../util/config";
|
||||||
import type { Participant, UserFlags } from "../util/types";
|
import type { IParticipant, UserFlags } from "../util/types";
|
||||||
|
|
||||||
export interface UsersConfig {
|
export interface UsersConfig {
|
||||||
defaultName: string;
|
defaultName: string;
|
||||||
|
@ -7,7 +7,7 @@ export interface UsersConfig {
|
||||||
enableColorChanging: boolean;
|
enableColorChanging: boolean;
|
||||||
enableCustomNoteData: boolean;
|
enableCustomNoteData: boolean;
|
||||||
enableTags: boolean;
|
enableTags: boolean;
|
||||||
adminParticipant: Participant;
|
adminParticipant: IParticipant;
|
||||||
enableAdminEval: boolean;
|
enableAdminEval: boolean;
|
||||||
tokenAuth: "jwt" | "uuid" | "none";
|
tokenAuth: "jwt" | "uuid" | "none";
|
||||||
browserChallenge: "none" | "obf" | "basic";
|
browserChallenge: "none" | "obf" | "basic";
|
||||||
|
|
Loading…
Reference in New Issue