Compare commits

...

22 Commits
master ... dev

Author SHA1 Message Date
Hri7566 e65159830f Update README.md 2024-10-02 05:57:25 -04:00
Hri7566 a82839085f Change HTTP server to require a function call to initialize 2024-09-20 11:13:27 -04:00
Hri7566 617c8c4bc5 Update packages 2024-09-19 20:56:00 -04:00
Hri7566 b7911c5cfa Update package.json 2024-09-19 20:55:36 -04:00
Hri7566 ceab9310b5 Update README.md 2024-09-19 20:51:51 -04:00
Hri7566 dd6f841b60 Update README.md 2024-09-19 20:46:47 -04:00
Hri7566 0e21bdc687 Move entrypoint 2024-09-19 20:46:42 -04:00
Hri7566 163a01bf8a Add logo 2024-09-19 20:34:16 -04:00
Hri7566 fd65fc57dd Update public 2024-09-19 20:29:26 -04:00
Hri7566 bac593dcef Fix comments 2024-09-19 20:28:36 -04:00
Hri7566 ad083a7196 Fix join bugs 2024-09-19 20:28:20 -04:00
Hri7566 e5fb941bb7 Rename forceload.ts 2024-09-19 20:27:36 -04:00
Hri7566 937082694e Update messages 2024-09-19 20:27:13 -04:00
Hri7566 9fcc3864c0 Bugfixes 2024-09-19 11:51:10 -04:00
Hri7566 8c8af2edcc Merge stashed changes 2024-09-19 10:59:31 -04:00
Hri7566 fb693d3b02 Update README.md 2024-09-18 09:02:54 -04:00
Hri7566 a5d3bd7670 Fix submodules 2024-09-17 07:15:22 -04:00
Hri7566 e8b5c24714 Fix submodules 2024-09-17 07:10:43 -04:00
Hri7566 134b978d07 Fix submodules 2024-09-17 07:09:52 -04:00
Hri7566 2133c21d22 Remove public submodule and change user config 2024-09-17 06:53:06 -04:00
Hri7566 754629feae Merge branch 'master' into dev 2024-09-16 12:59:44 -04:00
Hri7566 2332aa5810 organize 2024-09-16 12:49:43 -04:00
39 changed files with 772 additions and 367 deletions

3
.gitmodules vendored
View File

@ -1,4 +1,3 @@
[submodule "public"]
path = public
url = https://github.com/Hri7566/mpp-frontend-dev
branch = dev2
url = git@git.hri7566.info:Hri7566/mpp-frontend-dev

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

142
README.md
View File

@ -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
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.
@ -63,30 +63,38 @@ This has always been the future intention of this project.
- Ability to rename channels
- Chat clearing similar to MPP.net
- 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
- Change `socketsBySocketID` to `socketsByUUID`
- Channel data saving
- Permission groups and permissions
- Probable permission groups: owner, admin, mod, trialmod, default
- Setup tags for each permission group
- MPP.com data message
- [x] Token generation
- [ ] Frontend implementation
- [ ] Permission groups and permissions
- [x] Probable permission groups: owner, admin, mod, trialmod, default
- [x] Setup tags for each permission group
- [ ] Implement permissions into rest of server
- [ ] MPP.com data message
- Implement based on `spooky.js` given there is no official documentation
- No cussing setting
- Full server-wide event bus
- Channel events
- Socket events
- User data events
- Permission-related events
- Redo ratelimits
- Redo all of the validations with Zod
- This probably means making Zod schemas for every single message type
- Also user and channel data
- Test every frontend
- Test fishing bot
- Remote console
- Modify frontend to use templating
- [ ] No cussing setting
- badwords.txt
- [x] Full server-wide event bus
- [ ] Channel events
- [ ] Socket events
- [ ] User data events
- [ ] Permission-related events
- [ ] Redo ratelimits
- [ ] Test every frontend
- [ ] Test fishing bot
- [ ] Remote console
- [x] Modify frontend to use templating
- [x] index.html
- [ ] js files
- [ ] Completley reorganize script.js
## Backlog/Notes
@ -102,77 +110,85 @@ This has always been the future intention of this project.
- Check for different messages?
- Check for URL?
- Notifications for server-generated XSS?
- Somehow check for templating, maybe with the existing httpIPCache?
- 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
- Skip redis due to the infamous licensing issues
- fork?
- Probably use a simple in-memory cache
- Likely store with leveldb or JSON
## 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
```
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.
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.
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.**
I am also considering using handlebars or something similar for templating, where the frontend will require completely different code.
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.
If you would like to use a different repository for the frontend, the files go in the `public` folder.
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
$ cd mpp-server-dev2
$ git submodule update --init --recursive
```
```
$ git clone --recursive https://git.hri7566.info/Hri7566/mpp-server-dev2
```
2. Configure
- Copy environment variables
- Copy default environment variables
```
$ 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
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
- `uuid`: Use UUID 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.
This can be done by running the following command:
If you are using JWT token authentication, the server will generate a JSON Web Token for each user when they first connect.
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
```
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
- `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.
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
@ -189,26 +205,37 @@ such as enabling the color changing option in the userset modal menu, or sending
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
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
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.
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`.
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.
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.
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.
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.
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.
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`.
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.
**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`.
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.
**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.
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
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.
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

