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
prisma/*.sqlite
# TS build
/out
# Build script output
/dist
# JWT token keypair
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
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 { 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<UserFlags>;
flags: UserFlags;
}
type ExtraPart = Participant & ExtraPartData;
@ -48,23 +63,25 @@ export class Channel extends EventEmitter {
try {
this.chatHistory = await getChatHistory(this.getID());
this.sendArray([{
this.sendArray([
{
m: "c",
c: this.chatHistory
}]);
} catch (err) { }
}
]);
} 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<CachedKickban>();
public cursorCache = new Array<{ x: string | number; y: string | number; id: string }>();
public cursorCache = new Array<CachedCursor>();
public crown?: Crown;
private flags: Record<string, any> = {};
private flags: TChannelFlags = {};
constructor(
private _id: string,
set?: Partial<IChannelSettings>,
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,8 +373,7 @@ export class Channel extends EventEmitter {
y = found.y;
}
this.sendArray(
[
this.sendArray([
{
m: "p",
_id: p._id,
@ -345,8 +384,7 @@ export class Channel extends EventEmitter {
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<IChannelSettings>,
admin: boolean = false
) {
public changeSettings(set: Partial<IChannelSettings>, 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<string>();
const sentSocketIDs = new Array<string>();
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<string>();
const sentSocketIDs = new Array<string>();
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({
await this.sendChat(
{
m: "a",
message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.`
}, p);
},
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,10 +1197,12 @@ export class Channel extends EventEmitter {
this.chatHistory = [];
await saveChatHistory(this.getID(), this.chatHistory);
this.sendArray([{
this.sendArray([
{
m: "c",
c: this.chatHistory
}]);
}
]);
}
/**
@ -1152,7 +1210,8 @@ export class Channel extends EventEmitter {
* @param notif Notification to send
**/
public sendNotification(notif: Notification) {
this.sendArray([{
this.sendArray([
{
m: "notification",
id: notif.id,
target: notif.target,
@ -1161,7 +1220,8 @@ export class Channel extends EventEmitter {
title: notif.title,
text: notif.text,
html: notif.html
}]);
}
]);
}
/**
@ -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({
this.sendChat(
{
m: "a",
message
}, usersConfig.adminParticipant);
},
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<K extends keyof TChannelFlags>(
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<K extends keyof TChannelFlags>(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<string, any>) {
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`
);
}
}

View File

@ -1,4 +1,4 @@
import { loadConfig } from "../util/config";
import { ConfigManager } from "../util/config";
import { IChannelSettings } from "../util/types";
interface ChannelConfig {
@ -13,7 +13,9 @@ interface ChannelConfig {
channelDestroyTimeout: number;
}
export const config = loadConfig<ChannelConfig>("config/channels.yml", {
export const config = ConfigManager.loadConfig<ChannelConfig>(
"config/channels.yml",
{
forceLoad: ["lobby", "test/awkward"],
lobbySettings: {
lobby: true,
@ -31,10 +33,17 @@ export const config = loadConfig<ChannelConfig>("config/channels.yml", {
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/.+$"],
lobbyRegexes: [
"^lobby[0-9][0-9]$",
"^lobby[0-9]$",
"^lobby$",
"^lobbyNaN$",
"^test/.+$"
],
lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward",
sendLimit: false,
chownOnRejoin: true,
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);
@ -40,11 +40,11 @@ const adminOnlyKeys = [
*/
export function validateChannelSettings(set: Partial<IChannelSettings>, admin = false) {
// Create record
let record: Partial<Record<keyof IChannelSettings, boolean>> = {};
const record: Partial<Record<keyof IChannelSettings, boolean>> = {};
for (const key of Object.keys(set)) {
let val = (set as Record<string, any>)[key];
let validator = (
const val = (set as Record<string, unknown>)[key];
const validator = (
validationRecord as Record<string, Validator | undefined>
)[key];
@ -66,12 +66,15 @@ export function validateChannelSettings(set: Partial<IChannelSettings>, 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;
}

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";
// 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();

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 { Logger } from "./Logger";
/**
* This file uses the synchronous functions from the fs
@ -11,12 +12,22 @@ import { parse, stringify } from "yaml";
* 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
*
* Usage:
* ```ts
* const config = loadConfig("config/services.yml", {
* const config = ConfigManager.loadConfig("config/services.yml", {
* 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)
* @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?
if (existsSync(configPath)) {
// Load config
@ -44,7 +57,10 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
changed = true;
}
if (typeof obj[key] == "object" && !Array.isArray(obj[key])) {
if (
typeof obj[key] == "object" &&
!Array.isArray(obj[key])
) {
mix(
obj[key] as Record<string, unknown>,
obj2[key] as Record<string, unknown>
@ -57,23 +73,47 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
mix(config, defRecord);
// 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 {
// 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;
}
}
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<T>(configPath);
return defaultConfig;
}
}
/**
* Write a YAML config to disk
* @param configPath
* @param config
*/
export function writeConfig<T>(configPath: string, config: T) {
public static writeConfig<T>(configPath: string, config: T) {
// Write config to disk unconditionally
writeFileSync(
configPath,
@ -81,4 +121,31 @@ export function writeConfig<T>(configPath: string, config: T) {
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<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 { 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 <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(
new Command(["js", "eval"], "js <code>", 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) {

65
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>>;
@ -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<string, unknown>;
declare type ChannelSettingValue = Partial<string | number | boolean>;
@ -95,7 +97,7 @@ declare type CustomTarget = {
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<keyof 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<IChannelSettings>;
}
};
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<EventID extends keyof ServerEvents> = (msg: ServerEvents[EventID], socket: Socket) => Promise<void>;
type EventID = ServerEvents[keyof ServerEvents]["m"];
declare type ServerEventListener<EventID extends keyof ServerEvents> = {
id: EventID;
callback: ServerEventCallback<EventID>;
declare type ServerEventListener<E extends EventID> = {
id: E;
callback: (msg: ServerEvents[E], socket: Socket) => Promise<void>;
};
declare type Vector2<T = number> = {
@ -426,3 +428,8 @@ declare interface IChannelInfo {
settings: Partial<IChannelSettings>;
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
});

View File

@ -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");
@ -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([{
this.sendArray([
{
m: "b",
code: `~return btoa(JSON.stringify([true, navigator.userAgent]));`
}]);
} else if (config.browserChallenge == "obf") {
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<IChannelSettings>, force = false) {
public setChannel(_id: string, set?: Partial<IChannelSettings>, 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<string, RateLimit>)[key] = (
list.normal as Record<string, () => 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<string, RateLimitChain>)[key] = (
list.chains as Record<string, () => 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,7 +771,8 @@ export class Socket extends EventEmitter {
* ```
**/
public sendNotification(notif: Notification) {
this.sendArray([{
this.sendArray([
{
m: "notification",
id: notif.id,
target: notif.target,
@ -757,7 +781,8 @@ export class Socket extends EventEmitter {
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<string, Socket>();
(globalThis as any).socketsBySocketID = socketsBySocketID;
export const socketsByUUID = new Map<Socket["uuid"], Socket>();
// 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<string, Socket>();
* @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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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([{
const permissions: Record<string, boolean> = {};
(await getUserPermissions(socket.getUserID())).map(perm => {
permissions[perm] = true;
});
socket.sendArray([
{
m: "hi",
accountInfo: undefined,
permissions: undefined,
permissions,
t: Date.now(),
u: {
_id: part._id,
color: part.color,
name: part.name,
tag: part.tag
tag: config.enableTags ? part.tag : undefined
},
motd: getMOTD(),
token
}]);
}
]);
socket.gateway.hasProcessedHi = true;
}

View File

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

View File

@ -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;
this.points -= numNeeded;
if (this.cb) this.cb(this.points);
return true;
}
}
}

View File

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

View File

@ -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,7 +44,9 @@ export interface RateLimitsConfig {
admin: RateLimitConfigList;
}
export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
export const config = ConfigManager.loadConfig<RateLimitsConfig>(
"config/ratelimits.yml",
{
user: {
normal: {
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 { RateLimitChain } from "../RateLimitChain";
import { RateLimitConstructorList, config } from "../config";
import { type RateLimitConstructorList, config } from "../config";
export const adminLimits: RateLimitConstructorList = {
normal: {

View File

@ -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: {

View File

@ -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: {

View File

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

View File

@ -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<UsersConfig>(
export const config = ConfigManager.loadConfig<UsersConfig>(
usersConfigPath,
defaultUsersConfig
);

View File

@ -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. */