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",