diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index 17266e1..6ef532a 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -25,7 +25,14 @@ const emoteValidator = require('../../validators/emoteValidator'); const chat = require('./chat'); const {userModel} = require('../../schemas/user/userSchema'); +/** + * Class containing global server-side chat relay logic + */ module.exports = class{ + /** + * Instantiates a chatHandler object + * @param {channelManager} server - Parent Server Object + */ constructor(server){ //Set server this.server = server; @@ -35,6 +42,10 @@ module.exports = class{ this.chatBufferSize = 50; } + /** + * 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)}); @@ -43,10 +54,20 @@ module.exports = class{ 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 + */ handleChat(socket, data){ this.commandPreprocessor.preprocess(socket, data); } + /** + * 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}); @@ -66,6 +87,11 @@ module.exports = class{ } } + /** + * 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}); @@ -89,6 +115,11 @@ module.exports = class{ } } + /** + * 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); @@ -119,6 +150,11 @@ module.exports = class{ } } + /** + * 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}); @@ -130,10 +166,25 @@ module.exports = class{ } //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){ //Send out chat this.server.io.in(chan).emit("chatMessage", chat); @@ -150,46 +201,107 @@ module.exports = class{ 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); @@ -200,6 +312,11 @@ module.exports = class{ } //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); diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index 7396dac..219bddd 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -23,7 +23,15 @@ 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; @@ -31,6 +39,11 @@ module.exports = class commandPreprocessor{ 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 = { @@ -61,6 +74,11 @@ module.exports = class commandPreprocessor{ } } + /** + * 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)); @@ -69,17 +87,27 @@ module.exports = class commandPreprocessor{ 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); @@ -91,6 +119,10 @@ module.exports = class commandPreprocessor{ } } + /** + * 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 = []; @@ -103,6 +135,10 @@ module.exports = class commandPreprocessor{ } } + /** + * 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(); @@ -110,19 +146,36 @@ module.exports = class commandPreprocessor{ 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); @@ -134,6 +187,11 @@ class commandProcessor{ 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); @@ -145,6 +203,11 @@ class commandProcessor{ 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); @@ -156,6 +219,11 @@ class commandProcessor{ 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); @@ -167,6 +235,11 @@ class commandProcessor{ 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); @@ -178,6 +251,11 @@ class commandProcessor{ 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}); @@ -201,6 +279,11 @@ class commandProcessor{ 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')){ @@ -221,6 +304,11 @@ class commandProcessor{ 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')){ @@ -238,6 +326,11 @@ class commandProcessor{ 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}); @@ -254,6 +347,11 @@ class commandProcessor{ 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}); diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 659fb69..ff8a252 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -22,7 +22,17 @@ const flairModel = require('../../schemas/flairSchema'); const emoteModel = require('../../schemas/emoteSchema'); const { userModel } = require('../../schemas/user/userSchema'); +/** + * Class representing a single user connected to a channel + */ module.exports = class{ + /** + * Instantiates a connectedUser object + * @param {Mongoose.Document} userDB - User document to re-hydrate user from + * @param {PemissionModel.chanRank} chanRank - Enum representing user channel rank + * @param {String} - Channel the user is connecting to + * @param {Socket} socket - Socket associated with the users connection + */ constructor(userDB, chanRank, channel, socket){ this.id = userDB.id; this.user = userDB.user; @@ -44,6 +54,12 @@ module.exports = class{ this.sockets = [socket.id]; } + /** + * Handles server-side initialization for new connections from a specific user + * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access + * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access + * @param {Socket} socket - Requesting Socket + */ async handleConnection(userDB, chanDB, socket){ //send metadata to client this.sendClientMetadata(); @@ -69,6 +85,10 @@ module.exports = class{ } } + /** + * Iterates through all known connections for a given user, running them through a supplied callback function + * @param {Function} cb - Callback to call against found sockets for a given user + */ socketCrawl(cb){ //Crawl through user's sockets (lol) this.sockets.forEach((sockid) => { @@ -79,22 +99,33 @@ module.exports = class{ }); } - //My brain keeps going back to using dynamic per-user namespaces for this - //but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces - //and using per-user channels for this, but what of gold or mod-only features? or games? - //No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). - //at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. - //Having to crawl through these sockets is that. Because the other ways seem more gross somehow. - emit(eventName, args){ + + /** + * Emits an event to all known sockets for a given user + * + * My brain keeps going back to using dynamic per-user namespaces for this + * but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces + * and using per-user channels for this, but what of gold or mod-only features? or games? + * No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). + * at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. + * Having to crawl through these sockets is that. Because the other ways seem more gross somehow. + * @param {String} eventName - Event name to emit to client sockets + * @param {Object} data - Data to emit to client sockets + */ + emit(eventName, data){ this.socketCrawl((socket)=>{ //Ensure our socket is initialized if(socket != null){ - socket.emit(eventName, args); + socket.emit(eventName, data); } }); } - //generic disconnect function, defaults to kick + /** + * Disconnects all sockets for a given user + * @param {String} reason - Reason for being disconnected + * @param {String} type - Disconnection Type + */ disconnect(reason, type = "Disconnected"){ this.emit("kick",{type, reason}); this.socketCrawl((socket)=>{socket.disconnect()}); @@ -102,6 +133,11 @@ module.exports = class{ //This is the big first push upon connection //It should only fire once, so things that only need to be sent once can be slapped into here + /** + * Sends glut of required initial metadata to the client upon a new connection + * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access + * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access + */ async sendClientMetadata(userDB, chanDB){ //Get flairList from DB and setup flairList array const flairListDB = await flairModel.find({}); @@ -166,6 +202,9 @@ module.exports = class{ this.emit("clientMetadata", {user: userObj, flairList, queue, queueLock, chatBuffer}); } + /** + * Send copy of site emotes to the user + */ async sendSiteEmotes(){ //Get emote list from DB const emoteList = await emoteModel.getEmotes(); @@ -174,6 +213,10 @@ module.exports = class{ this.emit('siteEmotes', emoteList); } + /** + * Send copy of channel emotes to the user + * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access + */ async sendChanEmotes(chanDB){ //if we wherent handed a channel document if(chanDB == null){ @@ -188,6 +231,10 @@ module.exports = class{ this.emit('chanEmotes', emoteList); } + /** + * Send copy of channel emotes to the user + * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access + */ async sendPersonalEmotes(userDB){ //if we wherent handed a user document if(userDB == null){ @@ -202,6 +249,10 @@ module.exports = class{ this.emit('personalEmotes', emoteList); } + /** + * Send copy of channel emotes to the user + * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access + */ async sendUsedTokes(userDB){ //if we wherent handed a user document if(userDB == null){ @@ -215,6 +266,10 @@ module.exports = class{ }); } + /** + * Set flair for a given user and broadcast update to clients + * @param {String} flair - Flair string to update user's flair to + */ updateFlair(flair){ this.flair = flair; @@ -222,6 +277,10 @@ module.exports = class{ this.sendClientMetadata(); } + /** + * Set high level for a given user and broadcast update to clients + * @param {Number} highLevel - Number to update user's high-level to + */ updateHighLevel(highLevel){ this.highLevel = highLevel; diff --git a/src/app/channel/tokebot.js b/src/app/channel/tokebot.js index a40d559..fb181e2 100644 --- a/src/app/channel/tokebot.js +++ b/src/app/channel/tokebot.js @@ -17,11 +17,18 @@ along with this program. If not, see .*/ //Local Imports const tokeCommandModel = require('../../schemas/tokebot/tokeCommandSchema'); const {userModel} = require('../../schemas/user/userSchema'); -const statModel = require('../../schemas/statSchema'); const statSchema = require('../../schemas/statSchema'); +/** + * Class containing global server-side tokebot logic + */ module.exports = class tokebot{ + /** + * Instantiates a tokebot object + * @param {channelManager} server - Parent Server Object + * @param {chatHandler} chatHandler - Parent Chat Handler Object + */ constructor(server, chatHandler){ //Set parents this.server = server; @@ -46,11 +53,19 @@ module.exports = class tokebot{ this.refreshCommands(); } + /** + * Reloads toke commands from DB into RAM-based toke command store + */ async refreshCommands(){ //Pull Command Strings from DB this.tokeCommands = await tokeCommandModel.getCommandStrings(); } + /** + * Processes toke commands from Command Pre-Processor + * @param {Object} commandObj - Object representing a single given command/chat request, passed down from the Command Pre-Processor + * @returns {Boolean} True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) + */ tokeProcessor(commandObj){ //Check for site-wide toke commands if(this.tokeCommands.indexOf(commandObj.argumentArray[0].toLowerCase()) != -1){ @@ -127,6 +142,9 @@ module.exports = class tokebot{ } } + /** + * Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. + */ countdown(){ //If we're in the last three seconds if(this.tokeCounter <= 3 && this.tokeCounter > 0){ @@ -169,6 +187,10 @@ module.exports = class tokebot{ this.tokeTimer = setTimeout(this.countdown.bind(this), 1000) } + /** + * This method seems to be a vestage from a bygone era. We should remove it after documenting shit. + * I would now, but I don't want to break shit in a comment-only commit. + */ async asyncFinisher(){ //Grab a copy of the tokers map before it gets cleared out const tokers = this.tokers; @@ -177,6 +199,9 @@ module.exports = class tokebot{ await userModel.tattooToke(tokers); } + /** + * Runs every second for 60 seconds after a toke + */ cooldown(){ //If the cooldown timer isn't over if(this.cooldownCounter > 0){ @@ -191,6 +216,9 @@ module.exports = class tokebot{ } } + /** + * Resets toke cooldowns early upon authorized request + */ resetToke(){ //Set cooldown to 0 this.cooldownCounter = 0;