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 { validateChannelSettings } from "./settings";
import { findSocketByPartID, socketsBySocketID } from "../ws/Socket"; import { findSocketByPartID, socketsBySocketID } from "../ws/Socket";
import Crown from "./Crown"; import Crown from "./Crown";
import { ChannelList } from "./ChannelList";
interface ChannelConfig { interface ChannelConfig {
forceLoad: string[]; forceLoad: string[];
@ -40,14 +41,11 @@ export const config = loadConfig<ChannelConfig>("config/channels.yml", {
color2: "#001014", color2: "#001014",
visible: true visible: true
}, },
// TODO Test this regex lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[1-9]$", "^test/.+$"],
lobbyRegexes: ["^lobby[1-9]?[1-9]?$", "^test/.+$"],
lobbyBackdoor: "lolwutsecretlobbybackdoor", lobbyBackdoor: "lolwutsecretlobbybackdoor",
fullChannel: "test/awkward" fullChannel: "test/awkward"
}); });
export const channelList = new Array<Channel>();
export class Channel extends EventEmitter { export class Channel extends EventEmitter {
private settings: Partial<IChannelSettings> = config.defaultSettings; private settings: Partial<IChannelSettings> = config.defaultSettings;
private ppl = new Array<Participant>(); private ppl = new Array<Participant>();
@ -75,9 +73,8 @@ export class Channel extends EventEmitter {
if (set) { if (set) {
const validatedSet = validateChannelSettings(set); const validatedSet = validateChannelSettings(set);
for (const key in Object.keys(validatedSet)) { for (const key of Object.keys(set)) {
if (!(validatedSet as any)[key]) continue; if ((validatedSet as any)[key] === false) continue;
(this.settings as any)[key] = (set as any)[key]; (this.settings as any)[key] = (set as any)[key];
} }
} }
@ -85,10 +82,10 @@ export class Channel extends EventEmitter {
this.crown = new Crown(); this.crown = new Crown();
if (creator) { if (creator) {
if (this.crown.canBeSetBy(creator)) { // if (this.crown.canBeSetBy(creator)) {
const part = creator.getParticipant(); const part = creator.getParticipant();
if (part) this.giveCrown(part); if (part) this.giveCrown(part);
} // }
} }
} }
@ -98,8 +95,10 @@ export class Channel extends EventEmitter {
this.bindEventListeners(); this.bindEventListeners();
channelList.push(this); ChannelList.add(this);
// TODO channel closing // TODO channel closing
this.logger.info("Created");
} }
private alreadyBound = false; private alreadyBound = false;
@ -264,7 +263,7 @@ export class Channel extends EventEmitter {
if (hasChangedChannel) { if (hasChangedChannel) {
if (socket.currentChannelID) { if (socket.currentChannelID) {
const ch = channelList.find( const ch = ChannelList.getList().find(
ch => ch._id == socket.currentChannelID ch => ch._id == socket.currentChannelID
); );
if (ch) { 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) { 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 // Brandon
lobby: "boolean", lobby: "boolean",
visible: "boolean", visible: "boolean",
@ -25,13 +26,12 @@ const validationRecord: Record<keyof ChannelSettings, Validator> = {
/** /**
* Check the validity of channel settings * Check the validity of channel settings
* @param set Unknown data * @param set Dirty settings
* @returns Record of which settings are correct * @returns Record of which settings are correct
*/ */
export function validateChannelSettings(set: Partial<ChannelSettings>) { export function validateChannelSettings(set: Partial<IChannelSettings>) {
// Create record // Create record
let keys = Object.keys(validationRecord); let record: Partial<Record<keyof IChannelSettings, boolean>> = {};
let record: Partial<Record<keyof ChannelSettings, boolean>> = {};
for (const key of Object.keys(set)) { for (const key of Object.keys(set)) {
let val = (set as Record<string, any>)[key]; let val = (set as Record<string, any>)[key];
@ -46,7 +46,7 @@ export function validateChannelSettings(set: Partial<ChannelSettings>) {
} }
// Set valid status // Set valid status
record[key as keyof ChannelSettings] = validate(val, validator); record[key as keyof IChannelSettings] = validate(val, validator);
} }
return record; return record;

View File

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

View File

@ -2,6 +2,10 @@ import { padNum, unimportant } from "./helpers";
export class Logger { export class Logger {
private static log(method: string, ...args: any[]) { 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>)[ (console as unknown as Record<string, (..._args: any[]) => any>)[
method method
]( ](
@ -9,6 +13,10 @@ export class Logger {
unimportant(this.getHHMMSSMS()), unimportant(this.getHHMMSSMS()),
...args ...args
); );
// Fix the readline prompt (spooky code)
if ((globalThis as unknown as any).rl)
(globalThis as unknown as any).rl.prompt();
} }
public static getHHMMSSMS() { public static getHHMMSSMS() {

View File

@ -1,19 +1,35 @@
import readline from "readline"; import readline from "readline";
import { Logger } from "./Logger";
export const rl = readline.createInterface({ export const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}); });
rl.setPrompt("mpps> "); const logger = new Logger("CLI");
rl.setPrompt("mpps> ");
rl.prompt(); rl.prompt();
rl.on("line", msg => { rl.on("line", msg => {
// TODO readline commands // 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.prompt();
}); });
rl.on("SIGINT", () => { rl.on("SIGINT", () => {
process.exit(); 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; 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 // Events copied from Hri7566/mppclone-client typedefs
declare interface ServerEvents { declare interface ServerEvents {
a: { a: {
@ -200,6 +191,12 @@ declare interface ServerEvents {
set: { name?: string; color?: string }; set: { name?: string; color?: string };
}; };
"admin message": {
m: "admin message";
password: string;
msg: ServerEvents<keyof ServerEvents>;
};
// Admin // Admin
color: { color: {
@ -243,7 +240,7 @@ declare interface ClientEvents {
ch: { ch: {
m: "ch"; m: "ch";
p: string; p: string;
ch: ChannelInfo; ch: IChannelInfo;
ppl: Participant[]; ppl: Participant[];
}; };
@ -342,6 +339,7 @@ declare interface ICrown {
} }
declare interface IChannelInfo { declare interface IChannelInfo {
banned?: boolean;
_id: string; _id: string;
id: string; id: string;
count: number; count: number;

View File

@ -7,7 +7,7 @@
import { createColor, createID, createUserID } from "../util/id"; import { createColor, createID, createUserID } from "../util/id";
import EventEmitter from "events"; import EventEmitter from "events";
import { import {
ChannelInfo, IChannelInfo,
IChannelSettings, IChannelSettings,
ClientEvents, ClientEvents,
Participant, Participant,
@ -19,7 +19,8 @@ import { User } from "@prisma/client";
import { createUser, readUser, updateUser } from "../data/user"; import { createUser, readUser, updateUser } from "../data/user";
import { eventGroups } from "./events"; import { eventGroups } from "./events";
import { Gateway } from "./Gateway"; 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 { ServerWebSocket } from "bun";
import { Logger } from "../util/Logger"; import { Logger } from "../util/Logger";
import { RateLimitConstructorList, RateLimitList } from "./ratelimit/config"; import { RateLimitConstructorList, RateLimitList } from "./ratelimit/config";
@ -114,7 +115,7 @@ export class Socket extends EventEmitter {
this.desiredChannel.set = set; this.desiredChannel.set = set;
let channel; let channel;
for (const ch of channelList) { for (const ch of ChannelList.getList()) {
if (ch.getID() == _id) { if (ch.getID() == _id) {
channel = ch; channel = ch;
} }
@ -138,21 +139,25 @@ export class Socket extends EventEmitter {
); );
channel.join(this); channel.join(this);
}
}
// TODO Give the crown upon joining public admin = new EventEmitter();
}
}
private bindEventListeners() { private bindEventListeners() {
for (const group of eventGroups) { 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 // TODO Check event group permissions
if (group.id == "admin") continue;
for (const event of group.eventList) { for (const event of group.eventList) {
this.on(event.id, event.callback); this.on(event.id, event.callback);
} }
} }
} }
}
public sendArray<EventID extends keyof ClientEvents>( public sendArray<EventID extends keyof ClientEvents>(
arr: ClientEvents[EventID][] 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() { public getParticipant() {
if (this.user) { if (this.user) {
const flags = this.getUserFlags(); const flags = this.getUserFlags();
@ -226,7 +248,7 @@ export class Socket extends EventEmitter {
// Socket was closed or should be closed, clear data // Socket was closed or should be closed, clear data
// logger.debug("Destroying UID:", this._id); // logger.debug("Destroying UID:", this._id);
const foundCh = channelList.find( const foundCh = ChannelList.getList().find(
ch => ch.getID() === this.currentChannelID ch => ch.getID() === this.currentChannelID
); );
@ -290,10 +312,12 @@ export class Socket extends EventEmitter {
} }
public getCurrentChannel() { 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([ this.sendArray([
{ {
m: "ch", m: "ch",
@ -370,6 +394,8 @@ export class Socket extends EventEmitter {
if (!ch) return; if (!ch) return;
ch.playNotes(msg, this); ch.playNotes(msg, this);
} }
public subscribeToChannelList() {}
} }
export const socketsBySocketID = new Map<string, Socket>(); 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()) { for (const socket of socketsBySocketID.values()) {
// logger.debug("User ID:", socket.getUserID()); // 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) { 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"; 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 { color } from "./handlers/color";
import { user_flag } from "./handlers/user_flag";
EVENT_GROUP_ADMIN.add(color); EVENT_GROUP_ADMIN.add(color);
EVENT_GROUP_ADMIN.add(user_flag);
eventGroups.push(EVENT_GROUP_ADMIN); 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", id: "a",
callback: (msg, socket) => { callback: (msg, socket) => {
// Chat message // 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; if (!socket.rateLimits?.normal.a.attempt()) return;
const ch = socket.getCurrentChannel(); const ch = socket.getCurrentChannel();
if (!ch) return; 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);
}
};