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(){