BIN
bun.lockb

Binary file not shown.

View File

@ -1,3 +1,13 @@
owner:
- clearChat
- vanish
- chsetAnywhere
- chownAnywhere
- usersetOthers
- siteBan
- siteBanAnyReason
- siteBanAnyDuration
admin:
- clearChat
- vanish
@ -7,3 +17,19 @@ admin:
- siteBan
- siteBanAnyReason
- siteBanAnyDuration
mod:
- clearChat
- vanish
- chsetAnywhere
- chownAnywhere
- usersetOthers
- siteBan
- siteBanAnyReason
trialmod:
- clearChat
- vanish
- chsetAnywhere
- chownAnywhere
- siteBan

View File

@ -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:
# Rate limits
normal:
a: 1500 # Chat messages
m: 50 # Cursor messages
ch: 1000 # Channel join messages
kickban: 125 # Kickban messages
unban: 125 # Unban messages
t: 7.8125 # Ping messages
+ls: 16.666666666666668 # Channel list subscription messages
-ls: 16.666666666666668 # Channel list unsubscription messages
chown: 2000 # Channel ownership messages
hi: 50 # Handshake messages
bye: 50 # Disconnection messages
devices: 50 # MIDI device messages
admin message: 50 # Admin passthrough messages
# Rate limit chains
a: 1500
m: 50
ch: 1000
kickban: 125
unban: 125
t: 7.8125
+ls: 16.666666666666668
-ls: 16.666666666666668
chown: 2000
hi: 50
bye: 50
devices: 50
admin message: 50
+custom: 16.666666666666668
-custom: 16.666666666666668
chains:
userset: # Username/color update messages
userset:
interval: 1800000
num: 1000
chset: # Channel settings messages
chset:
interval: 1800000
num: 1024
n: # Note messages
# TODO is this correct?
n:
interval: 1000
num: 512
custom:
interval: 1000
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:
normal:
a: 600
@ -54,6 +43,8 @@ crown:
bye: 50
devices: 50
admin message: 50
+custom: 16.666666666666668
-custom: 16.666666666666668
chains:
userset:
interval: 1800000
@ -64,8 +55,9 @@ crown:
n:
interval: 1000
num: 512
# Rate limits for admins.
custom:
interval: 1000
num: 512
admin:
normal:
a: 120
@ -81,6 +73,8 @@ admin:
bye: 50
devices: 50
admin message: 16.666666666666668
+custom: 8.333333333333334
-custom: 8.333333333333334
chains:
userset:
interval: 500
@ -91,3 +85,6 @@ admin:
n:
interval: 50
num: 512
custom:
interval: 60000
num: 20000

View File

@ -11,7 +11,7 @@ defaultFlags:
# Whether or not to allow users to change their color.
# 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.
# 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.
# 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 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".
# 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.
tokenAuth: jwt
tokenAuth: none
# The browser challenge scheme. Valid options are "none", "obf" and "basic".
# 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,
# and "basic" will just send a simple function that expects a boolean.
# FIXME Note that "obf" is not implemented yet, and has undefined behavior.
browserChallenge: basic
browserChallenge: none
# Scheme for generating user IDs.
# Valid options are "random", "sha256", "mpp" and "uuid".

View File

