Source: 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/>.*/

/**
 * Class for object containing chat and command pre-processing logic
 */
class commandPreprocessor{
    /**
     * Instantiates a new commandPreprocessor object
     * @param {channel} client - Parent client mgmt object
     */
    constructor(client){
        /**
         * Parent Client Management object
         */
        this.client = client;

        /**
         * Child Command Processor object
         */
        this.commandProcessor = new commandProcessor(client);

        /**
         * Set of arrays containing site-wide, channel-wide, and user-specific emotes
         */
        this.emotes = {
            site: [],
            chan: [],
            personal: []
        }

        //define listeners
        this.defineListeners();
    }

    /**
     * Defines Network-Related Listeners
     */
    defineListeners(){
        //When we receive site-wide emote list
        this.client.socket.on("siteEmotes", this.setSiteEmotes.bind(this));
        this.client.socket.on("chanEmotes", this.setChanEmotes.bind(this));
        this.client.socket.on("personalEmotes", this.setPersonalEmotes.bind(this));
        this.client.socket.on("usedTokes", this.setUsedTokes.bind(this));
    }

    /**
     * Pre-Processes a single chat/command before sending it off to the server
     * @param {String} command - Chat/Command to pre-process
     */
    preprocess(command){
        //Set command and sendFlag
        this.command = command;
        this.sendFlag = true;

        //Attempt to process as local command
        this.processLocalCommand();

        //If we made it through the local command processor
        if(this.sendFlag){
            //Set the message to the command
            this.message = command;
            //Process message emotes into links
            this.processEmotes();
            //Process unmarked links into marked links
            this.processLinks();
            //Send command off to server
            this.sendRemoteCommand();
        }
    }

    /**
     * Processes local commands, starting with '/'
     */
    processLocalCommand(){
        //Create an empty array to hold the command
        this.commandArray = [];
        //Split string by words
        this.commandArray = this.command.split(/\b/g);//Split by word-borders
        this.argumentArray = this.command.match(/\b\w+\b/g);//Match by words surrounded by borders

        //If this is a local command
        if(this.commandArray[0] == '/'){
            //If the command exists
            if(this.argumentArray != null && this.commandProcessor[this.argumentArray[0].toLowerCase()] != null){
                //Don't send it to the server
                this.sendFlag = false;

                //Call the command with the argument array
                this.commandProcessor[this.argumentArray[0].toLowerCase()](this.argumentArray, this.commandArray);
            }
        }
    }

    /**
     * Processes emotes refrences in loaded message into links to be further processed by processLinks()
     */
    processEmotes(){
        //inject invisible whitespace in-between emotes to prevent from mushing links together
        this.message = this.message.replaceAll('][',']ㅤ[');

        //For each list of emotes
        Object.keys(this.emotes).forEach((key) => {
            //For each emote in the current list
            this.emotes[key].forEach((emote) => {
                //Inject emote links into the message, pad with invisible whitespace to keep link from getting mushed
                this.message = this.message.replaceAll(`[${emote.name}]`, `ㅤ${emote.link}ㅤ`);
            });
        });
    }

    /**
     * Processes links into numbered file seperators, putting links into a dedicated array.
     */
    processLinks(){
        //Strip out file seperators in-case the user is being a smart-ass
        this.message = this.message.replaceAll('␜','');
        //Split message by links
        var splitMessage = this.message.split(/(https?:\/\/[^\sㅤ]+)/g);
        //Create an empty array to hold links
        this.links = [];

        splitMessage.forEach((chunk, chunkIndex) => {
            //For each chunk that is a link
            if(chunk.match(/(https?:\/\/[^\sㅤ]+)/g)){
                //I looked online for obscure characters that no one would use to prevent people from chatting embed placeholders
                //Then I found this fucker, turns out it's literally made for the job lmao (even if it was originally intended for paper/magnetic tape)
                //Replace link with indexed placeholder
                splitMessage[chunkIndex] = `␜${this.links.length}`

                //push current chunk as link
                this.links.push(chunk);
            }
        });

        //Join the message back together
        this.message = splitMessage.join('');
    }

