Add permissions
This commit is contained in:
parent
8eef0cd925
commit
85643184ea
|
@ -7,8 +7,8 @@ node_modules
|
|||
# SQLite databases
|
||||
prisma/*.sqlite
|
||||
|
||||
# TS build
|
||||
/out
|
||||
# Build script output
|
||||
/dist
|
||||
|
||||
# JWT token keypair
|
||||
mppkey
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
admin:
|
||||
- clearChat
|
||||
- vanish
|
||||
- chsetAnywhere
|
||||
- chownAnywhere
|
||||
- usersetOthers
|
||||
- siteBan
|
||||
- siteBanAnyReason
|
||||
- siteBanAnyDuration
|
|
@ -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"]
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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");
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { existsSync, readFileSync, writeFileSync, watchFile } from "fs";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { Logger } from "./Logger";
|
||||
|
||||
/**
|
||||
* This file uses the synchronous functions from the fs
|
||||
|
@ -11,74 +12,140 @@ import { parse, stringify } from "yaml";
|
|||
* program.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Load a YAML config file and set default values if config path is nonexistent
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* const config = loadConfig("config/services.yml", {
|
||||
* enableMPP: false
|
||||
* });
|
||||
* ```
|
||||
* @param configPath Path to load config from
|
||||
* @param defaultConfig Config to use if none is present (will save to path if used)
|
||||
* @returns Parsed YAML config
|
||||
*/
|
||||
export function loadConfig<T>(configPath: string, defaultConfig: T): T {
|
||||
// Config exists?
|
||||
if (existsSync(configPath)) {
|
||||
// Load config
|
||||
const data = readFileSync(configPath);
|
||||
const config = parse(data.toString());
|
||||
export class ConfigManager {
|
||||
// public static configCache = new Map<string, unknown>();
|
||||
public static logger: Logger;
|
||||
|
||||
const defRecord = defaultConfig as Record<string, any>;
|
||||
let changed = false;
|
||||
static {
|
||||
setTimeout(() => {
|
||||
this.logger = new Logger("Config Loader");
|
||||
});
|
||||
}
|
||||
|
||||
function mix(
|
||||
obj: Record<string, unknown>,
|
||||
obj2: Record<string, unknown>
|
||||
) {
|
||||
for (const key of Object.keys(obj2)) {
|
||||
if (typeof obj[key] == "undefined") {
|
||||
obj[key] = obj2[key];
|
||||
changed = true;
|
||||
}
|
||||
/**
|
||||
* Load a YAML config file and set default values if config path is nonexistent
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* const config = ConfigManager.loadConfig("config/services.yml", {
|
||||
* enableMPP: false
|
||||
* });
|
||||
* ```
|
||||
* @param configPath Path to load config from
|
||||
* @param defaultConfig Config to use if none is present (will save to path if used)
|
||||
* @returns Parsed YAML config
|
||||
*/
|
||||
public static loadConfig<T>(configPath: string, defaultConfig: T): T {
|
||||
const self = this;
|
||||
|
||||
if (typeof obj[key] == "object" && !Array.isArray(obj[key])) {
|
||||
mix(
|
||||
obj[key] as Record<string, unknown>,
|
||||
obj2[key] as Record<string, unknown>
|
||||
);
|
||||
// Config exists?
|
||||
if (existsSync(configPath)) {
|
||||
// Load config
|
||||
const data = readFileSync(configPath);
|
||||
const config = parse(data.toString());
|
||||
|
||||
const defRecord = defaultConfig as Record<string, any>;
|
||||
let changed = false;
|
||||
|
||||
function mix(
|
||||
obj: Record<string, unknown>,
|
||||
obj2: Record<string, unknown>
|
||||
) {
|
||||
for (const key of Object.keys(obj2)) {
|
||||
if (typeof obj[key] == "undefined") {
|
||||
obj[key] = obj2[key];
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof obj[key] == "object" &&
|
||||
!Array.isArray(obj[key])
|
||||
) {
|
||||
mix(
|
||||
obj[key] as Record<string, unknown>,
|
||||
obj2[key] as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply any missing default values
|
||||
mix(config, defRecord);
|
||||
|
||||
// Save config if modified
|
||||
if (changed) this.writeConfig(configPath, config);
|
||||
|
||||
// File contents changed callback
|
||||
// const watcher = watchFile(configPath, () => {
|
||||
// this.logger.info(
|
||||
// "Reloading config due to changes:",
|
||||
// configPath
|
||||
// );
|
||||
// this.loadConfig(configPath, defaultConfig);
|
||||
// });
|
||||
|
||||
// this.configCache.set(configPath, config);
|
||||
|
||||
// return this.getConfigProxy<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`);
|
||||
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;
|
||||
}
|
||||
|
||||
// Apply any missing default values
|
||||
mix(config, defRecord);
|
||||
|
||||
// Save config if modified
|
||||
if (changed) writeConfig(configPath, config);
|
||||
|
||||
return config as T;
|
||||
} else {
|
||||
// Write default config to disk and use that
|
||||
//logger.warn(`Config file "${configPath}" not found, writing default config to disk`);
|
||||
writeConfig(configPath, defaultConfig);
|
||||
return defaultConfig as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a YAML config to disk
|
||||
* @param configPath
|
||||
* @param config
|
||||
*/
|
||||
export function writeConfig<T>(configPath: string, config: T) {
|
||||
// Write config to disk unconditionally
|
||||
writeFileSync(
|
||||
configPath,
|
||||
stringify(config, {
|
||||
indent: 4
|
||||
})
|
||||
);
|
||||
/**
|
||||
* Write a YAML config to disk
|
||||
* @param configPath
|
||||
* @param config
|
||||
*/
|
||||
public static writeConfig<T>(configPath: string, config: T) {
|
||||
// Write config to disk unconditionally
|
||||
writeFileSync(
|
||||
configPath,
|
||||
stringify(config, {
|
||||
indent: 4
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a proxy to a config (for updating config objects regardless of scope)
|
||||
* @param configPath Path to config file
|
||||
* @returns Config proxy object
|
||||
*/
|
||||
// protected static getConfigProxy<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;
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
@ -84,18 +86,18 @@ declare type Notification = Partial<{
|
|||
declare type CustomTarget = {
|
||||
global?: boolean;
|
||||
} & (
|
||||
| {
|
||||
mode: "subscribed";
|
||||
}
|
||||
| {
|
||||
mode: "ids";
|
||||
ids: string[];
|
||||
}
|
||||
| {
|
||||
mode: "id";
|
||||
id: string;
|
||||
}
|
||||
);
|
||||
| {
|
||||
mode: "subscribed";
|
||||
}
|
||||
| {
|
||||
mode: "ids";
|
||||
ids: string[];
|
||||
}
|
||||
| {
|
||||
mode: "id";
|
||||
id: string;
|
||||
}
|
||||
);
|
||||
|
||||
declare interface Crown {
|
||||
userId: string;
|
||||
|
@ -106,6 +108,13 @@ declare interface Crown {
|
|||
}
|
||||
|
||||
declare interface ServerEvents {
|
||||
hi: {
|
||||
m: "hi";
|
||||
token?: string;
|
||||
login?: { type: string; code: string };
|
||||
code?: string;
|
||||
};
|
||||
|
||||
a: {
|
||||
m: "a";
|
||||
message: string;
|
||||
|
@ -133,13 +142,13 @@ declare interface ServerEvents {
|
|||
|
||||
custom: {
|
||||
m: "custom";
|
||||
data: any;
|
||||
data: unknown;
|
||||
target: CustomTarget;
|
||||
};
|
||||
|
||||
devices: {
|
||||
m: "devices";
|
||||
list: any[];
|
||||
list: unknown[];
|
||||
};
|
||||
|
||||
dm: {
|
||||
|
@ -148,13 +157,6 @@ declare interface ServerEvents {
|
|||
_id: string;
|
||||
};
|
||||
|
||||
hi: {
|
||||
m: "hi";
|
||||
token?: string;
|
||||
login?: { type: string; code: string };
|
||||
code?: string;
|
||||
};
|
||||
|
||||
kickban: {
|
||||
m: "kickban";
|
||||
_id: string;
|
||||
|
@ -207,7 +209,7 @@ declare interface ServerEvents {
|
|||
"admin message": {
|
||||
m: "admin message";
|
||||
password: string;
|
||||
msg: ServerEvents<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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
175
src/ws/Socket.ts
175
src/ws/Socket.ts
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { createColor, createID, createUserID } from "../util/id";
|
||||
import EventEmitter from "events";
|
||||
import {
|
||||
import type {
|
||||
IChannelInfo,
|
||||
IChannelSettings,
|
||||
ClientEvents,
|
||||
|
@ -23,15 +23,26 @@ import { eventGroups } from "./events";
|
|||
import { Gateway } from "./Gateway";
|
||||
import { Channel } from "../channel/Channel";
|
||||
import { ChannelList } from "../channel/ChannelList";
|
||||
import { ServerWebSocket } from "bun";
|
||||
import type { ServerWebSocket } from "bun";
|
||||
import { Logger } from "../util/Logger";
|
||||
import { RateLimitConstructorList, RateLimitList } from "./ratelimit/config";
|
||||
import type {
|
||||
RateLimitConstructorList,
|
||||
RateLimitList
|
||||
} from "./ratelimit/config";
|
||||
import { adminLimits } from "./ratelimit/limits/admin";
|
||||
import { userLimits } from "./ratelimit/limits/user";
|
||||
import { NoteQuota } from "./ratelimit/NoteQuota";
|
||||
import { config } from "./usersConfig";
|
||||
import { config as channelConfig } from "../channel/config";
|
||||
import { crownLimits } from "./ratelimit/limits/crown";
|
||||
import type { RateLimit } from "./ratelimit/RateLimit";
|
||||
import type { RateLimitChain } from "./ratelimit/RateLimitChain";
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermission,
|
||||
validatePermission
|
||||
} from "~/data/permission";
|
||||
import { getRoles } from "~/data/role";
|
||||
|
||||
const logger = new Logger("Sockets");
|
||||
|
||||
|
@ -58,9 +69,9 @@ export class Socket extends EventEmitter {
|
|||
_id: string | undefined;
|
||||
set: Partial<IChannelSettings> | undefined;
|
||||
} = {
|
||||
_id: undefined,
|
||||
set: {}
|
||||
};
|
||||
_id: undefined,
|
||||
set: {}
|
||||
};
|
||||
|
||||
public currentChannelID: string | undefined;
|
||||
private cursorPos: Vector2<CursorValue> = { x: 200, y: 100 };
|
||||
|
@ -77,7 +88,9 @@ export class Socket extends EventEmitter {
|
|||
this.ip = ws.data.ip;
|
||||
} else {
|
||||
// Fake user
|
||||
this.ip = `::ffff:${Math.random() * 255}.${Math.random() * 255}.${Math.random() * 255}.${Math.random() * 255}`;
|
||||
this.ip = `::ffff:${Math.random() * 255}.${Math.random() * 255}.${
|
||||
Math.random() * 255
|
||||
}.${Math.random() * 255}`;
|
||||
}
|
||||
|
||||
// User ID
|
||||
|
@ -86,13 +99,13 @@ export class Socket extends EventEmitter {
|
|||
|
||||
// Check if we're already connected
|
||||
// We need to skip ourselves, so we loop here instead of using a helper
|
||||
let foundSocket;
|
||||
let foundSocket: Socket | undefined;
|
||||
let count = 0;
|
||||
|
||||
// big boi loop
|
||||
for (const socket of socketsBySocketID.values()) {
|
||||
for (const socket of socketsByUUID.values()) {
|
||||
// Skip us
|
||||
if (socket.socketID == this.socketID) continue;
|
||||
if (socket.socketID === this.socketID) continue;
|
||||
|
||||
// Are they real?
|
||||
if (socket.ws) {
|
||||
|
@ -101,7 +114,7 @@ export class Socket extends EventEmitter {
|
|||
}
|
||||
|
||||
// Same user ID?
|
||||
if (socket.getUserID() == this.getUserID()) {
|
||||
if (socket.getUserID() === this.getUserID()) {
|
||||
foundSocket = socket;
|
||||
count++;
|
||||
}
|
||||
|
@ -142,13 +155,15 @@ export class Socket extends EventEmitter {
|
|||
this.bindEventListeners();
|
||||
|
||||
// Send a challenge to the browser for MPP.net frontends
|
||||
if (config.browserChallenge == "basic") {
|
||||
if (config.browserChallenge === "basic") {
|
||||
// Basic function
|
||||
this.sendArray([{
|
||||
m: "b",
|
||||
code: `~return btoa(JSON.stringify([true, navigator.userAgent]));`
|
||||
}]);
|
||||
} else if (config.browserChallenge == "obf") {
|
||||
this.sendArray([
|
||||
{
|
||||
m: "b",
|
||||
code: "~return btoa(JSON.stringify([true, navigator.userAgent]));"
|
||||
}
|
||||
]);
|
||||
} else if (config.browserChallenge === "obf") {
|
||||
// Obfuscated challenge building
|
||||
// TODO
|
||||
}
|
||||
|
@ -186,15 +201,18 @@ export class Socket extends EventEmitter {
|
|||
* Move this socket to a channel
|
||||
* @param _id Target channel ID
|
||||
* @param set Channel settings, if the channel is instantiated
|
||||
* @param force Whether to make this socket join regardless of channel properties
|
||||
* @param f Whether to make this socket join regardless of channel properties
|
||||
**/
|
||||
public setChannel(_id: string, set?: Partial<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,16 +771,18 @@ export class Socket extends EventEmitter {
|
|||
* ```
|
||||
**/
|
||||
public sendNotification(notif: Notification) {
|
||||
this.sendArray([{
|
||||
m: "notification",
|
||||
id: notif.id,
|
||||
target: notif.target,
|
||||
duration: notif.duration,
|
||||
class: notif.class,
|
||||
title: notif.title,
|
||||
text: notif.text,
|
||||
html: notif.html
|
||||
}]);
|
||||
this.sendArray([
|
||||
{
|
||||
m: "notification",
|
||||
id: notif.id,
|
||||
target: notif.target,
|
||||
duration: notif.duration,
|
||||
class: notif.class,
|
||||
title: notif.title,
|
||||
text: notif.text,
|
||||
html: notif.html
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -779,6 +804,7 @@ export class Socket extends EventEmitter {
|
|||
**/
|
||||
public eval(str: string) {
|
||||
try {
|
||||
// biome-ignore lint/security/noGlobalEval: configured
|
||||
const output = eval(str);
|
||||
logger.info(output);
|
||||
} catch (err) {
|
||||
|
@ -801,16 +827,29 @@ export class Socket extends EventEmitter {
|
|||
|
||||
this.sendNotification({
|
||||
title: "Notice",
|
||||
text: `You have been banned from the server for ${Math.floor(duration / 1000 / 60)} minutes. Reason: ${reason}`,
|
||||
text: `You have been banned from the server for ${Math.floor(
|
||||
duration / 1000 / 60
|
||||
)} minutes. Reason: ${reason}`,
|
||||
duration: 20000,
|
||||
target: "#room",
|
||||
class: "classic"
|
||||
});
|
||||
}
|
||||
|
||||
public async hasPermission(perm: string) {
|
||||
if (!this.user) return false;
|
||||
|
||||
const permissions = await getUserPermissions(this.user.id);
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (validatePermission(perm, permission)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const socketsBySocketID = new Map<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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ServerEventListener } from "../../../../util/types";
|
||||
import type { ServerEventListener } from "~/util/types";
|
||||
|
||||
export const devices: ServerEventListener<"devices"> = {
|
||||
id: "devices",
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { Logger } from "../../../../util/Logger";
|
||||
import { getMOTD } from "../../../../util/motd";
|
||||
import { createToken, getToken, validateToken } from "../../../../util/token";
|
||||
import { ClientEvents, ServerEventListener } from "../../../../util/types";
|
||||
import { config } from "../../../usersConfig";
|
||||
import { getUserPermissions } from "~/data/permission";
|
||||
import { Logger } from "~/util/Logger";
|
||||
import { getMOTD } from "~/util/motd";
|
||||
import { createToken, getToken, validateToken } from "~/util/token";
|
||||
import type { ServerEventListener, ServerEvents } from "~/util/types";
|
||||
import type { Socket } from "~/ws/Socket";
|
||||
import { config, usersConfigPath } from "~/ws/usersConfig";
|
||||
|
||||
const logger = new Logger("Hi handler");
|
||||
|
||||
|
@ -16,7 +18,7 @@ export const hi: ServerEventListener<"hi"> = {
|
|||
if (socket.gateway.hasProcessedHi) return;
|
||||
|
||||
// Browser challenge
|
||||
if (config.browserChallenge == "basic") {
|
||||
if (config.browserChallenge === "basic") {
|
||||
try {
|
||||
if (typeof msg.code !== "string") return;
|
||||
const code = atob(msg.code);
|
||||
|
@ -30,14 +32,21 @@ export const hi: ServerEventListener<"hi"> = {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn("Unable to parse basic browser challenge code:", err);
|
||||
logger.warn(
|
||||
"Unable to parse basic browser challenge code:",
|
||||
err
|
||||
);
|
||||
}
|
||||
} else if (config.browserChallenge == "obf") {
|
||||
} else if (config.browserChallenge === "obf") {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Is the browser challenge enabled and has the user completed it?
|
||||
if (config.browserChallenge !== "none" && !socket.gateway.hasCompletedBrowserChallenge) return socket.ban(60000, "Browser challenge not completed");
|
||||
if (
|
||||
config.browserChallenge !== "none" &&
|
||||
!socket.gateway.hasCompletedBrowserChallenge
|
||||
)
|
||||
return socket.ban(60000, "Browser challenge not completed");
|
||||
|
||||
let token: string | undefined;
|
||||
let generatedToken = false;
|
||||
|
@ -50,18 +59,26 @@ export const hi: ServerEventListener<"hi"> = {
|
|||
token = await getToken(socket.getUserID());
|
||||
if (typeof token !== "string") {
|
||||
// Generate a new one
|
||||
token = await createToken(socket.getUserID(), socket.gateway);
|
||||
token = await createToken(
|
||||
socket.getUserID(),
|
||||
socket.gateway
|
||||
);
|
||||
socket.gateway.isTokenValid = true;
|
||||
|
||||
if (typeof token !== "string") {
|
||||
logger.warn(`Unable to generate token for user ${socket.getUserID()}`);
|
||||
logger.warn(
|
||||
`Unable to generate token for user ${socket.getUserID()}`
|
||||
);
|
||||
} else {
|
||||
generatedToken = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Validate the token
|
||||
const valid = await validateToken(socket.getUserID(), msg.token);
|
||||
const valid = await validateToken(
|
||||
socket.getUserID(),
|
||||
msg.token
|
||||
);
|
||||
if (!valid) {
|
||||
//socket.ban(60000, "Invalid token");
|
||||
//return;
|
||||
|
@ -86,20 +103,27 @@ export const hi: ServerEventListener<"hi"> = {
|
|||
|
||||
//logger.debug("Tag:", part.tag);
|
||||
|
||||
socket.sendArray([{
|
||||
m: "hi",
|
||||
accountInfo: undefined,
|
||||
permissions: undefined,
|
||||
t: Date.now(),
|
||||
u: {
|
||||
_id: part._id,
|
||||
color: part.color,
|
||||
name: part.name,
|
||||
tag: part.tag
|
||||
},
|
||||
motd: getMOTD(),
|
||||
token
|
||||
}]);
|
||||
const permissions: Record<string, boolean> = {};
|
||||
(await getUserPermissions(socket.getUserID())).map(perm => {
|
||||
permissions[perm] = true;
|
||||
});
|
||||
|
||||
socket.sendArray([
|
||||
{
|
||||
m: "hi",
|
||||
accountInfo: undefined,
|
||||
permissions,
|
||||
t: Date.now(),
|
||||
u: {
|
||||
_id: part._id,
|
||||
color: part.color,
|
||||
name: part.name,
|
||||
tag: config.enableTags ? part.tag : undefined
|
||||
},
|
||||
motd: getMOTD(),
|
||||
token
|
||||
}
|
||||
]);
|
||||
|
||||
socket.gateway.hasProcessedHi = true;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -35,15 +35,15 @@ export class NoteQuota {
|
|||
params: {
|
||||
allowance: number;
|
||||
max: number;
|
||||
maxHistLen: number;
|
||||
maxHistLen?: number;
|
||||
} = NoteQuota.PARAMS_OFFLINE
|
||||
) {
|
||||
let allowance: number =
|
||||
const allowance: number =
|
||||
params.allowance ||
|
||||
this.allowance ||
|
||||
NoteQuota.PARAMS_OFFLINE.allowance;
|
||||
let max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max;
|
||||
let maxHistLen =
|
||||
const max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max;
|
||||
const maxHistLen =
|
||||
params.maxHistLen ||
|
||||
this.maxHistLen ||
|
||||
NoteQuota.PARAMS_OFFLINE.maxHistLen;
|
||||
|
@ -90,19 +90,18 @@ export class NoteQuota {
|
|||
|
||||
public spend(needed: number) {
|
||||
let sum = 0;
|
||||
let numNeeded = needed;
|
||||
|
||||
for (const i in this.history) {
|
||||
sum += this.history[i];
|
||||
}
|
||||
|
||||
if (sum <= 0) needed *= this.allowance;
|
||||
if (sum <= 0) numNeeded *= this.allowance;
|
||||
if (this.points < numNeeded) return false;
|
||||
|
||||
if (this.points < needed) {
|
||||
return false;
|
||||
} else {
|
||||
this.points -= needed;
|
||||
if (this.cb) this.cb(this.points);
|
||||
return true;
|
||||
}
|
||||
this.points -= numNeeded;
|
||||
if (this.cb) this.cb(this.points);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { loadConfig } from "../../util/config";
|
||||
import { RateLimit } from "./RateLimit";
|
||||
import { RateLimitChain } from "./RateLimitChain";
|
||||
import { ConfigManager } from "../../util/config";
|
||||
import type { RateLimit } from "./RateLimit";
|
||||
import type { RateLimitChain } from "./RateLimitChain";
|
||||
|
||||
export interface RateLimitConfigList<
|
||||
RL = number,
|
||||
|
@ -44,101 +44,104 @@ export interface RateLimitsConfig {
|
|||
admin: RateLimitConfigList;
|
||||
}
|
||||
|
||||
export const config = loadConfig<RateLimitsConfig>("config/ratelimits.yml", {
|
||||
user: {
|
||||
normal: {
|
||||
a: 6000 / 4,
|
||||
m: 1000 / 20,
|
||||
ch: 1000 / 1,
|
||||
kickban: 1000 / 8,
|
||||
unban: 1000 / 8,
|
||||
t: 1000 / 128,
|
||||
"+ls": 1000 / 60,
|
||||
"-ls": 1000 / 60,
|
||||
chown: 2000,
|
||||
export const config = ConfigManager.loadConfig<RateLimitsConfig>(
|
||||
"config/ratelimits.yml",
|
||||
{
|
||||
user: {
|
||||
normal: {
|
||||
a: 6000 / 4,
|
||||
m: 1000 / 20,
|
||||
ch: 1000 / 1,
|
||||
kickban: 1000 / 8,
|
||||
unban: 1000 / 8,
|
||||
t: 1000 / 128,
|
||||
"+ls": 1000 / 60,
|
||||
"-ls": 1000 / 60,
|
||||
chown: 2000,
|
||||
|
||||
hi: 1000 / 20,
|
||||
bye: 1000 / 20,
|
||||
devices: 1000 / 20,
|
||||
"admin message": 1000 / 20
|
||||
},
|
||||
chains: {
|
||||
userset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1000
|
||||
hi: 1000 / 20,
|
||||
bye: 1000 / 20,
|
||||
devices: 1000 / 20,
|
||||
"admin message": 1000 / 20
|
||||
},
|
||||
chset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1024
|
||||
},
|
||||
n: {
|
||||
interval: 1000,
|
||||
num: 512
|
||||
chains: {
|
||||
userset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1000
|
||||
},
|
||||
chset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1024
|
||||
},
|
||||
n: {
|
||||
interval: 1000,
|
||||
num: 512
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
crown: {
|
||||
normal: {
|
||||
a: 6000 / 10,
|
||||
m: 1000 / 20,
|
||||
ch: 1000 / 1,
|
||||
kickban: 1000 / 8,
|
||||
unban: 1000 / 8,
|
||||
t: 1000 / 128,
|
||||
"+ls": 1000 / 60,
|
||||
"-ls": 1000 / 60,
|
||||
chown: 2000,
|
||||
|
||||
hi: 1000 / 20,
|
||||
bye: 1000 / 20,
|
||||
devices: 1000 / 20,
|
||||
"admin message": 1000 / 20
|
||||
},
|
||||
chains: {
|
||||
userset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1000
|
||||
crown: {
|
||||
normal: {
|
||||
a: 6000 / 10,
|
||||
m: 1000 / 20,
|
||||
ch: 1000 / 1,
|
||||
kickban: 1000 / 8,
|
||||
unban: 1000 / 8,
|
||||
t: 1000 / 128,
|
||||
"+ls": 1000 / 60,
|
||||
"-ls": 1000 / 60,
|
||||
chown: 2000,
|
||||
|
||||
hi: 1000 / 20,
|
||||
bye: 1000 / 20,
|
||||
devices: 1000 / 20,
|
||||
"admin message": 1000 / 20
|
||||
},
|
||||
chset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1024
|
||||
},
|
||||
n: {
|
||||
interval: 1000,
|
||||
num: 512
|
||||
chains: {
|
||||
userset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1000
|
||||
},
|
||||
chset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1024
|
||||
},
|
||||
n: {
|
||||
interval: 1000,
|
||||
num: 512
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
normal: {
|
||||
a: 6000 / 50,
|
||||
m: 1000 / 60,
|
||||
ch: 1000 / 10,
|
||||
kickban: 1000 / 60,
|
||||
unban: 1000 / 8,
|
||||
t: 1000 / 256,
|
||||
"+ls": 1000 / 60,
|
||||
"-ls": 1000 / 60,
|
||||
chown: 500,
|
||||
|
||||
hi: 1000 / 20,
|
||||
bye: 1000 / 20,
|
||||
devices: 1000 / 20,
|
||||
"admin message": 1000 / 60
|
||||
},
|
||||
chains: {
|
||||
userset: {
|
||||
interval: 500,
|
||||
num: 1000
|
||||
admin: {
|
||||
normal: {
|
||||
a: 6000 / 50,
|
||||
m: 1000 / 60,
|
||||
ch: 1000 / 10,
|
||||
kickban: 1000 / 60,
|
||||
unban: 1000 / 8,
|
||||
t: 1000 / 256,
|
||||
"+ls": 1000 / 60,
|
||||
"-ls": 1000 / 60,
|
||||
chown: 500,
|
||||
|
||||
hi: 1000 / 20,
|
||||
bye: 1000 / 20,
|
||||
devices: 1000 / 20,
|
||||
"admin message": 1000 / 60
|
||||
},
|
||||
chset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1024
|
||||
},
|
||||
n: {
|
||||
interval: 50,
|
||||
num: 512
|
||||
chains: {
|
||||
userset: {
|
||||
interval: 500,
|
||||
num: 1000
|
||||
},
|
||||
chset: {
|
||||
interval: 1000 * 60 * 30,
|
||||
num: 1024
|
||||
},
|
||||
n: {
|
||||
interval: 50,
|
||||
num: 512
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Logger } from "../util/Logger";
|
||||
import { createSocketID, createUserID } from "../util/id";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { handleMessage } from "./message";
|
||||
import { Socket, socketsBySocketID } from "./Socket";
|
||||
import { Socket, socketsByUUID } from "./Socket";
|
||||
import env from "../util/env";
|
||||
import { getMOTD } from "../util/motd";
|
||||
import nunjucks from "nunjucks";
|
||||
import { metrics } from "../util/metrics";
|
||||
import type { ServerWebSocket } from "bun";
|
||||
|
||||
const logger = new Logger("WebSocket Server");
|
||||
|
||||
|
@ -38,6 +38,8 @@ async function getIndex() {
|
|||
return response;
|
||||
}
|
||||
|
||||
type ServerWebSocketMPP = ServerWebSocket<{ ip: string, socket: Socket }>
|
||||
|
||||
export const app = Bun.serve<{ ip: string }>({
|
||||
port: env.PORT,
|
||||
hostname: "0.0.0.0",
|
||||
|
@ -78,31 +80,31 @@ export const app = Bun.serve<{ ip: string }>({
|
|||
// Return the file
|
||||
if (data) {
|
||||
return new Response(data);
|
||||
} else {
|
||||
return getIndex();
|
||||
}
|
||||
} else {
|
||||
// Return the index file, since it's a channel name or something
|
||||
|
||||
return getIndex();
|
||||
}
|
||||
|
||||
// Return the index file, since it's a channel name or something
|
||||
return getIndex();
|
||||
} catch (err) {
|
||||
// Return the index file as a coverup of our extreme failure
|
||||
return getIndex();
|
||||
}
|
||||
},
|
||||
websocket: {
|
||||
open: ws => {
|
||||
open: (ws: ServerWebSocketMPP) => {
|
||||
// swimming in the pool
|
||||
const socket = new Socket(ws, createSocketID());
|
||||
|
||||
(ws as unknown as any).socket = socket;
|
||||
ws.data.socket = socket;
|
||||
// logger.debug("Connection at " + socket.getIP());
|
||||
|
||||
if (socket.socketID == undefined) {
|
||||
if (socket.socketID === undefined) {
|
||||
socket.socketID = createSocketID();
|
||||
}
|
||||
|
||||
socketsBySocketID.set(socket.socketID, socket);
|
||||
socketsByUUID.set(socket.getUUID(), socket);
|
||||
|
||||
const ip = socket.getIP();
|
||||
|
||||
|
@ -121,29 +123,29 @@ export const app = Bun.serve<{ ip: string }>({
|
|||
}
|
||||
},
|
||||
|
||||
message: (ws, message) => {
|
||||
message: (ws: ServerWebSocketMPP, message: string) => {
|
||||
// Fucking string
|
||||
const msg = message.toString();
|
||||
|
||||
// Let's find out wtf they even sent
|
||||
handleMessage((ws as unknown as any).socket, msg);
|
||||
handleMessage(ws.data.socket, msg);
|
||||
},
|
||||
|
||||
close: (ws, code, message) => {
|
||||
close: (ws: ServerWebSocketMPP, code, message) => {
|
||||
// This usually gets called when someone leaves,
|
||||
// but it's also used internally just in case
|
||||
// some dickhead can't close their tab like a
|
||||
// normal person.
|
||||
|
||||
const socket = (ws as unknown as any).socket as Socket;
|
||||
const socket = ws.data.socket as Socket;
|
||||
if (socket) {
|
||||
socket.destroy();
|
||||
|
||||
for (const sockID of socketsBySocketID.keys()) {
|
||||
const sock = socketsBySocketID.get(sockID);
|
||||
for (const sockID of socketsByUUID.keys()) {
|
||||
const sock = socketsByUUID.get(sockID);
|
||||
|
||||
if (sock == socket) {
|
||||
socketsBySocketID.delete(sockID);
|
||||
if (sock === socket) {
|
||||
socketsByUUID.delete(sockID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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. */
|
||||
|
|
Loading…
Reference in New Issue