/*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 validator = require('validator');//No express here, so regular validator it is! //Local Imports const tokebot = require('./tokebot'); const linkUtils = require('../../utils/linkUtils'); const permissionModel = require('../../schemas/permissionSchema'); const channelModel = require('../../schemas/channel/channelSchema'); /** * Class containing global server-side chat/command pre-processing logic */ class commandPreprocessor{ /** * Instantiates a commandPreprocessor object * @param {channelManager} server - Parent Server Object * @param {chatHandler} chatHandler - Parent Chat Handler Object */ constructor(server, chatHandler){ /** * Parent Server Object */ this.server = server; /** * Parent Chat Handler Object */ this.chatHandler = chatHandler; /** * Child Command Processor Object */ this.commandProcessor = new commandProcessor(server, chatHandler); /** * Child Tokebot Object */ this.tokebot = new tokebot(server, chatHandler); } /** * Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ async preprocess(socket, data){ //Set command object const commandObj = { socket, sendFlag: true, rawData: data, chatType: 'chat' } //If we don't pass sanatization/validation turn this car around if(!this.sanatizeCommand(commandObj)){ return; } //split the command this.splitCommand(commandObj); //Process the command await this.processServerCommand(commandObj); //If we're going to relay this command as a message, continue on to chat processing if(commandObj.sendFlag){ //Prep the message await this.prepMessage(commandObj); //Send the chat this.sendChat(commandObj); } } /** * Sanatizes and Validates a single user chat message/command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} false if Command/Message is too long to send */ sanatizeCommand(commandObj){ //Trim and Sanatize for XSS commandObj.command = validator.trim(validator.escape(commandObj.rawData.msg)); //Return whether or not the shit was long enough return (validator.isLength(commandObj.rawData.msg, {min: 1, max: 255})); } /** * Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders * These arrays are used to handle further command/chat processing * @param {Object} commandObj - Object representing a single given command/chat request */ splitCommand(commandObj){ //Split string by words commandObj.commandArray = commandObj.command.split(/\b/g);//Split by word-borders commandObj.argumentArray = commandObj.command.match(/\b\w+\b/g);//Match by words surrounded by borders } /** * Uses the server's Command Processor object to process the chat/command request. * @param {Object} commandObj - Object representing a single given command/chat request */ async processServerCommand(commandObj){ //If the raw message starts with '!' (skip commands that start with whitespace so people can send example commands in chat) if(commandObj.rawData.msg[0] == '!'){ //if it isn't just an exclimation point, and we have a real command if(commandObj.argumentArray != null){ //If the command processor knows what to do with whatever the fuck the user sent us if(this.commandProcessor[commandObj.argumentArray[0].toLowerCase()] != null){ //Process the command and use the return value to set the sendflag (true if command valid) commandObj.sendFlag = await this.commandProcessor[commandObj.argumentArray[0].toLowerCase()](commandObj, this); }else{ //Process as toke command if we didnt get a match from the standard server-side command processor commandObj.sendFlag = await this.tokebot.tokeProcessor(commandObj); } } } } /** * Iterates through links in message and marks them by link type for later use by client-side post-processing * @param {Object} commandObj - Object representing a single given command/chat request */ async markLinks(commandObj){ //Setup the links array commandObj.links = []; //For each link sent from the client //this.rawData.links.forEach((link) => { for (const link of commandObj.rawData.links){ //Add a marked link object to our links array commandObj.links.push(await linkUtils.markLink(link)); } } /** * Re-creates message string from processed Command Array * @param {Object} commandObj - Object representing a single given command/chat request */ async prepMessage(commandObj){ //Create message from commandArray commandObj.message = commandObj.commandArray.join('').trimStart(); //Validate links and mark them by embed type await this.markLinks(commandObj); } /** * Relays chat to channel via parent Chat Handler object * @param {Object} commandObj - Object representing a single given command/chat request */ sendChat(commandObj){ //FUCKIN' SEND IT! this.chatHandler.relayUserChat(commandObj.socket, commandObj.message, commandObj.chatType, commandObj.links); } } /** * Class representing global server-side chat/command processing logic */ class commandProcessor{ /** * Instantiates a commandProcessor object * @param {channelManager} server - Parent Server Object * @param {chatHandler} chatHandler - Parent Chat Handler Object */ constructor(server, chatHandler){ this.server = server; this.chatHandler = chatHandler; } //Command keywords get run through .toLowerCase(), so we should use lowercase method names for command methods /** * Command Processor method to handle the '!whisper' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag */ whisper(commandObj){ //splice out our command commandObj.commandArray.splice(0,2); //Mark out the current message as a whisper commandObj.chatType = 'whisper'; //Make sure to throw the send flag return true } /** * Command Processor method to handle the '!spoiler' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag */ spoiler(commandObj){ //splice out our command commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler commandObj.chatType = 'spoiler'; //Make sure to throw the send flag return true } /** * Command Processor method to handle the '!strikethrough' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag */ strikethrough(commandObj){ //splice out our command commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler commandObj.chatType = 'strikethrough'; //Make sure to throw the send flag return true } /** * Command Processor method to handle the '!bold' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag */ bold(commandObj){ //splice out our command commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler commandObj.chatType = 'bold'; //Make sure to throw the send flag return true } /** * Command Processor method to handle the '!italics' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag */ italics(commandObj){ //splice out our command commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler commandObj.chatType = 'italics'; //Make sure to throw the send flag return true } /** * Command Processor method to handle the '!announce' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag on un-authorized call to shame the user */ async announce(commandObj, preprocessor){ //Get the current channel from the database const chanDB = await channelModel.findOne({name: commandObj.socket.chan}); //Check if the user has permission, and publicly shame them if they don't (lmao) if(chanDB != null && await chanDB.permCheck(commandObj.socket.user, 'announce')){ //splice out our command commandObj.commandArray.splice(0,2); //Prep the message using pre-processor functions chat-handling await preprocessor.prepMessage(commandObj); //send it this.chatHandler.relayChannelAnnouncement(commandObj.socket.chan, commandObj.message, commandObj.links); //throw send flag return false; } //throw send flag return true; } /** * Command Processor method to handle the '!serverannounce' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag on un-authorized call to shame the user */ async serverannounce(commandObj, preprocessor){ //Check if the user has permission, and publicly shame them if they don't (lmao) if(await permissionModel.permCheck(commandObj.socket.user, 'announce')){ //splice out our command commandObj.commandArray.splice(0,2); //Prep the message using pre-processor functions for chat-handling await preprocessor.prepMessage(commandObj); //send it this.chatHandler.relayServerAnnouncement(commandObj.message, commandObj.links); //disble send flag return false; } //throw send flag return true; } /** * Command Processor method to handle the '!resettoke' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag on un-authorized call to shame the user */ async resettoke(commandObj, preprocessor){ //Check if the user has permission, and publicly shame them if they don't (lmao) if(await permissionModel.permCheck(commandObj.socket.user, 'resetToke')){ //Acknowledge command this.chatHandler.relayTokeWhisper(commandObj.socket, 'Toke cooldown reset.'); //Tell tokebot to reset the toke preprocessor.tokebot.resetToke(); //disable send flag return false; } //throw send flag return true; } /** * Command Processor method to handle the '!clear' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag on un-authorized call to shame the user */ async clear(commandObj){ //Get the current channel from the database const chanDB = await channelModel.findOne({name: commandObj.socket.chan}); //Check if the user has permission, and publicly shame them if they don't (lmao) if(await chanDB.permCheck(commandObj.socket.user, 'clearChat')){ //Send off the command this.chatHandler.clearChat(commandObj.socket.chan, commandObj.argumentArray[1]); //disable send flag return false; } //throw send flag return true; } /** * Command Processor method to handle the '!kick' command * @param {Object} commandObj - Object representing a single given command/chat request * @returns {Boolean} True to enable send flag on un-authorized call to shame the user */ async kick(commandObj){ //Get the current channel from the database const chanDB = await channelModel.findOne({name: commandObj.socket.chan}); //Check if the user has permission, and publicly shame them if they don't (lmao) if(await chanDB.permCheck(commandObj.socket.user, 'kickUser')){ //Get username from argument array const username = commandObj.argumentArray[1]; //Get channel const channel = this.server.activeChannels.get(commandObj.socket.chan); //get initiator and target user objects const initiator = channel.userList.get(commandObj.socket.user.user); const target = channel.userList.get(username); //get initiator and target override abilities const override = await permissionModel.overrideCheck(commandObj.socket.user, 'kickUser'); const targetOverride = await permissionModel.overrideCheck(target, 'kickUser'); //If there is no user if(target == null){ //silently drop the command return false; } //If the user is capable of overriding this permission based on site permissions if(override || targetOverride){ //If the site rank is equal if(permissionModel.rankToNum(initiator.rank) == permissionModel.rankToNum(target.rank)){ //compare chan rank if(permissionModel.rankToNum(initiator.chanRank) <= permissionModel.rankToNum(target.chanRank)){ //shame the person running it return true; } //otherwise }else{ //compare site rank if(permissionModel.rankToNum(initiator.rank) <= permissionModel.rankToNum(target.rank)){ //shame the person running it return true; } } }else{ //If the target has a higher chan rank than the initiator if(permissionModel.rankToNum(initiator.chanRank) <= permissionModel.rankToNum(target.chanRank)){ //shame the person running it return true; } } //Splice out kick commandObj.commandArray.splice(0,4) //Get collect reason var reason = commandObj.commandArray.join(''); //If no reason was given if(reason == ''){ //Fill in a generic reason reason = "You have been kicked from the channel!"; } //Kick the user target.disconnect(reason); //throw send flag return false; } //throw send flag return true; } } module.exports = commandPreprocessor;