@ -7,13 +7,13 @@
"author": "Hri7566",
"license": "ISC",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run src/index.ts --watch"
"start": "bun run src/start.ts",
"dev": "bun run src/start.ts --watch"
},
"dependencies": {
"@prisma/client": "5.17.0",
"@t3-oss/env-core": "^0.6.1",
"bun-types": "^1.1.27",
"bun-types": "^1.1.28",
"commander": "^11.1.0",
"date-holidays": "^3.23.12",
"events": "^3.3.0",
@ -28,7 +28,7 @@
},
"devDependencies": {
"@types/bun": "latest",
"@types/jsonwebtoken": "^9.0.6",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^20.16.5",
"@types/nunjucks": "^3.2.6",
"@typescript-eslint/eslint-plugin": "^6.21.0",

2
public

@ -1 +1 @@
Subproject commit 7f58dd80a3cc72a4f141c3a94e561e3f2bf3c5fe
Subproject commit 1dc00c7f885ac919a1bda7d4c749d33bd594c42f

View File

@ -1,16 +1,15 @@
import EventEmitter from "events";
import { Logger } from "../util/Logger";
import type {
ChannelSettingValue,
IChannelSettings,
ClientEvents,
Participant,
ServerEvents,
OutgoingSocketEvents,
IncomingSocketEvents,
IParticipant,
IChannelInfo,
Notification,
UserFlags,
Tag,
ChannelFlags as TChannelFlags
TChannelFlags
} from "../util/types";
import type { Socket } from "../ws/Socket";
import { validateChannelSettings } from "./settings";
@ -32,7 +31,7 @@ import {
getSavedChannel,
saveChannel
} from "../data/channel";
import { forceloadChannel } from "./forceLoad";
import { forceloadChannel } from "./forceload";
interface CachedKickban {
userId: string;
@ -51,13 +50,13 @@ interface ExtraPartData {
flags: UserFlags;
}
type ExtraPart = Participant & ExtraPartData;
type ExtraPart = IParticipant & ExtraPartData;
export class Channel extends EventEmitter {
private settings: Partial<IChannelSettings>;
private ppl = new Array<ExtraPart>();
public chatHistory = new Array<ClientEvents["a"]>();
public chatHistory = new Array<OutgoingSocketEvents["a"]>();
private async loadChatHistory() {
try {
@ -92,7 +91,8 @@ export class Channel extends EventEmitter {
const data = {
id: info._id,
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);
@ -150,6 +150,8 @@ export class Channel extends EventEmitter {
// Copy default settings
mixin(this.settings, config.defaultSettings);
if (owner_id) this.settings.owner_id = owner_id;
if (!this.isLobby()) {
if (set) {
// 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();
// ...and, possibly, an owner, too
@ -203,6 +206,7 @@ export class Channel extends EventEmitter {
}
private alreadyBound = false;
private destroyTimeout: Timer | undefined;
private bindEventListeners() {
if (this.alreadyBound) return;
@ -212,6 +216,12 @@ export class Channel extends EventEmitter {
this.logger.info("Loaded chat history");
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
for (const socket of socketsByUUID.values()) {
for (const p of this.ppl) {
@ -231,31 +241,41 @@ export class Channel extends EventEmitter {
if (this.ppl.length === 0 && !this.stays) {
if (config.channelDestroyTimeout) {
setTimeout(() => {
this.destroyTimeout = setTimeout(() => {
this.destroy();
}, config.channelDestroyTimeout);
} else {
this.destroy();
}
}
if (
this.ppl.length > 0 &&
typeof this.destroyTimeout !== "undefined"
) {
clearTimeout(this.destroyTimeout);
}
});
const BANNED_WORDS = ["AMIGHTYWIND", "CHECKLYHQ"];
this.on("a", async (msg: ServerEvents["a"], socket: Socket) => {
this.on("a", async (msg: IncomingSocketEvents["a"], socket: Socket) => {
try {
if (typeof msg.message !== "string") return;
const userFlags = socket.getUserFlags();
let overrideColor: string | undefined;
if (userFlags) {
if (userFlags.cant_chat == 1) return;
if (userFlags.chat_curse_1 == 1)
if (userFlags.cant_chat === 1) return;
if (userFlags.chat_curse_1 === 1)
msg.message = msg.message
.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);
if (typeof userFlags.chat_color === "string")
overrideColor = userFlags.chat_color;
}
if (!this.settings.chat) return;
@ -282,15 +302,18 @@ export class Channel extends EventEmitter {
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
.trim();
const part = socket.getParticipant() as Participant;
const part = socket.getParticipant() as IParticipant;
const outgoing: ClientEvents["a"] = {
const outgoing: OutgoingSocketEvents["a"] = {
m: "a",
a: msg.message,
t: Date.now(),
p: part
};
if (typeof overrideColor !== "undefined")
outgoing.p.color = overrideColor;
this.sendArray([outgoing]);
this.chatHistory.push(outgoing);
await saveChatHistory(this.getID(), this.chatHistory);
@ -548,7 +571,7 @@ export class Channel extends EventEmitter {
*/
public join(socket: Socket, force = false): void {
if (this.isDestroyed()) return;
const part = socket.getParticipant() as Participant;
const part = socket.getParticipant() as IParticipant;
let hasChangedChannel = false;
@ -662,7 +685,7 @@ export class Channel extends EventEmitter {
if (p) {
// 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
//this.emit("update", this, socket.getUUID());
//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);
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) {
// this.logger.debug("Leave called");
const part = socket.getParticipant() as Participant;
const part = socket.getParticipant();
if (!part) return;
let dupeCount = 0;
for (const s of socketsByUUID.values()) {
@ -735,18 +763,22 @@ export class Channel extends EventEmitter {
if (dupeCount === 1) {
const p = this.ppl.find(p => p.id === socket.getParticipantID());
let hadCrown = false;
if (p) {
this.ppl.splice(this.ppl.indexOf(p), 1);
if (this.crown) {
if (this.crown.participantId === p.id) {
// Channel owner left, reset crown timeout
hadCrown = true;
this.chown();
}
}
}
// Broadcast bye
if (!hadCrown)
this.sendArray([
{
m: "bye",
@ -755,7 +787,7 @@ export class Channel extends EventEmitter {
]);
//this.logger.debug("Update from leave");
this.emit("update", this);
// this.emit("update", this);
} else {
for (const p of this.ppl) {
if (!p.uuids.includes(socket.getUUID())) continue;
@ -847,8 +879,8 @@ export class Channel extends EventEmitter {
* Send messages to everyone in this channel
* @param arr List of events to send to clients
*/
public sendArray<EventID extends keyof ClientEvents>(
arr: ClientEvents[EventID][],
public sendArray<EventID extends keyof OutgoingSocketEvents>(
arr: OutgoingSocketEvents[EventID][],
blockPartID?: string
) {
const sentSocketIDs = new Array<string>();
@ -875,7 +907,7 @@ export class Channel extends EventEmitter {
* @param socket Socket that is sending notes
* @returns undefined
*/
public playNotes(msg: ServerEvents["n"], socket?: Socket) {
public playNotes(msg: IncomingSocketEvents["n"], socket?: Socket) {
if (this.isDestroyed()) return;
let pianoPartID = usersConfig.adminParticipant.id;
@ -883,9 +915,23 @@ export class Channel extends EventEmitter {
const part = socket.getParticipant();
if (!part) return;
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",
n: msg.n,
t: msg.t,
@ -921,6 +967,7 @@ export class Channel extends EventEmitter {
*/
public destroy() {
if (this.destroyed) return;
this.destroyed = true;
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)
* @param part Participant to give crown to (or undefined to drop crown)
*/
public chown(part?: Participant) {
public chown(part?: IParticipant) {
if (this.crown) {
if (part) {
this.giveCrown(part);
@ -963,7 +1010,7 @@ export class Channel extends EventEmitter {
* @param part Participant to give crown to
* @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 (!this.crown) this.crown = new Crown();
}
@ -974,7 +1021,7 @@ export class Channel extends EventEmitter {
this.crown.time = Date.now();
if (update) {
//this.logger.debug("Update from giveCrown");
// this.logger.debug("Update from giveCrown");
this.emit("update", this);
}
}
@ -1040,7 +1087,7 @@ export class Channel extends EventEmitter {
if (!banChannel) return;
// Check if they are on the server at all
let bannedPart: Participant | undefined;
let bannedPart: IParticipant | undefined;
const bannedUUIDs: string[] = [];
for (const sock of socketsByUUID.values()) {
if (sock.getUserID() === _id) {
@ -1230,7 +1277,7 @@ export class Channel extends EventEmitter {
* @param msg Chat message event to send
* @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.length > 512) return;
@ -1241,7 +1288,7 @@ export class Channel extends EventEmitter {
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
.trim();
const outgoing: ClientEvents["a"] = {
const outgoing: OutgoingSocketEvents["a"] = {
m: "a",
a: msg.message,
t: Date.now(),
@ -1283,6 +1330,8 @@ export class Channel extends EventEmitter {
val: TChannelFlags[K]
) {
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`
);
}
public setForceload(enable: boolean) {
this.stays = enable;
this.save();
}
}
export default Channel;

View File

@ -1,4 +1,4 @@
import { Participant, Vector2 } from "../util/types";
import { IParticipant, Vector2 } from "../util/types";
import { Socket } from "../ws/Socket";
// shiny hat

View File

@ -14,9 +14,15 @@ const logger = new Logger("Channel Forceloader");
*/
export function forceloadChannel(id: string) {
try {
const existing = ChannelList.getChannel(id);
if (existing) {
logger.info("Keeping", id, "forceloaded");
existing.setForceload(true);
} else {
logger.info("Forceloading", id);
new Channel(id, undefined, undefined, undefined, true);
return true;
}
} catch (err) {
return false;
}

84
src/event/behaviors.ts Normal file
View File

@ -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");
}

9
src/event/bus.ts Normal file
View File

@ -0,0 +1,9 @@
import EventEmitter from "events";
class EventBus extends EventEmitter {
constructor() {
super();
}
}
export const bus = new EventBus();

View File

@ -13,29 +13,33 @@
// There are a lot of unhinged bs comments in this repo
// Pay no attention to the ones that cuss you out
// If you don't load the server first, bun will literally segfault
import "./ws/server";
import { loadForcedStartupChannels } from "./channel/forceLoad";
import { loadForcedStartupChannels } from "./channel/forceload";
import { Logger } from "./util/Logger";
// docker hates this next one
import { startReadline } from "./util/readline";
import { loadDefaultPermissions } from "./data/permissions";
import { loadBehaviors } from "./event/behaviors";
import { startHTTPServer } from "./ws/server";
// wrapper for some reason
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");
logger.info("Forceloading startup channels...");
loadForcedStartupChannels();
logger.info("Finished forceloading");
logger.info("Loading behaviors...");
loadBehaviors();
logger.info("Finished loading behaviors");
loadDefaultPermissions();
// Break the console
logger.info("Starting REPL");
startReadline();
// Nevermind, two things are printed
startHTTPServer();
logger.info("Ready");
}
startServer();
// startServer();

3
src/start.ts Normal file
View File

@ -0,0 +1,3 @@
import { startServer } from ".";
startServer();

View File

@ -13,7 +13,7 @@ import { Logger } from "./Logger";
*/
export class ConfigManager {
// public static configCache = new Map<string, unknown>();
public static configCache = new Map<string, unknown>();
public static logger: Logger;
static {
@ -32,10 +32,15 @@ export class ConfigManager {
* });
* ```
* @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
*/
public static loadConfig<T>(configPath: string, defaultConfig: T): T {
public static loadConfig<T>(
configPath: string,
defaultConfig: T,
saveDefault = true
): T {
const self = this;
// Config exists?
@ -73,38 +78,43 @@ export class ConfigManager {
mix(config, defRecord);
// Save config if modified
if (changed) this.writeConfig(configPath, config);
if (saveDefault && changed) this.writeConfig(configPath, config);
if (!this.configCache.has(configPath)) {
// File contents changed callback
// const watcher = watchFile(configPath, () => {
// this.logger.info(
// "Reloading config due to changes:",
// configPath
// );
// this.loadConfig(configPath, defaultConfig);
// });
const watcher = watchFile(configPath, () => {
this.logger.info(
"Reloading config due to changes:",
configPath
);
// this.configCache.set(configPath, config);
this.loadConfig(configPath, defaultConfig, false);
});
}
// return this.getConfigProxy<T>(configPath);
return config;
this.configCache.set(configPath, config);
return this.getConfigProxy<T>(configPath);
// return config;
} else {
// Write default config to disk and use that
//logger.warn(`Config file "${configPath}" not found, writing default config to disk`);
this.writeConfig(configPath, defaultConfig);
if (saveDefault) this.writeConfig(configPath, defaultConfig);
if (!this.configCache.has(configPath)) {
// File contents changed callback
// const watcher = watchFile(configPath, () => {
// this.logger.info(
// "Reloading config due to changes:",
// configPath
// );
// this.loadConfig(configPath, defaultConfig);
// });
const watcher = watchFile(configPath, () => {
this.logger.info(
"Reloading config due to changes:",
configPath
);
this.loadConfig(configPath, defaultConfig, false);
});
}
// this.configCache.set(configPath, defaultConfig);
// return this.getConfigProxy<T>(configPath);
return defaultConfig;
this.configCache.set(configPath, defaultConfig);
return this.getConfigProxy<T>(configPath);
// return defaultConfig;
}
}
@ -128,24 +138,24 @@ export class ConfigManager {
* @param configPath Path to config file
* @returns Config proxy object
*/
// protected static getConfigProxy<T>(configPath: string) {
// const self = this;
protected static getConfigProxy<T>(configPath: string) {
const self = this;
// return new Proxy(
// {},
// {
// get(_target: unknown, name: string) {
// // Get the updated in-memory version of the config
// const config = self.configCache.get(configPath) as T;
return new Proxy(
{},
{
get(_target: unknown, name: string) {
// Get the updated in-memory version of the config
const config = self.configCache.get(configPath) as T;
// if (config) {
// if (config.hasOwnProperty(name))
// return (config as Record<string, unknown>)[
// name
// ] as T[keyof T];
// }
// }
// }
// ) as T;
// }
if (config) {
if (config.hasOwnProperty(name))
return (config as Record<string, unknown>)[
name
] as T[keyof T];
}
}
}
) as T;
}
}

21
src/util/types.d.ts vendored
View File

@ -21,11 +21,13 @@ declare type UserFlags = Partial<{
mod: number;
admin: number;
vanish: number;
chat_color: string;
}>;
type ChannelFlags = Partial<{
type TChannelFlags = Partial<{
limit: number;
owner_id: string;
no_crown: boolean;
}>;
declare interface Tag {
@ -40,7 +42,7 @@ declare interface User {
tag?: Tag;
}
declare interface Participant extends User {
declare interface IParticipant extends User {
id: string; // participant id (same as user id on mppclone)
}
@ -107,7 +109,7 @@ declare interface Crown {
endPos: Vector2;
}
declare interface ServerEvents {
declare interface IncomingSocketEvents {
hi: {
m: "hi";
token?: string;
@ -209,7 +211,7 @@ declare interface ServerEvents {
"admin message": {
m: "admin message";
password: string;
msg: ServerEvents[keyof ServerEvents];
msg: IncomingSocketEvents[keyof IncomingSocketEvents];
};
b: {
@ -279,6 +281,11 @@ declare interface ServerEvents {
_id: string;
};
unforceload: {
m: "unforceload";
_id: string;
};
ch_flag: {
m: "ch_flag";
_id?: string;
@ -310,7 +317,7 @@ declare interface ServerEvents {
};
}
declare interface ClientEvents {
declare interface OutgoingSocketEvents {
a: {
m: "a";
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> = {
id: E;
callback: (msg: ServerEvents[E], socket: Socket) => Promise<void>;
callback: (msg: IncomingSocketEvents[E], socket: Socket) => Promise<void>;
};
declare type Vector2<T = number> = {

View File

@ -30,7 +30,7 @@ export class Gateway {
public isTokenValid = false; // implemented
// Their user agent, if sent
public userAgent = ""; // TODO
public userAgent = ""; // partially implemented
// Whether they have moved their cursor
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
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)
public hasChangedName = false; // implemented
public hasChangedColor = false; // implemented

View File

@ -9,9 +9,9 @@ import EventEmitter from "events";
import type {
IChannelInfo,
IChannelSettings,
ClientEvents,
Participant,
ServerEvents,
OutgoingSocketEvents,
IParticipant,
IncomingSocketEvents,
UserFlags,
Vector2,
Notification,
@ -44,6 +44,7 @@ import {
} from "~/data/permissions";
import { getRoles } from "~/data/role";
import { setTag } from "~/util/tags";
import { bus } from "~/event/bus";
const logger = new Logger("Sockets");
@ -295,8 +296,8 @@ export class Socket extends EventEmitter {
* Send this socket an array of messages
* @param arr Array of messages to send
**/
public sendArray<EventID extends keyof ClientEvents>(
arr: ClientEvents[EventID][]
public sendArray<EventID extends keyof OutgoingSocketEvents>(
arr: OutgoingSocketEvents[EventID][]
) {
if (this.isDestroyed() || !this.ws) return;
this.ws.send(JSON.stringify(arr));
@ -506,7 +507,7 @@ export class Socket extends EventEmitter {
/**
* Send this socket a channel update message
**/
public sendChannelUpdate(ch: IChannelInfo, ppl: Participant[]) {
public sendChannelUpdate(ch: IChannelInfo, ppl: IParticipant[]) {
this.sendArray([
{
m: "ch",
@ -545,7 +546,7 @@ export class Socket extends EventEmitter {
const ch = this.getCurrentChannel();
if (ch) {
const part = this.getParticipant() as Participant;
const part = this.getParticipant() as IParticipant;
const cursorPos = this.getCursorPos();
ch.sendArray([
@ -646,7 +647,7 @@ export class Socket extends EventEmitter {
* Make this socket play a note in the channel they are in
* @param msg Note message from client
**/
public playNotes(msg: ServerEvents["n"]) {
public playNotes(msg: IncomingSocketEvents["n"]) {
const ch = this.getCurrentChannel();
if (!ch) return;
ch.playNotes(msg, this);
@ -852,6 +853,28 @@ export class Socket extends EventEmitter {
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>();

View File

@ -1,4 +1,8 @@
import type { EventID, ServerEventListener, ServerEvents } from "../util/types";
import type {
EventID,
ServerEventListener,
IncomingSocketEvents
} from "../util/types";
export class EventGroup {
public eventList = new Array<ServerEventListener<any>>();

View File

@ -1,5 +1,8 @@
import { Logger } from "~/util/Logger";
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"> = {
id: "ch_flag",
@ -14,9 +17,15 @@ export const ch_flag: ServerEventListener<"ch_flag"> = {
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;
ch.setFlag(msg.key, msg.value);
ch.setFlag(
msg.key as keyof TChannelFlags,
msg.value as TChannelFlags[keyof TChannelFlags]
);
}
};

View File

@ -1,5 +1,5 @@
import { forceloadChannel } from "../../../../channel/forceLoad";
import { ServerEventListener } from "../../../../util/types";
import { forceloadChannel } from "~/channel/forceload";
import { ServerEventListener } from "~/util/types";
export const forceload: ServerEventListener<"forceload"> = {
id: "forceload",

View File

@ -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);
}
};

View File

@ -1,17 +1,20 @@
import { EventGroup, eventGroups } from "../../events";
import { admin_chat } from "./handlers/admin_chat";
import { ch_flag } from "./handlers/ch_flag";
import { clear_chat } from "./handlers/clear_chat";
export const EVENT_GROUP_ADMIN = new EventGroup("admin");
import { color } from "./handlers/color";
import { eval_msg } from "./handlers/eval";
import { forceload } from "./handlers/forceload";
import { move } from "./handlers/move";
import { name } from "./handlers/name";
import { notification } from "./handlers/notification";
import { rename_channel } from "./handlers/rename_channel";
import { restart } from "./handlers/restart";
import { tag } from "./handlers/tag";
import { unforceload } from "./handlers/unforceload";
import { user_flag } from "./handlers/user_flag";
// EVENT_GROUP_ADMIN.add(color);
@ -29,7 +32,10 @@ EVENT_GROUP_ADMIN.addMany(
rename_channel,
admin_chat,
eval_msg,
tag
tag,
ch_flag,
forceload,
unforceload
);
eventGroups.push(EVENT_GROUP_ADMIN);

View File

@ -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();
}
};

View File

@ -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();
}
};

View File

@ -1,10 +1,17 @@
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
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;
if (msg.message.toUpperCase() == msg.message) {

View File

@ -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;
}
};

View File

@ -2,7 +2,7 @@ import { getUserPermissions } from "~/data/permissions";
import { Logger } from "~/util/Logger";
import { getMOTD } from "~/util/motd";
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 { config, usersConfigPath } from "~/ws/usersConfig";

View File

@ -6,13 +6,24 @@ export const userset: ServerEventListener<"userset"> = {
callback: async (msg, socket) => {
// Change username/color
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;
}
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;
}

View File

@ -17,6 +17,9 @@ import { kickban } from "./handlers/kickban";
import { bye } from "./handlers/bye";
import { chown } from "./handlers/chown";
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...
@ -48,7 +51,10 @@ EVENTGROUP_USER.addMany(
kickban,
unban,
bye,
chown
chown,
plus_custom,
minus_custom,
custom
);
eventGroups.push(EVENTGROUP_USER);

View File

@ -1,6 +1,5 @@
// This is some convoluted dark magic I copied from some old mpp server I wrote
// No fucking clue where it came from or how it works internally, but I typedefized it
// It's just a bunch of rate limits in a chain... like a RateLimitChain...... hmmmm.......
// Replicated note quota class from client
// with types!
export class NoteQuota {
public allowance = 8000;
public max = 24000;

View File

@ -17,6 +17,9 @@ export interface RateLimitConfigList<
"-ls": RL;
chown: RL;
"+custom": RL;
"-custom": RL;
// weird limits
hi: RL;
bye: RL;
@ -28,6 +31,7 @@ export interface RateLimitConfigList<
userset: RLC;
chset: RLC;
n: RLC; // not to be confused with NoteQuota
custom: RLC;
};
}
@ -59,37 +63,8 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
"-ls": 1000 / 60,
chown: 2000,
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
}
}
},
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,
@ -108,6 +83,49 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
n: {
interval: 1000,
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,
chown: 500,
"+custom": 1000 / 120,
"-custom": 1000 / 120,
hi: 1000 / 20,
bye: 1000 / 20,
devices: 1000 / 20,
@ -140,6 +161,10 @@ export const config = ConfigManager.loadConfig<RateLimitsConfig>(
n: {
interval: 50,
num: 512
},
custom: {
interval: 1000 * 60,
num: 20000
}
}
}

View File

@ -14,10 +14,14 @@ export const adminLimits: RateLimitConstructorList = {
"-ls": () => new RateLimit(config.admin.normal["-ls"]),
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),
bye: () => new RateLimit(config.admin.normal.bye),
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: {
userset: () =>
@ -28,12 +32,17 @@ export const adminLimits: RateLimitConstructorList = {
chset: () =>
new RateLimitChain(
config.admin.chains.chset.num,
config.admin.chains.userset.interval
config.admin.chains.chset.interval
),
n: () =>
new RateLimitChain(
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
)
}
};

View File

@ -14,10 +14,14 @@ export const crownLimits: RateLimitConstructorList = {
"-ls": () => new RateLimit(config.crown.normal["-ls"]),
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),
bye: () => new RateLimit(config.crown.normal.bye),
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: {
userset: () =>
@ -28,12 +32,17 @@ export const crownLimits: RateLimitConstructorList = {
chset: () =>
new RateLimitChain(
config.crown.chains.chset.num,
config.crown.chains.userset.interval
config.crown.chains.chset.interval
),
n: () =>
new RateLimitChain(
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
)
}
};

View File

@ -14,10 +14,14 @@ export const userLimits: RateLimitConstructorList = {
"-ls": () => new RateLimit(config.user.normal["-ls"]),
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),
bye: () => new RateLimit(config.user.normal.bye),
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: {
userset: () =>
@ -28,12 +32,17 @@ export const userLimits: RateLimitConstructorList = {
chset: () =>
new RateLimitChain(
config.user.chains.chset.num,
config.user.chains.userset.interval
config.user.chains.chset.interval
),
n: () =>
new RateLimitChain(
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
)
}
};

View File

@ -7,7 +7,7 @@ import { Socket, socketsByUUID } from "./Socket";
import env from "../util/env";
import { getMOTD } from "../util/motd";
import nunjucks from "nunjucks";
import type { ServerWebSocket } from "bun";
import type { Server, ServerWebSocket } from "bun";
import { ConfigManager } from "~/util/config";
import { config as usersConfig } from "./usersConfig";
@ -15,7 +15,7 @@ const logger = new Logger("WebSocket Server");
// ip -> timestamp
// 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 {
topButtons: "original" | "none";
@ -59,7 +59,10 @@ async function getIndex() {
type ServerWebSocketMPP = ServerWebSocket<{ ip: string; socket: Socket }>;
export const app = Bun.serve<{ ip: string }>({
export let app: Server;
export function startHTTPServer() {
app = Bun.serve<{ ip: string }>({
port: env.PORT,
hostname: "0.0.0.0",
fetch: (req, server) => {
@ -170,6 +173,7 @@ export const app = Bun.serve<{ ip: string }>({
}
}
}
});
});
logger.info("Listening on port", env.PORT);
logger.info("Listening on port", env.PORT);
}

View File

@ -1,5 +1,5 @@
import { ConfigManager } from "../util/config";
import type { Participant, UserFlags } from "../util/types";
import type { IParticipant, UserFlags } from "../util/types";
export interface UsersConfig {
defaultName: string;
@ -7,7 +7,7 @@ export interface UsersConfig {
enableColorChanging: boolean;
enableCustomNoteData: boolean;
enableTags: boolean;
adminParticipant: Participant;
adminParticipant: IParticipant;
enableAdminEval: boolean;
tokenAuth: "jwt" | "uuid" | "none";
browserChallenge: "none" | "obf" | "basic";