/*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 .*/ //NPM Imports const {mongoose} = require('mongoose'); //Local Imports const hashUtil = require('../../utils/hashUtils'); const {userModel} = require('./userSchema'); const userBanSchema = new mongoose.Schema({ user: { type: mongoose.SchemaTypes.ObjectID, ref: "user" }, ips: { plaintext: { type: [mongoose.SchemaTypes.String], required: false }, hashed: { type: [mongoose.SchemaTypes.String], required: false } }, alts: [{ type: mongoose.SchemaTypes.ObjectID, ref: "user" }], deletedNames: { type: [mongoose.SchemaTypes.String], required: false }, banDate: { type: mongoose.SchemaTypes.Date, required: true, default: new Date() }, expirationDays: { type: mongoose.SchemaTypes.Number, required: true, default: 30 }, //If true, then expiration date deletes associated accounts instead of deleting the ban record permanent: { type: mongoose.SchemaTypes.Boolean, required: true, default: false } }); userBanSchema.statics.checkBanByIP = async function(ip){ //Get hash of ip const ipHash = hashUtil.hashIP(ip); //Get all bans const banDB = await this.find({}); //Create null variable to hold any found ban let foundBan = null; //For every ban for(ban of banDB){ //Create empty list to hold unmatched hashes in the advent that we match one let tempHashes = []; //Create flag to throw to save tempHashes in the advent that we have matches we dont want to save as hashes let saveBan = false; //For every plaintext IP in the ban for(ipIndex in ban.ips.plaintext){ //Get the current ip const curIP = ban.ips.plaintext[ipIndex]; //Check the current IP against the given ip if(ip == curIP){ //If it matches we found the ban foundBan = ban; } } //For every hashed IP in the ban for(ipIndex in ban.ips.hashed){ //Get the current ip hash const curHash = ban.ips.hashed[ipIndex]; //Check the current hash against the given hash if(ipHash == curHash){ //If it matches we found the ban foundBan = ban; //Push the match to plaintext IPs so we know who the fucker is ban.ips.plaintext.push(ip); //Throw the save ban flag to save the ban saveBan = true; //Otherwise }else{ //Keep the hash since it hasn't been matched yet tempHashes.push(curHash); } } //If we matched a hashed ip and we need to save it as plaintext if(saveBan){ //Keep unmatched hashes ban.ips.hashed = tempHashes; //Save the current ban await ban.save(); } } return foundBan; } userBanSchema.statics.checkBanByUserDoc = async function(userDB){ const banDB = await this.find({}); var foundBan = null; banDB.forEach((ban) => { if(ban.user != null){ //if we found a match if(ban.user.toString() == userDB._id.toString()){ //Set found ban foundBan = ban; } //For each banned alt for(altIndex in ban.alts){ //get current alt const alt = ban.alts[altIndex]; //if the alt matches our user if(alt._id.toString() == userDB._id.toString()){ //Set found ban foundBan = ban; } } } }); return foundBan; } userBanSchema.statics.checkBan = async function(user){ const userDB = await userModel.findOne({user: user.user}); return this.checkBanByUserDoc(userDB); } userBanSchema.statics.checkProcessedBans = async function(user){ //Pull banlist and create empty variable to hold any found ban const banDB = await this.find({}); var foundBan = null; //For each ban in list banDB.forEach((ban)=>{ //For each deleted account associated with the ban ban.deletedNames.forEach((name)=>{ //If the banned name equals the name we're checking against if(name == user){ //We've found our ban foundBan = ban; } }) }); //Return any found associated ban return foundBan; } userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays, ipBan = false){ //Prevent missing users if(userDB == null){ throw new Error("User not found") } //Ensure the user isn't already banned if(await this.checkBanByUserDoc(userDB) != null){ throw new Error("User already banned"); } //Verify time to expire/delete depending on action if(expirationDays < 0){ throw new Error("Expiration Days must be a positive integer!"); }else if(expirationDays < 30 && permanent){ throw new Error("Permanent bans must be given at least 30 days before automatic account deletion!"); }else if(expirationDays > 185){ throw new Error("Expiration/Deletion date cannot be longer than half a year out from the original ban date."); } await banSessions(userDB); //Add the ban to the database const banDB = await this.create({user: userDB._id, permanent, expirationDays}); //If we're banning the users IP if(ipBan){ //Scrape IP's from current user into the ban record await scrapeUserIPs(userDB); //Populate the users alts await userDB.populate('alts'); //For each of the users alts for(altIndex in userDB.alts){ //Add the current alt to the ban record banDB.alts.push(userDB.alts[altIndex]._id); //Scrape out the IPs from the current alt into the ban record await scrapeUserIPs(userDB.alts[altIndex]); //Kill all of alts sessions await banSessions(userDB.alts[altIndex]); } //Save commited IP information to the ban record await banDB.save(); async function scrapeUserIPs(curRecord){ //For each hashed ip on record for this user for(hashIndex in curRecord.recentIPs){ //Look for any occurance of the current hash const foundHash = banDB.ips.hashed.indexOf(curRecord.recentIPs[hashIndex].ipHash); //If its not listed in the ban record if(foundHash == -1){ //Add it to the list of hashed IPs for this ban banDB.ips.hashed.push(curRecord.recentIPs[hashIndex].ipHash); } } } } //return the ban record return banDB; async function banSessions(user){ //Log the user out if(permanent){ await user.killAllSessions(`Your account has been permanently banned, and will be nuked from the database in ${expirationDays} day(s).`); }else{ await user.killAllSessions(`Your account has been temporarily banned, and will be reinstated in: ${expirationDays} day(s).`); } } } userBanSchema.statics.ban = async function(user, permanent, expirationDays, ipBan){ const userDB = await userModel.findOne({user: user.user}); return this.banByUserDoc(userDB, permanent, expirationDays, ipBan); } userBanSchema.statics.unbanByUserDoc = async function(userDB){ //Prevent missing users if(userDB == null){ throw new Error("User not found") } const banDB = await this.checkBanByUserDoc(userDB); if(!banDB){ throw new Error("User already un-banned"); } //Use _id in-case mongoose wants to be a cunt var oldBan = await this.deleteOne({_id: banDB._id}); return oldBan; } userBanSchema.statics.unbanDeleted = async function(user){ const banDB = await this.checkProcessedBans(user); if(!banDB){ throw new Error("User already un-banned"); } const oldBan = await this.deleteOne({_id: banDB._id}); return oldBan; } userBanSchema.statics.unban = async function(user){ //Find user in DB const userDB = await userModel.findOne({user: user.user}); //If user was deleted if(userDB == null){ //unban deleted user return await this.unbanDeleted(user.user); }else{ //unban by user doc return await this.unbanByUserDoc(userDB); } } userBanSchema.statics.getBans = async function(){ //Get the ban, populating users and alts const banDB = await this.find({}).populate('user').populate('alts'); //Create an empty array to hold ban records var bans = []; banDB.forEach((ban) => { //Create array to hold alts var alts = []; //Calculate expiration date var expirationDate = new Date(ban.banDate); expirationDate.setDate(expirationDate.getDate() + ban.expirationDays); //Make sure we're not about to read the properties of a null object if(ban.user != null){ var userObj = ban.user.getProfile(); } //For each alt in the ban for(alt of ban.alts){ //Get the profile and push it to the alt list alts.push(alt.getProfile()); } //Create ban object const banObj = { banDate: ban.banDate, expirationDays: ban.expirationDays, expirationDate: expirationDate, daysUntilExpiration: ban.getDaysUntilExpiration(), user: userObj, ips: ban.ips, alts, deletedNames: ban.deletedNames, permanent: ban.permanent } //Add it to the array bans.push(banObj); }); //Return the array return bans; } userBanSchema.statics.processExpiredBans = async function(){ const banDB = await this.find({}); //Firem all off all at once seperately without waiting for one another banDB.forEach(async (ban) => { //This ban was already processed, and it's user has been deleted. There is no more to be done... if(ban.user == null){ return; } //If the ban hasn't been processed and it's got 0 or less days to go if(ban.getDaysUntilExpiration() <= 0){ //If the ban is permanent if(ban.permanent){ //Populate the user and alt fields await ban.populate('user'); await ban.populate('alts'); //Add the name to our deleted names list ban.deletedNames.push(ban.user.user); //Hey hey hey, goodbye! await userModel.deleteOne({_id: ban.user._id}); //Empty out the reference ban.user = null; //For every alt for(alt of ban.alts){ //Add the alts name to the deleted names list ban.deletedNames.push(alt.user); //Motherfuckin' Kablewie! await userModel.deleteOne({_id: alt._id}); } //Clear out the alts array ban.alts = []; //Save the ban await ban.save(); }else{ //Otherwise, delete the ban and let our user back in :P await this.deleteOne({_id: ban._id}); } } }); } //methods userBanSchema.methods.getDaysUntilExpiration = function(){ //Get ban date const expirationDate = new Date(this.banDate); //Get expiration days and calculate expiration date expirationDate.setDate(expirationDate.getDate() + this.expirationDays); //Calculate and return days until ban expiration return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); } module.exports = mongoose.model("userBan", userBanSchema);