Several improvements to chat pre/post processing

This commit is contained in:
rainbow napkin 2024-12-17 20:44:14 -05:00
parent 12922658b9
commit b9283607d6
10 changed files with 217 additions and 71 deletions

View file

@ -19,6 +19,7 @@ const validator = require('validator');//No express here, so regular validator i
//Local Imports //Local Imports
const tokebot = require('./tokebot'); const tokebot = require('./tokebot');
const linkUtils = require('../../utils/linkUtils');
const permissionModel = require('../../schemas/permissionSchema'); const permissionModel = require('../../schemas/permissionSchema');
const channelModel = require('../../schemas/channel/channelSchema'); 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 we don't pass sanatization/validation turn this car around
if(!this.sanatizeCommand()){ if(!this.sanatizeCommand()){
console.log('test');
return; return;
} }
@ -87,56 +89,14 @@ module.exports = class commandPreprocessor{
} }
async markLinks(){ async markLinks(){
const maxSize = 4000000;
//Empty out the links array //Empty out the links array
this.links = []; this.links = [];
//For each link sent from the client //For each link sent from the client
//this.rawData.links.forEach((link) => { //this.rawData.links.forEach((link) => {
for (const link of this.rawData.links){ 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 //Add a marked link object to our links array
this.links.push({ this.links.push(await linkUtils.markLink(link));
link,
type
});
} }
} }

View file

