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'); %>