Source: app/channel/activeChannel.js

/*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 <https://www.gnu.org/licenses/>.*/

//local imports
const connectedUser = require('./connectedUser');
const chatBuffer = require('./chatBuffer');
const queue = require('./media/queue');
const channelModel = require('../../schemas/channel/channelSchema');
const playlistHandler = require('./media/playlistHandler')

/**
 * Class representing a single active channel
 */
module.exports = class{

    /**
     * Instantiates an activeChannel object
     * @param {channelManager} server - Parent Server Object
     * @param {Mongoose.Document} chanDB  - chanDB to rehydrate buffer from
     */
    constructor(server, chanDB){
        this.server = server;
        this.name = chanDB.name;
        this.tokeCommands = chanDB.tokeCommands;
        //Keeping these in a map was originally a vestige but it's more preformant than an array or object so :P
        this.userList = new Map();
        this.queue = new queue(server, chanDB, this);
        this.playlistHandler = new playlistHandler(server, chanDB, this);
        //Define the chat buffer
        this.chatBuffer = new chatBuffer(server, chanDB, this);
    }


    /**
     * Handles server-side initialization for new connections to the channel
     * @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
     */
    async handleConnection(userDB, chanDB, socket){
        //get current user object from the userlist
        var userObj = this.userList.get(userDB.user);
        //get channel rank for current user
        const chanRank = await chanDB.getChannelRankByUserDoc(userDB);

        //If user is already connected
        if(userObj){
            //Add this socket on to the userobject
            userObj.sockets.push(socket.id);
        //If the user is joining the channel
        }else{
            //Grab flair
            await userDB.populate('flair');
            //Set user object
            userObj = new connectedUser(userDB, chanRank, this, socket);
        }

        //Set user entry in userlist
        this.userList.set(userDB.user, userObj);

        //if everything looks good, admit the connection to the channel
        socket.join(socket.chan);

        //Define per-channel event listeners
        this.queue.defineListeners(socket);
        this.playlistHandler.defineListeners(socket);

        //Hand off the connection initiation to it's user object
        await userObj.handleConnection(userDB, chanDB, socket)

        //Send out the userlist
        this.broadcastUserList(socket.chan);
    }

    /**
     * Handles server-side initialization for disconnecting from the channel
     * @param {Socket} socket - Requesting Socket
     */
    handleDisconnect(socket){
        //If we have more than one active connection
        if(this.userList.get(socket.user.user).sockets.length > 1){
            //temporarily store userObj
            var userObj = this.userList.get(socket.user.user);

            //Filter out disconnecting socket from socket list, and set as current socket list for user
            userObj.sockets = userObj.sockets.filter((id) => {
                return id != socket.id;
            });

            //Update the userlist
            this.userList.set(socket.user.user, userObj);
        }else{
            //If this is the last connection for this user, remove them from the userlist
            this.userList.delete(socket.user.user);
        }

        //and send out the filtered list
        this.broadcastUserList(socket.chan);
    }

    /**
     * Broadcasts user list to all users
     */
    broadcastUserList(){
        //Create a userlist object with the tokebot user pre-loaded
        var userList = [{
            user: "Tokebot",
            flair: "classic",
            highLevel: "∞",
        }];
        
        this.userList.forEach((userObj, user) => {
            userList.push({
                user: user,
                flair: userObj.flair,
                highLevel: userObj.highLevel
            });
        });

        this.server.io.in(this.name).emit("userList", userList);
    }

    /**
     * Broadcasts channel emote list to connected users
     * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access
     */
    async broadcastChanEmotes(chanDB){
        //if we wherent handed a channel document
        if(chanDB == null){
            //Pull it based on channel name
            chanDB = await channelModel.findOne({name: this.name});
        }       

        //Get emote list from channel document
        const emoteList = chanDB.getEmotes();

        //Broadcast that sumbitch
        this.server.io.in(this.name).emit('chanEmotes', emoteList);
    }
}