diff --git a/.gitignore b/.gitignore index c0a4cab..92aff95 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,8 @@ node_modules # SQLite databases prisma/*.sqlite -# TS build -/out +# Build script output +/dist # JWT token keypair mppkey diff --git a/config/permissions.yml b/config/permissions.yml new file mode 100644 index 0000000..c4df6ea --- /dev/null +++ b/config/permissions.yml @@ -0,0 +1,9 @@ +admin: + - clearChat + - vanish + - chsetAnywhere + - chownAnywhere + - usersetOthers + - siteBan + - siteBanAnyReason + - siteBanAnyDuration diff --git a/config/prometheus.yml b/config/prometheus.yml deleted file mode 100644 index 6687145..0000000 --- a/config/prometheus.yml +++ /dev/null @@ -1,12 +0,0 @@ -global: - scrape_interval: 15s - -scrape_configs: - - job_name: prometheus - scrape_interval: "5s" - static_configs: - - targets: ["localhost:9090"] - - - job_name: mpp - static_configs: - - targets: ["192.168.1.24:9100"] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 77fc6be..f393962 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,8 +27,19 @@ model ChatHistory { } model Channel { - id String @id @unique @map("_id") - settings String @default("{}") // JSON channel settings + id String @id @unique @map("_id") + settings String @default("{}") // JSON channel settings forceload Boolean @default(false) // Whether the channel is forceloaded - flags String @default("{}") // JSON flags object + flags String @default("{}") // JSON flags object +} + +model Role { + userId String @unique + roleId String +} + +model RolePermission { + id Int @id @unique @default(autoincrement()) + roleId String + permission String } diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..fcb3ffc --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,12 @@ +console.log("Building..."); + +await Bun.build({ + entrypoints: ["./src/index.ts"], + outdir: "./dist", + target: "bun", + format: "esm", + minify: false, + splitting: true +}); + +console.log("Done"); diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index 24f1e2d..c68defe 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -1,6 +1,6 @@ import EventEmitter from "events"; import { Logger } from "../util/Logger"; -import { +import type { ChannelSettingValue, IChannelSettings, ClientEvents, @@ -10,19 +10,28 @@ import { Notification, UserFlags, Tag, + ChannelFlags as TChannelFlags } from "../util/types"; import type { Socket } from "../ws/Socket"; import { validateChannelSettings } from "./settings"; -import { findSocketByPartID, socketsBySocketID } from "../ws/Socket"; +import { findSocketByPartID, socketsByUUID } from "../ws/Socket"; import Crown from "./Crown"; import { ChannelList } from "./ChannelList"; import { config } from "./config"; import { config as usersConfig } from "../ws/usersConfig"; -import { saveChatHistory, getChatHistory, deleteChatHistory } from "../data/history"; -import { mixin, darken } from "../util/helpers"; -import { User } from "@prisma/client"; +import { + saveChatHistory, + getChatHistory, + deleteChatHistory +} from "../data/history"; +import { mixin, darken, spoop_text } from "../util/helpers"; +import type { User } from "@prisma/client"; import { heapStats } from "bun:jsc"; -import { deleteSavedChannel, getSavedChannel, saveChannel } from "../data/channel"; +import { + deleteSavedChannel, + getSavedChannel, + saveChannel +} from "../data/channel"; import { forceloadChannel } from "./forceLoad"; interface CachedKickban { @@ -31,9 +40,15 @@ interface CachedKickban { endTime: number; } +interface CachedCursor { + x: string | number; + y: string | number; + id: string; +} + interface ExtraPartData { uuids: string[]; - flags: Partial; + flags: UserFlags; } type ExtraPart = Participant & ExtraPartData; @@ -48,23 +63,25 @@ export class Channel extends EventEmitter { try { this.chatHistory = await getChatHistory(this.getID()); - this.sendArray([{ - m: "c", - c: this.chatHistory - }]); - } catch (err) { } + this.sendArray([ + { + m: "c", + c: this.chatHistory + } + ]); + } catch (err) {} } private async deleteChatHistory() { try { await deleteChatHistory(this.getID()); - } catch (err) { } + } catch (err) {} } private async deleteData() { try { await deleteSavedChannel(this.getID()); - } catch (err) { } + } catch (err) {} } private async save() { @@ -107,27 +124,27 @@ export class Channel extends EventEmitter { this.logger.error("Error loading channel data:", err); } } - } catch (err) { } + } catch (err) {} } public logger: Logger; public bans = new Array(); - public cursorCache = new Array<{ x: string | number; y: string | number; id: string }>(); + public cursorCache = new Array(); public crown?: Crown; - private flags: Record = {}; + private flags: TChannelFlags = {}; constructor( private _id: string, set?: Partial, creator?: Socket, owner_id?: string, - public stays: boolean = false + public stays = false ) { super(); - this.logger = new Logger("Channel - " + _id, "logs/channel"); + this.logger = new Logger(`Channel - ${_id}`, "logs/channel"); this.settings = {}; // Copy default settings @@ -138,8 +155,8 @@ export class Channel extends EventEmitter { // Copied from changeSettings below // TODO do these cases need to be here? can this be determined another way? if ( - typeof set.color == "string" && - (typeof set.color2 == "undefined" || + typeof set.color === "string" && + (typeof set.color2 === "undefined" || set.color2 === this.settings.color2) ) { //this.logger.debug("color 2 darken triggered"); @@ -152,8 +169,8 @@ export class Channel extends EventEmitter { // Set the verified settings for (const key of Object.keys(validatedSet)) { //this.logger.debug(`${key}: ${(validatedSet as any)[key]}`); - if ((validatedSet as any)[key] === false) continue; - (this.settings as any)[key] = (set as any)[key]; + if (validatedSet[key] === false) continue; + this.settings[key] = set[key]; } } @@ -172,11 +189,13 @@ export class Channel extends EventEmitter { this.bindEventListeners(); ChannelList.add(this); - this.settings.owner_id = this.flags["owner_id"]; + if (this.flags.owner_id) { + this.settings.owner_id = this.flags.owner_id; + } this.logger.info("Created"); - if (this.getID() == "test/mem") { + if (this.getID() === "test/mem") { setInterval(() => { this.printMemoryInChat(); }, 1000); @@ -194,7 +213,7 @@ export class Channel extends EventEmitter { this.on("update", (self, uuid) => { // Send updated info - for (const socket of socketsBySocketID.values()) { + for (const socket of socketsByUUID.values()) { for (const p of this.ppl) { const socketUUID = socket.getUUID(); @@ -210,7 +229,7 @@ export class Channel extends EventEmitter { } } - if (this.ppl.length == 0 && !this.stays) { + if (this.ppl.length === 0 && !this.stays) { if (config.channelDestroyTimeout) { setTimeout(() => { this.destroy(); @@ -221,10 +240,7 @@ export class Channel extends EventEmitter { } }); - const BANNED_WORDS = [ - "AMIGHTYWIND", - "CHECKLYHQ" - ]; + const BANNED_WORDS = ["AMIGHTYWIND", "CHECKLYHQ"]; this.on("a", async (msg: ServerEvents["a"], socket: Socket) => { try { @@ -233,7 +249,13 @@ export class Channel extends EventEmitter { const userFlags = socket.getUserFlags(); if (userFlags) { - if (userFlags.cant_chat) return; + 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) + msg.message = spoop_text(msg.message); } if (!this.settings.chat) return; @@ -241,7 +263,13 @@ export class Channel extends EventEmitter { if (msg.message.length > 512) return; for (const word of BANNED_WORDS) { - if (msg.message.toLowerCase().split(" ").join("").includes(word.toLowerCase())) { + if ( + msg.message + .toLowerCase() + .split(" ") + .join("") + .includes(word.toLowerCase()) + ) { return; } } @@ -256,7 +284,7 @@ export class Channel extends EventEmitter { const part = socket.getParticipant() as Participant; - let outgoing: ClientEvents["a"] = { + const outgoing: ClientEvents["a"] = { m: "a", a: msg.message, t: Date.now(), @@ -274,7 +302,9 @@ export class Channel extends EventEmitter { } } catch (err) { this.logger.error(err); - this.logger.warn("Error whilst processing a chat message from user " + socket.getUserID()); + this.logger.warn( + `Error whilst processing a chat message from user ${socket.getUserID()}` + ); } }); @@ -283,8 +313,8 @@ export class Channel extends EventEmitter { const cmd = args[0].substring(1); const ownsChannel = this.hasUser(socket.getUserID()); - if (cmd == "help") { - } else if (cmd == "mem") { + if (cmd === "help") { + } else if (cmd === "mem") { this.printMemoryInChat(); } }); @@ -294,19 +324,27 @@ export class Channel extends EventEmitter { if (typeof user.name !== "string") return; if (typeof user.color !== "string") return; if (typeof user.id !== "string") return; - if (typeof user.tag !== "undefined" && typeof user.tag !== "string") return; - if (typeof user.flags !== "undefined" && typeof user.flags !== "string") return; + if ( + typeof user.tag !== "undefined" && + typeof user.tag !== "string" + ) + return; + if ( + typeof user.flags !== "undefined" && + typeof user.flags !== "string" + ) + return; - let tag; - let flags; + let tag: Tag | undefined; + let flags: UserFlags | undefined; try { tag = JSON.parse(user.tag); - } catch (err) { } + } catch (err) {} try { - flags = JSON.parse(user.flags); - } catch (err) { } + flags = JSON.parse(user.flags) as UserFlags; + } catch (err) {} for (const p of this.ppl) { if (p._id !== user.id) continue; @@ -315,13 +353,15 @@ export class Channel extends EventEmitter { p.name = user.name; p.color = user.color; p.tag = tag; - p.flags = flags; + if (flags) p.flags = flags; - let found; + let found: + | { x: string | number; y: string | number; id: string } + | undefined; for (const cursor of this.cursorCache) { - if (cursor.id == p.id) { - found = cursor + if (cursor.id === p.id) { + found = cursor; } } @@ -333,20 +373,18 @@ export class Channel extends EventEmitter { y = found.y; } - this.sendArray( - [ - { - m: "p", - _id: p._id, - name: p.name, - color: p.color, - id: p.id, - x: x, - y: y, - tag: usersConfig.enableTags ? p.tag : undefined - } - ] - ); + this.sendArray([ + { + m: "p", + _id: p._id, + name: p.name, + color: p.color, + id: p.id, + x: x, + y: y, + tag: usersConfig.enableTags ? p.tag : undefined + } + ]); } //this.logger.debug("Update from user data update handler"); @@ -357,11 +395,11 @@ export class Channel extends EventEmitter { } }); - this.on("cursor", (pos: { x: string | number; y: string | number; id: string }) => { - let found; + this.on("cursor", (pos: CachedCursor) => { + let found: CachedCursor | undefined; for (const cursor of this.cursorCache) { - if (cursor.id == pos.id) { + if (cursor.id === pos.id) { found = cursor; } } @@ -379,9 +417,8 @@ export class Channel extends EventEmitter { { m: "m", id: pos.id, - // not type safe - x: pos.x as string, - y: pos.y as string + x: pos.x, + y: pos.y } ]); }); @@ -416,7 +453,7 @@ export class Channel extends EventEmitter { */ public isLobby() { for (const reg of config.lobbyRegexes) { - let exp = new RegExp(reg, "g"); + const exp = new RegExp(reg, "g"); if (this.getID().match(exp)) { return true; @@ -430,9 +467,13 @@ export class Channel extends EventEmitter { * 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[0-9]$") && this.getID().match("^lobby$"), "^lobbyNaN$") return true; - - return false; + const _id = this.getID(); + return ( + _id.match("^lobby[0-9][0-9]$") && + _id.match("^lobby[0-9]$") && + _id.match("^lobby$") && + _id.match("^lobbyNaN$") + ); } /** @@ -441,10 +482,7 @@ export class Channel extends EventEmitter { * @param admin Whether a user is changing the settings (set to true to force the changes) * @returns undefined */ - public changeSettings( - set: Partial, - admin: boolean = false - ) { + public changeSettings(set: Partial, admin = false) { if (this.isDestroyed()) return; if (!admin) { if (set.lobby) set.lobby = undefined; @@ -452,8 +490,8 @@ export class Channel extends EventEmitter { } if ( - typeof set.color == "string" && - (typeof set.color2 == "undefined" || + typeof set.color === "string" && + (typeof set.color2 === "undefined" || set.color2 === this.settings.color2) ) { set.color2 = darken(set.color); @@ -467,8 +505,8 @@ export class Channel extends EventEmitter { // Set the verified settings for (const key of Object.keys(validatedSet)) { //this.logger.debug(`${key}: ${(validatedSet as any)[key]}`); - if ((validatedSet as any)[key] === false) continue; - (this.settings as any)[key] = (set as any)[key]; + if (validatedSet[key] === false) continue; + this.settings[key] = set[key]; } /* @@ -524,13 +562,17 @@ export class Channel extends EventEmitter { for (const ch of chs) { const chid = ch.getID(); - if (chid == config.fullChannel) { + if (chid === config.fullChannel) { const banTime = this.getBanTime(socket.getUserID()); //this.logger.debug("Ban time:", banTime); if (banTime) { - const minutes = Math.floor((banTime.endTime - banTime.startTime) / 1000 / 60); + const minutes = Math.floor( + (banTime.endTime - banTime.startTime) / + 1000 / + 60 + ); socket.sendNotification({ class: "short", @@ -540,7 +582,8 @@ export class Channel extends EventEmitter { }); } - return socket.setChannel(chid) + socket.setChannel(chid); + return; } } } @@ -554,7 +597,8 @@ export class Channel extends EventEmitter { const nextID = this.getNextLobbyID(); //this.logger.debug("New ID:", nextID); // Move them to the next lobby - return socket.setChannel(nextID); + socket.setChannel(nextID); + return; } } } @@ -566,7 +610,7 @@ export class Channel extends EventEmitter { for (const p of this.ppl) { if (p.id !== part.id) continue; - p.uuids.push(socket.getUUID()) + p.uuids.push(socket.getUUID()); } //socket.sendChannelUpdate(this.getInfo(), this.getParticipantList()); @@ -578,9 +622,9 @@ export class Channel extends EventEmitter { name: part.name, color: part.color, id: part.id, - tag: part.tag, uuids: [socket.getUUID()], - flags: socket.getUserFlags() || {} + flags: socket.getUserFlags() || {}, + tag: part.tag }); } @@ -590,7 +634,7 @@ export class Channel extends EventEmitter { if (socket.currentChannelID) { // Find the other channel they were in const ch = ChannelList.getList().find( - ch => ch._id == socket.currentChannelID + ch => ch._id === socket.currentChannelID ); // Tell the channel they left @@ -611,7 +655,7 @@ export class Channel extends EventEmitter { if (this.crown && config.chownOnRejoin) { // TODO Should we check participant ID as well? if (typeof this.crown.userId !== "undefined") { - if (socket.getUserID() == this.crown.userId) { + if (socket.getUserID() === this.crown.userId) { // Check if they exist const p = socket.getParticipant(); @@ -653,7 +697,8 @@ export class Channel extends EventEmitter { color: part.color, id: part.id, x: cursorPos.x, - y: cursorPos.y + y: cursorPos.y, + tag: usersConfig.enableTags ? part.tag : undefined } ], part.id @@ -676,9 +721,9 @@ export class Channel extends EventEmitter { const part = socket.getParticipant() as Participant; let dupeCount = 0; - for (const s of socketsBySocketID.values()) { - if (s.getParticipantID() == part.id) { - if (s.currentChannelID == this.getID()) { + for (const s of socketsByUUID.values()) { + if (s.getParticipantID() === part.id) { + if (s.currentChannelID === this.getID()) { dupeCount++; } } @@ -686,14 +731,14 @@ export class Channel extends EventEmitter { // this.logger.debug("Dupes:", dupeCount); - if (dupeCount == 1) { - const p = this.ppl.find(p => p.id == socket.getParticipantID()); + if (dupeCount === 1) { + const p = this.ppl.find(p => p.id === socket.getParticipantID()); if (p) { this.ppl.splice(this.ppl.indexOf(p), 1); if (this.crown) { - if (this.crown.participantId == p.id) { + if (this.crown.participantId === p.id) { // Channel owner left, reset crown timeout this.chown(); } @@ -751,19 +796,22 @@ export class Channel extends EventEmitter { /** * Get the people in this channel + * @param showVanished Whether to include vanished users * @returns List of people */ - public getParticipantList() { + public getParticipantList(showVanished = false) { const ppl = []; for (const p of this.ppl) { - if (p.flags.vanish) continue; + if (p.flags.vanish && !showVanished) continue; + ppl.push({ _id: p._id, name: p.name, color: p.color, id: p.id, - tag: usersConfig.enableTags ? p.tag : undefined + tag: usersConfig.enableTags ? p.tag : undefined, + vanished: p.flags.vanish }); } @@ -780,7 +828,7 @@ export class Channel extends EventEmitter { * @returns Boolean */ public hasUser(_id: string) { - const foundPart = this.ppl.find(p => p._id == _id); + const foundPart = this.ppl.find(p => p._id === _id); return !!foundPart; } @@ -790,7 +838,7 @@ export class Channel extends EventEmitter { * @returns Boolean */ public hasParticipant(id: string) { - const foundPart = this.ppl.find(p => p.id == id); + const foundPart = this.ppl.find(p => p.id === id); return !!foundPart; } @@ -802,19 +850,18 @@ export class Channel extends EventEmitter { arr: ClientEvents[EventID][], blockPartID?: string ) { - let sentSocketIDs = new Array(); + const sentSocketIDs = new Array(); for (const p of this.ppl) { if (blockPartID) { - if (p.id == blockPartID) continue; + if (p.id === blockPartID) continue; } - socketLoop: for (const socket of socketsBySocketID.values()) { - if (socket.isDestroyed()) continue socketLoop; - if (!socket.socketID) continue socketLoop; - if (socket.getParticipantID() != p.id) continue socketLoop; - if (sentSocketIDs.includes(socket.socketID)) - continue socketLoop; + for (const socket of socketsByUUID.values()) { + if (socket.isDestroyed()) continue; + if (!socket.socketID) continue; + if (socket.getParticipantID() !== p.id) continue; + if (sentSocketIDs.includes(socket.socketID)) continue; socket.sendArray(arr); sentSocketIDs.push(socket.socketID); } @@ -837,27 +884,27 @@ export class Channel extends EventEmitter { pianoPartID = part.id; } - let clientMsg: ClientEvents["n"] = { + const clientMsg: ClientEvents["n"] = { m: "n", n: msg.n, t: msg.t, p: pianoPartID }; - let sentSocketIDs = new Array(); + const sentSocketIDs = new Array(); for (const p of this.ppl) { - socketLoop: for (const sock of socketsBySocketID.values()) { - if (sock.isDestroyed()) continue socketLoop; - if (!sock.socketID) continue socketLoop; + for (const sock of socketsByUUID.values()) { + if (sock.isDestroyed()) continue; + if (!sock.socketID) continue; if (socket) { - if (sock.getUUID() == socket.getUUID()) continue socketLoop; + if (sock.getUUID() === socket.getUUID()) continue; } - if (sock.getParticipantID() != p.id) continue socketLoop; + if (sock.getParticipantID() !== p.id) continue; //if (socket.getParticipantID() == part.id) continue socketLoop; - if (sentSocketIDs.includes(sock.socketID)) continue socketLoop; + if (sentSocketIDs.includes(sock.socketID)) continue; sock.sendArray([clientMsg]); sentSocketIDs.push(sock.socketID); @@ -876,7 +923,7 @@ export class Channel extends EventEmitter { this.destroyed = true; if (this.ppl.length > 0) { - for (const socket of socketsBySocketID.values()) { + for (const socket of socketsByUUID.values()) { if (socket.currentChannelID !== this.getID()) continue; socket.setChannel(config.fullChannel); } @@ -939,31 +986,31 @@ export class Channel extends EventEmitter { if (this.crown) { this.crown.time = Date.now(); - let socket; + let socket: Socket | undefined; if (this.crown.participantId) socket = findSocketByPartID(this.crown.participantId); - let x = Math.random() * 100; - let y1 = Math.random() * 100; - let y2 = y1 + Math.random() * (100 - y1); + const x = Math.random() * 100; + const y1 = Math.random() * 100; + const y2 = y1 + Math.random() * (100 - y1); if (socket) { const cursorPos = socket.getCursorPos(); let cursorX = cursorPos.x; - if (typeof cursorPos.x == "string") - cursorX = parseInt(cursorPos.x); + if (typeof cursorPos.x === "string") + cursorX = Number.parseInt(cursorPos.x); let cursorY = cursorPos.y; - if (typeof cursorPos.y == "string") - cursorY = parseInt(cursorPos.y); + if (typeof cursorPos.y === "string") + cursorY = Number.parseInt(cursorPos.y); } // Screen positions this.crown.startPos = { x, y: y1 }; this.crown.endPos = { x, y: y2 }; - delete this.crown.participantId; + this.crown.participantId = undefined; //this.logger.debug("Update from dropCrown"); this.emit("update", this); @@ -975,14 +1022,18 @@ export class Channel extends EventEmitter { * @param _id User ID to ban * @param t Time in millseconds to ban for **/ - public async kickban(_id: string, t: number = 1000 * 60 * 30, banner?: string) { + public async kickban( + _id: string, + t: number = 1000 * 60 * 30, + banner?: string + ) { const now = Date.now(); if (t < 0 || t > 300 * 60 * 1000) return; let shouldUpdate = false; const banChannel = ChannelList.getList().find( - ch => ch.getID() == config.fullChannel + ch => ch.getID() === config.fullChannel ); if (!banChannel) return; @@ -990,8 +1041,8 @@ export class Channel extends EventEmitter { // Check if they are on the server at all let bannedPart: Participant | undefined; const bannedUUIDs: string[] = []; - for (const sock of socketsBySocketID.values()) { - if (sock.getUserID() == _id) { + for (const sock of socketsByUUID.values()) { + if (sock.getUserID() === _id) { bannedUUIDs.push(sock.getUUID()); const part = sock.getParticipant(); @@ -1001,7 +1052,7 @@ export class Channel extends EventEmitter { if (!bannedPart) return; - let isBanned = this.bans.map(b => b.userId).includes(_id); + const isBanned = this.bans.map(b => b.userId).includes(_id); let overwrite = false; if (isBanned) { @@ -1019,24 +1070,24 @@ export class Channel extends EventEmitter { shouldUpdate = true; } else { - for (const ban of this.bans) { if (ban.userId !== _id) continue; ban.startTime = now; ban.endTime = now + t; } - shouldUpdate = true; } uuidsToKick = [...uuidsToKick, ...bannedUUIDs]; - for (const socket of socketsBySocketID.values()) { + for (const socket of socketsByUUID.values()) { if (uuidsToKick.indexOf(socket.getUUID()) !== -1) { socket.sendNotification({ title: "Notice", - text: `Banned from "${this.getID()}" for ${Math.floor(t / 1000 / 60)} minutes.`, + text: `Banned from "${this.getID()}" for ${Math.floor( + t / 1000 / 60 + )} minutes.`, duration: 7000, target: "#room", class: "short" @@ -1045,7 +1096,7 @@ export class Channel extends EventEmitter { // If they are here, move them to the ban channel const ch = socket.getCurrentChannel(); if (ch) { - if (ch.getID() == this.getID()) + if (ch.getID() === this.getID()) socket.setChannel(banChannel.getID()); } } @@ -1056,14 +1107,19 @@ export class Channel extends EventEmitter { this.emit("update", this); if (typeof banner !== "undefined") { - const p = this.getParticipantListUnsanitized().find(p => p._id == banner); + const p = this.getParticipantListUnsanitized().find( + p => p._id === banner + ); const minutes = Math.floor(t / 1000 / 60); if (p && bannedPart) { - await this.sendChat({ - m: "a", - message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.` - }, p); + await this.sendChat( + { + m: "a", + message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.` + }, + p + ); this.sendNotification({ title: "Notice", text: `${p.name} banned ${bannedPart.name} from the channel for ${minutes} minutes.`, @@ -1072,7 +1128,7 @@ export class Channel extends EventEmitter { class: "short" }); - if (banner == _id) { + if (banner === _id) { const certificate = { title: "Certificate of Award", text: `Let it be known that ${p.name} kickbanned him/her self.`, @@ -1082,7 +1138,7 @@ export class Channel extends EventEmitter { this.sendNotification(certificate); - for (const s of socketsBySocketID.values()) { + for (const s of socketsByUUID.values()) { const userID = s.getUserID(); if (!userID) continue; if (userID !== banner) continue; @@ -1110,7 +1166,7 @@ export class Channel extends EventEmitter { } // Check if they are banned - if (ban.userId == _id) { + if (ban.userId === _id) { return true; } } @@ -1128,7 +1184,7 @@ export class Channel extends EventEmitter { if (!isBanned) return; for (const ban of this.bans) { - if (ban.userId == _id) { + if (ban.userId === _id) { this.bans.splice(this.bans.indexOf(ban), 1); } } @@ -1141,34 +1197,38 @@ export class Channel extends EventEmitter { this.chatHistory = []; await saveChatHistory(this.getID(), this.chatHistory); - this.sendArray([{ - m: "c", - c: this.chatHistory - }]); + this.sendArray([ + { + m: "c", + c: this.chatHistory + } + ]); } /** - * Send a notification to this channel - * @param notif Notification to send - **/ + * Send a notification to this channel + * @param notif Notification to send + **/ public sendNotification(notif: Notification) { - this.sendArray([{ - m: "notification", - id: notif.id, - target: notif.target, - duration: notif.duration, - class: notif.class, - title: notif.title, - text: notif.text, - html: notif.html - }]); + this.sendArray([ + { + m: "notification", + id: notif.id, + target: notif.target, + duration: notif.duration, + class: notif.class, + title: notif.title, + text: notif.text, + html: notif.html + } + ]); } /** - * Send a message in chat - * @param msg Chat message event to send - * @param p Participant who is "sending the message" - **/ + * 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) { if (!msg.message) return; @@ -1180,7 +1240,7 @@ export class Channel extends EventEmitter { .replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1") .trim(); - let outgoing: ClientEvents["a"] = { + const outgoing: ClientEvents["a"] = { m: "a", a: msg.message, t: Date.now(), @@ -1203,10 +1263,13 @@ export class Channel extends EventEmitter { * @param message Message to send in chat **/ public async sendChatAdmin(message: string) { - this.sendChat({ - m: "a", - message - }, usersConfig.adminParticipant); + this.sendChat( + { + m: "a", + message + }, + usersConfig.adminParticipant + ); } /** @@ -1214,7 +1277,10 @@ export class Channel extends EventEmitter { * @param key Flag ID * @param val Value of which the flag will be set to **/ - public setFlag(key: string, val: any) { + public setFlag( + key: K, + val: TChannelFlags[K] + ) { this.flags[key] = val; } @@ -1223,7 +1289,7 @@ export class Channel extends EventEmitter { * @param key Flag ID * @returns Value of flag **/ - public getFlag(key: string) { + public getFlag(key: K) { return this.flags[key]; } @@ -1231,7 +1297,7 @@ export class Channel extends EventEmitter { * Set the flags on this channel * @param flags Flags to set **/ - public setFlags(flags: Record) { + public setFlags(flags: TChannelFlags) { this.flags = flags; this.save(); this.emit("update", this); @@ -1244,8 +1310,8 @@ export class Channel extends EventEmitter { public getNextLobbyID() { try { const id = this.getID(); - if (id == "lobby") return "lobby2"; - const num = parseInt(id.substring(5)); + if (id === "lobby") return "lobby2"; + const num = Number.parseInt(id.substring(5)); return `lobby${num + 1}`; } catch (err) { return config.fullChannel; @@ -1259,7 +1325,7 @@ export class Channel extends EventEmitter { **/ public getBanTime(userId: string) { for (const ban of this.bans) { - if (userId == ban.userId) { + if (userId === ban.userId) { return { endTime: ban.endTime, startTime: ban.startTime }; } } @@ -1270,7 +1336,13 @@ export class Channel extends EventEmitter { **/ public printMemoryInChat() { const mem = heapStats(); - this.sendChatAdmin(`Used: ${(mem.heapSize / 1000 / 1000).toFixed(2)}M / Allocated: ${(mem.heapCapacity / 1000 / 1000).toFixed(2)}M`); + this.sendChatAdmin( + `Used: ${(mem.heapSize / 1000 / 1000).toFixed(2)}M / Allocated: ${( + mem.heapCapacity / + 1000 / + 1000 + ).toFixed(2)}M` + ); } } diff --git a/src/channel/config.ts b/src/channel/config.ts index 63563bb..5a089b2 100644 --- a/src/channel/config.ts +++ b/src/channel/config.ts @@ -1,4 +1,4 @@ -import { loadConfig } from "../util/config"; +import { ConfigManager } from "../util/config"; import { IChannelSettings } from "../util/types"; interface ChannelConfig { @@ -13,28 +13,37 @@ interface ChannelConfig { channelDestroyTimeout: number; } -export const config = loadConfig("config/channels.yml", { - forceLoad: ["lobby", "test/awkward"], - lobbySettings: { - lobby: true, - chat: true, - crownsolo: false, - visible: true, - color: "#73b3cc", - color2: "#273546" - }, - defaultSettings: { - chat: true, - crownsolo: false, - color: "#3b5054", - color2: "#001014", - visible: true - }, - // Here's a terrifying fact: Brandon used parseInt to check lobby names - lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[0-9]$", "^lobby$", "^lobbyNaN$", "^test/.+$"], - lobbyBackdoor: "lolwutsecretlobbybackdoor", - fullChannel: "test/awkward", - sendLimit: false, - chownOnRejoin: true, - channelDestroyTimeout: 1000 -}); +export const config = ConfigManager.loadConfig( + "config/channels.yml", + { + forceLoad: ["lobby", "test/awkward"], + lobbySettings: { + lobby: true, + chat: true, + crownsolo: false, + visible: true, + color: "#73b3cc", + color2: "#273546" + }, + defaultSettings: { + chat: true, + crownsolo: false, + color: "#3b5054", + color2: "#001014", + visible: true + }, + // Here's a terrifying fact: Brandon used parseInt to check lobby names + lobbyRegexes: [ + "^lobby[0-9][0-9]$", + "^lobby[0-9]$", + "^lobby$", + "^lobbyNaN$", + "^test/.+$" + ], + lobbyBackdoor: "lolwutsecretlobbybackdoor", + fullChannel: "test/awkward", + sendLimit: false, + chownOnRejoin: true, + channelDestroyTimeout: 1000 + } +); diff --git a/src/channel/settings.ts b/src/channel/settings.ts index 58b2ab8..e9b29a9 100644 --- a/src/channel/settings.ts +++ b/src/channel/settings.ts @@ -1,4 +1,4 @@ -import { IChannelSettings } from "../util/types"; +import type { IChannelSettings } from "~/util/types"; type Validator = "boolean" | "string" | "number" | ((val: unknown) => boolean); @@ -40,11 +40,11 @@ const adminOnlyKeys = [ */ export function validateChannelSettings(set: Partial, admin = false) { // Create record - let record: Partial> = {}; + const record: Partial> = {}; for (const key of Object.keys(set)) { - let val = (set as Record)[key]; - let validator = ( + const val = (set as Record)[key]; + const validator = ( validationRecord as Record )[key]; @@ -66,12 +66,15 @@ export function validateChannelSettings(set: Partial, admin = export default validateChannelSettings; -export function validate(value: any, validator: Validator) { +export function validate(value: unknown, validator: Validator) { // What type of validator? if (typeof validator === "function") { // Run the function return validator(value) === true; - } else if (typeof value === validator) { + } + + // biome-ignore lint/suspicious/useValidTypeof: biome is dumb + if (typeof value === validator) { return true; } diff --git a/src/data/permission.ts b/src/data/permission.ts new file mode 100644 index 0000000..4420381 --- /dev/null +++ b/src/data/permission.ts @@ -0,0 +1,119 @@ +import { ConfigManager } from "~/util/config"; +import { prisma } from "./prisma"; +import { getRoles } from "./role"; +import { permission } from "process"; +import { Logger } from "~/util/Logger"; + +export const config = ConfigManager.loadConfig>( + "config/permissions.yml", + { + admin: [ + "clearChat", + "vanish", + "chsetAnywhere", + "chownAnywhere", + "usersetOthers", + "siteBan", + "siteBanAnyReason", + "siteBanAnyDuration" + ] + } +); + +const logger = new Logger("Permission Handler"); + +export async function getRolePermissions(roleId: string) { + const permissions = await prisma.rolePermission.findMany({ + where: { roleId } + }); + + return permissions; +} + +export async function hasPermission(roleId: string, permission: string) { + const permissions = await getRolePermissions(roleId); + + if (permissions.find(p => p.permission === permission)) return true; + return false; +} + +export async function addRolePermission(roleId: string, permission: string) { + return await prisma.rolePermission.create({ + data: { + roleId, + permission + } + }); +} + +export async function removeRolePermission(roleId: string, permission: string) { + return await prisma.rolePermission.deleteMany({ + where: { + roleId, + permission + } + }); +} + +export async function removeAllRolePermissions(roleId?: string) { + return await prisma.rolePermission.deleteMany({ + where: { + roleId + } + }); +} + +export async function getUserPermissions(userId: string) { + const roles = await getRoles(userId); + let collectivePerms: string[] = []; + + for (const role of roles) { + const perms = await getRolePermissions(role.roleId); + collectivePerms.push(...perms.map(p => p.permission)); + } + + return collectivePerms; +} + +export function validatePermission(permission1: string, permission2: string) { + let perm1 = permission1.split("."); + let perm2 = permission2.split("."); + + let length = Math.max(perm1.length, perm2.length); + + for (let i = 0; i < length; i++) { + let p1 = perm1[i]; + let p2 = perm2[i]; + + if (p1 === "*" || p2 === "*") break; + if (p1 !== p2) return false; + + if (i === length - 1) { + return true; + } else if (p1 === p2) { + continue; + } + } + + return true; +} + +export async function loadDefaultPermissions() { + logger.info("Loading default permissions..."); + + for (const roleId of Object.keys(config)) { + // logger.debug("Adding roles for", roleId); + const permissions = config[roleId]; + + for (const permission of permissions) { + if (await hasPermission(roleId, permission)) { + // logger.debug("Permission already exists:", roleId, permission); + continue; + } + // logger.debug("Adding permission:", roleId, permission); + await addRolePermission(roleId, permission); + } + } + + logger.info("Loaded default permissions"); +} diff --git a/src/data/role.ts b/src/data/role.ts new file mode 100644 index 0000000..440d2cc --- /dev/null +++ b/src/data/role.ts @@ -0,0 +1,38 @@ +import { IRole } from "~/util/types"; +import { prisma } from "./prisma"; + +export async function getRoles(userId: string) { + const roles = await prisma.role.findMany({ + where: { userId } + }); + + return roles as IRole[]; +} + +export async function hasRole(userId: string, roleId: string) { + const roles = await getRoles(userId); + + for (const role of roles) { + if (role.roleId === roleId) return true; + } + + return false; +} + +export async function giveRole(userId: string, roleId: string) { + return (await prisma.role.create({ + data: { + userId, + roleId + } + })) as IRole; +} + +export async function removeRole(userId: string, roleId: string) { + return await prisma.role.delete({ + where: { + userId, + roleId + } + }); +} diff --git a/src/index.ts b/src/index.ts index a0a7eea..4060508 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { loadForcedStartupChannels } from "./channel/forceLoad"; import { Logger } from "./util/Logger"; // docker hates this next one import { startReadline } from "./util/readline"; +import { loadDefaultPermissions } from "./data/permission"; // wrapper for some reason export function startServer() { @@ -28,6 +29,8 @@ export function startServer() { logger.info("Forceloading startup channels..."); loadForcedStartupChannels(); + loadDefaultPermissions(); + // Break the console startReadline(); diff --git a/src/util/config.ts b/src/util/config.ts index 8eb8fb5..25098e9 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -1,5 +1,6 @@ -import { existsSync, readFileSync, writeFileSync } from "fs"; +import { existsSync, readFileSync, writeFileSync, watchFile } from "fs"; import { parse, stringify } from "yaml"; +import { Logger } from "./Logger"; /** * This file uses the synchronous functions from the fs @@ -11,74 +12,140 @@ import { parse, stringify } from "yaml"; * program. */ -/** - * Load a YAML config file and set default values if config path is nonexistent - * - * Usage: - * ```ts - * const config = loadConfig("config/services.yml", { - * enableMPP: false - * }); - * ``` - * @param configPath Path to load config from - * @param defaultConfig Config to use if none is present (will save to path if used) - * @returns Parsed YAML config - */ -export function loadConfig(configPath: string, defaultConfig: T): T { - // Config exists? - if (existsSync(configPath)) { - // Load config - const data = readFileSync(configPath); - const config = parse(data.toString()); +export class ConfigManager { + // public static configCache = new Map(); + public static logger: Logger; - const defRecord = defaultConfig as Record; - let changed = false; + static { + setTimeout(() => { + this.logger = new Logger("Config Loader"); + }); + } - function mix( - obj: Record, - obj2: Record - ) { - for (const key of Object.keys(obj2)) { - if (typeof obj[key] == "undefined") { - obj[key] = obj2[key]; - changed = true; - } + /** + * Load a YAML config file and set default values if config path is nonexistent + * + * Usage: + * ```ts + * const config = ConfigManager.loadConfig("config/services.yml", { + * enableMPP: false + * }); + * ``` + * @param configPath Path to load config from + * @param defaultConfig Config to use if none is present (will save to path if used) + * @returns Parsed YAML config + */ + public static loadConfig(configPath: string, defaultConfig: T): T { + const self = this; - if (typeof obj[key] == "object" && !Array.isArray(obj[key])) { - mix( - obj[key] as Record, - obj2[key] as Record - ); + // Config exists? + if (existsSync(configPath)) { + // Load config + const data = readFileSync(configPath); + const config = parse(data.toString()); + + const defRecord = defaultConfig as Record; + let changed = false; + + function mix( + obj: Record, + obj2: Record + ) { + for (const key of Object.keys(obj2)) { + if (typeof obj[key] == "undefined") { + obj[key] = obj2[key]; + changed = true; + } + + if ( + typeof obj[key] == "object" && + !Array.isArray(obj[key]) + ) { + mix( + obj[key] as Record, + obj2[key] as Record + ); + } } } + + // Apply any missing default values + mix(config, defRecord); + + // Save config if modified + if (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); + // }); + + // 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); + + // File contents changed callback + // const watcher = watchFile(configPath, () => { + // this.logger.info( + // "Reloading config due to changes:", + // configPath + // ); + // this.loadConfig(configPath, defaultConfig); + // }); + + // this.configCache.set(configPath, defaultConfig); + // return this.getConfigProxy(configPath); + return defaultConfig; } - - // Apply any missing default values - mix(config, defRecord); - - // Save config if modified - if (changed) writeConfig(configPath, config); - - return config as T; - } else { - // Write default config to disk and use that - //logger.warn(`Config file "${configPath}" not found, writing default config to disk`); - writeConfig(configPath, defaultConfig); - return defaultConfig as T; } -} -/** - * Write a YAML config to disk - * @param configPath - * @param config - */ -export function writeConfig(configPath: string, config: T) { - // Write config to disk unconditionally - writeFileSync( - configPath, - stringify(config, { - indent: 4 - }) - ); + /** + * Write a YAML config to disk + * @param configPath + * @param config + */ + public static writeConfig(configPath: string, config: T) { + // Write config to disk unconditionally + writeFileSync( + configPath, + stringify(config, { + indent: 4 + }) + ); + } + + /** + * Get a proxy to a config (for updating config objects regardless of scope) + * @param configPath Path to config file + * @returns Config proxy object + */ + // 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; + + // if (config) { + // if (config.hasOwnProperty(name)) + // return (config as Record)[ + // name + // ] as T[keyof T]; + // } + // } + // } + // ) as T; + // } } diff --git a/src/util/readline/commands.ts b/src/util/readline/commands.ts index 05e5ed8..0ede9db 100644 --- a/src/util/readline/commands.ts +++ b/src/util/readline/commands.ts @@ -1,6 +1,14 @@ +import { getRoles, giveRole, removeRole } from "~/data/role"; import { ChannelList } from "../../channel/ChannelList"; import { deleteUser, getUsers } from "../../data/user"; import Command from "./Command"; +import { + addRolePermission, + getRolePermissions, + loadDefaultPermissions, + removeAllRolePermissions, + removeRolePermission +} from "~/data/permission"; Command.addCommand( new Command(["help", "h", "commands", "cmds"], "help", msg => { @@ -76,6 +84,63 @@ Command.addCommand( }) ); +Command.addCommand( + new Command( + ["role"], + "role [role id]", + async msg => { + if (!msg.args[2]) + return "role [role id]"; + + if (msg.args[1] === "add") { + if (!msg.args[3]) return "No role id provided"; + await giveRole(msg.args[2], msg.args[3]); + return `Gave user ${msg.args[2]} role ${msg.args[3]}`; + } else if (msg.args[1] === "remove") { + if (!msg.args[3]) return "No role id provided"; + await removeRole(msg.args[2], msg.args[3]); + return `Removed role ${msg.args[3]} from ${msg.args[2]}`; + } else if (msg.args[1] === "list") { + const roles = await getRoles(msg.args[2]); + return `Roles of ${msg.args[2]}: ${roles + .map(r => r.roleId) + .join(", ")}`; + } + } + ) +); + +Command.addCommand( + new Command( + ["perms"], + "perms [role id] [permission]", + async msg => { + if (msg.args[1] === "add") { + if (!msg.args[3]) return "No permission provided"; + await addRolePermission(msg.args[2], msg.args[3]); + return `Added permission ${msg.args[3]} to role ${msg.args[2]}`; + } else if (msg.args[1] === "remove") { + if (!msg.args[3]) return "No role id provided"; + await removeRolePermission(msg.args[2], msg.args[3]); + return `Remove permission ${msg.args[3]} from role ${msg.args[2]}`; + } else if (msg.args[1] === "list") { + const perms = await getRolePermissions(msg.args[2]); + return `Permissions of ${msg.args[1]}: ${perms + .map(p => p.permission) + .join(", ")}`; + } else if (msg.args[1] === "clear") { + await removeAllRolePermissions(msg.args[2]); + if (msg.args[2]) { + return `Permissions of ${msg.args[2]} cleared`; + } else { + await loadDefaultPermissions(); + return `All permissions reset`; + } + } + } + ) +); + Command.addCommand( new Command(["js", "eval"], "js ", async msg => { function roughSizeOfObject(object: any) { @@ -87,16 +152,16 @@ Command.addCommand( const value = stack.pop(); switch (typeof value) { - case 'boolean': + case "boolean": bytes += 4; break; - case 'string': + case "string": bytes += value.length * 2; break; - case 'number': + case "number": bytes += 8; break; - case 'object': + case "object": if (!objectList.includes(value)) { objectList.push(value); for (const prop in value) { diff --git a/src/util/types.d.ts b/src/util/types.d.ts index 1115b55..d2140a6 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -1,4 +1,4 @@ -import { Socket } from "../ws/Socket"; +import type { Socket } from "../ws/Socket"; declare type Omit = Pick>; @@ -25,6 +25,7 @@ declare type UserFlags = Partial<{ type ChannelFlags = Partial<{ limit: number; + owner_id: string; }>; declare interface Tag { @@ -57,7 +58,8 @@ declare type IChannelSettings = { limit: number; noindex: boolean; -}>; +}> & + Record; declare type ChannelSettingValue = Partial; @@ -84,18 +86,18 @@ declare type Notification = Partial<{ declare type CustomTarget = { global?: boolean; } & ( - | { - mode: "subscribed"; - } - | { - mode: "ids"; - ids: string[]; - } - | { - mode: "id"; - id: string; - } - ); + | { + mode: "subscribed"; + } + | { + mode: "ids"; + ids: string[]; + } + | { + mode: "id"; + id: string; + } +); declare interface Crown { userId: string; @@ -106,6 +108,13 @@ declare interface Crown { } declare interface ServerEvents { + hi: { + m: "hi"; + token?: string; + login?: { type: string; code: string }; + code?: string; + }; + a: { m: "a"; message: string; @@ -133,13 +142,13 @@ declare interface ServerEvents { custom: { m: "custom"; - data: any; + data: unknown; target: CustomTarget; }; devices: { m: "devices"; - list: any[]; + list: unknown[]; }; dm: { @@ -148,13 +157,6 @@ declare interface ServerEvents { _id: string; }; - hi: { - m: "hi"; - token?: string; - login?: { type: string; code: string }; - code?: string; - }; - kickban: { m: "kickban"; _id: string; @@ -207,7 +209,7 @@ declare interface ServerEvents { "admin message": { m: "admin message"; password: string; - msg: ServerEvents; + msg: ServerEvents[keyof ServerEvents]; }; b: { @@ -244,7 +246,7 @@ declare interface ServerEvents { text: string; color: string; }; - } + }; clear_chat: { m: "clear_chat"; @@ -269,7 +271,7 @@ declare interface ServerEvents { m: "ch_flag"; _id?: string; key: string; - value: any; + value: unknown; }; move: { @@ -277,23 +279,23 @@ declare interface ServerEvents { ch: string; _id?: string; set?: Partial; - } + }; rename_channel: { m: "rename_channel"; _id: string; - } + }; admin_chat: { m: "admin_chat"; _id?: string; message: string; - } + }; eval: { m: "eval"; str: string; - } + }; } declare interface ClientEvents { @@ -323,7 +325,7 @@ declare interface ClientEvents { custom: { m: "custom"; - data: any; + data: unknown; p: string; }; @@ -331,9 +333,9 @@ declare interface ClientEvents { m: "hi"; t: number; u: User; - permissions: any; - token?: any; - accountInfo: any; + permissions: unknown; + token?: string; + accountInfo: unknown; motd?: string; }; @@ -345,8 +347,8 @@ declare interface ClientEvents { m: { m: "m"; - x: string; - y: string; + x: string | number; + y: string | number; id: string; }; @@ -393,11 +395,11 @@ declare interface ClientEvents { }; } -declare type ServerEventCallback = (msg: ServerEvents[EventID], socket: Socket) => Promise; +type EventID = ServerEvents[keyof ServerEvents]["m"]; -declare type ServerEventListener = { - id: EventID; - callback: ServerEventCallback; +declare type ServerEventListener = { + id: E; + callback: (msg: ServerEvents[E], socket: Socket) => Promise; }; declare type Vector2 = { @@ -426,3 +428,8 @@ declare interface IChannelInfo { settings: Partial; crown?: ICrown; } + +declare interface IRole { + userId: string; + roleId: string; +} diff --git a/src/util/utilConfig.ts b/src/util/utilConfig.ts index 85b8ec1..4cd8e08 100644 --- a/src/util/utilConfig.ts +++ b/src/util/utilConfig.ts @@ -1,5 +1,5 @@ -import { loadConfig } from "./config"; +import { ConfigManager } from "./config"; -export const config = loadConfig("config/util.yml", { +export const config = ConfigManager.loadConfig("config/util.yml", { enableLogFiles: true }); diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index 96ae5f9..17f7baa 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -6,7 +6,7 @@ import { createColor, createID, createUserID } from "../util/id"; import EventEmitter from "events"; -import { +import type { IChannelInfo, IChannelSettings, ClientEvents, @@ -23,15 +23,26 @@ import { eventGroups } from "./events"; import { Gateway } from "./Gateway"; import { Channel } from "../channel/Channel"; import { ChannelList } from "../channel/ChannelList"; -import { ServerWebSocket } from "bun"; +import type { ServerWebSocket } from "bun"; import { Logger } from "../util/Logger"; -import { RateLimitConstructorList, RateLimitList } from "./ratelimit/config"; +import type { + RateLimitConstructorList, + RateLimitList +} from "./ratelimit/config"; import { adminLimits } from "./ratelimit/limits/admin"; import { userLimits } from "./ratelimit/limits/user"; import { NoteQuota } from "./ratelimit/NoteQuota"; import { config } from "./usersConfig"; import { config as channelConfig } from "../channel/config"; import { crownLimits } from "./ratelimit/limits/crown"; +import type { RateLimit } from "./ratelimit/RateLimit"; +import type { RateLimitChain } from "./ratelimit/RateLimitChain"; +import { + getUserPermissions, + hasPermission, + validatePermission +} from "~/data/permission"; +import { getRoles } from "~/data/role"; const logger = new Logger("Sockets"); @@ -58,9 +69,9 @@ export class Socket extends EventEmitter { _id: string | undefined; set: Partial | undefined; } = { - _id: undefined, - set: {} - }; + _id: undefined, + set: {} + }; public currentChannelID: string | undefined; private cursorPos: Vector2 = { x: 200, y: 100 }; @@ -77,7 +88,9 @@ export class Socket extends EventEmitter { this.ip = ws.data.ip; } else { // Fake user - this.ip = `::ffff:${Math.random() * 255}.${Math.random() * 255}.${Math.random() * 255}.${Math.random() * 255}`; + this.ip = `::ffff:${Math.random() * 255}.${Math.random() * 255}.${ + Math.random() * 255 + }.${Math.random() * 255}`; } // User ID @@ -86,13 +99,13 @@ export class Socket extends EventEmitter { // Check if we're already connected // We need to skip ourselves, so we loop here instead of using a helper - let foundSocket; + let foundSocket: Socket | undefined; let count = 0; // big boi loop - for (const socket of socketsBySocketID.values()) { + for (const socket of socketsByUUID.values()) { // Skip us - if (socket.socketID == this.socketID) continue; + if (socket.socketID === this.socketID) continue; // Are they real? if (socket.ws) { @@ -101,7 +114,7 @@ export class Socket extends EventEmitter { } // Same user ID? - if (socket.getUserID() == this.getUserID()) { + if (socket.getUserID() === this.getUserID()) { foundSocket = socket; count++; } @@ -142,13 +155,15 @@ export class Socket extends EventEmitter { this.bindEventListeners(); // Send a challenge to the browser for MPP.net frontends - if (config.browserChallenge == "basic") { + if (config.browserChallenge === "basic") { // Basic function - this.sendArray([{ - m: "b", - code: `~return btoa(JSON.stringify([true, navigator.userAgent]));` - }]); - } else if (config.browserChallenge == "obf") { + this.sendArray([ + { + m: "b", + code: "~return btoa(JSON.stringify([true, navigator.userAgent]));" + } + ]); + } else if (config.browserChallenge === "obf") { // Obfuscated challenge building // TODO } @@ -186,15 +201,18 @@ export class Socket extends EventEmitter { * Move this socket to a channel * @param _id Target channel ID * @param set Channel settings, if the channel is instantiated - * @param force Whether to make this socket join regardless of channel properties + * @param f Whether to make this socket join regardless of channel properties **/ - public setChannel(_id: string, set?: Partial, force = false) { + public setChannel(_id: string, set?: Partial, f = false) { + let desiredChannelID = _id; + let force = f; + // Do we exist? if (this.isDestroyed()) return; // Are we trying to join the same channel like an idiot? - if (this.currentChannelID === _id) return; + if (this.currentChannelID === desiredChannelID) return; - this.desiredChannel._id = _id; + this.desiredChannel._id = desiredChannelID; this.desiredChannel.set = set; let channel: Channel | undefined; @@ -203,20 +221,20 @@ export class Socket extends EventEmitter { //logger.debug("Desired:", this.desiredChannel._id, "| Matching:", channelConfig.lobbyBackdoor, ",", this.desiredChannel._id == channelConfig.lobbyBackdoor); // Are we joining the lobby backdoor? - if (this.desiredChannel._id == channelConfig.lobbyBackdoor) { + if (this.desiredChannel._id === channelConfig.lobbyBackdoor) { // This is very likely not the original way the backdoor worked, // but considering the backdoor was changed sometime this decade // and the person who owns the original server is literally a // Chinese scammer, we don't really have much choice but to guess // at this point, unless a screenshot descends from the heavens above // and magically gives us all the info we need and we can fix it here. - _id = "lobby"; + desiredChannelID = "lobby"; force = true; } // Find the first channel that matches the desired ID for (const ch of ChannelList.getList()) { - if (ch.getID() == _id) { + if (ch.getID() === desiredChannelID) { channel = ch; } } @@ -259,7 +277,7 @@ export class Socket extends EventEmitter { **/ private bindEventListeners() { for (const group of eventGroups) { - if (group.id == "admin") { + if (group.id === "admin") { for (const event of group.eventList) { this.admin.on(event.id, event.callback); } @@ -373,7 +391,7 @@ export class Socket extends EventEmitter { try { tag = JSON.parse(this.user.tag) as Tag; - } catch (err) { } + } catch (err) {} return { _id: facadeID, @@ -382,9 +400,9 @@ export class Socket extends EventEmitter { id: this.getParticipantID(), tag: config.enableTags ? tag : undefined }; - } else { - return null; } + + return null; } private destroyed = false; @@ -421,7 +439,7 @@ export class Socket extends EventEmitter { * @returns Whether this socket is destroyed **/ public isDestroyed() { - return this.destroyed == true; + return this.destroyed === true; } /** @@ -442,12 +460,15 @@ export class Socket extends EventEmitter { * @param x X coordinate * @param y Y coordinate **/ - public setCursorPos(x: CursorValue, y: CursorValue) { - if (typeof x == "number") { + public setCursorPos(xpos: CursorValue, ypos: CursorValue) { + let x = xpos; + let y = ypos; + + if (typeof x === "number") { x = x.toFixed(2); } - if (typeof y == "number") { + if (typeof y === "number") { y = y.toFixed(2); } @@ -462,7 +483,7 @@ export class Socket extends EventEmitter { const part = this.getParticipant(); if (!part) return; - let pos = { + const pos = { x: this.cursorPos.x, y: this.cursorPos.y, id: this.getParticipantID() @@ -476,7 +497,7 @@ export class Socket extends EventEmitter { **/ public getCurrentChannel() { return ChannelList.getList().find( - ch => ch.getID() == this.currentChannelID + ch => ch.getID() === this.currentChannelID ); } @@ -500,11 +521,7 @@ export class Socket extends EventEmitter { * @param color Desired color * @param admin Whether to force this change **/ - public async userset( - name?: string, - color?: string, - admin: boolean = false - ) { + public async userset(name?: string, color?: string, admin = false) { let isColor = false; // Color changing @@ -517,7 +534,7 @@ export class Socket extends EventEmitter { if (name.length > 40) return; await updateUser(this._id, { - name: typeof name == "string" ? name : undefined, + name: typeof name === "string" ? name : undefined, color: color && isColor ? color : undefined }); @@ -526,8 +543,8 @@ export class Socket extends EventEmitter { const ch = this.getCurrentChannel(); if (ch) { - let part = this.getParticipant() as Participant; - let cursorPos = this.getCursorPos(); + const part = this.getParticipant() as Participant; + const cursorPos = this.getCursorPos(); ch.sendArray([ { @@ -555,11 +572,15 @@ export class Socket extends EventEmitter { } as RateLimitList; for (const key of Object.keys(list.normal)) { - (this.rateLimits.normal as any)[key] = (list.normal as any)[key](); + (this.rateLimits.normal as Record)[key] = ( + list.normal as Record RateLimit> + )[key](); } for (const key of Object.keys(list.chains)) { - (this.rateLimits.chains as any)[key] = (list.chains as any)[key](); + (this.rateLimits.chains as Record)[key] = ( + list.chains as Record RateLimitChain> + )[key](); } } @@ -569,7 +590,7 @@ export class Socket extends EventEmitter { public resetRateLimits() { // TODO Permissions let isAdmin = false; - let ch = this.getCurrentChannel(); + const ch = this.getCurrentChannel(); let hasNoteRateLimitBypass = false; try { @@ -581,7 +602,9 @@ export class Socket extends EventEmitter { } } } catch (err) { - logger.warn("Unable to get user flags while processing rate limits"); + logger.warn( + "Unable to get user flags while processing rate limits" + ); } if (isAdmin) { @@ -590,8 +613,8 @@ export class Socket extends EventEmitter { } else if (this.isOwner()) { this.setRateLimits(crownLimits); this.setNoteQuota(NoteQuota.PARAMS_RIDICULOUS); - } else if (ch && ch.isLobby()) { - this.setRateLimits(userLimits) + } else if (ch?.isLobby()) { + this.setRateLimits(userLimits); this.setNoteQuota(NoteQuota.PARAMS_LOBBY); } else { this.setRateLimits(userLimits); @@ -604,7 +627,7 @@ export class Socket extends EventEmitter { * @param params Note quota params object **/ public setNoteQuota(params = NoteQuota.PARAMS_NORMAL) { - this.noteQuota.setParams(params as any); // TODO why any + this.noteQuota.setParams(params); // TODO why any // Send note quota to client this.sendArray([ @@ -748,16 +771,18 @@ export class Socket extends EventEmitter { * ``` **/ public sendNotification(notif: Notification) { - this.sendArray([{ - m: "notification", - id: notif.id, - target: notif.target, - duration: notif.duration, - class: notif.class, - title: notif.title, - text: notif.text, - html: notif.html - }]); + this.sendArray([ + { + m: "notification", + id: notif.id, + target: notif.target, + duration: notif.duration, + class: notif.class, + title: notif.title, + text: notif.text, + html: notif.html + } + ]); } /** @@ -779,6 +804,7 @@ export class Socket extends EventEmitter { **/ public eval(str: string) { try { + // biome-ignore lint/security/noGlobalEval: configured const output = eval(str); logger.info(output); } catch (err) { @@ -801,16 +827,29 @@ export class Socket extends EventEmitter { this.sendNotification({ title: "Notice", - text: `You have been banned from the server for ${Math.floor(duration / 1000 / 60)} minutes. Reason: ${reason}`, + text: `You have been banned from the server for ${Math.floor( + duration / 1000 / 60 + )} minutes. Reason: ${reason}`, duration: 20000, target: "#room", class: "classic" }); } + + public async hasPermission(perm: string) { + if (!this.user) return false; + + const permissions = await getUserPermissions(this.user.id); + + for (const permission of permissions) { + if (validatePermission(perm, permission)) return true; + } + } } -export const socketsBySocketID = new Map(); -(globalThis as any).socketsBySocketID = socketsBySocketID; +export const socketsByUUID = new Map(); +// biome-ignore lint/suspicious/noExplicitAny: global access for console +(globalThis as any).socketsByUUID = socketsByUUID; /** * Find a socket by their participant ID @@ -819,8 +858,8 @@ export const socketsBySocketID = new Map(); * @returns Socket object **/ export function findSocketByPartID(id: string) { - for (const socket of socketsBySocketID.values()) { - if (socket.getParticipantID() == id) return socket; + for (const socket of socketsByUUID.values()) { + if (socket.getParticipantID() === id) return socket; } } @@ -833,9 +872,9 @@ export function findSocketByPartID(id: string) { export function findSocketsByUserID(_id: string) { const sockets = []; - for (const socket of socketsBySocketID.values()) { + for (const socket of socketsByUUID.values()) { // logger.debug("User ID:", socket.getUserID()); - if (socket.getUserID() == _id) sockets.push(socket); + if (socket.getUserID() === _id) sockets.push(socket); } return sockets; @@ -848,8 +887,8 @@ export function findSocketsByUserID(_id: string) { * @returns Socket object **/ export function findSocketByIP(ip: string) { - for (const socket of socketsBySocketID.values()) { - if (socket.getIP() == ip) { + for (const socket of socketsByUUID.values()) { + if (socket.getIP() === ip) { return socket; } } diff --git a/src/ws/events.ts b/src/ws/events.ts index 8e4c6b6..9b6de1d 100644 --- a/src/ws/events.ts +++ b/src/ws/events.ts @@ -1,22 +1,22 @@ -import { ServerEventListener, ServerEvents } from "../util/types"; +import type { ServerEventListener, ServerEvents } from "../util/types"; export class EventGroup { - public eventList = new Array>(); + public eventList = new Array>(); constructor(public id: string) {} - public add(listener: ServerEventListener) { + public add(listener: ServerEventListener) { this.eventList.push(listener); } - public addMany(...listeners: ServerEventListener[]) { - listeners.forEach(l => this.add(l)); + public addMany(...listeners: ServerEventListener[]) { + for (const l of listeners) this.add(l); } - public remove(listener: ServerEventListener) { + public remove(listener: ServerEventListener) { this.eventList.splice(this.eventList.indexOf(listener), 1); } } export const eventGroups = new Array(); -import "./events.inc"; +require("./events.inc"); diff --git a/src/ws/events/admin/handlers/move.ts b/src/ws/events/admin/handlers/move.ts index cb46be7..acfb385 100644 --- a/src/ws/events/admin/handlers/move.ts +++ b/src/ws/events/admin/handlers/move.ts @@ -1,5 +1,5 @@ import { ServerEventListener } from "../../../../util/types"; -import { socketsBySocketID } from "../../../Socket"; +import { socketsByUUID } from "../../../Socket"; export const move: ServerEventListener<"move"> = { id: "move", @@ -15,7 +15,7 @@ export const move: ServerEventListener<"move"> = { if (typeof set !== "object" && typeof set !== "undefined") return; // Loop through every socket - for (const sock of socketsBySocketID.values()) { + for (const sock of socketsByUUID.values()) { // Check their user ID if (sock.getUserID() == id) { // Forcefully move to channel diff --git a/src/ws/events/admin/handlers/notification.ts b/src/ws/events/admin/handlers/notification.ts index 4abe9bd..0b5c2ca 100644 --- a/src/ws/events/admin/handlers/notification.ts +++ b/src/ws/events/admin/handlers/notification.ts @@ -1,9 +1,9 @@ import { ChannelList } from "../../../../channel/ChannelList"; -import { loadConfig } from "../../../../util/config"; +import { ConfigManager } from "../../../../util/config"; import { ServerEventListener } from "../../../../util/types"; -import { socketsBySocketID } from "../../../Socket"; +import { socketsByUUID } from "../../../Socket"; -const config = loadConfig<{ +const config = ConfigManager.loadConfig<{ allowXSS: boolean; maxDuration: number; defaultDuration: number; @@ -19,24 +19,32 @@ export const notification: ServerEventListener<"notification"> = { id: "notification", callback: async (msg, socket) => { // Send notification to user/channel - if (typeof msg.targetChannel == "undefined" && typeof msg.targetUser == "undefined") return; + if ( + typeof msg.targetChannel == "undefined" && + typeof msg.targetUser == "undefined" + ) + return; if (msg.duration) { - if (msg.duration > config.maxDuration) msg.duration = config.maxDuration; + if (msg.duration > config.maxDuration) + msg.duration = config.maxDuration; } else { msg.duration = config.defaultDuration; } if (typeof msg.targetChannel !== "undefined") { for (const ch of ChannelList.getList().values()) { - if (ch.getID() == msg.targetChannel || msg.targetChannel == config.allTarget) { + if ( + ch.getID() == msg.targetChannel || + msg.targetChannel == config.allTarget + ) { ch.sendNotification(msg); } } } if (typeof msg.targetUser !== "undefined") { - for (const socket of socketsBySocketID.values()) { + for (const socket of socketsByUUID.values()) { if (socket.getUserID() == msg.targetUser) { socket.sendNotification(msg); } diff --git a/src/ws/events/admin/handlers/rename_channel.ts b/src/ws/events/admin/handlers/rename_channel.ts index fbc9c63..2b6b024 100644 --- a/src/ws/events/admin/handlers/rename_channel.ts +++ b/src/ws/events/admin/handlers/rename_channel.ts @@ -1,6 +1,6 @@ import { ChannelList } from "../../../../channel/ChannelList"; import { ServerEventListener } from "../../../../util/types"; -import { socketsBySocketID } from "../../../Socket"; +import { socketsByUUID } from "../../../Socket"; export const rename_channel: ServerEventListener<"rename_channel"> = { id: "rename_channel", @@ -39,7 +39,7 @@ export const rename_channel: ServerEventListener<"rename_channel"> = { } } - for (const sock of socketsBySocketID.values()) { + for (const sock of socketsByUUID.values()) { // Are they in this channel? if (sock.currentChannelID !== oldID) continue; // Move them forcefully diff --git a/src/ws/events/admin/handlers/restart.ts b/src/ws/events/admin/handlers/restart.ts index 910d6aa..e7b2f49 100644 --- a/src/ws/events/admin/handlers/restart.ts +++ b/src/ws/events/admin/handlers/restart.ts @@ -1,6 +1,6 @@ import { readUser, updateUser } from "../../../../data/user"; import { ServerEventListener } from "../../../../util/types"; -import { findSocketsByUserID, socketsBySocketID } from "../../../Socket"; +import { findSocketsByUserID, socketsByUUID } from "../../../Socket"; let timeout: Timer; @@ -13,7 +13,7 @@ export const restart: ServerEventListener<"restart"> = { } // Let everyone know - for (const sock of socketsBySocketID.values()) { + for (const sock of socketsByUUID.values()) { sock.sendNotification({ id: "server-restart", target: "#piano", diff --git a/src/ws/events/admin/handlers/user_flag.ts b/src/ws/events/admin/handlers/user_flag.ts index 22731a5..3b9dd02 100644 --- a/src/ws/events/admin/handlers/user_flag.ts +++ b/src/ws/events/admin/handlers/user_flag.ts @@ -12,8 +12,11 @@ export const user_flag: ServerEventListener<"user_flag"> = { // User flag modification (changing some real specific shit) if (typeof msg._id !== "string") return; if (typeof msg.key !== "string") return; - if (typeof msg.remove !== "boolean" && typeof msg.value == "undefined") { - return + if ( + typeof msg.remove !== "boolean" && + typeof msg.value == "undefined" + ) { + return; } // socket.getCurrentChannel()?.logger.debug(msg); @@ -43,7 +46,5 @@ export const user_flag: ServerEventListener<"user_flag"> = { for (const ch of ChannelList.getList()) { ch.emit("user data update", user); } - - // socket.getCurrentChannel()?.logger.debug("socks:", socks); } }; diff --git a/src/ws/events/user/handlers/devices.ts b/src/ws/events/user/handlers/devices.ts index ed147b9..b05e52f 100644 --- a/src/ws/events/user/handlers/devices.ts +++ b/src/ws/events/user/handlers/devices.ts @@ -1,4 +1,4 @@ -import { ServerEventListener } from "../../../../util/types"; +import type { ServerEventListener } from "~/util/types"; export const devices: ServerEventListener<"devices"> = { id: "devices", diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts index 236f20e..5c48e1d 100644 --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -1,8 +1,10 @@ -import { Logger } from "../../../../util/Logger"; -import { getMOTD } from "../../../../util/motd"; -import { createToken, getToken, validateToken } from "../../../../util/token"; -import { ClientEvents, ServerEventListener } from "../../../../util/types"; -import { config } from "../../../usersConfig"; +import { getUserPermissions } from "~/data/permission"; +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 { Socket } from "~/ws/Socket"; +import { config, usersConfigPath } from "~/ws/usersConfig"; const logger = new Logger("Hi handler"); @@ -16,7 +18,7 @@ export const hi: ServerEventListener<"hi"> = { if (socket.gateway.hasProcessedHi) return; // Browser challenge - if (config.browserChallenge == "basic") { + if (config.browserChallenge === "basic") { try { if (typeof msg.code !== "string") return; const code = atob(msg.code); @@ -30,14 +32,21 @@ export const hi: ServerEventListener<"hi"> = { } } } catch (err) { - logger.warn("Unable to parse basic browser challenge code:", err); + logger.warn( + "Unable to parse basic browser challenge code:", + err + ); } - } else if (config.browserChallenge == "obf") { + } else if (config.browserChallenge === "obf") { // TODO } // Is the browser challenge enabled and has the user completed it? - if (config.browserChallenge !== "none" && !socket.gateway.hasCompletedBrowserChallenge) return socket.ban(60000, "Browser challenge not completed"); + if ( + config.browserChallenge !== "none" && + !socket.gateway.hasCompletedBrowserChallenge + ) + return socket.ban(60000, "Browser challenge not completed"); let token: string | undefined; let generatedToken = false; @@ -50,18 +59,26 @@ export const hi: ServerEventListener<"hi"> = { token = await getToken(socket.getUserID()); if (typeof token !== "string") { // Generate a new one - token = await createToken(socket.getUserID(), socket.gateway); + token = await createToken( + socket.getUserID(), + socket.gateway + ); socket.gateway.isTokenValid = true; if (typeof token !== "string") { - logger.warn(`Unable to generate token for user ${socket.getUserID()}`); + logger.warn( + `Unable to generate token for user ${socket.getUserID()}` + ); } else { generatedToken = true; } } } else { // Validate the token - const valid = await validateToken(socket.getUserID(), msg.token); + const valid = await validateToken( + socket.getUserID(), + msg.token + ); if (!valid) { //socket.ban(60000, "Invalid token"); //return; @@ -86,20 +103,27 @@ export const hi: ServerEventListener<"hi"> = { //logger.debug("Tag:", part.tag); - socket.sendArray([{ - m: "hi", - accountInfo: undefined, - permissions: undefined, - t: Date.now(), - u: { - _id: part._id, - color: part.color, - name: part.name, - tag: part.tag - }, - motd: getMOTD(), - token - }]); + const permissions: Record = {}; + (await getUserPermissions(socket.getUserID())).map(perm => { + permissions[perm] = true; + }); + + socket.sendArray([ + { + m: "hi", + accountInfo: undefined, + permissions, + t: Date.now(), + u: { + _id: part._id, + color: part.color, + name: part.name, + tag: config.enableTags ? part.tag : undefined + }, + motd: getMOTD(), + token + } + ]); socket.gateway.hasProcessedHi = true; } diff --git a/src/ws/message.ts b/src/ws/message.ts index f71acf0..1bbde64 100644 --- a/src/ws/message.ts +++ b/src/ws/message.ts @@ -1,5 +1,5 @@ import { Logger } from "../util/Logger"; -import { Socket } from "./Socket"; +import type { Socket } from "./Socket"; import { hasOwn } from "../util/helpers"; // const logger = new Logger("Message Handler"); diff --git a/src/ws/ratelimit/NoteQuota.ts b/src/ws/ratelimit/NoteQuota.ts index f90327d..78c7b6d 100644 --- a/src/ws/ratelimit/NoteQuota.ts +++ b/src/ws/ratelimit/NoteQuota.ts @@ -35,15 +35,15 @@ export class NoteQuota { params: { allowance: number; max: number; - maxHistLen: number; + maxHistLen?: number; } = NoteQuota.PARAMS_OFFLINE ) { - let allowance: number = + const allowance: number = params.allowance || this.allowance || NoteQuota.PARAMS_OFFLINE.allowance; - let max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max; - let maxHistLen = + const max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max; + const maxHistLen = params.maxHistLen || this.maxHistLen || NoteQuota.PARAMS_OFFLINE.maxHistLen; @@ -90,19 +90,18 @@ export class NoteQuota { public spend(needed: number) { let sum = 0; + let numNeeded = needed; for (const i in this.history) { sum += this.history[i]; } - if (sum <= 0) needed *= this.allowance; + if (sum <= 0) numNeeded *= this.allowance; + if (this.points < numNeeded) return false; - if (this.points < needed) { - return false; - } else { - this.points -= needed; - if (this.cb) this.cb(this.points); - return true; - } + this.points -= numNeeded; + if (this.cb) this.cb(this.points); + + return true; } } diff --git a/src/ws/ratelimit/RateLimit.ts b/src/ws/ratelimit/RateLimit.ts index 2332411..dee9b37 100644 --- a/src/ws/ratelimit/RateLimit.ts +++ b/src/ws/ratelimit/RateLimit.ts @@ -1,9 +1,9 @@ // Thank you Brandon for this thing export class RateLimit { - public after: number = 0; - constructor(private interval_ms: number = 0) {} + public after = 0; + constructor(private interval_ms = 0) {} - public attempt(time: number = Date.now()) { + public attempt(time = Date.now()) { if (time < this.after) return false; this.after = time + this.interval_ms; diff --git a/src/ws/ratelimit/config.ts b/src/ws/ratelimit/config.ts index ea77159..865afd0 100644 --- a/src/ws/ratelimit/config.ts +++ b/src/ws/ratelimit/config.ts @@ -1,6 +1,6 @@ -import { loadConfig } from "../../util/config"; -import { RateLimit } from "./RateLimit"; -import { RateLimitChain } from "./RateLimitChain"; +import { ConfigManager } from "../../util/config"; +import type { RateLimit } from "./RateLimit"; +import type { RateLimitChain } from "./RateLimitChain"; export interface RateLimitConfigList< RL = number, @@ -44,101 +44,104 @@ export interface RateLimitsConfig { admin: RateLimitConfigList; } -export const config = loadConfig("config/ratelimits.yml", { - user: { - normal: { - a: 6000 / 4, - m: 1000 / 20, - ch: 1000 / 1, - kickban: 1000 / 8, - unban: 1000 / 8, - t: 1000 / 128, - "+ls": 1000 / 60, - "-ls": 1000 / 60, - chown: 2000, +export const config = ConfigManager.loadConfig( + "config/ratelimits.yml", + { + user: { + normal: { + a: 6000 / 4, + m: 1000 / 20, + ch: 1000 / 1, + kickban: 1000 / 8, + unban: 1000 / 8, + t: 1000 / 128, + "+ls": 1000 / 60, + "-ls": 1000 / 60, + chown: 2000, - hi: 1000 / 20, - bye: 1000 / 20, - devices: 1000 / 20, - "admin message": 1000 / 20 - }, - chains: { - userset: { - interval: 1000 * 60 * 30, - num: 1000 + hi: 1000 / 20, + bye: 1000 / 20, + devices: 1000 / 20, + "admin message": 1000 / 20 }, - chset: { - interval: 1000 * 60 * 30, - num: 1024 - }, - n: { - interval: 1000, - num: 512 + chains: { + userset: { + interval: 1000 * 60 * 30, + num: 1000 + }, + chset: { + interval: 1000 * 60 * 30, + num: 1024 + }, + n: { + interval: 1000, + num: 512 + } } - } - }, - crown: { - normal: { - a: 6000 / 10, - m: 1000 / 20, - ch: 1000 / 1, - kickban: 1000 / 8, - unban: 1000 / 8, - t: 1000 / 128, - "+ls": 1000 / 60, - "-ls": 1000 / 60, - chown: 2000, - - hi: 1000 / 20, - bye: 1000 / 20, - devices: 1000 / 20, - "admin message": 1000 / 20 }, - chains: { - userset: { - interval: 1000 * 60 * 30, - num: 1000 + crown: { + normal: { + a: 6000 / 10, + m: 1000 / 20, + ch: 1000 / 1, + kickban: 1000 / 8, + unban: 1000 / 8, + t: 1000 / 128, + "+ls": 1000 / 60, + "-ls": 1000 / 60, + chown: 2000, + + hi: 1000 / 20, + bye: 1000 / 20, + devices: 1000 / 20, + "admin message": 1000 / 20 }, - chset: { - interval: 1000 * 60 * 30, - num: 1024 - }, - n: { - interval: 1000, - num: 512 + chains: { + userset: { + interval: 1000 * 60 * 30, + num: 1000 + }, + chset: { + interval: 1000 * 60 * 30, + num: 1024 + }, + n: { + interval: 1000, + num: 512 + } } - } - }, - admin: { - normal: { - a: 6000 / 50, - m: 1000 / 60, - ch: 1000 / 10, - kickban: 1000 / 60, - unban: 1000 / 8, - 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: { - userset: { - interval: 500, - num: 1000 + admin: { + normal: { + a: 6000 / 50, + m: 1000 / 60, + ch: 1000 / 10, + kickban: 1000 / 60, + unban: 1000 / 8, + t: 1000 / 256, + "+ls": 1000 / 60, + "-ls": 1000 / 60, + chown: 500, + + hi: 1000 / 20, + bye: 1000 / 20, + devices: 1000 / 20, + "admin message": 1000 / 60 }, - chset: { - interval: 1000 * 60 * 30, - num: 1024 - }, - n: { - interval: 50, - num: 512 + chains: { + userset: { + interval: 500, + num: 1000 + }, + chset: { + interval: 1000 * 60 * 30, + num: 1024 + }, + n: { + interval: 50, + num: 512 + } } } } -}); +); diff --git a/src/ws/ratelimit/limits/admin.ts b/src/ws/ratelimit/limits/admin.ts index 18894ca..4a136ab 100644 --- a/src/ws/ratelimit/limits/admin.ts +++ b/src/ws/ratelimit/limits/admin.ts @@ -1,6 +1,6 @@ import { RateLimit } from "../RateLimit"; import { RateLimitChain } from "../RateLimitChain"; -import { RateLimitConstructorList, config } from "../config"; +import { type RateLimitConstructorList, config } from "../config"; export const adminLimits: RateLimitConstructorList = { normal: { diff --git a/src/ws/ratelimit/limits/crown.ts b/src/ws/ratelimit/limits/crown.ts index a9b102c..c4a77ab 100644 --- a/src/ws/ratelimit/limits/crown.ts +++ b/src/ws/ratelimit/limits/crown.ts @@ -1,6 +1,6 @@ import { RateLimit } from "../RateLimit"; import { RateLimitChain } from "../RateLimitChain"; -import { RateLimitConstructorList, config } from "../config"; +import { type RateLimitConstructorList, config } from "../config"; export const crownLimits: RateLimitConstructorList = { normal: { diff --git a/src/ws/ratelimit/limits/user.ts b/src/ws/ratelimit/limits/user.ts index 0266b7d..0b18cfd 100644 --- a/src/ws/ratelimit/limits/user.ts +++ b/src/ws/ratelimit/limits/user.ts @@ -1,6 +1,6 @@ import { RateLimit } from "../RateLimit"; import { RateLimitChain } from "../RateLimitChain"; -import { RateLimitConstructorList, config } from "../config"; +import { type RateLimitConstructorList, config } from "../config"; export const userLimits: RateLimitConstructorList = { normal: { diff --git a/src/ws/server.ts b/src/ws/server.ts index f7aefc6..d7a1b76 100644 --- a/src/ws/server.ts +++ b/src/ws/server.ts @@ -1,13 +1,13 @@ import { Logger } from "../util/Logger"; import { createSocketID, createUserID } from "../util/id"; -import fs from "fs"; -import path from "path"; +import fs from "node:fs"; +import path from "node:path"; import { handleMessage } from "./message"; -import { Socket, socketsBySocketID } from "./Socket"; +import { Socket, socketsByUUID } from "./Socket"; import env from "../util/env"; import { getMOTD } from "../util/motd"; import nunjucks from "nunjucks"; -import { metrics } from "../util/metrics"; +import type { ServerWebSocket } from "bun"; const logger = new Logger("WebSocket Server"); @@ -38,6 +38,8 @@ async function getIndex() { return response; } +type ServerWebSocketMPP = ServerWebSocket<{ ip: string, socket: Socket }> + export const app = Bun.serve<{ ip: string }>({ port: env.PORT, hostname: "0.0.0.0", @@ -78,31 +80,31 @@ export const app = Bun.serve<{ ip: string }>({ // Return the file if (data) { return new Response(data); - } else { - return getIndex(); } - } else { - // Return the index file, since it's a channel name or something + return getIndex(); } + + // Return the index file, since it's a channel name or something + return getIndex(); } catch (err) { // Return the index file as a coverup of our extreme failure return getIndex(); } }, websocket: { - open: ws => { + open: (ws: ServerWebSocketMPP) => { // swimming in the pool const socket = new Socket(ws, createSocketID()); - (ws as unknown as any).socket = socket; + ws.data.socket = socket; // logger.debug("Connection at " + socket.getIP()); - if (socket.socketID == undefined) { + if (socket.socketID === undefined) { socket.socketID = createSocketID(); } - socketsBySocketID.set(socket.socketID, socket); + socketsByUUID.set(socket.getUUID(), socket); const ip = socket.getIP(); @@ -121,29 +123,29 @@ export const app = Bun.serve<{ ip: string }>({ } }, - message: (ws, message) => { + message: (ws: ServerWebSocketMPP, message: string) => { // Fucking string const msg = message.toString(); // Let's find out wtf they even sent - handleMessage((ws as unknown as any).socket, msg); + handleMessage(ws.data.socket, msg); }, - close: (ws, code, message) => { + close: (ws: ServerWebSocketMPP, code, message) => { // 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; + const socket = ws.data.socket as Socket; if (socket) { socket.destroy(); - for (const sockID of socketsBySocketID.keys()) { - const sock = socketsBySocketID.get(sockID); + for (const sockID of socketsByUUID.keys()) { + const sock = socketsByUUID.get(sockID); - if (sock == socket) { - socketsBySocketID.delete(sockID); + if (sock === socket) { + socketsByUUID.delete(sockID); } } } diff --git a/src/ws/usersConfig.ts b/src/ws/usersConfig.ts index 685bef2..a3b3cbe 100644 --- a/src/ws/usersConfig.ts +++ b/src/ws/usersConfig.ts @@ -1,5 +1,5 @@ -import { loadConfig } from "../util/config"; -import { Participant, UserFlags } from "../util/types"; +import { ConfigManager } from "../util/config"; +import type { Participant, UserFlags } from "../util/types"; export interface UsersConfig { defaultName: string; @@ -45,7 +45,7 @@ export const defaultUsersConfig: UsersConfig = { // 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( +export const config = ConfigManager.loadConfig( usersConfigPath, defaultUsersConfig ); diff --git a/tsconfig.json b/tsconfig.json index 8afd498..5f96ec9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,9 @@ "moduleResolution": "Bundler", // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + "paths": { + "~/*": ["./src/*"] + }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */