diff --git a/config/users.yml b/config/users.yml index 9fa72ec..50beb07 100644 --- a/config/users.yml +++ b/config/users.yml @@ -2,3 +2,4 @@ defaultName: "Anonymous" defaultFlags: volume: 100 enableColorChanging: false +enableCustomNoteData: true diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index bce64b7..6ce61e8 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -103,6 +103,7 @@ export class Channel extends EventEmitter { set: Partial, admin: boolean = false ) { + if (this.isDestroyed()) return; if (!admin) { if (set.lobby) set.lobby = undefined; if (set.owner_id) set.owner_id = undefined; @@ -125,6 +126,7 @@ export class Channel extends EventEmitter { } public join(socket: Socket) { + if (this.isDestroyed()) return; const part = socket.getParticipant() as Participant; // Unknown side-effects, but for type safety... @@ -289,6 +291,7 @@ export class Channel extends EventEmitter { this.alreadyBound = true; this.on("update", () => { + // Send updated info for (const socket of socketsBySocketID.values()) { for (const p of this.ppl) { if (socket.getParticipantID() == p.id) { @@ -299,6 +302,10 @@ export class Channel extends EventEmitter { } } } + + if (this.ppl.length == 0) { + this.destroy(); + } }); this.on("message", (msg: ServerEvents["a"], socket: Socket) => { @@ -315,6 +322,53 @@ export class Channel extends EventEmitter { this.chatHistory.push(outgoing); }); } + + public playNotes(msg: ServerEvents["n"], socket: Socket) { + if (this.isDestroyed()) return; + const part = socket.getParticipant(); + if (!part) return; + + let clientMsg: ClientEvents["n"] = { + m: "n", + n: msg.n, + t: msg.t, + p: part.id + }; + + let sentSocketIDs = new Array(); + + for (const p of this.ppl) { + socketLoop: for (const socket of socketsBySocketID.values()) { + if (socket.isDestroyed()) continue socketLoop; + if (socket.getParticipantID() != p.id) continue socketLoop; + if (socket.getParticipantID() == part.id) continue socketLoop; + if (sentSocketIDs.includes(socket.socketID)) + continue socketLoop; + socket.sendArray([clientMsg]); + sentSocketIDs.push(socket.socketID); + } + } + } + + private destroyed = false; + + public destroy() { + if (this.destroyed) return; + this.destroyed = true; + + if (this.ppl.length > 0) { + for (const socket of socketsBySocketID.values()) { + if (socket.currentChannelID !== this.getID()) continue; + socket.setChannel(config.fullChannel); + } + } + + channelList.splice(channelList.indexOf(this), 1); + } + + public isDestroyed() { + return this.destroyed == true; + } } // Forceloader diff --git a/src/util/config.ts b/src/util/config.ts index ad74b94..a015fe1 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -1,11 +1,10 @@ import YAML from "yaml"; -import { readFileSync } from "fs"; +import fs from "fs"; export function loadConfig(filepath: string, def: T) { try { - const data = readFileSync(filepath).toString(); + const data = fs.readFileSync(filepath).toString(); const parsed = YAML.parse(data); - return parsed as T; } catch (err) { console.error("Unable to load config:", err); diff --git a/src/util/types.d.ts b/src/util/types.d.ts index e546fce..6982519 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -313,7 +313,7 @@ declare interface ClientEvents { t: { m: "t"; t: number; - e: number; + e: number | undefined; }; bye: { diff --git a/src/ws/Gateway.ts b/src/ws/Gateway.ts index 6069a3b..fbe6fc2 100644 --- a/src/ws/Gateway.ts +++ b/src/ws/Gateway.ts @@ -1,4 +1,5 @@ export class Gateway { public hasProcessedHi: boolean = false; public hasSentDevices: boolean = false; + public lastPing: number = Date.now(); } diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index 9c234a4..1560257 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -5,12 +5,12 @@ import { ChannelSettings, ClientEvents, Participant, + ServerEvents, UserFlags } from "../util/types"; import { User } from "@prisma/client"; import { createUser, readUser, updateUser } from "../data/user"; import { eventGroups } from "./events"; -import { loadConfig } from "../util/config"; import { Gateway } from "./Gateway"; import { Channel, channelList } from "../channel/Channel"; import { ServerWebSocket } from "bun"; @@ -20,20 +20,7 @@ import { RateLimitConstructorList, RateLimitList } from "./ratelimit/config"; import { adminLimits } from "./ratelimit/limits/admin"; import { userLimits } from "./ratelimit/limits/user"; import { NoteQuota } from "./ratelimit/NoteQuota"; - -interface UsersConfig { - defaultName: string; - defaultFlags: UserFlags; - enableColorChanging: boolean; -} - -const usersConfig = loadConfig("config/users.yml", { - defaultName: "Anonymous", - defaultFlags: { - volume: 100 - }, - enableColorChanging: false -}); +import { config } from "./usersConfig"; const logger = new Logger("Sockets"); @@ -46,7 +33,7 @@ export class Socket extends EventEmitter { public gateway = new Gateway(); public rateLimits: RateLimitList | undefined; - public noteQuota = new NoteQuota(this.onQuota); + public noteQuota = new NoteQuota(); public desiredChannel: { _id: string | undefined; @@ -176,9 +163,9 @@ export class Socket extends EventEmitter { if (!user) { await createUser( this._id, - usersConfig.defaultName, + config.defaultName, createColor(this.ip), - usersConfig.defaultFlags + config.defaultFlags ); user = await readUser(this._id); @@ -316,7 +303,7 @@ export class Socket extends EventEmitter { public async userset(name?: string, color?: string) { let isColor = false; - if (color && usersConfig.enableColorChanging) { + if (color && config.enableColorChanging) { isColor = typeof color === "string" && !!color.match(/^#[0-9a-f]{6}$/i); } @@ -373,5 +360,9 @@ export class Socket extends EventEmitter { ]); } - public onQuota(points: number) {} + public playNotes(msg: ServerEvents["n"]) { + const ch = this.getCurrentChannel(); + if (!ch) return; + ch.playNotes(msg, this); + } } diff --git a/src/ws/events/user/handlers/n.ts b/src/ws/events/user/handlers/n.ts new file mode 100644 index 0000000..125551d --- /dev/null +++ b/src/ws/events/user/handlers/n.ts @@ -0,0 +1,39 @@ +import { ServerEventListener } from "../../../../util/types"; +import { config } from "../../../usersConfig"; + +export const n: ServerEventListener<"n"> = { + id: "n", + callback: (msg, socket) => { + // Piano note + if (!Array.isArray(msg.n)) return; + if (typeof msg.t !== "number") return; + + // Check note properties + for (const n of msg.n) { + if (typeof n.n != "string") return; + + // TODO Check for config.enableCustomNoteData here + // For whatever reason, Bun likes to crash when we access that config object + continue; + + if (n.s) { + if (typeof n.s !== "number") return; + if (n.s == 1) { + if (typeof n.d !== "number") return; + if (n.d < 0 || n.d > 200) return; + + if (typeof n.v !== "number") return; + if (n.v < 0) n.v = 0; + if (n.v > 1) n.v = 1; + } + } + } + + let amount = msg.n.length; + + // TODO Check crownsolo + if (socket.noteQuota.spend(amount)) { + socket.playNotes(msg); + } + } +}; diff --git a/src/ws/events/user/handlers/t.ts b/src/ws/events/user/handlers/t.ts new file mode 100644 index 0000000..3bbb7d9 --- /dev/null +++ b/src/ws/events/user/handlers/t.ts @@ -0,0 +1,19 @@ +import { ServerEventListener } from "../../../../util/types"; + +export const t: ServerEventListener<"t"> = { + id: "t", + callback: (msg, socket) => { + // Ping + if (msg.e) { + if (typeof msg.e !== "number") return; + } + + socket.sendArray([ + { + m: "t", + t: Date.now(), + e: typeof msg.e == "number" ? msg.e : undefined + } + ]); + } +}; diff --git a/src/ws/events/user/index.ts b/src/ws/events/user/index.ts index 4ae03e6..8536653 100644 --- a/src/ws/events/user/index.ts +++ b/src/ws/events/user/index.ts @@ -8,6 +8,7 @@ import { ch } from "./handlers/ch"; import { m } from "./handlers/m"; import { a } from "./handlers/a"; import { userset } from "./handlers/userset"; +import { n } from "./handlers/n"; EVENTGROUP_USER.add(hi); EVENTGROUP_USER.add(devices); @@ -15,5 +16,6 @@ EVENTGROUP_USER.add(ch); EVENTGROUP_USER.add(m); EVENTGROUP_USER.add(a); EVENTGROUP_USER.add(userset); +EVENTGROUP_USER.add(n); eventGroups.push(EVENTGROUP_USER); diff --git a/src/ws/ratelimit/NoteQuota.ts b/src/ws/ratelimit/NoteQuota.ts index 687dd36..834ae33 100644 --- a/src/ws/ratelimit/NoteQuota.ts +++ b/src/ws/ratelimit/NoteQuota.ts @@ -14,7 +14,7 @@ export class NoteQuota { maxHistLen: 3 }; - constructor(public cb: (points: number) => void) { + constructor(public cb?: (points: number) => void) { this.setParams(); this.resetPoints(); } diff --git a/src/ws/usersConfig.ts b/src/ws/usersConfig.ts new file mode 100644 index 0000000..d29a293 --- /dev/null +++ b/src/ws/usersConfig.ts @@ -0,0 +1,26 @@ +import { loadConfig } from "../util/config"; +import { UserFlags } from "../util/types"; + +export interface UsersConfig { + defaultName: string; + defaultFlags: UserFlags; + enableColorChanging: boolean; + enableCustomNoteData: boolean; +} + +export const usersConfigPath = "config/users.yml"; + +export const defaultUsersConfig = { + defaultName: "Anonymous", + defaultFlags: { + volume: 100 + }, + enableColorChanging: false, + enableCustomNoteData: true +}; + +// Importing this elsewhere causes bun to segfault +export const config = loadConfig( + usersConfigPath, + defaultUsersConfig +);