/*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'); const { command } = require('../../validators/tokebotValidator'); module.exports = class commandPreprocessor{ constructor(server, chatHandler){ this.server = server; this.chatHandler = chatHandler; this.commandProcessor = new commandProcessor(server, chatHandler); this.tokebot = new tokebot(server, chatHandler); } 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); } } 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})); } 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 } 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(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); } } } } 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)); } } 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); } sendChat(commandObj){ //FUCKIN' SEND IT! this.chatHandler.relayUserChat(commandObj.socket, commandObj.message, commandObj.chatType, commandObj.links); } } class commandProcessor{ 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 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 } 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 } 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 } 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 } 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 } 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; } 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); //throw send flag return false; } //throw send flag return true; } 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]); //throw send flag return false; } //throw send flag return true; } 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; } }