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