    /**
     * Transmits message/command off to server
     */
    sendRemoteCommand(){
        this.client.socket.emit("chatMessage",{msg: this.message, links: this.links});
    }

    /**
     * Sets site emotes
     * @param {Object} data - Emote data from server
     */
    setSiteEmotes(data){
        this.emotes.site = data;
    }

    /**
     * Sets channel emotes
     * @param {Object} data - Emote data from server
     */
    setChanEmotes(data){
        this.emotes.chan = data;
    }

    /**
     * Sets personal emotes
     * @param {Object} data - Emote data from server
     */
    setPersonalEmotes(data){
        this.emotes.personal = data;
    }

    /**
     * Sets used tokes
     * @param {Object} data - Used toke data from server
     */
    setUsedTokes(data){
        this.usedTokes = data.tokes;
    }

    /**
     * Fetches emote by link
     * @param {String} link - Link to fetch emote with
     * @returns {Object} found emote
     */
    getEmoteByLink(link){
        //Create an empty variable to hold the found emote
        var foundEmote = null;

        //For each list of emotes
        Object.keys(this.emotes).forEach((key) => {
            //For each emote in the current list
            this.emotes[key].forEach((emote) => {
                //if we found a match
                if(emote.link == link){
                    //return the match
                    foundEmote = emote;
                }
            });
        });

        return foundEmote;
    }

    /**
     * Generates flat list of emote names
     * @returns {Array} List of strings containing emote names
     */
    getEmoteNames(){
        //Create an empty array to hold names
        let names = [];

        //For every set of emotes
        for(let set of Object.keys(this.emotes)){
            //for every emote in the current set of emotes
            for(let emote of this.emotes[set]){
                //push the name of the emote to the name list
                names.push(emote.name);
            }
        }

        //return our list of names
        return names;
    }

    /**
     * Generates auto-complete dictionary from pre-written commands, emotes, and used tokes from servers for use with autocomplete
     * @returns {Object} Generated Dictionary object
     */
    buildAutocompleteDictionary(){
        let dictionary = {
            tokes: {
                prefix: '!',
                postfix: '',
                cmds: [
                    ['toke', true]
                ].concat(injectPerms(this.usedTokes))
            },
            //Make sure to add spaces at the end for commands that take arguments
            //Not necissary but definitely nice to have
            serverCMD: {
                prefix: '!',
                postfix: '',
                cmds: [
                    ["whisper ", true],
                    ["announce ", client.user.permMap.chan.get('announce')],
                    ["serverannounce ", client.user.permMap.site.get('announce')],
                    ["clear ", client.user.permMap.chan.get('clearChat')],
                    ["kick ", client.user.permMap.chan.get('kickUser')],
                ]
            },
            localCMD:{
                prefix: '/',
                postfix: '',
                cmds: [
                    ["high ", true]
                ]
            },
            usernames:{
                prefix: '',
                postfix: '',
                cmds: injectPerms(Array.from(client.userList.colorMap.keys()))
            },
            emotes:{
                prefix:'[',
                postfix:']',
                cmds: injectPerms(this.getEmoteNames())
            }
        }; 

        //return our dictionary object
        return dictionary;
    
        function injectPerms(cmds, perm = true){
            //Create empty array to hold cmds
            let cmdSet = [];

            //For each cmd
            for(let cmd of cmds){
                //Add the cmd with its perm to the cmdset
                cmdSet.push([cmd, perm]);
            }

            //return the cmd set
            return cmdSet;
        }
    }

}

/**
 * Class for Object which contains logic for client-side commands
 */
class commandProcessor{
    /**
     * Instantiates a new Command Processor object
     * @param {channel} client - Parent client mgmt object
     */
    constructor(client){
        /**
         * Parent Client Management object
         */
        this.client = client
    }

    /**
     * Method handling /high client command
     * @param {Array} argumentArray - Array of arguments passed down from Command Pre-Processor
     */
    high(argumentArray){
        //If we have an argument
        if(argumentArray[1]){
            //Use it to set our high level
            //Technically this is less of a local command than it would be if it where telling the select to do this
            //but TTN used to treat this as a local command so fuck it
            this.client.socket.emit("setHighLevel", {highLevel: argumentArray[1]}); 
        }
    }
}