canopy/www/doc/server/schemas_user_userSchema.js.html

898 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: schemas/user/userSchema.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/user/userSchema.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 &lt;https://www.gnu.org/licenses/>.*/
//NPM Imports
const {mongoose} = require('mongoose');
//local imports
//server
const server = require('../../server');
//DB Models
const statModel = require('../statSchema');
const flairModel = require('../flairSchema');
const permissionModel = require('../permissionSchema');
const emoteModel = require('../emoteSchema');
const emailChangeModel = require('./emailChangeSchema');
const playlistSchema = require('../channel/media/playlistSchema');
//Utils
const hashUtil = require('../../utils/hashUtils');
const mailUtil = require('../../utils/mailUtils');
const loggerUtils = require('../../utils/loggerUtils')
/**
* Mongoose Schema for a document representing a single canopy user
*/
const userSchema = new mongoose.Schema({
id: {
type: mongoose.SchemaTypes.Number,
required: true
},
user: {
type: mongoose.SchemaTypes.String,
maxLength: 22,
required: true,
},
pass: {
type: mongoose.SchemaTypes.String,
required: true
},
email: {
type: mongoose.SchemaTypes.String,
optional: true,
default: ""
},
date: {
type: mongoose.SchemaTypes.Date,
required: true,
default: new Date()
},
rank: {
type: mongoose.SchemaTypes.String,
required: true,
enum: permissionModel.rankEnum,
default: "user"
},
tokes: {
type: mongoose.SchemaTypes.Map,
required: true,
default: new Map()
},
img: {
type: mongoose.SchemaTypes.String,
required: true,
default: "/img/johnny.png"
},
bio: {
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: "Bio not set!"
},
pronouns:{
type: mongoose.SchemaTypes.String,
optional: true,
//Calculate max length by the validator max length and the size of an escaped character
maxLength: 15 * 6,
default: ""
},
signature: {
type: mongoose.SchemaTypes.String,
required: true,
//Calculate max length by the validator max length and the size of an escaped character
maxLength: 25 * 6,
default: "Signature not set!"
},
highLevel: {
type: mongoose.SchemaTypes.Number,
required: true,
min: 0,
max: 10,
default: 0
},
flair: {
type: mongoose.SchemaTypes.ObjectID,
default: null,
ref: "flair"
},
//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]
}
}],
recentIPs: [{
ipHash: {
type: mongoose.SchemaTypes.String,
required: true
},
firstLog: {
type: mongoose.SchemaTypes.Date,
required: true,
default: new Date()
},
lastLog: {
type: mongoose.SchemaTypes.Date,
required: true,
default: new Date()
}
}],
alts:[{
type: mongoose.SchemaTypes.ObjectID,
ref: "user"
}],
playlists: [playlistSchema]
});
//This is one of those places where you really DON'T want to use an arrow function over an anonymous one!
/**
* Pre-Save function for user document, handles password hashing, flair updates, emote refreshes, and kills sessions upon rank change
*/
userSchema.pre('save', async function (next){
//If the password was changed
if(this.isModified("pass")){
//Hash that sunnovabitch, no questions asked.
this.pass = hashUtil.hashPassword(this.pass);
}
//If the flair was changed
if(this.isModified("flair")){
//Get flair properties
await this.populate('flair');
if(permissionModel.rankToNum(this.rank) &lt; permissionModel.rankToNum(this.flair.rank)){
throw loggerUtils.exceptionSmith(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`, "unauthorized");
}
}
//Ensure we don't have empty flair
if(this.flair == null){
const flairDB = await flairModel.findOne({});
this.flair = flairDB._id;
}
//If rank was changed
if(this.isModified("rank")){
//force a full log-out
await this.killAllSessions("Your site-wide rank has changed. Sign-in required.");
}
//if emotes where modified
if(this.isModified('emotes')){
//Get the active Channel object from the application side of the house
server.channelManager.crawlConnections(this.user, (conn)=>{
//Send out emotes to each one
conn.sendPersonalEmotes(this);
});
}
//All is good, continue on saving.
next();
});
/**
* Pre-Delete function for user accounts, drops connections and rips it self out from alt accounts
*/
userSchema.post('deleteOne', {document: true}, async function (){
//Kill any active sessions
await this.killAllSessions("If you're seeing this, your account has been deleted. So long, and thanks for all the fish! &lt;3");
//Populate alts
await this.populate('alts');
//iterate through alts
for(alt in this.alts){
//Find the index of the alt entry for this user inside of the alt users array of alt users
const altIndex = this.alts[alt].alts.indexOf(this._id);
//splice the entry for this user out of the alt users array of alt users
this.alts[alt].alts.splice(altIndex,1);
//Save the alt user
await this.alts[alt].save();
}
});
//statics
/**
* Registers a new user account with given information
* @param {Object} userObj - Object representing user to register, generated by the client
* @param {String} ip - IP Address of connection registering the account
*/
userSchema.statics.register = async function(userObj, ip){
//Pull values from user object
const {user, pass, passConfirm, email} = userObj;
//Check password confirmation matches
if(pass == passConfirm){
//Look for a user (case insensitive)
var userDB = await this.findOne({user: new RegExp(user, 'i')});
//If the user is found or someones trying to impersonate tokeboi
if(userDB || user.toLowerCase() == "tokebot"){
throw loggerUtils.exceptionSmith("User name/email already taken!", "validation");
}else{
//Increment the user count, pulling the id to tattoo to the user
const id = await statModel.incrementUserCount();
//Create user document in the database
const newUser = await this.create({id, user, pass});
//Tattoo the hashed IP used to register to the new user
await newUser.tattooIPRecord(ip);
//if we submitted an email
if(email != null){
const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: email, ipHash: ip});
await mailUtil.sendAddressVerification(requestDB, newUser, email)
}
}
}else{
throw loggerUtils.exceptionSmith("Confirmation password doesn't match!", "validation");
}
}
/**
* Authenticates a username and password pair
* @param {String} user - Username of account to login as
* @param {String} pass - Password to authenticat account
* @param {String} failLine - Line to paste into custom error upon login failure
* @returns {Mongoose.Document} - User DB Document upon success
*/
userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Username or Password."){
//check for missing pass
if(!user || !pass){
throw loggerUtils.exceptionSmith("Missing user/pass.", "validation");
}
//get the user if it exists
const userDB = await this.findOne({ user: new RegExp(user, 'i')});
//if not scream and shout
if(!userDB){
badLogin();
}
//Check our password is correct
if(userDB.checkPass(pass)){
return userDB;
}else{
//if not scream and shout
badLogin();
}
//standardize bad login response so it's unknown which is bad for security reasons.
function badLogin(){
throw loggerUtils.exceptionSmith(failLine, "unauthorized");
}
}
/**
* Gets profile by username
* @param {String} user - name of user to find
* @param {Boolean} includeEmail - Whether or not to include email in the final result
* @returns {Object} A network-friendly browser-digestable Object representing a single user profile
*/
userSchema.statics.findProfile = async function(user, includeEmail = false){
//Catch null users
if(user == null || user.user == null){
return null;
//If someone's looking for tokebot
}else if(user.user.toLowerCase() == "tokebot"){
//fake a profile hashtable for tokebot
const profile = {
id: -420,
user: "Tokebot",
date: (await statModel.getStats()).firstLaunch,
tokes: await statModel.getTokeCommandCounts(),
tokeCount: await statModel.getTokeCount(),
img: "/img/johnny.png",
signature: "!TOKE",
bio: "!TOKE OR DIE!"
};
//return the faked profile
return profile;
}else{
//find user
const userDB = await this.findOne({user: user.user});
//If we don't find a user just return a null profile
if(userDB == null){
return null
}
//return the profile
return userDB.getProfile(includeEmail);
}
}
/**
* Tattoos a single toke callout to all the users within it
* @param {Map} tokemap - Map containing list of users and the toke command they used to join the toke
*/
userSchema.statics.tattooToke = function(tokemap){
//For each toke, asynchronously:
tokemap.forEach(async (toke, user) => {
//get user
const userDB = await this.findOne({user});
//Check that the user exists (might come in handy for future treez.one integration?)
if(userDB != null){
var tokeCount = userDB.tokes.get(toke);
//if this is the first time using this toke command
if(tokeCount == null){
//set toke count to one
tokeCount = 1;
//otherwise
}else{
//increment tokecount
tokeCount++;
}
//Set the toke count for the specific command
userDB.tokes.set(toke, tokeCount);
//Save the user doc to the database
await userDB.save();
//Would rather do this inside of tokebot but then our boi would have to wait for DB or pass a nasty-looking callback function
//Crawl through active connections
server.channelManager.crawlConnections(userDB.user, (conn)=>{
//Update used toke list
conn.sendUsedTokes(userDB);
});
}
});
}
/**
* Acquires a list of the entire userbase
* @param {Boolean} fullList - Determines whether or not we're listing rank and email
* @returns {Array} Array of objects containing user metadata
*/
userSchema.statics.getUserList = async function(fullList = false){
var userList = [];
//Get all of our users
const users = await this.find({});
//Return empty if we don't find nuthin'
if(users == null){
return [];
}
//For each user
users.forEach((user)=>{
//create a user object with limited properties (safe for public consumption)
var userObj = {
id: user.id,
user: user.user,
img: user.img,
date: user.date
}
//Put together a spicier version for admins when told so (permission checks should happen before this is called)
if(fullList){
userObj.rank = user.rank,
userObj.email = user.email
}
//Add user object to list
userList.push(userObj);
});
//return the userlist
return userList;
}
/**
* Process and Deletes Aged IP Records
*/
userSchema.statics.processAgedIPRecords = async function(){
//Pull full userlist
const users = await this.find({});
//Not a fan of iterating through the DB but there doesn't seem to be a way to search for a doc based on the properties of it's subdoc
for(let userIndex in users){
//Pull user record from users by index
const userDB = users[userIndex];
//For every recent ip within the user
for(let recordIndex in userDB.recentIPs){
//Pull record from recent IPs by index
const record = userDB.recentIPs[recordIndex];
//Check how long it's been since we've last seen the IP
const daysSinceLastUse = ((new Date() - record.lastLog) / (1000 * 60 * 60 * 24)).toFixed(1);
//If it's been more than a week
if(daysSinceLastUse >= 7){
//Splice out the IP record
userDB.recentIPs.splice(recordIndex, 1);
//No reason to wait on this since we're done with this user
await userDB.save();
}
}
}
}
//methods
/**
* Checks password against a user document
* @param {String} pass - Password to authenticate
* @returns {Boolean} True if authenticated
*/
userSchema.methods.checkPass = function(pass){
return hashUtil.comparePassword(pass, this.pass)
}
/**
* Lists authenticated sessions for a given user document
* @returns {Promise} Promise containing an array of active user sessions for a given user
*/
userSchema.methods.getAuthenticatedSessions = async function(){
var returnArr = [];
//retrieve active sessions (they really need to implement this shit async already)
return new Promise((resolve) => {
server.store.all((err, sessions) => {
//You guys ever hear of a 'not my' problem? Fucking y33tskies lmao, better use a try/catch
if(err){
throw err;
}
//crawl through active sessions
sessions.forEach((session) => {
//Skip un-authed sessions
if(session.user != null){
//if a session matches the current user
if(session.user.id == this.id){
//we return it
returnArr.push(session);
}
}
});
resolve(returnArr);
});
});
}
/**
* Generates a network-friendly browser-digestable profile object for a sepcific user document
* @param {Boolean} includeEmail - Whether or not to include the email address in the complete profile object
* @returns {Object} Network-Friendly Browser-Digestable object containing profile metadata
*/
userSchema.methods.getProfile = function(includeEmail = false){
//Create profile hashtable
const profile = {
id: this.id,
user: this.user,
date: this.date,
tokes: this.tokes,
tokeCount: this.getTokeCount(),
img: this.img,
signature: this.signature,
pronouns: this.pronouns,
bio: this.bio
};
//Include the email if we need to
if(includeEmail){
profile.email = this.email;
}
//return profile hashtable
return profile;
}
/**
* Gets list of profiles of alt accounts related to the given user document
* @returns {Array} List of alternate profiles
*/
userSchema.methods.getAltProfiles = async function(){
//Create an empty list to hold alt profiles
const profileList = [];
//populate the users alt list
await this.populate('alts');
//For every alt for the current user
for(let alt of this.alts){
//get the alts profile and push it to the profile list
profileList.push(alt.getProfile());
}
//return our generated profile list
return profileList;
}
/**
* Sets flair for current user documetn
* @param {String} flair - flair to set
* @returns {Mongoose.Document} returns found flair document from DB
*/
userSchema.methods.setFlair = async function(flair){
//Find flair by name
const flairDB = await flairModel.findOne({name: flair});
//Set the users flair ref to the found flairs _id
this.flair = flairDB._id;
//Save the user
await this.save();
//return the found flair
return flairDB;
}
/**
* Gets number of tokes for a given user document
* @returns {Number} Number of tokes recorded for the given user document
*/
userSchema.methods.getTokeCount = function(){
//Set tokeCount to 0
var tokeCount = 0;
//For each toke command the user has used
this.tokes.forEach((commandCount) => {
//Add the count for that specific command to the total
tokeCount += commandCount;
});
//Return the amount of tokes recorded
return tokeCount;
}
/**
* Gets number of emotes for a given user document
* @returns {Array} Array of objects representing emotes for the given user document
*/
userSchema.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;
}
/**
* Deletes an emote from the user Document
* @param {String} name - Name of emote to delete
*/
userSchema.methods.deleteEmote = async function(name){
//Get index by emote name
const emoteIndex = this.emotes.findIndex(checkName);
//Splice out found emote
this.emotes.splice(emoteIndex, 1);
//Save the user doc
await this.save();
function checkName(emote){
//return emotes
return emote.name == name;
}
}
/**
* Gets list of user playlists
* @returns {Array} Array of objects represnting a playlist containing media objects
*/
userSchema.methods.getPlaylists = function(){
//Create an empty array to hold our emote list
const playlists = [];
//For each channel emote
for(let playlist of this.playlists){
//Push an object with select information from the emote to the emote list
playlists.push(playlist.dehydrate());
}
//return the emote list
return playlists;
}
/**
* Iterates through user playlists and calls a given callback function against them
* @param {Function} cb - Callback function to call against user playlists
*/
userSchema.methods.playlistCrawl = function(cb){
for(let listIndex in this.playlists){
//Grab the associated playlist
playlist = this.playlists[listIndex];
//Call the callback with the playlist and list index as arguments
cb(playlist, listIndex);
}
}
/**
* Returns playlist by name
* @param {String} name - Playlist to get by name
* @returns {Object} Object representing the requested playlist
*/
userSchema.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 a playlist from the user document by name
* @param {String} name - Name of playlist to delete
*/
userSchema.methods.deletePlaylistByName = async function(name){
//Find the playlist
const playlist = this.getPlaylistByName(name);
//splice out the given playlist
this.playlists.splice(playlist.listIndex, 1);
//save the channel document
await this.save();
}
/**
* Salts, Hashes and Tattoo's IP address to user document in a privacy respecting manner
* @param {String} ip - Plaintext IP Address to Salt, Hash and Tattoo
*/
userSchema.methods.tattooIPRecord = async function(ip){
//Hash the users ip
const ipHash = hashUtil.hashIP(ip);
//Look for a pre-existing entry for this ipHash
const foundIndex = this.recentIPs.findIndex(checkHash);
//If there is no entry
if(foundIndex == -1){
//Pull the entire userlist
//TODO: update query to only pull users with recentIPs, so we aren't looping through inactive users
const users = await this.model().find({});
//create record object
const record = {
ipHash: ipHash,
firstLog: new Date(),
lastLog: new Date()
};
//We should really start using for loops and stop acting like its 2008
//Though to be quite honest this bit would be particularly brutal without them
//For every user in the userlist
for(let curUser of users){
//Ensure we're not checking the user against itself
if(curUser._id != this._id){
//For every IP record in the current user
for(let curRecord of curUser.recentIPs){
//If it matches the current ipHash
if(curRecord.ipHash == ipHash){
//Check if we've already marked the user as an alt
const foundAlt = this.alts.indexOf(curUser._id);
//If these accounts aren't already marked as alts
if(foundAlt == -1){
//Add found user to this users alt list
this.alts.push(curUser._id);
//add this user to found users alt list
curUser.alts.push(this._id);
//Save changes to the found user, this user will save at the end of the function
await curUser.save();
}
}
}
}
}
//Pop it into place
this.recentIPs.push(record);
//Save the user doc
await this.save();
//Otherwise, if we already have a record for this IP
}else{
//Update the last logged date for the found record
this.recentIPs[foundIndex].lastLog = new Date();
//Save the user doc
await this.save();
}
//Look for matching ip record
function checkHash(ipRecord){
//return matching records
return ipRecord.ipHash == ipHash;
}
}
//note: if you gotta call this from a request authenticated by it's user, make sure to kill that session first!
/**
* Kills all sessions for a given user
* @param {String} reason - Reason to kill user sessions
*/
userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){
//get authenticated sessions
var sessions = await this.getAuthenticatedSessions();
//crawl through and kill all sessions
sessions.forEach((session) => {
server.store.destroy(session.seshid);
});
//Tell the application side of the house to kick the user out as well
server.channelManager.kickConnections(this.user, reason);
}
/**
* Changes password for a given user document
* @param {Object} passChange - passChange object handed down from Browser
*/
userSchema.methods.changePassword = async function(passChange){
if(this.checkPass(passChange.oldPass)){
if(passChange.newPass == passChange.confirmPass){
//Note: We don't have to worry about hashing here because the schema is written to do it auto-magically
this.pass = passChange.newPass;
//Save our password
await this.save();
//Kill all authed sessions for security purposes
await this.killAllSessions("Your password has been reset.");
}else{
//confirmation pass doesn't match
throw loggerUtils.exceptionSmith("Mismatched confirmation password!", "validation");
}
}else{
//Old password wrong
throw loggerUtils.exceptionSmith("Incorrect Password!", "validation");
}
}
/**
* Checks another user document against this one to see if they're alts
* @param {Mongoose.Document} userDB - User document to check for alts against
* @returns {Boolean} True if alt
*/
userSchema.methods.altCheck = async function(userDB){
//Found alt flag
let foundAlt = false;
for(alt in this.alts){
//If we found a match
if(this.alts[alt]._id.toString() == userDB._id.toString()){
foundAlt = true;
}
}
//return the results
return foundAlt;
}
/**
* Nukes user account from the face of the planet upon said user's request
* @param {String} pass - Password to authenticate against before deleting
*/
userSchema.methods.nuke = async function(pass){
//Check we have a confirmation password
if(pass == "" || pass == null){
//scream and shout
throw loggerUtils.exceptionSmith("No confirmation password!", "validation");
}
//Check that the password is correct
if(this.checkPass(pass)){
//delete the user
var oldUser = await this.deleteOne();
}else{
//complain about a bad pass
throw loggerUtils.exceptionSmith("Bad pass.", "unauthorized");
}
}
module.exports.userModel = mongoose.model("user", userSchema);</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 Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>