From 8c8af2edcc96af3f18c6bd20436e127c33326546 Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Thu, 19 Sep 2024 10:59:31 -0400 Subject: [PATCH] Merge stashed changes --- README.md | 53 ++++++++----- bun.lockb | Bin 71987 -> 72438 bytes config/ratelimits.yml | 65 ++++++++------- config/users.yml | 4 +- public | 2 +- src/channel/Channel.ts | 35 +++++---- src/channel/Crown.ts | 2 +- src/event/behaviors.ts | 84 ++++++++++++++++++++ src/event/bus.ts | 2 +- src/index.ts | 11 ++- src/util/config.ts | 100 +++++++++++++----------- src/util/types.d.ts | 6 +- src/ws/Gateway.ts | 6 ++ src/ws/Socket.ts | 29 ++++++- src/ws/events/admin/handlers/ch_flag.ts | 10 ++- src/ws/events/user/handlers/+custom.ts | 15 ++++ src/ws/events/user/handlers/-custom.ts | 15 ++++ src/ws/events/user/handlers/custom.ts | 9 +++ src/ws/ratelimit/config.ts | 87 +++++++++++++-------- src/ws/usersConfig.ts | 4 +- 20 files changed, 378 insertions(+), 161 deletions(-) create mode 100644 src/event/behaviors.ts create mode 100644 src/ws/events/user/handlers/+custom.ts create mode 100644 src/ws/events/user/handlers/-custom.ts create mode 100644 src/ws/events/user/handlers/custom.ts diff --git a/README.md b/README.md index 84687eb..edc4385 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ 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 @@ -119,60 +125,69 @@ Don't expect these instructions to stay the same. They might not even be up to d $ curl -fsSL https://bun.sh/install | bash ``` -1. Clone the repo and setup Git submodules +1. 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, **however, templating will likely not function properly.** +If you would like to use a different repository for the frontend, the 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. -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. +However, if you would like the templating features and want the frontend to change based on the server's configuration, setting up git submodules is required for full compatability. +- Clone the repository ``` $ git clone https://git.hri7566.info/Hri7566/mpp-server-dev2 + ``` + +- Setup git submodules + + ``` $ cd mpp-server-dev2 $ git submodule update --init --recursive ``` 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 - 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 as of 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 @@ -195,7 +210,7 @@ such as enabling the color changing option in the userset modal menu, or sending ## 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 in this section. ### General Explanation @@ -228,7 +243,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`. diff --git a/bun.lockb b/bun.lockb index f7dad10ca2ef96bc086245bbb93b4cd2104cb568..355fd9b78243d33df54a720f726cb8ce78c6cd5e 100755 GIT binary patch delta 718 zcmdnIiRIf?mI->A9pSGWimtE-KRb4oW!qQQ(-qGQmo8rLg~L#`^NcBDQCXT4BLfKR zpBOIBBcdb9en0wtl1Rdg3+s~AF9P`lG zP%`Df)a;24$MYbTPl#Ic1O;_#Ov#Rzk6W;*SlsZ z+s?Dz^nbH!q=CM}E-v%yE0_5;q~78;J!6=jzFq!U^xAv<`vuvf>^Ido++C8q((`sk znqx!P6URF$Wv`>3r*Nb+*|8n_Fgv1u)ueTkw{}ExGBW(*{|9vBzR5o${3j>#XaK#z zve~Bd4j<4HBAX><=&4M8;BLs(u@@Xl3<3Kl2kfzt0CN8QhX9cHb|3}^8^VgTQ_u1}SU+NrLEuljZiRZFbmuhe;3Q z5RfG@KnyY%WRNQq-v`n!fq26~AOQq_j)L_i9B^O}0~uus#Ku5ua12NQf!w3b>yA8; zp1ka|30KA=h}eopz(5n3yy1b)AB|S113)A@*W=`6n{Q1YRsdcy3Rd7q~jDE&v!svKo^YXGRG_N>>zGUUYZ$Hf8ym59is9 zlPW$c+On>;v~+ngBP9Lk$ulQ*zB>`%@u$uEbj~ks=XibR$)cUnlZ$yYHh<`Rz&F{0 zUt{xv$u6w;h&&dqjy+&6Fo^7(?7P=S^xuC7010jfVvq;9CokM9TrUI?0)ibt3=(Aq z;!8jb0y}{iB*p^7E>QL^D9s9_ZvZh!{cflj8<2hq#2^5&AEeh|FNnv$U~&LVF>u+G z8XMUum?#uyR;A{rP0rsZxVdHD4JIX!1u{SkGSn4_?*s8mAWk>{B!FPak;zf}ls0cT z=)l5f1mxd10v2YN^k6gFu_w}84<11H3J)i1JaPaznPGCqBL}X4hY<1E5}P+YdcwzO ZzIpn`AdTsZBpKDG3rH~vPd?Z)0|4jMsm%ZY diff --git a/config/ratelimits.yml b/config/ratelimits.yml index b33a993..5c42ff2 100644 --- a/config/ratelimits.yml +++ b/config/ratelimits.yml @@ -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 diff --git a/config/users.yml b/config/users.yml index 2d46dfb..ae3e3d6 100644 --- a/config/users.yml +++ b/config/users.yml @@ -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. diff --git a/public b/public index 1dc00c7..a4e9210 160000 --- a/public +++ b/public @@ -1 +1 @@ -Subproject commit 1dc00c7f885ac919a1bda7d4c749d33bd594c42f +Subproject commit a4e9210048ef2e7648c5614a9187e5788a6cc827 diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index 9c79184..9feb534 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -3,13 +3,13 @@ import { Logger } from "../util/Logger"; import type { IChannelSettings, OutgoingSocketEvents, - Participant, IncomingSocketEvents, + IParticipant, IChannelInfo, Notification, UserFlags, Tag, - ChannelFlags as TChannelFlags + TChannelFlags } from "../util/types"; import type { Socket } from "../ws/Socket"; import { validateChannelSettings } from "./settings"; @@ -50,7 +50,7 @@ interface ExtraPartData { flags: UserFlags; } -type ExtraPart = Participant & ExtraPartData; +type ExtraPart = IParticipant & ExtraPartData; export class Channel extends EventEmitter { private settings: Partial; @@ -173,7 +173,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 @@ -246,15 +247,18 @@ export class Channel extends EventEmitter { 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; @@ -281,7 +285,7 @@ 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: OutgoingSocketEvents["a"] = { m: "a", @@ -290,6 +294,9 @@ export class Channel extends EventEmitter { p: part }; + if (typeof overrideColor !== "undefined") + outgoing.p.color = overrideColor; + this.sendArray([outgoing]); this.chatHistory.push(outgoing); await saveChatHistory(this.getID(), this.chatHistory); @@ -547,7 +554,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; @@ -718,7 +725,7 @@ export class Channel extends EventEmitter { */ public leave(socket: Socket) { // this.logger.debug("Leave called"); - const part = socket.getParticipant() as Participant; + const part = socket.getParticipant() as IParticipant; let dupeCount = 0; for (const s of socketsByUUID.values()) { @@ -947,7 +954,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); @@ -962,7 +969,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(); } @@ -1039,7 +1046,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) { @@ -1229,7 +1236,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: IncomingSocketEvents["a"], p: Participant) { + public async sendChat(msg: IncomingSocketEvents["a"], p: IParticipant) { if (!msg.message) return; if (msg.message.length > 512) return; diff --git a/src/channel/Crown.ts b/src/channel/Crown.ts index fe0b3c8..d0dbe8f 100644 --- a/src/channel/Crown.ts +++ b/src/channel/Crown.ts @@ -1,4 +1,4 @@ -import { Participant, Vector2 } from "../util/types"; +import { IParticipant, Vector2 } from "../util/types"; import { Socket } from "../ws/Socket"; // shiny hat diff --git a/src/event/behaviors.ts b/src/event/behaviors.ts new file mode 100644 index 0000000..a265a74 --- /dev/null +++ b/src/event/behaviors.ts @@ -0,0 +1,84 @@ +import { ChannelList } from "~/channel/ChannelList"; +import { bus } from "./bus"; +import type { ClientEvents, ServerEvents } 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: ServerEvents["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 ClientEvents["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 ClientEvents["custom"] + ]); + } + } else if (msg.target.mode === "subscribed") { + receiver.sendArray([ + { + m: "custom", + data: msg.data, + p: sender.getUserID() + } as ClientEvents["custom"] + ]); + } + } + }); + + bus.emit("ready"); +} diff --git a/src/event/bus.ts b/src/event/bus.ts index f0c099d..c0a6a8d 100644 --- a/src/event/bus.ts +++ b/src/event/bus.ts @@ -6,4 +6,4 @@ class EventBus extends EventEmitter { } } -export const eventBus = new EventBus(); +export const bus = new EventBus(); diff --git a/src/index.ts b/src/index.ts index 0dff84d..697065c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,21 +20,24 @@ 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"; // 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 logger.info("Ready"); } diff --git a/src/util/config.ts b/src/util/config.ts index 25098e9..ca5f635 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -13,7 +13,7 @@ import { Logger } from "./Logger"; */ export class ConfigManager { - // public static configCache = new Map(); + public static configCache = new Map(); 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(configPath: string, defaultConfig: T): T { + public static loadConfig( + 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); - // File contents changed callback - // const watcher = watchFile(configPath, () => { - // this.logger.info( - // "Reloading config due to changes:", - // configPath - // ); - // this.loadConfig(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.configCache.set(configPath, config); + this.loadConfig(configPath, defaultConfig, false); + }); + } - // return this.getConfigProxy(configPath); - return config; + this.configCache.set(configPath, config); + + return this.getConfigProxy(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); - // File contents changed callback - // const watcher = watchFile(configPath, () => { - // this.logger.info( - // "Reloading config due to changes:", - // configPath - // ); - // this.loadConfig(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, false); + }); + } - // this.configCache.set(configPath, defaultConfig); - // return this.getConfigProxy(configPath); - return defaultConfig; + this.configCache.set(configPath, defaultConfig); + return this.getConfigProxy(configPath); + // return defaultConfig; } } @@ -128,24 +138,24 @@ export class ConfigManager { * @param configPath Path to config file * @returns Config proxy object */ - // protected static getConfigProxy(configPath: string) { - // const self = this; + protected static getConfigProxy(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)[ - // name - // ] as T[keyof T]; - // } - // } - // } - // ) as T; - // } + if (config) { + if (config.hasOwnProperty(name)) + return (config as Record)[ + name + ] as T[keyof T]; + } + } + } + ) as T; + } } diff --git a/src/util/types.d.ts b/src/util/types.d.ts index abe7f0b..3fd6b13 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -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) } diff --git a/src/ws/Gateway.ts b/src/ws/Gateway.ts index b6f945a..cb43908 100644 --- a/src/ws/Gateway.ts +++ b/src/ws/Gateway.ts @@ -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 diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index 6d546c6..cab48bd 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -10,7 +10,7 @@ import type { IChannelInfo, IChannelSettings, OutgoingSocketEvents, - Participant, + IParticipant, IncomingSocketEvents, UserFlags, Vector2, @@ -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"); @@ -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([ @@ -852,6 +853,28 @@ export class Socket extends EventEmitter { return false; } + + private isSubscribedToCustom = false; + + public isCustomSubbed() { + return this.isSubscribedToCustom === true; + } + + /** + * Start sending this socket the list of channels periodically + **/ + public subscribeToCustom() { + if (this.isSubscribedToCustom) return; + this.isSubscribedToCustom = true; + } + + /** + * Stop sending this socket the list of channels periodically + **/ + public unsubscribeFromCustom() { + if (!this.isSubscribedToCustom) return; + this.isSubscribedToCustom = false; + } } export const socketsByUUID = new Map(); diff --git a/src/ws/events/admin/handlers/ch_flag.ts b/src/ws/events/admin/handlers/ch_flag.ts index 9eb8af3..89fd59f 100644 --- a/src/ws/events/admin/handlers/ch_flag.ts +++ b/src/ws/events/admin/handlers/ch_flag.ts @@ -1,5 +1,5 @@ import { ChannelList } from "../../../../channel/ChannelList"; -import { ServerEventListener } from "../../../../util/types"; +import { ServerEventListener, TChannelFlags } from "../../../../util/types"; export const ch_flag: ServerEventListener<"ch_flag"> = { id: "ch_flag", @@ -14,9 +14,15 @@ export const ch_flag: ServerEventListener<"ch_flag"> = { chid = ch.getID(); } + if (typeof msg.key !== "string") return; + if (typeof msg.value === "undefined") return; + const ch = ChannelList.getList().find(c => c.getID() == chid); if (!ch) return; - ch.setFlag(msg.key, msg.value); + ch.setFlag( + msg.key as keyof TChannelFlags, + msg.value as TChannelFlags[keyof TChannelFlags] + ); } }; diff --git a/src/ws/events/user/handlers/+custom.ts b/src/ws/events/user/handlers/+custom.ts new file mode 100644 index 0000000..d4d17ab --- /dev/null +++ b/src/ws/events/user/handlers/+custom.ts @@ -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(); + } +}; diff --git a/src/ws/events/user/handlers/-custom.ts b/src/ws/events/user/handlers/-custom.ts new file mode 100644 index 0000000..9d7683c --- /dev/null +++ b/src/ws/events/user/handlers/-custom.ts @@ -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(); + } +}; diff --git a/src/ws/events/user/handlers/custom.ts b/src/ws/events/user/handlers/custom.ts new file mode 100644 index 0000000..ab5e201 --- /dev/null +++ b/src/ws/events/user/handlers/custom.ts @@ -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; + } +}; diff --git a/src/ws/ratelimit/config.ts b/src/ws/ratelimit/config.ts index 865afd0..0295e7f 100644 --- a/src/ws/ratelimit/config.ts +++ b/src/ws/ratelimit/config.ts @@ -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( "-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( 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( "-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( n: { interval: 50, num: 512 + }, + custom: { + interval: 1000 * 60, + num: 20000 } } } diff --git a/src/ws/usersConfig.ts b/src/ws/usersConfig.ts index a3b3cbe..590072d 100644 --- a/src/ws/usersConfig.ts +++ b/src/ws/usersConfig.ts @@ -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";