316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
/*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 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;
|
|
}
|
|
} |