Add permissions

This commit is contained in:
Hri7566 2024-09-15 12:41:51 -04:00
parent 8eef0cd925
commit 85643184ea
34 changed files with 1070 additions and 588 deletions

4
.gitignore vendored
View File

@ -7,8 +7,8 @@ node_modules
# SQLite databases # SQLite databases
prisma/*.sqlite prisma/*.sqlite
# TS build # Build script output
/out /dist
# JWT token keypair # JWT token keypair
mppkey mppkey

9
config/permissions.yml Normal file
View File

@ -0,0 +1,9 @@
admin:
- clearChat
- vanish
- chsetAnywhere
- chownAnywhere
- usersetOthers
- siteBan
- siteBanAnyReason
- siteBanAnyDuration

View File

@ -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"]

View File

@ -32,3 +32,14 @@ model Channel {
forceload Boolean @default(false) // Whether the channel is forceloaded 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
}

12
scripts/build.js Normal file
View File

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

View File

@ -1,6 +1,6 @@
import EventEmitter from "events"; import EventEmitter from "events";
import { Logger } from "../util/Logger"; import { Logger } from "../util/Logger";
import { import type {
ChannelSettingValue, ChannelSettingValue,
IChannelSettings, IChannelSettings,
ClientEvents, ClientEvents,
@ -10,19 +10,28 @@ import {
Notification, Notification,
UserFlags, UserFlags,
Tag, Tag,
ChannelFlags as TChannelFlags
} from "../util/types"; } from "../util/types";
import type { Socket } from "../ws/Socket"; import type { Socket } from "../ws/Socket";
import { validateChannelSettings } from "./settings"; import { validateChannelSettings } from "./settings";
import { findSocketByPartID, socketsBySocketID } from "../ws/Socket"; import { findSocketByPartID, socketsByUUID } from "../ws/Socket";
import Crown from "./Crown"; import Crown from "./Crown";
import { ChannelList } from "./ChannelList"; import { ChannelList } from "./ChannelList";
import { config } from "./config"; import { config } from "./config";
import { config as usersConfig } from "../ws/usersConfig"; import { config as usersConfig } from "../ws/usersConfig";
import { saveChatHistory, getChatHistory, deleteChatHistory } from "../data/history"; import {
import { mixin, darken } from "../util/helpers"; saveChatHistory,
import { User } from "@prisma/client"; 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 { heapStats } from "bun:jsc";
import { deleteSavedChannel, getSavedChannel, saveChannel } from "../data/channel"; import {
deleteSavedChannel,
getSavedChannel,
saveChannel
} from "../data/channel";
import { forceloadChannel } from "./forceLoad"; import { forceloadChannel } from "./forceLoad";
interface CachedKickban { interface CachedKickban {
@ -31,9 +40,15 @@ interface CachedKickban {
endTime: number; endTime: number;
} }
interface CachedCursor {
x: string | number;
y: string | number;
id: string;
}
interface ExtraPartData { interface ExtraPartData {
uuids: string[]; uuids: string[];
flags: Partial<UserFlags>; flags: UserFlags;
} }
type ExtraPart = Participant & ExtraPartData; type ExtraPart = Participant & ExtraPartData;
@ -48,10 +63,12 @@ export class Channel extends EventEmitter {
try { try {
this.chatHistory = await getChatHistory(this.getID()); this.chatHistory = await getChatHistory(this.getID());
this.sendArray([{ this.sendArray([
{
m: "c", m: "c",
c: this.chatHistory c: this.chatHistory
}]); }
]);
} catch (err) {} } catch (err) {}
} }
@ -112,22 +129,22 @@ export class Channel extends EventEmitter {
public logger: Logger; public logger: Logger;
public bans = new Array<CachedKickban>(); public bans = new Array<CachedKickban>();
public cursorCache = new Array<{ x: string | number; y: string | number; id: string }>(); public cursorCache = new Array<CachedCursor>();
public crown?: Crown; public crown?: Crown;
private flags: Record<string, any> = {}; private flags: TChannelFlags = {};
constructor( constructor(
private _id: string, private _id: string,
set?: Partial<IChannelSettings>, set?: Partial<IChannelSettings>,
creator?: Socket, creator?: Socket,
owner_id?: string, owner_id?: string,
public stays: boolean = false public stays = false
) { ) {
super(); super();
this.logger = new Logger("Channel - " + _id, "logs/channel"); this.logger = new Logger(`Channel - ${_id}`, "logs/channel");
this.settings = {}; this.settings = {};
// Copy default settings // Copy default settings
@ -138,8 +155,8 @@ export class Channel extends EventEmitter {
// Copied from changeSettings below // Copied from changeSettings below
// TODO do these cases need to be here? can this be determined another way? // TODO do these cases need to be here? can this be determined another way?
if ( if (
typeof set.color == "string" && typeof set.color === "string" &&
(typeof set.color2 == "undefined" || (typeof set.color2 === "undefined" ||
set.color2 === this.settings.color2) set.color2 === this.settings.color2)
) { ) {
//this.logger.debug("color 2 darken triggered"); //this.logger.debug("color 2 darken triggered");
@ -152,8 +169,8 @@ export class Channel extends EventEmitter {
// Set the verified settings // Set the verified settings
for (const key of Object.keys(validatedSet)) { for (const key of Object.keys(validatedSet)) {
//this.logger.debug(`${key}: ${(validatedSet as any)[key]}`); //this.logger.debug(`${key}: ${(validatedSet as any)[key]}`);
if ((validatedSet as any)[key] === false) continue; if (validatedSet[key] === false) continue;
(this.settings as any)[key] = (set as any)[key]; this.settings[key] = set[key];
} }
} }
@ -172,11 +189,13 @@ export class Channel extends EventEmitter {
this.bindEventListeners(); this.bindEventListeners();
ChannelList.add(this); 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"); this.logger.info("Created");
if (this.getID() == "test/mem") { if (this.getID() === "test/mem") {
setInterval(() => { setInterval(() => {
this.printMemoryInChat(); this.printMemoryInChat();
}, 1000); }, 1000);
@ -194,7 +213,7 @@ export class Channel extends EventEmitter {
this.on("update", (self, uuid) => { this.on("update", (self, uuid) => {
// Send updated info // Send updated info
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
for (const p of this.ppl) { for (const p of this.ppl) {
const socketUUID = socket.getUUID(); 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) { if (config.channelDestroyTimeout) {
setTimeout(() => { setTimeout(() => {
this.destroy(); this.destroy();
@ -221,10 +240,7 @@ export class Channel extends EventEmitter {
} }
}); });
const BANNED_WORDS = [ const BANNED_WORDS = ["AMIGHTYWIND", "CHECKLYHQ"];
"AMIGHTYWIND",
"CHECKLYHQ"
];
this.on("a", async (msg: ServerEvents["a"], socket: Socket) => { this.on("a", async (msg: ServerEvents["a"], socket: Socket) => {
try { try {
@ -233,7 +249,13 @@ export class Channel extends EventEmitter {
const userFlags = socket.getUserFlags(); const userFlags = socket.getUserFlags();
if (userFlags) { 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; if (!this.settings.chat) return;
@ -241,7 +263,13 @@ export class Channel extends EventEmitter {
if (msg.message.length > 512) return; if (msg.message.length > 512) return;
for (const word of BANNED_WORDS) { 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; return;
} }
} }
@ -256,7 +284,7 @@ export class Channel extends EventEmitter {
const part = socket.getParticipant() as Participant; const part = socket.getParticipant() as Participant;
let outgoing: ClientEvents["a"] = { const outgoing: ClientEvents["a"] = {
m: "a", m: "a",
a: msg.message, a: msg.message,
t: Date.now(), t: Date.now(),
@ -274,7 +302,9 @@ export class Channel extends EventEmitter {
} }
} catch (err) { } catch (err) {
this.logger.error(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 cmd = args[0].substring(1);
const ownsChannel = this.hasUser(socket.getUserID()); const ownsChannel = this.hasUser(socket.getUserID());
if (cmd == "help") { if (cmd === "help") {
} else if (cmd == "mem") { } else if (cmd === "mem") {
this.printMemoryInChat(); this.printMemoryInChat();
} }
}); });
@ -294,18 +324,26 @@ export class Channel extends EventEmitter {
if (typeof user.name !== "string") return; if (typeof user.name !== "string") return;
if (typeof user.color !== "string") return; if (typeof user.color !== "string") return;
if (typeof user.id !== "string") return; if (typeof user.id !== "string") return;
if (typeof user.tag !== "undefined" && typeof user.tag !== "string") return; if (
if (typeof user.flags !== "undefined" && typeof user.flags !== "string") return; typeof user.tag !== "undefined" &&
typeof user.tag !== "string"
)
return;
if (
typeof user.flags !== "undefined" &&
typeof user.flags !== "string"
)
return;
let tag; let tag: Tag | undefined;
let flags; let flags: UserFlags | undefined;
try { try {
tag = JSON.parse(user.tag); tag = JSON.parse(user.tag);
} catch (err) {} } catch (err) {}
try { try {
flags = JSON.parse(user.flags); flags = JSON.parse(user.flags) as UserFlags;
} catch (err) {} } catch (err) {}
for (const p of this.ppl) { for (const p of this.ppl) {
@ -315,13 +353,15 @@ export class Channel extends EventEmitter {
p.name = user.name; p.name = user.name;
p.color = user.color; p.color = user.color;
p.tag = tag; 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) { for (const cursor of this.cursorCache) {
if (cursor.id == p.id) { if (cursor.id === p.id) {
found = cursor found = cursor;
} }
} }
@ -333,8 +373,7 @@ export class Channel extends EventEmitter {
y = found.y; y = found.y;
} }
this.sendArray( this.sendArray([
[
{ {
m: "p", m: "p",
_id: p._id, _id: p._id,
@ -345,8 +384,7 @@ export class Channel extends EventEmitter {
y: y, y: y,
tag: usersConfig.enableTags ? p.tag : undefined tag: usersConfig.enableTags ? p.tag : undefined
} }
] ]);
);
} }
//this.logger.debug("Update from user data update handler"); //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 }) => { this.on("cursor", (pos: CachedCursor) => {
let found; let found: CachedCursor | undefined;
for (const cursor of this.cursorCache) { for (const cursor of this.cursorCache) {
if (cursor.id == pos.id) { if (cursor.id === pos.id) {
found = cursor; found = cursor;
} }
} }
@ -379,9 +417,8 @@ export class Channel extends EventEmitter {
{ {
m: "m", m: "m",
id: pos.id, id: pos.id,
// not type safe x: pos.x,
x: pos.x as string, y: pos.y
y: pos.y as string
} }
]); ]);
}); });
@ -416,7 +453,7 @@ export class Channel extends EventEmitter {
*/ */
public isLobby() { public isLobby() {
for (const reg of config.lobbyRegexes) { for (const reg of config.lobbyRegexes) {
let exp = new RegExp(reg, "g"); const exp = new RegExp(reg, "g");
if (this.getID().match(exp)) { if (this.getID().match(exp)) {
return true; return true;
@ -430,9 +467,13 @@ export class Channel extends EventEmitter {
* Determine whether this channel is a lobby with the name "lobby" in it * Determine whether this channel is a lobby with the name "lobby" in it
*/ */
public isTrueLobby() { public isTrueLobby() {
if (this.getID().match("^lobby[0-9][0-9]$") && this.getID().match("^lobby[0-9]$") && this.getID().match("^lobby$"), "^lobbyNaN$") return true; const _id = this.getID();
return (
return false; _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) * @param admin Whether a user is changing the settings (set to true to force the changes)
* @returns undefined * @returns undefined
*/ */
public changeSettings( public changeSettings(set: Partial<IChannelSettings>, admin = false) {
set: Partial<IChannelSettings>,
admin: boolean = false
) {
if (this.isDestroyed()) return; if (this.isDestroyed()) return;
if (!admin) { if (!admin) {
if (set.lobby) set.lobby = undefined; if (set.lobby) set.lobby = undefined;
@ -452,8 +490,8 @@ export class Channel extends EventEmitter {
} }
if ( if (
typeof set.color == "string" && typeof set.color === "string" &&
(typeof set.color2 == "undefined" || (typeof set.color2 === "undefined" ||
set.color2 === this.settings.color2) set.color2 === this.settings.color2)
) { ) {
set.color2 = darken(set.color); set.color2 = darken(set.color);
@ -467,8 +505,8 @@ export class Channel extends EventEmitter {
// Set the verified settings // Set the verified settings
for (const key of Object.keys(validatedSet)) { for (const key of Object.keys(validatedSet)) {
//this.logger.debug(`${key}: ${(validatedSet as any)[key]}`); //this.logger.debug(`${key}: ${(validatedSet as any)[key]}`);
if ((validatedSet as any)[key] === false) continue; if (validatedSet[key] === false) continue;
(this.settings as any)[key] = (set as any)[key]; this.settings[key] = set[key];
} }
/* /*
@ -524,13 +562,17 @@ export class Channel extends EventEmitter {
for (const ch of chs) { for (const ch of chs) {
const chid = ch.getID(); const chid = ch.getID();
if (chid == config.fullChannel) { if (chid === config.fullChannel) {
const banTime = this.getBanTime(socket.getUserID()); const banTime = this.getBanTime(socket.getUserID());
//this.logger.debug("Ban time:", banTime); //this.logger.debug("Ban time:", banTime);
if (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({ socket.sendNotification({
class: "short", 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(); const nextID = this.getNextLobbyID();
//this.logger.debug("New ID:", nextID); //this.logger.debug("New ID:", nextID);
// Move them to the next lobby // 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) { for (const p of this.ppl) {
if (p.id !== part.id) continue; if (p.id !== part.id) continue;
p.uuids.push(socket.getUUID()) p.uuids.push(socket.getUUID());
} }
//socket.sendChannelUpdate(this.getInfo(), this.getParticipantList()); //socket.sendChannelUpdate(this.getInfo(), this.getParticipantList());
@ -578,9 +622,9 @@ export class Channel extends EventEmitter {
name: part.name, name: part.name,
color: part.color, color: part.color,
id: part.id, id: part.id,
tag: part.tag,
uuids: [socket.getUUID()], uuids: [socket.getUUID()],
flags: socket.getUserFlags() || {} flags: socket.getUserFlags() || {},
tag: part.tag
}); });
} }
@ -590,7 +634,7 @@ export class Channel extends EventEmitter {
if (socket.currentChannelID) { if (socket.currentChannelID) {
// Find the other channel they were in // Find the other channel they were in
const ch = ChannelList.getList().find( const ch = ChannelList.getList().find(
ch => ch._id == socket.currentChannelID ch => ch._id === socket.currentChannelID
); );
// Tell the channel they left // Tell the channel they left
@ -611,7 +655,7 @@ export class Channel extends EventEmitter {
if (this.crown && config.chownOnRejoin) { if (this.crown && config.chownOnRejoin) {
// TODO Should we check participant ID as well? // TODO Should we check participant ID as well?
if (typeof this.crown.userId !== "undefined") { if (typeof this.crown.userId !== "undefined") {
if (socket.getUserID() == this.crown.userId) { if (socket.getUserID() === this.crown.userId) {
// Check if they exist // Check if they exist
const p = socket.getParticipant(); const p = socket.getParticipant();
@ -653,7 +697,8 @@ export class Channel extends EventEmitter {
color: part.color, color: part.color,
id: part.id, id: part.id,
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y y: cursorPos.y,
tag: usersConfig.enableTags ? part.tag : undefined
} }
], ],
part.id part.id
@ -676,9 +721,9 @@ export class Channel extends EventEmitter {
const part = socket.getParticipant() as Participant; const part = socket.getParticipant() as Participant;
let dupeCount = 0; let dupeCount = 0;
for (const s of socketsBySocketID.values()) { for (const s of socketsByUUID.values()) {
if (s.getParticipantID() == part.id) { if (s.getParticipantID() === part.id) {
if (s.currentChannelID == this.getID()) { if (s.currentChannelID === this.getID()) {
dupeCount++; dupeCount++;
} }
} }
@ -686,14 +731,14 @@ export class Channel extends EventEmitter {
// this.logger.debug("Dupes:", dupeCount); // this.logger.debug("Dupes:", dupeCount);
if (dupeCount == 1) { if (dupeCount === 1) {
const p = this.ppl.find(p => p.id == socket.getParticipantID()); const p = this.ppl.find(p => p.id === socket.getParticipantID());
if (p) { if (p) {
this.ppl.splice(this.ppl.indexOf(p), 1); this.ppl.splice(this.ppl.indexOf(p), 1);
if (this.crown) { if (this.crown) {
if (this.crown.participantId == p.id) { if (this.crown.participantId === p.id) {
// Channel owner left, reset crown timeout // Channel owner left, reset crown timeout
this.chown(); this.chown();
} }
@ -751,19 +796,22 @@ export class Channel extends EventEmitter {
/** /**
* Get the people in this channel * Get the people in this channel
* @param showVanished Whether to include vanished users
* @returns List of people * @returns List of people
*/ */
public getParticipantList() { public getParticipantList(showVanished = false) {
const ppl = []; const ppl = [];
for (const p of this.ppl) { for (const p of this.ppl) {
if (p.flags.vanish) continue; if (p.flags.vanish && !showVanished) continue;
ppl.push({ ppl.push({
_id: p._id, _id: p._id,
name: p.name, name: p.name,
color: p.color, color: p.color,
id: p.id, 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 * @returns Boolean
*/ */
public hasUser(_id: string) { public hasUser(_id: string) {
const foundPart = this.ppl.find(p => p._id == _id); const foundPart = this.ppl.find(p => p._id === _id);
return !!foundPart; return !!foundPart;
} }
@ -790,7 +838,7 @@ export class Channel extends EventEmitter {
* @returns Boolean * @returns Boolean
*/ */
public hasParticipant(id: string) { public hasParticipant(id: string) {
const foundPart = this.ppl.find(p => p.id == id); const foundPart = this.ppl.find(p => p.id === id);
return !!foundPart; return !!foundPart;
} }
@ -802,19 +850,18 @@ export class Channel extends EventEmitter {
arr: ClientEvents[EventID][], arr: ClientEvents[EventID][],
blockPartID?: string blockPartID?: string
) { ) {
let sentSocketIDs = new Array<string>(); const sentSocketIDs = new Array<string>();
for (const p of this.ppl) { for (const p of this.ppl) {
if (blockPartID) { if (blockPartID) {
if (p.id == blockPartID) continue; if (p.id === blockPartID) continue;
} }
socketLoop: for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
if (socket.isDestroyed()) continue socketLoop; if (socket.isDestroyed()) continue;
if (!socket.socketID) continue socketLoop; if (!socket.socketID) continue;
if (socket.getParticipantID() != p.id) continue socketLoop; if (socket.getParticipantID() !== p.id) continue;
if (sentSocketIDs.includes(socket.socketID)) if (sentSocketIDs.includes(socket.socketID)) continue;
continue socketLoop;
socket.sendArray(arr); socket.sendArray(arr);
sentSocketIDs.push(socket.socketID); sentSocketIDs.push(socket.socketID);
} }
@ -837,27 +884,27 @@ export class Channel extends EventEmitter {
pianoPartID = part.id; pianoPartID = part.id;
} }
let clientMsg: ClientEvents["n"] = { const clientMsg: ClientEvents["n"] = {
m: "n", m: "n",
n: msg.n, n: msg.n,
t: msg.t, t: msg.t,
p: pianoPartID p: pianoPartID
}; };
let sentSocketIDs = new Array<string>(); const sentSocketIDs = new Array<string>();
for (const p of this.ppl) { for (const p of this.ppl) {
socketLoop: for (const sock of socketsBySocketID.values()) { for (const sock of socketsByUUID.values()) {
if (sock.isDestroyed()) continue socketLoop; if (sock.isDestroyed()) continue;
if (!sock.socketID) continue socketLoop; if (!sock.socketID) continue;
if (socket) { 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 (socket.getParticipantID() == part.id) continue socketLoop;
if (sentSocketIDs.includes(sock.socketID)) continue socketLoop; if (sentSocketIDs.includes(sock.socketID)) continue;
sock.sendArray([clientMsg]); sock.sendArray([clientMsg]);
sentSocketIDs.push(sock.socketID); sentSocketIDs.push(sock.socketID);
@ -876,7 +923,7 @@ export class Channel extends EventEmitter {
this.destroyed = true; this.destroyed = true;
if (this.ppl.length > 0) { if (this.ppl.length > 0) {
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
if (socket.currentChannelID !== this.getID()) continue; if (socket.currentChannelID !== this.getID()) continue;
socket.setChannel(config.fullChannel); socket.setChannel(config.fullChannel);
} }
@ -939,31 +986,31 @@ export class Channel extends EventEmitter {
if (this.crown) { if (this.crown) {
this.crown.time = Date.now(); this.crown.time = Date.now();
let socket; let socket: Socket | undefined;
if (this.crown.participantId) if (this.crown.participantId)
socket = findSocketByPartID(this.crown.participantId); socket = findSocketByPartID(this.crown.participantId);
let x = Math.random() * 100; const x = Math.random() * 100;
let y1 = Math.random() * 100; const y1 = Math.random() * 100;
let y2 = y1 + Math.random() * (100 - y1); const y2 = y1 + Math.random() * (100 - y1);
if (socket) { if (socket) {
const cursorPos = socket.getCursorPos(); const cursorPos = socket.getCursorPos();
let cursorX = cursorPos.x; let cursorX = cursorPos.x;
if (typeof cursorPos.x == "string") if (typeof cursorPos.x === "string")
cursorX = parseInt(cursorPos.x); cursorX = Number.parseInt(cursorPos.x);
let cursorY = cursorPos.y; let cursorY = cursorPos.y;
if (typeof cursorPos.y == "string") if (typeof cursorPos.y === "string")
cursorY = parseInt(cursorPos.y); cursorY = Number.parseInt(cursorPos.y);
} }
// Screen positions // Screen positions
this.crown.startPos = { x, y: y1 }; this.crown.startPos = { x, y: y1 };
this.crown.endPos = { x, y: y2 }; this.crown.endPos = { x, y: y2 };
delete this.crown.participantId; this.crown.participantId = undefined;
//this.logger.debug("Update from dropCrown"); //this.logger.debug("Update from dropCrown");
this.emit("update", this); this.emit("update", this);
@ -975,14 +1022,18 @@ export class Channel extends EventEmitter {
* @param _id User ID to ban * @param _id User ID to ban
* @param t Time in millseconds to ban for * @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(); const now = Date.now();
if (t < 0 || t > 300 * 60 * 1000) return; if (t < 0 || t > 300 * 60 * 1000) return;
let shouldUpdate = false; let shouldUpdate = false;
const banChannel = ChannelList.getList().find( const banChannel = ChannelList.getList().find(
ch => ch.getID() == config.fullChannel ch => ch.getID() === config.fullChannel
); );
if (!banChannel) return; if (!banChannel) return;
@ -990,8 +1041,8 @@ export class Channel extends EventEmitter {
// Check if they are on the server at all // Check if they are on the server at all
let bannedPart: Participant | undefined; let bannedPart: Participant | undefined;
const bannedUUIDs: string[] = []; const bannedUUIDs: string[] = [];
for (const sock of socketsBySocketID.values()) { for (const sock of socketsByUUID.values()) {
if (sock.getUserID() == _id) { if (sock.getUserID() === _id) {
bannedUUIDs.push(sock.getUUID()); bannedUUIDs.push(sock.getUUID());
const part = sock.getParticipant(); const part = sock.getParticipant();
@ -1001,7 +1052,7 @@ export class Channel extends EventEmitter {
if (!bannedPart) return; 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; let overwrite = false;
if (isBanned) { if (isBanned) {
@ -1019,24 +1070,24 @@ export class Channel extends EventEmitter {
shouldUpdate = true; shouldUpdate = true;
} else { } else {
for (const ban of this.bans) { for (const ban of this.bans) {
if (ban.userId !== _id) continue; if (ban.userId !== _id) continue;
ban.startTime = now; ban.startTime = now;
ban.endTime = now + t; ban.endTime = now + t;
} }
shouldUpdate = true; shouldUpdate = true;
} }
uuidsToKick = [...uuidsToKick, ...bannedUUIDs]; uuidsToKick = [...uuidsToKick, ...bannedUUIDs];
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
if (uuidsToKick.indexOf(socket.getUUID()) !== -1) { if (uuidsToKick.indexOf(socket.getUUID()) !== -1) {
socket.sendNotification({ socket.sendNotification({
title: "Notice", 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, duration: 7000,
target: "#room", target: "#room",
class: "short" class: "short"
@ -1045,7 +1096,7 @@ export class Channel extends EventEmitter {
// If they are here, move them to the ban channel // If they are here, move them to the ban channel
const ch = socket.getCurrentChannel(); const ch = socket.getCurrentChannel();
if (ch) { if (ch) {
if (ch.getID() == this.getID()) if (ch.getID() === this.getID())
socket.setChannel(banChannel.getID()); socket.setChannel(banChannel.getID());
} }
} }
@ -1056,14 +1107,19 @@ export class Channel extends EventEmitter {
this.emit("update", this); this.emit("update", this);
if (typeof banner !== "undefined") { 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); const minutes = Math.floor(t / 1000 / 60);
if (p && bannedPart) { if (p && bannedPart) {
await this.sendChat({ await this.sendChat(
{
m: "a", m: "a",
message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.` message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.`
}, p); },
p
);
this.sendNotification({ this.sendNotification({
title: "Notice", title: "Notice",
text: `${p.name} banned ${bannedPart.name} from the channel for ${minutes} minutes.`, text: `${p.name} banned ${bannedPart.name} from the channel for ${minutes} minutes.`,
@ -1072,7 +1128,7 @@ export class Channel extends EventEmitter {
class: "short" class: "short"
}); });
if (banner == _id) { if (banner === _id) {
const certificate = { const certificate = {
title: "Certificate of Award", title: "Certificate of Award",
text: `Let it be known that ${p.name} kickbanned him/her self.`, text: `Let it be known that ${p.name} kickbanned him/her self.`,
@ -1082,7 +1138,7 @@ export class Channel extends EventEmitter {
this.sendNotification(certificate); this.sendNotification(certificate);
for (const s of socketsBySocketID.values()) { for (const s of socketsByUUID.values()) {
const userID = s.getUserID(); const userID = s.getUserID();
if (!userID) continue; if (!userID) continue;
if (userID !== banner) continue; if (userID !== banner) continue;
@ -1110,7 +1166,7 @@ export class Channel extends EventEmitter {
} }
// Check if they are banned // Check if they are banned
if (ban.userId == _id) { if (ban.userId === _id) {
return true; return true;
} }
} }
@ -1128,7 +1184,7 @@ export class Channel extends EventEmitter {
if (!isBanned) return; if (!isBanned) return;
for (const ban of this.bans) { for (const ban of this.bans) {
if (ban.userId == _id) { if (ban.userId === _id) {
this.bans.splice(this.bans.indexOf(ban), 1); this.bans.splice(this.bans.indexOf(ban), 1);
} }
} }
@ -1141,10 +1197,12 @@ export class Channel extends EventEmitter {
this.chatHistory = []; this.chatHistory = [];
await saveChatHistory(this.getID(), this.chatHistory); await saveChatHistory(this.getID(), this.chatHistory);
this.sendArray([{ this.sendArray([
{
m: "c", m: "c",
c: this.chatHistory c: this.chatHistory
}]); }
]);
} }
/** /**
@ -1152,7 +1210,8 @@ export class Channel extends EventEmitter {
* @param notif Notification to send * @param notif Notification to send
**/ **/
public sendNotification(notif: Notification) { public sendNotification(notif: Notification) {
this.sendArray([{ this.sendArray([
{
m: "notification", m: "notification",
id: notif.id, id: notif.id,
target: notif.target, target: notif.target,
@ -1161,7 +1220,8 @@ export class Channel extends EventEmitter {
title: notif.title, title: notif.title,
text: notif.text, text: notif.text,
html: notif.html html: notif.html
}]); }
]);
} }
/** /**
@ -1180,7 +1240,7 @@ export class Channel extends EventEmitter {
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1") .replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
.trim(); .trim();
let outgoing: ClientEvents["a"] = { const outgoing: ClientEvents["a"] = {
m: "a", m: "a",
a: msg.message, a: msg.message,
t: Date.now(), t: Date.now(),
@ -1203,10 +1263,13 @@ export class Channel extends EventEmitter {
* @param message Message to send in chat * @param message Message to send in chat
**/ **/
public async sendChatAdmin(message: string) { public async sendChatAdmin(message: string) {
this.sendChat({ this.sendChat(
{
m: "a", m: "a",
message message
}, usersConfig.adminParticipant); },
usersConfig.adminParticipant
);
} }
/** /**
@ -1214,7 +1277,10 @@ export class Channel extends EventEmitter {
* @param key Flag ID * @param key Flag ID
* @param val Value of which the flag will be set to * @param val Value of which the flag will be set to
**/ **/
public setFlag(key: string, val: any) { public setFlag<K extends keyof TChannelFlags>(
key: K,
val: TChannelFlags[K]
) {
this.flags[key] = val; this.flags[key] = val;
} }
@ -1223,7 +1289,7 @@ export class Channel extends EventEmitter {
* @param key Flag ID * @param key Flag ID
* @returns Value of flag * @returns Value of flag
**/ **/
public getFlag(key: string) { public getFlag<K extends keyof TChannelFlags>(key: K) {
return this.flags[key]; return this.flags[key];
} }
@ -1231,7 +1297,7 @@ export class Channel extends EventEmitter {
* Set the flags on this channel * Set the flags on this channel
* @param flags Flags to set * @param flags Flags to set
**/ **/
public setFlags(flags: Record<string, any>) { public setFlags(flags: TChannelFlags) {
this.flags = flags; this.flags = flags;
this.save(); this.save();
this.emit("update", this); this.emit("update", this);
@ -1244,8 +1310,8 @@ export class Channel extends EventEmitter {
public getNextLobbyID() { public getNextLobbyID() {
try { try {
const id = this.getID(); const id = this.getID();
if (id == "lobby") return "lobby2"; if (id === "lobby") return "lobby2";
const num = parseInt(id.substring(5)); const num = Number.parseInt(id.substring(5));
return `lobby${num + 1}`; return `lobby${num + 1}`;
} catch (err) { } catch (err) {
return config.fullChannel; return config.fullChannel;
@ -1259,7 +1325,7 @@ export class Channel extends EventEmitter {
**/ **/
public getBanTime(userId: string) { public getBanTime(userId: string) {
for (const ban of this.bans) { for (const ban of this.bans) {
if (userId == ban.userId) { if (userId === ban.userId) {
return { endTime: ban.endTime, startTime: ban.startTime }; return { endTime: ban.endTime, startTime: ban.startTime };
} }
} }
@ -1270,7 +1336,13 @@ export class Channel extends EventEmitter {
**/ **/
public printMemoryInChat() { public printMemoryInChat() {
const mem = heapStats(); 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`
);
} }
} }

View File

@ -1,4 +1,4 @@
import { loadConfig } from "../util/config"; import { ConfigManager } from "../util/config";
import { IChannelSettings } from "../util/types"; import { IChannelSettings } from "../util/types";
interface ChannelConfig { interface ChannelConfig {
@ -13,7 +13,9 @@ interface ChannelConfig {
channelDestroyTimeout: number; channelDestroyTimeout: number;
} }
export const config = loadConfig<ChannelConfig>("config/channels.yml", { export const config = ConfigManager.loadConfig<ChannelConfig>(
"config/channels.yml",
{
forceLoad: ["lobby", "test/awkward"], forceLoad: ["lobby", "test/awkward"],
lobbySettings: { lobbySettings: {
lobby: true, lobby: true,
@ -31,10 +33,17 @@ export const config = loadConfig<ChannelConfig>("config/channels.yml", {
visible: true visible: true
}, },
// Here's a terrifying fact: Brandon used parseInt to check lobby names // Here's a terrifying fact: Brandon used parseInt to check lobby names
lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[0-9]$", "^lobby$", "^lobbyNaN$", "^test/.+$"], lobbyRegexes: [
"^lobby[0-9][0-9]$",
"^lobby[0-9]$",
"^lobby$",
"^lobbyNaN$",
"^test/.+$"
],
lobbyBackdoor: "lolwutsecretlobbybackdoor", lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward", fullChannel: "test/awkward",
sendLimit: false, sendLimit: false,
chownOnRejoin: true, chownOnRejoin: true,
channelDestroyTimeout: 1000 channelDestroyTimeout: 1000
}); }
);

View File

@ -1,4 +1,4 @@
import { IChannelSettings } from "../util/types"; import type { IChannelSettings } from "~/util/types";
type Validator = "boolean" | "string" | "number" | ((val: unknown) => boolean); type Validator = "boolean" | "string" | "number" | ((val: unknown) => boolean);
@ -40,11 +40,11 @@ const adminOnlyKeys = [
*/ */
export function validateChannelSettings(set: Partial<IChannelSettings>, admin = false) { export function validateChannelSettings(set: Partial<IChannelSettings>, admin = false) {
// Create record // Create record
let record: Partial<Record<keyof IChannelSettings, boolean>> = {}; const record: Partial<Record<keyof IChannelSettings, boolean>> = {};
for (const key of Object.keys(set)) { for (const key of Object.keys(set)) {
let val = (set as Record<string, any>)[key]; const val = (set as Record<string, unknown>)[key];
let validator = ( const validator = (
validationRecord as Record<string, Validator | undefined> validationRecord as Record<string, Validator | undefined>
)[key]; )[key];
@ -66,12 +66,15 @@ export function validateChannelSettings(set: Partial<IChannelSettings>, admin =
export default validateChannelSettings; export default validateChannelSettings;
export function validate(value: any, validator: Validator) { export function validate(value: unknown, validator: Validator) {
// What type of validator? // What type of validator?
if (typeof validator === "function") { if (typeof validator === "function") {
// Run the function // Run the function
return validator(value) === true; return validator(value) === true;
} else if (typeof value === validator) { }
// biome-ignore lint/suspicious/useValidTypeof: biome is dumb
if (typeof value === validator) {
return true; return true;
} }

119
src/data/permission.ts Normal file
View File

@ -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<Record<string, string[]>>(
"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");
}

38
src/data/role.ts Normal file
View File

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

View File

@ -19,6 +19,7 @@ import { loadForcedStartupChannels } from "./channel/forceLoad";
import { Logger } from "./util/Logger"; import { Logger } from "./util/Logger";
// docker hates this next one // docker hates this next one
import { startReadline } from "./util/readline"; import { startReadline } from "./util/readline";
import { loadDefaultPermissions } from "./data/permission";
// wrapper for some reason // wrapper for some reason
export function startServer() { export function startServer() {
@ -28,6 +29,8 @@ export function startServer() {
logger.info("Forceloading startup channels..."); logger.info("Forceloading startup channels...");
loadForcedStartupChannels(); loadForcedStartupChannels();
loadDefaultPermissions();
// Break the console // Break the console
startReadline(); startReadline();

View File

@ -1,5 +1,6 @@
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, readFileSync, writeFileSync, watchFile } from "fs";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
import { Logger } from "./Logger";
/** /**
* This file uses the synchronous functions from the fs * This file uses the synchronous functions from the fs
@ -11,12 +12,22 @@ import { parse, stringify } from "yaml";
* program. * program.
*/ */
export class ConfigManager {
// public static configCache = new Map<string, unknown>();
public static logger: Logger;
static {
setTimeout(() => {
this.logger = new Logger("Config Loader");
});
}
/** /**
* Load a YAML config file and set default values if config path is nonexistent * Load a YAML config file and set default values if config path is nonexistent
* *
* Usage: * Usage:
* ```ts * ```ts
* const config = loadConfig("config/services.yml", { * const config = ConfigManager.loadConfig("config/services.yml", {
* enableMPP: false * enableMPP: false
* }); * });
* ``` * ```
@ -24,7 +35,9 @@ import { parse, stringify } from "yaml";
* @param defaultConfig Config to use if none is present (will save to path if used) * @param defaultConfig Config to use if none is present (will save to path if used)
* @returns Parsed YAML config * @returns Parsed YAML config
*/ */
export function loadConfig<T>(configPath: string, defaultConfig: T): T { public static loadConfig<T>(configPath: string, defaultConfig: T): T {
const self = this;
// Config exists? // Config exists?
if (existsSync(configPath)) { if (existsSync(configPath)) {
// Load config // Load config
@ -44,7 +57,10 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
changed = true; changed = true;
} }
if (typeof obj[key] == "object" && !Array.isArray(obj[key])) { if (
typeof obj[key] == "object" &&
!Array.isArray(obj[key])
) {
mix( mix(
obj[key] as Record<string, unknown>, obj[key] as Record<string, unknown>,
obj2[key] as Record<string, unknown> obj2[key] as Record<string, unknown>
@ -57,14 +73,38 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
mix(config, defRecord); mix(config, defRecord);
// Save config if modified // Save config if modified
if (changed) writeConfig(configPath, config); if (changed) this.writeConfig(configPath, config);
return config as T; // 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<T>(configPath);
return config;
} else { } else {
// Write default config to disk and use that // Write default config to disk and use that
//logger.warn(`Config file "${configPath}" not found, writing default config to disk`); //logger.warn(`Config file "${configPath}" not found, writing default config to disk`);
writeConfig(configPath, defaultConfig); this.writeConfig(configPath, defaultConfig);
return defaultConfig as T;
// 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<T>(configPath);
return defaultConfig;
} }
} }
@ -73,7 +113,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
* @param configPath * @param configPath
* @param config * @param config
*/ */
export function writeConfig<T>(configPath: string, config: T) { public static writeConfig<T>(configPath: string, config: T) {
// Write config to disk unconditionally // Write config to disk unconditionally
writeFileSync( writeFileSync(
configPath, configPath,
@ -82,3 +122,30 @@ export function writeConfig<T>(configPath: string, config: T) {
}) })
); );
} }
/**
* 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<T>(configPath: string) {
// const self = this;
// return new Proxy(
// {},
// {
// get(_target: unknown, name: string) {
// // Get the updated in-memory version of the config
// const config = self.configCache.get(configPath) as T;
// if (config) {
// if (config.hasOwnProperty(name))
// return (config as Record<string, unknown>)[
// name
// ] as T[keyof T];
// }
// }
// }
// ) as T;
// }
}

View File

@ -1,6 +1,14 @@
import { getRoles, giveRole, removeRole } from "~/data/role";
import { ChannelList } from "../../channel/ChannelList"; import { ChannelList } from "../../channel/ChannelList";
import { deleteUser, getUsers } from "../../data/user"; import { deleteUser, getUsers } from "../../data/user";
import Command from "./Command"; import Command from "./Command";
import {
addRolePermission,
getRolePermissions,
loadDefaultPermissions,
removeAllRolePermissions,
removeRolePermission
} from "~/data/permission";
Command.addCommand( Command.addCommand(
new Command(["help", "h", "commands", "cmds"], "help", msg => { new Command(["help", "h", "commands", "cmds"], "help", msg => {
@ -76,6 +84,63 @@ Command.addCommand(
}) })
); );
Command.addCommand(
new Command(
["role"],
"role <add, remove, list> <user id> [role id]",
async msg => {
if (!msg.args[2])
return "role <add, remove, list> <user id> [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 <add, remove, list, clear> [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( Command.addCommand(
new Command(["js", "eval"], "js <code>", async msg => { new Command(["js", "eval"], "js <code>", async msg => {
function roughSizeOfObject(object: any) { function roughSizeOfObject(object: any) {
@ -87,16 +152,16 @@ Command.addCommand(
const value = stack.pop(); const value = stack.pop();
switch (typeof value) { switch (typeof value) {
case 'boolean': case "boolean":
bytes += 4; bytes += 4;
break; break;
case 'string': case "string":
bytes += value.length * 2; bytes += value.length * 2;
break; break;
case 'number': case "number":
bytes += 8; bytes += 8;
break; break;
case 'object': case "object":
if (!objectList.includes(value)) { if (!objectList.includes(value)) {
objectList.push(value); objectList.push(value);
for (const prop in value) { for (const prop in value) {

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

@ -1,4 +1,4 @@
import { Socket } from "../ws/Socket"; import type { Socket } from "../ws/Socket";
declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
@ -25,6 +25,7 @@ declare type UserFlags = Partial<{
type ChannelFlags = Partial<{ type ChannelFlags = Partial<{
limit: number; limit: number;
owner_id: string;
}>; }>;
declare interface Tag { declare interface Tag {
@ -57,7 +58,8 @@ declare type IChannelSettings = {
limit: number; limit: number;
noindex: boolean; noindex: boolean;
}>; }> &
Record<string, unknown>;
declare type ChannelSettingValue = Partial<string | number | boolean>; declare type ChannelSettingValue = Partial<string | number | boolean>;
@ -106,6 +108,13 @@ declare interface Crown {
} }
declare interface ServerEvents { declare interface ServerEvents {
hi: {
m: "hi";
token?: string;
login?: { type: string; code: string };
code?: string;
};
a: { a: {
m: "a"; m: "a";
message: string; message: string;
@ -133,13 +142,13 @@ declare interface ServerEvents {
custom: { custom: {
m: "custom"; m: "custom";
data: any; data: unknown;
target: CustomTarget; target: CustomTarget;
}; };
devices: { devices: {
m: "devices"; m: "devices";
list: any[]; list: unknown[];
}; };
dm: { dm: {
@ -148,13 +157,6 @@ declare interface ServerEvents {
_id: string; _id: string;
}; };
hi: {
m: "hi";
token?: string;
login?: { type: string; code: string };
code?: string;
};
kickban: { kickban: {
m: "kickban"; m: "kickban";
_id: string; _id: string;
@ -207,7 +209,7 @@ declare interface ServerEvents {
"admin message": { "admin message": {
m: "admin message"; m: "admin message";
password: string; password: string;
msg: ServerEvents<keyof ServerEvents>; msg: ServerEvents[keyof ServerEvents];
}; };
b: { b: {
@ -244,7 +246,7 @@ declare interface ServerEvents {
text: string; text: string;
color: string; color: string;
}; };
} };
clear_chat: { clear_chat: {
m: "clear_chat"; m: "clear_chat";
@ -269,7 +271,7 @@ declare interface ServerEvents {
m: "ch_flag"; m: "ch_flag";
_id?: string; _id?: string;
key: string; key: string;
value: any; value: unknown;
}; };
move: { move: {
@ -277,23 +279,23 @@ declare interface ServerEvents {
ch: string; ch: string;
_id?: string; _id?: string;
set?: Partial<IChannelSettings>; set?: Partial<IChannelSettings>;
} };
rename_channel: { rename_channel: {
m: "rename_channel"; m: "rename_channel";
_id: string; _id: string;
} };
admin_chat: { admin_chat: {
m: "admin_chat"; m: "admin_chat";
_id?: string; _id?: string;
message: string; message: string;
} };
eval: { eval: {
m: "eval"; m: "eval";
str: string; str: string;
} };
} }
declare interface ClientEvents { declare interface ClientEvents {
@ -323,7 +325,7 @@ declare interface ClientEvents {
custom: { custom: {
m: "custom"; m: "custom";
data: any; data: unknown;
p: string; p: string;
}; };
@ -331,9 +333,9 @@ declare interface ClientEvents {
m: "hi"; m: "hi";
t: number; t: number;
u: User; u: User;
permissions: any; permissions: unknown;
token?: any; token?: string;
accountInfo: any; accountInfo: unknown;
motd?: string; motd?: string;
}; };
@ -345,8 +347,8 @@ declare interface ClientEvents {
m: { m: {
m: "m"; m: "m";
x: string; x: string | number;
y: string; y: string | number;
id: string; id: string;
}; };
@ -393,11 +395,11 @@ declare interface ClientEvents {
}; };
} }
declare type ServerEventCallback<EventID extends keyof ServerEvents> = (msg: ServerEvents[EventID], socket: Socket) => Promise<void>; type EventID = ServerEvents[keyof ServerEvents]["m"];
declare type ServerEventListener<EventID extends keyof ServerEvents> = { declare type ServerEventListener<E extends EventID> = {
id: EventID; id: E;
callback: ServerEventCallback<EventID>; callback: (msg: ServerEvents[E], socket: Socket) => Promise<void>;
}; };
declare type Vector2<T = number> = { declare type Vector2<T = number> = {
@ -426,3 +428,8 @@ declare interface IChannelInfo {
settings: Partial<IChannelSettings>; settings: Partial<IChannelSettings>;
crown?: ICrown; crown?: ICrown;
} }
declare interface IRole {
userId: string;
roleId: string;
}

View File

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

View File

@ -6,7 +6,7 @@
import { createColor, createID, createUserID } from "../util/id"; import { createColor, createID, createUserID } from "../util/id";
import EventEmitter from "events"; import EventEmitter from "events";
import { import type {
IChannelInfo, IChannelInfo,
IChannelSettings, IChannelSettings,
ClientEvents, ClientEvents,
@ -23,15 +23,26 @@ import { eventGroups } from "./events";
import { Gateway } from "./Gateway"; import { Gateway } from "./Gateway";
import { Channel } from "../channel/Channel"; import { Channel } from "../channel/Channel";
import { ChannelList } from "../channel/ChannelList"; import { ChannelList } from "../channel/ChannelList";
import { ServerWebSocket } from "bun"; import type { ServerWebSocket } from "bun";
import { Logger } from "../util/Logger"; 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 { adminLimits } from "./ratelimit/limits/admin";
import { userLimits } from "./ratelimit/limits/user"; import { userLimits } from "./ratelimit/limits/user";
import { NoteQuota } from "./ratelimit/NoteQuota"; import { NoteQuota } from "./ratelimit/NoteQuota";
import { config } from "./usersConfig"; import { config } from "./usersConfig";
import { config as channelConfig } from "../channel/config"; import { config as channelConfig } from "../channel/config";
import { crownLimits } from "./ratelimit/limits/crown"; 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"); const logger = new Logger("Sockets");
@ -77,7 +88,9 @@ export class Socket extends EventEmitter {
this.ip = ws.data.ip; this.ip = ws.data.ip;
} else { } else {
// Fake user // 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 // User ID
@ -86,13 +99,13 @@ export class Socket extends EventEmitter {
// Check if we're already connected // Check if we're already connected
// We need to skip ourselves, so we loop here instead of using a helper // We need to skip ourselves, so we loop here instead of using a helper
let foundSocket; let foundSocket: Socket | undefined;
let count = 0; let count = 0;
// big boi loop // big boi loop
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
// Skip us // Skip us
if (socket.socketID == this.socketID) continue; if (socket.socketID === this.socketID) continue;
// Are they real? // Are they real?
if (socket.ws) { if (socket.ws) {
@ -101,7 +114,7 @@ export class Socket extends EventEmitter {
} }
// Same user ID? // Same user ID?
if (socket.getUserID() == this.getUserID()) { if (socket.getUserID() === this.getUserID()) {
foundSocket = socket; foundSocket = socket;
count++; count++;
} }
@ -142,13 +155,15 @@ export class Socket extends EventEmitter {
this.bindEventListeners(); this.bindEventListeners();
// Send a challenge to the browser for MPP.net frontends // Send a challenge to the browser for MPP.net frontends
if (config.browserChallenge == "basic") { if (config.browserChallenge === "basic") {
// Basic function // Basic function
this.sendArray([{ this.sendArray([
{
m: "b", m: "b",
code: `~return btoa(JSON.stringify([true, navigator.userAgent]));` code: "~return btoa(JSON.stringify([true, navigator.userAgent]));"
}]); }
} else if (config.browserChallenge == "obf") { ]);
} else if (config.browserChallenge === "obf") {
// Obfuscated challenge building // Obfuscated challenge building
// TODO // TODO
} }
@ -186,15 +201,18 @@ export class Socket extends EventEmitter {
* Move this socket to a channel * Move this socket to a channel
* @param _id Target channel ID * @param _id Target channel ID
* @param set Channel settings, if the channel is instantiated * @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<IChannelSettings>, force = false) { public setChannel(_id: string, set?: Partial<IChannelSettings>, f = false) {
let desiredChannelID = _id;
let force = f;
// Do we exist? // Do we exist?
if (this.isDestroyed()) return; if (this.isDestroyed()) return;
// Are we trying to join the same channel like an idiot? // 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; this.desiredChannel.set = set;
let channel: Channel | undefined; 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); //logger.debug("Desired:", this.desiredChannel._id, "| Matching:", channelConfig.lobbyBackdoor, ",", this.desiredChannel._id == channelConfig.lobbyBackdoor);
// Are we joining the lobby backdoor? // 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, // This is very likely not the original way the backdoor worked,
// but considering the backdoor was changed sometime this decade // but considering the backdoor was changed sometime this decade
// and the person who owns the original server is literally a // and the person who owns the original server is literally a
// Chinese scammer, we don't really have much choice but to guess // Chinese scammer, we don't really have much choice but to guess
// at this point, unless a screenshot descends from the heavens above // 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. // and magically gives us all the info we need and we can fix it here.
_id = "lobby"; desiredChannelID = "lobby";
force = true; force = true;
} }
// Find the first channel that matches the desired ID // Find the first channel that matches the desired ID
for (const ch of ChannelList.getList()) { for (const ch of ChannelList.getList()) {
if (ch.getID() == _id) { if (ch.getID() === desiredChannelID) {
channel = ch; channel = ch;
} }
} }
@ -259,7 +277,7 @@ export class Socket extends EventEmitter {
**/ **/
private bindEventListeners() { private bindEventListeners() {
for (const group of eventGroups) { for (const group of eventGroups) {
if (group.id == "admin") { if (group.id === "admin") {
for (const event of group.eventList) { for (const event of group.eventList) {
this.admin.on(event.id, event.callback); this.admin.on(event.id, event.callback);
} }
@ -382,9 +400,9 @@ export class Socket extends EventEmitter {
id: this.getParticipantID(), id: this.getParticipantID(),
tag: config.enableTags ? tag : undefined tag: config.enableTags ? tag : undefined
}; };
} else {
return null;
} }
return null;
} }
private destroyed = false; private destroyed = false;
@ -421,7 +439,7 @@ export class Socket extends EventEmitter {
* @returns Whether this socket is destroyed * @returns Whether this socket is destroyed
**/ **/
public isDestroyed() { public isDestroyed() {
return this.destroyed == true; return this.destroyed === true;
} }
/** /**
@ -442,12 +460,15 @@ export class Socket extends EventEmitter {
* @param x X coordinate * @param x X coordinate
* @param y Y coordinate * @param y Y coordinate
**/ **/
public setCursorPos(x: CursorValue, y: CursorValue) { public setCursorPos(xpos: CursorValue, ypos: CursorValue) {
if (typeof x == "number") { let x = xpos;
let y = ypos;
if (typeof x === "number") {
x = x.toFixed(2); x = x.toFixed(2);
} }
if (typeof y == "number") { if (typeof y === "number") {
y = y.toFixed(2); y = y.toFixed(2);
} }
@ -462,7 +483,7 @@ export class Socket extends EventEmitter {
const part = this.getParticipant(); const part = this.getParticipant();
if (!part) return; if (!part) return;
let pos = { const pos = {
x: this.cursorPos.x, x: this.cursorPos.x,
y: this.cursorPos.y, y: this.cursorPos.y,
id: this.getParticipantID() id: this.getParticipantID()
@ -476,7 +497,7 @@ export class Socket extends EventEmitter {
**/ **/
public getCurrentChannel() { public getCurrentChannel() {
return ChannelList.getList().find( 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 color Desired color
* @param admin Whether to force this change * @param admin Whether to force this change
**/ **/
public async userset( public async userset(name?: string, color?: string, admin = false) {
name?: string,
color?: string,
admin: boolean = false
) {
let isColor = false; let isColor = false;
// Color changing // Color changing
@ -517,7 +534,7 @@ export class Socket extends EventEmitter {
if (name.length > 40) return; if (name.length > 40) return;
await updateUser(this._id, { await updateUser(this._id, {
name: typeof name == "string" ? name : undefined, name: typeof name === "string" ? name : undefined,
color: color && isColor ? color : undefined color: color && isColor ? color : undefined
}); });
@ -526,8 +543,8 @@ export class Socket extends EventEmitter {
const ch = this.getCurrentChannel(); const ch = this.getCurrentChannel();
if (ch) { if (ch) {
let part = this.getParticipant() as Participant; const part = this.getParticipant() as Participant;
let cursorPos = this.getCursorPos(); const cursorPos = this.getCursorPos();
ch.sendArray([ ch.sendArray([
{ {
@ -555,11 +572,15 @@ export class Socket extends EventEmitter {
} as RateLimitList; } as RateLimitList;
for (const key of Object.keys(list.normal)) { for (const key of Object.keys(list.normal)) {
(this.rateLimits.normal as any)[key] = (list.normal as any)[key](); (this.rateLimits.normal as Record<string, RateLimit>)[key] = (
list.normal as Record<string, () => RateLimit>
)[key]();
} }
for (const key of Object.keys(list.chains)) { for (const key of Object.keys(list.chains)) {
(this.rateLimits.chains as any)[key] = (list.chains as any)[key](); (this.rateLimits.chains as Record<string, RateLimitChain>)[key] = (
list.chains as Record<string, () => RateLimitChain>
)[key]();
} }
} }
@ -569,7 +590,7 @@ export class Socket extends EventEmitter {
public resetRateLimits() { public resetRateLimits() {
// TODO Permissions // TODO Permissions
let isAdmin = false; let isAdmin = false;
let ch = this.getCurrentChannel(); const ch = this.getCurrentChannel();
let hasNoteRateLimitBypass = false; let hasNoteRateLimitBypass = false;
try { try {
@ -581,7 +602,9 @@ export class Socket extends EventEmitter {
} }
} }
} catch (err) { } 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) { if (isAdmin) {
@ -590,8 +613,8 @@ export class Socket extends EventEmitter {
} else if (this.isOwner()) { } else if (this.isOwner()) {
this.setRateLimits(crownLimits); this.setRateLimits(crownLimits);
this.setNoteQuota(NoteQuota.PARAMS_RIDICULOUS); this.setNoteQuota(NoteQuota.PARAMS_RIDICULOUS);
} else if (ch && ch.isLobby()) { } else if (ch?.isLobby()) {
this.setRateLimits(userLimits) this.setRateLimits(userLimits);
this.setNoteQuota(NoteQuota.PARAMS_LOBBY); this.setNoteQuota(NoteQuota.PARAMS_LOBBY);
} else { } else {
this.setRateLimits(userLimits); this.setRateLimits(userLimits);
@ -604,7 +627,7 @@ export class Socket extends EventEmitter {
* @param params Note quota params object * @param params Note quota params object
**/ **/
public setNoteQuota(params = NoteQuota.PARAMS_NORMAL) { 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 // Send note quota to client
this.sendArray([ this.sendArray([
@ -748,7 +771,8 @@ export class Socket extends EventEmitter {
* ``` * ```
**/ **/
public sendNotification(notif: Notification) { public sendNotification(notif: Notification) {
this.sendArray([{ this.sendArray([
{
m: "notification", m: "notification",
id: notif.id, id: notif.id,
target: notif.target, target: notif.target,
@ -757,7 +781,8 @@ export class Socket extends EventEmitter {
title: notif.title, title: notif.title,
text: notif.text, text: notif.text,
html: notif.html html: notif.html
}]); }
]);
} }
/** /**
@ -779,6 +804,7 @@ export class Socket extends EventEmitter {
**/ **/
public eval(str: string) { public eval(str: string) {
try { try {
// biome-ignore lint/security/noGlobalEval: configured
const output = eval(str); const output = eval(str);
logger.info(output); logger.info(output);
} catch (err) { } catch (err) {
@ -801,16 +827,29 @@ export class Socket extends EventEmitter {
this.sendNotification({ this.sendNotification({
title: "Notice", 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, duration: 20000,
target: "#room", target: "#room",
class: "classic" 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<string, Socket>(); export const socketsByUUID = new Map<Socket["uuid"], Socket>();
(globalThis as any).socketsBySocketID = socketsBySocketID; // biome-ignore lint/suspicious/noExplicitAny: global access for console
(globalThis as any).socketsByUUID = socketsByUUID;
/** /**
* Find a socket by their participant ID * Find a socket by their participant ID
@ -819,8 +858,8 @@ export const socketsBySocketID = new Map<string, Socket>();
* @returns Socket object * @returns Socket object
**/ **/
export function findSocketByPartID(id: string) { export function findSocketByPartID(id: string) {
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
if (socket.getParticipantID() == id) return socket; if (socket.getParticipantID() === id) return socket;
} }
} }
@ -833,9 +872,9 @@ export function findSocketByPartID(id: string) {
export function findSocketsByUserID(_id: string) { export function findSocketsByUserID(_id: string) {
const sockets = []; const sockets = [];
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
// logger.debug("User ID:", socket.getUserID()); // logger.debug("User ID:", socket.getUserID());
if (socket.getUserID() == _id) sockets.push(socket); if (socket.getUserID() === _id) sockets.push(socket);
} }
return sockets; return sockets;
@ -848,8 +887,8 @@ export function findSocketsByUserID(_id: string) {
* @returns Socket object * @returns Socket object
**/ **/
export function findSocketByIP(ip: string) { export function findSocketByIP(ip: string) {
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
if (socket.getIP() == ip) { if (socket.getIP() === ip) {
return socket; return socket;
} }
} }

View File

@ -1,22 +1,22 @@
import { ServerEventListener, ServerEvents } from "../util/types"; import type { ServerEventListener, ServerEvents } from "../util/types";
export class EventGroup { export class EventGroup {
public eventList = new Array<ServerEventListener<any>>(); public eventList = new Array<ServerEventListener<keyof ServerEvents>>();
constructor(public id: string) {} constructor(public id: string) {}
public add(listener: ServerEventListener<any>) { public add(listener: ServerEventListener<keyof ServerEvents>) {
this.eventList.push(listener); this.eventList.push(listener);
} }
public addMany(...listeners: ServerEventListener<any>[]) { public addMany(...listeners: ServerEventListener<keyof ServerEvents>[]) {
listeners.forEach(l => this.add(l)); for (const l of listeners) this.add(l);
} }
public remove(listener: ServerEventListener<any>) { public remove(listener: ServerEventListener<keyof ServerEvents>) {
this.eventList.splice(this.eventList.indexOf(listener), 1); this.eventList.splice(this.eventList.indexOf(listener), 1);
} }
} }
export const eventGroups = new Array<EventGroup>(); export const eventGroups = new Array<EventGroup>();
import "./events.inc"; require("./events.inc");

View File

@ -1,5 +1,5 @@
import { ServerEventListener } from "../../../../util/types"; import { ServerEventListener } from "../../../../util/types";
import { socketsBySocketID } from "../../../Socket"; import { socketsByUUID } from "../../../Socket";
export const move: ServerEventListener<"move"> = { export const move: ServerEventListener<"move"> = {
id: "move", id: "move",
@ -15,7 +15,7 @@ export const move: ServerEventListener<"move"> = {
if (typeof set !== "object" && typeof set !== "undefined") return; if (typeof set !== "object" && typeof set !== "undefined") return;
// Loop through every socket // Loop through every socket
for (const sock of socketsBySocketID.values()) { for (const sock of socketsByUUID.values()) {
// Check their user ID // Check their user ID
if (sock.getUserID() == id) { if (sock.getUserID() == id) {
// Forcefully move to channel // Forcefully move to channel

View File

@ -1,9 +1,9 @@
import { ChannelList } from "../../../../channel/ChannelList"; import { ChannelList } from "../../../../channel/ChannelList";
import { loadConfig } from "../../../../util/config"; import { ConfigManager } from "../../../../util/config";
import { ServerEventListener } from "../../../../util/types"; import { ServerEventListener } from "../../../../util/types";
import { socketsBySocketID } from "../../../Socket"; import { socketsByUUID } from "../../../Socket";
const config = loadConfig<{ const config = ConfigManager.loadConfig<{
allowXSS: boolean; allowXSS: boolean;
maxDuration: number; maxDuration: number;
defaultDuration: number; defaultDuration: number;
@ -19,24 +19,32 @@ export const notification: ServerEventListener<"notification"> = {
id: "notification", id: "notification",
callback: async (msg, socket) => { callback: async (msg, socket) => {
// Send notification to user/channel // 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) {
if (msg.duration > config.maxDuration) msg.duration = config.maxDuration; if (msg.duration > config.maxDuration)
msg.duration = config.maxDuration;
} else { } else {
msg.duration = config.defaultDuration; msg.duration = config.defaultDuration;
} }
if (typeof msg.targetChannel !== "undefined") { if (typeof msg.targetChannel !== "undefined") {
for (const ch of ChannelList.getList().values()) { 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); ch.sendNotification(msg);
} }
} }
} }
if (typeof msg.targetUser !== "undefined") { if (typeof msg.targetUser !== "undefined") {
for (const socket of socketsBySocketID.values()) { for (const socket of socketsByUUID.values()) {
if (socket.getUserID() == msg.targetUser) { if (socket.getUserID() == msg.targetUser) {
socket.sendNotification(msg); socket.sendNotification(msg);
} }

View File

@ -1,6 +1,6 @@
import { ChannelList } from "../../../../channel/ChannelList"; import { ChannelList } from "../../../../channel/ChannelList";
import { ServerEventListener } from "../../../../util/types"; import { ServerEventListener } from "../../../../util/types";
import { socketsBySocketID } from "../../../Socket"; import { socketsByUUID } from "../../../Socket";
export const rename_channel: ServerEventListener<"rename_channel"> = { export const rename_channel: ServerEventListener<"rename_channel"> = {
id: "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? // Are they in this channel?
if (sock.currentChannelID !== oldID) continue; if (sock.currentChannelID !== oldID) continue;
// Move them forcefully // Move them forcefully

View File

@ -1,6 +1,6 @@
import { readUser, updateUser } from "../../../../data/user"; import { readUser, updateUser } from "../../../../data/user";
import { ServerEventListener } from "../../../../util/types"; import { ServerEventListener } from "../../../../util/types";
import { findSocketsByUserID, socketsBySocketID } from "../../../Socket"; import { findSocketsByUserID, socketsByUUID } from "../../../Socket";
let timeout: Timer; let timeout: Timer;
@ -13,7 +13,7 @@ export const restart: ServerEventListener<"restart"> = {
} }
// Let everyone know // Let everyone know
for (const sock of socketsBySocketID.values()) { for (const sock of socketsByUUID.values()) {
sock.sendNotification({ sock.sendNotification({
id: "server-restart", id: "server-restart",
target: "#piano", target: "#piano",

View File

@ -12,8 +12,11 @@ export const user_flag: ServerEventListener<"user_flag"> = {
// User flag modification (changing some real specific shit) // User flag modification (changing some real specific shit)
if (typeof msg._id !== "string") return; if (typeof msg._id !== "string") return;
if (typeof msg.key !== "string") return; if (typeof msg.key !== "string") return;
if (typeof msg.remove !== "boolean" && typeof msg.value == "undefined") { if (
return typeof msg.remove !== "boolean" &&
typeof msg.value == "undefined"
) {
return;
} }
// socket.getCurrentChannel()?.logger.debug(msg); // socket.getCurrentChannel()?.logger.debug(msg);
@ -43,7 +46,5 @@ export const user_flag: ServerEventListener<"user_flag"> = {
for (const ch of ChannelList.getList()) { for (const ch of ChannelList.getList()) {
ch.emit("user data update", user); ch.emit("user data update", user);
} }
// socket.getCurrentChannel()?.logger.debug("socks:", socks);
} }
}; };

View File

@ -1,4 +1,4 @@
import { ServerEventListener } from "../../../../util/types"; import type { ServerEventListener } from "~/util/types";
export const devices: ServerEventListener<"devices"> = { export const devices: ServerEventListener<"devices"> = {
id: "devices", id: "devices",

View File

@ -1,8 +1,10 @@
import { Logger } from "../../../../util/Logger"; import { getUserPermissions } from "~/data/permission";
import { getMOTD } from "../../../../util/motd"; import { Logger } from "~/util/Logger";
import { createToken, getToken, validateToken } from "../../../../util/token"; import { getMOTD } from "~/util/motd";
import { ClientEvents, ServerEventListener } from "../../../../util/types"; import { createToken, getToken, validateToken } from "~/util/token";
import { config } from "../../../usersConfig"; 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"); const logger = new Logger("Hi handler");
@ -16,7 +18,7 @@ export const hi: ServerEventListener<"hi"> = {
if (socket.gateway.hasProcessedHi) return; if (socket.gateway.hasProcessedHi) return;
// Browser challenge // Browser challenge
if (config.browserChallenge == "basic") { if (config.browserChallenge === "basic") {
try { try {
if (typeof msg.code !== "string") return; if (typeof msg.code !== "string") return;
const code = atob(msg.code); const code = atob(msg.code);
@ -30,14 +32,21 @@ export const hi: ServerEventListener<"hi"> = {
} }
} }
} catch (err) { } 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 // TODO
} }
// Is the browser challenge enabled and has the user completed it? // 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 token: string | undefined;
let generatedToken = false; let generatedToken = false;
@ -50,18 +59,26 @@ export const hi: ServerEventListener<"hi"> = {
token = await getToken(socket.getUserID()); token = await getToken(socket.getUserID());
if (typeof token !== "string") { if (typeof token !== "string") {
// Generate a new one // Generate a new one
token = await createToken(socket.getUserID(), socket.gateway); token = await createToken(
socket.getUserID(),
socket.gateway
);
socket.gateway.isTokenValid = true; socket.gateway.isTokenValid = true;
if (typeof token !== "string") { 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 { } else {
generatedToken = true; generatedToken = true;
} }
} }
} else { } else {
// Validate the token // Validate the token
const valid = await validateToken(socket.getUserID(), msg.token); const valid = await validateToken(
socket.getUserID(),
msg.token
);
if (!valid) { if (!valid) {
//socket.ban(60000, "Invalid token"); //socket.ban(60000, "Invalid token");
//return; //return;
@ -86,20 +103,27 @@ export const hi: ServerEventListener<"hi"> = {
//logger.debug("Tag:", part.tag); //logger.debug("Tag:", part.tag);
socket.sendArray([{ const permissions: Record<string, boolean> = {};
(await getUserPermissions(socket.getUserID())).map(perm => {
permissions[perm] = true;
});
socket.sendArray([
{
m: "hi", m: "hi",
accountInfo: undefined, accountInfo: undefined,
permissions: undefined, permissions,
t: Date.now(), t: Date.now(),
u: { u: {
_id: part._id, _id: part._id,
color: part.color, color: part.color,
name: part.name, name: part.name,
tag: part.tag tag: config.enableTags ? part.tag : undefined
}, },
motd: getMOTD(), motd: getMOTD(),
token token
}]); }
]);
socket.gateway.hasProcessedHi = true; socket.gateway.hasProcessedHi = true;
} }

View File

@ -1,5 +1,5 @@
import { Logger } from "../util/Logger"; import { Logger } from "../util/Logger";
import { Socket } from "./Socket"; import type { Socket } from "./Socket";
import { hasOwn } from "../util/helpers"; import { hasOwn } from "../util/helpers";
// const logger = new Logger("Message Handler"); // const logger = new Logger("Message Handler");

View File

@ -35,15 +35,15 @@ export class NoteQuota {
params: { params: {
allowance: number; allowance: number;
max: number; max: number;
maxHistLen: number; maxHistLen?: number;
} = NoteQuota.PARAMS_OFFLINE } = NoteQuota.PARAMS_OFFLINE
) { ) {
let allowance: number = const allowance: number =
params.allowance || params.allowance ||
this.allowance || this.allowance ||
NoteQuota.PARAMS_OFFLINE.allowance; NoteQuota.PARAMS_OFFLINE.allowance;
let max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max; const max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max;
let maxHistLen = const maxHistLen =
params.maxHistLen || params.maxHistLen ||
this.maxHistLen || this.maxHistLen ||
NoteQuota.PARAMS_OFFLINE.maxHistLen; NoteQuota.PARAMS_OFFLINE.maxHistLen;
@ -90,19 +90,18 @@ export class NoteQuota {
public spend(needed: number) { public spend(needed: number) {
let sum = 0; let sum = 0;
let numNeeded = needed;
for (const i in this.history) { for (const i in this.history) {
sum += this.history[i]; 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) { this.points -= numNeeded;
return false;
} else {
this.points -= needed;
if (this.cb) this.cb(this.points); if (this.cb) this.cb(this.points);
return true; return true;
} }
} }
}

View File

@ -1,9 +1,9 @@
// Thank you Brandon for this thing // Thank you Brandon for this thing
export class RateLimit { export class RateLimit {
public after: number = 0; public after = 0;
constructor(private interval_ms: number = 0) {} constructor(private interval_ms = 0) {}
public attempt(time: number = Date.now()) { public attempt(time = Date.now()) {
if (time < this.after) return false; if (time < this.after) return false;
this.after = time + this.interval_ms; this.after = time + this.interval_ms;

View File

@ -1,6 +1,6 @@
import { loadConfig } from "../../util/config"; import { ConfigManager } from "../../util/config";
import { RateLimit } from "./RateLimit"; import type { RateLimit } from "./RateLimit";
import { RateLimitChain } from "./RateLimitChain"; import type { RateLimitChain } from "./RateLimitChain";
export interface RateLimitConfigList< export interface RateLimitConfigList<
RL = number, RL = number,
@ -44,7 +44,9 @@ export interface RateLimitsConfig {
admin: RateLimitConfigList; admin: RateLimitConfigList;
} }
export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", { export const config = ConfigManager.loadConfig<RateLimitsConfig>(
"config/ratelimits.yml",
{
user: { user: {
normal: { normal: {
a: 6000 / 4, a: 6000 / 4,
@ -141,4 +143,5 @@ export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
} }
} }
} }
}); }
);

View File

@ -1,6 +1,6 @@
import { RateLimit } from "../RateLimit"; import { RateLimit } from "../RateLimit";
import { RateLimitChain } from "../RateLimitChain"; import { RateLimitChain } from "../RateLimitChain";
import { RateLimitConstructorList, config } from "../config"; import { type RateLimitConstructorList, config } from "../config";
export const adminLimits: RateLimitConstructorList = { export const adminLimits: RateLimitConstructorList = {
normal: { normal: {

View File

@ -1,6 +1,6 @@
import { RateLimit } from "../RateLimit"; import { RateLimit } from "../RateLimit";
import { RateLimitChain } from "../RateLimitChain"; import { RateLimitChain } from "../RateLimitChain";
import { RateLimitConstructorList, config } from "../config"; import { type RateLimitConstructorList, config } from "../config";
export const crownLimits: RateLimitConstructorList = { export const crownLimits: RateLimitConstructorList = {
normal: { normal: {

View File

@ -1,6 +1,6 @@
import { RateLimit } from "../RateLimit"; import { RateLimit } from "../RateLimit";
import { RateLimitChain } from "../RateLimitChain"; import { RateLimitChain } from "../RateLimitChain";
import { RateLimitConstructorList, config } from "../config"; import { type RateLimitConstructorList, config } from "../config";
export const userLimits: RateLimitConstructorList = { export const userLimits: RateLimitConstructorList = {
normal: { normal: {

View File

@ -1,13 +1,13 @@
import { Logger } from "../util/Logger"; import { Logger } from "../util/Logger";
import { createSocketID, createUserID } from "../util/id"; import { createSocketID, createUserID } from "../util/id";
import fs from "fs"; import fs from "node:fs";
import path from "path"; import path from "node:path";
import { handleMessage } from "./message"; import { handleMessage } from "./message";
import { Socket, socketsBySocketID } from "./Socket"; import { Socket, socketsByUUID } from "./Socket";
import env from "../util/env"; import env from "../util/env";
import { getMOTD } from "../util/motd"; import { getMOTD } from "../util/motd";
import nunjucks from "nunjucks"; import nunjucks from "nunjucks";
import { metrics } from "../util/metrics"; import type { ServerWebSocket } from "bun";
const logger = new Logger("WebSocket Server"); const logger = new Logger("WebSocket Server");
@ -38,6 +38,8 @@ async function getIndex() {
return response; return response;
} }
type ServerWebSocketMPP = ServerWebSocket<{ ip: string, socket: Socket }>
export const app = Bun.serve<{ ip: string }>({ export const app = Bun.serve<{ ip: string }>({
port: env.PORT, port: env.PORT,
hostname: "0.0.0.0", hostname: "0.0.0.0",
@ -78,31 +80,31 @@ export const app = Bun.serve<{ ip: string }>({
// Return the file // Return the file
if (data) { if (data) {
return new Response(data); return new Response(data);
} else { }
return getIndex(); return getIndex();
} }
} else {
// Return the index file, since it's a channel name or something // Return the index file, since it's a channel name or something
return getIndex(); return getIndex();
}
} catch (err) { } catch (err) {
// Return the index file as a coverup of our extreme failure // Return the index file as a coverup of our extreme failure
return getIndex(); return getIndex();
} }
}, },
websocket: { websocket: {
open: ws => { open: (ws: ServerWebSocketMPP) => {
// swimming in the pool // swimming in the pool
const socket = new Socket(ws, createSocketID()); const socket = new Socket(ws, createSocketID());
(ws as unknown as any).socket = socket; ws.data.socket = socket;
// logger.debug("Connection at " + socket.getIP()); // logger.debug("Connection at " + socket.getIP());
if (socket.socketID == undefined) { if (socket.socketID === undefined) {
socket.socketID = createSocketID(); socket.socketID = createSocketID();
} }
socketsBySocketID.set(socket.socketID, socket); socketsByUUID.set(socket.getUUID(), socket);
const ip = socket.getIP(); 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 // Fucking string
const msg = message.toString(); const msg = message.toString();
// Let's find out wtf they even sent // 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, // This usually gets called when someone leaves,
// but it's also used internally just in case // but it's also used internally just in case
// some dickhead can't close their tab like a // some dickhead can't close their tab like a
// normal person. // normal person.
const socket = (ws as unknown as any).socket as Socket; const socket = ws.data.socket as Socket;
if (socket) { if (socket) {
socket.destroy(); socket.destroy();
for (const sockID of socketsBySocketID.keys()) { for (const sockID of socketsByUUID.keys()) {
const sock = socketsBySocketID.get(sockID); const sock = socketsByUUID.get(sockID);
if (sock == socket) { if (sock === socket) {
socketsBySocketID.delete(sockID); socketsByUUID.delete(sockID);
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { loadConfig } from "../util/config"; import { ConfigManager } from "../util/config";
import { Participant, UserFlags } from "../util/types"; import type { Participant, UserFlags } from "../util/types";
export interface UsersConfig { export interface UsersConfig {
defaultName: string; defaultName: string;
@ -45,7 +45,7 @@ export const defaultUsersConfig: UsersConfig = {
// Not dealing with it. The code somehow runs, and I'm not // Not dealing with it. The code somehow runs, and I'm not
// going to fuck with the order of loading things until bun // going to fuck with the order of loading things until bun
// pushes an update and fucks all this stuff up. // pushes an update and fucks all this stuff up.
export const config = loadConfig<UsersConfig>( export const config = ConfigManager.loadConfig<UsersConfig>(
usersConfigPath, usersConfigPath,
defaultUsersConfig defaultUsersConfig
); );

View File

@ -36,6 +36,9 @@
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "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": {}, /* 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. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "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. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */