Add rate limits for every message, fix lobby regex, implement tags into database, reword comments, add channel flags

This commit is contained in:
Hri7566 2024-07-10 17:02:42 -04:00
parent bb0516b367
commit 71960104eb
29 changed files with 506 additions and 96 deletions

View File

@ -1,7 +1,6 @@
forceLoad: forceLoad:
- lobby - lobby
- test/awkward - test/awkward
lobbySettings: lobbySettings:
lobby: true lobby: true
chat: true chat: true
@ -9,17 +8,18 @@ lobbySettings:
visible: true visible: true
color: "#73b3cc" color: "#73b3cc"
color2: "#273546" color2: "#273546"
defaultSettings: defaultSettings:
chat: true chat: true
crownsolo: false crownsolo: false
color: "#3b5054" color: "#3b5054"
color2: "#001014" color2: "#001014"
visible: true visible: true
lobbyRegexes: lobbyRegexes:
- "^lobby[1-9]?[1-9]?$" - ^lobby[0-9][0-9]$
- "^test/.+$" - ^lobby[0-9]$
- ^lobby$
lobbyBackdoor: "lolwutsecretlobbybackdoor" - ^test/.+$
fullChannel: "test/awkward" lobbyBackdoor: lolwutsecretlobbybackdoor
fullChannel: test/awkward
sendLimit: false
sendTags: false

View File

@ -3,28 +3,73 @@ user:
a: 1500 a: 1500
m: 50 m: 50
ch: 1000 ch: 1000
kickban: 250 kickban: 125
t: 7.8125
+ls: 16.666666666666668
-ls: 16.666666666666668
chown: 2000
hi: 50
bye: 50
devices: 50
admin_message: 50
admin message: 50
chains: chains:
userset: userset:
interval: 1800000 interval: 1800000
num: 1000 num: 1000
chset:
interval: 1800000
num: 1024
n:
interval: 1000
num: 512
crown: crown:
normal: normal:
a: 600 a: 600
m: 50 m: 50
ch: 1000 ch: 1000
kickban: 250 kickban: 125
t: 7.8125
+ls: 16.666666666666668
-ls: 16.666666666666668
chown: 2000
hi: 50
bye: 50
devices: 50
admin_message: 50
admin message: 50
chains: chains:
userset: userset:
interval: 1800000 interval: 1800000
num: 1000 num: 1000
chset:
interval: 1800000
num: 1024
n:
interval: 1000
num: 512
admin: admin:
normal: normal:
a: 120 a: 120
m: 16.666666666666668 m: 16.666666666666668
ch: 100 ch: 100
kickban: 31.25 kickban: 16.666666666666668
t: 3.90625
+ls: 16.666666666666668
-ls: 16.666666666666668
chown: 500
hi: 50
bye: 50
devices: 50
admin_message: 16.666666666666668
admin message: 16.666666666666668
chains: chains:
userset: userset:
interval: 1800000 interval: 500
num: 1000 num: 1000
chset:
interval: 1800000
num: 1024
n:
interval: 50
num: 512

View File

