# JSDoc: Source: schemas/channel/channelSchema.js

# Source: schemas/channel/channelSchema.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/>.*/

    //NPM Imports
    const {mongoose} = require('mongoose');
    const {validationResult, matchedData} = require('express-validator');

    //Local Imports
    //Server
    const server = require('../../server');
    //DB Models
    const statModel = require('../statSchema');
    const {userModel} = require('../user/userSchema');
    const permissionModel = require('../permissionSchema');
    const emoteModel = require('../emoteSchema');
    //DB Schemas
    const channelPermissionSchema = require('./channelPermissionSchema');
    const channelBanSchema = require('./channelBanSchema');
    const queuedMediaSchema = require('./media/queuedMediaSchema');
    const playlistSchema = require('./media/playlistSchema');
    const chatSchema = require('./chatSchema');
    //Utils
    const { exceptionHandler, errorHandler } = require('../../utils/loggerUtils');

    /**
     * DB Schema for Documents containing de-hydrated representations of Canopy Stream/Chat Channels
     */
    const channelSchema = new mongoose.Schema({
        id: {
            type: mongoose.SchemaTypes.Number,
            required: true
        },
        name: {
            type: mongoose.SchemaTypes.String,
            required: true,
            //Calculate max length by the validator max length and the size of an escaped character
            maxLength: 50 * 6,
            default: 0
        },
        description: {
            type: mongoose.SchemaTypes.String,
            required: true,
            //Calculate max length by the validator max length and the size of an escaped character
            maxLength: 1000 * 6,
            default: 0
        },
        thumbnail: {
            type: mongoose.SchemaTypes.String,
            required: true,
            default: "/img/johnny.png"
        },
        settings: {
            hidden: {
                type: mongoose.SchemaTypes.Boolean,
                required: true,
                default: true
            },
            streamURL: {
                type: mongoose.SchemaTypes.String,
                default: ''
            }
        },
        permissions: {
            type: channelPermissionSchema,
            default: () => ({})
        },
        rankList: [{
            user: {
                type: mongoose.SchemaTypes.ObjectID,
                required: true,
                ref: "user"
            },
            rank: {
                type: mongoose.SchemaTypes.String,
                required: true,
                enum: permissionModel.rankEnum
            }
        }],
        tokeCommands: [{
                type: mongoose.SchemaTypes.String,
                required: true
        }],
        //Not re-using the site-wide schema because post/pre save should call different functions
        emotes: [{
            name:{
                type: mongoose.SchemaTypes.String,
                required: true
            },
            link:{
                type: mongoose.SchemaTypes.String,
                required: true
            },
            type:{
                type: mongoose.SchemaTypes.String,
                required: true,
                enum: emoteModel.typeEnum,
                default: emoteModel.typeEnum[0]
            }
        }],
        media: {
            nowPlaying: queuedMediaSchema,
            scheduled: [queuedMediaSchema],
            //We should consider moving archived media and channel playlists to their own collections/models for preformances sake
            archived: [queuedMediaSchema],
            playlists: [playlistSchema],
            liveRemainder: {
                type: mongoose.SchemaTypes.UUID,
                required: false
            }
        },
        //Thankfully we don't have to keep track of alts, ips, or deleted users so this should be a lot easier than site-wide bans :P
        banList: [channelBanSchema],
        chatBuffer: [chatSchema]
    });

    /**
     * Channel pre-save function. Ensures name requirements (for some reason, we should move that to the schema probably), kicks users after rank change, and handles housekeeping after adding tokes/emotes
     */
    channelSchema.pre('save', async function (next){
        if(this.isModified("name")){
            if(this.name.match(/^[a-z0-9_\-.]+$/i) == null){
                throw loggerUtils.exceptionSmith("Username must only contain alpha-numerics and the following symbols: '-_.'", "validation");
            }
        }

        //This entire block is just about finding users after rank-change and making sure they get kicked
        //Getting the affected user would be a million times easier elsewhere
        //But this ensures it happens every time channel rank gets changed no matter what
        if(this.isModified('rankList') && this.rankList != null){
            //Get the rank list before it was modified (gross but works, find a better way if you dont like it :P)
            var chanDB = await module.exports.findOne({_id: this._id});
            //Create empty variable for the found rank object
            var foundRank = null;
            if(chanDB != null){
                //If we're removing one
                if(chanDB.rankList.length > this.rankList.length){
                    //Child/Parent is *WAY* tooo atomic family for my tastes :P
                    var top = chanDB;
                    var bottom = this;
                }else{
                    //otherwise reverse the loops
                    var top = this;
                    var bottom = chanDB;
                }

                //Populate the top doc
                await top.populate('rankList.user');

                //For each rank in the dommy-top copy of the rank list
                top.rankList.forEach((topObj) => {
                    //Create empty variable for the matched rank
                    var matchedRank = null;
                    //For each rank in the subby-bottom copy of the rank list
                    bottom.rankList.forEach((bottomObj) => {
                        //So long as both users exist (we're not working with deleted users)
                        if(topObj.user != null && bottomObj.user != null){
                            //If it's the same user
                            if(topObj.user._id.toString() == bottomObj.user._id.toString()){
                                //matched rank found
                                matchedRank = bottomObj;
                            }
                        }
                    });

                    //If matched rank is null or isn't the topObject rank
                    if(matchedRank == null || matchedRank.rank != topObj.rank){
                        //Set top object to found rank
                        foundRank = topObj;
                    }

                });

                //get relevant active channel
                const activeChan = server.channelManager.activeChannels.get(this.name);

                //if the channel is online
                if(activeChan != null){
                    //make sure we're not trying to kick a deleted user
                    if(foundRank.user != null){
                        //Get the relevant user connection
                        const userConn = activeChan.userList.get(foundRank.user.user);
                        //if the user is online
                        if(userConn != null){
                            //kick the user
                            userConn.disconnect("Your channel rank has changed!");
                        }
                    }
                }
            }
        }

        //if the toke commands where changed
        if(this.isModified("tokeCommands")){
            //Get the active Channel object from the application side of the house
            const activeChannel = server.channelManager.activeChannels.get(this.name);

            //If the channel is active
            if(activeChannel != null){
                //Reload the toke command list
                activeChannel.tokeCommands = this.tokeCommands;
            }
        }

        //if emotes where modified
        if(this.isModified('emotes')){
            //Get the active Channel object from the application side of the house
            const activeChannel = server.channelManager.activeChannels.get(this.name);

            //If the channel is active
            if(activeChannel != null){
                //Broadcast the emote list
                activeChannel.broadcastChanEmotes(this);
            }
        }

        next();
    });

    //statics
    /**
     * Registers a new channel to the DB
     * @param {Object} channelObj - Channel Object from Browser to register
     * @param {Mongoose.Document} ownerObj - DB Docuement representing user
     */
    channelSchema.statics.register = async function(channelObj, ownerObj){
        const {name, description, thumbnail} = channelObj;

        const chanDB = await this.findOne({ name });

        if(chanDB){
            throw loggerUtils.exceptionSmith("Channel name already taken!", "validation");
        }else{
            const id = await statModel.incrementChannelCount();
            const rankList = [{
                user: ownerObj._id,
                rank: "admin"
            }];

            const newChannelObj = {
                id,
                name,
                description,
                thumbnail,
                rankList,
                media: {
                    nowPlaying: null,
                    scheduledMedia: [],
                    archived: []
                }
            };

            const newChannel = await this.create(newChannelObj);
        }
    }

    /**
     * Generates Network-Friendly Browser-Digestable list of channels
     * @param {Boolean} includeHidden - Whether or not to include hidden channels within the list
     * @returns {Array} List of Network-Friendly Browser-Digestable Objects representing channels on the server
     */
    channelSchema.statics.getChannelList = async function(includeHidden = false){
        const chanDB = await this.find({});
        var chanGuide = [];

        //crawl through channels
        chanDB.forEach((channel) => {
            //For each channel, push an object with only the information we need to the channel guide
            if(!channel.settings.hidden || includeHidden){
                chanGuide.push({
                    id: channel.id,
                    name: channel.name,
                    description: channel.description,
                    thumbnail: channel.thumbnail
                });
            }
        });

        //return the channel guide
        return chanGuide;
    }

    //Middleware for rank checks
    /**
     * Configurable Express Middleware for Per-Channel Endpoint Authorization
     *
     * Man, it would be really nice if express middleware actually supported async functions, you know, as if it where't still 2015 >:(
     * Also holy shit, sharing a function between two middleware functions is a nightmare
     * I'd rather just have this check chanField for '/c/' to handle channels in URL, fuck me this was obnoxious to write
     * @param {String} - Permission to check against
     * @param {String} - Name of channel to authorize against
     * @returns {Function} Express middleware function with arguments injected into logic
    */
    channelSchema.statics.reqPermCheck = function(perm, chanField = "chanName"){
        return (req, res, next)=>{
            try{
                //Check validation result
                const validResult = validationResult(req);

                //if our chan field is set to '/c/', telling us to check the URL
                if(chanField == '/c/'){
                    //Rip the chan name out of the URL
                    var chanName = (req.originalUrl.split('/c/')[1].replace('/settings',''));
                }else if(validResult.isEmpty()){
                    //otherwise if our input is valid, use that
                    var chanName = matchedData(req)[chanField];
                }else{
                    //We didn't get /c/ and we got a bad input, time for shit to hit the fan!
                    res.status(400);
                    return res.send({errors: validResult.array()})
                }

                //Find the related channel document, and handle it using a then() block
                this.findOne({name: chanName}).then((chanDB) => {
                    //If we didnt find a channel
                    if(chanDB == null){
                        //FUCK
                        return errorHandler(res, "You cannot check permissions against a non-existant channel!", 'Unauthorized', 401);
                    }

                    //Run a perm check against the current user and permission
                    chanDB.permCheck(req.session.user, perm).then((permitted) => {
                        if(permitted){
                            //if we're permitted, go on to fulfill the request
                            next();
                        }else{
                            //If not, prevent the request from going through and tell them why
                            return errorHandler(res, "You do not have a high enough rank to access this resource.", 'Unauthorized', 401);
                        }
                    });
                });
            }catch(err){
                return exceptionHandler(res, err);
            }
        }
    }

    /**
     * Schedulable Function for Processing and Deleting Expired Channel-level User Bans
     */
    channelSchema.statics.processExpiredBans = async function(){
        const chanDB = await this.find({});

        for(let chanIndex in chanDB){
            //Pull channel from channels by index
            const channel = chanDB[chanIndex];

            //channel.banList.forEach(async (ban, banIndex) => {
            for(let banIndex in channel.banList){
                //Pull ban from channel ban list
                const ban = channel.banList[banIndex];

                //ignore permanent and non-expired bans
                if(ban.expirationDays >= 0 && ban.getDaysUntilExpiration() <= 0){
                    //Get the index of the ban
                    channel.banList.splice(banIndex,1);
                    await channel.save();
                }
            }
        }
    }

    //methods
    /**
     * Updates settings map for a given channel document
     * @param {Map} settingsMap - Map of settings updates to apply against channel document
     * @returns {Map} Map of all channel settings
     */
    channelSchema.methods.updateSettings = async function(settingsMap){
        settingsMap.forEach((value, key) => {
            if(this.settings[key] == null){
                throw loggerUtils.exceptionSmith("Invalid channel setting.", "validation");
            }

            this.settings[key] = value;
        })

        await this.save();

        return this.settings;
    }

    /**
     * Crawls through channel rank and runs a callback against the requested user's rank sub-doc
     * @param {Mongoose.Document} userDB - User DB Document to run the callback against
     * @param {Function} cb - Callback Function to call against the given users rank sub-doc
     */
    channelSchema.methods.rankCrawl = async function(userDB,cb){
        //Crawl through channel rank list
        //TODO: replace this with rank check function shared with setRank
        this.rankList.forEach(async (rankObj, rankIndex) => {
            //check against user ID to speed things up
            if(rankObj.user != null && rankObj.user._id.toString() == userDB._id.toString()){
                //If we found a match, call back
                cb(rankObj, rankIndex);
            }
        });
    }

    /**
     * Sets users rank by User Doc
     * @param {Mongoose.Document} userDB - DB Document of user's channel rank to change
     * @param {String} rank - Channel rank to set user to
     * @returns {Array} Channel Rank List
     */
    channelSchema.methods.setRank = async function(userDB,rank){
        //Create variable to store found ranks
        var foundRankIndex = null;

        //Crawl through ranks to find matching index
        this.rankCrawl(userDB,(rankObj, rankIndex)=>{foundRankIndex = rankIndex});

        //If we found an existing rank object
        if(foundRankIndex != null){
            if(rank == "user"){
                this.rankList.splice(foundRankIndex,1);
            }else{
                //otherwise, set the users rank
                this.rankList[foundRankIndex].rank = rank;
            }
        }else if(rank != "user"){
            //if the user rank object doesn't exist, and we're not setting to user
            //Create rank object based on input
            const rankObj = {
                user: userDB._id,
                rank: rank
            }

            //Add it to rank list
            this.rankList.push(rankObj);
        }

        //Save our channel and return rankList
        await this.save();
        return this.rankList;
    }

    /**
     * Generates Network-Friendly Browser-Digestable channel rank list
     * @returns {Array} Network-Friendly Browser-Digestable channel rank list
     */
    channelSchema.methods.getRankList = async function(){
        //Create an empty array to hold the user list
        const rankList = new Map()
        //Create temp rank list to replace the current one in the advant we have busted users
        let tempRankList = [];
        //Flag that lets us know we gotta save
        let reqSave = false;

        //Populate the user objects in our ranklist based off of their DB ID's
        await this.populate('rankList.user');

        //For each rank object in the rank list
        for(rankObjIndex in this.rankList){
            const rankObj = this.rankList[rankObjIndex];
            //If the use still exists
            if(rankObj.user != null){
                //Push current rank object to the temp rank list in the advant that it doesn't get saved
                tempRankList.push(rankObj);

                //Create a new user object from rank object data
                const userObj = {
                    id: rankObj.user.id,
                    user: rankObj.user.user,
                    img: rankObj.user.img,
                    rank: rankObj.rank
                }

                //Add our user object to the list
                rankList.set(rankObj.user.user, userObj);
            //Otherwise if it's an invalid rank for a deleted user
            }else{
                //Ignore the rank object and throw the save flag to save the temporary rank list
                reqSave = true;
            }
        }

        //if we need to save the temp rank list
        if(reqSave){
            //set rank list
            this.rankList = tempRankList;
            //save
            await this.save();
        }

        //return userList
        return rankList;
    }

    /**
     * Gets channel rank by user document
     * @param {Mongoose.Document} userDB - DB Document of User to pull Channel Rank of
     * @returns {String} Channel rank of requested user
     */
    channelSchema.methods.getChannelRankByUserDoc = async function(userDB = null){
        var foundRank = null;

        //Check to make sure userDB exists before going forward
        if(userDB == null){
            //If so this user is probably not signed in
            return "anon"
        }

        //Crawl through ranks to find matching rank
        this.rankCrawl(userDB,(rankObj)=>{foundRank = rankObj});

        //If we found an existing rank object
        if(foundRank != null){
            //return rank
            return foundRank.rank;
        }else{
            //default to "user" for registered users, and "anon" for anonymous
            if(userDB.rank == "anon"){
                return "anon";
            }else{
                return "user";
            }
        }
    }

    /**
     * Gets channel rank by username
     * @param {String} user - Username of user to pull channel rank of
     * @returns {String} Channel rank of requested user
     */
    channelSchema.methods.getChannelRank = async function(user){
        const userDB = await userModel.findOne({user: user.user});
        return await this.getChannelRankByUserDoc(userDB);
    }

    /**
     * Calculates a permission check against a specific channel permission for a given user by username
     * @param {String} user - Username of user to check against
     * @param {String} perm - Name of channel Permission to check against
     * @returns {Boolean} Whether or not the given user passes the given channel perm check
     */
    channelSchema.methods.permCheck = async function (user, perm){
        //Set userDB to null if we wheren't passed a real user
        if(user != null){
            var userDB = await userModel.findOne({user: user.user});
        }else{
            var userDB = null;
        }

        return await this.permCheckByUserDoc(userDB, perm)
    }

    /**
     * Calculates a permission check against a specific channel permission for a given user by DB Document
     * @param {Mongoose.Document} userDB - DB Document of user to check against
     * @param {String} perm - Name of channel Permission to check against
     * @returns {Boolean} Whether or not the given user passes the given channel perm check
     */
    channelSchema.methods.permCheckByUserDoc = async function(userDB, perm){
        //Get site-wide rank as number, default to anon for anonymous users
        const rank = userDB ? permissionModel.rankToNum(userDB.rank) : permissionModel.rankToNum("anon");
        //Get channel rank as number
        const chanRank = permissionModel.rankToNum(await this.getChannelRankByUserDoc(userDB));
        //Get channel permission rank requirement as number
        const permRank = permissionModel.rankToNum(this.permissions[perm]);
        //Get site-wide rank requirement to override as number
        const overrideRank = permissionModel.rankToNum((await permissionModel.getPerms()).channelOverrides[perm]);
        //Get channel perm check result
        const permCheck = (chanRank >= permRank);
        //Get site-wide override perm check result
        const overrideCheck = (rank >= overrideRank);

        return (permCheck || overrideCheck);
    }

    /**
     * Generates channel-wide permission map for a given user by user doc
     * @param {Mongoose.Document} userDB - DB Document representing a single user account
     * @returns {Object} Object containing two maps, one for channel perms, another for site-wide perms
     */
    channelSchema.methods.getPermMapByUserDoc = async function(userDB){
            //Grap site-wide permissions
            const sitePerms = await permissionModel.getPerms();
            const siteMap = sitePerms.getPermMapByUserDoc(userDB);
            //Pull chan permissions keys
            let permTree = channelPermissionSchema.tree;
            let permMap = new Map();

            //For each object in the temporary permissions object
            for(let perm of Object.keys(permTree)){
                //Check the current permission
                permMap.set(perm, await this.permCheckByUserDoc(userDB, perm));
            }

            //return perm map
            return {
                site: siteMap.site,
                chan: permMap
            };
    }

    /**
     * Checks if a specific user has been issued a channel-specific ban by DB doc
     * @param {Mongoose.Document} userDB - DB Document representing a single user account
     * @returns {Object} Found ban, if one exists
     */
    channelSchema.methods.checkBanByUserDoc = async function(userDB){
        var foundBan = null;

        //this needs to be a for loop for async
        //this.banList.forEach((ban) => {
        for(banIndex in this.banList){

            if(this.banList[banIndex].user != null){
                if(this.banList[banIndex].user.toString() == userDB._id.toString()){
                    foundBan = this.banList[banIndex];
                }

                //If this bans alts are banned
                if(this.banList[banIndex].banAlts){
                    //Populate the user of the current ban being checked
                    await this.populate(`banList.${banIndex}.user`);

                    //If this is an alt of the banned user
                    if(await this.banList[banIndex].user.altCheck(userDB)){
                        foundBan = this.banList[banIndex];
                    }
                }
            }
        }

        return foundBan;
    }

    /**
     * Generates Network-Friendly Browser-Digestable list of channel emotes
     * @returns {Array} Network-Friendly Browser-Digestable list of channel emotes
     */
    channelSchema.methods.getEmotes = function(){
        //Create an empty array to hold our emote list
        const emoteList = [];

        //For each channel emote
        this.emotes.forEach((emote) => {
            //Push an object with select information from the emote to the emote list
            emoteList.push({
                name: emote.name,
                link: emote.link,
                type: emote.type
            });
        });

        //return the emote list
        return emoteList;
    }

    /**
     * Generates Network-Friendly Browser-Digestable list of channel playlists
     * @returns {Array} Network-Friendly Browser-Digestable list of channel playlists
     */
    channelSchema.methods.getPlaylists = function(){
        //Create an empty array to hold our emote list
        const playlists = [];

        //For each channel emote
        for(let playlist of this.media.playlists){
            //Push an object with select information from the emote to the emote list
            playlists.push(playlist.dehydrate());
        }

        //return the emote list
        return playlists;
    }

    /**
     * Crawls through channel playlists, running a given callback function against each one
     * @param {Function} cb - Callback function to run against channel playlists
     */
    channelSchema.methods.playlistCrawl = function(cb){
        for(let listIndex in this.media.playlists){
            //Grab the associated playlist
            playlist = this.media.playlists[listIndex];

            //Call the callback with the playlist and list index as arguments
            cb(playlist, listIndex);
        }
    }

    /**
     * Finds channel playlist by playlist name
     * @param {String} name - name of given playlist to find
     * @returns {Mongoose.Document} - Sub-Document representing a single playlist
     */
    channelSchema.methods.getPlaylistByName = function(name){
        //Create null value to hold our found playlist
        let foundPlaylist = null;

        //Crawl through active playlists
        this.playlistCrawl((playlist, listIndex) => {
            //If we found a match based on name
            if(playlist.name == name){
                //Keep it
                foundPlaylist = playlist;
                //Pass down the list index
                foundPlaylist.listIndex = listIndex;
            }
        });

        //return the given playlist
        return foundPlaylist;
    }

    /**
     * Deletes channel playlist by playlist name
     * @param {String} name - name of given playlist to Delete
     */
    channelSchema.methods.deletePlaylistByName = async function(name){
        //Find the playlist
        let playlist = this.getPlaylistByName(name);

        //splice out the given playlist
        this.media.playlists.splice(playlist.listIndex, 1);

        //save the channel document
        await this.save();
    }

    /**
     * Generates Network-Friendly Browser-Digestable list of Channel-Wide user bans
     * @returns {Array} Network-Friendly Browser-Digestable list of Channel-Wide user bans
     */
    channelSchema.methods.getChanBans = async function(){
        //Create an empty list to hold our found bans
        var banList = [];
        //Populate the users in the banList
        await this.populate('banList.user');

        //Crawl through known bans
        this.banList.forEach((ban) => {

            var banObj = {
                banDate: ban.banDate,
                expirationDays: ban.expirationDays,
                banAlts: ban.banAlts,
            }

            //Check if the ban was permanent (expiration set before ban date)
            if(ban.expirationDays > 0){
                //if not calculate expiration date
                var expirationDate = new Date(ban.banDate);
                expirationDate.setDate(expirationDate.getDate() + ban.expirationDays);

                //Set calculated expiration date
                banObj.expirationDate = expirationDate;
                banObj.daysUntilExpiration = ban.getDaysUntilExpiration();
            }

            //Setup user object (Do this last to keep it at bottom for human-readibility of json :P)
            banObj.user = {
                id: ban.user.id,
                user: ban.user.user,
                img: ban.user.img,
                date: ban.user.date
            }

            banList.push(banObj);
        });

        return banList;
    }

    /**
     * Issues channel-wide ban to user based on user DB document
     * @param {Mongoose.Document} userDB - DB Document representing a single user account to ban
     * @param {Number} expirationDays - Days until ban expiration
     * @param {Boolean} banAlts - Whether or not to ban alts
     */
    channelSchema.methods.banByUserDoc = async function(userDB, expirationDays, banAlts){
        //Throw a shitfit if the user doesn't exist
        if(userDB == null){
            throw loggerUtils.exceptionSmith("Cannot ban non-existant user!", "validation");
        }

        const foundBan = await this.checkBanByUserDoc(userDB);

        if(foundBan != null){
            throw loggerUtils.exceptionSmith("User already banned!", "validation");
        }

        //Create a new ban document based on input
        const banDoc = {
            user: userDB._id,
            expirationDays,
            banAlts
        }

        const activeChan = server.channelManager.activeChannels.get(this.name);
        if(activeChan != null){
            const userConn = activeChan.userList.get(userDB.user);
            if(userConn != null){
                if(expirationDays < 0){
                    userConn.disconnect("You have been permanently banned from this channel!");
                }else{
                    userConn.disconnect(`You have been banned from this channel for ${expirationDays} day(s)!`);
                }
            }
        }

        //Push the ban to the list
        this.banList.push(banDoc);

        await this.save();
    }

    /**
     * Syntatic sugar for banning users by username
     * @param {String} user - Username of user to ban
     * @param {Number} expirationDays - Days until ban expiration
     * @param {Boolean} banAlts - Whether or not to ban alts
     * @returns {Promise} promise from this.banByUserDoc
     */
    channelSchema.methods.ban = async function(user, expirationDays, banAlts){
        const userDB = await userModel.find({user});
        return await this.banByUserDoc(userDB, expirationDays, banAlts);
    }

    /**
     * Un-Bans user by DB Document
     * @param {Mongoose.Document} userDB - DB Document representing a single user account to un-ban
     * @returns {Mongoose.Document} Saved channel document
     */
    channelSchema.methods.unbanByUserDoc = async function(userDB){
        //Throw a shitfit if the user doesn't exist
        if(userDB == null){
            throw loggerUtils.exceptionSmith("Cannot ban non-existant user!", "validation");
        }

        const foundBan = await this.checkBanByUserDoc(userDB);

        if(foundBan == null){
            throw loggerUtils.exceptionSmith("User already unbanned!", "validation");
        }

        //You know I can't help but feel like an asshole for looking for the index of something I just pulled out of an array using forEach...
        //Then again this is such an un-used function that the issue of code re-use overshadows performance
        //I mean how often are we REALLY going to be un-banning users from channels?
        const banIndex = this.banList.indexOf(foundBan);
        this.banList.splice(banIndex,1);
        return await this.save();
    }

    /**
     * Syntatic sugar for un-banning by username
     * @param {String} user - Username of user to un-ban
     * @returns {Mongoose.Document} Saved channel document
     */
    channelSchema.methods.unban = async function(user){
        const userDB = await userModel.find({user});
        return await this.unbanByUserDoc(userDB);
    }

    /**
     * Nukes channel upon channel-admin request
     * @param {String} confirm - Channel name to confirm deletion of channel
     */
    channelSchema.methods.nuke = async function(confirm){
        if(confirm == "" || confirm == null){
            throw loggerUtils.exceptionSmith("Empty Confirmation String!", "validation");
        }else if(confirm != this.name){
            throw loggerUtils.exceptionSmith("Bad Confirmation String!", "validation");
        }

        //Annoyingly there isnt a good way to do this from 'this'
        var oldChan = await this.deleteOne();

        if(oldChan.deletedCount == 0){
            throw loggerUtils.exceptionSmith("Server Error: Unable to delete channel! Please report this error to your server administrator, and with timestamp.", "internal");
        }
    }

    module.exports = mongoose.model("channel", channelSchema);