diff --git a/src/app/channel/activeChannel.js b/src/app/channel/activeChannel.js
index 7abbc5b..cc26724 100644
--- a/src/app/channel/activeChannel.js
+++ b/src/app/channel/activeChannel.js
@@ -56,6 +56,7 @@ module.exports = class{
//await this.sendClientMetadata(userDB, socket);
await userObj.sendClientMetadata();
await userObj.sendSiteEmotes();
+ await userObj.sendChanEmotes();
//Send out the userlist
this.broadcastUserList(socket.chan);
@@ -101,4 +102,18 @@ module.exports = class{
this.server.io.in(this.name).emit("userList", userList);
}
+
+ async broadcastChanEmotes(chanDB){
+ //if we wherent handed a channel document
+ if(chanDB == null){
+ //Pull it based on channel name
+ chanDB = await channelModel.findOne({name: this.name});
+ }
+
+ //Get emote list from channel document
+ const emoteList = chanDB.getEmotes();
+
+ //Broadcast that sumbitch
+ this.server.io.in(this.name).emit('chanEmotes', emoteList);
+ }
}
\ No newline at end of file
diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js
index 6977e66..53aaa12 100644
--- a/src/app/channel/connectedUser.js
+++ b/src/app/channel/connectedUser.js
@@ -15,9 +15,10 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .*/
//local imports
+const channelModel = require('../../schemas/channel/channelSchema');
+const permissionModel = require('../../schemas/permissionSchema');
const flairModel = require('../../schemas/flairSchema');
const emoteModel = require('../../schemas/emoteSchema');
-const permissionModel = require('../../schemas/permissionSchema');
module.exports = class{
constructor(userDB, chanRank, channel, socket){
@@ -96,6 +97,20 @@ module.exports = class{
this.emit('siteEmotes', emoteList);
}
+ async sendChanEmotes(chanDB){
+ //if we wherent handed a channel document
+ if(chanDB == null){
+ //Pull it based on channel name
+ chanDB = await channelModel.findOne({name: this.channel.name});
+ }
+
+ //Pull emotes from channel
+ const emoteList = chanDB.getEmotes();
+
+ //Send it off to the user
+ this.emit('chanEmotes', emoteList);
+ }
+
updateFlair(flair){
this.flair = flair;
diff --git a/src/controllers/api/channel/emoteController.js b/src/controllers/api/channel/emoteController.js
new file mode 100644
index 0000000..7369cf3
--- /dev/null
+++ b/src/controllers/api/channel/emoteController.js
@@ -0,0 +1,167 @@
+/*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 channelModel = require('../../../schemas/channel/channelSchema');
+const linkUtils = require('../../../utils/linkUtils');
+
+module.exports.get = async function(req, res){
+ try{
+ //get validation error results
+ const validResult = validationResult(req);
+
+ //if they're empty
+ if(validResult.isEmpty()){
+ //Pull sanatized/validated input
+ const data = matchedData(req);
+ //Get the requested channel
+ const chanDB = await channelModel.findOne({name: data.chanName});
+
+ //If the requested channel doesn't exist
+ if(chanDB == null){
+ //ACK
+ return errorHandler(res, "Channel not found.", "Bad Query");
+ }
+
+ //Return found channel's emotes
+ res.status(200);
+ return res.send(chanDB.getEmotes());
+ }else{
+ //otherwise scream
+ res.status(400);
+ return res.send({errors: validResult.array()})
+ }
+ }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()){
+ //get matched data
+ const {chanName, emoteName, link} = matchedData(req);
+ //query for requested channel
+ const chanDB = await channelModel.findOne({name: chanName});
+
+ //If the requested channel doesn't exist
+ if(chanDB == null){
+ //ACK
+ return errorHandler(res, "Channel not found.", "Bad Query");
+ }
+
+ //Filter emote list for...
+ const foundEmotes = chanDB.emotes.filter((emote) => {
+ //emotes with a name that matches emoteName
+ return emote.name == emoteName;
+ });
+
+ //If we found one
+ if(foundEmotes.length > 0){
+ //ACK
+ return errorHandler(res, "Emote already exists!");
+ }
+
+ //Initialize the emote object from linkUtils
+ var emoteObj = await linkUtils.markLink(link);
+
+ //If we didn't get a valid image/video file
+ if(emoteObj.type != 'image' && emoteObj.type != 'video'){
+ //AAAAAAAAAAAAAAAAAA
+ return errorHandler(res, 'URL either does not lead to a valid image/video file, or leads to one that is larger than 4MB');
+ }
+
+
+ //Add the name to the emoteObj
+ emoteObj.name = emoteName;
+
+ //Push the newly created emote object into the channels emotes list
+ chanDB.emotes.push(emoteObj);
+
+ //Save the channel DB document
+ chanDB.save();
+
+ //Return the updated command list
+ res.status(200);
+ return res.send(chanDB.getEmotes());
+ }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()){
+ //get matched data
+ const {chanName, emoteName, link} = matchedData(req);
+ //query for requested channel
+ const chanDB = await channelModel.findOne({name: chanName});
+
+ //If the requested channel doesn't exist
+ if(chanDB == null){
+ //ACK
+ return errorHandler(res, "Channel not found.", "Bad Query");
+ }
+
+ //Filter emote list for...
+ const foundEmotes = chanDB.emotes.filter((emote, emoteIndex) => {
+ //If we find an emote that matches the emote name
+ if(emote.name == emoteName){
+ //Splice out the emote
+ chanDB.emotes.splice(emoteIndex, 1);
+ //Return true to signify we found and deleted one
+ return true;
+ }
+ });
+
+ //If didn't find one
+ if(foundEmotes.length <= 0){
+ //ACK
+ return errorHandler(res, "Cannot delete non-existant emote!");
+ }
+
+ //Save the channel DB document
+ chanDB.save();
+
+ //Return the updated command list
+ res.status(200);
+ return res.send(chanDB.getEmotes());
+ }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/channelRouter.js b/src/routers/api/channelRouter.js
index e426e04..4be8abc 100644
--- a/src/routers/api/channelRouter.js
+++ b/src/routers/api/channelRouter.js
@@ -19,12 +19,16 @@ const { body, checkExact } = require('express-validator');
const { Router } = require('express');
//local imports
-const permissionSchema = require("../../schemas/permissionSchema");
+//Models
+const permissionModel = require("../../schemas/permissionSchema");
const channelModel = require("../../schemas/channel/channelSchema");
+//Valudators
const channelValidator = require("../../validators/channelValidator");
const accountValidator = require("../../validators/accountValidator");
const {channelPermissionValidator} = require("../../validators/permissionsValidator");
const tokebotValidator = require("../../validators/tokebotValidator");
+const emoteValidator = require("../../validators/emoteValidator");
+//Controllers
const registerController = require("../../controllers/api/channel/registerController");
const listController = require("../../controllers/api/channel/listController");
const settingsController = require("../../controllers/api/channel/settingsController");
@@ -33,18 +37,20 @@ const rankController = require("../../controllers/api/channel/rankController");
const deleteController = require("../../controllers/api/channel/deleteController");
const banController = require("../../controllers/api/channel/banController");
const tokeCommandController = require("../../controllers/api/channel/tokeCommandController");
+const emoteController = require('../../controllers/api/channel/emoteController');
//globals
const router = Router();
//user authentication middleware
-router.use("/register",permissionSchema.reqPermCheck("registerChannel"));
+router.use("/register",permissionModel.reqPermCheck("registerChannel"));
router.use("/settings", channelValidator.name('chanName'));
router.use("/permissions", channelValidator.name('chanName'));
router.use("/rank", channelValidator.name('chanName'));
router.use("/delete", channelValidator.name('chanName'));
router.use("/ban", channelValidator.name('chanName'));
router.use("/tokeCommand", channelValidator.name('chanName'));
+router.use("/emote", channelValidator.name('chanName'));
//routing functions
//register
@@ -70,5 +76,9 @@ router.delete('/ban', channelModel.reqPermCheck("banUser"), accountValidator.u
router.get('/tokeCommand', channelModel.reqPermCheck("manageChannel"), tokeCommandController.get);
router.post('/tokeCommand', tokebotValidator.command(), channelModel.reqPermCheck("editTokeCommands"), tokeCommandController.post);
router.delete('/tokeCommand', tokebotValidator.command(), channelModel.reqPermCheck("editTokeCommands"), tokeCommandController.delete);
+//emote
+router.get('/emote', channelModel.reqPermCheck("manageChannel"), emoteController.get);
+router.post('/emote', channelModel.reqPermCheck("editEmotes"), emoteValidator.name('emoteName'), emoteValidator.link(), emoteController.post);
+router.delete('/emote', channelModel.reqPermCheck("editEmotes"), emoteValidator.name('emoteName'), emoteController.delete);
module.exports = router;
\ No newline at end of file
diff --git a/src/schemas/channel/channelPermissionSchema.js b/src/schemas/channel/channelPermissionSchema.js
index a15dcbb..28601ca 100644
--- a/src/schemas/channel/channelPermissionSchema.js
+++ b/src/schemas/channel/channelPermissionSchema.js
@@ -76,6 +76,12 @@ const channelPermissionSchema = new mongoose.Schema({
default: "admin",
required: true
},
+ editEmotes: {
+ type: mongoose.SchemaTypes.String,
+ enum: rankEnum,
+ default: "admin",
+ required: true
+ },
deleteChannel: {
type: mongoose.SchemaTypes.String,
enum: rankEnum,
diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js
index 4b21de6..798f3e1 100644
--- a/src/schemas/channel/channelSchema.js
+++ b/src/schemas/channel/channelSchema.js
@@ -23,6 +23,7 @@ const server = require('../../server');
const statModel = require('../statSchema');
const {userModel} = require('../userSchema');
const permissionModel = require('../permissionSchema');
+const emoteModel = require('../emoteSchema');
const channelPermissionSchema = require('./channelPermissionSchema');
const channelBanSchema = require('./channelBanSchema');
const { exceptionHandler, errorHandler } = require('../../utils/loggerUtils');
@@ -76,6 +77,23 @@ const channelSchema = new mongoose.Schema({
type: mongoose.SchemaTypes.String,
required: true
}],
+ //Not re-using the site-wide schema because post save should call different functions
+ emotes: [{
+ name:{
+ type: mongoose.SchemaTypes.String,
+ required: true
+ },
+ link:{
+ type: mongoose.SchemaTypes.String,
+ required: true
+ },
+ type:{
+ type: mongoose.SchemaTypes.String,
+ required: true,
+ enum: emoteModel.typeEnum,
+ default: emoteModel.typeEnum[0]
+ }
+ }],
//Thankfully we don't have to keep track of alts, ips, or deleted users so this should be a lot easier than site-wide bans :P
banList: [channelBanSchema]
});
@@ -160,6 +178,17 @@ channelSchema.pre('save', async function (next){
}
}
+ if(this.isModified('emotes')){
+ //Get the active Channel object from the application side of the house
+ const activeChannel = server.channelManager.activeChannels.get(this.name);
+
+ //If the channel is active
+ if(activeChannel != null){
+ //Broadcast the emote list
+ activeChannel.broadcastChanEmotes(this);
+ }
+ }
+
next();
});
@@ -439,6 +468,24 @@ channelSchema.methods.checkBanByUserDoc = async function(userDB){
return foundBan;
}
+channelSchema.methods.getEmotes = function(){
+ //Create an empty array to hold our emote list
+ const emoteList = [];
+
+ //For each channel emote
+ this.emotes.forEach((emote) => {
+ //Push an object with select information from the emote to the emote list
+ emoteList.push({
+ name: emote.name,
+ link: emote.link,
+ type: emote.type
+ });
+ });
+
+ //return the emote list
+ return emoteList;
+}
+
channelSchema.methods.getChanBans = async function(){
//Create an empty list to hold our found bans
var banList = [];
diff --git a/src/schemas/emoteSchema.js b/src/schemas/emoteSchema.js
index 782aaca..1fc7af2 100644
--- a/src/schemas/emoteSchema.js
+++ b/src/schemas/emoteSchema.js
@@ -40,7 +40,7 @@ const emoteSchema = new mongoose.Schema({
}
});
-//pre-save function
+//post-save function
emoteSchema.post('save', async function (next){
//broadcast updated emotes
server.channelManager.broadcastSiteEmotes();
diff --git a/src/views/channelSettings.ejs b/src/views/channelSettings.ejs
index dced55c..d44a231 100644
--- a/src/views/channelSettings.ejs
+++ b/src/views/channelSettings.ejs
@@ -30,6 +30,7 @@ along with this program. If not, see .-->
<%- include('partial/channelSettings/settings.ejs'); %>
<%- include('partial/channelSettings/permList.ejs'); %>
<%- include('partial/channelSettings/tokeCommandList.ejs'); %>
+ <%- include('partial/channelSettings/emoteList.ejs'); %>