diff --git a/src/controllers/api/channel/descriptionController.js b/src/controllers/api/channel/descriptionController.js new file mode 100644 index 0000000..9f30560 --- /dev/null +++ b/src/controllers/api/channel/descriptionController.js @@ -0,0 +1,90 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024-2025 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 channelModel = require('../../../schemas/channel/channelSchema'); +const {exceptionHandler} = require('../../../utils/loggerUtils'); + +//get thumby +module.exports.get = async function(req, res){ + try{ + //Pull validated result + const validResult = validationResult(req); + + //if everything validated proper + if(validResult.isEmpty()){ + //Get matched data + const data = matchedData(req); + //pull channel + const chanDB = await channelModel.findOne({name: data.chanName}); + + //Null check channel + if(chanDB == null){ + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); + } + + //return thumby + res.status(200); + return res.send({description: chanDB.description}); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} + +//Post function +module.exports.post = async function(req, res){ + try{ + //Pull validated result + const validResult = validationResult(req); + + //if everything validated proper + if(validResult.isEmpty()){ + //Get matched data + const data = matchedData(req); + //pull channel + const chanDB = await channelModel.findOne({name: data.chanName}); + + //Null check channel + if(chanDB == null){ + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); + } + + //Set thumbnail + chanDB.description = data.description; + + //Save channel doc + await chanDB.save(); + + //return thumby + res.status(200); + return res.send({description: chanDB.description}); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} \ No newline at end of file diff --git a/src/controllers/api/channel/thumbnailController.js b/src/controllers/api/channel/thumbnailController.js new file mode 100644 index 0000000..bfb66a3 --- /dev/null +++ b/src/controllers/api/channel/thumbnailController.js @@ -0,0 +1,90 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024-2025 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 channelModel = require('../../../schemas/channel/channelSchema'); +const {exceptionHandler} = require('../../../utils/loggerUtils'); + +//get thumby +module.exports.get = async function(req, res){ + try{ + //Pull validated result + const validResult = validationResult(req); + + //if everything validated proper + if(validResult.isEmpty()){ + //Get matched data + const data = matchedData(req); + //pull channel + const chanDB = await channelModel.findOne({name: data.chanName}); + + //Null check channel + if(chanDB == null){ + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); + } + + //return thumby + res.status(200); + return res.send({thumbnail: chanDB.thumbnail}); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} + +//Post function +module.exports.post = async function(req, res){ + try{ + //Pull validated result + const validResult = validationResult(req); + + //if everything validated proper + if(validResult.isEmpty()){ + //Get matched data + const data = matchedData(req); + //pull channel + const chanDB = await channelModel.findOne({name: data.chanName}); + + //Null check channel + if(chanDB == null){ + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); + } + + //Set thumbnail + chanDB.thumbnail = data.thumbnail; + + //Save channel doc + await chanDB.save(); + + //return thumby + res.status(200); + return res.send({thumbnail: chanDB.thumbnail}); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} \ No newline at end of file diff --git a/src/routers/api/channelRouter.js b/src/routers/api/channelRouter.js index d703816..9db0eff 100644 --- a/src/routers/api/channelRouter.js +++ b/src/routers/api/channelRouter.js @@ -22,7 +22,7 @@ const { Router } = require('express'); //Models const permissionModel = require("../../schemas/permissionSchema"); const channelModel = require("../../schemas/channel/channelSchema"); -//Valudators +//Validators const channelValidator = require("../../validators/channelValidator"); const accountValidator = require("../../validators/accountValidator"); const {channelPermissionValidator} = require("../../validators/permissionsValidator"); @@ -30,6 +30,8 @@ const tokebotValidator = require("../../validators/tokebotValidator"); const emoteValidator = require("../../validators/emoteValidator"); //Controllers const registerController = require("../../controllers/api/channel/registerController"); +const thumbnailController = require("../../controllers/api/channel/thumbnailController"); +const descriptionController = require("../../controllers/api/channel/descriptionController"); const listController = require("../../controllers/api/channel/listController"); const settingsController = require("../../controllers/api/channel/settingsController"); const permissionsController = require("../../controllers/api/channel/permissionsController") @@ -42,8 +44,10 @@ const emoteController = require('../../controllers/api/channel/emoteController') //globals const router = Router(); -//user authentication middleware +//Set validator functions router.use("/register",permissionModel.reqPermCheck("registerChannel")); +router.use("/thumbnail",channelValidator.name("chanName")); +router.use("/description",channelValidator.name("chanName")); router.use("/settings", channelValidator.name('chanName')); router.use("/permissions", channelValidator.name('chanName')); router.use("/rank", channelValidator.name('chanName')); @@ -55,6 +59,12 @@ router.use("/emote", channelValidator.name('chanName')); //routing functions //register router.post('/register', channelValidator.name(), channelValidator.description(), channelValidator.thumbnail(), registerController.post); +//Thumbnail +router.get('/thumbnail', thumbnailController.get); +router.post('/thumbnail', channelValidator.thumbnail(), thumbnailController.post); +//Description +router.get('/description', descriptionController.get); +router.post('/description', channelValidator.description(), descriptionController.post); //list router.get('/list', channelModel.reqPermCheck("manageChannel"), listController.get); //settings diff --git a/src/views/channelSettings.ejs b/src/views/channelSettings.ejs index 5a2780d..d94e44f 100644 --- a/src/views/channelSettings.ejs +++ b/src/views/channelSettings.ejs @@ -26,7 +26,7 @@ along with this program. If not, see . %> <%- include('partial/navbar', {user}); %>

