From 6c39c3405b1feb136b0e35a1c10135c98c1a6652 Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Wed, 25 Oct 2023 00:00:20 -0400 Subject: [PATCH] Organize Channel.ts, add comments, and implement crown --- src/channel/Channel.ts | 252 +++++++++++++++++++++++++++++++++-------- src/channel/Crown.ts | 41 +++++++ src/channel/index.ts | 7 ++ 3 files changed, 253 insertions(+), 47 deletions(-) create mode 100644 src/channel/Crown.ts create mode 100644 src/channel/index.ts diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index 6ce61e8..9c66671 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -11,6 +11,7 @@ import { import { Socket } from "../ws/Socket"; import { validateChannelSettings } from "./settings"; import { socketsBySocketID } from "../ws/server"; +import Crown from "./Crown"; interface ChannelConfig { forceLoad: string[]; @@ -54,8 +55,14 @@ export class Channel extends EventEmitter { public chatHistory = new Array(); // TODO Add the crown + public crown?: Crown; - constructor(private _id: string, set?: Partial) { + constructor( + private _id: string, + set?: Partial, + creator?: Socket, + owner_id?: string + ) { super(); this.logger = new Logger("Channel - " + _id); @@ -63,13 +70,24 @@ export class Channel extends EventEmitter { // Validate settings in set // Set the verified settings - if (set && !this.isLobby()) { - const validatedSet = validateChannelSettings(set); + if (!this.isLobby()) { + if (set) { + const validatedSet = validateChannelSettings(set); - for (const key in Object.keys(validatedSet)) { - if (!(validatedSet as any)[key]) continue; + for (const key in Object.keys(validatedSet)) { + if (!(validatedSet as any)[key]) continue; - (this.settings as any)[key] = (set as any)[key]; + (this.settings as any)[key] = (set as any)[key]; + } + } + + this.crown = new Crown(); + + if (creator) { + if (this.crown.canBeSetBy(creator)) { + const part = creator.getParticipant(); + if (part) this.giveCrown(part); + } } } @@ -83,10 +101,79 @@ export class Channel extends EventEmitter { // TODO channel closing } + private alreadyBound = false; + + private bindEventListeners() { + if (this.alreadyBound) return; + 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) { + socket.sendChannelUpdate( + this.getInfo(), + this.getParticipantList() + ); + } + } + } + + if (this.ppl.length == 0) { + this.destroy(); + } + }); + + this.on("message", (msg: ServerEvents["a"], socket: Socket) => { + if (!msg.message) return; + + const userFlags = socket.getUserFlags(); + + this.logger.debug(userFlags); + + if (userFlags) { + if (userFlags.cant_chat) return; + } + + // Sanitize + msg.message = msg.message + .replace(/\p{C}+/gu, "") + .replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1") + .trim(); + + let outgoing: ClientEvents["a"] = { + m: "a", + a: msg.message, + t: Date.now(), + p: socket.getParticipant() as Participant + }; + + this.sendArray([outgoing]); + this.chatHistory.push(outgoing); + + try { + if (msg.message.startsWith("/")) { + this.emit("command", msg, socket); + } + } catch (err) { + this.logger.debug(err); + } + }); + } + + /** + * Get this channel's ID (channel name) + * @returns Channel ID + */ public getID() { return this._id; } + /** + * Determine whether this channel is a lobby (uses regex from config) + * @returns Boolean + */ public isLobby() { for (const reg of config.lobbyRegexes) { let exp = new RegExp(reg, "g"); @@ -99,6 +186,12 @@ export class Channel extends EventEmitter { return false; } + /** + * Change this channel's settings + * @param set Channel settings + * @param admin Whether a user is changing the settings (set to true to force the changes) + * @returns undefined + */ public changeSettings( set: Partial, admin: boolean = false @@ -125,6 +218,20 @@ export class Channel extends EventEmitter { } } + /** + * Get a channel setting's value + * @param setting Channel setting to get + * @returns Value of setting + */ + public getSetting(setting: keyof ChannelSettings) { + return this.settings[setting]; + } + + /** + * Make a socket join this channel + * @param socket Socket that is joining + * @returns undefined + */ public join(socket: Socket) { if (this.isDestroyed()) return; const part = socket.getParticipant() as Participant; @@ -200,6 +307,10 @@ export class Channel extends EventEmitter { ]); } + /** + * Make a socket leave this channel + * @param socket Socket that is leaving + */ public leave(socket: Socket) { // this.logger.debug("Leave called"); const part = socket.getParticipant() as Participant; @@ -234,6 +345,10 @@ export class Channel extends EventEmitter { this.emit("update"); } + /** + * Determine whether this channel has too many users + * @returns Boolean + */ public isFull() { // TODO Use limit setting @@ -244,29 +359,52 @@ export class Channel extends EventEmitter { return false; } + /** + * Get this channel's information + * @returns Channel info object (includes ID, number of users, settings, and the crown) + */ public getInfo() { return { _id: this.getID(), id: this.getID(), count: this.ppl.length, - settings: this.settings + settings: this.settings, + crown: JSON.parse(JSON.stringify(this.crown)) }; } + /** + * Get the people in this channel + * @returns List of people + */ public getParticipantList() { return this.ppl; } + /** + * Determine whether a user is in this channel (by user ID) + * @param _id User ID + * @returns Boolean + */ public hasUser(_id: string) { const foundPart = this.ppl.find(p => p._id == _id); return !!foundPart; } + /** + * Determine whether a user is in this channel (by participant ID) + * @param id Participant ID + * @returns Boolean + */ public hasParticipant(id: string) { const foundPart = this.ppl.find(p => p.id == id); return !!foundPart; } + /** + * Send messages to everyone in this channel + * @param arr List of events to send to clients + */ public sendArray( arr: ClientEvents[EventID][] ) { @@ -284,45 +422,12 @@ export class Channel extends EventEmitter { } } - private alreadyBound = false; - - private bindEventListeners() { - if (this.alreadyBound) return; - 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) { - socket.sendChannelUpdate( - this.getInfo(), - this.getParticipantList() - ); - } - } - } - - if (this.ppl.length == 0) { - this.destroy(); - } - }); - - this.on("message", (msg: ServerEvents["a"], socket: Socket) => { - if (!msg.message) return; - - let outgoing: ClientEvents["a"] = { - m: "a", - a: msg.message, - t: Date.now(), - p: socket.getParticipant() as Participant - }; - - this.sendArray([outgoing]); - this.chatHistory.push(outgoing); - }); - } - + /** + * Play notes (usually from a socket) + * @param msg Note message + * @param socket Socket that is sending notes + * @returns undefined + */ public playNotes(msg: ServerEvents["n"], socket: Socket) { if (this.isDestroyed()) return; const part = socket.getParticipant(); @@ -352,6 +457,10 @@ export class Channel extends EventEmitter { private destroyed = false; + /** + * Set this channel to the destroyed state + * @returns undefined + */ public destroy() { if (this.destroyed) return; this.destroyed = true; @@ -366,12 +475,61 @@ export class Channel extends EventEmitter { channelList.splice(channelList.indexOf(this), 1); } + /** + * Determine whether the channel is in a destroyed state + * @returns Boolean + */ public isDestroyed() { return this.destroyed == true; } + + /** + * Change ownership (don't forget to use crown.canBeSetBy if you're letting a user call this) + * @param part Participant to give crown to (or undefined to drop crown) + */ + public chown(part?: Participant) { + if (this.crown) { + if (part) { + this.giveCrown(part); + } else { + this.dropCrown(); + } + } + } + + /** + * Give the crown to a user (no matter what) + * @param part Participant to give crown to + * @param force Whether or not to force-create a crown (useful for lobbies) + */ + public giveCrown(part: Participant, force?: boolean) { + if (force) { + if (!this.crown) this.crown = new Crown(); + } + + if (this.crown) { + this.crown.userId = part._id; + this.crown.participantId = part.id; + this.crown.time = Date.now(); + this.emit("update"); + } + } + + /** + * Drop the crown (remove from user) + */ + public dropCrown() { + if (this.crown) { + delete this.crown.participantId; + this.crown.time = Date.now(); + this.emit("update"); + } + } } -// Forceloader +export default Channel; + +// Channel forceloader (cringe) let hasFullChannel = false; for (const id of config.forceLoad) { diff --git a/src/channel/Crown.ts b/src/channel/Crown.ts new file mode 100644 index 0000000..e27434a --- /dev/null +++ b/src/channel/Crown.ts @@ -0,0 +1,41 @@ +import { Participant } from "../util/types"; +import { Socket } from "../ws/Socket"; + +export class Crown { + public userId: string | undefined; + public participantId: string | undefined; + public time: number = Date.now(); + + public canBeSetBy(socket: Socket) { + // can claim, drop, or give if... + const flags = socket.getUserFlags(); + + if (!flags) return false; + if (flags.cansetcrowns) return true; + + const channel = socket.getCurrentChannel(); + if (!channel) return false; + + const part = socket.getParticipant(); + if (!part) return false; + + if (!channel.getSetting("lobby")) { + // if there is no user (never been owned) + if (!this.userId) return true; + + // if you're the user (you dropped it or left the room, nobody has claimed it) + if (this.userId === part._id) return true; + + // if there is no participant and 15 seconds have passed + if (!this.participantId && this.time + 15000 < Date.now()) + return true; + + // you're the specially designated channel owner + if (channel.getSetting("owner_id") === part._id) return true; + } + + return false; + } +} + +export default Crown; diff --git a/src/channel/index.ts b/src/channel/index.ts new file mode 100644 index 0000000..2f9faa1 --- /dev/null +++ b/src/channel/index.ts @@ -0,0 +1,7 @@ +import Channel from "./Channel"; +import Crown from "./Crown"; +import validateChannelSettings, { + validate as validateChannelSetting +} from "./settings"; + +export { Channel, Crown, validateChannelSettings, validateChannelSetting };