# JSDoc: Source: app/channel/commandPreprocessor.js

# Source: app/channel/commandPreprocessor.js

    /*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');

    /**
     * Class containing global server-side chat/command pre-processing logic
     */
    module.exports = class commandPreprocessor{
        /**
         * Instantiates a commandPreprocessor object
         * @param {channelManager} server - Parent Server Object
         * @param {chatHandler} chatHandler - Parent Chat Handler Object
         */
        constructor(server, chatHandler){
            this.server = server;
            this.chatHandler = chatHandler;
            this.commandProcessor = new commandProcessor(server, chatHandler);
            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;
        }
    }