Fix channel settings validation, attempt to implement channel list and admin messages

This commit is contained in:
Hri7566 2024-01-22 07:04:20 -05:00
parent ffb8044734
commit 8a8ce405fd
13 changed files with 206 additions and 51 deletions

View File

@ -13,6 +13,7 @@ import { Socket } from "../ws/Socket";
import { validateChannelSettings } from "./settings";
import { findSocketByPartID, socketsBySocketID } from "../ws/Socket";
import Crown from "./Crown";
import { ChannelList } from "./ChannelList";
interface ChannelConfig {
forceLoad: string[];
@ -40,14 +41,11 @@ export const config = loadConfig<ChannelConfig>("config/channels.yml", {
color2: "#001014",
visible: true
},
// TODO Test this regex
lobbyRegexes: ["^lobby[1-9]?[1-9]?$", "^test/.+$"],
lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[1-9]$", "^test/.+$"],
lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward"
});
export const channelList = new Array<Channel>();
export class Channel extends EventEmitter {
private settings: Partial<IChannelSettings> = config.defaultSettings;
private ppl = new Array<Participant>();
@ -75,9 +73,8 @@ export class Channel extends EventEmitter {
if (set) {
const validatedSet = validateChannelSettings(set);
for (const key in Object.keys(validatedSet)) {
if (!(validatedSet as any)[key]) continue;
for (const key of Object.keys(set)) {
if ((validatedSet as any)[key] === false) continue;
(this.settings as any)[key] = (set as any)[key];
}
}
@ -85,10 +82,10 @@ export class Channel extends EventEmitter {
this.crown = new Crown();
if (creator) {
if (this.crown.canBeSetBy(creator)) {
// if (this.crown.canBeSetBy(creator)) {
const part = creator.getParticipant();
if (part) this.giveCrown(part);
}
// }
}
}
@ -98,8 +95,10 @@ export class Channel extends EventEmitter {
this.bindEventListeners();
channelList.push(this);
ChannelList.add(this);
// TODO channel closing
this.logger.info("Created");
}
private alreadyBound = false;
@ -264,7 +263,7 @@ export class Channel extends EventEmitter {
if (hasChangedChannel) {
if (socket.currentChannelID) {
const ch = channelList.find(
const ch = ChannelList.getList().find(
ch => ch._id == socket.currentChannelID
);
if (ch) {
@ -475,7 +474,8 @@ export class Channel extends EventEmitter {
}
}
channelList.splice(channelList.indexOf(this), 1);
ChannelList.remove(this);
this.logger.info("Destroyed");
}
/**
@ -567,5 +567,5 @@ for (const id of config.forceLoad) {
}
if (!hasFullChannel) {
channelList.push(new Channel(config.fullChannel));
new Channel(config.fullChannel);
}

View File

@ -0,0 +1,41 @@
import { findSocketByPartID } from "../ws/Socket";
import type Channel from "./Channel";
const onChannelUpdate = (channel: Channel) => {
const info = channel.getInfo();
const ppl = channel.getParticipantList();
for (const partId of ChannelList.subscribers) {
const socket = findSocketByPartID(partId);
if (typeof socket == "undefined") {
ChannelList.subscribers.splice(
ChannelList.subscribers.indexOf(partId),
1
);
return;
}
socket.sendChannelUpdate(info, ppl);
}
};
export class ChannelList {
private static list = new Array<Channel>();
public static subscribers = new Array<string>();
public static add(channel: Channel) {
this.list.push(channel);
channel.on("update", () => {
onChannelUpdate(channel);
});
}
public static remove(channel: Channel) {
this.list.splice(this.list.indexOf(channel), 1);
}
public static getList() {
return this.list;
}
}

View File

@ -1,8 +1,9 @@
import { ChannelSettings } from "../util/types";
import { Logger } from "../util/Logger";
import { IChannelSettings } from "../util/types";
type Validator = "boolean" | "string" | "number" | ((val: any) => boolean);
type Validator = "boolean" | "string" | "number" | ((val: unknown) => boolean);
const validationRecord: Record<keyof ChannelSettings, Validator> = {
const validationRecord: Record<keyof IChannelSettings, Validator> = {
// Brandon
lobby: "boolean",
visible: "boolean",
@ -25,13 +26,12 @@ const validationRecord: Record<keyof ChannelSettings, Validator> = {
/**
* Check the validity of channel settings
* @param set Unknown data
* @param set Dirty settings
* @returns Record of which settings are correct
*/
export function validateChannelSettings(set: Partial<ChannelSettings>) {
export function validateChannelSettings(set: Partial<IChannelSettings>) {
// Create record
let keys = Object.keys(validationRecord);
let record: Partial<Record<keyof ChannelSettings, boolean>> = {};
let record: Partial<Record<keyof IChannelSettings, boolean>> = {};
for (const key of Object.keys(set)) {
let val = (set as Record<string, any>)[key];
@ -46,7 +46,7 @@ export function validateChannelSettings(set: Partial<ChannelSettings>) {
}
// Set valid status
record[key as keyof ChannelSettings] = validate(val, validator);
record[key as keyof IChannelSettings] = validate(val, validator);
}
return record;

View File

@ -12,3 +12,5 @@ import "./ws/server";
import { Logger } from "./util/Logger";
const logger = new Logger("Main");
import "./util/readline";

View File

@ -2,6 +2,10 @@ import { padNum, unimportant } from "./helpers";
export class Logger {
private static log(method: string, ...args: any[]) {
// Clear current line
process.stdout.write("\x1b[2K\r");
// Log our stuff
(console as unknown as Record<string, (..._args: any[]) => any>)[
method
](
@ -9,6 +13,10 @@ export class Logger {
unimportant(this.getHHMMSSMS()),
...args
);
// Fix the readline prompt (spooky code)
if ((globalThis as unknown as any).rl)
(globalThis as unknown as any).rl.prompt();
}
public static getHHMMSSMS() {

View File

@ -1,19 +1,35 @@
import readline from "readline";
import { Logger } from "./Logger";
export const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.setPrompt("mpps> ");
const logger = new Logger("CLI");
rl.setPrompt("mpps> ");
rl.prompt();
rl.on("line", msg => {
// TODO readline commands
if (msg == "mem" || msg == "memory") {
const mem = process.memoryUsage();
logger.info(
`Memory: ${(mem.heapUsed / 1000 / 1000).toFixed(2)} MB used / ${(
mem.heapTotal /
1000 /
1000
).toFixed(2)} MB total`
);
}
rl.prompt();
});
rl.on("SIGINT", () => {
process.exit();
});
(globalThis as unknown as any).rl = rl;

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

@ -91,15 +91,6 @@ declare interface Crown {
endPos: Vector2;
}
declare interface ChannelInfo {
banned?: boolean;
count: number;
id: string;
_id: string;
crown?: Crown;
settings: Partial<IChannelSettings>;
}
// Events copied from Hri7566/mppclone-client typedefs
declare interface ServerEvents {
a: {
@ -200,6 +191,12 @@ declare interface ServerEvents {
set: { name?: string; color?: string };
};
"admin message": {
m: "admin message";
password: string;
msg: ServerEvents<keyof ServerEvents>;
};
// Admin
color: {
@ -243,7 +240,7 @@ declare interface ClientEvents {
ch: {
m: "ch";
p: string;
ch: ChannelInfo;
ch: IChannelInfo;
ppl: Participant[];
};
@ -342,6 +339,7 @@ declare interface ICrown {
}
declare interface IChannelInfo {
banned?: boolean;
_id: string;
id: string;
count: number;

View File

@ -7,7 +7,7 @@
import { createColor, createID, createUserID } from "../util/id";
import EventEmitter from "events";
import {
ChannelInfo,
IChannelInfo,
IChannelSettings,
ClientEvents,
Participant,
@ -19,7 +19,8 @@ import { User } from "@prisma/client";
import { createUser, readUser, updateUser } from "../data/user";
import { eventGroups } from "./events";
import { Gateway } from "./Gateway";
import { channelList, Channel } from "../channel/Channel";
import { Channel } from "../channel/Channel";
import { ChannelList } from "../channel/ChannelList";
import { ServerWebSocket } from "bun";
import { Logger } from "../util/Logger";
import { RateLimitConstructorList, RateLimitList } from "./ratelimit/config";
@ -114,7 +115,7 @@ export class Socket extends EventEmitter {
this.desiredChannel.set = set;
let channel;
for (const ch of channelList) {
for (const ch of ChannelList.getList()) {
if (ch.getID() == _id) {
channel = ch;
}
@ -138,21 +139,25 @@ export class Socket extends EventEmitter {
);
channel.join(this);
}
}
// TODO Give the crown upon joining
}
}
public admin = new EventEmitter();
private bindEventListeners() {
for (const group of eventGroups) {
if (group.id == "admin") {
for (const event of group.eventList) {
this.admin.on(event.id, event.callback);
}
} else {
// TODO Check event group permissions
if (group.id == "admin") continue;
for (const event of group.eventList) {
this.on(event.id, event.callback);
}
}
}
}
public sendArray<EventID extends keyof ClientEvents>(
arr: ClientEvents[EventID][]
@ -198,6 +203,23 @@ export class Socket extends EventEmitter {
}
}
public async setUserFlag(key: keyof UserFlags, value: unknown) {
if (this.user) {
try {
const flags = JSON.parse(this.user.flags) as Partial<UserFlags>;
if (!flags) return false;
(flags as unknown as Record<string, unknown>)[key] = value;
this.user.flags = JSON.stringify(flags);
await updateUser(this.user.id, this.user);
return true;
} catch (err) {
return false;
}
} else {
return false;
}
}
public getParticipant() {
if (this.user) {
const flags = this.getUserFlags();
@ -226,7 +248,7 @@ export class Socket extends EventEmitter {
// Socket was closed or should be closed, clear data
// logger.debug("Destroying UID:", this._id);
const foundCh = channelList.find(
const foundCh = ChannelList.getList().find(
ch => ch.getID() === this.currentChannelID
);
@ -290,10 +312,12 @@ export class Socket extends EventEmitter {
}
public getCurrentChannel() {
return channelList.find(ch => ch.getID() == this.currentChannelID);
return ChannelList.getList().find(
ch => ch.getID() == this.currentChannelID
);
}
public sendChannelUpdate(ch: ChannelInfo, ppl: Participant[]) {
public sendChannelUpdate(ch: IChannelInfo, ppl: Participant[]) {
this.sendArray([
{
m: "ch",
@ -370,6 +394,8 @@ export class Socket extends EventEmitter {
if (!ch) return;
ch.playNotes(msg, this);
}
public subscribeToChannelList() {}
}
export const socketsBySocketID = new Map<string, Socket>();
@ -380,11 +406,15 @@ export function findSocketByPartID(id: string) {
}
}
export function findSocketByUserID(_id: string) {
export function findSocketsByUserID(_id: string) {
const sockets = [];
for (const socket of socketsBySocketID.values()) {
// logger.debug("User ID:", socket.getUserID());
if (socket.getUserID() == _id) return socket;
if (socket.getUserID() == _id) sockets.push(socket);
}
return sockets;
}
export function findSocketByIP(ip: string) {

View File

@ -0,0 +1,34 @@
import { readUser, updateUser } from "../../../../data/user";
import { ServerEventListener } from "../../../../util/types";
import { findSocketsByUserID } from "../../../Socket";
export const user_flag: ServerEventListener<"user_flag"> = {
id: "user_flag",
callback: async (msg, socket) => {
if (typeof msg._id !== "string") return;
if (typeof msg.key !== "string") return;
if (typeof msg.value == "undefined") return;
socket.getCurrentChannel()?.logger.debug(msg);
// Find the user data we're modifying
const user = await readUser(msg._id);
if (!user) return;
// Set the flag
const flags = JSON.parse(user.flags);
flags[msg.key] = msg.value;
user.flags = JSON.stringify(flags);
// Save the user data
await updateUser(user.id, user);
// Update this data for loaded users as well
const socks = findSocketsByUserID(user.id);
socks.forEach(sock => {
sock.setUserFlag(msg.key, msg.value);
});
socket.getCurrentChannel()?.logger.debug("socks:", socks);
}
};

View File

@ -1,9 +1,11 @@
import { EventGroup, eventGroups } from "../../events";
export const EVENT_GROUP_ADMIN = new EventGroup("user");
export const EVENT_GROUP_ADMIN = new EventGroup("admin");
import { color } from "./handlers/color";
import { user_flag } from "./handlers/user_flag";
EVENT_GROUP_ADMIN.add(color);
EVENT_GROUP_ADMIN.add(user_flag);
eventGroups.push(EVENT_GROUP_ADMIN);

View File

@ -0,0 +1,8 @@
import { ServerEventListener } from "../../../../util/types";
export const plus_ls: ServerEventListener<"+ls"> = {
id: "+ls",
callback: (msg, socket) => {
socket.subscribeToChannelList();
}
};

View File

@ -4,6 +4,10 @@ export const a: ServerEventListener<"a"> = {
id: "a",
callback: (msg, socket) => {
// Chat message
const flags = socket.getUserFlags();
if (!flags) return;
if (!flags["no chat rate limit"] || flags["no chat rate limit"] == 0)
if (!socket.rateLimits?.normal.a.attempt()) return;
const ch = socket.getCurrentChannel();
if (!ch) return;

View File

@ -0,0 +1,12 @@
import env from "../../../../util/env";
import { ServerEventListener } from "../../../../util/types";
import { config } from "../../../usersConfig";
export const admin_message: ServerEventListener<"admin message"> = {
id: "admin message",
callback: (msg, socket) => {
if (typeof msg.password !== "string") return;
if (msg.password !== env.ADMIN_PASS) return;
socket.admin.emit(msg.msg.m, msg.msg, socket, true);
}
};