diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index fbd9c3f..32a57cb 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -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("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(); - export class Channel extends EventEmitter { private settings: Partial = config.defaultSettings; private ppl = new Array(); @@ -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)) { - const part = creator.getParticipant(); - if (part) this.giveCrown(part); - } + // 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); } diff --git a/src/channel/ChannelList.ts b/src/channel/ChannelList.ts new file mode 100644 index 0000000..f7df01e --- /dev/null +++ b/src/channel/ChannelList.ts @@ -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(); + public static subscribers = new Array(); + + 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; + } +} diff --git a/src/channel/settings.ts b/src/channel/settings.ts index edff61b..9fdb584 100644 --- a/src/channel/settings.ts +++ b/src/channel/settings.ts @@ -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 = { +const validationRecord: Record = { // Brandon lobby: "boolean", visible: "boolean", @@ -25,13 +26,12 @@ const validationRecord: Record = { /** * 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) { +export function validateChannelSettings(set: Partial) { // Create record - let keys = Object.keys(validationRecord); - let record: Partial> = {}; + let record: Partial> = {}; for (const key of Object.keys(set)) { let val = (set as Record)[key]; @@ -46,7 +46,7 @@ export function validateChannelSettings(set: Partial) { } // Set valid status - record[key as keyof ChannelSettings] = validate(val, validator); + record[key as keyof IChannelSettings] = validate(val, validator); } return record; diff --git a/src/index.ts b/src/index.ts index 9202a72..62055a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,5 @@ import "./ws/server"; import { Logger } from "./util/Logger"; const logger = new Logger("Main"); + +import "./util/readline"; diff --git a/src/util/Logger.ts b/src/util/Logger.ts index e252588..1a568a8 100755 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -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 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() { diff --git a/src/util/readline.ts b/src/util/readline.ts index 69b2e80..ae7f87f 100644 --- a/src/util/readline.ts +++ b/src/util/readline.ts @@ -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; diff --git a/src/util/types.d.ts b/src/util/types.d.ts index 9cabef3..a7a5b42 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -91,15 +91,6 @@ declare interface Crown { endPos: Vector2; } -declare interface ChannelInfo { - banned?: boolean; - count: number; - id: string; - _id: string; - crown?: Crown; - settings: Partial; -} - // 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; + }; + // 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; diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index 0cf57a5..29ffaae 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -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,18 +139,22 @@ 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) { - // TODO Check event group permissions - if (group.id == "admin") continue; - - for (const event of group.eventList) { - this.on(event.id, event.callback); + if (group.id == "admin") { + for (const event of group.eventList) { + this.admin.on(event.id, event.callback); + } + } else { + // TODO Check event group permissions + for (const event of group.eventList) { + this.on(event.id, event.callback); + } } } } @@ -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; + if (!flags) return false; + (flags as unknown as Record)[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(); @@ -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) { diff --git a/src/ws/events/admin/handlers/user_flag.ts b/src/ws/events/admin/handlers/user_flag.ts new file mode 100644 index 0000000..d605128 --- /dev/null +++ b/src/ws/events/admin/handlers/user_flag.ts @@ -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); + } +}; diff --git a/src/ws/events/admin/index.ts b/src/ws/events/admin/index.ts index ca0fb14..d06d284 100644 --- a/src/ws/events/admin/index.ts +++ b/src/ws/events/admin/index.ts @@ -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); diff --git a/src/ws/events/user/handlers/+ls.ts b/src/ws/events/user/handlers/+ls.ts new file mode 100644 index 0000000..d804c88 --- /dev/null +++ b/src/ws/events/user/handlers/+ls.ts @@ -0,0 +1,8 @@ +import { ServerEventListener } from "../../../../util/types"; + +export const plus_ls: ServerEventListener<"+ls"> = { + id: "+ls", + callback: (msg, socket) => { + socket.subscribeToChannelList(); + } +}; diff --git a/src/ws/events/user/handlers/a.ts b/src/ws/events/user/handlers/a.ts index 74f74ca..0b9f7df 100644 --- a/src/ws/events/user/handlers/a.ts +++ b/src/ws/events/user/handlers/a.ts @@ -4,7 +4,11 @@ export const a: ServerEventListener<"a"> = { id: "a", callback: (msg, socket) => { // Chat message - if (!socket.rateLimits?.normal.a.attempt()) return; + 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; diff --git a/src/ws/events/user/handlers/admin_message.ts b/src/ws/events/user/handlers/admin_message.ts new file mode 100644 index 0000000..d00a654 --- /dev/null +++ b/src/ws/events/user/handlers/admin_message.ts @@ -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); + } +};