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

@ -27,8 +27,19 @@ model ChatHistory {
}
model Channel {
id String @id @unique @map("_id")
settings String @default("{}") // JSON channel settings
id String @id @unique @map("_id")
settings String @default("{}") // JSON channel settings
forceload Boolean @default(false) // Whether the channel is forceloaded
flags String @default("{}") // JSON flags object
flags String @default("{}") // JSON flags object
}
model Role {
userId String @unique
roleId String
}
model RolePermission {
id Int @id @unique @default(autoincrement())
roleId String
permission String
}

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([{
m: "c",
c: this.chatHistory
}]);
} catch (err) { }
this.sendArray([
{
m: "c",
c: this.chatHistory
}
]);
} catch (err) {}
}
private async deleteChatHistory() {
try {
await deleteChatHistory(this.getID());
} catch (err) { }
} catch (err) {}
}
private async deleteData() {
try {
await deleteSavedChannel(this.getID());
} catch (err) { }
} catch (err) {}
}
private async save() {
@ -107,27 +124,27 @@ export class Channel extends EventEmitter {
this.logger.error("Error loading channel data:", err);
}
}
} catch (err) { }
} catch (err) {}
}
public logger: Logger;
public bans = new Array<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,20 +373,18 @@ export class Channel extends EventEmitter {
y = found.y;
}
this.sendArray(
[
{
m: "p",
_id: p._id,
name: p.name,
color: p.color,
id: p.id,
x: x,
y: y,
tag: usersConfig.enableTags ? p.tag : undefined
}
]
);
this.sendArray([
{
m: "p",
_id: p._id,
name: p.name,
color: p.color,
id: p.id,
x: x,
y: y,
tag: usersConfig.enableTags ? p.tag : undefined
}
]);
}
//this.logger.debug("Update from user data update handler");
@ -357,11 +395,11 @@ export class Channel extends EventEmitter {
}
});
this.on("cursor", (pos: { x: string | number; y: string | number; id: string }) => {
let found;
this.on("cursor", (pos: CachedCursor) => {
let found: CachedCursor | undefined;
for (const cursor of this.cursorCache) {
if (cursor.id == pos.id) {
if (cursor.id === pos.id) {
found = cursor;
}
}
@ -379,9 +417,8 @@ export class Channel extends EventEmitter {
{
m: "m",
id: pos.id,
// not type safe
x: pos.x as string,
y: pos.y as string
x: pos.x,
y: pos.y
}
]);
});
@ -416,7 +453,7 @@ export class Channel extends EventEmitter {
*/
public isLobby() {
for (const reg of config.lobbyRegexes) {
let exp = new RegExp(reg, "g");
const exp = new RegExp(reg, "g");
if (this.getID().match(exp)) {
return true;
@ -430,9 +467,13 @@ export class Channel extends EventEmitter {
* Determine whether this channel is a lobby with the name "lobby" in it
*/
public isTrueLobby() {
if (this.getID().match("^lobby[0-9][0-9]$") && this.getID().match("^lobby[0-9]$") && this.getID().match("^lobby$"), "^lobbyNaN$") return true;
return false;
const _id = this.getID();
return (
_id.match("^lobby[0-9][0-9]$") &&
_id.match("^lobby[0-9]$") &&
_id.match("^lobby$") &&
_id.match("^lobbyNaN$")
);
}
/**
@ -441,10 +482,7 @@ export class Channel extends EventEmitter {
* @param admin Whether a user is changing the settings (set to true to force the changes)
* @returns undefined
*/
public changeSettings(
set: Partial<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({
m: "a",
message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.`
}, p);
await this.sendChat(
{
m: "a",
message: `Banned ${bannedPart.name} from the channel for ${minutes} minutes.`
},
p
);
this.sendNotification({
title: "Notice",
text: `${p.name} banned ${bannedPart.name} from the channel for ${minutes} minutes.`,
@ -1072,7 +1128,7 @@ export class Channel extends EventEmitter {
class: "short"
});
if (banner == _id) {
if (banner === _id) {
const certificate = {
title: "Certificate of Award",
text: `Let it be known that ${p.name} kickbanned him/her self.`,
@ -1082,7 +1138,7 @@ export class Channel extends EventEmitter {
this.sendNotification(certificate);
for (const s of socketsBySocketID.values()) {
for (const s of socketsByUUID.values()) {
const userID = s.getUserID();
if (!userID) continue;
if (userID !== banner) continue;
@ -1110,7 +1166,7 @@ export class Channel extends EventEmitter {
}
// Check if they are banned
if (ban.userId == _id) {
if (ban.userId === _id) {
return true;
}
}
@ -1128,7 +1184,7 @@ export class Channel extends EventEmitter {
if (!isBanned) return;
for (const ban of this.bans) {
if (ban.userId == _id) {
if (ban.userId === _id) {
this.bans.splice(this.bans.indexOf(ban), 1);
}
}
@ -1141,34 +1197,38 @@ export class Channel extends EventEmitter {
this.chatHistory = [];
await saveChatHistory(this.getID(), this.chatHistory);
this.sendArray([{
m: "c",
c: this.chatHistory
}]);
this.sendArray([
{
m: "c",
c: this.chatHistory
}
]);
}
/**
* Send a notification to this channel
* @param notif Notification to send
**/
* Send a notification to this channel
* @param notif Notification to send
**/
public sendNotification(notif: Notification) {
this.sendArray([{
m: "notification",
id: notif.id,
target: notif.target,
duration: notif.duration,
class: notif.class,
title: notif.title,
text: notif.text,
html: notif.html
}]);
this.sendArray([
{
m: "notification",
id: notif.id,
target: notif.target,
duration: notif.duration,
class: notif.class,
title: notif.title,
text: notif.text,
html: notif.html
}
]);
}
/**
* Send a message in chat
* @param msg Chat message event to send
* @param p Participant who is "sending the message"
**/
* Send a message in chat
* @param msg Chat message event to send
* @param p Participant who is "sending the message"
**/
public async sendChat(msg: ServerEvents["a"], p: Participant) {
if (!msg.message) return;
@ -1180,7 +1240,7 @@ export class Channel extends EventEmitter {
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
.trim();
let outgoing: ClientEvents["a"] = {
const outgoing: ClientEvents["a"] = {
m: "a",
a: msg.message,
t: Date.now(),
@ -1203,10 +1263,13 @@ export class Channel extends EventEmitter {
* @param message Message to send in chat
**/
public async sendChatAdmin(message: string) {
this.sendChat({
m: "a",
message
}, usersConfig.adminParticipant);
this.sendChat(
{
m: "a",
message
},
usersConfig.adminParticipant
);
}
/**
@ -1214,7 +1277,10 @@ export class Channel extends EventEmitter {
* @param key Flag ID
* @param val Value of which the flag will be set to
**/
public setFlag(key: string, val: any) {
public setFlag<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,28 +13,37 @@ interface ChannelConfig {
channelDestroyTimeout: number;
}
export const config = loadConfig<ChannelConfig>("config/channels.yml", {
forceLoad: ["lobby", "test/awkward"],
lobbySettings: {
lobby: true,
chat: true,
crownsolo: false,
visible: true,
color: "#73b3cc",
color2: "#273546"
},
defaultSettings: {
chat: true,
crownsolo: false,
color: "#3b5054",
color2: "#001014",
visible: true
},
// Here's a terrifying fact: Brandon used parseInt to check lobby names
lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[0-9]$", "^lobby$", "^lobbyNaN$", "^test/.+$"],
lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward",
sendLimit: false,
chownOnRejoin: true,
channelDestroyTimeout: 1000
});
export const config = ConfigManager.loadConfig<ChannelConfig>(
"config/channels.yml",
{
forceLoad: ["lobby", "test/awkward"],
lobbySettings: {
lobby: true,
chat: true,
crownsolo: false,
visible: true,
color: "#73b3cc",
color2: "#273546"
},
defaultSettings: {
chat: true,
crownsolo: false,
color: "#3b5054",
color2: "#001014",
visible: true
},
// Here's a terrifying fact: Brandon used parseInt to check lobby names
lobbyRegexes: [
"^lobby[0-9][0-9]$",
"^lobby[0-9]$",
"^lobby$",
"^lobbyNaN$",
"^test/.+$"
],
lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward",
sendLimit: false,
chownOnRejoin: true,
channelDestroyTimeout: 1000
}
);

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