@ -16,6 +16,7 @@ model User {
name String @default("Anonymous") name String @default("Anonymous")
color String @default("#ffffff") color String @default("#ffffff")
flags String @default("{}") // JSON flags object flags String @default("{}") // JSON flags object
tag String // JSON tag
} }
model ChatHistory { model ChatHistory {

View File

@ -7,7 +7,8 @@ import {
Participant, Participant,
ServerEvents, ServerEvents,
IChannelInfo, IChannelInfo,
Notification Notification,
UserFlags
} 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";
@ -17,7 +18,7 @@ import { ChannelList } from "./ChannelList";
import { config } from "./config"; import { config } from "./config";
import { saveChatHistory, getChatHistory } from "../data/history"; import { saveChatHistory, getChatHistory } from "../data/history";
import { mixin } from "../util/helpers"; import { mixin } from "../util/helpers";
import { NoteQuota } from "../ws/ratelimit/NoteQuota"; import { readUser } from "../data/user";
interface CachedKickban { interface CachedKickban {
userId: string; userId: string;
@ -25,9 +26,17 @@ interface CachedKickban {
endTime: number; endTime: number;
} }
interface ExtraPartData {
uuids: string[];
flags: Partial<UserFlags>;
}
type ExtraPart = Participant & ExtraPartData;
export class Channel extends EventEmitter { export class Channel extends EventEmitter {
private settings: Partial<IChannelSettings>; private settings: Partial<IChannelSettings>;
private ppl = new Array<Participant & { uuids: string[] }>(); private ppl = new Array<ExtraPart>();
public chatHistory = new Array<ClientEvents["a"]>(); public chatHistory = new Array<ClientEvents["a"]>();
private async loadChatHistory() { private async loadChatHistory() {
@ -39,6 +48,8 @@ export class Channel extends EventEmitter {
public crown?: Crown; public crown?: Crown;
private flags: Record<string, any> = {};
constructor( constructor(
private _id: string, private _id: string,
set?: Partial<IChannelSettings>, set?: Partial<IChannelSettings>,
@ -55,6 +66,8 @@ export class Channel extends EventEmitter {
// Validate settings in set // Validate settings in set
// Set the verified settings // Set the verified settings
this.logger.debug("lobby me?", this.isLobby());
if (!this.isLobby()) { if (!this.isLobby()) {
if (set) { if (set) {
//this.logger.debug("Passed settings:", set); //this.logger.debug("Passed settings:", set);
@ -185,6 +198,15 @@ export class Channel extends EventEmitter {
return false; return false;
} }
/**
* Determine whether this channel is a lobby with the name "lobby" in it
*/
public isTrueLobby() {
if (this.getID().match("^lobby[0-9][0-9]$") && this.getID().match("^lobby[1-9]$")) return true;
return false;
}
/** /**
* Change this channel's settings * Change this channel's settings
* @param set Channel settings * @param set Channel settings
@ -265,7 +287,7 @@ export class Channel extends EventEmitter {
* @param socket Socket that is joining * @param socket Socket that is joining
* @returns undefined * @returns undefined
*/ */
public join(socket: Socket): void { public join(socket: Socket, force: boolean = false): void {
//! /!\ Players are forced to join the same channel on two different tabs! //! /!\ Players are forced to join the same channel on two different tabs!
//? TODO Should this be a bug or a feature? //? TODO Should this be a bug or a feature?
//this.logger.debug("join triggered"); //this.logger.debug("join triggered");
@ -274,17 +296,28 @@ export class Channel extends EventEmitter {
const part = socket.getParticipant() as Participant; const part = socket.getParticipant() as Participant;
let hasChangedChannel = false; let hasChangedChannel = false;
let oldChannelID = socket.currentChannelID;
// Is user banned? if (!force) {
if (this.isBanned(part._id)) { // Is user banned?
// Send user to ban channel instead if (this.isBanned(part._id)) {
// TODO Send notification for ban // Send user to ban channel instead
const chs = ChannelList.getList(); // TODO Send notification for ban
for (const ch of chs) { const chs = ChannelList.getList();
const chid = ch.getID(); for (const ch of chs) {
if (chid == config.fullChannel) { const chid = ch.getID();
return socket.setChannel(chid) if (chid == config.fullChannel) {
return socket.setChannel(chid)
}
}
}
// Is the channel full?
if (this.isFull()) {
// Is this a genuine lobby (not a test/ room)?
if (this.isTrueLobby()) {
const nextID = this.getNextIncrementationFromID();
this.logger.debug("New ID:", nextID);
return socket.setChannel(nextID, undefined, true)
} }
} }
} }
@ -311,7 +344,8 @@ export class Channel extends EventEmitter {
color: part.color, color: part.color,
id: part.id, id: part.id,
tag: part.tag, tag: part.tag,
uuids: [socket.getUUID()] uuids: [socket.getUUID()],
flags: socket.getUserFlags() || {}
}); });
} else { } else {
if (socket.currentChannelID !== config.fullChannel) { if (socket.currentChannelID !== config.fullChannel) {
@ -331,7 +365,7 @@ export class Channel extends EventEmitter {
if (hasChangedChannel) { if (hasChangedChannel) {
// Were they in a channel before? // Were they in a channel before?
if (socket.currentChannelID) { if (socket.currentChannelID) {
// Find the channel they were in // Find the other channel they were in
const ch = ChannelList.getList().find( const ch = ChannelList.getList().find(
ch => ch._id == socket.currentChannelID ch => ch._id == socket.currentChannelID
); );
@ -340,7 +374,7 @@ export class Channel extends EventEmitter {
if (ch) ch.leave(socket); if (ch) ch.leave(socket);
} }
// Change the thing we checked to point to us now // You belong here now
socket.currentChannelID = this.getID(); socket.currentChannelID = this.getID();
} }
@ -443,9 +477,9 @@ export class Channel extends EventEmitter {
public isFull() { public isFull() {
// TODO Use limit setting // TODO Use limit setting
// if (this.isLobby() && this.ppl.length >= 20) { if (this.isTrueLobby() && this.ppl.length >= 20) {
// return true; return true;
// } }
return false; return false;
} }
@ -472,13 +506,20 @@ export class Channel extends EventEmitter {
* @returns List of people * @returns List of people
*/ */
public getParticipantList() { public getParticipantList() {
return this.ppl.map(p => ({ const ppl = [];
id: p.id,
_id: p._id, for (const p of this.ppl) {
name: p.name, if (p.flags.vanish) continue;
color: p.color, ppl.push({
tag: p.tag _id: p._id,
})); name: p.name,
color: p.color,
id: p.id,
tag: config.sendTags ? p.tag : undefined
});
}
return ppl;
} }
public getParticipantListUnsanitized() { public getParticipantListUnsanitized() {
@ -626,7 +667,7 @@ export class Channel extends EventEmitter {
} }
/** /**
* Drop the crown (remove from user) * Drop the crown (reset timer, and, if applicable, remove from user's head)
*/ */
public dropCrown() { public dropCrown() {
if (this.crown) { if (this.crown) {
@ -780,6 +821,11 @@ export class Channel extends EventEmitter {
} }
} }
/**
* Check if a user is banned here right now
* @param _id User ID
* @returns True if the user is banned, otherwise false
**/
public isBanned(_id: string) { public isBanned(_id: string) {
const now = Date.now(); const now = Date.now();
@ -799,6 +845,9 @@ export class Channel extends EventEmitter {
return false; return false;
} }
/**
* Clear the chat and chat history
**/
public async clearChat() { public async clearChat() {
this.chatHistory = []; this.chatHistory = [];
await saveChatHistory(this.getID(), this.chatHistory); await saveChatHistory(this.getID(), this.chatHistory);
@ -826,6 +875,11 @@ export class Channel extends EventEmitter {
}]); }]);
} }
/**
* Send a message in chat
* @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: ServerEvents["a"], p: Participant) {
if (!msg.message) return; if (!msg.message) return;
@ -848,6 +902,38 @@ export class Channel extends EventEmitter {
this.chatHistory.push(outgoing); this.chatHistory.push(outgoing);
await saveChatHistory(this.getID(), this.chatHistory); await saveChatHistory(this.getID(), this.chatHistory);
} }
/**
* Set a flag on this channel
* @param key Flag ID
* @param val Value of which the flag will be set to
**/
public setFlag(key: string, val: any) {
this.flags[key] = val;
}
/**
* Get a flag on this channel
* @param key Flag ID
* @returns Value of flag
*/
public getFlag(key: string) {
return this.flags[key];
}
/**
* Get the name of this channel where the number at the end is one higher than this one, given it ends with a number
**/
public getNextIncrementationFromID() {
try {
const id = this.getID();
const num = parseInt((id.match(/\d+$/) as string[])[0]);
return `${id.substring(0, id.length - num.toString().length)}${num + 1}`;
} catch (err) {
return config.fullChannel;
}
}
} }
export default Channel; export default Channel;

View File

@ -8,6 +8,8 @@ interface ChannelConfig {
lobbyRegexes: string[]; lobbyRegexes: string[];
lobbyBackdoor: string; lobbyBackdoor: string;
fullChannel: string; fullChannel: string;
sendLimit: boolean;
sendTags: boolean;
} }
export const config = loadConfig<ChannelConfig>("config/channels.yml", { export const config = loadConfig<ChannelConfig>("config/channels.yml", {
@ -27,8 +29,10 @@ export const config = loadConfig<ChannelConfig>("config/channels.yml", {
color2: "#001014", color2: "#001014",
visible: true visible: true
}, },
// Here's a terrifying fact: Brandon used parseInt to check lobby names in the OG server code // Here's a terrifying fact: Brandon used parseInt to check lobby names
lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[1-9]$", "^test/.+$"], lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[0-9]$", "^lobby$", "^test/.+$"],
lobbyBackdoor: "lolwutsecretlobbybackdoor", lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward" fullChannel: "test/awkward",
sendLimit: false,
sendTags: false,
}); });

View File

@ -1,16 +1,41 @@
import { Channel } from "./Channel"; import { Channel } from "./Channel";
import { config } from "./config"; import { config } from "./config";
import { Logger } from "../util/Logger";
// This shit was moved here to try to fix the unit tests segfaulting but it didn't work import { ChannelList } from "./ChannelList";
// Channel forceloader (cringe) // Channel forceloader (cringe)
let hasFullChannel = false;
for (const id of config.forceLoad) { const logger = new Logger("Channel Forceloader");
new Channel(id, undefined, undefined, undefined, true);
if (id == config.fullChannel) hasFullChannel = true; export function forceloadChannel(id: string) {
try {
logger.info("Forceloading", id);
new Channel(id, undefined, undefined, undefined, true);
return true;
} catch (err) {
return false;
}
} }
if (!hasFullChannel) { export function unforceloadChannel(id: string) {
new Channel(config.fullChannel, undefined, undefined, undefined, true); const ch = ChannelList.getList().find(ch => ch.getID() == id);
if (!ch) return false;
logger.info("Unloading", id);
ch.destroy();
return true;
}
export function loadDefaultForcedChannels() {
let hasFullChannel = false;
for (const id of config.forceLoad) {
forceloadChannel(id);
if (id == config.fullChannel) hasFullChannel = true;
}
if (!hasFullChannel) {
new Channel(config.fullChannel, undefined, undefined, undefined, true);
}
} }

View File

@ -1,20 +1,27 @@
import { User } from "@prisma/client"; import { User } from "@prisma/client";
import { prisma } from "./prisma"; import { prisma } from "./prisma";
import { UserFlags } from "../util/types"; import { Tag, UserFlags } from "../util/types";
export async function createUser( export async function createUser(
_id: string, _id: string,
name?: string, name?: string,
color?: string, color?: string,
flags?: UserFlags flags?: UserFlags,
tag?: Tag
) { ) {
return await prisma.user.create({ return await prisma.user.create({
data: { id: _id, name, color, flags: JSON.stringify(flags) } data: {
id: _id,
name,
color,
flags: JSON.stringify(flags) || "",
tag: JSON.stringify(tag) || ""
}
}); });
} }
export async function getUsers() { export async function getUsers() {
return await { return {
users: await prisma.user.findMany(), users: await prisma.user.findMany(),
count: await prisma.user.count() count: await prisma.user.count()
} }

View File

@ -6,10 +6,12 @@
// If you don't load the server first, bun will literally segfault // If you don't load the server first, bun will literally segfault
import "./ws/server"; import "./ws/server";
import "./channel/forceLoad"; import { loadDefaultForcedChannels } from "./channel/forceLoad";
import { Logger } from "./util/Logger"; import { Logger } from "./util/Logger";
const logger = new Logger("Main"); const logger = new Logger("Main");
logger.info("Loading default channels...");
loadDefaultForcedChannels();
import "./util/readline"; import "./util/readline";

0
src/util/Logger.ts Executable file → Normal file
View File

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

@ -1,18 +1,5 @@
/**
* Typedefs
*/
import { Socket } from "../ws/Socket"; import { Socket } from "../ws/Socket";
/**
* Halfway through this file, the same types have appeared again
*
* I am not a decent enough person to go looking down there, so
* good luck when you get there. Somehow I forgot what is even
* in this file, so don't come and ask me why something isn't
* defined correctly here.
*/
declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
declare type UserFlags = Partial<{ declare type UserFlags = Partial<{
@ -27,9 +14,17 @@ declare type UserFlags = Partial<{
cansetcrowns: number; cansetcrowns: number;
// new // new
"no note quota": number;
"no note rate limit": number; "no note rate limit": number;
"no cursor rate limit": number; "no cursor rate limit": number;
"no userset rate limit": number; "no userset rate limit": number;
mod: number;
admin: number;
vanish: number;
}>;
type ChannelFlags = Partial<{
limit: number;
}>; }>;
declare interface Tag { declare interface Tag {
@ -110,7 +105,6 @@ declare interface Crown {
endPos: Vector2; endPos: Vector2;
} }
// Events copied from Hri7566/mppclone-client typedefs
declare interface ServerEvents { declare interface ServerEvents {
a: { a: {
m: "a"; m: "a";
@ -238,19 +232,40 @@ declare interface ServerEvents {
remove?: true; remove?: true;
}; };
tag: {
m: "tag";
_id: string;
tag: {
text: string;
color: string;
};
}
clear_chat: { clear_chat: {
m: "clear_chat" m: "clear_chat";
}; };
notification: { notification: {
m: "notification" m: "notification";
targetChannel?: string; targetChannel?: string;
targetUser?: string; targetUser?: string;
} & Notification; } & Notification;
restart: { restart: {
m: "restart" m: "restart";
} };
forceload: {
m: "forceload";
_id: string;
};
ch_flag: {
m: "ch_flag";
_id?: string;
key: string;
value: any;
};
} }
declare interface ClientEvents { declare interface ClientEvents {

View File

@ -123,7 +123,7 @@ export class Socket extends EventEmitter {
return this.id; return this.id;
} }
public setChannel(_id: string, set?: Partial<IChannelSettings>) { public setChannel(_id: string, set?: Partial<IChannelSettings>, force: boolean = false) {
if (this.isDestroyed()) return; if (this.isDestroyed()) return;
if (this.currentChannelID === _id) { if (this.currentChannelID === _id) {
logger.debug("Guy in channel was already in"); logger.debug("Guy in channel was already in");
@ -155,7 +155,7 @@ export class Socket extends EventEmitter {
this this
); );
channel.join(this); channel.join(this, force);
} }
} }
@ -443,17 +443,25 @@ export class Socket extends EventEmitter {
ch.playNotes(msg, this); ch.playNotes(msg, this);
} }
private isSubscribedToChannelList = false;
public subscribeToChannelList() { public subscribeToChannelList() {
if (this.isSubscribedToChannelList) return;
ChannelList.subscribe(this.id); ChannelList.subscribe(this.id);
const firstList = ChannelList.getPublicList().map(v => const firstList = ChannelList.getPublicList().map(v =>
v.getInfo(this._id) v.getInfo(this._id)
); );
this.sendChannelList(firstList); this.sendChannelList(firstList);
this.isSubscribedToChannelList = true;
} }
public unsubscribeFromChannelList() { public unsubscribeFromChannelList() {
if (!this.isSubscribedToChannelList) return;
ChannelList.unsubscribe(this.id); ChannelList.unsubscribe(this.id);
this.isSubscribedToChannelList = false;
} }
public sendChannelList(list: IChannelInfo[], complete: boolean = true) { public sendChannelList(list: IChannelInfo[], complete: boolean = true) {
@ -476,7 +484,6 @@ export class Socket extends EventEmitter {
const channel = this.getCurrentChannel(); const channel = this.getCurrentChannel();
const part = this.getParticipant(); const part = this.getParticipant();
// this looks cool
if (!channel) return false; if (!channel) return false;
if (!channel.crown) return false; if (!channel.crown) return false;
if (!channel.crown.userId) return false; if (!channel.crown.userId) return false;
@ -514,6 +521,13 @@ export class Socket extends EventEmitter {
html: notif.html html: notif.html
}]); }]);
} }
public setTag(text: string, color: string) {
const user = this.getUser();
if (!user) return;
user.tag = JSON.stringify({ text, color });
updateUser(this.getUserID(), user);
}
} }
export const socketsBySocketID = new Map<string, Socket>(); export const socketsBySocketID = new Map<string, Socket>();

View File

@ -0,0 +1,20 @@
import { ChannelList } from "../../../../channel/ChannelList";
import { ServerEventListener } from "../../../../util/types";
export const ch_flag: ServerEventListener<"ch_flag"> = {
id: "ch_flag",
callback: async (msg, socket) => {
// Edit channel flag
let chid = msg._id;
if (typeof chid !== "string") {
const ch = socket.getCurrentChannel();
if (!ch) return;
chid = ch.getID();
}
const ch = ChannelList.getList().find(c => c.getID() == chid);
if (!ch) return;
}
};

View File

@ -0,0 +1,12 @@
import { forceloadChannel } from "../../../../channel/forceLoad";
import { ServerEventListener } from "../../../../util/types";
export const forceload: ServerEventListener<"forceload"> = {
id: "forceload",
callback: async (msg, socket) => {
// Forceload channel
if (typeof msg._id !== "string") return;
forceloadChannel(msg._id);
}
};

View File

@ -0,0 +1,29 @@
import { readUser, updateUser } from "../../../../data/user";
import { ServerEventListener } from "../../../../util/types";
import { findSocketsByUserID } from "../../../Socket";
export const tag: ServerEventListener<"tag"> = {
id: "tag",
callback: async (msg, socket) => {
// Change someone else's tag
const id = msg._id;
const tag = msg.tag;
if (typeof id !== "string") return;
if (typeof tag !== "object") return;
if (typeof tag["text"] !== "string") return;
if (typeof tag.color !== "string") return;
if (!tag.color.match(/^#[0-9a-f]{6}$/i)) return;
const user = await readUser(msg._id);
if (!user) return;
user.tag = JSON.stringify(tag);
await updateUser(id, user);
const toUpdate = findSocketsByUserID(id);
toUpdate.forEach(s => {
s.setTag(msg.tag.text, msg.tag.color);
});
}
};

View File

@ -12,6 +12,10 @@ export const plus_ls: ServerEventListener<"+ls"> = {
// and when I see their cursor disappear I'll know // and when I see their cursor disappear I'll know
// precisely where they went to follow them and to // precisely where they went to follow them and to
// annoy them in chat when I see them again. // annoy them in chat when I see them again.
if (socket.rateLimits) {
if (!socket.rateLimits.normal["+ls"].attempt()) return;
}
socket.subscribeToChannelList(); socket.subscribeToChannelList();
} }
}; };

View File

@ -4,6 +4,10 @@ export const minus_ls: ServerEventListener<"-ls"> = {
id: "-ls", id: "-ls",
callback: (msg, socket) => { callback: (msg, socket) => {
// Stop giving us the latest server forecast // Stop giving us the latest server forecast
if (socket.rateLimits) {
if (!socket.rateLimits.normal["-ls"].attempt()) return;
}
socket.unsubscribeFromChannelList(); socket.unsubscribeFromChannelList();
} }
}; };

View File

@ -1,13 +1,13 @@
import { Logger } from "../../../../util/Logger";
import env from "../../../../util/env"; import env from "../../../../util/env";
import { ServerEventListener } from "../../../../util/types"; import { ServerEventListener } from "../../../../util/types";
import { config } from "../../../usersConfig";
const logger = new Logger("Admin Message Handler");
export const admin_message: ServerEventListener<"admin message"> = { export const admin_message: ServerEventListener<"admin message"> = {
id: "admin message", id: "admin message",
callback: (msg, socket) => { callback: (msg, socket) => {
// Administrator control message
if (socket.rateLimits)
if (!socket.rateLimits.normal["admin message"].attempt()) return;
if (typeof msg.password !== "string") return; if (typeof msg.password !== "string") return;
if (msg.password !== env.ADMIN_PASS) return; if (msg.password !== env.ADMIN_PASS) return;

View File

@ -4,6 +4,9 @@ export const bye: ServerEventListener<"bye"> = {
id: "bye", id: "bye",
callback: (msg, socket) => { callback: (msg, socket) => {
// Leave server // Leave server
if (socket.rateLimits)
if (!socket.rateLimits.normal.bye.attempt()) return;
socket.destroy(); socket.destroy();
} }
}; };

View File

@ -7,6 +7,9 @@ export const chown: ServerEventListener<"chown"> = {
id: "chown", id: "chown",
callback: (msg, socket) => { callback: (msg, socket) => {
// Change channel ownership // Change channel ownership
if (socket.rateLimits)
if (!socket.rateLimits.normal["chown"].attempt()) return;
const ch = socket.getCurrentChannel(); const ch = socket.getCurrentChannel();
if (!ch) return; if (!ch) return;

View File

@ -4,6 +4,9 @@ export const chset: ServerEventListener<"chset"> = {
id: "chset", id: "chset",
callback: (msg, socket) => { callback: (msg, socket) => {
// Change channel settings // Change channel settings
if (socket.rateLimits)
if (!socket.rateLimits.chains.chset.attempt()) return;
if (typeof msg.set == "undefined") return; if (typeof msg.set == "undefined") return;
const ch = socket.getCurrentChannel(); const ch = socket.getCurrentChannel();

View File

@ -4,6 +4,9 @@ export const devices: ServerEventListener<"devices"> = {
id: "devices", id: "devices",
callback: (msg, socket) => { callback: (msg, socket) => {
// List of MIDI Devices (or software ports, because nobody can tell the difference) // List of MIDI Devices (or software ports, because nobody can tell the difference)
if (socket.rateLimits)
if (!socket.rateLimits.normal.devices.attempt()) return;
if (socket.gateway.hasSentDevices) return; if (socket.gateway.hasSentDevices) return;
socket.gateway.hasSentDevices = true; socket.gateway.hasSentDevices = true;
} }

View File

@ -5,9 +5,10 @@ export const hi: ServerEventListener<"hi"> = {
callback: (msg, socket) => { callback: (msg, socket) => {
// Handshake message // Handshake message
// TODO Hi message tokens // TODO Hi message tokens
// I'm not actually sure if I'm up for doing tokens,
// but if someone wants to submit a pull request, I if (socket.rateLimits)
// look forward to watching you do all the work if (!socket.rateLimits.normal.hi.attempt()) return;
if (socket.gateway.hasProcessedHi) return; if (socket.gateway.hasProcessedHi) return;
let part = socket.getParticipant(); let part = socket.getParticipant();

View File

@ -4,8 +4,10 @@ export const m: ServerEventListener<"m"> = {
id: "m", id: "m",
callback: (msg, socket) => { callback: (msg, socket) => {
// Cursor movement // Cursor movement
if (!socket.rateLimits) return; if (socket.rateLimits) {
if (!socket.rateLimits.normal.m.attempt()) return; if (!socket.rateLimits.normal.m.attempt()) return;
}
if (!msg.x || !msg.y) return; if (!msg.x || !msg.y) return;
let x = msg.x; let x = msg.x;

View File

@ -5,6 +5,9 @@ export const n: ServerEventListener<"n"> = {
id: "n", id: "n",
callback: (msg, socket) => { callback: (msg, socket) => {
// Piano note // Piano note
if (socket.rateLimits)
if (!socket.rateLimits.chains.n.attempt()) return;
if (!Array.isArray(msg.n)) return; if (!Array.isArray(msg.n)) return;
if (typeof msg.t !== "number") return; if (typeof msg.t !== "number") return;

View File

@ -4,11 +4,15 @@ export const t: ServerEventListener<"t"> = {
id: "t", id: "t",
callback: (msg, socket) => { callback: (msg, socket) => {
// Ping // Ping
if (socket.rateLimits)
if (!socket.rateLimits.normal.t.attempt()) return
if (msg.e) { if (msg.e) {
if (typeof msg.e !== "number") return; if (typeof msg.e !== "number") return;
} }
// Pong! // Pong
socket.sendArray([ socket.sendArray([
{ {
m: "t", m: "t",

View File

@ -7,14 +7,26 @@ export interface RateLimitConfigList<
RLC = { num: number; interval: number } RLC = { num: number; interval: number }
> { > {
normal: { normal: {
m: RL;
a: RL; a: RL;
m: RL;
ch: RL; ch: RL;
kickban: RL; kickban: RL;
t: RL;
"+ls": RL;
"-ls": RL;
chown: RL;
// weird limits
hi: RL;
bye: RL;
devices: RL;
"admin message": RL;
}; };
chains: { chains: {
userset: RLC; userset: RLC;
chset: RLC;
n: RLC; // not to be confused with NoteQuota
}; };
} }
@ -37,12 +49,29 @@ export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
a: 6000 / 4, a: 6000 / 4,
m: 1000 / 20, m: 1000 / 20,
ch: 1000 / 1, ch: 1000 / 1,
kickban: 1000 / 4 kickban: 1000 / 8,
t: 1000 / 128,
"+ls": 1000 / 60,
"-ls": 1000 / 60,
chown: 2000,
hi: 1000 / 20,
bye: 1000 / 20,
devices: 1000 / 20,
"admin message": 1000 / 20
}, },
chains: { chains: {
userset: { userset: {
interval: 1000 * 60 * 30, interval: 1000 * 60 * 30,
num: 1000 num: 1000
},
chset: {
interval: 1000 * 60 * 30,
num: 1024
},
n: {
interval: 1000,
num: 512
} }
} }
}, },
@ -51,12 +80,29 @@ export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
a: 6000 / 10, a: 6000 / 10,
m: 1000 / 20, m: 1000 / 20,
ch: 1000 / 1, ch: 1000 / 1,
kickban: 1000 / 4 kickban: 1000 / 8,
t: 1000 / 128,
"+ls": 1000 / 60,
"-ls": 1000 / 60,
chown: 2000,
hi: 1000 / 20,
bye: 1000 / 20,
devices: 1000 / 20,
"admin message": 1000 / 20
}, },
chains: { chains: {
userset: { userset: {
interval: 1000 * 60 * 30, interval: 1000 * 60 * 30,
num: 1000 num: 1000
},
chset: {
interval: 1000 * 60 * 30,
num: 1024
},
n: {
interval: 1000,
num: 512
} }
} }
}, },
@ -65,12 +111,29 @@ export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
a: 6000 / 50, a: 6000 / 50,
m: 1000 / 60, m: 1000 / 60,
ch: 1000 / 10, ch: 1000 / 10,
kickban: 1000 / 32 kickban: 1000 / 60,
t: 1000 / 256,
"+ls": 1000 / 60,
"-ls": 1000 / 60,
chown: 500,
hi: 1000 / 20,
bye: 1000 / 20,
devices: 1000 / 20,
"admin message": 1000 / 60
}, },
chains: { chains: {
userset: { userset: {
interval: 1000 * 60 * 30, interval: 500,
num: 1000 num: 1000
},
chset: {
interval: 1000 * 60 * 30,
num: 1024
},
n: {
interval: 50,
num: 512
} }
} }
} }

View File

@ -7,13 +7,32 @@ export const adminLimits: RateLimitConstructorList = {
a: () => new RateLimit(config.admin.normal.a), a: () => new RateLimit(config.admin.normal.a),
m: () => new RateLimit(config.admin.normal.m), m: () => new RateLimit(config.admin.normal.m),
ch: () => new RateLimit(config.admin.normal.ch), ch: () => new RateLimit(config.admin.normal.ch),
kickban: () => new RateLimit(config.admin.normal.kickban) kickban: () => new RateLimit(config.crown.normal.kickban),
t: () => new RateLimit(config.crown.normal.t),
"+ls": () => new RateLimit(config.crown.normal["+ls"]),
"-ls": () => new RateLimit(config.crown.normal["-ls"]),
chown: () => new RateLimit(config.crown.normal.chown),
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"])
}, },
chains: { chains: {
userset: () => userset: () =>
new RateLimitChain( new RateLimitChain(
config.admin.chains.userset.interval, config.admin.chains.userset.interval,
config.admin.chains.userset.num config.admin.chains.userset.num
),
chset: () =>
new RateLimitChain(
config.crown.chains.chset.interval,
config.crown.chains.userset.num
),
n: () =>
new RateLimitChain(
config.crown.chains.n.interval,
config.crown.chains.userset.num
) )
} }
}; };

View File

@ -6,13 +6,33 @@ export const crownLimits: RateLimitConstructorList = {
normal: { normal: {
a: () => new RateLimit(config.crown.normal.a), a: () => new RateLimit(config.crown.normal.a),
m: () => new RateLimit(config.crown.normal.m), m: () => new RateLimit(config.crown.normal.m),
ch: () => new RateLimit(config.crown.normal.ch) ch: () => new RateLimit(config.crown.normal.ch),
kickban: () => new RateLimit(config.crown.normal.kickban),
t: () => new RateLimit(config.crown.normal.t),
"+ls": () => new RateLimit(config.crown.normal["+ls"]),
"-ls": () => new RateLimit(config.crown.normal["-ls"]),
chown: () => new RateLimit(config.crown.normal.chown),
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"])
}, },
chains: { chains: {
userset: () => userset: () =>
new RateLimitChain( new RateLimitChain(
config.crown.chains.userset.interval, config.crown.chains.userset.interval,
config.crown.chains.userset.num config.crown.chains.userset.num
),
chset: () =>
new RateLimitChain(
config.crown.chains.chset.interval,
config.crown.chains.userset.num
),
n: () =>
new RateLimitChain(
config.crown.chains.n.interval,
config.crown.chains.userset.num
) )
} }
}; };

View File

@ -2,19 +2,37 @@ import { RateLimit } from "../RateLimit";
import { RateLimitChain } from "../RateLimitChain"; import { RateLimitChain } from "../RateLimitChain";
import { RateLimitConstructorList, config } from "../config"; import { RateLimitConstructorList, config } from "../config";
// All this does is instantiate rate limits from the config
export const userLimits: RateLimitConstructorList = { export const userLimits: RateLimitConstructorList = {
normal: { normal: {
a: () => new RateLimit(config.user.normal.a), a: () => new RateLimit(config.user.normal.a),
m: () => new RateLimit(config.user.normal.m), m: () => new RateLimit(config.user.normal.m),
ch: () => new RateLimit(config.user.normal.ch), ch: () => new RateLimit(config.user.normal.ch),
kickban: () => new RateLimit(config.user.normal.kickban) kickban: () => new RateLimit(config.user.normal.kickban),
t: () => new RateLimit(config.user.normal.t),
"+ls": () => new RateLimit(config.user.normal["+ls"]),
"-ls": () => new RateLimit(config.user.normal["-ls"]),
chown: () => new RateLimit(config.user.normal.chown),
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"])
}, },
chains: { chains: {
userset: () => userset: () =>
new RateLimitChain( new RateLimitChain(
config.user.chains.userset.interval, config.user.chains.userset.interval,
config.user.chains.userset.num config.user.chains.userset.num
),
chset: () =>
new RateLimitChain(
config.user.chains.chset.interval,
config.user.chains.userset.num
),
n: () =>
new RateLimitChain(
config.user.chains.n.interval,
config.user.chains.userset.num
) )
} }
}; };