+ /*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]});
+ }
+ }
+}
+
+