@ -20,6 +20,7 @@ const {validationResult, matchedData} = require('express-validator');
//local imports //local imports
const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
const emoteModel = require('../../../schemas/emoteSchema'); const emoteModel = require('../../../schemas/emoteSchema');
const linkUtils = require('../../../utils/linkUtils');
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){
try{ try{
@ -39,14 +40,22 @@ module.exports.post = async function(req, res){
//if they're empty //if they're empty
if(validResult.isEmpty()){ if(validResult.isEmpty()){
/* //get matched data
const {command} = matchedData(req); const {name, link} = matchedData(req);
const tokeDB = await tokeCommandModel.findOne({command}); //query for existing emote
const emoteDB = await emoteModel.findOne({name});
if(tokeDB != null){ //if we have one
return errorHandler(res, `Toke command '!${command}' already exists!`); 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 //Add the toke
await tokeCommandModel.create({command}); await tokeCommandModel.create({command});

View file

@ -23,6 +23,7 @@ const { Router } = require('express');
const accountValidator = require("../../validators/accountValidator"); const accountValidator = require("../../validators/accountValidator");
const {permissionsValidator, channelPermissionValidator} = require("../../validators/permissionsValidator"); const {permissionsValidator, channelPermissionValidator} = require("../../validators/permissionsValidator");
const tokebotValidator = require("../../validators/tokebotValidator"); const tokebotValidator = require("../../validators/tokebotValidator");
const emoteValidator = require("../../validators/emoteValidator");
const permissionSchema = require("../../schemas/permissionSchema"); const permissionSchema = require("../../schemas/permissionSchema");
const listUsersController = require("../../controllers/api/admin/listUsersController"); const listUsersController = require("../../controllers/api/admin/listUsersController");
const listChannelsController = require("../../controllers/api/admin/listChannelsController"); 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); router.delete('/tokeCommands', permissionSchema.reqPermCheck("editTokeCommands"), tokebotValidator.command(), tokeCommandController.delete);
//emote //emote
router.get('/emote', permissionSchema.reqPermCheck('adminPanel'), emoteController.get); router.get('/emote', permissionSchema.reqPermCheck('adminPanel'), emoteController.get);
router.post('/emote', permissionSchema.reqPermCheck('editEmotes'), emoteValidator.name(), emoteValidator.link(), emoteController.post);
module.exports = router; module.exports = router;

View file

@ -81,6 +81,12 @@ const permissionSchema = new mongoose.Schema({
default: "admin", default: "admin",
required: true required: true
}, },
editEmotes: {
type: mongoose.SchemaTypes.String,
enum: rankEnum,
default: "admin",
required: true
},
channelOverrides: { channelOverrides: {
type: channelPermissionSchema, type: channelPermissionSchema,
default: () => ({}) default: () => ({})

70
src/utils/linkUtils.js Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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()
}

View file

@ -122,6 +122,20 @@ button:active{
box-shadow: var(--danger-glow0-alt1); 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{ .positive-button{
background-color: var(--focus0); background-color: var(--focus0);
color: white; color: white;
@ -297,6 +311,12 @@ select.panel-head-element{
.chat-link{ .chat-link{
color: var(--focus0); color: var(--focus0);
user-select: all;
}
.chat-dead-link{
text-decoration: line-through;
text-decoration-color: var(--danger0-alt0);
} }
/* popup */ /* popup */

View file

@ -227,6 +227,23 @@ class canopyAdminUtils{
utils.ux.displayResponseError(await response.json()); 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{ class adminUserList{

View file

@ -122,7 +122,6 @@ class chatBox{
//Create chat body //Create chat body
var chatBody = document.createElement('p'); var chatBody = document.createElement('p');
chatBody.classList.add("chat-panel-buffer","chat-entry-body"); chatBody.classList.add("chat-panel-buffer","chat-entry-body");
chatBody.innerHTML = data.msg;
chatEntry.appendChild(chatBody); chatEntry.appendChild(chatBody);

View file

@ -29,8 +29,8 @@ class chatPostprocessor{
splitBody(){ splitBody(){
//Create an empty array to hold the body //Create an empty array to hold the body
this.bodyArray = []; this.bodyArray = [];
//Split string by word-boundries, with negative/forward lookaheads to exclude file seperators so we don't split link placeholders //Split string by word-boundries, with negative lookaheads to exclude file seperators so we don't split link placeholders
const splitString = this.chatBody.innerHTML.split(/(?<!␜)\b/g); const splitString = this.rawData.msg.split(/(?<!␜)\b/g);
//for each word in the splitstring //for each word in the splitstring
splitString.forEach((string) => { splitString.forEach((string) => {
@ -46,23 +46,39 @@ class chatPostprocessor{
} }
injectBody(){ injectBody(){
//Clear our chat body //Create an empty array to hold the objects to inject
this.chatBody.innerHTML = ""; const injectionArray = [""];
//Extract strings into the empty array const _this = this;
//For each word object
this.bodyArray.forEach((wordObj) => { this.bodyArray.forEach((wordObj) => {
if(wordObj.type == 'word'){ if(wordObj.type == 'word'){
//Inject current wordObj string into the chat body //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'){ }else if(wordObj.type == 'link'){
//Create a link node from our link //Create a link node from our link
const link = document.createElement('a'); const link = document.createElement('a');
link.classList.add('chat-link'); link.classList.add('chat-link');
link.href = wordObj.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 //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'){ }else if(wordObj.type == 'image'){
//Create an img node from our link //Create an img node from our link
const img = document.createElement('img'); const img = document.createElement('img');
@ -71,7 +87,7 @@ class chatPostprocessor{
//stringArray.push(wordObj.string.replace('␜',img.outerHTML)); //stringArray.push(wordObj.string.replace('␜',img.outerHTML));
//Append node to chatBody //Append node to chatBody
this.injectNode(wordObj, img); injectNode(wordObj, img);
}else if(wordObj.type == 'video'){ }else if(wordObj.type == 'video'){
//Create a video node from our link //Create a video node from our link
const vid = document.createElement('video'); const vid = document.createElement('video');
@ -83,24 +99,48 @@ class chatPostprocessor{
vid.muted = true; vid.muted = true;
//stringArray.push(wordObj.string.replace('␜',vid.outerHTML)); //stringArray.push(wordObj.string.replace('␜',vid.outerHTML));
this.injectNode(wordObj, vid); injectNode(wordObj, vid);
} }
}); });
//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);
} }
})
//Like string.replace except it actually injects the node so we can keep things like event handlers //Like string.replace except it actually injects the node so we can keep things like event handlers
injectNode(wordObj, node, placeholder = '␜'){ function injectNode(wordObj, node, placeholder = '␜'){
//Split string by the placeholder so we can keep surrounding whitespace //Split string by the placeholder so we can keep surrounding whitespace
const splitWord = wordObj.string.split(placeholder, 2); const splitWord = wordObj.string.split(placeholder, 2);
//Append the first half of the string //Append the first half of the string
this.chatBody.innerHTML += splitWord[0]; injectString(splitWord[0]);
//Append the node //Append the node
this.chatBody.appendChild(node); injectionArray.push(node);
//Append the second half of the string //Append the second half of the string
this.chatBody.innerHTML += splitWord[1]; 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(){ addWhitespace(){