/*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'); module.exports = class{ constructor(userDB, chanRank, channel, socket){ this.id = userDB.id; this.user = userDB.user; this.rank = userDB.rank; this.highLevel = userDB.highLevel; //Check to make sure users flair entry from DB is good if(userDB.flair != null){ //Use flair from DB this.flair = userDB.flair.name; //Otherwise }else{ //Gracefully default to classic this.flair = 'classic'; } this.chanRank = chanRank; this.channel = channel; this.sockets = [socket.id]; } 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); } } 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); }); } //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. emit(eventName, args){ this.socketCrawl((socket)=>{ //Ensure our socket is initialized if(socket != null){ socket.emit(eventName, args); } }); } //generic disconnect function, defaults to kick 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 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 as a temporary array const queue = await this.channel.queue.prepQueue(chanDB); //Get schedule lock status const queueLock = this.channel.queue.locked; //Get chat buffer const chatBuffer = this.channel.chatBuffer; //Send off the metadata to our user's clients this.emit("clientMetadata", {user: userObj, flairList, queue, queueLock, chatBuffer}); } async sendSiteEmotes(){ //Get emote list from DB const emoteList = await emoteModel.getEmotes(); //Send it off to the user this.emit('siteEmotes', emoteList); } 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); } 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); } 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()) }); } updateFlair(flair){ this.flair = flair; this.channel.broadcastUserList(); this.sendClientMetadata(); } updateHighLevel(highLevel){ this.highLevel = highLevel; //TODO: show high-level in userlist this.channel.broadcastUserList(); this.sendClientMetadata(); } }