/*Canopy - The next generation of stoner streaming software Copyright (C) 2024-2026 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 .*/ //NPM imports const validator = require('validator') //local imports const chatPreprocessor = require('../chatPreprocessor'); const commandProcessor = require('./commandProcessor'); const tokebot = require('./tokebot'); const loggerUtils = require('../../utils/loggerUtils'); const linkUtils = require('../../utils/linkUtils'); const emoteValidator = require('../../validators/emoteValidator'); const chat = require('./chat'); const {userModel} = require('../../schemas/user/userSchema'); /** * Class containing global server-side chat relay logic */ class chatHandler{ /** * Instantiates a chatHandler object * @param {channelManager} server - Parent Server Object */ constructor(server){ /** * Parent Server Object */ this.server = server; /** * Child Command Pre-Processor Object */ this.chatPreprocessor = new chatPreprocessor( new commandProcessor(server, this), new tokebot(server, this) ); /** * Max chat buffer message count */ this.chatBufferSize = 50; /** * set max high level */ this.maxHighLevel = 10; } /** * Defines global server-side chat relay event listeners * @param {Socket} socket - Requesting Socket */ defineListeners(socket){ socket.on("chatMessage", (data) => {this.handleChat(socket, data)}); socket.on("setFlair", (data) => {this.setFlair(socket, data)}); socket.on("setHighLevel", (data) => {this.setHighLevel(socket, data)}); socket.on("addPersonalEmote", (data) => {this.addPersonalEmote(socket, data)}); socket.on("deletePersonalEmote", (data) => {this.deletePersonalEmote(socket, data)}); } /** * Handles incoming chat messages from client connections * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ async handleChat(socket, data){ try{ //Preprocess chat data const preprocessedChat = await this.chatPreprocessor.preprocess(socket, data); //If send flag wasn't set to false if(preprocessedChat != false){ //Send that shit! this.relayUserChat(socket, preprocessedChat.message, preprocessedChat.chatType, preprocessedChat.links); } //If something fucked up }catch(err){ //Bitch and moan return loggerUtils.socketExceptionHandler(socket, err); } } /** * Handles incoming client request to change flair * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ async setFlair(socket, data){ var userDB = await userModel.findOne({user: socket.user.user}); if(userDB){ try{ //We can take this data raw since our schema checks it against existing flairs, and mongoose sanatizes queries const flairDB = await userDB.setFlair(data.flair); //Crawl through users active connections this.server.crawlConnections(socket.user.user, (conn)=>{ //Update flair conn.updateFlair(flairDB.name); }); }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } } /** * Handles incoming client request to change high level * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ async setHighLevel(socket, data){ var userDB = await userModel.findOne({user: socket.user.user}); if(userDB){ try{ if(data.highLevel < 0 || data.highLevel > this.maxHighLevel){ throw loggerUtils.exceptionSmith("Invalid High Level!", "High Level"); } //Floor input to an integer and set high level userDB.highLevel = Math.floor(data.highLevel); //Save user DB Document await userDB.save(); //GetConnects across all channels const connections = this.server.getConnections(socket.user.user); //For each connection connections.forEach((conn) => { conn.updateHighLevel(userDB.highLevel); }); }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } } /** * Handles incoming client request to add a personal emote * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ async addPersonalEmote(socket, data){ //Sanatize and Validate input const name = emoteValidator.manualName(data.name); const link = emoteValidator.manualLink(data.link); //If we received good input if(link && name){ //Generate marked link object var emote = await linkUtils.markLink(link); //If the link we have is an image or video if(emote.type == 'image' || emote.type == 'video'){ //Get user document from DB const userDB = await userModel.findOne({user: socket.user.user}) //if we have a user in the DB if(userDB != null){ //Convert marked link into emote object with 1 ez step for only $19.95 emote.name = name; //add emote to user document emotes list userDB.emotes.push(emote); //Save user doc await userDB.save(); } } } } /** * Handles incoming client request to delete a personal emote * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ async deletePersonalEmote(socket, data){ //Get user doc from DB based on socket const userDB = await userModel.findOne({user: socket.user.user}); //if we found a user if(userDB != null){ await userDB.deleteEmote(data.name); } } //Base chat functions /** * Creates a new chatObject and relays the resulting message to the given channel * @param {String} user - Originating user * @param {String} flair - Flair ID to mark chat with * @param {Number} highLevel - High Level to mark chat with * @param {String} msg - Message Text Content * @param {String} type - Message Type, used for client-side chat post-processing. * @param {String} chan - Channel to broadcast message within * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayChat(user, flair, highLevel, msg, type = 'chat', chan, links){ this.relayChatObject(chan, new chat(user, flair, highLevel, msg, type, links)); } /** * Relays an existing chat object to a channel * @param {String} chan - Channel to broadcast message within * @param {chat} chat - Chat Object representing the message to broadcast to the given channel */ relayChatObject(chan, chat){ //If we have an empty chat if(chat.msg.length <= 0){ //Drop it return; } //Send out chat this.server.io.in(chan).emit("chatMessage", chat); const channel = this.server.activeChannels.get(chan); //If chat buffer length is over mandated size if(channel.chatBuffer.buffer.length >= this.chatBufferSize){ //Take out oldest chat channel.chatBuffer.shift(); } //Add buffer to chat channel.chatBuffer.push(chat); } /** * Creates a new chatObject and relays the resulting message to the given socket * @param {Socket} socket - Socket we're sending a message to (sounds menacing, huh?) * @param {String} user - Originating user * @param {String} flair - Flair ID to mark chat with * @param {Number} highLevel - High Level to mark chat with * @param {String} msg - Message Text Content * @param {String} type - Message Type, used for client-side chat post-processing. * @param {String} chan - Channel to broadcast message within * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayPrivateChat(socket, user, flair, highLevel, msg, type, links){ this.relayPrivateChatObject(socket , new chat(user, flair, highLevel, msg, type, links)); } /** * Handles incoming client request to delete a personal emote * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ relayPrivateChatObject(socket, chat){ socket.emit("chatMessage", chat); } /** * Creates a new chatObject and relays the resulting message to the entire server * @param {String} user - Originating user * @param {String} flair - Flair ID to mark chat with * @param {Number} highLevel - High Level to mark chat with * @param {String} msg - Message Text Content * @param {String} type - Message Type, used for client-side chat post-processing. * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayGlobalChat(user, flair, highLevel, msg, type = 'chat', links){ this.relayGlobalChatObject(new chat(user, flair, highLevel, msg, type, links)); } /** * Relays an existing chat object to the entire server * @param {chat} chat - Chat Object representing the message to broadcast throughout the server */ relayGlobalChatObject(chat){ this.server.io.emit("chatMessage", chat); } //User Chat Functions /** * Relays a chat message from a user to the rest of the channel based on socket * @param {Socket} socket - Socket we're receiving the request from * @param {String} msg - Message Text Content * @param {String} type - Message Type, used for client-side chat post-processing. * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayUserChat(socket, msg, type, links){ const user = this.server.getSocketInfo(socket); this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links); } //Toke Chat Functions /** * Broadcasts toke callout to the server * @param {String} msg - Message Text Content * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayTokeCallout(msg, links){ this.relayGlobalChat("Tokebot", "", '∞', msg, "toke", links); } /** * Broadcasts toke callout to the server * @param {Socket} socket - Socket we're sending the whisper to * @param {String} msg - Message Text Content * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayTokeWhisper(socket, msg, links){ this.relayPrivateChat(socket, "Tokebot", "", '∞', msg, "tokewhisper", links); } /** * Broadcasts toke whisper to the server * @param {String} msg - Message Text Content * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayGlobalTokeWhisper(msg, links){ this.relayGlobalChat("Tokebot", "", '∞', msg, "tokewhisper", links); } //Announcement Functions /** * Broadcasts announcement to the server * @param {String} msg - Message Text Content * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayServerAnnouncement(msg, links){ this.relayGlobalChat("Server", "", '∞', msg, "announcement", links); } /** * Broadcasts announcement to a given channel * @param {String} msg - Message Text Content * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message. */ relayChannelAnnouncement(chan, msg, links){ const activeChan = this.server.activeChannels.get(chan); //If channel isn't null if(activeChan != null){ this.relayChat("Channel", "", '∞', msg, "announcement", chan, links); } } //Misc Functions /** * Clears chat for a given channel, targets specified user or entire channel if none found/specified. * @param {String} user - User chats to clear * @param {String} chan - Channel to broadcast message within */ clearChat(chan, user){ const activeChan = this.server.activeChannels.get(chan); //If channel isn't null if(activeChan != null){ const target = activeChan.userList.get(user); //If no user was entered OR the user was found if(user == null || target != null){ //Send command out to browsers to drop chats from buffer this.server.io.in(chan).emit("clearChat", {user}); //Clear serverside buffer, down to the DB activeChan.chatBuffer.clearBuffer(user); } } } } module.exports = chatHandler;