<%- channel.name %> - Channel Settings

- <%- include('partial/channelSettings/description.ejs', {channel}); %> + <%- include('partial/channelSettings/info.ejs', {channel}); %> <%- include('partial/channelSettings/userList.ejs'); %> <%- include('partial/channelSettings/banList.ejs'); %> <%- include('partial/channelSettings/settings.ejs'); %> diff --git a/src/views/partial/channelSettings/description.ejs b/src/views/partial/channelSettings/info.ejs similarity index 100% rename from src/views/partial/channelSettings/description.ejs rename to src/views/partial/channelSettings/info.ejs diff --git a/www/css/adminPanel.css b/www/css/adminPanel.css index b570a16..4c07d36 100644 --- a/www/css/adminPanel.css +++ b/www/css/adminPanel.css @@ -68,8 +68,16 @@ img.admin-list-entry-item{ margin: 0; } +#channel-info-thumbnail { + max-width: 8em; + max-height: 8em; + align-self: center; +} + #channel-info-thumbnail-span div{ position: relative; + display: flex; + flex-direction: column; } #channel-info-description-span{ @@ -83,12 +91,12 @@ img.admin-list-entry-item{ left: calc(50% - 4.5em); } - #channel-info-description-prompt{ resize: vertical; width: 100%; min-height: 2em; max-height: 8em; + height: 8em; } #channel-rank-title{ diff --git a/www/js/channelSettings.js b/www/js/channelSettings.js index 1713335..ef94eeb 100644 --- a/www/js/channelSettings.js +++ b/www/js/channelSettings.js @@ -41,6 +41,7 @@ class chanInfo{ //Create description prompt this.descriptionInput = document.createElement("textarea"); this.descriptionInput.id = "channel-info-description-prompt"; + this.descriptionInput.value = this.description.textContent; //Setup Input Event Handlers this.setupInput(); @@ -58,50 +59,71 @@ class chanInfo{ this.thumbnailInput.style.display = enabled ? "" : "none"; if(enabled){ + //focus thumbnail input + this.thumbnailInput.focus(); + + //Remove interactive class from thumby this.thumbnail.classList.remove('interactive'); }else{ + //add interactive class to thumby this.thumbnail.classList.add('interactive'); } } - submitThumbnail(event){ + async submitThumbnail(event){ //If we hit didnt hit enter or escape if(!(event.key == "Enter" || event.key == "Escape") && event.key != null){ //bail! return; + //Only returns w/ content after this point + }else if(event.key == "Escape" && event.key != null){ + //Toggle prompt + this.toggleThumbnailPrompt(false); + return; } + //Send update off to server and wait for response + const data = await utils.ajax.setChannelThumbnail(this.channel, this.thumbnailInput.value); + + //Set new image from updated thumby + this.thumbnail.src = data.thumbnail; + //Toggle prompt this.toggleThumbnailPrompt(false); - - //Only returns after this point - if(event.key != "Enter" && event.key != null){ - return; - } } toggleDescriptionPrompt(enabled){ if(enabled){ this.description.replaceWith(this.descriptionInput); + this.descriptionInput.focus() }else{ this.descriptionInput.replaceWith(this.description); } } - submitDescription(event){ + async submitDescription(event){ //If we hit didnt hit enter (without shift) or escape if(!((event.key == "Enter" && !event.shiftKey) || event.key == "Escape") && event.key != null){ //bail! return; + }else if(event.key == "Escape" && event.key != null){ + //Toggle prompt + this.toggleDescriptionPrompt(false); + return; } + //Stop newline from being processed + event.preventDefault(); + + //Set Description + const data = await utils.ajax.setChannelDescription(this.channel, this.descriptionInput.value); + + //Unescape entities from server-side sanatization and safely put the newly made un-safe text inside of the element via .textContent. + //Ensuring sanatized content displays proper, and that any unsanatized content that some how made it through is still safe. + this.description.textContent = utils.unescapeEntities(data.description); + //Toggle prompt this.toggleDescriptionPrompt(false); - - //Only returns after this point - if(event.key != "Enter" && !event.shiftKey && event.key != null){ - return; - } } } diff --git a/www/js/utils.js b/www/js/utils.js index b43c5a6..e7bfce2 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -690,7 +690,7 @@ class canopyAjaxUtils{ constructor(){ - } + } //Account async register(user, pass, passConfirm, email, verification){ @@ -872,6 +872,40 @@ class canopyAjaxUtils{ } } + async setChannelThumbnail(chanName, thumbnail){ + var response = await fetch(`/api/channel/thumbnail`,{ + method: "POST", + headers: { + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() + }, + body: JSON.stringify({chanName, thumbnail}) + }); + + if(response.ok){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + + async setChannelDescription(chanName, description){ + var response = await fetch(`/api/channel/description`,{ + method: "POST", + headers: { + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() + }, + body: JSON.stringify({chanName, description}) + }); + + if(response.ok){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + async setChannelSetting(chanName, settingsMap){ var response = await fetch(`/api/channel/settings`,{ method: "POST",