diff --git a/defaultEmotes.json b/defaultEmotes.json new file mode 100644 index 0000000..6bb9ae5 --- /dev/null +++ b/defaultEmotes.json @@ -0,0 +1,19 @@ +{ + "default": { + "name": "[bill]", + "link": "https://upload.wikimedia.org/wikipedia/en/thumb/9/9b/Bill_Dauterive.png/150px-Bill_Dauterive.png", + "type": "image" + }, + "array": [ + { + "name": "[crabrave]", + "link": "https://media.tenor.com/PqFN1orijJ4AAAAC/crab-sneeth.gif", + "type": "image" + }, + { + "name": "[homerbushes]", + "link": "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExb3gydGNrcnF4OWthbDg1c2RxczU4cTUzaGJsb3Bmazdsa3F5NWwxOSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9dg/O4B7pH57BbdVZVcNgS/giphy.mp4", + "type": "video" + } + ] +} diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index 547904f..6b7710e 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -95,7 +95,7 @@ module.exports = class commandPreprocessor{ //this.rawData.links.forEach((link) => { for (const link of this.rawData.links){ //Set standard link type - var linkType = 'link'; + 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 @@ -116,14 +116,14 @@ module.exports = class commandPreprocessor{ //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 - linkType = '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 - linkType = 'video'; + type = 'video'; } } } @@ -134,7 +134,7 @@ module.exports = class commandPreprocessor{ //Add a marked link object to our links array this.links.push({ link, - type: linkType + type }); } diff --git a/src/controllers/api/admin/emoteController.js b/src/controllers/api/admin/emoteController.js new file mode 100644 index 0000000..2fdf793 --- /dev/null +++ b/src/controllers/api/admin/emoteController.js @@ -0,0 +1,96 @@ +/*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 {validationResult, matchedData} = require('express-validator'); + +//local imports +const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); +const emoteModel = require('../../../schemas/emoteSchema'); + +module.exports.get = async function(req, res){ + try{ + const emoteList = await emoteModel.getEmotes(); + + res.status(200); + return res.send(emoteList); + }catch(err){ + return exceptionHandler(res, err); + } +} + +module.exports.post = async function(req, res){ + try{ + //get validation error results + const validResult = validationResult(req); + + //if they're empty + if(validResult.isEmpty()){ + /* + const {command} = matchedData(req); + const tokeDB = await tokeCommandModel.findOne({command}); + + if(tokeDB != null){ + return errorHandler(res, `Toke command '!${command}' already exists!`); + } + + //Add the toke + await tokeCommandModel.create({command}); + + //Return the updated command list + res.status(200); + return res.send(await tokeCommandModel.getCommandStrings()); + */ + }else{ + //otherwise scream + res.status(400); + return res.send({errors: validResult.array()}) + } + }catch(err){ + return exceptionHandler(res, err); + } +} + +module.exports.delete = async function(req, res){ + try{ + //get validation error results + const validResult = validationResult(req); + + //if they're empty + if(validResult.isEmpty()){ + /* + const {command} = matchedData(req); + const tokeDB = await tokeCommandModel.findOne({command}); + + if(tokeDB == null){ + return errorHandler(res, `Cannot delete non-existant toke command '!${command}'!`); + } + + await tokeDB.deleteOne(); + + //Return the updated command list + res.status(200); + return res.send(await tokeCommandModel.getCommandStrings()); + */ + }else{ + //otherwise scream + res.status(400); + return res.send({errors: validResult.array()}) + } + }catch(err){ + return exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js index 048d9c0..687fe4e 100644 --- a/src/routers/api/adminRouter.js +++ b/src/routers/api/adminRouter.js @@ -30,22 +30,31 @@ const changeRankController = require("../../controllers/api/admin/changeRankCont const permissionsController = require("../../controllers/api/admin/permissionsController"); const banController = require("../../controllers/api/admin/banController"); const tokeCommandController = require('../../controllers/api/admin/tokeCommandController'); +const emoteController = require('../../controllers/api/admin/emoteController'); //globals const router = Router(); //routing functions +//listUsers router.get('/listUsers', permissionSchema.reqPermCheck("adminPanel"), listUsersController.get); +//listChannels router.get('/listChannels', permissionSchema.reqPermCheck("adminPanel"), listChannelsController.get); +//permissions router.get('/permissions', permissionSchema.reqPermCheck("adminPanel"), permissionsController.get); router.post('/permissions', permissionSchema.reqPermCheck("changePerms"), checkExact([permissionsValidator.permissionsMap(), channelPermissionValidator.channelPermissionsMap()]), permissionsController.post); +//changeRank router.post('/changeRank', permissionSchema.reqPermCheck("changeRank"), accountValidator.user(), accountValidator.rank(), changeRankController.post); +//Ban router.get('/ban', permissionSchema.reqPermCheck("adminPanel"), banController.get); //Sometimes they're so simple you don't need to put your validators in their own special place :P router.post('/ban', permissionSchema.reqPermCheck("banUser"), accountValidator.user(), body("permanent").isBoolean(), body("expirationDays").isInt(), banController.post); router.delete('/ban', permissionSchema.reqPermCheck("banUser"), accountValidator.user(), banController.delete); +//TokeCommands router.get('/tokeCommands', permissionSchema.reqPermCheck("adminPanel"), tokeCommandController.get); router.post('/tokeCommands', permissionSchema.reqPermCheck("editTokeCommands"), tokebotValidator.command(), tokeCommandController.post); router.delete('/tokeCommands', permissionSchema.reqPermCheck("editTokeCommands"), tokebotValidator.command(), tokeCommandController.delete); +//emote +router.get('/emote', permissionSchema.reqPermCheck('adminPanel'), emoteController.get); module.exports = router; diff --git a/src/schemas/emoteSchema.js b/src/schemas/emoteSchema.js new file mode 100644 index 0000000..af38371 --- /dev/null +++ b/src/schemas/emoteSchema.js @@ -0,0 +1,92 @@ +/*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 {mongoose} = require('mongoose'); + +//Local Imports +const defaultEmote = require("../../defaultEmotes.json"); + +const typeEnum = ["image", "video"]; + +const emoteSchema = new mongoose.Schema({ + name:{ + type: mongoose.SchemaTypes.String, + required: true + }, + link:{ + type: mongoose.SchemaTypes.String, + required: true + }, + type:{ + type: mongoose.SchemaTypes.String, + required: true, + enum: typeEnum, + default: typeEnum[0] + } +}); + +emoteSchema.statics.loadDefaults = async function(){ + //Make sure registerEmote function is happy + const _this = this; + + //Ensure default comes first (.bind(this) doesn't seem to work here...) + await registerEmote(defaultEmote.default); + //For each entry in the defaultEmote.json file + defaultEmote.array.forEach(registerEmote); + + async function registerEmote(emote){ + try{ + //Look for emote matching the one from our file + const foundEmote = await _this.findOne({name: emote.name}); + + //if the emote doesn't exist + if(!foundEmote){ + const emoteDB = await _this.create(emote); + console.log(`Loading default emote '${emote.name}' into DB from defaultEmote.json`); + } + + }catch(err){ + if(emote != null){ + console.log(err); + console.log(`Error loading emote '${emote.name}':`); + }else{ + console.log("Error, null emote:"); + } + } + } +} + +emoteSchema.statics.getEmotes = async function(){ + //Create an empty array to hold our emote list + const emoteList = []; + //Pull emotes from database + const emoteDB = await this.find({}); + + emoteDB.forEach((emote) => { + emoteList.push({ + name: emote.name, + link: emote.link, + type: emote.type + }); + }); + + return emoteList; +} + +emoteSchema.statics.typeEnum = typeEnum; + +module.exports = mongoose.model("emote", emoteSchema); \ No newline at end of file diff --git a/src/server.js b/src/server.js index ed08e97..1525b31 100644 --- a/src/server.js +++ b/src/server.js @@ -28,6 +28,7 @@ const channelManager = require('./app/channel/channelManager'); const scheduler = require('./utils/scheduler'); const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); +const emoteModel = require('./schemas/emoteSchema'); const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema'); const indexRouter = require('./routers/indexRouter'); const registerRouter = require('./routers/registerRouter'); @@ -114,6 +115,9 @@ statModel.incrementLaunchCount(); //Load default flairs flairModel.loadDefaults(); +//Load default emots +emoteModel.loadDefaults(); + //Load default toke commands tokeCommandModel.loadDefaults(); diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js index 2620646..d183bf9 100644 --- a/www/js/adminPanel.js +++ b/www/js/adminPanel.js @@ -215,6 +215,18 @@ class canopyAdminUtils{ utils.ux.displayResponseError(await response.json()); } } + + async getEmotes(){ + var response = await fetch(`/api/admin/emote`,{ + method: "GET" + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } } class adminUserList{ diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 01800be..6acb49e 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -30,7 +30,7 @@ class chatPostprocessor{ //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,13 +46,14 @@ class chatPostprocessor{ } injectBody(){ - //Create empty array to hold strings - var stringArray = []; + //Clear our chat body + this.chatBody.innerHTML = ""; //Extract strings into the empty array this.bodyArray.forEach((wordObj) => { if(wordObj.type == 'word'){ - stringArray.push(wordObj.string); + //Inject current wordObj string into the chat body + this.chatBody.innerHTML += wordObj.string }else if(wordObj.type == 'link'){ //Create a link node from our link const link = document.createElement('a'); @@ -60,16 +61,17 @@ class chatPostprocessor{ link.href = wordObj.link; link.innerHTML = wordObj.link; - //Inject it into the original string, and add it to string array - stringArray.push(wordObj.string.replace('␜',link.outerHTML)); + //Append node to chatBody + this.injectNode(wordObj, link); }else if(wordObj.type == 'image'){ //Create an img node from our link const img = document.createElement('img'); img.classList.add('chat-img'); img.src = wordObj.link; - //Inject it into the original string, and add it to string array - stringArray.push(wordObj.string.replace('␜',img.outerHTML)); + //stringArray.push(wordObj.string.replace('␜',img.outerHTML)); + //Append node to chatBody + this.injectNode(wordObj, img); }else if(wordObj.type == 'video'){ //Create a video node from our link const vid = document.createElement('video'); @@ -78,14 +80,27 @@ class chatPostprocessor{ vid.controls = false; vid.autoplay = true; vid.loop = true; + vid.muted = true; - //Inject it into the original string, and add it to string array - stringArray.push(wordObj.string.replace('␜',vid.outerHTML)); + //stringArray.push(wordObj.string.replace('␜',vid.outerHTML)); + this.injectNode(wordObj, vid); } }); + } - //Join the strings to fill the chat entry - this.chatBody.innerHTML = stringArray.join(""); + //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); + + //Append the first half of the string + this.chatBody.innerHTML += splitWord[0]; + + //Append the node + this.chatBody.appendChild(node); + + //Append the second half of the string + this.chatBody.innerHTML += splitWord[1]; } addWhitespace(){ @@ -120,12 +135,12 @@ class chatPostprocessor{ this.rawData.links.forEach((link, linkIndex) => { this.bodyArray.forEach((wordObj, wordIndex) => { //Check current wordobj for link (placeholder may contain whitespace with it) - if(wordObj.string.match(`␜${linkIndex}␜`)){ + if(wordObj.string.match(`␜${linkIndex}`)){ //Set current word object in the body array to the new link object this.bodyArray[wordIndex] = { //Don't want to use a numbered placeholder to make this easier during body injection //but we also don't want to clobber any surrounding whitespace - string: wordObj.string.replace(`␜${linkIndex}␜`, '␜'), + string: wordObj.string.replace(`␜${linkIndex}`, '␜'), link: link.link, type: link.type } diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index 2f8b158..34e2410 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -51,7 +51,7 @@ class commandPreprocessor{ //I looked online for obscure characters that no one would use to prevent people from chatting embed placeholders //Then I found this fucker, turns out it's literally made for the job lmao (even if it was originally intended for paper/magnetic tape) //Replace link with indexed placeholder - splitMessage[chunkIndex] = `␜${this.links.length}␜` + splitMessage[chunkIndex] = `␜${this.links.length}` //push current chunk as link this.links.push(chunk);