From b9283607d6b4c24b4cf80f57a8a46b547bf3aa11 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 17 Dec 2024 20:44:14 -0500 Subject: [PATCH] Several improvements to chat pre/post processing --- src/app/channel/commandPreprocessor.js | 46 +---------- src/controllers/api/admin/emoteController.js | 19 +++-- src/routers/api/adminRouter.js | 2 + src/schemas/permissionSchema.js | 6 ++ src/utils/linkUtils.js | 70 ++++++++++++++++ src/validators/emoteValidator.js | 23 ++++++ www/css/theme/movie-night.css | 20 +++++ www/js/adminPanel.js | 17 ++++ www/js/channel/chat.js | 1 - www/js/channel/chatPostprocessor.js | 84 +++++++++++++++----- 10 files changed, 217 insertions(+), 71 deletions(-) create mode 100644 src/utils/linkUtils.js create mode 100644 src/validators/emoteValidator.js diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index 6b7710e..ba5f69a 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -19,6 +19,7 @@ const validator = require('validator');//No express here, so regular validator i //Local Imports const tokebot = require('./tokebot'); +const linkUtils = require('../../utils/linkUtils'); const permissionModel = require('../../schemas/permissionSchema'); const channelModel = require('../../schemas/channel/channelSchema'); @@ -39,6 +40,7 @@ module.exports = class commandPreprocessor{ //If we don't pass sanatization/validation turn this car around if(!this.sanatizeCommand()){ + console.log('test'); return; } @@ -87,56 +89,14 @@ module.exports = class commandPreprocessor{ } async markLinks(){ - const maxSize = 4000000; //Empty out the links array this.links = []; //For each link sent from the client //this.rawData.links.forEach((link) => { for (const link of this.rawData.links){ - //Set standard link type - var type = 'link'; - - //Don't try this at home, we're what you call "Experts" - //TODO: Handle this shit simultaneously and send the chat before its done, then send updated types for each link as they're pulled individually - try{ - //Pull content type - var response = await fetch(link,{ - method: "HEAD", - }); - - //Get file type from header - const fileType = response.headers.get('content-type'); - const fileSize = response.headers.get('content-length'); - - //If they're reporting file types - if(fileType != null){ - //If we have an image - if(fileType.match('image/')){ - //If the file size is unreported OR it's smaller than 4MB (not all servers report this and images that big are pretty rare) - if(fileSize == null || fileSize <= maxSize){ - //Mark link as an image - type = 'image'; - } - //If it's a video - }else if(fileType.match('video/mp4' || 'video/webm')){ - //If the server is reporting file-size and it's reporting under 4MB (Reject unreported sizes to be on the safe side is video is huge) - if(fileSize != null && fileSize <= maxSize){ - //mark link as a video - type = 'video'; - } - } - } - //Probably bad form but if something happens in here I'm blaming whoever hosted the link - //maybe don't host a fucked up server and I wouldn't handle with an empty catch - }catch{}; - //Add a marked link object to our links array - this.links.push({ - link, - type - }); - + this.links.push(await linkUtils.markLink(link)); } } diff --git a/src/controllers/api/admin/emoteController.js b/src/controllers/api/admin/emoteController.js index 2fdf793..dbd0680 100644 --- a/src/controllers/api/admin/emoteController.js +++ b/src/controllers/api/admin/emoteController.js @@ -20,6 +20,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); const emoteModel = require('../../../schemas/emoteSchema'); +const linkUtils = require('../../../utils/linkUtils'); module.exports.get = async function(req, res){ try{ @@ -39,14 +40,22 @@ module.exports.post = async function(req, res){ //if they're empty if(validResult.isEmpty()){ - /* - const {command} = matchedData(req); - const tokeDB = await tokeCommandModel.findOne({command}); + //get matched data + const {name, link} = matchedData(req); + //query for existing emote + const emoteDB = await emoteModel.findOne({name}); - if(tokeDB != null){ - return errorHandler(res, `Toke command '!${command}' already exists!`); + //if we have one + if(emoteDB != null){ + //Throw a shit fit + return errorHandler(res, `Emote '[${name}]' already exists!`); } + const linkObj = linkUtils.markLink(link); + + console.log(linkObj); + + /* //Add the toke await tokeCommandModel.create({command}); diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js index 687fe4e..95c2cbd 100644 --- a/src/routers/api/adminRouter.js +++ b/src/routers/api/adminRouter.js @@ -23,6 +23,7 @@ const { Router } = require('express'); const accountValidator = require("../../validators/accountValidator"); const {permissionsValidator, channelPermissionValidator} = require("../../validators/permissionsValidator"); const tokebotValidator = require("../../validators/tokebotValidator"); +const emoteValidator = require("../../validators/emoteValidator"); const permissionSchema = require("../../schemas/permissionSchema"); const listUsersController = require("../../controllers/api/admin/listUsersController"); const listChannelsController = require("../../controllers/api/admin/listChannelsController"); @@ -56,5 +57,6 @@ router.post('/tokeCommands', permissionSchema.reqPermCheck("editTokeCommands"), router.delete('/tokeCommands', permissionSchema.reqPermCheck("editTokeCommands"), tokebotValidator.command(), tokeCommandController.delete); //emote router.get('/emote', permissionSchema.reqPermCheck('adminPanel'), emoteController.get); +router.post('/emote', permissionSchema.reqPermCheck('editEmotes'), emoteValidator.name(), emoteValidator.link(), emoteController.post); module.exports = router; diff --git a/src/schemas/permissionSchema.js b/src/schemas/permissionSchema.js index 7068518..5cf8e5b 100644 --- a/src/schemas/permissionSchema.js +++ b/src/schemas/permissionSchema.js @@ -81,6 +81,12 @@ const permissionSchema = new mongoose.Schema({ default: "admin", required: true }, + editEmotes: { + type: mongoose.SchemaTypes.String, + enum: rankEnum, + default: "admin", + required: true + }, channelOverrides: { type: channelPermissionSchema, default: () => ({}) diff --git a/src/utils/linkUtils.js b/src/utils/linkUtils.js new file mode 100644 index 0000000..120379d --- /dev/null +++ b/src/utils/linkUtils.js @@ -0,0 +1,70 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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');//No express here, so regular validator it is! + +module.exports.markLink = async function(link){ + //Set max file size to 4MB + const maxSize = 4000000; + //Set badLink type + var type = 'deadLink'; + + //Make sure we have an actual, factual URL + if(validator.isURL(link)){ + //Don't try this at home, we're what you call "Experts" + //TODO: Handle this shit simultaneously and send the chat before its done, then send updated types for each link as they're pulled individually + try{ + //Pull content type + var response = await fetch(link,{ + method: "HEAD", + }); + + //If we made it this far then the link is, at the very least, not dead. + type = 'link' + + //Get file type from header + const fileType = response.headers.get('content-type'); + const fileSize = response.headers.get('content-length'); + + //If they're reporting file types + if(fileType != null){ + //If we have an image + if(fileType.match('image/')){ + //If the file size is unreported OR it's smaller than 4MB (not all servers report this and images that big are pretty rare) + if(fileSize == null || fileSize <= maxSize){ + //Mark link as an image + type = 'image'; + } + //If it's a video + }else if(fileType.match('video/mp4' || 'video/webm')){ + //If the server is reporting file-size and it's reporting under 4MB (Reject unreported sizes to be on the safe side is video is huge) + if(fileSize != null && fileSize <= maxSize){ + //mark link as a video + type = 'video'; + } + } + } + //Probably bad form but if something happens in here I'm blaming whoever hosted the link + //maybe don't host a fucked up server and I wouldn't handle with an empty catch + }catch{}; + } + + return { + link, + type + } +} \ No newline at end of file diff --git a/src/validators/emoteValidator.js b/src/validators/emoteValidator.js new file mode 100644 index 0000000..74edeef --- /dev/null +++ b/src/validators/emoteValidator.js @@ -0,0 +1,23 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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 { check } = require('express-validator'); + +module.exports = { + name: (field = 'name') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 30}), + link: (field = 'link') => check(field).trim().isURL() +} \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index ab0b1df..3642b77 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -122,6 +122,20 @@ button:active{ box-shadow: var(--danger-glow0-alt1); } +.danger-link{ + color: var(--danger0); +} + +.danger-link:hover{ + color: var(--danger0-alt1); + text-shadow: var(--danger-glow0); +} + +.danger-link:active{ + color: var(--danger0-alt0); + text-shadow: var(--danger-glow0-alt1); +} + .positive-button{ background-color: var(--focus0); color: white; @@ -297,6 +311,12 @@ select.panel-head-element{ .chat-link{ color: var(--focus0); + user-select: all; +} + +.chat-dead-link{ + text-decoration: line-through; + text-decoration-color: var(--danger0-alt0); } /* popup */ diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js index d183bf9..411e76b 100644 --- a/www/js/adminPanel.js +++ b/www/js/adminPanel.js @@ -227,6 +227,23 @@ class canopyAdminUtils{ utils.ux.displayResponseError(await response.json()); } } + + async addEmote(name, link){ + var response = await fetch(`/api/admin/emote`,{ + method: "POST", + headers: { + "Content-Type": "application/json" + }, + //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... + body: JSON.stringify({name, link}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } } class adminUserList{ diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index f1955a1..fc1fb10 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -122,7 +122,6 @@ class chatBox{ //Create chat body var chatBody = document.createElement('p'); chatBody.classList.add("chat-panel-buffer","chat-entry-body"); - chatBody.innerHTML = data.msg; chatEntry.appendChild(chatBody); diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 6acb49e..241a02d 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -29,8 +29,8 @@ class chatPostprocessor{ splitBody(){ //Create an empty array to hold the body this.bodyArray = []; - //Split string by word-boundries, with negative/forward lookaheads to exclude file seperators so we don't split link placeholders - const splitString = this.chatBody.innerHTML.split(/(? { @@ -46,23 +46,39 @@ class chatPostprocessor{ } injectBody(){ - //Clear our chat body - this.chatBody.innerHTML = ""; - - //Extract strings into the empty array + //Create an empty array to hold the objects to inject + const injectionArray = [""]; + + const _this = this; + //For each word object this.bodyArray.forEach((wordObj) => { if(wordObj.type == 'word'){ //Inject current wordObj string into the chat body - this.chatBody.innerHTML += wordObj.string + //this doesnt work right with escaped strings atm + //we should make an array that contains all the strings split by nodes + //so text can be added in word chunks, allowing escaped strings and + //node injections w/o adding them as html instead of an appended node + injectString(wordObj.string); }else if(wordObj.type == 'link'){ //Create a link node from our link const link = document.createElement('a'); link.classList.add('chat-link'); link.href = wordObj.link; - link.innerHTML = wordObj.link; + //Use textContent to be safe since links can't be escaped + link.textContent = wordObj.link; //Append node to chatBody - this.injectNode(wordObj, link); + injectNode(wordObj, link); + }else if(wordObj.type == 'deadLink'){ + //Create a text span node from our link + const badLink = document.createElement('a'); + badLink.classList.add('chat-dead-link', 'danger-link'); + badLink.href = wordObj.link; + //Use textContent to be safe since links can't be escaped + badLink.textContent = wordObj.link; + + //Append node to chatBody + injectNode(wordObj, badLink); }else if(wordObj.type == 'image'){ //Create an img node from our link const img = document.createElement('img'); @@ -71,7 +87,7 @@ class chatPostprocessor{ //stringArray.push(wordObj.string.replace('␜',img.outerHTML)); //Append node to chatBody - this.injectNode(wordObj, img); + injectNode(wordObj, img); }else if(wordObj.type == 'video'){ //Create a video node from our link const vid = document.createElement('video'); @@ -83,24 +99,48 @@ class chatPostprocessor{ vid.muted = true; //stringArray.push(wordObj.string.replace('␜',vid.outerHTML)); - this.injectNode(wordObj, vid); + injectNode(wordObj, vid); } }); - } - //Like string.replace except it actually injects the node so we can keep things like event handlers - injectNode(wordObj, node, placeholder = '␜'){ - //Split string by the placeholder so we can keep surrounding whitespace - const splitWord = wordObj.string.split(placeholder, 2); + //For each item found in the injection array + injectionArray.forEach((item) => { + //if it's a string + if(typeof item == "string"){ + //Add it to the chat's innerHTML (it should already be escaped by the server) + this.chatBody.innerHTML += item; + }else{ + //Otherwise it should be a DOM node, therefore we should append it + this.chatBody.appendChild(item); + } + }) - //Append the first half of the string - this.chatBody.innerHTML += splitWord[0]; - //Append the node - this.chatBody.appendChild(node); + //Like string.replace except it actually injects the node so we can keep things like event handlers + function injectNode(wordObj, node, placeholder = '␜'){ + //Split string by the placeholder so we can keep surrounding whitespace + const splitWord = wordObj.string.split(placeholder, 2); - //Append the second half of the string - this.chatBody.innerHTML += splitWord[1]; + //Append the first half of the string + injectString(splitWord[0]); + + //Append the node + injectionArray.push(node); + + //Append the second half of the string + injectString(splitWord[1]); + } + + function injectString(string){ + //If the last item was a string + if(typeof injectionArray[injectionArray.length - 1] == "string"){ + //add the word string on to the end of the string + injectionArray[injectionArray.length - 1] += string; + }else{ + //Pop the string at the end of the array + injectionArray.push(string); + } + } } addWhitespace(){