944 lines
36 KiB
HTML
944 lines
36 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>JSDoc: Source: schemas/channel/channelSchema.js</title>
|
|
|
|
<script src="scripts/prettify/prettify.js"> </script>
|
|
<script src="scripts/prettify/lang-css.js"> </script>
|
|
<!--[if lt IE 9]>
|
|
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
<![endif]-->
|
|
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
|
|
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="main">
|
|
|
|
<h1 class="page-title">Source: schemas/channel/channelSchema.js</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
<article>
|
|
<pre class="prettyprint source linenums"><code>/*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);</code></pre>
|
|
</article>
|
|
</section>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<nav>
|
|
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="activeChannel.html">activeChannel</a></li><li><a href="channelManager.html">channelManager</a></li><li><a href="chat.html">chat</a></li><li><a href="chatBuffer.html">chatBuffer</a></li><li><a href="chatHandler.html">chatHandler</a></li><li><a href="commandPreprocessor.html">commandPreprocessor</a></li><li><a href="commandProcessor.html">commandProcessor</a></li><li><a href="connectedUser.html">connectedUser</a></li><li><a href="media.html">media</a></li><li><a href="playlistHandler.html">playlistHandler</a></li><li><a href="queue.html">queue</a></li><li><a href="queuedMedia.html">queuedMedia</a></li><li><a href="tokebot.html">tokebot</a></li></ul><h3>Global</h3><ul><li><a href="global.html#authenticateSession">authenticateSession</a></li><li><a href="global.html#cache">cache</a></li><li><a href="global.html#channelBanSchema">channelBanSchema</a></li><li><a href="global.html#channelPermissionSchema">channelPermissionSchema</a></li><li><a href="global.html#channelSchema">channelSchema</a></li><li><a href="global.html#chatSchema">chatSchema</a></li><li><a href="global.html#comparePassword">comparePassword</a></li><li><a href="global.html#consoleWarn">consoleWarn</a></li><li><a href="global.html#daysToExpire">daysToExpire</a></li><li><a href="global.html#emailChangeSchema">emailChangeSchema</a></li><li><a href="global.html#emoteSchema">emoteSchema</a></li><li><a href="global.html#errorHandler">errorHandler</a></li><li><a href="global.html#errorMiddleware">errorMiddleware</a></li><li><a href="global.html#escapeRegex">escapeRegex</a></li><li><a href="global.html#exceptionHandler">exceptionHandler</a></li><li><a href="global.html#exceptionSmith">exceptionSmith</a></li><li><a href="global.html#failedAttempts">failedAttempts</a></li><li><a href="global.html#fetchMetadata">fetchMetadata</a></li><li><a href="global.html#fetchVideoMetadata">fetchVideoMetadata</a></li><li><a href="global.html#fetchYoutubeMetadata">fetchYoutubeMetadata</a></li><li><a href="global.html#fetchYoutubePlaylistMetadata">fetchYoutubePlaylistMetadata</a></li><li><a href="global.html#flairSchema">flairSchema</a></li><li><a href="global.html#genCaptcha">genCaptcha</a></li><li><a href="global.html#getLoginAttempts">getLoginAttempts</a></li><li><a href="global.html#getMediaType">getMediaType</a></li><li><a href="global.html#hashIP">hashIP</a></li><li><a href="global.html#hashPassword">hashPassword</a></li><li><a href="global.html#kickoff">kickoff</a></li><li><a href="global.html#killSession">killSession</a></li><li><a href="global.html#lifetime">lifetime</a></li><li><a href="global.html#localExceptionHandler">localExceptionHandler</a></li><li><a href="global.html#mailem">mailem</a></li><li><a href="global.html#markLink">markLink</a></li><li><a href="global.html#maxAttempts">maxAttempts</a></li><li><a href="global.html#mediaSchema">mediaSchema</a></li><li><a href="global.html#passwordResetSchema">passwordResetSchema</a></li><li><a href="global.html#permissionSchema">permissionSchema</a></li><li><a href="global.html#playlistMediaProperties">playlistMediaProperties</a></li><li><a href="global.html#playlistSchema">playlistSchema</a></li><li><a href="global.html#processExpiredAttempts">processExpiredAttempts</a></li><li><a href="global.html#queuedProperties">queuedProperties</a></li><li><a href="global.html#rankEnum">rankEnum</a></li><li><a href="global.html#refreshRawLink">refreshRawLink</a></li><li><a href="global.html#schedule">schedule</a></li><li><a href="global.html#securityCheck">securityCheck</a></li><li><a href="global.html#sendAddressVerification">sendAddressVerification</a></li><li><a href="global.html#socketCriticalExceptionHandler">socketCriticalExceptionHandler</a></li><li><a href="global.html#socketErrorHandler">socketErrorHandler</a></li><li><a href="global.html#socketExceptionHandler">socketExceptionHandler</a></li><li><a href="global.html#spent">spent</a></li><li><a href="global.html#statSchema">statSchema</a></li><li><a href="global.html#throttleAttempts">throttleAttempts</a></li><li><a href="global.html#tokeCommandSchema">tokeCommandSchema</a></li><li><a href="global.html#transporter">transporter</a></li><li><a href="global.html#typeEnum">typeEnum</a></li><li><a href="global.html#userBanSchema">userBanSchema</a></li><li><a href="global.html#userSchema">userSchema</a></li><li><a href="global.html#verify">verify</a></li><li><a href="global.html#yankMedia">yankMedia</a></li><li><a href="global.html#ytdlpFetch">ytdlpFetch</a></li></ul>
|
|
</nav>
|
|
|
|
<br class="clear">
|
|
|
|
<footer>
|
|
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.4</a> on Fri Sep 05 2025 05:52:24 GMT-0400 (Eastern Daylight Time)
|
|
</footer>
|
|
|
|
<script> prettyPrint(); </script>
|
|
<script src="scripts/linenumber.js"> </script>
|
|
</body>
|
|
</html>
|