/*Canopy - The next generation of stoner streaming software Copyright (C) 2024-2025 Rainbownapkin and the TTN Community This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //local imports const config = require('../../../config.json'); const channelModel = require('../../schemas/channel/channelSchema'); const permissionModel = require('../../schemas/permissionSchema'); const flairModel = require('../../schemas/flairSchema'); const emoteModel = require('../../schemas/emoteSchema'); const { userModel } = require('../../schemas/user/userSchema'); /** * Class representing a single user connected to a channel */ class connectedUser{ /** * Instantiates a connectedUser object * @param {Mongoose.Document} userDB - User document to re-hydrate user from * @param {PemissionModel.chanRank} chanRank - Enum representing user channel rank * @param {String} - Channel the user is connecting to * @param {Socket} socket - Socket associated with the users connection */ constructor(userDB, chanRank, channel, socket){ /** * User ID Number */ this.id = userDB.id; /** * User Name */ this.user = userDB.user; /** * User Rank */ this.rank = userDB.rank; /** * User High-Level */ this.highLevel = userDB.highLevel; //Check to make sure users flair entry from DB is good if(userDB.flair != null){ //Set flair from DB /** * User Flair */ this.flair = userDB.flair.name; //Otherwise }else{ //Gracefully default to classic /** * User Flair */ this.flair = 'classic'; } /** * User Channel-Rank */ this.chanRank = chanRank; /** * Connected Channel */ this.channel = channel; /** * List of active sockets to current channel */ this.sockets = [socket.id]; } /** * Handles server-side initialization for new connections from a specific user * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access * @param {Socket} socket - Requesting Socket * @returns {activeUser} active user object generated by the new connection */ async handleConnection(userDB, chanDB, socket){ //send metadata to client this.sendClientMetadata(); //Send out emotes this.sendSiteEmotes(); this.sendChanEmotes(chanDB); this.sendPersonalEmotes(userDB); //Send out used tokes this.sendUsedTokes(userDB); //Send out the currently playing item this.channel.queue.sendMedia(socket); //If we're proxied if(config.proxied){ //Tattoo hashed IP address from reverse proxy to user account for seven days await userDB.tattooIPRecord(socket.handshake.headers['x-forwarded-for']); }else{ //Tattoo hashed IP address to user account for seven days await userDB.tattooIPRecord(socket.handshake.address); } //Return active user object for use by activeChannel and channelManager objects return this; } /** * Iterates through all known connections for a given user, running them through a supplied callback function * @param {Function} cb - Callback to call against found sockets for a given user */ socketCrawl(cb){ //Crawl through user's sockets (lol) this.sockets.forEach((sockid) => { //get socket based on ID const socket = this.channel.server.io.sockets.sockets.get(sockid); //Callback with socket cb(socket); }); } /** * Emits an event to all known sockets for a given user * * My brain keeps going back to using dynamic per-user namespaces for this * but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces * and using per-user channels for this, but what of gold or mod-only features? or games? * No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). * at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. * Having to crawl through these sockets is that. Because the other ways seem more gross somehow. * @param {String} eventName - Event name to emit to client sockets * @param {Object} data - Data to emit to client sockets */ emit(eventName, data){ this.socketCrawl((socket)=>{ //Ensure our socket is initialized if(socket != null){ socket.emit(eventName, data); } }); } /** * Disconnects all sockets for a given user * @param {String} reason - Reason for being disconnected * @param {String} type - Disconnection Type */ disconnect(reason, type = "Disconnected"){ this.emit("kick",{type, reason}); this.socketCrawl((socket)=>{socket.disconnect()}); } //This is the big first push upon connection //It should only fire once, so things that only need to be sent once can be slapped into here /** * Sends glut of required initial metadata to the client upon a new connection * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access */ async sendClientMetadata(userDB, chanDB){ //Get flairList from DB and setup flairList array const flairListDB = await flairModel.find({}); var flairList = []; //if we wherent handed a user document if(userDB == null){ //Pull it based on user name userDB = await userModel.findOne({user: this.user}); } //if we wherent handed a channel document if(chanDB == null){ //Pull it based on channel name chanDB = await channelModel.findOne({name: this.channel.name}); } //If our perm map is un-initiated //can't set this in constructor easily since it's asyncornous //need to wait for it to complete before sending this off, but shouldnt re-do the wait for later connections if(this.permMap == null){ //Grab perm map this.permMap = await chanDB.getPermMapByUserDoc(userDB); } //Setup our userObj const userObj = { id: this.id, user: this.user, rank: this.rank, chanRank: this.chanRank, highLevel: this.highLevel, permMap: { site: Array.from(this.permMap.site), chan: Array.from(this.permMap.chan), }, flair: this.flair, } //For each flair listed in the Database flairListDB.forEach((flair)=>{ //Check if the user has permission to use the current flair if(permissionModel.rankToNum(flair.rank) <= permissionModel.rankToNum(this.rank)){ //If so push a light version of the flair object into our final flair list flairList.push({ name: flair.name, displayName: flair.displayName }); } }); //Get schedule lock status const queueLock = this.channel.queue.locked; //Get chat buffer const chatBuffer = this.channel.chatBuffer.buffer; //Send off the metadata to our user's clients this.emit("clientMetadata", {user: userObj, flairList, queueLock, chatBuffer}); } /** * Send copy of site emotes to the user */ async sendSiteEmotes(){ //Get emote list from DB const emoteList = await emoteModel.getEmotes(); //Send it off to the user this.emit('siteEmotes', emoteList); } /** * Send copy of channel emotes to the user * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access */ async sendChanEmotes(chanDB){ //if we wherent handed a channel document if(chanDB == null){ //Pull it based on channel name chanDB = await channelModel.findOne({name: this.channel.name}); } //Pull emotes from channel const emoteList = chanDB.getEmotes(); //Send it off to the user this.emit('chanEmotes', emoteList); } /** * Send copy of channel emotes to the user * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access */ async sendPersonalEmotes(userDB){ //if we wherent handed a user document if(userDB == null){ //Pull it based on user name userDB = await userModel.findOne({user: this.user}); } //Pull emotes from channel const emoteList = userDB.getEmotes(); //Send it off to the user this.emit('personalEmotes', emoteList); } /** * Send copy of channel emotes to the user * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access */ async sendUsedTokes(userDB){ //if we wherent handed a user document if(userDB == null){ //Pull it based on user name userDB = await userModel.findOne({user: this.user}); } //Create array of used toks from toke map and send it out to the user this.emit('usedTokes',{ tokes: Array.from(userDB.tokes.keys()) }); } /** * Set flair for a given user and broadcast update to clients * @param {String} flair - Flair string to update user's flair to */ updateFlair(flair){ this.flair = flair; this.channel.broadcastUserList(); this.sendClientMetadata(); } /** * Set high level for a given user and broadcast update to clients * @param {Number} highLevel - Number to update user's high-level to */ updateHighLevel(highLevel){ this.highLevel = highLevel; //TODO: show high-level in userlist this.channel.broadcastUserList(); this.sendClientMetadata(); } } module.exports = connectedUser;