/*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 .*/ /** * Class containing chat and command pre-processing logic */ class commandPreprocessor{ /** * Instantiates a new commandPreprocessor object * @param {channel} client - Parent client Management 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(); //Return pre-processed message data return {msg: this.message, links: this.links}; } //Return false for bad message/command on fall-through return false; } /** * 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(''); } /** * 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 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]}); } } }