Update everything

This commit is contained in:
Hri7566 2024-01-31 05:09:25 -05:00
parent 8644f3787e
commit 275ad3823f
50 changed files with 265 additions and 53 deletions

View File

@ -16,3 +16,8 @@ model User {
color String @default("#ffffff")
flags String @default("{}") // JSON flags object
}
model ChatHistory {
id String @id @unique @map("_id")
messages String @default("[]") // JSON messages
}

View File

@ -8,7 +8,7 @@ import {
ServerEvents,
IChannelInfo
} from "../util/types";
import { Socket } from "../ws/Socket";
import type { Socket } from "../ws/Socket";
import { validateChannelSettings } from "./settings";
import { findSocketByPartID, socketsBySocketID } from "../ws/Socket";
import Crown from "./Crown";
@ -185,8 +185,6 @@ export class Channel extends EventEmitter {
(typeof set.color2 == "undefined" ||
set.color2 === this.settings.color2)
) {
this.logger.debug("Setting color 2 from first color:", set.color);
this.logger.debug("Red:", parseInt(set.color.substring(1, 2), 16));
const r = Math.max(
0,
parseInt(set.color.substring(1, 3), 16) - 0x40
@ -201,7 +199,6 @@ export class Channel extends EventEmitter {
);
set.color2 = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
this.logger.debug("Color 2 is now:", set.color2);
}
if (this.isLobby() && !admin) return;

View File

@ -2,6 +2,7 @@ import { type Socket, findSocketByPartID } from "../ws/Socket";
import type Channel from "./Channel";
const onChannelUpdate = (channel: Channel) => {
// If this shit ever manages to handle over 10 people I'd be impressed
const info = channel.getInfo();
// const ppl = channel.getParticipantList();
if (info.settings.visible !== true) return;

View File

@ -1,6 +1,7 @@
import { Participant, Vector2 } from "../util/types";
import { Socket } from "../ws/Socket";
// shiny hat
export class Crown {
public userId: string | undefined;
public participantId: string | undefined;

View File

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

View File

@ -1,6 +1,8 @@
import { Channel } from "./Channel";
import { config } from "./config";
// This shit was moved here to try to fix the unit tests segfaulting but it didn't work
// Channel forceloader (cringe)
let hasFullChannel = false;

View File

@ -4,4 +4,6 @@ import validateChannelSettings, {
validate as validateChannelSetting
} from "./settings";
// I don't even know if I bothered to use this file
export { Channel, Crown, validateChannelSettings, validateChannelSetting };

View File

@ -3,6 +3,8 @@ import { IChannelSettings } from "../util/types";
type Validator = "boolean" | "string" | "number" | ((val: unknown) => boolean);
// This record contains the exact data Brandon used to check channel settings, down to the regex.
// It also contains things that might be useful to other people in the future (things that make me vomit)
const validationRecord: Record<keyof IChannelSettings, Validator> = {
// Brandon
lobby: "boolean",

View File

@ -4,6 +4,7 @@
* by Hri7566
*/
// If you don't load the server first, bun will literally segfault
import "./ws/server";
import "./channel/forceLoad";
import { Logger } from "./util/Logger";
@ -12,4 +13,5 @@ const logger = new Logger("Main");
import "./util/readline";
// Does this really even need to be here?
logger.info("Ready");

View File

@ -14,7 +14,7 @@ export class Logger {
...args
);
// Fix the readline prompt (spooky code)
// Fix the readline prompt (spooky cringe code)
if ((globalThis as unknown as any).rl)
(globalThis as unknown as any).rl.prompt();
}

View File

@ -2,6 +2,16 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
import { parse, stringify } from "yaml";
import { z } from "zod";
/**
* This file uses the synchronous functions from the fs
* module because these functions should only be used
* to load configs at the beginning of runtime
*
* Hint: This means you shouldn't load configs in the
* middle of doing other shit, only when you start the
* program.
*/
/**
* Load a YAML config file and set default values if config path is nonexistent
*
@ -64,6 +74,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
* @param config
*/
export function writeConfig<T>(configPath: string, config: T) {
// Write config to disk unconditionally
writeFileSync(
configPath,
stringify(config, {

View File

@ -1,6 +1,7 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
// Best way to do env ever
export const env = createEnv({
server: {
PORT: z.coerce.number(),
@ -9,6 +10,7 @@ export const env = createEnv({
ADMIN_PASS: z.string()
},
isServer: true,
// Bun loads process.env automatically so we don't have to use modules
runtimeEnv: process.env
});

View File

@ -40,3 +40,20 @@ export function darken(hex: string) {
.padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`;
} catch (err) {}
}
// Brandon made this literally eons ago and it's fucking hilarious
export function spoop_text(message: string) {
var old = message;
message = "";
for (var i = 0; i < old.length; i++) {
if (Math.random() < 0.9) {
message += String.fromCharCode(
old.charCodeAt(i) + Math.floor(Math.random() * 20 - 10)
);
//message[i] = String.fromCharCode(Math.floor(Math.random() * 255));
} else {
message += old[i];
}
}
return message;
}

View File

@ -1,8 +1,24 @@
import { createHash, randomBytes } from "crypto";
import env from "./env";
import { spoop_text } from "./helpers";
export function createID() {
return randomBytes(12).toString("hex");
// Maybe I could make this funnier than it needs to be...
// return randomBytes(12).toString("hex");
let weirdness = "";
while (weirdness.length < 24) {
const time = new Date().toString();
const randomShit = spoop_text(time); // looks like this: We]%Cau&:,\u0018403*"32>8,B15&GP[2)='7\u0019-@etyhlw\u0017QqXqiime&Khhe-
let index1 = Math.floor(Math.random() * randomShit.length);
let index2 = Math.floor(Math.random() * randomShit.length);
weirdness += randomShit.substring(index1, index2);
}
// Get 12 bytes
return Buffer.from(weirdness.substring(0, 12)).toString("hex");
}
export function createUserID(ip: string) {
@ -25,6 +41,6 @@ export function createColor(ip: string) {
.update(env.SALT)
.update("color")
.digest("hex")
.substring(0, 6)
.substring(0, 24 + 6)
);
}

View File

@ -1,3 +1,4 @@
// bruh
export function getMOTD() {
return "This site makes a lot of sound! You may want to adjust the volume before continuing.";
}

View File

@ -21,4 +21,5 @@ rl.on("SIGINT", () => {
process.exit();
});
// Fucking cringe but it works
(globalThis as unknown as any).rl = rl;

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

@ -1,5 +1,18 @@
/**
* Typedefs
*/
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 UserFlags = Partial<{

View File

@ -1,3 +1,16 @@
/**
* I made this thing to keep track of what sockets
* have and haven't done yet so we know if they
* should be doing certain things.
*
* For instance, being logged in in the first place,
* or if they're on shitty McDonalds WiFi and they
* lost connection for over a minute, or if they
* decided that they're going to put their browser
* in a chokehold and force it to load weird shit...
* or, you know, maybe I could log their user agent
* and IP address instead sometime in the future.
*/
export class Gateway {
public hasProcessedHi: boolean = false;
public hasSentDevices: boolean = false;

View File

@ -15,7 +15,7 @@ import {
UserFlags,
Vector2
} from "../util/types";
import { User } from "@prisma/client";
import type { User } from "@prisma/client";
import { createUser, readUser, updateUser } from "../data/user";
import { eventGroups } from "./events";
import { Gateway } from "./Gateway";
@ -55,10 +55,7 @@ export class Socket extends EventEmitter {
public currentChannelID: string | undefined;
private cursorPos: Vector2<CursorValue> = { x: 200, y: 100 };
constructor(
private ws: ServerWebSocket<unknown>,
public socketID: string
) {
constructor(private ws: ServerWebSocket<unknown>, public socketID: string) {
super();
this.ip = ws.remoteAddress; // Participant ID
@ -432,6 +429,22 @@ export class Socket extends EventEmitter {
}
]);
}
public isOwner() {
const channel = this.getCurrentChannel();
const part = this.getParticipant();
// this looks cool
if (!channel) return false;
if (!channel.crown) return false;
if (!channel.crown.userId) return false;
if (!channel.crown.participantId) return false;
if (!part) return;
if (!part.id) return;
if (channel.crown.participantId !== part.id) return false;
return true;
}
}
export const socketsBySocketID = new Map<string, Socket>();

View File

@ -1,4 +1,8 @@
// Bun hoists import so we are kinda forced to use require here...
// Maybe bun should have a setting for that :/
// This feels really dirty and months have passed... guess who's fixing it?
// Nobody is. It's going to stay like this.
require("./events/user");
require("./events/admin");

View File

@ -8,6 +8,10 @@ export class EventGroup {
this.eventList.push(listener);
}
public addMany(...listeners: ServerEventListener<any>[]) {
listeners.forEach(l => this.add(l));
}
public remove(listener: ServerEventListener<any>) {
this.eventList.splice(this.eventList.indexOf(listener), 1);
}

View File

@ -5,7 +5,7 @@ import { findSocketsByUserID } from "../../../Socket";
export const color: ServerEventListener<"color"> = {
id: "color",
callback: async (msg, socket) => {
// TODO color
// I'm not allowed to use this feature on MPP.net for a really stupid reason
const id = msg._id;
const color = msg.color;

View File

@ -5,6 +5,7 @@ import { findSocketsByUserID } from "../../../Socket";
export const name: ServerEventListener<"name"> = {
id: "name",
callback: async (msg, socket) => {
// Change someone else's name but it's an annoying admin feature
const id = msg._id;
const name = msg.name;

View File

@ -5,6 +5,7 @@ import { findSocketsByUserID } from "../../../Socket";
export const user_flag: ServerEventListener<"user_flag"> = {
id: "user_flag",
callback: async (msg, socket) => {
// User flag modification (changing some real specific shit)
if (typeof msg._id !== "string") return;
if (typeof msg.key !== "string") return;
if (typeof msg.value == "undefined") return;

View File

@ -6,8 +6,10 @@ import { color } from "./handlers/color";
import { name } from "./handlers/name";
import { user_flag } from "./handlers/user_flag";
EVENT_GROUP_ADMIN.add(color);
EVENT_GROUP_ADMIN.add(name);
EVENT_GROUP_ADMIN.add(user_flag);
// EVENT_GROUP_ADMIN.add(color);
// EVENT_GROUP_ADMIN.add(name);
// EVENT_GROUP_ADMIN.add(user_flag);
EVENT_GROUP_ADMIN.addMany(color, name, user_flag);
eventGroups.push(EVENT_GROUP_ADMIN);

View File

@ -3,6 +3,15 @@ import { ServerEventListener } from "../../../../util/types";
export const plus_ls: ServerEventListener<"+ls"> = {
id: "+ls",
callback: (msg, socket) => {
// Give us the latest news on literally everything
// that's happening in the server. In fact, I want
// to know when someone clicks a button instantly,
// so I can stalk other users by watching the room
// count go up somewhere else when I watch someone
// leave the channel I'm reading their messages in
// and when I see their cursor disappear I'll know
// precisely where they went to follow them and to
// annoy them in chat when I see them again.
socket.subscribeToChannelList();
}
};

View File

@ -3,6 +3,7 @@ import { ServerEventListener } from "../../../../util/types";
export const minus_ls: ServerEventListener<"-ls"> = {
id: "-ls",
callback: (msg, socket) => {
// Stop giving us the latest server forecast
socket.unsubscribeFromChannelList();
}
};

View File

@ -7,11 +7,15 @@ export const a: ServerEventListener<"a"> = {
const flags = socket.getUserFlags();
if (!flags) return;
// Why did I write this statement so weird
if (!flags["no chat rate limit"] || flags["no chat rate limit"] == 0)
if (!socket.rateLimits?.normal.a.attempt()) return;
const ch = socket.getCurrentChannel();
if (!ch) return;
// msg.m
// Permission denied: msg.m
// sudo msg.m
ch.emit("message", msg, socket);
}
};

View File

@ -11,6 +11,8 @@ export const admin_message: ServerEventListener<"admin message"> = {
if (typeof msg.password !== "string") return;
if (msg.password !== env.ADMIN_PASS) return;
// Probably shouldn't be using password auth in 2024
// Maybe I'll setup a dashboard instead some day
socket.admin.emit(msg.msg.m, msg.msg, socket, true);
}
};

View File

@ -5,6 +5,8 @@ export const ch: ServerEventListener<"ch"> = {
callback: (msg, socket) => {
// Switch channel
if (!msg._id) return;
// So technical and convoluted...
socket.setChannel(msg._id, msg.set);
}
};

View File

@ -5,8 +5,11 @@ export const chset: ServerEventListener<"chset"> = {
callback: (msg, socket) => {
// Change channel settings
if (typeof msg.set == "undefined") return;
const ch = socket.getCurrentChannel();
if (!ch) return;
// Edit room now
ch.changeSettings(msg.set, false);
}
};

View File

@ -3,7 +3,7 @@ import { ServerEventListener } from "../../../../util/types";
export const devices: ServerEventListener<"devices"> = {
id: "devices",
callback: (msg, socket) => {
// List of MIDI Devices
// List of MIDI Devices (or software ports, because nobody can tell the difference)
if (socket.gateway.hasSentDevices) return;
socket.gateway.hasSentDevices = true;
}

View File

@ -5,6 +5,9 @@ export const hi: ServerEventListener<"hi"> = {
callback: (msg, socket) => {
// Handshake message
// 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
// look forward to watching you do all the work
if (socket.gateway.hasProcessedHi) return;
let part = socket.getParticipant();

View File

@ -4,15 +4,18 @@ export const m: ServerEventListener<"m"> = {
id: "m",
callback: (msg, socket) => {
// Cursor movement
if (!socket.rateLimits?.normal.m.attempt()) return;
if (!socket.rateLimits) return;
if (!socket.rateLimits.normal.m.attempt()) return;
if (!msg.x || !msg.y) return;
let x = msg.x;
let y = msg.y;
// Make it numbers
if (typeof msg.x == "string") x = parseFloat(msg.x);
if (typeof msg.y == "string") y = parseFloat(msg.y);
// Move the laggy piece of shit
socket.setCursorPos(x, y);
}
};

View File

@ -8,6 +8,10 @@ export const n: ServerEventListener<"n"> = {
if (!Array.isArray(msg.n)) return;
if (typeof msg.t !== "number") return;
// This should've been here months ago
const channel = socket.getCurrentChannel();
if (!channel) return;
// Check note properties
for (const n of msg.n) {
if (typeof n.n != "string") return;
@ -29,9 +33,15 @@ export const n: ServerEventListener<"n"> = {
let amount = msg.n.length;
// TODO Check crownsolo
const crownsolo = channel.getSetting("crownsolo");
if ((crownsolo && socket.isOwner()) || !crownsolo) {
// Shiny hat exists and we have shiny hat
// or there is no shiny hat
if (socket.noteQuota.spend(amount)) {
// make noise
socket.playNotes(msg);
}
}
}
};

View File

@ -8,6 +8,7 @@ export const t: ServerEventListener<"t"> = {
if (typeof msg.e !== "number") return;
}
// Pong!
socket.sendArray([
{
m: "t",

View File

@ -5,6 +5,15 @@ export const userset: ServerEventListener<"userset"> = {
callback: (msg, socket) => {
// Change username/color
if (!socket.rateLimits?.chains.userset.attempt()) return;
// You can disable color in the config because
// Brandon's/jacored's server doesn't allow color changes,
// and that's the OG server, but folks over at MPP.net
// said otherwise because they're dumb roleplayers
// or something and don't understand the unique value
// of the fishing bot and how it allows you to change colors
// without much control, giving it the feeling of value...
// Kinda reminds me of crypto.
// Also, Brandon's server had color changing on before.
if (!msg.set.name && !msg.set.color) return;
socket.userset(msg.set.name, msg.set.color);
}

View File

@ -14,16 +14,33 @@ import { minus_ls } from "./handlers/-ls";
import { admin_message } from "./handlers/admin_message";
import { chset } from "./handlers/chset";
EVENTGROUP_USER.add(hi);
EVENTGROUP_USER.add(devices);
EVENTGROUP_USER.add(ch);
EVENTGROUP_USER.add(m);
EVENTGROUP_USER.add(a);
EVENTGROUP_USER.add(userset);
EVENTGROUP_USER.add(n);
EVENTGROUP_USER.add(plus_ls);
EVENTGROUP_USER.add(minus_ls);
EVENTGROUP_USER.add(admin_message);
EVENTGROUP_USER.add(chset);
// Imagine not having an "addMany" function...
// EVENTGROUP_USER.add(hi);
// EVENTGROUP_USER.add(devices);
// EVENTGROUP_USER.add(ch);
// EVENTGROUP_USER.add(m);
// EVENTGROUP_USER.add(a);
// EVENTGROUP_USER.add(userset);
// EVENTGROUP_USER.add(n);
// EVENTGROUP_USER.add(plus_ls);
// EVENTGROUP_USER.add(minus_ls);
// EVENTGROUP_USER.add(admin_message);
// EVENTGROUP_USER.add(chset);
// Imagine it looks exactly the same and calls the same function underneath
EVENTGROUP_USER.addMany(
hi,
devices,
ch,
m,
a,
userset,
n,
plus_ls,
minus_ls,
admin_message,
chset
);
eventGroups.push(EVENTGROUP_USER);

View File

@ -2,7 +2,9 @@ import { Logger } from "../util/Logger";
import { Socket } from "./Socket";
import { hasOwn } from "../util/helpers";
const logger = new Logger("Message Handler");
// const logger = new Logger("Message Handler");
// this one sounds cooler
const logger = new Logger("The Messenger");
export function handleMessage(socket: Socket, text: string) {
try {

View File

@ -1,3 +1,6 @@
// 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.......
export class NoteQuota {
public allowance = 8000;
public max = 24000;

View File

@ -1,3 +1,4 @@
// Thank you Brandon for this thing
export class RateLimit {
public after: number = 0;
constructor(private interval_ms: number = 0) {}

View File

@ -1,5 +1,6 @@
import { RateLimit } from "./RateLimit";
// Thank you Brandon for this other thing
export class RateLimitChain {
public chain: RateLimit[] = [];

View File

@ -47,7 +47,8 @@ export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
crown: {
normal: {
a: 6000 / 10,
m: 1000 / 20
m: 1000 / 20,
ch: 1000 / 1
},
chains: {
userset: {
@ -59,7 +60,8 @@ export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
admin: {
normal: {
a: 6000 / 50,
m: 1000 / 60
m: 1000 / 60,
ch: 1000 / 10
},
chains: {
userset: {

View File

@ -5,7 +5,8 @@ import { RateLimitConstructorList, config } from "../config";
export const adminLimits: RateLimitConstructorList = {
normal: {
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)
},
chains: {
userset: () =>

View File

@ -5,7 +5,8 @@ import { RateLimitConstructorList, config } from "../config";
export const crownLimits: RateLimitConstructorList = {
normal: {
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)
},
chains: {
userset: () =>

View File

@ -2,6 +2,9 @@ import { RateLimit } from "../RateLimit";
import { RateLimitChain } from "../RateLimitChain";
import { RateLimitConstructorList, config } from "../config";
// I have no idea why these things exist but I made it apparently
// All it does it construct the rate limits from the config instead
// of using random numbers I found on the internet
export const userLimits: RateLimitConstructorList = {
normal: {
a: () => new RateLimit(config.user.normal.a),

View File

@ -10,7 +10,15 @@ import nunjucks from "nunjucks";
const logger = new Logger("WebSocket Server");
/**
* Get a rendered version of the index file
* @returns Response with html in it
*/
async function getIndex() {
// This tiny function took like an hour to write because
// nobody realistically uses templates in 2024 and documents
// it well enough to say what library they used
const index = Bun.file("./public/index.html");
const rendered = nunjucks.renderString(await index.text(), {
@ -19,6 +27,7 @@ async function getIndex() {
const response = new Response(rendered);
response.headers.set("Content-Type", "text/html");
return response;
}
@ -30,11 +39,21 @@ export const app = Bun.serve({
return;
} else {
const url = new URL(req.url).pathname;
// lol
// const ip = decoder.decode(res.getRemoteAddressAsText());
// logger.debug(`${req.getMethod()} ${url} ${ip}`);
// res.writeStatus(`200 OK`).end("HI!");
// I have no clue if this is even safe...
// wtf do I do when the user types "/../.env" in the URL?
// From my testing, nothing out of the ordinary happens...
// but just in case, if you find something wrong with URLs,
// this is the most likely culprit
const file = path.join("./public/", url);
// Time for unreadable blocks of confusion
try {
if (fs.lstatSync(file).isFile()) {
const data = Bun.file(file);
@ -54,20 +73,33 @@ export const app = Bun.serve({
},
websocket: {
open: ws => {
// We got one!
const socket = new Socket(ws, createSocketID());
// Reel 'em in...
(ws as unknown as any).socket = socket;
// logger.debug("Connection at " + socket.getIP());
// Let's put it in the dinner bucket.
socketsBySocketID.set(socket.socketID, socket);
},
message: (ws, message) => {
// "Let's make it binary" said all websocket developers for some reason
const msg = message.toString();
// Let's find out wtf they even sent
handleMessage((ws as unknown as any).socket, msg);
},
close: (ws, code, message) => {
// logger.debug("Close called");
// This usually gets called when someone leaves,
// but it's also used internally just in case
// some dickhead can't close their tab like a
// normal person.
const socket = (ws as unknown as any).socket as Socket;
if (socket) {
socket.destroy();

View File

@ -20,6 +20,12 @@ export const defaultUsersConfig = {
};
// Importing this elsewhere causes bun to segfault
// Now that I look back at this, using this elsewhere
// before calling other things tends to make bun segfault?
// Not dealing with it. The code somehow runs, and I'm not
// going to fuck with the order of loading things until bun
// pushes an update and fucks all this stuff up.
export const config = loadConfig<UsersConfig>(
usersConfigPath,
defaultUsersConfig

View File

@ -1,15 +0,0 @@
import { test, expect } from "bun:test";
import { Channel } from "../../src/channel/Channel";
test("Channel is created correctly", () => {
const channel = new Channel("my room");
expect(channel.getID()).toBe("my room");
const info = channel.getInfo();
expect(info.id).toBe("my room");
expect(info._id).toBe("my room");
expect(info.count).toBe(0);
const ppl = channel.getParticipantList();
expect(ppl).toBeEmpty();
});

View File

@ -1 +0,0 @@
export class FakeSocket {}