From e1528c01551b271cf7f630fe3b94a9fd4ff46ec0 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 21 Apr 2025 06:21:07 -0400 Subject: [PATCH 001/209] Added verbose queue --- README.md | 8 +++++++- src/app/channel/media/queue.js | 29 +++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7ed09f0..692e49a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -Canopy - 0.2.1-INDEV +Canopy - 0.2.1-INDEV (verbose queue) ====== +--- + +This branch exists to seperate the extra shit I added to the queue for bug-hunting so it wouldn't make a mess of the main development branch :P + +--- + Canopy - /ˈkæ.nə.pi/: - The upper layer of foliage and branches of a forest, containing the majority of animal life. diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index fa489ae..7e342dd 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -60,8 +60,22 @@ module.exports = class{ socket.on("clear", (data) => {this.deleteRange(socket, data)}); socket.on("move", (data) => {this.moveMedia(socket, data)}); socket.on("lock", () => {this.toggleLock(socket)}); + + + socket.on("chanDump", () => {this.chanDump(socket)}); } + chanDump(socket){ + socket.emit('chanDump', + { + nowPlaying: this.nowPlaying, + schedule: Array.from(this.schedule), + //timer: this.nextTimer + } + ) + } + + //--- USER FACING QUEUEING FUNCTIONS --- async queueURL(socket, data){ //Get the current channel from the database @@ -149,6 +163,7 @@ module.exports = class{ //End the media this.end(); + console.log(`Media set to end due at the request of ${socket.user}`); } } @@ -298,6 +313,9 @@ module.exports = class{ clearTimeout(this.nextTimer); //Set the next timer this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp, volatile)}, startsIn); + console.log(`next item '${nextItem.title}' timer set to start in ${startsIn} seconds`); + }else{ + console.log('next timer is unset'); } } @@ -461,6 +479,7 @@ module.exports = class{ if(this.nowPlaying != null && this.nowPlaying.uuid == uuid){ //End playback this.end(false, true); + console.log("Media ended due to removal of media"); //otherwise }else{ try{ @@ -661,6 +680,7 @@ module.exports = class{ //Silently end the media in RAM so the database isn't stepping on itself up ahead //Alternatively we could've used await, but then we'd be doubling up on DB transactions :P this.end(true, true, true); + console.log("Media ended due to start of new media"); } //reset current timestamp @@ -732,6 +752,7 @@ module.exports = class{ //Call the end function once the video is over this.syncTimer = setTimeout(this.end.bind(this), leftover); + console.log(`Media set to end by sync function in ${leftover} milliseconds`); } } @@ -768,13 +789,17 @@ module.exports = class{ //FUCK throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`); } - - //If we haven't changed 'nowPlaying' in the play list + + //If we haven't changed 'nowPlaying' in the DB if(chanDB.media.nowPlaying.uuid == wasPlaying.uuid){ //Take it out await chanDB.media.nowPlaying.deleteOne(); } + //NOTE: Keep an eye on this + //It seems this was part of my original design (don't remember was high) + //Though it is weird we keep it in both this.schedule and this.nowPlaying at the same time... + //More testing will have to be done... //Take it out of the active schedule this.schedule.delete(wasPlaying.startTime); From 91c89ba28faf18242cf3dc8430b5acd0e7e073e3 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 21 Apr 2025 06:49:01 -0400 Subject: [PATCH 002/209] Added verbosity and fixed bug in refreshNextTimer --- src/app/channel/media/queue.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 7e342dd..0e2627e 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -302,10 +302,11 @@ module.exports = class{ if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){ //Start the found item at w/ a pre-calculated time stamp to reflect the given start time this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp, volatile); - } - - //If we have a next item - if(nextItem != null){ + console.log("starting now from refreshNextTimer") + //Otherwise, if we have a next item + //CODE CHANGE CODE CHANGE + //Changes this over to an else if so refreshNextTimer wouldn't start it twice when there was no current item or next item + }else if(nextItem != null){ //Calculate the amount of time in ms that the next item will start in const startsIn = nextItem.startTime - new Date().getTime(); @@ -721,6 +722,7 @@ module.exports = class{ //Kick off the sync timer this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta); + console.log('kicking off sync timer from start'); //Setup the next video this.refreshNextTimer(); @@ -746,6 +748,7 @@ module.exports = class{ //Call the sync function in another second this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta); + console.log('re-kicking sync timer from sync'); }else{ //Get leftover video length in ms const leftover = (this.nowPlaying.duration - this.timestamp) * 1000; From e34dcbdec70971808e169745afac4fef9aa0bdc8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 21 Apr 2025 07:20:14 -0400 Subject: [PATCH 003/209] Fixed double-start bug in refreshNextTimer. --- src/app/channel/media/queue.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index fa489ae..8c8ae1d 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -287,10 +287,8 @@ module.exports = class{ if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){ //Start the found item at w/ a pre-calculated time stamp to reflect the given start time this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp, volatile); - } - //If we have a next item - if(nextItem != null){ + }else if(nextItem != null){ //Calculate the amount of time in ms that the next item will start in const startsIn = nextItem.startTime - new Date().getTime(); From 3426d6764a8ccc090134f98a08da98d05896937a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 22 Apr 2025 01:18:24 -0400 Subject: [PATCH 004/209] Fixed ghost media caused by stale nextTimers --- src/app/channel/media/queue.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 0e2627e..2ae14ec 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -298,6 +298,9 @@ module.exports = class{ //Get current item const currentItem = this.getItemAtEpoch() + //Clear out any stale timer to prevent ghost queueing + clearTimeout(this.nextTimer); + //If we have a current item and it isn't currently playing if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){ //Start the found item at w/ a pre-calculated time stamp to reflect the given start time @@ -310,8 +313,6 @@ module.exports = class{ //Calculate the amount of time in ms that the next item will start in const startsIn = nextItem.startTime - new Date().getTime(); - //Clear out any item that might be up next - clearTimeout(this.nextTimer); //Set the next timer this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp, volatile)}, startsIn); console.log(`next item '${nextItem.title}' timer set to start in ${startsIn} seconds`); From ef0344942bea9f3b7d818196135b810478b866d1 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 22 Apr 2025 01:42:07 -0400 Subject: [PATCH 005/209] Fixed refreshNextTimer so it clears out stale timers. --- src/app/channel/media/queue.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 8c8ae1d..7f677c5 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -283,6 +283,10 @@ module.exports = class{ //Get current item const currentItem = this.getItemAtEpoch() + + //Clear out any stale timers to prevent ghost queueing + clearTimeout(this.nextTimer); + //If we have a current item and it isn't currently playing if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){ //Start the found item at w/ a pre-calculated time stamp to reflect the given start time @@ -292,8 +296,6 @@ module.exports = class{ //Calculate the amount of time in ms that the next item will start in const startsIn = nextItem.startTime - new Date().getTime(); - //Clear out any item that might be up next - clearTimeout(this.nextTimer); //Set the next timer this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp, volatile)}, startsIn); } From 878ee4bb2d17aa31e20b5ed7d947ba55fc8eb55e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 22 Apr 2025 07:03:13 -0400 Subject: [PATCH 006/209] Converted pop-ups to standard HTML. None use templating features and the router was poorly written anywho. --- src/controllers/popupController.js | 24 --------------- src/routers/popupRouter.js | 30 ------------------- src/server.js | 4 +-- .../channel/panels/queuePanel/queuePanel.js | 2 +- www/js/utils.js | 2 +- .../popup/addToPlaylist.html | 6 ++-- .../popup/changeEmail.html | 4 +-- .../popup/changePassword.html | 4 +-- .../popup/channelUserBan.html | 8 ++--- .../popup/clearMedia.html | 4 +-- .../popup/newPlaylist.html | 4 +-- .../popup/nukeChannel.html | 4 +-- .../nukeUser.ejs => www/popup/nukeUser.html | 4 +-- .../popup/placeholder.html | 4 +-- .../popup/playlistDefaultTitles.html | 4 +-- .../popup/renamePlaylist.html | 6 ++-- .../popup/scheduleMedia.html | 6 ++-- .../userBan.ejs => www/popup/userBan.html | 4 +-- 18 files changed, 35 insertions(+), 89 deletions(-) delete mode 100644 src/controllers/popupController.js delete mode 100644 src/routers/popupRouter.js rename src/views/partial/popup/addToPlaylist.ejs => www/popup/addToPlaylist.html (85%) rename src/views/partial/popup/changeEmail.ejs => www/popup/changeEmail.html (93%) rename src/views/partial/popup/changePassword.ejs => www/popup/changePassword.html (94%) rename src/views/partial/popup/channelUserBan.ejs => www/popup/channelUserBan.html (88%) rename src/views/partial/popup/clearMedia.ejs => www/popup/clearMedia.html (95%) rename src/views/partial/popup/newPlaylist.ejs => www/popup/newPlaylist.html (94%) rename src/views/partial/popup/nukeChannel.ejs => www/popup/nukeChannel.html (93%) rename src/views/partial/popup/nukeUser.ejs => www/popup/nukeUser.html (93%) rename src/views/partial/popup/placeholder.ejs => www/popup/placeholder.html (90%) rename src/views/partial/popup/playlistDefaultTitles.ejs => www/popup/playlistDefaultTitles.html (93%) rename src/views/partial/popup/renamePlaylist.ejs => www/popup/renamePlaylist.html (85%) rename src/views/partial/popup/scheduleMedia.ejs => www/popup/scheduleMedia.html (85%) rename src/views/partial/popup/userBan.ejs => www/popup/userBan.html (95%) diff --git a/src/controllers/popupController.js b/src/controllers/popupController.js deleted file mode 100644 index d844daf..0000000 --- a/src/controllers/popupController.js +++ /dev/null @@ -1,24 +0,0 @@ -/*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 .*/ - -//root index functions -module.exports.get = async function(req, res){ - try{ - res.render(`partial/popup${req.url}`, {}); - }catch(err){ - return res.sendStatus(400); - } -} \ No newline at end of file diff --git a/src/routers/popupRouter.js b/src/routers/popupRouter.js deleted file mode 100644 index 55f3ba7..0000000 --- a/src/routers/popupRouter.js +++ /dev/null @@ -1,30 +0,0 @@ -/*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 { Router } = require('express'); - - -//local imports -const popupController = require("../controllers/popupController"); - -//globals -const router = Router(); - -//routing functions -router.get('/*', popupController.get); - -module.exports = router; diff --git a/src/server.js b/src/server.js index fdd9393..11f6c5f 100644 --- a/src/server.js +++ b/src/server.js @@ -58,7 +58,7 @@ const emailChangeRouter = require('./routers/emailChangeController'); //Panel const panelRouter = require('./routers/panelRouter'); //Popup -const popupRouter = require('./routers/popupRouter'); +//const popupRouter = require('./routers/popupRouter'); //Tooltip const tooltipRouter = require('./routers/tooltipRouter'); //Api @@ -161,7 +161,7 @@ app.use('/emailChange', emailChangeRouter); //Panel app.use('/panel', panelRouter); //Popup -app.use('/popup', popupRouter); +//app.use('/popup', popupRouter); //tooltip app.use('/tooltip', tooltipRouter); //Bot-Ready diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index 6372fca..0adb6da 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -603,7 +603,7 @@ class queuePanel extends panelObj{ //Create entry title const entryTitle = document.createElement('p'); - entryTitle.textContent = entry[1].title; + entryTitle.textContent = utils.unescapeEntities(entry[1].title); //Tooltip generation //tooltip div diff --git a/www/js/utils.js b/www/js/utils.js index 7191c1b..24da23d 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -1028,7 +1028,7 @@ class canopyAjaxUtils{ //Popup async getPopup(popup){ - var response = await fetch(`/popup/${popup}`,{ + var response = await fetch(`/popup/${popup}.html`,{ method: "GET" }); diff --git a/src/views/partial/popup/addToPlaylist.ejs b/www/popup/addToPlaylist.html similarity index 85% rename from src/views/partial/popup/addToPlaylist.ejs rename to www/popup/addToPlaylist.html index 8c875c3..e6a748f 100644 --- a/src/views/partial/popup/addToPlaylist.ejs +++ b/www/popup/addToPlaylist.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software + +
diff --git a/src/views/partial/popup/changeEmail.ejs b/www/popup/changeEmail.html similarity index 93% rename from src/views/partial/popup/changeEmail.ejs rename to www/popup/changeEmail.html index 6aaa253..74e9930 100644 --- a/src/views/partial/popup/changeEmail.ejs +++ b/www/popup/changeEmail.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/changePassword.ejs b/www/popup/changePassword.html similarity index 94% rename from src/views/partial/popup/changePassword.ejs rename to www/popup/changePassword.html index b726362..d3777b4 100644 --- a/src/views/partial/popup/changePassword.ejs +++ b/www/popup/changePassword.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/channelUserBan.ejs b/www/popup/channelUserBan.html similarity index 88% rename from src/views/partial/popup/channelUserBan.ejs rename to www/popup/channelUserBan.html index 039e972..f54a077 100644 --- a/src/views/partial/popup/channelUserBan.ejs +++ b/www/popup/channelUserBan.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software + +
diff --git a/src/views/partial/popup/clearMedia.ejs b/www/popup/clearMedia.html similarity index 95% rename from src/views/partial/popup/clearMedia.ejs rename to www/popup/clearMedia.html index 0b331cf..33c0592 100644 --- a/src/views/partial/popup/clearMedia.ejs +++ b/www/popup/clearMedia.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/newPlaylist.ejs b/www/popup/newPlaylist.html similarity index 94% rename from src/views/partial/popup/newPlaylist.ejs rename to www/popup/newPlaylist.html index d20f32f..14e4df5 100644 --- a/src/views/partial/popup/newPlaylist.ejs +++ b/www/popup/newPlaylist.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/nukeChannel.ejs b/www/popup/nukeChannel.html similarity index 93% rename from src/views/partial/popup/nukeChannel.ejs rename to www/popup/nukeChannel.html index 548081c..486e462 100644 --- a/src/views/partial/popup/nukeChannel.ejs +++ b/www/popup/nukeChannel.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/nukeUser.ejs b/www/popup/nukeUser.html similarity index 93% rename from src/views/partial/popup/nukeUser.ejs rename to www/popup/nukeUser.html index e332732..46eaa24 100644 --- a/src/views/partial/popup/nukeUser.ejs +++ b/www/popup/nukeUser.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/placeholder.ejs b/www/popup/placeholder.html similarity index 90% rename from src/views/partial/popup/placeholder.ejs rename to www/popup/placeholder.html index 67aa05d..64d79d2 100644 --- a/src/views/partial/popup/placeholder.ejs +++ b/www/popup/placeholder.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +

This is a test popup!

\ No newline at end of file diff --git a/src/views/partial/popup/playlistDefaultTitles.ejs b/www/popup/playlistDefaultTitles.html similarity index 93% rename from src/views/partial/popup/playlistDefaultTitles.ejs rename to www/popup/playlistDefaultTitles.html index 087df4e..eb2011e 100644 --- a/src/views/partial/popup/playlistDefaultTitles.ejs +++ b/www/popup/playlistDefaultTitles.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
diff --git a/src/views/partial/popup/renamePlaylist.ejs b/www/popup/renamePlaylist.html similarity index 85% rename from src/views/partial/popup/renamePlaylist.ejs rename to www/popup/renamePlaylist.html index de5e2f7..1c4190a 100644 --- a/src/views/partial/popup/renamePlaylist.ejs +++ b/www/popup/renamePlaylist.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software + +
diff --git a/src/views/partial/popup/scheduleMedia.ejs b/www/popup/scheduleMedia.html similarity index 85% rename from src/views/partial/popup/scheduleMedia.ejs rename to www/popup/scheduleMedia.html index 3f83583..cb20cdd 100644 --- a/src/views/partial/popup/scheduleMedia.ejs +++ b/www/popup/scheduleMedia.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software + +
diff --git a/src/views/partial/popup/userBan.ejs b/www/popup/userBan.html similarity index 95% rename from src/views/partial/popup/userBan.ejs rename to www/popup/userBan.html index b3c91c4..4654fb0 100644 --- a/src/views/partial/popup/userBan.ejs +++ b/www/popup/userBan.html @@ -1,4 +1,4 @@ -<%# Canopy - The next generation of stoner streaming software +
From 8baa42478a2db93f59a226e61fca04a16b4d3943 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 22 Apr 2025 07:38:46 -0400 Subject: [PATCH 007/209] Updated README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7ed09f0..d35b5b6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Canopy intends to be a simple node/express.js app. It leverages the piped and th The Canopy codebase does not, nor will it ever contain: - Advertisements (targetted or otherwise) - Proprietary Code + - Cryptocurrency/Blockchain integration - 'Analytics/Telemtry' spyware - The use of video sources which require proprietary 'Digital ~~Rights Management~~ Ristricitons Malware' such as Widevine. From bddbb3a4a36fdae3d07c039a79042b0b6a29e428 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 26 Apr 2025 07:52:52 -0400 Subject: [PATCH 008/209] Homepage CSS hotfix --- www/css/index.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/css/index.css b/www/css/index.css index 8e9218f..00d8e56 100644 --- a/www/css/index.css +++ b/www/css/index.css @@ -49,6 +49,11 @@ div.channel-guide-entry{ margin: 0.1em auto 0.1em auto; } +.channel-guide-entry-item img{ + max-width: 7em; + max-height: 7em; +} + span.channel-guide-entry-item{ overflow: scroll; height: 2em; From eadfa7c1266c86919e01d541c6ea11628c38d2ce Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 26 Apr 2025 09:48:02 -0400 Subject: [PATCH 009/209] Critical Hotfix for !announce and !serverannounce --- src/app/channel/commandPreprocessor.js | 81 +++++++++++++------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index 03a4bc9..b1733c2 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -22,6 +22,7 @@ const tokebot = require('./tokebot'); const linkUtils = require('../../utils/linkUtils'); const permissionModel = require('../../schemas/permissionSchema'); const channelModel = require('../../schemas/channel/channelSchema'); +const { command } = require('../../validators/tokebotValidator'); module.exports = class commandPreprocessor{ constructor(server, chatHandler){ @@ -82,7 +83,7 @@ module.exports = class commandPreprocessor{ if(commandObj.argumentArray != null){ if(this.commandProcessor[commandObj.argumentArray[0].toLowerCase()] != null){ //Process the command and use the return value to set the sendflag (true if command valid) - commandObj.sendFlag = await this.commandProcessor[commandObj.argumentArray[0].toLowerCase()](commandObj); + commandObj.sendFlag = await this.commandProcessor[commandObj.argumentArray[0].toLowerCase()](commandObj, this); }else{ //Process as toke command if we didnt get a match from the standard server-side command processor commandObj.sendFlag = await this.tokebot.tokeProcessor(commandObj); @@ -123,75 +124,75 @@ class commandProcessor{ } //Command keywords get run through .toLowerCase(), so we should use lowercase method names for command methods - whisper(preprocessor){ + whisper(commandObj){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Mark out the current message as a whisper - preprocessor.chatType = 'whisper'; + commandObj.chatType = 'whisper'; //Make sure to throw the send flag return true } - spoiler(preprocessor){ + spoiler(commandObj){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler - preprocessor.chatType = 'spoiler'; + commandObj.chatType = 'spoiler'; //Make sure to throw the send flag return true } - strikethrough(preprocessor){ + strikethrough(commandObj){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler - preprocessor.chatType = 'strikethrough'; + commandObj.chatType = 'strikethrough'; //Make sure to throw the send flag return true } - bold(preprocessor){ + bold(commandObj){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler - preprocessor.chatType = 'bold'; + commandObj.chatType = 'bold'; //Make sure to throw the send flag return true } - italics(preprocessor){ + italics(commandObj){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Mark out the current message as a spoiler - preprocessor.chatType = 'italics'; + commandObj.chatType = 'italics'; //Make sure to throw the send flag return true } - async announce(preprocessor){ + async announce(commandObj, preprocessor){ //Get the current channel from the database - const chanDB = await channelModel.findOne({name: preprocessor.socket.chan}); + const chanDB = await channelModel.findOne({name: commandObj.socket.chan}); //Check if the user has permission, and publicly shame them if they don't (lmao) - if(chanDB != null && await chanDB.permCheck(preprocessor.socket.user, 'announce')){ + if(chanDB != null && await chanDB.permCheck(commandObj.socket.user, 'announce')){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Prep the message using pre-processor functions chat-handling - await preprocessor.prepMessage(); + await preprocessor.prepMessage(commandObj); //send it - this.chatHandler.relayChannelAnnouncement(preprocessor.socket.chan, preprocessor.message, preprocessor.links); + this.chatHandler.relayChannelAnnouncement(commandObj.socket.chan, commandObj.message, commandObj.links); //throw send flag return false; @@ -201,17 +202,17 @@ class commandProcessor{ return true; } - async serverannounce(preprocessor){ + async serverannounce(commandObj, preprocessor){ //Check if the user has permission, and publicly shame them if they don't (lmao) - if(await permissionModel.permCheck(preprocessor.socket.user, 'announce')){ + if(await permissionModel.permCheck(commandObj.socket.user, 'announce')){ //splice out our command - preprocessor.commandArray.splice(0,2); + commandObj.commandArray.splice(0,2); //Prep the message using pre-processor functions for chat-handling - await preprocessor.prepMessage(); + await preprocessor.prepMessage(commandObj); //send it - this.chatHandler.relayServerAnnouncement(preprocessor.message, preprocessor.links); + this.chatHandler.relayServerAnnouncement(commandObj.message, commandObj.links); //throw send flag return false; @@ -221,14 +222,14 @@ class commandProcessor{ return true; } - async clear(preprocessor){ + async clear(commandObj){ //Get the current channel from the database - const chanDB = await channelModel.findOne({name: preprocessor.socket.chan}); + const chanDB = await channelModel.findOne({name: commandObj.socket.chan}); //Check if the user has permission, and publicly shame them if they don't (lmao) - if(await chanDB.permCheck(preprocessor.socket.user, 'clearChat')){ + if(await chanDB.permCheck(commandObj.socket.user, 'clearChat')){ //Send off the command - this.chatHandler.clearChat(preprocessor.socket.chan, preprocessor.argumentArray[1]); + this.chatHandler.clearChat(commandObj.socket.chan, commandObj.argumentArray[1]); //throw send flag return false; } @@ -237,24 +238,24 @@ class commandProcessor{ return true; } - async kick(preprocessor){ + async kick(commandObj){ //Get the current channel from the database - const chanDB = await channelModel.findOne({name: preprocessor.socket.chan}); + const chanDB = await channelModel.findOne({name: commandObj.socket.chan}); //Check if the user has permission, and publicly shame them if they don't (lmao) - if(await chanDB.permCheck(preprocessor.socket.user, 'kickUser')){ + if(await chanDB.permCheck(commandObj.socket.user, 'kickUser')){ //Get username from argument array - const username = preprocessor.argumentArray[1]; + const username = commandObj.argumentArray[1]; //Get channel - const channel = this.server.activeChannels.get(preprocessor.socket.chan); + const channel = this.server.activeChannels.get(commandObj.socket.chan); //get initiator and target user objects - const initiator = channel.userList.get(preprocessor.socket.user.user); + const initiator = channel.userList.get(commandObj.socket.user.user); const target = channel.userList.get(username); //get initiator and target override abilities - const override = await permissionModel.overrideCheck(preprocessor.socket.user, 'kickUser'); + const override = await permissionModel.overrideCheck(commandObj.socket.user, 'kickUser'); const targetOverride = await permissionModel.overrideCheck(target, 'kickUser'); //If there is no user @@ -291,10 +292,10 @@ class commandProcessor{ //Splice out kick - preprocessor.commandArray.splice(0,4) + commandObj.commandArray.splice(0,4) //Get collect reason - var reason = preprocessor.commandArray.join(''); + var reason = commandObj.commandArray.join(''); //If no reason was given if(reason == ''){ From 9bf68a499b961d38904f287c61514c3b5934fc32 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 26 Apr 2025 16:53:51 -0400 Subject: [PATCH 010/209] Added case-insensitive logins --- README.md | 2 +- src/schemas/user/userSchema.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d35b5b6..03e2e51 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Canopy - 0.2.1-INDEV +Canopy - 0.2.2-INDEV ====== Canopy - /ˈkæ.nə.pi/: diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index f98a137..6fcc9bc 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -253,7 +253,7 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use } //get the user if it exists - const userDB = await this.findOne({ user }); + const userDB = await this.findOne({ user: new RegExp(user, 'i')}); //if not scream and shout if(!userDB){ From af982f8611146cb4b99cd70cc6f1b1b656bc5426 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 26 Apr 2025 18:50:19 -0400 Subject: [PATCH 011/209] Add math.floor() function call to setHighLevel() --- src/app/channel/chatHandler.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index c9d07ef..3da520a 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -14,6 +14,9 @@ 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 validator = require('validator') + //local imports const commandPreprocessor = require('./commandPreprocessor'); const loggerUtils = require('../../utils/loggerUtils'); @@ -63,8 +66,8 @@ module.exports = class{ if(userDB){ try{ - //Set high level - userDB.highLevel = data.highLevel; + //Floor input to an integer and set high level + userDB.highLevel = Math.floor(data.highLevel); //Save user DB Document await userDB.save(); From 46a7e9e0672c724db956465ed4e2233ff639eb79 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 26 Apr 2025 19:14:10 -0400 Subject: [PATCH 012/209] Updated server-side self-refrencing links to only use port when not reverse proxied. --- config.example.json | 1 + src/schemas/user/emailChangeSchema.js | 2 +- src/schemas/user/passwordResetSchema.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config.example.json b/config.example.json index 42c171d..0afa5f4 100644 --- a/config.example.json +++ b/config.example.json @@ -2,6 +2,7 @@ "instanceName": "Canopy", "verbose": false, "port": 8080, + "proxied": false, "protocol": "http", "domain": "localhost", "sessionSecret": "CHANGE_ME", diff --git a/src/schemas/user/emailChangeSchema.js b/src/schemas/user/emailChangeSchema.js index 96bf5d2..ca20900 100644 --- a/src/schemas/user/emailChangeSchema.js +++ b/src/schemas/user/emailChangeSchema.js @@ -134,7 +134,7 @@ emailChangeSchema.methods.consume = async function(){ emailChangeSchema.methods.getChangeURL = function(){ //Check for default port based on protocol - if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443)){ + if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443 || config.proxied)){ //Return path return `${config.protocol}://${config.domain}/emailChange?token=${this.token}`; }else{ diff --git a/src/schemas/user/passwordResetSchema.js b/src/schemas/user/passwordResetSchema.js index 31bd9aa..85ef296 100644 --- a/src/schemas/user/passwordResetSchema.js +++ b/src/schemas/user/passwordResetSchema.js @@ -106,7 +106,7 @@ passwordResetSchema.methods.consume = async function(pass, confirmPass){ passwordResetSchema.methods.getResetURL = function(){ //Check for default port based on protocol - if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443)){ + if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443) || config.proxied){ //Return path return `${config.protocol}://${config.domain}/passwordReset?token=${this.token}`; }else{ From 8b6aa69c51c580a3364f73a87066911f6085cab8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 05:46:01 -0400 Subject: [PATCH 013/209] Fixed IP-Hashing and Alt Detection behind Reverse Proxies --- src/app/channel/channelManager.js | 8 +++++++- src/app/channel/connectedUser.js | 11 +++++++++-- .../api/account/emailChangeRequestController.js | 5 ++++- .../api/account/passwordResetRequestController.js | 5 ++++- src/controllers/api/account/registerController.js | 10 ++++++++-- src/controllers/api/admin/passwordResetController.js | 5 ++++- src/utils/sessionUtils.js | 8 ++++++-- 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index d0d4eb6..7a7ef6b 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -14,6 +14,9 @@ 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 .*/ +//Config +const config = require('../../../config.json'); + //Local Imports const channelModel = require('../../schemas/channel/channelSchema'); const emoteModel = require('../../schemas/emoteSchema'); @@ -90,8 +93,11 @@ module.exports = class{ } async validateSocket(socket){ + //If we're proxied use passthrough IP + const ip = config.proxied ? socket.handshake.headers['x-forwarded-for'] : socket.handshake.address; + //Look for ban by IP - const ipBanDB = await userBanModel.checkBanByIP(socket.handshake.address); + const ipBanDB = await userBanModel.checkBanByIP(ip); //If this ip is randy bobandy if(ipBanDB != null){ diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 12edd34..cebc454 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //local imports +const config = require('../../../config.json'); const channelModel = require('../../schemas/channel/channelSchema'); const permissionModel = require('../../schemas/permissionSchema'); const flairModel = require('../../schemas/flairSchema'); @@ -58,8 +59,14 @@ module.exports = class{ //Send out the currently playing item this.channel.queue.sendMedia(socket); - //Tattoo hashed IP address to user account for seven days - await userDB.tattooIPRecord(socket.handshake.address); + //If we're proxied + if(config.proxied){ + //Tattoo hashed IP address from reverse proxy to user account for seven days + await userDB.tattooIPRecord(socket.handshake.headers['x-forwarded-for']); + }else{ + //Tattoo hashed IP address to user account for seven days + await userDB.tattooIPRecord(socket.handshake.address); + } } socketCrawl(cb){ diff --git a/src/controllers/api/account/emailChangeRequestController.js b/src/controllers/api/account/emailChangeRequestController.js index 83fc654..1b82d74 100644 --- a/src/controllers/api/account/emailChangeRequestController.js +++ b/src/controllers/api/account/emailChangeRequestController.js @@ -38,6 +38,9 @@ module.exports.post = async function(req, res){ //Get sanatized/validated data const {email, pass} = matchedData(req); + //If we're proxied use passthrough IP + const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; + //Check to make sure the user is logged in if(req.session.user == null){ errorHandler(res, "Invalid user!"); @@ -56,7 +59,7 @@ module.exports.post = async function(req, res){ } //Generate the password reset link - const requestDB = await emailChangeModel.create({user: userDB._id, newEmail: email, ipHash: req.ip}); + const requestDB = await emailChangeModel.create({user: userDB._id, newEmail: email, ipHash: ip}); //Don't wait on mailer to get back to the browser res.sendStatus(200); diff --git a/src/controllers/api/account/passwordResetRequestController.js b/src/controllers/api/account/passwordResetRequestController.js index e36fb60..899cbe3 100644 --- a/src/controllers/api/account/passwordResetRequestController.js +++ b/src/controllers/api/account/passwordResetRequestController.js @@ -40,6 +40,9 @@ module.exports.post = async function(req, res){ //Verify Altcha Payload const verified = await altchaUtils.verify(req.body.verification); + //If we're proxied use passthrough IP + const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; + //If altcha verification failed if(!verified){ return errorHandler(res, 'Altcha verification failed, Please refresh the page!', 'unauthorized'); @@ -63,7 +66,7 @@ module.exports.post = async function(req, res){ } //Generate the password reset link - const requestDB = await passwordResetModel.create({user: userDB._id, ipHash: req.ip}); + const requestDB = await passwordResetModel.create({user: userDB._id, ipHash: ip}); //Send the reset url via email const mailInfo = await mailUtils.mailem( diff --git a/src/controllers/api/account/registerController.js b/src/controllers/api/account/registerController.js index 3fe962b..43ba037 100644 --- a/src/controllers/api/account/registerController.js +++ b/src/controllers/api/account/registerController.js @@ -43,6 +43,10 @@ module.exports.post = async function(req, res){ return errorHandler(res, 'Altcha verification failed, Please refresh the page!', 'unauthorized'); } + + //If we're proxied use passthrough IP + const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; + //Would prefer to stick this in userModel.statics.register() but we end up with circular dependencies >:( const nukedBans = await userBanModel.checkProcessedBans(user.user); @@ -53,7 +57,7 @@ module.exports.post = async function(req, res){ } //Look for ban by IP - const ipBanDB = await userBanModel.checkBanByIP(req.ip); + const ipBanDB = await userBanModel.checkBanByIP(ip); //If this ip is randy bobandy if(ipBanDB != null){ @@ -68,7 +72,9 @@ module.exports.post = async function(req, res){ return errorHandler(res, banMsg.join('
'), 'unauthorized'); } - await userModel.register(user, req.ip); + //Register off of given IP + await userModel.register(user, ip); + return res.sendStatus(200); }else{ res.status(400); diff --git a/src/controllers/api/admin/passwordResetController.js b/src/controllers/api/admin/passwordResetController.js index 7b5fdba..deff3a3 100644 --- a/src/controllers/api/admin/passwordResetController.js +++ b/src/controllers/api/admin/passwordResetController.js @@ -34,6 +34,9 @@ module.exports.post = async function(req, res){ //Find user from input const userDB = await userModel.findOne({user}); + //If we're proxied use passthrough IP + const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; + //If there is no user if(userDB == null){ //Scream @@ -41,7 +44,7 @@ module.exports.post = async function(req, res){ } //Generate the password reset link - const requestDB = await passwordResetModel.create({user: userDB._id, ipHash: req.ip}); + const requestDB = await passwordResetModel.create({user: userDB._id, ipHash: ip}); //send URL res.status(200); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 7395f5a..16fcb3e 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //Local Imports +const config = require('../../config.json'); const {userModel} = require('../schemas/user/userSchema'); const userBanModel = require('../schemas/user/userBanSchema') const altchaUtils = require('../utils/altchaUtils'); @@ -32,8 +33,11 @@ module.exports.authenticateSession = async function(user, pass, req){ //Grab previous attempts const attempt = failedAttempts.get(user); + //If we're proxied use passthrough IP + const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; + //Look for ban by IP - const ipBanDB = await userBanModel.checkBanByIP(req.ip); + const ipBanDB = await userBanModel.checkBanByIP(ip); //If this ip is randy bobandy if(ipBanDB != null){ @@ -89,7 +93,7 @@ module.exports.authenticateSession = async function(user, pass, req){ } //Tattoo hashed IP address to user account for seven days - userDB.tattooIPRecord(req.ip); + userDB.tattooIPRecord(ip); //If we got to here then the log-in was successful. We should clear-out any failed attempts. failedAttempts.delete(user); From cc5c63d3b1c1d551d98cbc29437824d1ec6377fe Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 08:08:39 -0400 Subject: [PATCH 014/209] Added instance-unique salt to IP hashes --- config.example.json | 1 + src/server.js | 2 -- src/utils/configCheck.js | 6 +++++- src/utils/hashUtils.js | 7 +++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/config.example.json b/config.example.json index 0afa5f4..68c5002 100644 --- a/config.example.json +++ b/config.example.json @@ -7,6 +7,7 @@ "domain": "localhost", "sessionSecret": "CHANGE_ME", "altchaSecret": "CHANGE_ME", + "ipSecret": "CHANGE_ME", "ssl":{ "cert": "./server.cert", "key": "./server.key" diff --git a/src/server.js b/src/server.js index 11f6c5f..fc32a3d 100644 --- a/src/server.js +++ b/src/server.js @@ -180,8 +180,6 @@ app.use(errorMiddleware); //Basic 404 handler app.use(fileNotFoundController); - - //Increment launch counter statModel.incrementLaunchCount(); diff --git a/src/utils/configCheck.js b/src/utils/configCheck.js index 76b8137..d0f9c55 100644 --- a/src/utils/configCheck.js +++ b/src/utils/configCheck.js @@ -47,6 +47,11 @@ module.exports.securityCheck = function(){ loggerUtil.consoleWarn("Insecure Altcha Secret! Change Altcha Secret!"); } + //check ipHash secret + if(!validator.isStrongPassword(config.ipSecret) || config.ipSecret == "CHANGE_ME"){ + loggerUtil.consoleWarn("Insecure IP Hashing Secret! Change IP Hashing Secret!"); + } + //check DB pass if(!validator.isStrongPassword(config.db.pass) || config.db.pass == "CHANGE_ME" || config.db.pass == config.db.user){ loggerUtil.consoleWarn("Insecure Database Password! Change Database password!"); @@ -56,5 +61,4 @@ module.exports.securityCheck = function(){ if(!validator.isStrongPassword(config.mail.pass) || config.mail.pass == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure Email Password! Change Email password!"); } - } \ No newline at end of file diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index 115ec45..cc78d01 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -14,6 +14,9 @@ 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 .*/ +//Config +const config = require('../../config.json'); + //Node Imports const crypto = require('node:crypto'); @@ -33,8 +36,8 @@ module.exports.hashIP = function(ip){ //Create hash object const hashObj = crypto.createHash('md5'); - //add IP to the hash - hashObj.update(ip); + //add IP and salt to the hash + hashObj.update(`${ip}${config.ipSecret}`); //return the IP hash as a string return hashObj.digest('hex'); From 4dff3e562b8f76e7a4833cd1c266cfc7b2558ced Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 08:15:53 -0400 Subject: [PATCH 015/209] Remove guff added by IDE --- src/app/channel/commandPreprocessor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index b1733c2..b956de6 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -22,7 +22,6 @@ const tokebot = require('./tokebot'); const linkUtils = require('../../utils/linkUtils'); const permissionModel = require('../../schemas/permissionSchema'); const channelModel = require('../../schemas/channel/channelSchema'); -const { command } = require('../../validators/tokebotValidator'); module.exports = class commandPreprocessor{ constructor(server, chatHandler){ From d9bab09d5360271f8ff9cd756b70d7024e0d55ab Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 08:55:11 -0400 Subject: [PATCH 016/209] Little bit of a hacky method, but queuePanel now waits until queue scale is properly rendered before rendering queue items --- www/js/channel/panels/queuePanel/queuePanel.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index 0adb6da..d368f57 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -515,8 +515,12 @@ class queuePanel extends panelObj{ //Render out time scale this.renderQueueScale(date); - //wait a few frames so the scale can finish rendering, because dom function aren't async for some fucking reason - for(let i = 0; i <= 2; i++){ + //Grab the first marker + let firstMarker = this.panelDocument.querySelector('.queue-marker-first'); + + //Loop until first marker is properly positioned + while(firstMarker.offsetTop > 0){ + //wait a few frames so the scale can finish rendering, because dom function aren't async for some fucking reason await utils.ux.awaitNextFrame(); } From 6639cee8ca9b7e9c65e0f159624337ad80bfe53d Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 09:49:14 -0400 Subject: [PATCH 017/209] Added !resettoke command to tokebot --- src/app/channel/chatHandler.js | 19 ++++++++++++------- src/app/channel/commandPreprocessor.js | 21 +++++++++++++++++++-- src/app/channel/tokebot.js | 8 ++++++++ src/schemas/permissionSchema.js | 6 ++++++ 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index 3da520a..4248b61 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -124,16 +124,12 @@ module.exports = class{ } } - relayUserChat(socket, msg, type, links){ - const user = this.server.getSocketInfo(socket); - this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links) - } - + //Base chat functions relayChat(user, flair, highLevel, msg, type = 'chat', chan, links){ this.server.io.in(chan).emit("chatMessage", {user, flair, highLevel, msg, type, links}); } - relayServerWisper(socket, user, flair, highLevel, msg, type, links){ + relayPrivateChat(socket, user, flair, highLevel, msg, type, links){ socket.emit("chatMessage", {user, flair, highLevel, msg, type, links}); } @@ -141,18 +137,26 @@ module.exports = class{ this.server.io.emit("chatMessage", {user, flair, highLevel, msg, type, links}); } + //User Chat Functions + relayUserChat(socket, msg, type, links){ + const user = this.server.getSocketInfo(socket); + this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links) + } + + //Toke Chat Functions relayTokeCallout(msg, links){ this.relayGlobalChat("Tokebot", "", '∞', msg, "toke", links); } relayTokeWhisper(socket, msg, links){ - this.relayServerWisper(socket, "Tokebot", "", '∞', msg, "tokewhisper", links); + this.relayPrivateChat(socket, "Tokebot", "", '∞', msg, "tokewhisper", links); } relayGlobalTokeWhisper(msg, links){ this.relayGlobalChat("Tokebot", "", '∞', msg, "tokewhisper", links); } + //Announcement Functions relayServerAnnouncement(msg, links){ this.relayGlobalChat("Server", "", '∞', msg, "announcement", links); } @@ -166,6 +170,7 @@ module.exports = class{ } } + //Misc Functions clearChat(chan, user){ const activeChan = this.server.activeChannels.get(chan); diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index b956de6..7396dac 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -213,7 +213,24 @@ class commandProcessor{ //send it this.chatHandler.relayServerAnnouncement(commandObj.message, commandObj.links); - //throw send flag + //disble send flag + return false; + } + + //throw send flag + return true; + } + + async resettoke(commandObj, preprocessor){ + //Check if the user has permission, and publicly shame them if they don't (lmao) + if(await permissionModel.permCheck(commandObj.socket.user, 'resetToke')){ + //Acknowledge command + this.chatHandler.relayTokeWhisper(commandObj.socket, 'Toke cooldown reset.'); + + //Tell tokebot to reset the toke + preprocessor.tokebot.resetToke(); + + //disable send flag return false; } @@ -229,7 +246,7 @@ class commandProcessor{ if(await chanDB.permCheck(commandObj.socket.user, 'clearChat')){ //Send off the command this.chatHandler.clearChat(commandObj.socket.chan, commandObj.argumentArray[1]); - //throw send flag + //disable send flag return false; } diff --git a/src/app/channel/tokebot.js b/src/app/channel/tokebot.js index fe0444f..b04d7fe 100644 --- a/src/app/channel/tokebot.js +++ b/src/app/channel/tokebot.js @@ -176,4 +176,12 @@ module.exports = class tokebot{ } } + resetToke(){ + //Set cooldown to 0 + this.cooldownCounter = 0; + + //Null out the timer + this.cooldownTimer = null; + } + } diff --git a/src/schemas/permissionSchema.js b/src/schemas/permissionSchema.js index d1621f8..0e6070a 100644 --- a/src/schemas/permissionSchema.js +++ b/src/schemas/permissionSchema.js @@ -51,6 +51,12 @@ const permissionSchema = new mongoose.Schema({ default: "admin", required: true }, + resetToke: { + type: mongoose.SchemaTypes.String, + enum: rankEnum, + default: "admin", + required: true + }, editTokeCommands: { type: mongoose.SchemaTypes.String, enum: rankEnum, From 7d3c31f0aafe612f3408222c20c904ad9754d867 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 10:26:35 -0400 Subject: [PATCH 018/209] !r added to tokebot --- src/app/channel/tokebot.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/channel/tokebot.js b/src/app/channel/tokebot.js index b04d7fe..a40d559 100644 --- a/src/app/channel/tokebot.js +++ b/src/app/channel/tokebot.js @@ -56,6 +56,21 @@ module.exports = class tokebot{ if(this.tokeCommands.indexOf(commandObj.argumentArray[0].toLowerCase()) != -1){ //Seems lame to set a bool in an if statement but this would've made a really ugly turinary var foundToke = true; + }else if(commandObj.argumentArray[0].toLowerCase() == 'r'){ + //Find the users active channel + const activeChan = this.server.activeChannels.get(commandObj.socket.chan); + + //Combile site-wide and channel tokes into one list + const tokeList = this.tokeCommands.concat(activeChan.tokeCommands); + + //Pick a random number between 0 and one less than the number of tokes + const foundIndex = Math.round(Math.random() * (tokeList.length - 1)); + + //Set override command argument 0 w/ the found toke + commandObj.argumentArray[0] = tokeList[foundIndex]; + + //throw toke flag + var foundToke = true; }else{ //Find the users active channel const activeChan = this.server.activeChannels.get(commandObj.socket.chan); @@ -105,7 +120,7 @@ module.exports = class tokebot{ } //Toke command found, and there isn't any extra text, don't send as chat (re-create fore.st tokebot behaviour) - return (commandObj.command != `!${commandObj.argumentArray[0]}`) + return (commandObj.command != `!${commandObj.argumentArray[0]}` && commandObj.command != '!r'); }else{ //No toke found, send it down the line, because shaming the user is funny return true; From 853f67fe15612f1081c600d1b0c210e6078af984 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 29 May 2025 09:48:48 -0400 Subject: [PATCH 019/209] Improved exception handling, started work on improving error messages for IP bans --- src/app/channel/channelManager.js | 4 +- src/app/channel/media/queue.js | 14 ++--- .../api/channel/deleteController.js | 2 +- .../api/channel/permissionsController.js | 4 +- .../api/channel/settingsController.js | 4 +- src/controllers/channelSettingsController.js | 2 +- src/schemas/channel/channelSchema.js | 20 +++---- src/schemas/permissionSchema.js | 4 +- src/schemas/user/passwordResetSchema.js | 5 +- src/schemas/user/userBanSchema.js | 21 ++++---- src/schemas/user/userSchema.js | 24 ++++----- src/utils/loggerUtils.js | 52 +++++++++++++++---- src/utils/media/internetArchiveUtils.js | 7 +-- src/utils/scheduler.js | 2 +- src/utils/sessionUtils.js | 30 +++++++---- 15 files changed, 118 insertions(+), 77 deletions(-) diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 7a7ef6b..52ae732 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -122,7 +122,7 @@ module.exports = class{ const userDB = await userModel.findOne({user: socket.request.session.user.user}); if(userDB == null){ - throw new Error("User not found!"); + throw loggerUtils.exceptionSmith("User not found!", "unauthorized"); } //Set socket user and channel values @@ -140,7 +140,7 @@ module.exports = class{ //Check if channel exists if(chanDB == null){ - throw new Error("Channel not found!"); + throw loggerUtils.exceptionSmith("Channel not found", "validation"); } //Check if current channel is active diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 7f677c5..8f9c469 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -312,7 +312,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); } //For each item @@ -409,7 +409,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); } //Keep a copy of the archive that hasn't been changed @@ -470,7 +470,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); } //Filter media out by UUID @@ -611,7 +611,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while saving item to queue!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while saving item to queue!`, "queue"); } //Add media to the persistant schedule @@ -766,7 +766,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while ending queue item!`, "queue"); } //If we haven't changed 'nowPlaying' in the play list @@ -909,7 +909,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while rehydrating queue!`, "queue"); } //Create an empty array to hold our schedule @@ -957,7 +957,7 @@ module.exports = class{ //If we couldn't find the channel if(chanDB == null){ //FUCK - throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`); + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while rehydrating queue!`, "queue"); } const now = new Date().getTime(); diff --git a/src/controllers/api/channel/deleteController.js b/src/controllers/api/channel/deleteController.js index 6dafa1a..8a18ae2 100644 --- a/src/controllers/api/channel/deleteController.js +++ b/src/controllers/api/channel/deleteController.js @@ -31,7 +31,7 @@ module.exports.post = async function(req, res){ const channel = await channelModel.findOne({name: data.chanName}); if(channel == null){ - throw new Error("Chanenl does not exist!"); + throw loggerUtils.exceptionSmith("Chanenl does not exist!", "validation"); } await channel.nuke(data.confirm); diff --git a/src/controllers/api/channel/permissionsController.js b/src/controllers/api/channel/permissionsController.js index d50af28..cb05585 100644 --- a/src/controllers/api/channel/permissionsController.js +++ b/src/controllers/api/channel/permissionsController.js @@ -33,7 +33,7 @@ module.exports.get = async function(req, res){ if(channel == null){ - throw new Error("Channel not found."); + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); } res.status(200); @@ -64,7 +64,7 @@ module.exports.post = async function(req, res){ var permError = null; if(chanDB == null){ - throw new Error("Channel not found."); + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); } //For each permission submitted diff --git a/src/controllers/api/channel/settingsController.js b/src/controllers/api/channel/settingsController.js index ff738a3..9c719c8 100644 --- a/src/controllers/api/channel/settingsController.js +++ b/src/controllers/api/channel/settingsController.js @@ -31,7 +31,7 @@ module.exports.get = async function(req, res){ const channel = await channelModel.findOne({name: data.chanName}); if(channel == null){ - throw new Error("Channel not found."); + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); } res.status(200); @@ -56,7 +56,7 @@ module.exports.post = async function(req, res){ const settingsMap = new Map(Object.entries(data.settingsMap)); if(channel == null){ - throw new Error("Channel not found."); + throw loggerUtils.exceptionSmith("Channel not found.", "validation"); } res.status(200); diff --git a/src/controllers/channelSettingsController.js b/src/controllers/channelSettingsController.js index 2dadad2..5b20aba 100644 --- a/src/controllers/channelSettingsController.js +++ b/src/controllers/channelSettingsController.js @@ -36,7 +36,7 @@ module.exports.get = async function(req, res){ delete chanDB.permissions._doc._id; if(chanDB == null){ - throw new Error("Channel not found."); + throw loggerUtils.exceptionSmith("Channel not found.", "queue"); } return res.render('channelSettings', {instance: config.instanceName, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req)}); diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 4f15ba2..4dbbe2f 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -115,7 +115,7 @@ const channelSchema = new mongoose.Schema({ channelSchema.pre('save', async function (next){ if(this.isModified("name")){ if(this.name.match(/^[a-z0-9_\-.]+$/i) == null){ - throw new Error("Username must only contain alpha-numerics and the following symbols: '-_.'"); + throw loggerUtils.exceptionSmith("Username must only contain alpha-numerics and the following symbols: '-_.'", "validation"); } } @@ -221,7 +221,7 @@ channelSchema.statics.register = async function(channelObj, ownerObj){ const chanDB = await this.findOne({ name }); if(chanDB){ - throw new Error("Channel name already taken!"); + throw loggerUtils.exceptionSmith("Channel name already taken!", "validation"); }else{ const id = await statModel.incrementChannelCount(); const rankList = [{ @@ -334,7 +334,7 @@ channelSchema.statics.processExpiredBans = async function(){ channelSchema.methods.updateSettings = async function(settingsMap){ settingsMap.forEach((value, key) => { if(this.settings[key] == null){ - throw new Error("Invalid channel setting."); + throw loggerUtils.exceptionSmith("Invalid channel setting.", "validation"); } this.settings[key] = value; @@ -661,13 +661,13 @@ channelSchema.methods.getChanBans = async function(){ channelSchema.methods.banByUserDoc = async function(userDB, expirationDays, banAlts){ //Throw a shitfit if the user doesn't exist if(userDB == null){ - throw new Error("Cannot ban non-existant user!"); + throw loggerUtils.exceptionSmith("Cannot ban non-existant user!", "validation"); } const foundBan = await this.checkBanByUserDoc(userDB); if(foundBan != null){ - throw new Error("User already banned!"); + throw loggerUtils.exceptionSmith("User already banned!", "validation"); } //Create a new ban document based on input @@ -703,13 +703,13 @@ channelSchema.methods.ban = async function(user, expirationDays, banAlts){ channelSchema.methods.unbanByUserDoc = async function(userDB){ //Throw a shitfit if the user doesn't exist if(userDB == null){ - throw new Error("Cannot ban non-existant user!"); + throw loggerUtils.exceptionSmith("Cannot ban non-existant user!", "validation"); } const foundBan = await this.checkBanByUserDoc(userDB); if(foundBan == null){ - throw new Error("User already unbanned!"); + throw loggerUtils.exceptionSmith("User already unbanned!", "validation"); } //You know I can't help but feel like an asshole for looking for the index of something I just pulled out of an array using forEach... @@ -727,16 +727,16 @@ channelSchema.methods.unban = async function(user){ channelSchema.methods.nuke = async function(confirm){ if(confirm == "" || confirm == null){ - throw new Error("Empty Confirmation String!"); + throw loggerUtils.exceptionSmith("Empty Confirmation String!", "validation"); }else if(confirm != this.name){ - throw new Error("Bad Confirmation String!"); + throw loggerUtils.exceptionSmith("Bad Confirmation String!", "validation"); } //Annoyingly there isnt a good way to do this from 'this' var oldChan = await this.deleteOne(); if(oldChan.deletedCount == 0){ - throw new Error("Server Error: Unable to delete channel! Please report this error to your server administrator, and with timestamp."); + throw loggerUtils.exceptionSmith("Server Error: Unable to delete channel! Please report this error to your server administrator, and with timestamp.", "internal"); } } diff --git a/src/schemas/permissionSchema.js b/src/schemas/permissionSchema.js index 0e6070a..e3dcde9 100644 --- a/src/schemas/permissionSchema.js +++ b/src/schemas/permissionSchema.js @@ -201,7 +201,7 @@ permissionSchema.methods.permCheckByUserDoc = function(userDB, perm){ return (userRank >= requiredRank); }else{ //if not scream and shout - throw new Error(`Permission check '${perm}' not found!`); + throw loggerUtils.exceptionSmith(`Permission check '${perm}' not found!`, "Validation"); } } @@ -223,7 +223,7 @@ permissionSchema.methods.overrideCheckByUserDoc = function(userDB, perm){ return (userRank >= requiredRank); }else{ //if not scream and shout - throw new Error(`Permission check '${perm}' not found!`); + throw loggerUtils.exceptionSmith(`Permission check '${perm}' not found!`, "validation"); } } diff --git a/src/schemas/user/passwordResetSchema.js b/src/schemas/user/passwordResetSchema.js index 85ef296..41ee05b 100644 --- a/src/schemas/user/passwordResetSchema.js +++ b/src/schemas/user/passwordResetSchema.js @@ -27,7 +27,8 @@ const crypto = require("node:crypto"); const {mongoose} = require('mongoose'); //Local Imports -const hashUtil = require('../../utils/hashUtils'); +const hashUtil = require('../../utils/hashUtils.js'); +const loggerUtils = require('../../utils/loggerUtils.js') const daysToExpire = 7; @@ -85,7 +86,7 @@ passwordResetSchema.statics.processExpiredRequests = async function(){ passwordResetSchema.methods.consume = async function(pass, confirmPass){ //Check confirmation pass if(pass != confirmPass){ - throw new Error("Confirmation password does not match!"); + throw loggerUtils.exceptionSmith("Confirmation password does not match!", "validation"); } //Populate the user reference diff --git a/src/schemas/user/userBanSchema.js b/src/schemas/user/userBanSchema.js index 8af9502..0d49cb8 100644 --- a/src/schemas/user/userBanSchema.js +++ b/src/schemas/user/userBanSchema.js @@ -18,8 +18,9 @@ along with this program. If not, see .*/ const {mongoose} = require('mongoose'); //Local Imports -const hashUtil = require('../../utils/hashUtils'); -const {userModel} = require('./userSchema'); +const hashUtil = require('../../utils/hashUtils.js'); +const {userModel} = require('./userSchema.js'); +const loggerUtils = require('../../utils/loggerUtils.js'); const userBanSchema = new mongoose.Schema({ user: { @@ -184,21 +185,21 @@ userBanSchema.statics.checkProcessedBans = async function(user){ userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays, ipBan = false){ //Prevent missing users if(userDB == null){ - throw new Error("User not found") + throw loggerUtils.exceptionSmith("User not found", "validation"); } //Ensure the user isn't already banned if(await this.checkBanByUserDoc(userDB) != null){ - throw new Error("User already banned"); + throw loggerUtils.exceptionSmith("User already banned", "validation"); } //Verify time to expire/delete depending on action if(expirationDays < 0){ - throw new Error("Expiration Days must be a positive integer!"); + throw loggerUtils.exceptionSmith("Expiration Days must be a positive integer!", "validation"); }else if(expirationDays < 30 && permanent){ - throw new Error("Permanent bans must be given at least 30 days before automatic account deletion!"); + throw loggerUtils.exceptionSmith("Permanent bans must be given at least 30 days before automatic account deletion!", "validation"); }else if(expirationDays > 185){ - throw new Error("Expiration/Deletion date cannot be longer than half a year out from the original ban date."); + throw loggerUtils.exceptionSmith("Expiration/Deletion date cannot be longer than half a year out from the original ban date.", "validation"); } await banSessions(userDB); @@ -266,13 +267,13 @@ userBanSchema.statics.unbanByUserDoc = async function(userDB){ //Prevent missing users if(userDB == null){ - throw new Error("User not found") + throw loggerUtils.exceptionSmith("User not found", "validation"); } const banDB = await this.checkBanByUserDoc(userDB); if(!banDB){ - throw new Error("User already un-banned"); + throw loggerUtils.exceptionSmith("User already un-banned", "validation"); } //Use _id in-case mongoose wants to be a cunt @@ -284,7 +285,7 @@ userBanSchema.statics.unbanDeleted = async function(user){ const banDB = await this.checkProcessedBans(user); if(!banDB){ - throw new Error("User already un-banned"); + throw loggerUtils.exceptionSmith("User already un-banned", "validation"); } const oldBan = await this.deleteOne({_id: banDB._id}); diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 6fcc9bc..639ae15 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -14,9 +14,6 @@ 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 .*/ -//Node Imports -const { profile } = require('console'); - //NPM Imports const {mongoose} = require('mongoose'); @@ -33,6 +30,7 @@ const playlistSchema = require('../channel/media/playlistSchema'); //Utils const hashUtil = require('../../utils/hashUtils'); const mailUtil = require('../../utils/mailUtils'); +const loggerUtils = require('../../utils/loggerUtils') const userSchema = new mongoose.Schema({ @@ -161,7 +159,7 @@ userSchema.pre('save', async function (next){ await this.populate('flair'); if(permissionModel.rankToNum(this.rank) < permissionModel.rankToNum(this.flair.rank)){ - throw new Error(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`); + throw loggerUtils.exceptionSmith(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`, "unauthorized"); } } @@ -223,7 +221,7 @@ userSchema.statics.register = async function(userObj, ip){ //If the user is found or someones trying to impersonate tokeboi if(userDB || user.toLowerCase() == "tokebot"){ - throw new Error("User name/email already taken!"); + throw loggerUtils.exceptionSmith("User name/email already taken!", "validation"); }else{ //Increment the user count, pulling the id to tattoo to the user const id = await statModel.incrementUserCount(); @@ -242,14 +240,14 @@ userSchema.statics.register = async function(userObj, ip){ } } }else{ - throw new Error("Confirmation password doesn't match!"); + throw loggerUtils.exceptionSmith("Confirmation password doesn't match!", "validation"); } } userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Username or Password."){ //check for missing pass if(!user || !pass){ - throw new Error("Missing user/pass."); + throw loggerUtils.exceptionSmith("Missing user/pass.", "validation"); } //get the user if it exists @@ -268,9 +266,9 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use badLogin(); } - //standardize bad login response so it's unknowin which is bad for security reasons. + //standardize bad login response so it's unknown which is bad for security reasons. function badLogin(){ - throw new Error(failLine); + throw loggerUtils.exceptionSmith(failLine, "unauthorized"); } } @@ -688,11 +686,11 @@ userSchema.methods.changePassword = async function(passChange){ await this.killAllSessions("Your password has been reset."); }else{ //confirmation pass doesn't match - throw new Error("Mismatched confirmation password!"); + throw loggerUtils.exceptionSmith("Mismatched confirmation password!", "validation"); } }else{ //Old password wrong - throw new Error("Incorrect Password!"); + throw loggerUtils.exceptionSmith("Incorrect Password!", "validation"); } } @@ -716,7 +714,7 @@ userSchema.methods.nuke = async function(pass){ //Check we have a confirmation password if(pass == "" || pass == null){ //scream and shout - throw new Error("No confirmation password!"); + throw loggerUtils.exceptionSmith("No confirmation password!", "validation"); } //Check that the password is correct @@ -725,7 +723,7 @@ userSchema.methods.nuke = async function(pass){ var oldUser = await this.deleteOne(); }else{ //complain about a bad pass - throw new Error("Bad pass."); + throw loggerUtils.exceptionSmith("Bad pass.", "unauthorized"); } } diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index e8033df..4858618 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -17,7 +17,20 @@ along with this program. If not, see .*/ //Config const config = require('../../config.json'); -//At some point this will be a bit more advanced, right now it's just a placeholder :P +module.exports.exceptionSmith = function(msg, type){ + //Create the new error with the given message + const exception = new Error(msg); + + //Set the error type + exception.type = type; + + //Mark the error as a custom error + exception.custom = true; + + //Return the error + return exception; +} + module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){ //Some controllers do things after sending headers, for those, we should remain silent if(!res.headersSent){ @@ -35,11 +48,16 @@ module.exports.localExceptionHandler = function(err){ } module.exports.exceptionHandler = function(res, err){ - //Locally handle the exception - module.exports.localExceptionHandler(err); + //If this is a self-made problem + if(err.custom){ + module.exports.errorHandler(res, err.message, err.type); + }else{ + //Locally handle the exception + module.exports.localExceptionHandler(err); - //if not yell at the browser for fucking up, and tell it what it did wrong. - module.exports.errorHandler(res, err.message, "Caught Exception"); + //if not yell at the browser for fucking up, and tell it what it did wrong. + module.exports.errorHandler(res, "An unexpected server crash was just prevented. You should probably report this to an admin.", "Caught Exception"); + } } module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){ @@ -47,16 +65,28 @@ module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){ } module.exports.socketExceptionHandler = function(socket, err){ - //Locally handle the exception - module.exports.localExceptionHandler(err); + //If this is a self made problem + if(err.custom){ + //at the browser for fucking up, and tell it what it did wrong. + return module.exports.socketErrorHandler(socket, err.message, err.type); + }else{ + //Locally handle the exception + module.exports.localExceptionHandler(err); - //if not yell at the browser for fucking up, and tell it what it did wrong. - return module.exports.socketErrorHandler(socket, err.msg, "Caught Exception"); + //if not yell at the browser for fucking up + return module.exports.socketErrorHandler(socket, "Caught Exception", "Caught Exception"); + } } module.exports.socketCriticalExceptionHandler = function(socket, err){ - //if not yell at the browser for fucking up, and tell it what it did wrong. - socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`}); + //If this is a self made problem + if(err.custom){ + //yell at the browser for fucking up, and tell it what it did wrong. + socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`}); + }else{ + //yell at the browser for fucking up + socket.emit("kick", {type: "Disconnected", reason: "An unexpected server crash was just prevented. You should probably report this to an admin."}); + } return socket.disconnect(); } diff --git a/src/utils/media/internetArchiveUtils.js b/src/utils/media/internetArchiveUtils.js index 77bb4a7..0b940be 100644 --- a/src/utils/media/internetArchiveUtils.js +++ b/src/utils/media/internetArchiveUtils.js @@ -19,8 +19,9 @@ const url = require("node:url"); const validator = require('validator'); //Local Imports -const regexUtils = require('../regexUtils'); -const media = require('../../app/channel/media/media'); +const media = require('../../app/channel/media/media.js'); +const regexUtils = require('../regexUtils.js'); +const loggerUtils = require('../loggerUtils.js') module.exports.fetchMetadata = async function(link, title){ //Parse link @@ -52,7 +53,7 @@ module.exports.fetchMetadata = async function(link, title){ if(!response.ok){ //Scream and shout const errorBody = await response.text(); - throw new Error(`Internet Archive Error '${response.status}': ${errorBody}`); + throw loggerUtils.exceptionSmith(`Internet Archive Error '${response.status}': ${errorBody}`, "queue"); } //Collect our metadata diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js index 71aa672..44a8dd9 100644 --- a/src/utils/scheduler.js +++ b/src/utils/scheduler.js @@ -45,7 +45,7 @@ module.exports.kickoff = function(){ //Process Hashed IP Records that haven't been recorded in a week or more userModel.processAgedIPRecords(); //Process expired global bans that may have expired since last restart - userBanModel.processExpiredBans() + userBanModel.processExpiredBans(); //Process expired channel bans that may have expired since last restart channelModel.processExpiredBans(); //Process expired password reset requests that may have expired since last restart diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 16fcb3e..818a1ae 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -16,9 +16,10 @@ along with this program. If not, see .*/ //Local Imports const config = require('../../config.json'); -const {userModel} = require('../schemas/user/userSchema'); -const userBanModel = require('../schemas/user/userBanSchema') -const altchaUtils = require('../utils/altchaUtils'); +const {userModel} = require('../schemas/user/userSchema.js'); +const userBanModel = require('../schemas/user/userBanSchema.js') +const altchaUtils = require('../utils/altchaUtils.js'); +const loggerUtils = require('../utils/loggerUtils.js') //Create failed sign-in cache since it's easier and more preformant to implement it this way than adding extra burdon to the database //Server restarts are far and few between. It would take multiple during a single bruteforce attempt for this to become an issue. @@ -41,15 +42,24 @@ module.exports.authenticateSession = async function(user, pass, req){ //If this ip is randy bobandy if(ipBanDB != null){ - //tell it to fuck off - throw new Error(`The IP address you are trying to login from has been banned.`); + //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P + const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration(); + + //If the ban is permanent + if(ipBanDB.permanent){ + //tell it to fuck off + throw loggerUtils.exceptionSmith(`The IP address you are trying to login from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`, "unauthorized"); + }else{ + //tell it to fuck off + throw loggerUtils.exceptionSmith(`The IP address you are trying to login from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`, "unauthorized"); + } } //If we have failed attempts if(attempt != null){ //If we have more failed attempts than allowed if(attempt.count > maxAttempts){ - throw new Error("This account has been locked for at 24 hours due to a large amount of failed log-in attempts"); + throw loggerUtils.exceptionSmith("This account has been locked for at 24 hours due to a large amount of failed log-in attempts", "unauthorized"); } //If we're throttling logins @@ -57,9 +67,9 @@ module.exports.authenticateSession = async function(user, pass, req){ //Verification doesnt get sanatized or checked since that would most likely break the cryptography //Since we've already got access to the request and dont need to import anything, why bother getting it from a parameter? if(req.body.verification == null){ - throw new Error("Verification failed!"); + throw loggerUtils.exceptionSmith("Verification failed!", "unauthorized"); }else if(!altchaUtils.verify(req.body.verification, user)){ - throw new Error("Verification failed!"); + throw loggerUtils.exceptionSmith("Verification failed!", ""); } } } @@ -75,9 +85,9 @@ module.exports.authenticateSession = async function(user, pass, req){ //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P const expiration = userBanDB.getDaysUntilExpiration() < 1 ? 0 : userBanDB.getDaysUntilExpiration(); if(userBanDB.permanent){ - throw new Error(`Your account has been permanently banned, and will be nuked from the database in: ${expiration} day(s)`); + throw loggerUtils.exceptionSmith(`Your account has been permanently banned, and will be nuked from the database in: ${expiration} day(s)`, "unauthorized"); }else{ - throw new Error(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`); + throw loggerUtils.exceptionSmith(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`, "unauthorized"); } } From 730d816c459eab74976b129cd155a028aece55c7 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 27 Apr 2025 20:39:47 -0400 Subject: [PATCH 020/209] Fixed expiration handlers --- src/schemas/channel/channelSchema.js | 19 +++++++++++++------ src/schemas/user/emailChangeSchema.js | 9 +++++++-- src/schemas/user/passwordResetSchema.js | 9 +++++++-- src/schemas/user/userBanSchema.js | 8 ++++++-- src/schemas/user/userSchema.js | 18 +++++++++++------- 5 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 4dbbe2f..b56cdd3 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -318,16 +318,23 @@ channelSchema.statics.reqPermCheck = function(perm, chanField = "chanName"){ channelSchema.statics.processExpiredBans = async function(){ const chanDB = await this.find({}); - chanDB.forEach((channel) => { - channel.banList.forEach(async (ban, i) => { + for(let chanIndex in chanDB){ + //Pull channel from channels by index + const channel = chanDB[chanIndex]; + + //channel.banList.forEach(async (ban, banIndex) => { + for(let banIndex in channel.banList){ + //Pull ban from channel ban list + const ban = channel.banList[banIndex]; + //ignore permanent and non-expired bans if(ban.expirationDays >= 0 && ban.getDaysUntilExpiration() <= 0){ //Get the index of the ban - channel.banList.splice(i,1); - return await channel.save(); + channel.banList.splice(banIndex,1); + await channel.save(); } - }) - }); + } + } } //methods diff --git a/src/schemas/user/emailChangeSchema.js b/src/schemas/user/emailChangeSchema.js index ca20900..8ad104d 100644 --- a/src/schemas/user/emailChangeSchema.js +++ b/src/schemas/user/emailChangeSchema.js @@ -79,16 +79,21 @@ emailChangeSchema.pre('save', async function (next){ //statics emailChangeSchema.statics.processExpiredRequests = async function(){ //Pull all requests from the DB + //Tested finding request by date, but mongoose kept throwing casting errors. + //This seems to be an intermittent issue online. Maybe it will work in a future version? const requestDB = await this.find({}); //Fire em all off at once without waiting for the last one to complete since we don't fuckin' need to - requestDB.forEach(async (request) => { + for(let requestIndex in requestDB){ + //Pull request from requestDB by index + const request = requestDB[requestIndex]; + //If the request hasn't been processed and it's been expired if(request.getDaysUntilExpiration() <= 0){ //Delete the request await this.deleteOne({_id: request._id}); } - }); + } } //methods diff --git a/src/schemas/user/passwordResetSchema.js b/src/schemas/user/passwordResetSchema.js index 41ee05b..6fad8fd 100644 --- a/src/schemas/user/passwordResetSchema.js +++ b/src/schemas/user/passwordResetSchema.js @@ -70,16 +70,21 @@ passwordResetSchema.pre('save', async function (next){ //statics passwordResetSchema.statics.processExpiredRequests = async function(){ //Pull all requests from the DB + //Tested finding request by date, but mongoose kept throwing casting errors. + //This seems to be an intermittent issue online. Maybe it will work in a future version? const requestDB = await this.find({}); //Fire em all off at once without waiting for the last one to complete since we don't fuckin' need to - requestDB.forEach(async (request) => { + for(let requestIndex in requestDB){ + //pull request from requestDB by index + const request = requestDB[requestIndex]; + //If the request hasn't been processed and it's been expired if(request.getDaysUntilExpiration() <= 0){ //Delete the request await this.deleteOne({_id: request._id}); } - }); + } } //methods diff --git a/src/schemas/user/userBanSchema.js b/src/schemas/user/userBanSchema.js index 0d49cb8..88da80c 100644 --- a/src/schemas/user/userBanSchema.js +++ b/src/schemas/user/userBanSchema.js @@ -353,10 +353,14 @@ userBanSchema.statics.getBans = async function(){ } userBanSchema.statics.processExpiredBans = async function(){ + //Channel ban expirations may vary so there's no way to search for expired bans const banDB = await this.find({}); //Firem all off all at once seperately without waiting for one another - banDB.forEach(async (ban) => { + for(let banIndex in banDB){ + //Pull ban from banlist by index + const ban = banDB[banIndex]; + //This ban was already processed, and it's user has been deleted. There is no more to be done... if(ban.user == null){ return; @@ -394,7 +398,7 @@ userBanSchema.statics.processExpiredBans = async function(){ await this.deleteOne({_id: ban._id}); } } - }); + } } //methods diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 639ae15..4c1a983 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -381,22 +381,26 @@ userSchema.statics.processAgedIPRecords = async function(){ //Pull full userlist const users = await this.find({}); - //for every user - users.forEach((userDB) => { + //Not a fan of iterating through the DB but there doesn't seem to be a way to search for a doc based on the properties of it's subdoc + for(let userIndex in users){ + //Pull user record from users by index + const userDB = users[userIndex]; //For every recent ip within the user - userDB.recentIPs.forEach((record, recordI) => { + for(let recordIndex in userDB.recentIPs){ + //Pull record from recent IPs by index + const record = userDB.recentIPs[recordIndex]; //Check how long it's been since we've last seen the IP const daysSinceLastUse = ((new Date() - record.lastLog) / (1000 * 60 * 60 * 24)).toFixed(1); //If it's been more than a week if(daysSinceLastUse >= 7){ //Splice out the IP record - userDB.recentIPs.splice(recordI, 1); + userDB.recentIPs.splice(recordIndex, 1); //No reason to wait on this since we're done with this user - userDB.save(); + await userDB.save(); } - }); - }); + } + } } From 8305494915f4ecf9e456dc475c8d13efd5bce5cd Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 28 Apr 2025 17:44:07 -0400 Subject: [PATCH 021/209] Imrpoved IP Ban Messages --- src/app/channel/channelManager.js | 16 ++++++++-- .../api/account/registerController.js | 32 +++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 52ae732..c4a623f 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -101,8 +101,20 @@ module.exports = class{ //If this ip is randy bobandy if(ipBanDB != null){ - //tell it to fuck off - socket.emit("kick", {type: "kicked", reason: "The IP address you are trying to connect from has been banned!"}); + //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P + const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration(); + + //If the ban is permanent + if(ipBanDB.permanent){ + //tell it to fuck off + socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`}); + //Otherwise + }else{ + //tell it to fuck off + socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`}); + } + + return false; } diff --git a/src/controllers/api/account/registerController.js b/src/controllers/api/account/registerController.js index 43ba037..cbd7951 100644 --- a/src/controllers/api/account/registerController.js +++ b/src/controllers/api/account/registerController.js @@ -61,12 +61,32 @@ module.exports.post = async function(req, res){ //If this ip is randy bobandy if(ipBanDB != null){ - //Make the code and message look pretty (kinda) at the same time - const banMsg = [ - 'The IP address you are trying to register an account from has been banned.', - 'If you beleive this to be an error feel free to reach out to your server administrator.', - 'Otherwise, fuck off :)' - ]; + + //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P + const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration(); + let banMsg = []; + + //If the ban is permanent + if(ipBanDB.permanent){ + //tell it to fuck off + //Make the code and message look pretty (kinda) at the same time + banMsg = [ + 'The IP address you are trying to register an account from has been permanently banned.', + 'Your cleartext IP has been saved to the database.', + `Any associated accounts will be nuked in ${expiration} day(s).`, + 'If you beleive this to be an error feel free to reach out to your server administrator.', + 'Otherwise, fuck off :)' + ]; + }else{ + //tell it to fuck off + //Make the code and message look pretty (kinda) at the same time + banMsg = [ + 'The IP address you are trying to register an account from has been temporarily banned.', + `Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`, + 'If you beleive this to be an error feel free to reach out to your server administrator.', + 'Otherwise, fuck off :)' + ]; + } //tell it to fuck off return errorHandler(res, banMsg.join('
'), 'unauthorized'); From a6228a9fd90316303124c273585b12859f6b1a68 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 29 Apr 2025 00:13:19 -0400 Subject: [PATCH 022/209] Started improvement of validators and respective error messages --- .../api/admin/passwordResetController.js | 3 + src/schemas/channel/channelSchema.js | 3 +- src/schemas/user/userSchema.js | 10 +- src/validators/accountValidator.js | 165 ++++++++++++++++-- src/validators/channelValidator.js | 21 ++- 5 files changed, 180 insertions(+), 22 deletions(-) diff --git a/src/controllers/api/admin/passwordResetController.js b/src/controllers/api/admin/passwordResetController.js index deff3a3..4e87bfb 100644 --- a/src/controllers/api/admin/passwordResetController.js +++ b/src/controllers/api/admin/passwordResetController.js @@ -14,6 +14,9 @@ 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 .*/ +//config +const config = require('../../../../config.json'); + //npm imports const {validationResult, matchedData} = require('express-validator'); diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index b56cdd3..590d964 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -42,7 +42,8 @@ const channelSchema = new mongoose.Schema({ name: { type: mongoose.SchemaTypes.String, required: true, - maxLength: 50, + //Calculate max length by the validator max length and the size of an escaped character + maxLength: 50 * 6, default: 0 }, description: { diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 4c1a983..85d9f7b 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -73,23 +73,25 @@ const userSchema = new mongoose.Schema({ required: true, default: "/img/johnny.png" }, - //These should be larger than validator values to make room for escaped characters bio: { type: mongoose.SchemaTypes.String, required: true, - maxLength: 2000, + //Calculate max length by the validator max length and the size of an escaped character + maxLength: 1000 * 6, default: "Bio not set!" }, pronouns:{ type: mongoose.SchemaTypes.String, optional: true, - maxLength: 50, + //Calculate max length by the validator max length and the size of an escaped character + maxLength: 15 * 6, default: "" }, signature: { type: mongoose.SchemaTypes.String, required: true, - maxLength: 300, + //Calculate max length by the validator max length and the size of an escaped character + maxLength: 25 * 6, default: "Signature not set!" }, highLevel: { diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 9d1e9ed..4fc9d32 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -15,31 +15,166 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //NPM Imports -const { check, body, checkSchema, checkExact} = require('express-validator'); +const {checkSchema} = require('express-validator'); //local imports const {isRank} = require('./permissionsValidator'); -module.exports = { - user: (field = 'user') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 22}), +module.exports.user = function(field = 'user'){ + return checkSchema({ + [field]: { + escape: true, + trim: true, + isAlphanumeric: { + errorMessage: "Usernames must be alphanumeric." + }, + isLength: { + options: { + min: 1, + max: 22 + }, + errorMessage: "Usernames must be between 1 and 22 characters." + }, + } + }); +} - //Password security requirements may change over time, therefore we should only validate against strongPassword() when creating new accounts - //that way we don't break old ones upon change - pass: (field = 'pass') => body(field).notEmpty().escape().trim(), - securePass: (field) => module.exports.pass(field).isStrongPassword({minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1}), +function getPassSchema(field = 'pass'){ + //Heavily simplified from previous versions. + //Trimming passwords is iffy, and escaping them is a down-right bad idea + return { + [field]: { + notEmpty: true, + } + } +} - email: (field = 'email') => body(field).optional().isEmail().normalizeEmail(), +module.exports.pass = function(field = 'pass'){ + return checkSchema(getPassSchema(field)); +} - img: (field = 'img') => body(field).optional().isURL({require_tld: false, require_host: false}).trim(), +module.exports.securePass = function(field = 'pass'){ + const schema = getPassSchema(field); - //Length check before escaping to keep symbols from throwing the count - pronouns: (field = 'pronouns') => body(field).optional().trim().isLength({min: 0, max: 15}).escape(), + schema[field].isStrongPassword = { + options: { + minLength: 8, + minLowercase: 1, + minUppercase: 1, + minNumbers: 1, + minSymbols: 1 + }, + errorMessage: "Passwords must contain 8 characters, including at least one: Upper, Lower, Number, and Special char." + } - signature: (field = 'signature') => body(field).optional().trim().isLength({min: 1, max: 25}).escape(), + return checkSchema(schema); +} - bio: (field = 'bio') => body(field).optional().trim().isLength({min: 1, max: 1000}).escape(), +module.exports.email = function(field = 'email'){ + return checkSchema({ + [field]: { + optional: true, + isEmail: { + errorMessage: "Invalid E-Mail Address" + }, + normalizeEmail: true + } + }); +} - rank: (field = 'rank') => body(field).escape().trim().custom(isRank), +module.exports.img = function(field = 'img'){ + return checkSchema({ + [field]: { + optional: true, + isURL: { + options: { + require_tld: false, + require_host: false + }, + errorMessage: "Invalid URL." + }, + trim: true + } + }); +} - securityToken: (field = 'token') => check(field).escape().trim().isHexadecimal().isLength({min:32, max:32}) +module.exports.pronouns = function(field = 'pronouns'){ + return checkSchema({ + [field]: { + optional: true, + trim: true, + isLength: { + options: { + min: 0, + max: 15 + }, + errorMessage: "Pronouns must be under 15 characters." + }, + escape: true + } + }); +} + +module.exports.signature = function(field = 'signature'){ + return checkSchema({ + [field]: { + optional: true, + trim: true, + isLength: { + options: { + min: 1, + max: 25 + }, + errorMessage: "Signature must be between 1 and 25 characters." + }, + escape: true + } + }); +} + +module.exports.bio = function(field = 'bio'){ + return checkSchema({ + [field]: { + optional: true, + trim: true, + isLength: { + options: { + min: 1, + max: 1000 + }, + errorMessage: "Bio must be between 1 and 1000 characters." + }, + escape: true + } + }); +} + +module.exports.rank = function(field = 'rank'){ + return checkSchema({ + [field]: { + escape: true, + trim: true, + custom: { + options: isRank, + }, + errorMessage: "Invalid rank." + } + }); +} + +module.exports.securityToken = function(field = 'token'){ + return checkSchema({ + [field]: { + escape: true, + trim: true, + isHexadecimal: true, + isLength: { + options: { + min: 32, + max: 32 + } + }, + errorMessage: "Invalid security token." + } + }); } \ No newline at end of file diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index 29b6995..0032ad8 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -21,8 +21,6 @@ const { check, body, checkSchema, checkExact} = require('express-validator'); const accountValidator = require('./accountValidator'); module.exports = { - name: (field = 'name') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 50}), - description: (field = 'description') => body(field).escape().trim().isLength({min: 1, max: 1000}), thumbnail: (field = 'thumbnail') => accountValidator.img(field), @@ -35,4 +33,23 @@ module.exports = { isBoolean: true, } })) +} + +module.exports.name = function(field = 'name'){ + return checkSchema({ + [field]: { + esacpe: true, + isAlphanumeric: { + errorMessage: "Channel names must be alphanumeric." + }, + isLength: { + options: { + min: 1, + max: 50 + }, + errorMessage: "Channel numes must be between 1 and 50 characters." + }, + trim: true + } + }); } \ No newline at end of file From 23ceb748834967501f96531d9852478524f6f657 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 29 Apr 2025 07:05:38 -0400 Subject: [PATCH 023/209] Finished up with improvements to error handling. --- src/schemas/channel/channelSchema.js | 3 +- src/schemas/emoteSchema.js | 3 +- src/validators/accountValidator.js | 2 +- src/validators/channelValidator.js | 51 ++++++++++++---- src/validators/emoteValidator.js | 80 +++++++++++++++++++------- src/validators/permissionsValidator.js | 2 +- src/validators/tokebotValidator.js | 21 ++++++- www/js/utils.js | 6 +- 8 files changed, 128 insertions(+), 40 deletions(-) diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 590d964..f98332e 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -49,7 +49,8 @@ const channelSchema = new mongoose.Schema({ description: { type: mongoose.SchemaTypes.String, required: true, - maxLength: 1000, + //Calculate max length by the validator max length and the size of an escaped character + maxLength: 1000 * 6, default: 0 }, thumbnail: { diff --git a/src/schemas/emoteSchema.js b/src/schemas/emoteSchema.js index dafda2a..9afb512 100644 --- a/src/schemas/emoteSchema.js +++ b/src/schemas/emoteSchema.js @@ -26,7 +26,8 @@ const typeEnum = ["image", "video"]; const emoteSchema = new mongoose.Schema({ name:{ type: mongoose.SchemaTypes.String, - required: true + required: true, + maxLength: 14, }, link:{ type: mongoose.SchemaTypes.String, diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 4fc9d32..1e021a4 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //NPM Imports -const {checkSchema} = require('express-validator'); +const { checkSchema } = require('express-validator'); //local imports const {isRank} = require('./permissionsValidator'); diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index 0032ad8..b0390a3 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -15,30 +15,24 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //NPM Imports -const { check, body, checkSchema, checkExact} = require('express-validator'); +const { checkSchema, checkExact } = require('express-validator'); //local imports const accountValidator = require('./accountValidator'); module.exports = { - description: (field = 'description') => body(field).escape().trim().isLength({min: 1, max: 1000}), - - thumbnail: (field = 'thumbnail') => accountValidator.img(field), - - rank: (field = 'rank') => accountValidator.rank(field), settingsMap: () => checkExact(checkSchema({ 'settingsMap.hidden': { optional: true, isBoolean: true, - } - })) + } })) } module.exports.name = function(field = 'name'){ return checkSchema({ [field]: { - esacpe: true, + escape: true, isAlphanumeric: { errorMessage: "Channel names must be alphanumeric." }, @@ -47,9 +41,46 @@ module.exports.name = function(field = 'name'){ min: 1, max: 50 }, - errorMessage: "Channel numes must be between 1 and 50 characters." + errorMessage: "Channel names must be between 1 and 50 characters." }, trim: true } }); +} + +module.exports.description = function(field = 'description'){ + return checkSchema({ + [field]:{ + escape: true, + trim: true, + isLength: { + options: { + min: 1, + max: 1000 + }, + errorMessage: "Description must be between 1 and 1000 characters." + }, + } + }); +} + +module.exports.thumbnail = function(field = 'thumbnail'){ + return accountValidator.img(field); +} + + +module.exports.rank = function(field = 'rank'){ + return accountValidator.rank(field); +} + +module.exports.settingsMap = function(){ + return checkExact( + checkSchema({ + 'settingsMap.hidden': { + optional: true, + isBoolean: true, + errorMessage: "Bad channel settings map." + } + }) + ); } \ No newline at end of file diff --git a/src/validators/emoteValidator.js b/src/validators/emoteValidator.js index 806536f..3c4ee74 100644 --- a/src/validators/emoteValidator.js +++ b/src/validators/emoteValidator.js @@ -15,35 +15,71 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //NPM Imports -const { check } = require('express-validator'); +const { checkSchema } = require('express-validator'); const validator = require('validator');//We need validators for express-less code too! +const { errorMiddleware } = require('../utils/loggerUtils'); module.exports = { - name: (field = 'name') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 14}), - link: (field = 'link') => check(field).trim().isURL(), - manualName: (input) => { - //Trim and sanatize input - const clean = validator.trim(validator.escape(input)); - //if cleaned input is a proper emote name - if(validator.isLength(clean, {min: 1, max: 14}) && validator.isAlphanumeric(clean)){ - //return cleaned input - return clean; +} + +module.exports.name = function(field = 'name'){ + return checkSchema({ + [field]: { + escape: true, + trim: true, + isAlphanumeric: { + errorMessage: "Emote names must be alphanumeric." + }, + isLength: { + options: { + min: 1, + max: 14 + }, + errorMessage: "Emote name must be between 1 and 14 characters." + }, } + }) +} - //otherwise return false - return false; - }, - manualLink: (input) => { - //Trim the input - const clean = validator.trim(input) - - //If we have a URL return the trimmed input - if(validator.isURL(clean)){ - return clean; +module.exports.link = function(field = 'link'){ + return checkSchema({ + [field]: { + isURL: { + options: { + require_tld: false, + require_host: false + }, + errorMessage: "Invalid URL." + }, + trim: true } + }) +} - //otherwise return false - return false; +module.exports.manualName = function(input){ + //Trim and sanatize input + const clean = validator.trim(validator.escape(input)); + + //if cleaned input is a proper emote name + if(validator.isLength(clean, {min: 1, max: 14}) && validator.isAlphanumeric(clean)){ + //return cleaned input + return clean; } + + //otherwise return false + return false; +} + +module.exports.manualLink = function(input){ + //Trim the input + const clean = validator.trim(input) + + //If we have a URL return the trimmed input + if(validator.isURL(clean)){ + return clean; + } + + //otherwise return false + return false; } \ No newline at end of file diff --git a/src/validators/permissionsValidator.js b/src/validators/permissionsValidator.js index c304406..baa04ae 100644 --- a/src/validators/permissionsValidator.js +++ b/src/validators/permissionsValidator.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //NPM Imports -const { check, body, checkSchema, checkExact} = require('express-validator'); +const { checkSchema } = require('express-validator'); //local imports const permissionModel = require("../schemas/permissionSchema"); diff --git a/src/validators/tokebotValidator.js b/src/validators/tokebotValidator.js index a4edec3..02a7008 100644 --- a/src/validators/tokebotValidator.js +++ b/src/validators/tokebotValidator.js @@ -15,8 +15,23 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //NPM Imports -const { check } = require('express-validator'); +const { check, checkSchema } = require('express-validator'); -module.exports = { - command: (field = 'command') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 30}), +module.exports.command = function(field = 'command'){ + return checkSchema({ + [field]: { + escape: true, + trim: true, + isAlphanumeric: { + errorMessage: "Toke commands must be alphanumeric." + }, + isLength: { + options: { + min: 1, + max: 30 + }, + errorMessage: "Toke commands must be between 1 and 30 characters." + } + } + }); } \ No newline at end of file diff --git a/www/js/utils.js b/www/js/utils.js index 24da23d..f93e2b7 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -105,7 +105,11 @@ class canopyUXUtils{ try{ const errors = body.errors; errors.forEach((err)=>{ - new canopyUXUtils.popup(`

${err.type} Error:


${err.msg}

`); + //Capitalize the first letter of the type + const type = `${err.type[0].toUpperCase()}${err.type.slice(1)}` + + //Display our error + new canopyUXUtils.popup(`

${type} Error:


${err.msg}

`); }); }catch(err){ console.error("Display Error Body:"); From d3aa4b8274ab24664d66d8ebb333acd31349e722 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 2 May 2025 03:47:46 -0400 Subject: [PATCH 024/209] Added underscores and dashes to usernames --- src/validators/accountValidator.js | 14 ++++++++++++-- www/css/channel.css | 2 ++ www/js/channel/chatPostprocessor.js | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 1e021a4..38f08b7 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -25,9 +25,19 @@ module.exports.user = function(field = 'user'){ [field]: { escape: true, trim: true, - isAlphanumeric: { - errorMessage: "Usernames must be alphanumeric." + //Caution: Nerd Rant + //isAlphanumerics only takes locale for the option flag in schemas for some reason... + matches: { + //See this is the shit I'm talking about, WHY IS THIS CALLED OPTIONS? IT SHOULD BE PATTERN + //OPTIONS IS SUPPOSED TO BE AN OBJECT THAT PASSES EXTRA VALUES THATS LITERALLY HOW EVERYTHING ELSE IN THIS FUCKING LIBRARY WORKS + //WHO FUCKING WROTE THIS SHIT!?!?!?!?! + //HOW IS THIS ACCEPTED ON ONE OF THE MOST WIDELY USED VALIDATION LIBRARIES ON THE WEB!??!?!?!!? + //IT'S NOT EVEN FUCKING DOCUMENTED ANYWHERE!!!!!!!!!!!! + //WEBDEVS, GET YOUR FUCKING SHIT TOGETHER, FUCK! + options: [/^[A-Za-z0-9-_]+$/], + errorMessage: "Usernames can only contain numbers, letters, underscores, and dashes." }, + //matches: /^[A-Za-z0-9-_]+$/, isLength: { options: { min: 1, diff --git a/www/css/channel.css b/www/css/channel.css index 138302b..2f18ea6 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -165,6 +165,7 @@ p.panel-head-element{ .chat-entry-username{ margin: auto 0.2em auto 0; + text-wrap: nowrap; } .chat-entry-body{ @@ -203,6 +204,7 @@ span.user-entry{ font-size: 1em; width: fit-content; user-select: none; + text-wrap: nowrap; } .whisper{ diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index e9c2518..818c533 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -73,11 +73,11 @@ class chatPostprocessor{ //Create an empty array to hold the body this.messageArray = []; - //Escape any sanatized char codes as we use .textContent for double-safety, and to prevent splitting of char codes - //Split string by word-boundries on words and non-word boundries around whitespace, with negative lookaheads to exclude file seperators so we don't split link placeholders + //Unescape any sanatized char codes as we use .textContent for double-safety, and to prevent splitting of char codes + //Split string by word-boundries on words and non-word boundries around whitespace, with negative lookaheads to exclude file seperators so we don't split link placeholders, and dashes so we dont split usernames and other things //Also split by any invisble whitespace as a crutch to handle mushed links/emotes //If we can one day figure out how to split non-repeating special chars instead of special chars with whitespace, that would be perf, unfortunately my brain hasn't rotted enough to understand regex like that just yet. - const splitString = utils.unescapeEntities(this.rawData.msg).split(/(? { From 99fa549469443af164bde29f039abfea0850a05d Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 4 May 2025 16:43:57 -0400 Subject: [PATCH 025/209] Moved sync latching logic over to mediaHandler class. --- www/js/channel/mediaHandler.js | 94 ++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index fcfbb76..595055c 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -23,6 +23,9 @@ class mediaHandler{ //Set handler type this.type = type + //Denotes wether a seek call was made by the syncing function + this.selfAct = false; + //Set last received timestamp to 0 this.lastTimestamp = 0; @@ -69,12 +72,20 @@ class mediaHandler{ } sync(timestamp = this.lastTimestamp){ + //Skip sync calls that won't seek so we don't pointlessly throw selfAct + if(timestamp != this.video.currentTime){ + //Set self act flag + this.selfAct = true; + } } reload(){ //Get current timestamp const timestamp = this.video.currentTime; + //Throw self act flag to make sure we don't un-sync the player + this.selfAct = true; + //Load video from source this.video.load(); @@ -86,7 +97,13 @@ class mediaHandler{ } end(){ + //Null out current media this.nowPlaying = null; + + //Throw self act to prevent unlock on video end + this.selfAct = true; + + //Destroy the player this.destroyPlayer(); } @@ -121,6 +138,30 @@ class mediaHandler{ setVideoTitle(title){ this.player.title.textContent = `Currently Playing: ${title}`; } + + onMetadataLoad(event){ + //Resize aspect (if locked), since the video doesn't properly report it's resolution until it's been loaded + this.client.chatBox.resizeAspect(); + } + + onPause(event){ + //If the video was paused out-side of code + if(!this.selfAct){ + this.player.unlockSync(); + } + + this.selfAct = false; + } + + onSeek(event){ + //If the video was seeked out-side of code + if(!this.selfAct){ + this.player.unlockSync(); + } + + //reset self act flag + this.selfAct = false; + } } class nullHandler extends mediaHandler{ @@ -157,15 +198,16 @@ class nullHandler extends mediaHandler{ } } +//This at first was hard to seperate out as parts of it are used for other handlers because they're based on the
\ No newline at end of file diff --git a/www/css/panel/settings.css b/www/css/panel/settings.css new file mode 100644 index 0000000..95bfa62 --- /dev/null +++ b/www/css/panel/settings.css @@ -0,0 +1,33 @@ +/*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 .*/ +#settings-panel{ + display: flex; + flex-direction: column; + gap: 1em; +} + +#settings-panel h2{ + text-align: center; + margin: 1em 0 0; +} + +.settings-panel-setting{ + display: flex; + flex-direction: row; + text-wrap: nowrap; + height: 1em; + align-items: center; +} \ No newline at end of file diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index e4e5eda..97b1b19 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -32,6 +32,12 @@ class channel{ this.userList = new userList(this); //Create the Canopy Panel Object this.cPanel = new cPanel(this); + + //Set defaults for any unset settings and run any required process steps for the current config + this.setDefaults(false, true); + + //Freak out any weirdos who take a peek in the dev console for gaffs + console.log("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇."); } connect(){ @@ -91,6 +97,52 @@ class channel{ //Store queue lock status this.queueLock = data.queueLock; } + + setDefaults(force = false, processConfig = false){ + //Iterate through default config + for(let [key, value] of channel.defaultConfig){ + //If the setting is unset or function was called forcefully + if(force || localStorage.getItem(key) == null){ + //Set item from default map + localStorage.setItem(key, value); + } + + //If we're running process steps for the config + if(processConfig){ + //Process the current config value + this.processConfig(key, localStorage.getItem(key)); + } + } + } + + processConfig(key, value){ + //Switch/case by config key + switch(key){ + case 'ytPlayerType': + console.log("FUCK"); + //If the user is running the embedded player + if(value == 'embed'){ + //Find our footer + const footer = document.querySelector('footer'); + + //Create new script tag + const ytEmbedAPI = document.createElement('script'); + //Link the iframe api from youtube + ytEmbedAPI.src = "https://www.youtube.com/iframe_api"; + //set the iframe api script id + ytEmbedAPI.classList.add('yt-embed-api'); + + //Append the script tag to the top of the footer to give everything else access + footer.prepend(ytEmbedAPI); + } + //Stop while we're ahead + return; + } + } + + static defaultConfig = new Map([ + ["ytPlayerType","raw"] + ]); } const client = new channel(); \ No newline at end of file diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index 30d8840..5ba1b3b 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -64,7 +64,7 @@ class chatBox{ this.chatPrompt.addEventListener("input", this.displayAutocomplete.bind(this)); this.autocompleteDisplay.addEventListener("click", this.tabComplete.bind(this)); this.sendButton.addEventListener("click", this.send.bind(this)); - this.settingsIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new panelObj(client))}); + this.settingsIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new settingsPanel(client))}); this.adminIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new queuePanel(client))}); this.emoteIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new emotePanel(client))}); diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js new file mode 100644 index 0000000..c17ef7c --- /dev/null +++ b/www/js/channel/panels/settingsPanel.js @@ -0,0 +1,37 @@ +/*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 .*/ +class settingsPanel extends panelObj{ + constructor(client, panelDocument){ + super(client, "Client Settings", "/panel/settings", panelDocument); + } + + closer(){ + } + + docSwitch(){ + this.youtubeSource = this.panelDocument.querySelector("#settings-panel-youtube-source select"); + + this.setupInput(); + } + + setupInput(){ + this.youtubeSource.addEventListener('change', this.updateYoutubeSource.bind(this)); + } + + updateYoutubeSource(){ + localStorage.setItem("ytPlayerType", this.youtubeSource.value); + } +} \ No newline at end of file From 047c368b70b448bbae0ec9ccdbc19cf7a87f9537 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 7 May 2025 01:09:36 -0400 Subject: [PATCH 041/209] FUCK --- www/js/channel/channel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 97b1b19..90402f8 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -119,7 +119,6 @@ class channel{ //Switch/case by config key switch(key){ case 'ytPlayerType': - console.log("FUCK"); //If the user is running the embedded player if(value == 'embed'){ //Find our footer From 1344756449fe0c47a29cfef97ab040c82daf9b3f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 7 May 2025 08:17:38 -0400 Subject: [PATCH 042/209] Started working on implementing official YT iframe-embed API --- www/js/channel/channel.js | 37 ++++++++++++++++++++++++-- www/js/channel/mediaHandler.js | 28 +++++++++++++++++++ www/js/channel/panels/settingsPanel.js | 6 +++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 90402f8..4670dcb 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -21,6 +21,9 @@ class channel{ //Define socket listeners this.defineListeners(); + //Flag youtube iframe-embed api as unloaded + this.ytEmbedAPILoaded = false; + //Scrape channel name off URL this.channelName = window.location.pathname.split('/c/')[1].split('/')[0]; @@ -119,8 +122,9 @@ class channel{ //Switch/case by config key switch(key){ case 'ytPlayerType': - //If the user is running the embedded player - if(value == 'embed'){ + const embedScript = document.querySelector(".yt-embed-api"); + //If the user is running the embedded player and we don't have en embed script loaded + if(value == 'embed' && embedScript == null){ //Find our footer const footer = document.querySelector('footer'); @@ -133,7 +137,21 @@ class channel{ //Append the script tag to the top of the footer to give everything else access footer.prepend(ytEmbedAPI); + //If we're not using the embed player but the script is loaded + }else if(embedScript != null){ + //Pull all scripts since the main one might have pulled others + const scripts = document.querySelectorAll('script'); + + //Iterate through all script tags on the page + for(let script of scripts){ + //If the script came from youtube + if(script.src.match(/youtube\.com|youtu\.be/)){ + //Rip it out + script.remove(); + } + } } + //Stop while we're ahead return; } @@ -144,4 +162,19 @@ class channel{ ]); } +//Youtube iframe-embed API load handler +function onYoutubeIframeAPIReady(){ + //Set embed api to true + client.ytEmbedAPILoaded = true; + + //Get currently playing item + const nowPlaying = client.player.mediaHandler.nowPlaying; + + //If we're playing a youtube video and the official embeds are enabled + if(nowPlaying.type == 'yt' && localStorage.get('ytPlayerType') == "embed"){ + //Restart the video now that the embed api has loaded + client.player.start({media: nowPlaying}); + } +} + const client = new channel(); \ No newline at end of file diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 209cd8f..04d6ed6 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -303,4 +303,32 @@ class rawFileHandler extends rawFileBase{ //Return current timestamp return this.video.currentTime; } +} + +class youtubeEmbedHandler extends mediaHandler{ + constructor(client, player, media){ + //Call derived constructor + super(client, player, media, 'ytEmbed'); + } + + buildPlayer(){ + //If the embed API has loaded + if(this.client.ytEmbedAPILoaded){ + //Create a new youtube player using the official YT iframe-embed api + this.video = new YT.Player('video', { + //Inject video id + videoID: media.id, + //Set up event listeners (NGL kinda nice of google to do it this way...) + events: { + 'onReady': this.embedPlayer.bind(this) + } + }); + } + + //Call derived function + super.buildPlayer(); + } + + embedPlayer(){ + } } \ No newline at end of file diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index c17ef7c..c2e0aca 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -24,6 +24,7 @@ class settingsPanel extends panelObj{ docSwitch(){ this.youtubeSource = this.panelDocument.querySelector("#settings-panel-youtube-source select"); + this.renderSettings(); this.setupInput(); } @@ -31,7 +32,12 @@ class settingsPanel extends panelObj{ this.youtubeSource.addEventListener('change', this.updateYoutubeSource.bind(this)); } + renderSettings(){ + this.youtubeSource.value = localStorage.getItem("ytPlayerType"); + } + updateYoutubeSource(){ localStorage.setItem("ytPlayerType", this.youtubeSource.value); + client.processConfig("ytPlayerType", this.youtubeSource.value); } } \ No newline at end of file From 1b0caa5e02cf4c3232e3a87731586a7395b535e0 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 8 May 2025 08:22:42 -0400 Subject: [PATCH 043/209] Completed buildPlayer method for youtubeEmbedHandler --- src/utils/media/yanker.js | 4 ++-- src/utils/media/ytdlpUtils.js | 2 +- www/css/channel.css | 5 +++++ www/js/channel/channel.js | 4 ++-- www/js/channel/mediaHandler.js | 40 ++++++++++++++++++++++++---------- www/js/channel/player.js | 7 ++++-- 6 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 6ee63db..09cb486 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -33,7 +33,7 @@ module.exports.yankMedia = async function(url, title){ return await iaUtil.fetchMetadata(pullType.id, title); case "yt": //return mediao object list from the YT-DLP module's youtube function - return await ytdlpUtil.fetchYoutubeVideoMetadata(pullType.id, title); + return await ytdlpUtil.fetchYoutubeMetadata(pullType.id, title); case "dm": //return mediao object list from the YT-DLP module's dailymotion function return await ytdlpUtil.fetchDailymotionMetadata(pullType.id, title); @@ -58,7 +58,7 @@ module.exports.refreshRawLink = async function(mediaObj){ } //Re-fetch media metadata - metadata = await ytdlpUtil.fetchYoutubeVideoMetadata(mediaObj.id); + metadata = await ytdlpUtil.fetchYoutubeMetadata(mediaObj.id); //Refresh media rawlink from metadata mediaObj.rawLink = metadata[0].rawLink; diff --git a/src/utils/media/ytdlpUtils.js b/src/utils/media/ytdlpUtils.js index 16bbd4e..af5805d 100644 --- a/src/utils/media/ytdlpUtils.js +++ b/src/utils/media/ytdlpUtils.js @@ -29,7 +29,7 @@ const media = require('../../app/channel/media/media.js'); const regexUtils = require('../regexUtils.js'); const loggerUtils = require('../loggerUtils.js') -module.exports.fetchYoutubMetadata = async function(id, title){ +module.exports.fetchYoutubeMetadata = async function(id, title){ try{ //Try to pull media from youtube id const media = await fetchMetadata(`https://youtu.be/${id}`, title, 'yt'); diff --git a/www/css/channel.css b/www/css/channel.css index abad3cc..e7644c4 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -63,6 +63,11 @@ div#media-panel-div{ font-size: 1.2em; } +#youtube-embed-player{ + width: 100%; + height: 100%; +} + div#chat-panel-div{ position: relative; display: flex; diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 4670dcb..a246996 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -163,7 +163,7 @@ class channel{ } //Youtube iframe-embed API load handler -function onYoutubeIframeAPIReady(){ +function onYouTubeIframeAPIReady(){ //Set embed api to true client.ytEmbedAPILoaded = true; @@ -171,7 +171,7 @@ function onYoutubeIframeAPIReady(){ const nowPlaying = client.player.mediaHandler.nowPlaying; //If we're playing a youtube video and the official embeds are enabled - if(nowPlaying.type == 'yt' && localStorage.get('ytPlayerType') == "embed"){ + if(nowPlaying.type == 'yt' && localStorage.getItem('ytPlayerType') == "embed"){ //Restart the video now that the embed api has loaded client.player.start({media: nowPlaying}); } diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 04d6ed6..e6f7e2a 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -312,23 +312,39 @@ class youtubeEmbedHandler extends mediaHandler{ } buildPlayer(){ - //If the embed API has loaded - if(this.client.ytEmbedAPILoaded){ - //Create a new youtube player using the official YT iframe-embed api - this.video = new YT.Player('video', { - //Inject video id - videoID: media.id, - //Set up event listeners (NGL kinda nice of google to do it this way...) - events: { - 'onReady': this.embedPlayer.bind(this) - } - }); + //If the embed API hasn't loaded + if(!this.client.ytEmbedAPILoaded){ + //Complain and stop + return console.warn("youtubeEmbedHandler.buildPlayer() Called before YT Iframe API Loaded, waiting on refresh to rebuild..."); } + //Clear out the player title so that youtube's baked in title can do it's thing. + //This will be replaced once we complete the full player control and remove the defualt youtube UI + this.player.title.textContent = ""; + + //Create temp div for yt api to replace + const tempDiv = document.createElement('div'); + //Name the div + tempDiv.id = "youtube-embed-player" + //Append it to the video container + this.player.videoContainer.appendChild(tempDiv); + + //Create a new youtube player using the official YT iframe-embed api + this.video = new YT.Player('youtube-embed-player', { + //Inject video id + videoId: this.nowPlaying.id, + //Set up event listeners (NGL kinda nice of google to do it this way...) + events: { + 'onReady': this.initializePlayer.bind(this) + } + }); + //Call derived function super.buildPlayer(); } - embedPlayer(){ + initializePlayer(){ + //Kick the video off + this.video.playVideo(); } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 552d3fb..ec59b1e 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -87,8 +87,11 @@ class player{ this.mediaHandler = new nullHandler(client, this); //Otherwise }else{ - //If we have a raw-file compatible source - if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){ + //If we have a youtube video and the official embedded iframe player is selected + if(data.media.type == 'yt' && localStorage.getItem("ytPlayerType") == 'embed'){ + this.mediaHandler = new youtubeEmbedHandler(this.client, this, data.media); + //Otherwise, if we have a raw-file compatible source + }else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){ //Create a new raw file handler for it this.mediaHandler = new rawFileHandler(client, this, data.media); //Sync to time stamp From 473b54356e3749969da9481f2420664230c736c8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 10 May 2025 12:23:33 -0400 Subject: [PATCH 044/209] Implemented official yt iframe embed player support. --- www/js/channel/channel.js | 18 +++- www/js/channel/mediaHandler.js | 178 +++++++++++++++++++++++++++++---- www/js/channel/player.js | 6 ++ 3 files changed, 181 insertions(+), 21 deletions(-) diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index a246996..c373e56 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -39,7 +39,7 @@ class channel{ //Set defaults for any unset settings and run any required process steps for the current config this.setDefaults(false, true); - //Freak out any weirdos who take a peek in the dev console for gaffs + //Freak out any weirdos who take a peek in the dev console for shits n gigs console.log("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇."); } @@ -152,6 +152,22 @@ class channel{ } } + //If the player or mediaHandler isn't loaded + if(this.player == null || this.player.mediaHandler == null){ + //We're fuggin done here + return; + } + + + //Get current video + const nowPlaying = this.player.mediaHandler.nowPlaying; + + //If we're playing a youtube video + if(nowPlaying != null && nowPlaying.type == 'yt'){ + //Restart the video + this.player.start({media: nowPlaying}); + } + //Stop while we're ahead return; } diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index e6f7e2a..4b6079f 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -64,9 +64,15 @@ class mediaHandler{ } start(){ + this.setVideoTitle(this.nowPlaying.title); } sync(timestamp = this.lastTimestamp){ + //Skip sync calls that won't seek so we don't pointlessly throw selfAct + if(timestamp != this.video.currentTime){ + //Set self act flag + this.selfAct = true; + } } reload(){ @@ -95,8 +101,8 @@ class mediaHandler{ } setPlayerLock(lock){ - //set lock property - this.lock = lock; + //set lock property + this.lock = lock; } getRatio(){ @@ -165,14 +171,6 @@ class rawFileBase extends mediaHandler{ super.destroyPlayer(); } - sync(timestamp = this.lastTimestamp){ - //Skip sync calls that won't seek so we don't pointlessly throw selfAct - if(timestamp != this.video.currentTime){ - //Set self act flag - this.selfAct = true; - } - } - reload(){ //Call derived method super.reload(); @@ -270,9 +268,6 @@ class rawFileHandler extends rawFileBase{ //Set video volume this.video.volume = this.player.volume; - //Set video title - this.setVideoTitle(this.nowPlaying.title); - //Unlock player this.setPlayerLock(false); @@ -309,6 +304,21 @@ class youtubeEmbedHandler extends mediaHandler{ constructor(client, player, media){ //Call derived constructor super(client, player, media, 'ytEmbed'); + + //Set flag to notify functions when the player is actually ready + this.ready = false; + + //Create property to hold video iframe for easy access + this.iframe = null; + } + + //custom start media function since we want the youtube player to call the start function once it's ready + startMedia(media){ + //If we properly ingested the media + if(this.ingestMedia(media)){ + //Build the video player + this.buildPlayer(); + } } buildPlayer(){ @@ -318,10 +328,6 @@ class youtubeEmbedHandler extends mediaHandler{ return console.warn("youtubeEmbedHandler.buildPlayer() Called before YT Iframe API Loaded, waiting on refresh to rebuild..."); } - //Clear out the player title so that youtube's baked in title can do it's thing. - //This will be replaced once we complete the full player control and remove the defualt youtube UI - this.player.title.textContent = ""; - //Create temp div for yt api to replace const tempDiv = document.createElement('div'); //Name the div @@ -333,9 +339,9 @@ class youtubeEmbedHandler extends mediaHandler{ this.video = new YT.Player('youtube-embed-player', { //Inject video id videoId: this.nowPlaying.id, - //Set up event listeners (NGL kinda nice of google to do it this way...) events: { - 'onReady': this.initializePlayer.bind(this) + 'onReady': this.start.bind(this), + 'onStateChange': this.onStateChange.bind(this) } }); @@ -343,8 +349,140 @@ class youtubeEmbedHandler extends mediaHandler{ super.buildPlayer(); } - initializePlayer(){ + start(){ + //Call derived start function + super.start(); + + //Set volume based on player volume + this.video.setVolume(this.player.volume * 100); + //Kick the video off this.video.playVideo(); + + //Pull iframe + this.iframe = this.video.getIframe() + + //Throw the ready flag + this.ready = true; + } + + destroyPlayer(){ + //If we've had enough time to create a player frame + if(this.ready){ + //Pull volume from player before destroying since google didn't give us a volume change event like a bunch of dicks + this.player.volume = (this.video.getVolume() / 100); + + //Use the embed api's built in destroy function + this.video.destroy(); + } + + //If we have any leftovers + if(this.iframe != null){ + //Nukem like last nights chicken + iframe.remove(); + } + + //Call derived destroy function + super.destroyPlayer(); + } + + sync(timestamp = this.lastTimestamp){ + //If we're not ready + if(!this.ready){ + //Kick off a timer to wait it out and try again l8r + setTimeout(this.sync.bind(this), 100); + + //If it failed, tell randy to fuck off + return; + } + + //Seek to timestamp, allow buffering + this.video.seekTo(timestamp, true); + } + + reload(){ + //if we're ready + if(this.ready){ + //re-load the video by id + this.video.loadVideoById(this.nowPlaying.id); + } + } + + play(){ + //If we're ready + if(this.ready){ + //play the video + this.video.playVideo(); + } + } + + pause(){ + //If we're ready + if(this.ready){ + //pause the video + this.video.pauseVideo(); + } + } + + getRatio(){ + //TODO: Implement a type-specific metadata property object in the media class to hold type-sepecifc meta-data + //Alternatively we could fill in resolution information from the raw link + //However keeping embedded functionality dependant on raw-links seems like bad practice + } + + getTimestamp(){ + //If we're ready + if(this.ready){ + //Return the timestamp + return this.video.getCurrentTime(); + } + + //If we fall through, simply report that the video hasn't gone anywhere yet + return 0; + } + + setVideoTitle(){ + //Clear out the player title so that youtube's baked in title can do it's thing. + //This will be replaced once we complete the full player control and remove the defualt youtube UI + this.player.title.textContent = ""; + } + + //Generic handler for state changes since google is a dick + onStateChange(event){ + switch(event.data){ + //video unstarted + case -1: + return; + //video ended + case 0: + return; + //video playing + case 1: + return; + //video paused + case 2: + super.onPause(event); + return; + //video buffering + case 3: + //There is no good way to tell slow connections apart from user seeking + //This will be easier to implement once we get custom player controls up + //super.onSeek(event); + return; + //video queued + case 5: + return; + //bad status code + default: + return; + } + } + + setPlayerLock(lock){ + super.setPlayerLock(lock); + + if(this.ready){ + this.iframe.style.pointerEvents = (lock ? "none" : ""); + } } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index ec59b1e..55b8387 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -149,6 +149,12 @@ class player{ } updateCurrentRawFile(data){ + //typecheck the media handler to see if we really need to do any of this shit, if not... + if(this.mediaHandler.type == 'ytEmbed'){ + //Ignore it + return; + } + //Grab current item from media handler const currentItem = this.mediaHandler.nowPlaying; From e4bebce431c97dc3229cd10ec99ba2992827b2a9 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 10 May 2025 12:28:48 -0400 Subject: [PATCH 045/209] Fixed official yt embed player destruction --- www/js/channel/mediaHandler.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 4b6079f..6b03c30 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -376,10 +376,13 @@ class youtubeEmbedHandler extends mediaHandler{ this.video.destroy(); } + //Check the f̶r̶i̶d̶g̶e video container for leftovers + const leftovers = this.player.videoContainer.querySelector("#youtube-embed-player"); + //If we have any leftovers - if(this.iframe != null){ + if(leftovers != null){ //Nukem like last nights chicken - iframe.remove(); + leftovers.remove(); } //Call derived destroy function From 93265b789017abd826d015cbf257ba0627b0bff3 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 10 May 2025 22:07:20 -0400 Subject: [PATCH 046/209] Added stream URL to channel settings. --- src/schemas/channel/channelSchema.js | 4 ++ src/utils/media/yanker.js | 20 +++++++- src/utils/media/ytdlpUtils.js | 49 +++++++++++++++---- src/validators/channelValidator.js | 5 ++ .../partial/channelSettings/settings.ejs | 9 +++- www/js/channelSettings.js | 19 ++++++- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index f98332e..235023f 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -64,6 +64,10 @@ const channelSchema = new mongoose.Schema({ required: true, default: true }, + streamURL: { + type: mongoose.SchemaTypes.String, + default: '' + } }, permissions: { type: channelPermissionSchema, diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 09cb486..02f9193 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -32,8 +32,15 @@ module.exports.yankMedia = async function(url, title){ //return media object list from IA module return await iaUtil.fetchMetadata(pullType.id, title); case "yt": - //return mediao object list from the YT-DLP module's youtube function + //return media object list from the YT-DLP module's youtube function return await ytdlpUtil.fetchYoutubeMetadata(pullType.id, title); + case "ytp": + //return media object list from YT-DLP module's youtube playlist function + //return await ytdlpUtil.fetchYoutubePlaylistMetadata(pullType.id, title); + //Holding off on this since YT-DLP takes 10 years to do a playlist as it needs to pull each and every video one-by-one + //Maybe in the future a piped alternative might be in order, however this would most likely require us to host our own local instance. + //Though it could give us added resistance against youtube/google's rolling IP bans + return null; case "dm": //return mediao object list from the YT-DLP module's dailymotion function return await ytdlpUtil.fetchDailymotionMetadata(pullType.id, title); @@ -85,7 +92,7 @@ module.exports.getMediaType = async function(url){ //If we have link to a resource from archive.org if(match = url.match(/archive\.org\/(?:details|download)\/([a-zA-Z0-9\/._-\s\%]+)/)){ - //return internet archive code + //return internet archive upload id and filepath return { type: "ia", id: match[1] @@ -101,6 +108,15 @@ module.exports.getMediaType = async function(url){ } } + //If we have a match to a youtube playlist + if((match = url.match(/youtube\.com\/playlist\?list=([a-zA-Z0-9_-]{34})/)) || (match = url.match(/youtu\.be\/playlist\?list=([a-zA-Z0-9_-]{34})/))){ + //return youtube playlist id + return { + type: "ytp", + id: match[1] + } + } + //If we have a match to a dailymotion video if(match = url.match(/dailymotion\.com\/video\/([a-z0-9]{7})/)){ return { diff --git a/src/utils/media/ytdlpUtils.js b/src/utils/media/ytdlpUtils.js index af5805d..c487b86 100644 --- a/src/utils/media/ytdlpUtils.js +++ b/src/utils/media/ytdlpUtils.js @@ -32,7 +32,28 @@ const loggerUtils = require('../loggerUtils.js') module.exports.fetchYoutubeMetadata = async function(id, title){ try{ //Try to pull media from youtube id - const media = await fetchMetadata(`https://youtu.be/${id}`, title, 'yt'); + const media = await fetchVideoMetadata(`https://youtu.be/${id}`, title, 'yt'); + + //Return found media + return media; + //If something went wrong + }catch(err){ + //If our IP was banned by youtube + if(err.message.match("Sign in to confirm you’re not a bot.")){ + //Make our own error with blackjack and hookers + throw loggerUtils.exceptionSmith("The server's IP address has been banned by youtube. Please contact your server's administrator.", "queue"); + //Otherwise if we don't have a good way to handle it + }else{ + //toss it back up + throw err; + } + } +} + +module.exports.fetchYoutubePlaylistMetadata = async function(id, title){ + try{ + //Try to pull media from youtube id + const media = await fetchPlaylistMetadata(`https://youtu.be/playlist?list=${id}`, title, 'yt'); //Return found media return media; @@ -52,22 +73,19 @@ module.exports.fetchYoutubeMetadata = async function(id, title){ module.exports.fetchDailymotionMetadata = async function(id, title){ //Pull media from dailymotion link - const media = await fetchMetadata(`https://dailymotion.com/video/${id}`, title, 'dm'); + const media = await fetchVideoMetadata(`https://dailymotion.com/video/${id}`, title, 'dm'); //Return found media; return media; } -//Generic YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata -async function fetchMetadata(link, title, type, format = 'b'){ +//Generic single video YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata +async function fetchVideoMetadata(link, title, type, format = 'b'){ //Create media list const mediaList = []; - //Pull raw metadata - const rawMetadata = await ytdlp(link, { - dumpSingleJson: true, - format - }); + //Pull raw metadata from YT-DLP + const rawMetadata = await ytdlpFetch(link, format); //Pull data from rawMetadata, sanatizing title to prevent XSS const name = validator.escape(validator.trim(rawMetadata.title)); @@ -85,4 +103,17 @@ async function fetchMetadata(link, title, type, format = 'b'){ //Return list of media return mediaList; +} + +//YT-DLP takes forever to handle playlists, we'll handle this via piped in the future perhaps +/*async function fetchPlaylistMetadata(link, title, type, format = 'b'){ +}*/ + +//Wrapper function for YT-DLP NPM package with pre-set cli-flags +async function ytdlpFetch(link, format = 'b'){ + //return promise from ytdlp + return ytdlp(link, { + dumpSingleJson: true, + format + }); } \ No newline at end of file diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index b0390a3..60c9170 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -80,6 +80,11 @@ module.exports.settingsMap = function(){ optional: true, isBoolean: true, errorMessage: "Bad channel settings map." + }, + 'settingsMap.streamURL': { + optional: true, + isURL: true, + errorMessage: "Invalid Stream URL" } }) ); diff --git a/src/views/partial/channelSettings/settings.ejs b/src/views/partial/channelSettings/settings.ejs index b54b138..b2bd804 100644 --- a/src/views/partial/channelSettings/settings.ejs +++ b/src/views/partial/channelSettings/settings.ejs @@ -20,7 +20,14 @@ along with this program. If not, see . %> <% Object.keys(channel.settings).forEach((key) => { %> - class="channel-preference-list-item" type="checkbox" <% if(channel.settings[key]){ %> checked <% } %>> + <% switch(typeof channel.settings[key]){ + case "string": %> + class="channel-preference-list-item" value="<%- channel.settings[key] %>"> + <% break; + default: %> + class="channel-preference-list-item" type="checkbox" <% if(channel.settings[key]){ %> checked <% } %>> + <% break; + } %> <% }); %> diff --git a/www/js/channelSettings.js b/www/js/channelSettings.js index a98bd08..980b1cb 100644 --- a/www/js/channelSettings.js +++ b/www/js/channelSettings.js @@ -235,13 +235,28 @@ class prefrenceList{ } async submitUpdate(event){ + //Get key from event target const key = event.target.id.replace("channel-preference-",""); - const value = event.target.checked; + + //Pull value from event target + let value = event.target.value; + + //If this is a checkmark + if(event.target.type == "checkbox"){ + //Use the .checked property instead of .value + value = event.target.checked; + } + + //Create settings map const settingsMap = new Map([ [key, value] ]); - this.handleUpdate(await utils.ajax.setChannelSetting(this.channel, settingsMap), event.target, key); + //Send update and collect results + const update = await utils.ajax.setChannelSetting(this.channel, settingsMap); + + //Handle update from server + this.handleUpdate(update, event.target, key); } handleUpdate(data, target, key){ From c6de68b47474af5dbd93246355c9265e83fc3d8a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 11 May 2025 08:19:30 -0400 Subject: [PATCH 047/209] Started work on HLS livestreaming --- package.json | 1 + src/app/channel/media/queue.js | 164 +++++++++++++++++++++++++++------ src/server.js | 3 +- src/views/channel.ejs | 1 + www/js/channel/mediaHandler.js | 44 +++++++++ www/js/channel/player.js | 9 +- 6 files changed, 193 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index ecc3f6a..c3c1bc6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "express": "^4.18.2", "express-session": "^1.18.0", "express-validator": "^7.2.0", + "hls.js": "^1.6.2", "mongoose": "^8.4.3", "node-cron": "^3.0.3", "nodemailer": "^6.9.16", diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index af7cc67..6638f57 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -65,6 +65,7 @@ module.exports = class{ socket.on("clear", (data) => {this.deleteRange(socket, data)}); socket.on("move", (data) => {this.moveMedia(socket, data)}); socket.on("lock", () => {this.toggleLock(socket)}); + socket.on("goLive", (data) => {this.goLive(socket, data)}); } //--- USER FACING QUEUEING FUNCTIONS --- @@ -128,32 +129,16 @@ module.exports = class{ } async stopMedia(socket){ - //Get the current channel from the database - const chanDB = await channelModel.findOne({name: socket.chan}); + try{ + //Get the current channel from the database + const chanDB = await channelModel.findOne({name: socket.chan}); - //Permcheck to make sure the user can fuck w/ the queue - if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ - - //If we're not currently playing anything - if(this.nowPlaying == null){ - //If an originating socket was provided for this request - if(socket != null){ - //Yell at the user for being an asshole - loggerUtils.socketErrorHandler(socket, "No media playing!", "queue"); - } - - //Ignore it - return false; + //Permcheck to make sure the user can fuck w/ the queue + if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ + await this.stop(); } - - //Stop playing - const stoppedMedia = this.nowPlaying; - - //Get difference between current time and start time and set as early end - stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000; - - //End the media - this.end(); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } } @@ -249,7 +234,90 @@ module.exports = class{ } } + async goLive(socket, data){ + //Grab the channel from DB + const chanDB = await channelModel.findOne({name:this.channel.name}); + + let title = "Livestream"; + + if(data != null && data.title != null){ + //If the title is too long + if(!validator.isLength(data.title, {max:30})){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Title too long!", "validation"); + //and ignore it! + return; + } + + //Set title + title = validator.escape(validator.trim(data.title)); + + //If we've got no title + if(title == null || title == ''){ + title = "Livestream"; + } + } + + //If we couldn't find the channel + if(chanDB == null){ + //FUCK + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); + } + + //Kill schedule timers to prevent items from starting during the stream + await this.stopScheduleTimers(); + + //Syntatic sugar because I'm lazy :P + const streamURL = chanDB.settings.streamURL; + + //Pull filename from streamURL + let filename = streamURL.match(/^.+\..+\/(.+)$/); + + //If we're streaming from the root of the domain + if(filename == null){ + //Set filename to root + filename = '/'; + }else{ + //Otherwise, hand over the filename + filename = filename[1]; + } + + //Create queued media object from stream URL and set it to nowPlaying + this.nowPlaying = new queuedMedia( + title, + filename, + streamURL, + streamURL, + "livehls", + 0, + streamURL, + new Date().getTime() + ); + + //Broadcast new media object to users + this.sendMedia(); + } + //--- INTERNAL USE ONLY QUEUEING FUNCTIONS --- + async stopScheduleTimers(){ + //End any currently playing media media + await this.end(); + + //Clear sync timer + clearTimeout(this.syncTimer); + //Clear next timer + clearTimeout(this.nextTimer); + //Clear the pre-switch timer + clearTimeout(this.preSwitchTimer); + + //Null out the sync timer + this.syncTimer = null; + //Null out the next playing item timer + this.nextTimer = null; + //Null out the pre-switch timer + this.preSwitchTimer = null; + } + getStart(start){ //Pull current time const now = new Date().getTime(); @@ -421,8 +489,11 @@ module.exports = class{ //If we got a bad request if(media == null){ try{ - //DO everything ourselves since we don't have a fance end() function to do it - chanDB = await channelModel.findOne({name:this.channel.name}); + //If we wheren't handed a channel + if(chanDB == null){ + //DO everything ourselves since we don't have a fance end() function to do it + chanDB = await channelModel.findOne({name:this.channel.name}); + } //If we couldn't find the channel if(chanDB == null){ @@ -781,13 +852,18 @@ module.exports = class{ async end(quiet = false, noArchive = false, volatile = false, chanDB){ try{ + //If we're not playing anything + if(this.nowPlaying == null){ + //Silently ignore the request + return; + } + //Call off any existing sync timer clearTimeout(this.syncTimer); //Clear out the sync timer this.syncTimer = null; - //Keep a copy of whats playing for later when we need to clear the DB const wasPlaying = this.nowPlaying; @@ -842,6 +918,40 @@ module.exports = class{ } } + async stop(chanDB){ + //If we wheren't handed a channel + if(chanDB == null){ + //DO everything ourselves since we don't have a fance end() function to do it + chanDB = await channelModel.findOne({name:this.channel.name}); + } + + //If we wheren't handed a channel + if(chanDB == null){ + //Complain about the lack of a channel + throw loggerUtils.exceptionSmith(`Channel not found!`, "queue"); + } + + //If we're not currently playing anything + if(this.nowPlaying == null){ + //If an originating socket was provided for this request + if(socket != null){ + //Yell at the user for being an asshole + throw loggerUtils.exceptionSmith(`No media playing`, "queue"); + } + //Ignore it + return false; + } + + //Stop playing + const stoppedMedia = this.nowPlaying; + + //Get difference between current time and start time and set as early end + stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000; + + //End the media + this.end(); + } + getItemsBetweenEpochs(start, end){ //Create an empty array to hold found items const foundItems = []; diff --git a/src/server.js b/src/server.js index fc32a3d..f6dcd98 100644 --- a/src/server.js +++ b/src/server.js @@ -168,9 +168,10 @@ app.use('/tooltip', tooltipRouter); app.use('/api', apiRouter); //Static File Server -//Serve bootstrap icons +//Serve client-side libraries app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); +app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //Server public 'www' folder app.use(express.static(path.join(__dirname, '../www'))); diff --git a/src/views/channel.ejs b/src/views/channel.ejs index 2bd8cc9..d66fdc3 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -34,6 +34,7 @@ along with this program. If not, see . %> <%- include('partial/scripts', {user}); %> <%# 3rd party code %> + <%# 1st party code %> <%# admin gunk %> diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 6b03c30..4f64926 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -158,8 +158,13 @@ class rawFileBase extends mediaHandler{ buildPlayer(){ //Create player this.video = document.createElement('video'); + + //Enable controls + this.video.controls = true; + //Append it to page this.player.videoContainer.appendChild(this.video); + //Run derived method super.buildPlayer(); } @@ -488,4 +493,43 @@ class youtubeEmbedHandler extends mediaHandler{ this.iframe.style.pointerEvents = (lock ? "none" : ""); } } +} + +class hlsBase extends rawFileBase{ + constructor(client, player, media, type){ + //Call derived constructor + super(client, player, media, type); + + //Create property to hold HLS object + this.hls = null; + } + + buildPlayer(){ + //Call derived player + super.buildPlayer(); + + //Instantiate HLS object + this.hls = new Hls(); + + //Load HLS Stream + this.hls.loadSource(this.nowPlaying.url); + + //Attatch hls object to video element + this.hls.attachMedia(this.video); + + //Bind onMetadataLoad to MANIFEST_PARSED + this.hls.on(Hls.Events.MANIFEST_PARSED, this.onMetadataLoad.bind(this)); + } + + onMetadataLoad(){ + //Start the video + this.video.play(); + } +} + +class hlsLiveStreamHandler extends hlsBase{ + constructor(client, player, media){ + //Call derived constructor + super(client, player, media, "livehls"); + } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 55b8387..009013c 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -89,13 +89,20 @@ class player{ }else{ //If we have a youtube video and the official embedded iframe player is selected if(data.media.type == 'yt' && localStorage.getItem("ytPlayerType") == 'embed'){ + //Create a new yt handler for it this.mediaHandler = new youtubeEmbedHandler(this.client, this, data.media); + //Sync to time stamp + this.mediaHandler.sync(data.timestamp); + //If we have an HLS Livestream + }else if(data.media.type == "livehls"){ + //Create a new HLS Livestream Handler for it + this.mediaHandler = new hlsLiveStreamHandler(this.client, this, data.media); //Otherwise, if we have a raw-file compatible source }else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){ //Create a new raw file handler for it this.mediaHandler = new rawFileHandler(client, this, data.media); //Sync to time stamp - this.mediaHandler.sync(data.timestamp); + this.mediaHandler.sync(data.timestamp); }else{ this.mediaHandler = new nullHandler(client, this); } From dd00a11b92a0541e8d8fdd30befe1e5da1d3db14 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 12 May 2025 17:54:47 -0400 Subject: [PATCH 048/209] Started work on HLS Livestreaming. Created basic HLS Livestream Media Handler. --- src/app/channel/media/queue.js | 162 +++++++++++++++++++++------------ src/server.js | 8 +- src/views/channel.ejs | 2 +- www/css/theme/movie-night.css | 5 + www/js/channel/mediaHandler.js | 101 ++++++++++++++++++-- www/js/channel/player.js | 2 + 6 files changed, 208 insertions(+), 72 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 6638f57..0887723 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -235,67 +235,76 @@ module.exports = class{ } async goLive(socket, data){ - //Grab the channel from DB - const chanDB = await channelModel.findOne({name:this.channel.name}); + try{ + //Grab the channel from DB + const chanDB = await channelModel.findOne({name:this.channel.name}); - let title = "Livestream"; + let title = "Livestream"; - if(data != null && data.title != null){ - //If the title is too long - if(!validator.isLength(data.title, {max:30})){ - //Bitch, moan, complain... - loggerUtils.socketErrorHandler(socket, "Title too long!", "validation"); - //and ignore it! - return; + if(data != null && data.title != null){ + //If the title is too long + if(!validator.isLength(data.title, {max:30})){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Title too long!", "validation"); + //and ignore it! + return; + } + + //Set title + title = validator.escape(validator.trim(data.title)); + + //If we've got no title + if(title == null || title == ''){ + title = "Livestream"; + } } - //Set title - title = validator.escape(validator.trim(data.title)); - - //If we've got no title - if(title == null || title == ''){ - title = "Livestream"; + //If we couldn't find the channel + if(chanDB == null){ + //FUCK + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); } + + //Kill schedule timers to prevent items from starting during the stream + await this.stopScheduleTimers(); + + //Syntatic sugar because I'm lazy :P + const streamURL = chanDB.settings.streamURL; + + if(streamURL == ''){ + throw loggerUtils.exceptionSmith('This channel\'s HLS Livestream Source has not been set!', 'queue'); + } + + + //Pull filename from streamURL + let filename = streamURL.match(/^.+\..+\/(.+)$/); + + //If we're streaming from the root of the domain + if(filename == null){ + //Set filename to root + filename = '/'; + }else{ + //Otherwise, hand over the filename + filename = filename[1]; + } + + //Create queued media object from stream URL and set it to nowPlaying + this.nowPlaying = new queuedMedia( + title, + filename, + streamURL, + streamURL, + "livehls", + 0, + streamURL, + new Date().getTime() + ); + + //Broadcast new media object to users + this.sendMedia(); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } - - //If we couldn't find the channel - if(chanDB == null){ - //FUCK - throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); - } - - //Kill schedule timers to prevent items from starting during the stream - await this.stopScheduleTimers(); - - //Syntatic sugar because I'm lazy :P - const streamURL = chanDB.settings.streamURL; - - //Pull filename from streamURL - let filename = streamURL.match(/^.+\..+\/(.+)$/); - - //If we're streaming from the root of the domain - if(filename == null){ - //Set filename to root - filename = '/'; - }else{ - //Otherwise, hand over the filename - filename = filename[1]; - } - - //Create queued media object from stream URL and set it to nowPlaying - this.nowPlaying = new queuedMedia( - title, - filename, - streamURL, - streamURL, - "livehls", - 0, - streamURL, - new Date().getTime() - ); - - //Broadcast new media object to users - this.sendMedia(); } //--- INTERNAL USE ONLY QUEUEING FUNCTIONS --- @@ -553,8 +562,11 @@ module.exports = class{ //otherwise }else{ try{ - //DO everything ourselves since we don't have a fance end() function to do it - chanDB = await channelModel.findOne({name:this.channel.name}); + //If we wheren't handed a channel + if(chanDB == null){ + //DO everything ourselves since we don't have a fance end() function to do it + chanDB = await channelModel.findOne({name:this.channel.name}); + } //If we couldn't find the channel if(chanDB == null){ @@ -814,6 +826,9 @@ module.exports = class{ //Send play signal out to the channel this.sendMedia(); + //Kill existing sync timers to prevent kicking-off ghost timer loops + clearTimeout(this.syncTimer); + //Kick off the sync timer this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta); @@ -879,9 +894,19 @@ module.exports = class{ this.server.io.in(this.channel.name).emit('end', {}); } + //If we're ending an HLS Livestream + if(wasPlaying.type == "livehls"){ + //Redirect to the endLivestream function + return this.endLivestream(chanDB); + } + + //If we're not in volatile mode and we're not ending a livestream if(!volatile){ - //Now that everything is clean, we can take our time with the DB :P - chanDB = await channelModel.findOne({name:this.channel.name}); + //If we wheren't handed a channel + if(chanDB == null){ + //Now that everything is clean, we can take our time with the DB :P + chanDB = await channelModel.findOne({name:this.channel.name}); + } //If we couldn't find the channel if(chanDB == null){ @@ -898,12 +923,13 @@ module.exports = class{ //Take it out of the active schedule this.schedule.delete(wasPlaying.startTime); + //If archiving is enabled if(!noArchive){ //Add the item to the channel archive chanDB.media.archived.push(wasPlaying); } - //broadcast queue using unsaved archive + //broadcast queue using unsaved archive, run this before chanDB.save() for better responsiveness this.broadcastQueue(chanDB); //Save our changes to the DB @@ -918,6 +944,22 @@ module.exports = class{ } } + async endLivestream(chanDB){ + try{ + //Refresh next timer + this.refreshNextTimer(); + + //Broadcast Queue + this.broadcastQueue(); + //ACK + }catch(err){ + //Broadcast queue + this.broadcastQueue(); + //Handle the error + loggerUtils.localExceptionHandler(err); + } + } + async stop(chanDB){ //If we wheren't handed a channel if(chanDB == null){ diff --git a/src/server.js b/src/server.js index f6dcd98..f584830 100644 --- a/src/server.js +++ b/src/server.js @@ -160,8 +160,6 @@ app.use('/passwordReset', passwordResetRouter); app.use('/emailChange', emailChangeRouter); //Panel app.use('/panel', panelRouter); -//Popup -//app.use('/popup', popupRouter); //tooltip app.use('/tooltip', tooltipRouter); //Bot-Ready @@ -169,9 +167,9 @@ app.use('/api', apiRouter); //Static File Server //Serve client-side libraries -app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); -app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); -app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); +app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); //Icon set +app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); //Self-Hosted PoW-based Captcha +app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler //Server public 'www' folder app.use(express.static(path.join(__dirname, '../www'))); diff --git a/src/views/channel.ejs b/src/views/channel.ejs index d66fdc3..561702d 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -34,7 +34,7 @@ along with this program. If not, see . %> <%- include('partial/scripts', {user}); %> <%# 3rd party code %> - + <%# 1st party code %> <%# admin gunk %> diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 1f28d4e..6df7a64 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -172,6 +172,11 @@ textarea{ box-shadow: var(--danger-glow0-alt1); } +.critical-danger-text{ + color: var(--danger0-alt1); + text-shadow: var(--danger-glow0); +} + .danger-link, .danger-text{ color: var(--danger0); } diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 4f64926..aa272c5 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -141,6 +141,10 @@ class mediaHandler{ //reset self act flag this.selfAct = false; } + + onBuffer(){ + this.selfAct = true; + } } //Basic building blocks for anything that touches a

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_commandPreprocessor.js.html b/www/doc/app_channel_commandPreprocessor.js.html index c3fb194..173338b 100644 --- a/www/doc/app_channel_commandPreprocessor.js.html +++ b/www/doc/app_channel_commandPreprocessor.js.html @@ -54,7 +54,7 @@ const channelModel = require('../../schemas/channel/channelSchema'); /** * Class containing global server-side chat/command pre-processing logic */ -module.exports = class commandPreprocessor{ +class commandPreprocessor{ /** * Instantiates a commandPreprocessor object * @param {channelManager} server - Parent Server Object @@ -455,7 +455,9 @@ class commandProcessor{ //throw send flag return true; } -} +} + +module.exports = commandPreprocessor; @@ -465,13 +467,13 @@ class commandProcessor{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_connectedUser.js.html b/www/doc/app_channel_connectedUser.js.html index f670d7b..56a2999 100644 --- a/www/doc/app_channel_connectedUser.js.html +++ b/www/doc/app_channel_connectedUser.js.html @@ -53,7 +53,7 @@ const { userModel } = require('../../schemas/user/userSchema'); /** * Class representing a single user connected to a channel */ -module.exports = class{ +class connectedUser{ /** * Instantiates a connectedUser object * @param {Mongoose.Document} userDB - User document to re-hydrate user from @@ -316,7 +316,9 @@ module.exports = class{ this.channel.broadcastUserList(); this.sendClientMetadata(); } -} +} + +module.exports = connectedUser; @@ -326,13 +328,13 @@ module.exports = class{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_media.js.html b/www/doc/app_channel_media_media.js.html index 37a1490..7ac6996 100644 --- a/www/doc/app_channel_media_media.js.html +++ b/www/doc/app_channel_media_media.js.html @@ -45,7 +45,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/ /** * Object representing a piece of media */ -module.exports = class{ +class media{ /** * Creates a new media object from scraped information * @param {String} title - Chosen title of media @@ -65,7 +65,9 @@ module.exports = class{ this.duration = duration; this.rawLink = rawLink; } -} +} + +module.exports = media; @@ -75,13 +77,13 @@ module.exports = class{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_playlistHandler.js.html b/www/doc/app_channel_media_playlistHandler.js.html index 5773a86..12e23ab 100644 --- a/www/doc/app_channel_media_playlistHandler.js.html +++ b/www/doc/app_channel_media_playlistHandler.js.html @@ -55,7 +55,7 @@ const { userModel } = require('../../../schemas/user/userSchema'); /** * Class containing playlist management logic for a single channel */ -module.exports = class{ +class playlistHandler{ /** * Instantiates a new object to handle playlist management for a single channel * @param {channelManager} server - Parent server object @@ -1162,7 +1162,9 @@ module.exports = class{ return loggerUtils.socketExceptionHandler(socket, err); } } -} +} + +module.exports = playlistHandler; @@ -1172,13 +1174,13 @@ module.exports = class{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_queue.js.html b/www/doc/app_channel_media_queue.js.html index 6c98c13..d1c0f3c 100644 --- a/www/doc/app_channel_media_queue.js.html +++ b/www/doc/app_channel_media_queue.js.html @@ -54,7 +54,7 @@ const channelModel = require('../../../schemas/channel/channelSchema'); /** * Object represneting a single channel's media queue */ -module.exports = class{ +class queue{ /** * Instantiates a new media queue for a given channel * @param {channelManager} server - Parent server object @@ -1777,7 +1777,9 @@ module.exports = class{ loggerUtils.localExceptionHandler(err); } } -} +} + +module.exports = queue; @@ -1787,13 +1789,13 @@ module.exports = class{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_queuedMedia.js.html b/www/doc/app_channel_media_queuedMedia.js.html index 6add7c3..5593036 100644 --- a/www/doc/app_channel_media_queuedMedia.js.html +++ b/www/doc/app_channel_media_queuedMedia.js.html @@ -49,7 +49,7 @@ const media = require('./media'); * Class extending media which represents a queued piece of media * @extends media */ -module.exports = class extends media{ +class queuedMedia extends media{ /** * Creates a new queued media object * @param {Number} startTime - JS Epoch representing start time @@ -147,7 +147,9 @@ module.exports = class extends media{ return this.startTime + (this.earlyEnd * 1000); } } -} +} + +module.exports = queuedMedia; @@ -157,13 +159,13 @@ module.exports = class extends media{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_tokebot.js.html b/www/doc/app_channel_tokebot.js.html index cbb9662..c301d8d 100644 --- a/www/doc/app_channel_tokebot.js.html +++ b/www/doc/app_channel_tokebot.js.html @@ -51,15 +51,21 @@ const statSchema = require('../../schemas/statSchema'); /** * Class containing global server-side tokebot logic */ -module.exports = class tokebot{ +class tokebot{ /** * Instantiates a tokebot object * @param {channelManager} server - Parent Server Object * @param {chatHandler} chatHandler - Parent Chat Handler Object */ constructor(server, chatHandler){ - //Set parents + /** + * Parent channelManager object + */ this.server = server; + + /** + * Parent chatHandler object + */ this.chatHandler = chatHandler; //Set timeouts to null @@ -256,7 +262,8 @@ module.exports = class tokebot{ } } - + +module.exports = tokebot; @@ -266,13 +273,13 @@ module.exports = class tokebot{

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/channelManager.html b/www/doc/channelManager.html new file mode 100644 index 0000000..361136a --- /dev/null +++ b/www/doc/channelManager.html @@ -0,0 +1,2000 @@ + + + + + JSDoc: Class: channelManager + + + + + + + + + + +
+ +

Class: channelManager

+ + + + + + +
+ +
+ +

channelManager(io)

+ +
Class containing global server-side channel connection management logic
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new channelManager(io)

+ + + + + + +
+ Instantiates object containing global server-side channel conection management logic +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
io + + +Server + + + + Socket.io server instanced passed down from server.js
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) authSocket(socket) → {Mongoose.Document}

+ + + + + + +
+ Global server-side authorization logic for new connections to any channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - Authorized User Document upon success +
+ + + +
+
+ Type +
+
+ +Mongoose.Document + + +
+
+ + + + + + + + + + + + + +

(async) broadcastSiteEmotes()

+ + + + + + +
+ Broadcast global emote list +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

crawlConnections(user, cb)

+ + + + + + +
+ Iterates through connections by a given username, and runs them through a given callback function/method +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
user + + +String + + + + Username to crawl connections against
cb + + +function + + + + Callback function to run active connections of a given user against
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners(socket)

+ + + + + + +
+ Define Global Server-Side socket event listeners +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket to check
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) getActiveChan(socket) → {Object}

+ + + + + + +
+ Gets active channel from a given socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket to check
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Object containing users active channel name and channel document object +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

getConnectedChannels(socket)

+ + + + + + +
+ Pulls user information by socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket to check
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns related user info +
+ + + + + + + + + + + + + + + +

getConnections(user, cb)

+ + + + + + +
+ Iterates through connections by a given username, and runs them through a given callback function/method +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
user + + +String + + + + Username to crawl connections against
cb + + +function + + + + Callback function to run active connections of a given user against
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getSocketInfo(socket)

+ + + + + + +
+ Pulls user information by socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket to check
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns related user info +
+ + + + + + + + + + + + + + + +

(async) handleConnection(socket)

+ + + + + + +
+ Handles global server-side initialization for new connections to any channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleDisconnect(socket, reason)

+ + + + + + +
+ Global server-side logic for handling disconncted sockets +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket to check
reason + + +String + + + + Reason for disconnection
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

kickConnections(user, reason)

+ + + + + + +
+ Kicks a user from all channels by username +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
user + + +String + + + + Username to kick from the server
reason + + +String + + + + Reason for kick
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) validateSocket(socket) → {Boolean}

+ + + + + + +
+ Global server-side validation logic for new connections to any channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true on success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/chat.html b/www/doc/chat.html index 417f4d3..cff85f0 100644 --- a/www/doc/chat.html +++ b/www/doc/chat.html @@ -95,7 +95,7 @@ -connectedUser +connectedUser @@ -323,13 +323,13 @@

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/chatBuffer.html b/www/doc/chatBuffer.html index d5b0936..925cfd4 100644 --- a/www/doc/chatBuffer.html +++ b/www/doc/chatBuffer.html @@ -95,7 +95,7 @@ -channelManager +channelManager @@ -141,7 +141,7 @@ -activeChannel +activeChannel @@ -823,13 +823,13 @@ Left here since it seems like good form anywho, since this would be a private, o

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/chatHandler.html b/www/doc/chatHandler.html new file mode 100644 index 0000000..d8ebdf7 --- /dev/null +++ b/www/doc/chatHandler.html @@ -0,0 +1,3695 @@ + + + + + JSDoc: Class: chatHandler + + + + + + + + + + +
+ +

Class: chatHandler

+ + + + + + +
+ +
+ +

chatHandler(server)

+ +
Class containing global server-side chat relay logic
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new chatHandler(server)

+ + + + + + +
+ Instantiates a chatHandler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
server + + +channelManager + + + + Parent Server Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) addPersonalEmote(socket, data)

+ + + + + + +
+ Handles incoming client request to add a personal emote +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

clearChat(user, chan)

+ + + + + + +
+ Clears chat for a given channel, targets specified user or entire channel if none found/specified. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
user + + +String + + + + User chats to clear
chan + + +String + + + + Channel to broadcast message within
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners(socket)

+ + + + + + +
+ Defines global server-side chat relay event listeners +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deletePersonalEmote(socket, data)

+ + + + + + +
+ Handles incoming client request to delete a personal emote +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleChat(socket, data)

+ + + + + + +
+ Handles incoming chat messages from client connections +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayChannelAnnouncement(msg, links)

+ + + + + + +
+ Broadcasts announcement to a given channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +String + + + + Message Text Content
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayChat(user, flair, highLevel, msg, type, chan, links)

+ + + + + + +
+ Creates a new chatObject and relays the resulting message to the given channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
user + + +String + + + + + + Originating user
flair + + +String + + + + + + Flair ID to mark chat with
highLevel + + +Number + + + + + + High Level to mark chat with
msg + + +String + + + + + + Message Text Content
type + + +String + + + + + + chat + + Message Type, used for client-side chat post-processing.
chan + + +String + + + + + + Channel to broadcast message within
links + + +Array + + + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayChatObject(chan, chat)

+ + + + + + +
+ Relays an existing chat object to a channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chan + + +String + + + + Channel to broadcast message within
chat + + +chat + + + + Chat Object representing the message to broadcast to the given channel
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayGlobalChat(user, flair, highLevel, msg, type, links)

+ + + + + + +
+ Creates a new chatObject and relays the resulting message to the entire server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
user + + +String + + + + + + Originating user
flair + + +String + + + + + + Flair ID to mark chat with
highLevel + + +Number + + + + + + High Level to mark chat with
msg + + +String + + + + + + Message Text Content
type + + +String + + + + + + chat + + Message Type, used for client-side chat post-processing.
links + + +Array + + + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayGlobalChatObject(chat)

+ + + + + + +
+ Relays an existing chat object to the entire server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chat + + +chat + + + + Chat Object representing the message to broadcast throughout the server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayGlobalTokeWhisper(msg, links)

+ + + + + + +
+ Broadcasts toke whisper to the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +String + + + + Message Text Content
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

+ + + + + + +
+ Creates a new chatObject and relays the resulting message to the given socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're sending a message to (sounds menacing, huh?)
user + + +String + + + + Originating user
flair + + +String + + + + Flair ID to mark chat with
highLevel + + +Number + + + + High Level to mark chat with
msg + + +String + + + + Message Text Content
type + + +String + + + + Message Type, used for client-side chat post-processing.
chan + + +String + + + + Channel to broadcast message within
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayPrivateChatObject(socket, data)

+ + + + + + +
+ Handles incoming client request to delete a personal emote +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayServerAnnouncement(msg, links)

+ + + + + + +
+ Broadcasts announcement to the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +String + + + + Message Text Content
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayTokeCallout(msg, links)

+ + + + + + +
+ Broadcasts toke callout to the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +String + + + + Message Text Content
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayTokeWhisper(socket, msg, links)

+ + + + + + +
+ Broadcasts toke callout to the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're sending the whisper to
msg + + +String + + + + Message Text Content
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

relayUserChat(socket, msg, type, links)

+ + + + + + +
+ Relays a chat message from a user to the rest of the channel based on socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
msg + + +String + + + + Message Text Content
type + + +String + + + + Message Type, used for client-side chat post-processing.
links + + +Array + + + + Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) setFlair(socket, data)

+ + + + + + +
+ Handles incoming client request to change flair +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) setHighLevel(socket, data)

+ + + + + + +
+ Handles incoming client request to change high level +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/commandPreprocessor.html b/www/doc/commandPreprocessor.html new file mode 100644 index 0000000..dc93df3 --- /dev/null +++ b/www/doc/commandPreprocessor.html @@ -0,0 +1,1255 @@ + + + + + JSDoc: Class: commandPreprocessor + + + + + + + + + + +
+ +

Class: commandPreprocessor

+ + + + + + +
+ +
+ +

commandPreprocessor(server, chatHandler)

+ +
Class containing global server-side chat/command pre-processing logic
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new commandPreprocessor(server, chatHandler)

+ + + + + + +
+ Instantiates a commandPreprocessor object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
server + + +channelManager + + + + Parent Server Object
chatHandler + + +chatHandler + + + + Parent Chat Handler Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + + + + + + + + +
+ Iterates through links in message and marks them by link type for later use by client-side post-processing +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) prepMessage(commandObj)

+ + + + + + +
+ Re-creates message string from processed Command Array +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) preprocess(socket, data)

+ + + + + + +
+ Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the request from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) processServerCommand(commandObj)

+ + + + + + +
+ Uses the server's Command Processor object to process the chat/command request. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sanatizeCommand(commandObj) → {Boolean}

+ + + + + + +
+ Sanatizes and Validates a single user chat message/command +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ false if Command/Message is too long to send +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

sendChat(commandObj)

+ + + + + + +
+ Relays chat to channel via parent Chat Handler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

splitCommand(commandObj)

+ + + + + + +
+ Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders +These arrays are used to handle further command/chat processing +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/commandProcessor.html b/www/doc/commandProcessor.html index 251dce7..c983344 100644 --- a/www/doc/commandProcessor.html +++ b/www/doc/commandProcessor.html @@ -95,7 +95,7 @@ -channelManager +channelManager @@ -118,7 +118,7 @@ -chatHandler +chatHandler @@ -1825,13 +1825,13 @@

- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/connectedUser.html b/www/doc/connectedUser.html new file mode 100644 index 0000000..75681f1 --- /dev/null +++ b/www/doc/connectedUser.html @@ -0,0 +1,1888 @@ + + + + + JSDoc: Class: connectedUser + + + + + + + + + + +
+ +

Class: connectedUser

+ + + + + + +
+ +
+ +

connectedUser(userDB, chanRank, channel, socket)

+ +
Class representing a single user connected to a channel
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new connectedUser(userDB, chanRank, channel, socket)

+ + + + + + +
+ Instantiates a connectedUser object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userDB + + +Mongoose.Document + + + + User document to re-hydrate user from
chanRank + + +PemissionModel.chanRank + + + + Enum representing user channel rank
channel + + +String + + + + Channel the user is connecting to
socket + + +Socket + + + + Socket associated with the users connection
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

disconnect(reason, type)

+ + + + + + +
+ Disconnects all sockets for a given user +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
reason + + +String + + + + + + Reason for being disconnected
type + + +String + + + + + + Disconnected + + Disconnection Type
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

emit(eventName, data)

+ + + + + + +
+ Emits an event to all known sockets for a given user + +My brain keeps going back to using dynamic per-user namespaces for this +but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces +and using per-user channels for this, but what of gold or mod-only features? or games? +No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). +at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. +Having to crawl through these sockets is that. Because the other ways seem more gross somehow. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
eventName + + +String + + + + Event name to emit to client sockets
data + + +Object + + + + Data to emit to client sockets
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) handleConnection(userDB, chanDB, socket)

+ + + + + + +
+ Handles server-side initialization for new connections from a specific user +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) sendChanEmotes(chanDB)

+ + + + + + +
+ Send copy of channel emotes to the user +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) sendClientMetadata(userDB, chanDB)

+ + + + + + +
+ Sends glut of required initial metadata to the client upon a new connection +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) sendPersonalEmotes(userDB)

+ + + + + + +
+ Send copy of channel emotes to the user +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) sendSiteEmotes()

+ + + + + + +
+ Send copy of site emotes to the user +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) sendUsedTokes(userDB)

+ + + + + + +
+ Send copy of channel emotes to the user +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

socketCrawl(cb)

+ + + + + + +
+ Iterates through all known connections for a given user, running them through a supplied callback function +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + Callback to call against found sockets for a given user
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateFlair(flair)

+ + + + + + +
+ Set flair for a given user and broadcast update to clients +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
flair + + +String + + + + Flair string to update user's flair to
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateHighLevel(highLevel)

+ + + + + + +
+ Set high level for a given user and broadcast update to clients +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
highLevel + + +Number + + + + Number to update user's high-level to
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/global.html b/www/doc/global.html index 038459c..fcb66b1 100644 --- a/www/doc/global.html +++ b/www/doc/global.html @@ -7371,13 +7371,13 @@ Warns server admin against unsafe config options.
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/index.html b/www/doc/index.html index 4927765..fdf486a 100644 --- a/www/doc/index.html +++ b/www/doc/index.html @@ -50,13 +50,13 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/media.html b/www/doc/media.html new file mode 100644 index 0000000..9abb2cd --- /dev/null +++ b/www/doc/media.html @@ -0,0 +1,361 @@ + + + + + JSDoc: Class: media + + + + + + + + + + +
+ +

Class: media

+ + + + + + +
+ +
+ +

media(title, fileName, url, id, type, duration, rawLink)

+ +
Object representing a piece of media
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new media(title, fileName, url, id, type, duration, rawLink)

+ + + + + + +
+ Creates a new media object from scraped information +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Chosen title of media
fileName + + +String + + + + Original filename/title of media provided by source
url + + +String + + + + Original URL to file
id + + +String + + + + Video ID from source (IE: youtube watch code/archive.org file path)
type + + +String + + + + Original video source
duration + + +Number + + + + Length of media in seconds
rawLink + + +String + + + + URL to raw file copy of media, not applicable to all sources
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/module.exports.html b/www/doc/module.exports.html index 128de4a..0033064 100644 --- a/www/doc/module.exports.html +++ b/www/doc/module.exports.html @@ -10145,143 +10145,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -10579,143 +10442,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -10863,303 +10589,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -16726,165 +16155,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -17391,143 +16661,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -18644,144 +17777,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -30342,143 +29337,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -30776,143 +29634,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -31060,303 +29781,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -36923,165 +35347,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -37588,143 +35853,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -38841,144 +36969,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -50539,143 +48529,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -50973,143 +48826,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -51257,303 +48973,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -57120,165 +54539,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -57785,143 +55045,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -59038,20364 +56161,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - - - - - - - - - - - -
- -
- -

exports(server, chatHandler)

- -
Class containing global server-side chat/command pre-processing logic
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(server, chatHandler)

- - - - - - -
- Instantiates a commandPreprocessor object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -channelManager - - - - Parent Server Object
chatHandler - - -chatHandler - - - - Parent Chat Handler Object
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -91025,143 +67790,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -91459,143 +68087,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -91743,303 +68234,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -97606,165 +73800,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -98271,143 +74306,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -99524,144 +75422,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -111360,143 +87120,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -111794,143 +87417,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -112078,303 +87564,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -117941,165 +93130,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -118606,143 +93636,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -119859,144 +94752,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -131580,143 +106335,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -132014,143 +106632,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -132298,303 +106779,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -138161,165 +112345,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -138826,143 +112851,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -140079,144 +113967,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -151823,143 +125573,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -152257,143 +125870,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -152541,303 +126017,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -158404,165 +131583,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -159069,143 +132089,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -160322,144 +133205,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -172120,143 +144865,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -172554,143 +145162,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -172838,303 +145309,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -178701,165 +150875,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -179366,143 +151381,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -180619,144 +152497,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -192340,143 +164080,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - - - - - - - - -
- Iterates through links in message and marks them by link type for later use by client-side post-processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -192774,143 +164377,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) prepMessage(commandObj)

- - - - - - -
- Re-creates message string from processed Command Array -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -193058,303 +164524,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

(async) preprocess(socket, data)

- - - - - - -
- Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) processServerCommand(commandObj)

- - - - - - -
- Uses the server's Command Processor object to process the chat/command request. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -198921,165 +170090,6 @@ Having to crawl through these sockets is that. Because the other ways seem more - - - - - - -

sanatizeCommand(commandObj) → {Boolean}

- - - - - - -
- Sanatizes and Validates a single user chat message/command -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- false if Command/Message is too long to send -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - @@ -199586,143 +170596,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

sendChat(commandObj)

- - - - - - -
- Relays chat to channel via parent Chat Handler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -200839,144 +171712,6 @@ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-thi - - - - - - -

splitCommand(commandObj)

- - - - - - -
- Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders -These arrays are used to handle further command/chat processing -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -202439,13 +173174,13 @@ Called auto-magically by the Synchronization Timer
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:42 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:17:22 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/playlistHandler.html b/www/doc/playlistHandler.html new file mode 100644 index 0000000..971f6c8 --- /dev/null +++ b/www/doc/playlistHandler.html @@ -0,0 +1,5117 @@ + + + + + JSDoc: Class: playlistHandler + + + + + + + + + + +
+ +

Class: playlistHandler

+ + + + + + +
+ +
+ +

playlistHandler(server, channel)

+ +
Class containing playlist management logic for a single channel
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new playlistHandler(server, channel)

+ + + + + + +
+ Instantiates a new object to handle playlist management for a single channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
server + + +channelManager + + + + Parent server object
channel + + +activeChannel + + + + Parent Channel object for desired channel queue
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) addToChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Adds media to channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) addToPlaylistValidator(socket, URL) → {Array}

+ + + + + + +
+ Validates client requests to add media to a playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
URL + + +String + + + + URL String handed over from the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ List of media objects which where added +
+ + + +
+
+ Type +
+
+ +Array + + +
+
+ + + + + + + + + + + + + +

(async) addToUserPlaylist(socket, data, userDB)

+ + + + + + +
+ Adds media to user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Changes default titles for a given channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

+ + + + + + +
+ Changes default titles for a given user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

changeDefaultTitlesValidator(data) → {Array}

+ + + + + + +
+ Validates client requests to change default titles for a given playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Data handed over from the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Array of strings containing valid titles from the output +
+ + + +
+
+ Type +
+
+ +Array + + +
+
+ + + + + + + + + + + + + +

(async) createChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Creates a new channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

createPlaylistValidator(socket, data) → {Object}

+ + + + + + +
+ Validates client requests to create a playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
data + + +Object + + + + Data handed over from the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns validated titles +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

(async) createUserPlaylist(socket, data, userDB)

+ + + + + + +
+ Creates a new user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners(socket)

+ + + + + + +
+ Defines server-side socket.io listeners for newly connected sockets +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deleteChannelPlaylist(socket, data, userDB)

+ + + + + + +
+ Deletes a user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

+ + + + + + +
+ Deletes media from a given channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

deletePlaylistMediaValidator(socket, data)

+ + + + + + +
+ Validates client requests to rename the playlist validator +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
data + + +Object + + + + Data handed over from the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deleteUserPlaylist(socket, data, chanDB)

+ + + + + + +
+ Deletes a Channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deleteUserPlaylistMedia(socket, data, userDB)

+ + + + + + +
+ Deletes media from a given user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) getChannelPlaylists(socket, chanDB)

+ + + + + + +
+ Sends channel playlist data to a requesting socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) getUserPlaylists(socket, userDB)

+ + + + + + +
+ Sends user playlist data to a requesting socket +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
userDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) queueChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Queues an entire channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) queueFromChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Queues media from a given channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

queueFromChannelPlaylistValidator(socket, data) → {Number}

+ + + + + + +
+ Validates client requests to queue media from a playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
data + + +Object + + + + Data handed over from the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns validated start time on success +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

+ + + + + + +
+ Queues media from a given user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Queues random media from a given channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

+ + + + + + +
+ Queues random media from a given user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) queueUserPlaylist(socket, data, userDB, chanDB)

+ + + + + + +
+ Queues an entire user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) renameChannelPlaylist(socket, data, chanDB)

+ + + + + + +
+ Renames a channel playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
chanDB + + +Mongoose.Document + + + + Channel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

renameChannelPlaylistValidator(socket, data) → {String}

+ + + + + + +
+ Validates client requests to rename the playlist validator +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
data + + +Object + + + + Data handed over from the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns escaped/trimmed name upon success +
+ + + +
+
+ Type +
+
+ +String + + +
+
+ + + + + + + + + + + + + +

(async) renameUserPlaylist(socket, data, userDB)

+ + + + + + +
+ Renames a user playlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Data handed over from the client
userDB + + +Mongoose.Document + + + + User Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/queue.html b/www/doc/queue.html new file mode 100644 index 0000000..a61f0f1 --- /dev/null +++ b/www/doc/queue.html @@ -0,0 +1,5814 @@ + + + + + JSDoc: Class: queue + + + + + + + + + + +
+ +

Class: queue

+ + + + + + +
+ +
+ +

queue(server, chanDB, channel)

+ +
Object represneting a single channel's media queue
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new queue(server, chanDB, channel)

+ + + + + + +
+ Instantiates a new media queue for a given channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
server + + +channelManager + + + + Parent server object
chanDB + + +Document + + + + Related Channel Document from DB
channel + + +activeChannel + + + + Parent Channel object for desired channel queue
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) broadcastQueue(chanDB)

+ + + + + + +
+ Broadcasts channel queue +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanDB + + +Mongoose.Document + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners(socket)

+ + + + + + +
+ Defines server-side socket.io listeners for newly connected sockets +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Newly connected socket to define listeners against
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deleteMedia(socket, data)

+ + + + + + +
+ Processes client requests to delete queued media +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) deleteRange(socket, data)

+ + + + + + +
+ Processes request to delete a range of media items from the queue +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) end(quiet, noArchive, volatile, chanDB)

+ + + + + + +
+ End currently playing media +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
quiet + + +Boolean + + + + + + false + + Enable to prevent ending the media client-side
noArchive + + +Boolean + + + + + + false + + Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile + + +Boolean + + + + + + false + + Enable to prevent DB Transactions
chanDB + + +Mongoose.Document + + + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) endLivestream(wasPlaying, chanDB)

+ + + + + + +
+ Ends running Livestream +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
wasPlaying + + +queuedMedia + + + + Media object that was playing while we started the Livestream
chanDB + + +Mongoose.Document + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getItemAtEpoch(epoch) → {queuedMedia}

+ + + + + + +
+ Gets a media item by epoch +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
epoch + + +Number + + + + Date to check by JS Epoch (Millis)
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ found media item +
+ + + +
+
+ Type +
+
+ +queuedMedia + + +
+
+ + + + + + + + + + + + + +

getItemByUUID(uuid) → {queuedMedia}

+ + + + + + +
+ Get Scheduled Item by UUID +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
uuid + + +String + + + + UUID of item to reschedule
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ found item +
+ + + +
+
+ Type +
+
+ +queuedMedia + + +
+
+ + + + + + + + + + + + + +

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

+ + + + + + +
+ Returns scheduled media between two given datetimes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
start + + +Number + + + + + + Start date by JS Epoch (Millis)
end + + +Number + + + + + + End date by JS Epoch (Millis)
noUnfinished + + +Boolean + + + + + + false + + Enable to include currently playing media
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Found Media Objects +
+ + + +
+
+ Type +
+
+ +queuedMedia + + +
+
+ + + + + + + + + + + + + +

getLastItem(epoch)

+ + + + + + +
+ Gets last item from a given epoch +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
epoch + + +Number + + + + Date to check by JS Epoch (Millis), defaults to now
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Last played item +
+ + + + + + + + + + + + + + + +

getNextItem(epoch) → {queuedMedia}

+ + + + + + +
+ Gets next item from a given epoch +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
epoch + + +Number + + + + Date to check by JS Epoch (Millis), defaults to now
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Next item on the schedule +
+ + + +
+
+ Type +
+
+ +queuedMedia + + +
+
+ + + + + + + + + + + + + +

getStart(start)

+ + + + + + +
+ Validates start times, and replaces bad ones with 5ms in the future +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
start + + +Number + + + + Start time to validate by JS Epoch (millis)
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Start time as JS Epoch (millis) +
+ + + + + + + + + + + + + + + +

(async) goLive(socket, data)

+ + + + + + +
+ Handle client request to start an HLS live stream +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) handleRawRefresh(mediaObj) → {queuedMedia}

+ + + + + + +
+ Refreshes expired raw links before media plays +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
mediaObj + + +queuedMedia + + + + Media object that's about to play
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ passes through Media object with updated link upon success +
+ + + +
+
+ Type +
+
+ +queuedMedia + + +
+
+ + + + + + + + + + + + + +

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

+ + + + + + +
+ Overwrites livestream over scheduled media content after it has ended +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
wasPlaying + + +queuedMedia + + + + Media object that was playing while we started the Livestream
chanDB + + +Mongoose.Document + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

+ + + + + + +
+ Pushes back any missed content scheduled during Livestream after Livestream has ended. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
wasPlaying + + +queuedMedia + + + + Media object that was playing while we started the Livestream
chanDB + + +Mongoose.Document + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) moveMedia(socket, data)

+ + + + + + +
+ Processes request to move queued media item +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) preSwitch(mediaObj)

+ + + + + + +
+ Called 10 seconds before media begins to play +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
mediaObj + + +queuedMedia + + + + Media object that's about to play
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) prepQueue(chanDB)

+ + + + + + +
+ Prepares channel queue for network transmission +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanDB + + +Mongoose.Document + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ de-hydrated scehdule information +
+ + + + + + + + + + + + + + + +

(async) queueURL(socket, data)

+ + + + + + +
+ Accepts new URL's to queue from the client +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we're receiving the URL from
data + + +Object + + + + Event payload
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

refreshNextTimer(volatile)

+ + + + + + +
+ Calculates next item to play, and sets timer to play it at it's scheduled start +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
volatile + + +Boolean + + + + + + false + + Disables DB Transactions if true
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) rehydrateQueue(chanDB)

+ + + + + + +
+ Rehydrates media schedule from DB +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanDB + + +Mongoose.Document + + + + Pass through Channel Document to save on DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

+ + + + + + +
+ Removes a media item +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
uuid + + +String + + + + + + UUID of item to reschedule
socket + + +Socket + + + + + + Requesting Socket
chanDB + + +Mongoose.Document + + + + + + Channnel Document Passthrough to save on DB Access
noScheduling + + +Boolean + + + + + + false + + Disables schedule timer refresh if true
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Deleted Media Item +
+ + + +
+
+ Type +
+
+ +Media + + +
+
+ + + + + + + + + + + + + +

(async) removeRange(start, end, socket, noUnfinished)

+ + + + + + +
+ Removes range of media items from the queue +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
start + + +Number + + + + + + Start date by JS Epoch (millis)
end + + +Number + + + + + + End date by JS Epoch (millis)
socket + + +Socket + + + + + + Requesting Socket
noUnfinished + + +Boolean + + + + + + false + + Set to true to include items that may be currently playing
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) rescheduleMedia(uuid, start, socket, chanDB)

+ + + + + + +
+ Reschedules a media item +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
uuid + + +String + + + + UUID of item to reschedule
start + + +Number + + + + New start time by JS Epoch (Millis)
socket + + +Socket + + + + Requesting Socket
chanDB + + +Mongoose.Document + + + + Channnel Document Passthrough to save on DB Access
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

+ + + + + + +
+ Schedules a Media Item + +This is a fun method and I think it deserves it's own little explination... +Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option +I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. +Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... + +That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, +we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. + +Also a note on preformance: +While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, +simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, +since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. +Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. +This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider +that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... + + +Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. +This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) +Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P + +If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication + +Further Reading: +https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays +https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
media + + +Media + + + + + + Media item to schedule
socket + + +Socket + + + + + + Requesting Socket
chanDB + + +Mongoose.Document + + + + + + Channnel Document Passthrough to save on DB Access
force + + +Boolean + + + + + + false + + Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile + + +Boolean + + + + + + false + + Prevent DB Writes, used for internal function calls
startVolatile + + +Boolean + + + + + + false + + Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate + + +Boolean + + + + + + false + + Saves items even if they're about to, or have already started. Used for internal function calls
noSave + + +Boolean + + + + + + false + + Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sendMedia(socket)

+ + + + + + +
+ Send media update to a specific socket or broadcast it to the entire channel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) start(mediaObj, timestamp, volatile)

+ + + + + + +
+ Kicks off a media item +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
mediaObj + + +queuedMedia + + + + + + Media object that's about to play
timestamp + + +Number + + + + + + Media start timestamp in seconds
volatile + + +Boolean + + + + + + false + + Disables DB Transactions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

stop(socket)

+ + + + + + +
+ Stops currently playing media item +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting Socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns false if there is nothing to stop +
+ + + + + + + + + + + + + + + +

(async) stopMedia(socket)

+ + + + + + +
+ Processes requests to stop currently playing media from client +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket we received the request from
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) stopScheduleTimers(noArchive)

+ + + + + + +
+ Clears and scheduling timers +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
noArchive + + +Boolean + + + + + + true + + Disables Archiving
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync()

+ + + + + + +
+ Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta +Called auto-magically by the Synchronization Timer +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) toggleLock(socket)

+ + + + + + +
+ Handle client request to (un)lock queue +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Requesting socket
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/queuedMedia.html b/www/doc/queuedMedia.html new file mode 100644 index 0000000..d57db67 --- /dev/null +++ b/www/doc/queuedMedia.html @@ -0,0 +1,945 @@ + + + + + JSDoc: Class: queuedMedia + + + + + + + + + + +
+ +

Class: queuedMedia

+ + + + + + +
+ +
+ +

queuedMedia(startTime, startTimeStamp, earlyEnd, uuid)

+ +
Class extending media which represents a queued piece of media
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new queuedMedia(startTime, startTimeStamp, earlyEnd, uuid)

+ + + + + + +
+ Creates a new queued media object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
startTime + + +Number + + + + + + JS Epoch representing start time
startTimeStamp + + +Number + + + + + + 0 + + Media start time stamp in seconds (relative to duration)
earlyEnd + + +Number + + + + + + Media end timestamp in seconds (relative to duration)
uuid + + +String + + + + + + Media object's unique identifier
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

genUUID()

+ + + + + + +
+ Generates new unique identifier for queued media +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getEndTime(fullTime)

+ + + + + + +
+ return the end time of a given queuedMedia object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
fullTime + + +boolean + + + + + + false + + Overrides early ends
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ end time of given queuedMedia object +
+ + + + + + + + + + + + + + + +

(static) fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

+ + + + + + +
+ Creates a queuedMedia object from a media object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +media + + + + Media object to queue
startTime + + +Number + + + + Start time formatted as a JS Epoch
startTimeStamp + + +Number + + + + Start time stamp in seconds
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ queuedMedia object created from given media object +
+ + + +
+
+ Type +
+
+ +queuedMedia + + +
+
+ + + + + + + + + + + + + +

(static) fromMediaArray(mediaList, start)

+ + + + + + +
+ Converts array of media objects into array of queuedMedia objects +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
mediaList + + +Array + + + + Array of media objects to queue
start + + +Number + + + + Start time formatted as JS Epoch
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Array of converted queued media objects +
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/schemas_channel_channelBanSchema.js.html b/www/doc/schemas_channel_channelBanSchema.js.html index 4aaa763..a676c99 100644 --- a/www/doc/schemas_channel_channelBanSchema.js.html +++ b/www/doc/schemas_channel_channelBanSchema.js.html @@ -95,13 +95,13 @@ module.exports = channelBanSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_channelPermissionSchema.js.html b/www/doc/schemas_channel_channelPermissionSchema.js.html index 234d334..d563aa5 100644 --- a/www/doc/schemas_channel_channelPermissionSchema.js.html +++ b/www/doc/schemas_channel_channelPermissionSchema.js.html @@ -163,13 +163,13 @@ module.exports = channelPermissionSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_channelSchema.js.html b/www/doc/schemas_channel_channelSchema.js.html index 32ef99f..fb7b92f 100644 --- a/www/doc/schemas_channel_channelSchema.js.html +++ b/www/doc/schemas_channel_channelSchema.js.html @@ -928,13 +928,13 @@ module.exports = mongoose.model("channel", channelSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_chatSchema.js.html b/www/doc/schemas_channel_chatSchema.js.html index 218f637..33d4e2d 100644 --- a/www/doc/schemas_channel_chatSchema.js.html +++ b/www/doc/schemas_channel_chatSchema.js.html @@ -90,13 +90,13 @@ module.exports = chatSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_mediaSchema.js.html b/www/doc/schemas_channel_media_mediaSchema.js.html index 8f31ede..7db4172 100644 --- a/www/doc/schemas_channel_media_mediaSchema.js.html +++ b/www/doc/schemas_channel_media_mediaSchema.js.html @@ -90,13 +90,13 @@ module.exports = mediaSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_playlistMediaSchema.js.html b/www/doc/schemas_channel_media_playlistMediaSchema.js.html index 9d43cd0..8523ae3 100644 --- a/www/doc/schemas_channel_media_playlistMediaSchema.js.html +++ b/www/doc/schemas_channel_media_playlistMediaSchema.js.html @@ -118,13 +118,13 @@ module.exports = mediaSchema.discriminator('saved', playlistMediaProperties);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_playlistSchema.js.html b/www/doc/schemas_channel_media_playlistSchema.js.html index 1db3493..601a46c 100644 --- a/www/doc/schemas_channel_media_playlistSchema.js.html +++ b/www/doc/schemas_channel_media_playlistSchema.js.html @@ -168,13 +168,13 @@ module.exports = playlistSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_queuedMediaSchema.js.html b/www/doc/schemas_channel_media_queuedMediaSchema.js.html index 2395723..a384c5e 100644 --- a/www/doc/schemas_channel_media_queuedMediaSchema.js.html +++ b/www/doc/schemas_channel_media_queuedMediaSchema.js.html @@ -107,13 +107,13 @@ module.exports = mediaSchema.discriminator('queued', queuedProperties);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_emoteSchema.js.html b/www/doc/schemas_emoteSchema.js.html index 6860191..aa89855 100644 --- a/www/doc/schemas_emoteSchema.js.html +++ b/www/doc/schemas_emoteSchema.js.html @@ -158,13 +158,13 @@ module.exports = mongoose.model("emote", emoteSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_flairSchema.js.html b/www/doc/schemas_flairSchema.js.html index 4a4c8c3..8443064 100644 --- a/www/doc/schemas_flairSchema.js.html +++ b/www/doc/schemas_flairSchema.js.html @@ -112,13 +112,13 @@ module.exports = mongoose.model("flair", flairSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_permissionSchema.js.html b/www/doc/schemas_permissionSchema.js.html index 8d3f904..5dfd02a 100644 --- a/www/doc/schemas_permissionSchema.js.html +++ b/www/doc/schemas_permissionSchema.js.html @@ -350,13 +350,13 @@ module.exports = mongoose.model("permissions", permissionSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_statSchema.js.html b/www/doc/schemas_statSchema.js.html index 010df6d..1d420a8 100644 --- a/www/doc/schemas_statSchema.js.html +++ b/www/doc/schemas_statSchema.js.html @@ -234,13 +234,13 @@ module.exports = mongoose.model("statistics", statSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_tokebot_tokeCommandSchema.js.html b/www/doc/schemas_tokebot_tokeCommandSchema.js.html index fc81b54..edfb17c 100644 --- a/www/doc/schemas_tokebot_tokeCommandSchema.js.html +++ b/www/doc/schemas_tokebot_tokeCommandSchema.js.html @@ -154,13 +154,13 @@ module.exports = mongoose.model("tokeCommand", tokeCommandSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_emailChangeSchema.js.html b/www/doc/schemas_user_emailChangeSchema.js.html index 0a1f2c8..edb5d51 100644 --- a/www/doc/schemas_user_emailChangeSchema.js.html +++ b/www/doc/schemas_user_emailChangeSchema.js.html @@ -216,13 +216,13 @@ module.exports = mongoose.model("emailChange", emailChangeSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_passwordResetSchema.js.html b/www/doc/schemas_user_passwordResetSchema.js.html index 1a3fff1..03a6c5d 100644 --- a/www/doc/schemas_user_passwordResetSchema.js.html +++ b/www/doc/schemas_user_passwordResetSchema.js.html @@ -192,13 +192,13 @@ module.exports = mongoose.model("passwordReset", passwordResetSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_userBanSchema.js.html b/www/doc/schemas_user_userBanSchema.js.html index 3db347e..3d4ae40 100644 --- a/www/doc/schemas_user_userBanSchema.js.html +++ b/www/doc/schemas_user_userBanSchema.js.html @@ -515,13 +515,13 @@ module.exports = mongoose.model("userBan", userBanSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_userSchema.js.html b/www/doc/schemas_user_userSchema.js.html index e5c04b0..08d08b8 100644 --- a/www/doc/schemas_user_userSchema.js.html +++ b/www/doc/schemas_user_userSchema.js.html @@ -882,13 +882,13 @@ module.exports.userModel = mongoose.model("user", userSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/tokebot.html b/www/doc/tokebot.html new file mode 100644 index 0000000..3df574f --- /dev/null +++ b/www/doc/tokebot.html @@ -0,0 +1,978 @@ + + + + + JSDoc: Class: tokebot + + + + + + + + + + +
+ +

Class: tokebot

+ + + + + + +
+ +
+ +

tokebot(server, chatHandler)

+ +
Class containing global server-side tokebot logic
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new tokebot(server, chatHandler)

+ + + + + + +
+ Instantiates a tokebot object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
server + + +channelManager + + + + Parent Server Object
chatHandler + + +chatHandler + + + + Parent Chat Handler Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

chatHandler

+ + + + +
+ Parent chatHandler object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

server

+ + + + +
+ Parent channelManager object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(async) asyncFinisher()

+ + + + + + +
+ This method seems to be a vestage from a bygone era. We should remove it after documenting shit. +I would now, but I don't want to break shit in a comment-only commit. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

cooldown()

+ + + + + + +
+ Runs every second for 60 seconds after a toke +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

countdown()

+ + + + + + +
+ Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) refreshCommands()

+ + + + + + +
+ Reloads toke commands from DB into RAM-based toke command store +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

resetToke()

+ + + + + + +
+ Resets toke cooldowns early upon authorized request +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

tokeProcessor(commandObj) → {Boolean}

+ + + + + + +
+ Processes toke commands from Command Pre-Processor +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
commandObj + + +Object + + + + Object representing a single given command/chat request, passed down from the Command Pre-Processor
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/utils_altchaUtils.js.html b/www/doc/utils_altchaUtils.js.html index 827d344..90b44bc 100644 --- a/www/doc/utils_altchaUtils.js.html +++ b/www/doc/utils_altchaUtils.js.html @@ -112,13 +112,13 @@ module.exports.verify = async function(payload, uniqueSecret = ''){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_configCheck.js.html b/www/doc/utils_configCheck.js.html index 93fea81..81cc912 100644 --- a/www/doc/utils_configCheck.js.html +++ b/www/doc/utils_configCheck.js.html @@ -102,13 +102,13 @@ module.exports.securityCheck = function(){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_hashUtils.js.html b/www/doc/utils_hashUtils.js.html index 21dcb02..cc2e43e 100644 --- a/www/doc/utils_hashUtils.js.html +++ b/www/doc/utils_hashUtils.js.html @@ -97,13 +97,13 @@ module.exports.hashIP = function(ip){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_linkUtils.js.html b/www/doc/utils_linkUtils.js.html index baff084..d1c98ac 100644 --- a/www/doc/utils_linkUtils.js.html +++ b/www/doc/utils_linkUtils.js.html @@ -140,13 +140,13 @@ module.exports.markLink = async function(link){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_loggerUtils.js.html b/www/doc/utils_loggerUtils.js.html index 9c85192..3b30489 100644 --- a/www/doc/utils_loggerUtils.js.html +++ b/www/doc/utils_loggerUtils.js.html @@ -201,13 +201,13 @@ module.exports.errorMiddleware = function(err, req, res, next){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_mailUtils.js.html b/www/doc/utils_mailUtils.js.html index 38d831e..3310c7d 100644 --- a/www/doc/utils_mailUtils.js.html +++ b/www/doc/utils_mailUtils.js.html @@ -134,13 +134,13 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_media_internetArchiveUtils.js.html b/www/doc/utils_media_internetArchiveUtils.js.html index be4210f..6e8c8ff 100644 --- a/www/doc/utils_media_internetArchiveUtils.js.html +++ b/www/doc/utils_media_internetArchiveUtils.js.html @@ -148,13 +148,13 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_media_yanker.js.html b/www/doc/utils_media_yanker.js.html index e070b5d..825203f 100644 --- a/www/doc/utils_media_yanker.js.html +++ b/www/doc/utils_media_yanker.js.html @@ -187,13 +187,13 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_media_ytdlpUtils.js.html b/www/doc/utils_media_ytdlpUtils.js.html index 460ede2..9814973 100644 --- a/www/doc/utils_media_ytdlpUtils.js.html +++ b/www/doc/utils_media_ytdlpUtils.js.html @@ -180,13 +180,13 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_regexUtils.js.html b/www/doc/utils_regexUtils.js.html index 53407f9..505d037 100644 --- a/www/doc/utils_regexUtils.js.html +++ b/www/doc/utils_regexUtils.js.html @@ -63,13 +63,13 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_scheduler.js.html b/www/doc/utils_scheduler.js.html index ad2f750..90c8742 100644 --- a/www/doc/utils_scheduler.js.html +++ b/www/doc/utils_scheduler.js.html @@ -99,13 +99,13 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_sessionUtils.js.html b/www/doc/utils_sessionUtils.js.html index 3b73c64..55a03eb 100644 --- a/www/doc/utils_sessionUtils.js.html +++ b/www/doc/utils_sessionUtils.js.html @@ -230,13 +230,13 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:08:41 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time)
From 5ad20f6823be2a76d4a564a010472531ad364076 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 2 Sep 2025 07:46:46 -0400 Subject: [PATCH 077/209] Created seperate commands in build script to build client and server documentation seperately. --- package.json | 2 +- www/doc/module.exports.html | 173189 --------------- www/doc/{ => server}/activeChannel.html | 2 +- .../app_channel_activeChannel.js.html | 2 +- .../app_channel_channelManager.js.html | 2 +- www/doc/{ => server}/app_channel_chat.js.html | 2 +- .../app_channel_chatBuffer.js.html | 2 +- .../app_channel_chatHandler.js.html | 2 +- .../app_channel_commandPreprocessor.js.html | 2 +- .../app_channel_connectedUser.js.html | 2 +- .../app_channel_media_media.js.html | 2 +- .../app_channel_media_playlistHandler.js.html | 2 +- .../app_channel_media_queue.js.html | 2 +- .../app_channel_media_queuedMedia.js.html | 2 +- .../{ => server}/app_channel_tokebot.js.html | 10 +- www/doc/{ => server}/channelManager.html | 2 +- www/doc/{ => server}/chat.html | 2 +- www/doc/{ => server}/chatBuffer.html | 2 +- www/doc/{ => server}/chatHandler.html | 2 +- www/doc/{ => server}/commandPreprocessor.html | 2 +- www/doc/{ => server}/commandProcessor.html | 2 +- www/doc/{ => server}/connectedUser.html | 2 +- .../fonts/OpenSans-Bold-webfont.eot | Bin .../fonts/OpenSans-Bold-webfont.svg | 0 .../fonts/OpenSans-Bold-webfont.woff | Bin .../fonts/OpenSans-BoldItalic-webfont.eot | Bin .../fonts/OpenSans-BoldItalic-webfont.svg | 0 .../fonts/OpenSans-BoldItalic-webfont.woff | Bin .../fonts/OpenSans-Italic-webfont.eot | Bin .../fonts/OpenSans-Italic-webfont.svg | 0 .../fonts/OpenSans-Italic-webfont.woff | Bin .../fonts/OpenSans-Light-webfont.eot | Bin .../fonts/OpenSans-Light-webfont.svg | 0 .../fonts/OpenSans-Light-webfont.woff | Bin .../fonts/OpenSans-LightItalic-webfont.eot | Bin .../fonts/OpenSans-LightItalic-webfont.svg | 0 .../fonts/OpenSans-LightItalic-webfont.woff | Bin .../fonts/OpenSans-Regular-webfont.eot | Bin .../fonts/OpenSans-Regular-webfont.svg | 0 .../fonts/OpenSans-Regular-webfont.woff | Bin www/doc/{ => server}/global.html | 2 +- www/doc/{ => server}/index.html | 33 +- www/doc/{ => server}/media.html | 2 +- www/doc/{ => server}/playlistHandler.html | 2 +- www/doc/{ => server}/queue.html | 2 +- www/doc/{ => server}/queuedMedia.html | 2 +- .../schemas_channel_channelBanSchema.js.html | 2 +- ...as_channel_channelPermissionSchema.js.html | 2 +- .../schemas_channel_channelSchema.js.html | 2 +- .../schemas_channel_chatSchema.js.html | 2 +- .../schemas_channel_media_mediaSchema.js.html | 2 +- ..._channel_media_playlistMediaSchema.js.html | 2 +- ...hemas_channel_media_playlistSchema.js.html | 2 +- ...as_channel_media_queuedMediaSchema.js.html | 2 +- .../{ => server}/schemas_emoteSchema.js.html | 2 +- .../{ => server}/schemas_flairSchema.js.html | 2 +- .../schemas_permissionSchema.js.html | 2 +- .../{ => server}/schemas_statSchema.js.html | 2 +- .../schemas_tokebot_tokeCommandSchema.js.html | 2 +- .../schemas_user_emailChangeSchema.js.html | 2 +- .../schemas_user_passwordResetSchema.js.html | 2 +- .../schemas_user_userBanSchema.js.html | 2 +- .../schemas_user_userSchema.js.html | 2 +- www/doc/{ => server}/scripts/linenumber.js | 0 .../scripts/prettify/Apache-License-2.0.txt | 0 .../{ => server}/scripts/prettify/lang-css.js | 0 .../{ => server}/scripts/prettify/prettify.js | 0 www/doc/{ => server}/styles/jsdoc-default.css | 0 .../{ => server}/styles/prettify-jsdoc.css | 0 .../{ => server}/styles/prettify-tomorrow.css | 0 www/doc/{ => server}/tokebot.html | 142 +- .../{ => server}/utils_altchaUtils.js.html | 2 +- .../{ => server}/utils_configCheck.js.html | 2 +- www/doc/{ => server}/utils_hashUtils.js.html | 2 +- www/doc/{ => server}/utils_linkUtils.js.html | 2 +- .../{ => server}/utils_loggerUtils.js.html | 2 +- www/doc/{ => server}/utils_mailUtils.js.html | 2 +- .../utils_media_internetArchiveUtils.js.html | 2 +- .../{ => server}/utils_media_yanker.js.html | 2 +- .../utils_media_ytdlpUtils.js.html | 2 +- www/doc/{ => server}/utils_regexUtils.js.html | 2 +- www/doc/{ => server}/utils_scheduler.js.html | 2 +- .../{ => server}/utils_sessionUtils.js.html | 2 +- 83 files changed, 95 insertions(+), 173387 deletions(-) delete mode 100644 www/doc/module.exports.html rename www/doc/{ => server}/activeChannel.html (99%) rename www/doc/{ => server}/app_channel_activeChannel.js.html (99%) rename www/doc/{ => server}/app_channel_channelManager.js.html (99%) rename www/doc/{ => server}/app_channel_chat.js.html (99%) rename www/doc/{ => server}/app_channel_chatBuffer.js.html (99%) rename www/doc/{ => server}/app_channel_chatHandler.js.html (99%) rename www/doc/{ => server}/app_channel_commandPreprocessor.js.html (99%) rename www/doc/{ => server}/app_channel_connectedUser.js.html (99%) rename www/doc/{ => server}/app_channel_media_media.js.html (99%) rename www/doc/{ => server}/app_channel_media_playlistHandler.js.html (99%) rename www/doc/{ => server}/app_channel_media_queue.js.html (99%) rename www/doc/{ => server}/app_channel_media_queuedMedia.js.html (99%) rename www/doc/{ => server}/app_channel_tokebot.js.html (98%) rename www/doc/{ => server}/channelManager.html (99%) rename www/doc/{ => server}/chat.html (99%) rename www/doc/{ => server}/chatBuffer.html (99%) rename www/doc/{ => server}/chatHandler.html (99%) rename www/doc/{ => server}/commandPreprocessor.html (99%) rename www/doc/{ => server}/commandProcessor.html (99%) rename www/doc/{ => server}/connectedUser.html (99%) rename www/doc/{ => server}/fonts/OpenSans-Bold-webfont.eot (100%) rename www/doc/{ => server}/fonts/OpenSans-Bold-webfont.svg (100%) rename www/doc/{ => server}/fonts/OpenSans-Bold-webfont.woff (100%) rename www/doc/{ => server}/fonts/OpenSans-BoldItalic-webfont.eot (100%) rename www/doc/{ => server}/fonts/OpenSans-BoldItalic-webfont.svg (100%) rename www/doc/{ => server}/fonts/OpenSans-BoldItalic-webfont.woff (100%) rename www/doc/{ => server}/fonts/OpenSans-Italic-webfont.eot (100%) rename www/doc/{ => server}/fonts/OpenSans-Italic-webfont.svg (100%) rename www/doc/{ => server}/fonts/OpenSans-Italic-webfont.woff (100%) rename www/doc/{ => server}/fonts/OpenSans-Light-webfont.eot (100%) rename www/doc/{ => server}/fonts/OpenSans-Light-webfont.svg (100%) rename www/doc/{ => server}/fonts/OpenSans-Light-webfont.woff (100%) rename www/doc/{ => server}/fonts/OpenSans-LightItalic-webfont.eot (100%) rename www/doc/{ => server}/fonts/OpenSans-LightItalic-webfont.svg (100%) rename www/doc/{ => server}/fonts/OpenSans-LightItalic-webfont.woff (100%) rename www/doc/{ => server}/fonts/OpenSans-Regular-webfont.eot (100%) rename www/doc/{ => server}/fonts/OpenSans-Regular-webfont.svg (100%) rename www/doc/{ => server}/fonts/OpenSans-Regular-webfont.woff (100%) rename www/doc/{ => server}/global.html (99%) rename www/doc/{ => server}/index.html (67%) rename www/doc/{ => server}/media.html (99%) rename www/doc/{ => server}/playlistHandler.html (99%) rename www/doc/{ => server}/queue.html (99%) rename www/doc/{ => server}/queuedMedia.html (99%) rename www/doc/{ => server}/schemas_channel_channelBanSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_channelPermissionSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_channelSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_chatSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_media_mediaSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_media_playlistMediaSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_media_playlistSchema.js.html (99%) rename www/doc/{ => server}/schemas_channel_media_queuedMediaSchema.js.html (99%) rename www/doc/{ => server}/schemas_emoteSchema.js.html (99%) rename www/doc/{ => server}/schemas_flairSchema.js.html (99%) rename www/doc/{ => server}/schemas_permissionSchema.js.html (99%) rename www/doc/{ => server}/schemas_statSchema.js.html (99%) rename www/doc/{ => server}/schemas_tokebot_tokeCommandSchema.js.html (99%) rename www/doc/{ => server}/schemas_user_emailChangeSchema.js.html (99%) rename www/doc/{ => server}/schemas_user_passwordResetSchema.js.html (99%) rename www/doc/{ => server}/schemas_user_userBanSchema.js.html (99%) rename www/doc/{ => server}/schemas_user_userSchema.js.html (99%) rename www/doc/{ => server}/scripts/linenumber.js (100%) rename www/doc/{ => server}/scripts/prettify/Apache-License-2.0.txt (100%) rename www/doc/{ => server}/scripts/prettify/lang-css.js (100%) rename www/doc/{ => server}/scripts/prettify/prettify.js (100%) rename www/doc/{ => server}/styles/jsdoc-default.css (100%) rename www/doc/{ => server}/styles/prettify-jsdoc.css (100%) rename www/doc/{ => server}/styles/prettify-tomorrow.css (100%) rename www/doc/{ => server}/tokebot.html (88%) rename www/doc/{ => server}/utils_altchaUtils.js.html (99%) rename www/doc/{ => server}/utils_configCheck.js.html (99%) rename www/doc/{ => server}/utils_hashUtils.js.html (99%) rename www/doc/{ => server}/utils_linkUtils.js.html (99%) rename www/doc/{ => server}/utils_loggerUtils.js.html (99%) rename www/doc/{ => server}/utils_mailUtils.js.html (99%) rename www/doc/{ => server}/utils_media_internetArchiveUtils.js.html (99%) rename www/doc/{ => server}/utils_media_yanker.js.html (99%) rename www/doc/{ => server}/utils_media_ytdlpUtils.js.html (99%) rename www/doc/{ => server}/utils_regexUtils.js.html (99%) rename www/doc/{ => server}/utils_scheduler.js.html (99%) rename www/doc/{ => server}/utils_sessionUtils.js.html (99%) diff --git a/package.json b/package.json index cb166b1..5c31afb 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "start": "node ./src/server.js", "start:dev": "nodemon ./src/server.js", - "build": "node node_modules/jsdoc/jsdoc.js --recurse src/ --destination www/doc/" + "build": "node node_modules/jsdoc/jsdoc.js --verbose -r src/ -R README.md -d www/doc/server/ && node node_modules/jsdoc/jsdoc.js --verbose -r www/js/ -r README.md -d www/doc/client/" }, "devDependencies": { "nodemon": "^3.1.10", diff --git a/www/doc/module.exports.html b/www/doc/module.exports.html deleted file mode 100644 index 0033064..0000000 --- a/www/doc/module.exports.html +++ /dev/null @@ -1,173189 +0,0 @@ - - - - - JSDoc: Class: exports - - - - - - - - - - -
- -

Class: exports

- - - - - - -
- -
- -

exports(server, chanDB)

- -
Class representing a single active channel
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(server, chanDB)

- - - - - - -
- Instantiates an activeChannel object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -channelManager - - - - Parent Server Object
chanDB - - -Mongoose.Document - - - - chanDB to rehydrate buffer from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(io)

- -
Class containing global server-side channel connection management logic
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(io)

- - - - - - -
- Instantiates object containing global server-side channel conection management logic -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
io - - -Server - - - - Socket.io server instanced passed down from server.js
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(server)

- -
Class containing global server-side chat relay logic
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(server)

- - - - - - -
- Instantiates a chatHandler object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -channelManager - - - - Parent Server Object
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(userDB, chanRank, channel, socket)

- -
Class representing a single user connected to a channel
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(userDB, chanRank, channel, socket)

- - - - - - -
- Instantiates a connectedUser object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User document to re-hydrate user from
chanRank - - -PemissionModel.chanRank - - - - Enum representing user channel rank
channel - - -String - - - - Channel the user is connecting to
socket - - -Socket - - - - Socket associated with the users connection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(title, fileName, url, id, type, duration, rawLink)

- -
Object representing a piece of media
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(title, fileName, url, id, type, duration, rawLink)

- - - - - - -
- Creates a new media object from scraped information -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
title - - -String - - - - Chosen title of media
fileName - - -String - - - - Original filename/title of media provided by source
url - - -String - - - - Original URL to file
id - - -String - - - - Video ID from source (IE: youtube watch code/archive.org file path)
type - - -String - - - - Original video source
duration - - -Number - - - - Length of media in seconds
rawLink - - -String - - - - URL to raw file copy of media, not applicable to all sources
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(server, channel)

- -
Class containing playlist management logic for a single channel
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(server, channel)

- - - - - - -
- Instantiates a new object to handle playlist management for a single channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -channelManager - - - - Parent server object
channel - - -activeChannel - - - - Parent Channel object for desired channel queue
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(server, chanDB, channel)

- -
Object represneting a single channel's media queue
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(server, chanDB, channel)

- - - - - - -
- Instantiates a new media queue for a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -channelManager - - - - Parent server object
chanDB - - -Document - - - - Related Channel Document from DB
channel - - -activeChannel - - - - Parent Channel object for desired channel queue
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(startTime, startTimeStamp, earlyEnd, uuid)

- -
Class extending media which represents a queued piece of media
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(startTime, startTimeStamp, earlyEnd, uuid)

- - - - - - -
- Creates a new queued media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
startTime - - -Number - - - - - - JS Epoch representing start time
startTimeStamp - - -Number - - - - - - 0 - - Media start time stamp in seconds (relative to duration)
earlyEnd - - -Number - - - - - - Media end timestamp in seconds (relative to duration)
uuid - - -String - - - - - - Media object's unique identifier
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - -

Extends

- - - - -
    -
  • media
  • -
- - - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

exports(server, chatHandler)

- -
Class containing global server-side tokebot logic
- - -
- -
-
- - - - -

Constructor

- - - -

new exports(server, chatHandler)

- - - - - - -
- Instantiates a tokebot object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -channelManager - - - - Parent Server Object
chatHandler - - -chatHandler - - - - Parent Chat Handler Object
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

(async) addPersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to add a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Adds media to channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) addToPlaylistValidator(socket, URL) → {Array}

- - - - - - -
- Validates client requests to add media to a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
URL - - -String - - - - URL String handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- List of media objects which where added -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

(async) addToUserPlaylist(socket, data, userDB)

- - - - - - -
- Adds media to user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) asyncFinisher()

- - - - - - -
- This method seems to be a vestage from a bygone era. We should remove it after documenting shit. -I would now, but I don't want to break shit in a comment-only commit. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) authSocket(socket) → {Mongoose.Document}

- - - - - - -
- Global server-side authorization logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- - Authorized User Document upon success -
- - - -
-
- Type -
-
- -Mongoose.Document - - -
-
- - - - - - - - - - - - - -

(async) broadcastChanEmotes(chanDB)

- - - - - - -
- Broadcasts channel emote list to connected users -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastQueue(chanDB)

- - - - - - -
- Broadcasts channel queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) broadcastSiteEmotes()

- - - - - - -
- Broadcast global emote list -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

broadcastUserList()

- - - - - - -
- Broadcasts user list to all users -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Changes default titles for a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) changeDefaultTitlesUserPlaylist(socket, data, userDB)

- - - - - - -
- Changes default titles for a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

changeDefaultTitlesValidator(data) → {Array}

- - - - - - -
- Validates client requests to change default titles for a given playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of strings containing valid titles from the output -
- - - -
-
- Type -
-
- -Array - - -
-
- - - - - - - - - - - - - -

clearChat(user, chan)

- - - - - - -
- Clears chat for a given channel, targets specified user or entire channel if none found/specified. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - User chats to clear
chan - - -String - - - - Channel to broadcast message within
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

cooldown()

- - - - - - -
- Runs every second for 60 seconds after a toke -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

countdown()

- - - - - - -
- Called each second during the toke. Handles decrementing the timer variable, and countdown end logic. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

crawlConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) createChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Creates a new channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

createPlaylistValidator(socket, data) → {Object}

- - - - - - -
- Validates client requests to create a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated titles -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) createUserPlaylist(socket, data, userDB)

- - - - - - -
- Creates a new user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Define Global Server-Side socket event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines global server-side chat relay event listeners -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

defineListeners(socket)

- - - - - - -
- Defines server-side socket.io listeners for newly connected sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylist(socket, data, userDB)

- - - - - - -
- Deletes a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteChannelPlaylistMedia(socket, data, chanDB)

- - - - - - -
- Deletes media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteMedia(socket, data)

- - - - - - -
- Processes client requests to delete queued media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deletePersonalEmote(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

deletePlaylistMediaValidator(socket, data)

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteRange(socket, data)

- - - - - - -
- Processes request to delete a range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylist(socket, data, chanDB)

- - - - - - -
- Deletes a Channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) deleteUserPlaylistMedia(socket, data, userDB)

- - - - - - -
- Deletes media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

disconnect(reason, type)

- - - - - - -
- Disconnects all sockets for a given user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
reason - - -String - - - - - - Reason for being disconnected
type - - -String - - - - - - Disconnected - - Disconnection Type
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

emit(eventName, data)

- - - - - - -
- Emits an event to all known sockets for a given user - -My brain keeps going back to using dynamic per-user namespaces for this -but everytime i look into it I come to the conclusion that it's a bad idea, then I toy with making chans namespaces -and using per-user channels for this, but what of gold or mod-only features? or games? -No matter what it'd probably end up hacky, as namespaces where meant for splitting app logic not user comms (like rooms). -at the end of the day there has to be some penance for decent multi-session handling on-top of a library that doesn't do it. -Having to crawl through these sockets is that. Because the other ways seem more gross somehow. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
eventName - - -String - - - - Event name to emit to client sockets
data - - -Object - - - - Data to emit to client sockets
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) end(quiet, noArchive, volatile, chanDB)

- - - - - - -
- End currently playing media -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
quiet - - -Boolean - - - - - - false - - Enable to prevent ending the media client-side
noArchive - - -Boolean - - - - - - false - - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false
volatile - - -Boolean - - - - - - false - - Enable to prevent DB Transactions
chanDB - - -Mongoose.Document - - - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) endLivestream(wasPlaying, chanDB)

- - - - - - -
- Ends running Livestream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

fromMedia(media, startTime, startTimeStamp) → {queuedMedia}

- - - - - - -
- Creates a queuedMedia object from a media object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -media - - - - Media object to queue
startTime - - -Number - - - - Start time formatted as a JS Epoch
startTimeStamp - - -Number - - - - Start time stamp in seconds
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- queuedMedia object created from given media object -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

fromMediaArray(mediaList, start)

- - - - - - -
- Converts array of media objects into array of queuedMedia objects -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaList - - -Array - - - - Array of media objects to queue
start - - -Number - - - - Start time formatted as JS Epoch
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Array of converted queued media objects -
- - - - - - - - - - - - - - - -

genUUID()

- - - - - - -
- Generates new unique identifier for queued media -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) getActiveChan(socket) → {Object}

- - - - - - -
- Gets active channel from a given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Object containing users active channel name and channel document object -
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - - - - - - - - - -

(async) getChannelPlaylists(socket, chanDB)

- - - - - - -
- Sends channel playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getConnectedChannels(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getConnections(user, cb)

- - - - - - -
- Iterates through connections by a given username, and runs them through a given callback function/method -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to crawl connections against
cb - - -function - - - - Callback function to run active connections of a given user against
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

getEndTime(fullTime)

- - - - - - -
- return the end time of a given queuedMedia object -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
fullTime - - -boolean - - - - - - false - - Overrides early ends
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- end time of given queuedMedia object -
- - - - - - - - - - - - - - - -

getItemAtEpoch(epoch) → {queuedMedia}

- - - - - - -
- Gets a media item by epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found media item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemByUUID(uuid) → {queuedMedia}

- - - - - - -
- Get Scheduled Item by UUID -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- found item -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getItemsBetweenEpochs(start, end, noUnfinished) → {queuedMedia}

- - - - - - -
- Returns scheduled media between two given datetimes -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (Millis)
end - - -Number - - - - - - End date by JS Epoch (Millis)
noUnfinished - - -Boolean - - - - - - false - - Enable to include currently playing media
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Found Media Objects -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getLastItem(epoch)

- - - - - - -
- Gets last item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Last played item -
- - - - - - - - - - - - - - - -

getNextItem(epoch) → {queuedMedia}

- - - - - - -
- Gets next item from a given epoch -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
epoch - - -Number - - - - Date to check by JS Epoch (Millis), defaults to now
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Next item on the schedule -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

getSocketInfo(socket)

- - - - - - -
- Pulls user information by socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns related user info -
- - - - - - - - - - - - - - - -

getStart(start)

- - - - - - -
- Validates start times, and replaces bad ones with 5ms in the future -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
start - - -Number - - - - Start time to validate by JS Epoch (millis)
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Start time as JS Epoch (millis) -
- - - - - - - - - - - - - - - -

(async) getUserPlaylists(socket, userDB)

- - - - - - -
- Sends user playlist data to a requesting socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
userDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) goLive(socket, data)

- - - - - - -
- Handle client request to start an HLS live stream -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleChat(socket, data)

- - - - - - -
- Handles incoming chat messages from client connections -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections to the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(socket)

- - - - - - -
- Handles global server-side initialization for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleConnection(userDB, chanDB, socket)

- - - - - - -
- Handles server-side initialization for new connections from a specific user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket)

- - - - - - -
- Handles server-side initialization for disconnecting from the channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

handleDisconnect(socket, reason)

- - - - - - -
- Global server-side logic for handling disconncted sockets -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket to check
reason - - -String - - - - Reason for disconnection
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) handleRawRefresh(mediaObj) → {queuedMedia}

- - - - - - -
- Refreshes expired raw links before media plays -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- passes through Media object with updated link upon success -
- - - -
-
- Type -
-
- -queuedMedia - - -
-
- - - - - - - - - - - - - -

kickConnections(user, reason)

- - - - - - -
- Kicks a user from all channels by username -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -String - - - - Username to kick from the server
reason - - -String - - - - Reason for kick
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamOverwriteSchedule(wasPlaying, chanDB)

- - - - - - -
- Overwrites livestream over scheduled media content after it has ended -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) livestreamPushbackSchedule(wasPlaying, chanDB)

- - - - - - -
- Pushes back any missed content scheduled during Livestream after Livestream has ended. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
wasPlaying - - -queuedMedia - - - - Media object that was playing while we started the Livestream
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) moveMedia(socket, data)

- - - - - - -
- Processes request to move queued media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) preSwitch(mediaObj)

- - - - - - -
- Called 10 seconds before media begins to play -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
mediaObj - - -queuedMedia - - - - Media object that's about to play
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) prepQueue(chanDB)

- - - - - - -
- Prepares channel queue for network transmission -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- de-hydrated scehdule information -
- - - - - - - - - - - - - - - -

(async) queueChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues an entire channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

queueFromChannelPlaylistValidator(socket, data) → {Number}

- - - - - - -
- Validates client requests to queue media from a playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns validated start time on success -
- - - -
-
- Type -
-
- -Number - - -
-
- - - - - - - - - - - - - -

(async) queueFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Queues random media from a given channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueRandomFromUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues random media from a given user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueURL(socket, data)

- - - - - - -
- Accepts new URL's to queue from the client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the URL from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) queueUserPlaylist(socket, data, userDB, chanDB)

- - - - - - -
- Queues an entire user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) refreshCommands()

- - - - - - -
- Reloads toke commands from DB into RAM-based toke command store -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

refreshNextTimer(volatile)

- - - - - - -
- Calculates next item to play, and sets timer to play it at it's scheduled start -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
volatile - - -Boolean - - - - - - false - - Disables DB Transactions if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rehydrateQueue(chanDB)

- - - - - - -
- Rehydrates media schedule from DB -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Pass through Channel Document to save on DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChannelAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to a given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChat(user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - - - Channel to broadcast message within
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayChatObject(chan, chat)

- - - - - - -
- Relays an existing chat object to a channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chan - - -String - - - - Channel to broadcast message within
chat - - -chat - - - - Chat Object representing the message to broadcast to the given channel
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChat(user, flair, highLevel, msg, type, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
user - - -String - - - - - - Originating user
flair - - -String - - - - - - Flair ID to mark chat with
highLevel - - -Number - - - - - - High Level to mark chat with
msg - - -String - - - - - - Message Text Content
type - - -String - - - - - - chat - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalChatObject(chat)

- - - - - - -
- Relays an existing chat object to the entire server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chat - - -chat - - - - Chat Object representing the message to broadcast throughout the server
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayGlobalTokeWhisper(msg, links)

- - - - - - -
- Broadcasts toke whisper to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChat(socket, user, flair, highLevel, msg, type, chan, links)

- - - - - - -
- Creates a new chatObject and relays the resulting message to the given socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending a message to (sounds menacing, huh?)
user - - -String - - - - Originating user
flair - - -String - - - - Flair ID to mark chat with
highLevel - - -Number - - - - High Level to mark chat with
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
chan - - -String - - - - Channel to broadcast message within
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayPrivateChatObject(socket, data)

- - - - - - -
- Handles incoming client request to delete a personal emote -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayServerAnnouncement(msg, links)

- - - - - - -
- Broadcasts announcement to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeCallout(msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayTokeWhisper(socket, msg, links)

- - - - - - -
- Broadcasts toke callout to the server -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're sending the whisper to
msg - - -String - - - - Message Text Content
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

relayUserChat(socket, msg, type, links)

- - - - - - -
- Relays a chat message from a user to the rest of the channel based on socket -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
msg - - -String - - - - Message Text Content
type - - -String - - - - Message Type, used for client-side chat post-processing.
links - - -Array - - - - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) removeMedia(uuid, socket, chanDB, noScheduling) → {Media}

- - - - - - -
- Removes a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
uuid - - -String - - - - - - UUID of item to reschedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
noScheduling - - -Boolean - - - - - - false - - Disables schedule timer refresh if true
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- Deleted Media Item -
- - - -
-
- Type -
-
- -Media - - -
-
- - - - - - - - - - - - - -

(async) removeRange(start, end, socket, noUnfinished)

- - - - - - -
- Removes range of media items from the queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
start - - -Number - - - - - - Start date by JS Epoch (millis)
end - - -Number - - - - - - End date by JS Epoch (millis)
socket - - -Socket - - - - - - Requesting Socket
noUnfinished - - -Boolean - - - - - - false - - Set to true to include items that may be currently playing
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) renameChannelPlaylist(socket, data, chanDB)

- - - - - - -
- Renames a channel playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
chanDB - - -Mongoose.Document - - - - Channel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

renameChannelPlaylistValidator(socket, data) → {String}

- - - - - - -
- Validates client requests to rename the playlist validator -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Newly connected socket to define listeners against
data - - -Object - - - - Data handed over from the client
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns escaped/trimmed name upon success -
- - - -
-
- Type -
-
- -String - - -
-
- - - - - - - - - - - - - -

(async) renameUserPlaylist(socket, data, userDB)

- - - - - - -
- Renames a user playlist -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
data - - -Object - - - - Data handed over from the client
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) rescheduleMedia(uuid, start, socket, chanDB)

- - - - - - -
- Reschedules a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uuid - - -String - - - - UUID of item to reschedule
start - - -Number - - - - New start time by JS Epoch (Millis)
socket - - -Socket - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

resetToke()

- - - - - - -
- Resets toke cooldowns early upon authorized request -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) scheduleMedia(media, socket, chanDB, force, volatile, startVolatile, saveLate, noSave)

- - - - - - -
- Schedules a Media Item - -This is a fun method and I think it deserves it's own little explination... -Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option -I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. -Maps seem like a good choice, if it wheren't for the issue of keeping them ordered... - -That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map, -we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally. - -Also a note on preformance: -While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background, -simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely, -since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. -Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. -This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider -that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... - - -Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. -This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC) -Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P - -If for some reason they haven't and we're not dead, we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication - -Further Reading: -https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays -https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
media - - -Media - - - - - - Media item to schedule
socket - - -Socket - - - - - - Requesting Socket
chanDB - - -Mongoose.Document - - - - - - Channnel Document Passthrough to save on DB Access
force - - -Boolean - - - - - - false - - Ignore certain conditions that would prevent scehduling and play the bitch anyways, used for internal function calls
volatile - - -Boolean - - - - - - false - - Prevent DB Writes, used for internal function calls
startVolatile - - -Boolean - - - - - - false - - Runs refreshNextTimer calls without DB writes, used for internal function calls
saveLate - - -Boolean - - - - - - false - - Saves items even if they're about to, or have already started. Used for internal function calls
noSave - - -Boolean - - - - - - false - - Allows function to edit Channel Document, but not save. Used for internal function calls in which the channel document is passed through, but will be saved immediatly after the scheduleMedia() call.
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendChanEmotes(chanDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendClientMetadata(userDB, chanDB)

- - - - - - -
- Sends glut of required initial metadata to the client upon a new connection -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
chanDB - - -Mongoose.Document - - - - Channnel Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sendMedia(socket)

- - - - - - -
- Send media update to a specific socket or broadcast it to the entire channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendPersonalEmotes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendSiteEmotes()

- - - - - - -
- Send copy of site emotes to the user -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) sendUsedTokes(userDB)

- - - - - - -
- Send copy of channel emotes to the user -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userDB - - -Mongoose.Document - - - - User Document Passthrough to save on DB Access
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setFlair(socket, data)

- - - - - - -
- Handles incoming client request to change flair -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) setHighLevel(socket, data)

- - - - - - -
- Handles incoming client request to change high level -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we're receiving the request from
data - - -Object - - - - Event payload
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

socketCrawl(cb)

- - - - - - -
- Iterates through all known connections for a given user, running them through a supplied callback function -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cb - - -function - - - - Callback to call against found sockets for a given user
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) start(mediaObj, timestamp, volatile)

- - - - - - -
- Kicks off a media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
mediaObj - - -queuedMedia - - - - - - Media object that's about to play
timestamp - - -Number - - - - - - Media start timestamp in seconds
volatile - - -Boolean - - - - - - false - - Disables DB Transactions
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

stop(socket)

- - - - - - -
- Stops currently playing media item -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- returns false if there is nothing to stop -
- - - - - - - - - - - - - - - -

(async) stopMedia(socket)

- - - - - - -
- Processes requests to stop currently playing media from client -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Socket we received the request from
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) stopScheduleTimers(noArchive)

- - - - - - -
- Clears and scheduling timers -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
noArchive - - -Boolean - - - - - - true - - Disables Archiving
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

sync()

- - - - - - -
- Sends a syncronization ping out to client Sockets and increments the tracked timestamp by the Synchronization Delta -Called auto-magically by the Synchronization Timer -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) toggleLock(socket)

- - - - - - -
- Handle client request to (un)lock queue -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

tokeProcessor(commandObj) → {Boolean}

- - - - - - -
- Processes toke commands from Command Pre-Processor -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
commandObj - - -Object - - - - Object representing a single given command/chat request, passed down from the Command Pre-Processor
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- True if the toke is an invalid toke command (tells Command Pre-Processor to send command as chat) -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -

updateFlair(flair)

- - - - - - -
- Set flair for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
flair - - -String - - - - Flair string to update user's flair to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

updateHighLevel(highLevel)

- - - - - - -
- Set high level for a given user and broadcast update to clients -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
highLevel - - -Number - - - - Number to update user's high-level to
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) validateSocket(socket) → {Boolean}

- - - - - - -
- Global server-side validation logic for new connections to any channel -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
socket - - -Socket - - - - Requesting Socket
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - -
- true on success -
- - - -
-
- Type -
-
- -Boolean - - -
-
- - - - - - - - - - - - - -
- -
- - - - -
- - - -
- -
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:17:22 GMT-0400 (Eastern Daylight Time) -
- - - - - \ No newline at end of file diff --git a/www/doc/activeChannel.html b/www/doc/server/activeChannel.html similarity index 99% rename from www/doc/activeChannel.html rename to www/doc/server/activeChannel.html index 51f3d07..e550d80 100644 --- a/www/doc/activeChannel.html +++ b/www/doc/server/activeChannel.html @@ -786,7 +786,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_activeChannel.js.html b/www/doc/server/app_channel_activeChannel.js.html similarity index 99% rename from www/doc/app_channel_activeChannel.js.html rename to www/doc/server/app_channel_activeChannel.js.html index 18c6698..f351621 100644 --- a/www/doc/app_channel_activeChannel.js.html +++ b/www/doc/server/app_channel_activeChannel.js.html @@ -196,7 +196,7 @@ module.exports = activeChannel;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_channelManager.js.html b/www/doc/server/app_channel_channelManager.js.html similarity index 99% rename from www/doc/app_channel_channelManager.js.html rename to www/doc/server/app_channel_channelManager.js.html index 7511d25..ac28645 100644 --- a/www/doc/app_channel_channelManager.js.html +++ b/www/doc/server/app_channel_channelManager.js.html @@ -347,7 +347,7 @@ module.exports = channelManager;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_chat.js.html b/www/doc/server/app_channel_chat.js.html similarity index 99% rename from www/doc/app_channel_chat.js.html rename to www/doc/server/app_channel_chat.js.html index d70c35f..12a97a2 100644 --- a/www/doc/app_channel_chat.js.html +++ b/www/doc/server/app_channel_chat.js.html @@ -81,7 +81,7 @@ module.exports = chat;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_chatBuffer.js.html b/www/doc/server/app_channel_chatBuffer.js.html similarity index 99% rename from www/doc/app_channel_chatBuffer.js.html rename to www/doc/server/app_channel_chatBuffer.js.html index 2f38cfa..039412f 100644 --- a/www/doc/app_channel_chatBuffer.js.html +++ b/www/doc/server/app_channel_chatBuffer.js.html @@ -178,7 +178,7 @@ module.exports = chatBuffer;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_chatHandler.js.html b/www/doc/server/app_channel_chatHandler.js.html similarity index 99% rename from www/doc/app_channel_chatHandler.js.html rename to www/doc/server/app_channel_chatHandler.js.html index c660eb2..1fa9eaa 100644 --- a/www/doc/app_channel_chatHandler.js.html +++ b/www/doc/server/app_channel_chatHandler.js.html @@ -376,7 +376,7 @@ module.exports = chatHandler;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_commandPreprocessor.js.html b/www/doc/server/app_channel_commandPreprocessor.js.html similarity index 99% rename from www/doc/app_channel_commandPreprocessor.js.html rename to www/doc/server/app_channel_commandPreprocessor.js.html index 173338b..554d104 100644 --- a/www/doc/app_channel_commandPreprocessor.js.html +++ b/www/doc/server/app_channel_commandPreprocessor.js.html @@ -473,7 +473,7 @@ module.exports = commandPreprocessor;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_connectedUser.js.html b/www/doc/server/app_channel_connectedUser.js.html similarity index 99% rename from www/doc/app_channel_connectedUser.js.html rename to www/doc/server/app_channel_connectedUser.js.html index 56a2999..b37162c 100644 --- a/www/doc/app_channel_connectedUser.js.html +++ b/www/doc/server/app_channel_connectedUser.js.html @@ -334,7 +334,7 @@ module.exports = connectedUser;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_media.js.html b/www/doc/server/app_channel_media_media.js.html similarity index 99% rename from www/doc/app_channel_media_media.js.html rename to www/doc/server/app_channel_media_media.js.html index 7ac6996..08fd81b 100644 --- a/www/doc/app_channel_media_media.js.html +++ b/www/doc/server/app_channel_media_media.js.html @@ -83,7 +83,7 @@ module.exports = media;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_playlistHandler.js.html b/www/doc/server/app_channel_media_playlistHandler.js.html similarity index 99% rename from www/doc/app_channel_media_playlistHandler.js.html rename to www/doc/server/app_channel_media_playlistHandler.js.html index 12e23ab..0a01917 100644 --- a/www/doc/app_channel_media_playlistHandler.js.html +++ b/www/doc/server/app_channel_media_playlistHandler.js.html @@ -1180,7 +1180,7 @@ module.exports = playlistHandler;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_queue.js.html b/www/doc/server/app_channel_media_queue.js.html similarity index 99% rename from www/doc/app_channel_media_queue.js.html rename to www/doc/server/app_channel_media_queue.js.html index d1c0f3c..eb6d75f 100644 --- a/www/doc/app_channel_media_queue.js.html +++ b/www/doc/server/app_channel_media_queue.js.html @@ -1795,7 +1795,7 @@ module.exports = queue;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_media_queuedMedia.js.html b/www/doc/server/app_channel_media_queuedMedia.js.html similarity index 99% rename from www/doc/app_channel_media_queuedMedia.js.html rename to www/doc/server/app_channel_media_queuedMedia.js.html index 5593036..d8ba89f 100644 --- a/www/doc/app_channel_media_queuedMedia.js.html +++ b/www/doc/server/app_channel_media_queuedMedia.js.html @@ -165,7 +165,7 @@ module.exports = queuedMedia;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/app_channel_tokebot.js.html b/www/doc/server/app_channel_tokebot.js.html similarity index 98% rename from www/doc/app_channel_tokebot.js.html rename to www/doc/server/app_channel_tokebot.js.html index c301d8d..f272b14 100644 --- a/www/doc/app_channel_tokebot.js.html +++ b/www/doc/server/app_channel_tokebot.js.html @@ -58,14 +58,8 @@ class tokebot{ * @param {chatHandler} chatHandler - Parent Chat Handler Object */ constructor(server, chatHandler){ - /** - * Parent channelManager object - */ + //Set parents this.server = server; - - /** - * Parent chatHandler object - */ this.chatHandler = chatHandler; //Set timeouts to null @@ -279,7 +273,7 @@ module.exports = tokebot;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/channelManager.html b/www/doc/server/channelManager.html similarity index 99% rename from www/doc/channelManager.html rename to www/doc/server/channelManager.html index 361136a..5cc831d 100644 --- a/www/doc/channelManager.html +++ b/www/doc/server/channelManager.html @@ -1991,7 +1991,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/chat.html b/www/doc/server/chat.html similarity index 99% rename from www/doc/chat.html rename to www/doc/server/chat.html index cff85f0..1103ae6 100644 --- a/www/doc/chat.html +++ b/www/doc/server/chat.html @@ -329,7 +329,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/chatBuffer.html b/www/doc/server/chatBuffer.html similarity index 99% rename from www/doc/chatBuffer.html rename to www/doc/server/chatBuffer.html index 925cfd4..b33a017 100644 --- a/www/doc/chatBuffer.html +++ b/www/doc/server/chatBuffer.html @@ -829,7 +829,7 @@ Left here since it seems like good form anywho, since this would be a private, o
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/chatHandler.html b/www/doc/server/chatHandler.html similarity index 99% rename from www/doc/chatHandler.html rename to www/doc/server/chatHandler.html index d8ebdf7..534792b 100644 --- a/www/doc/chatHandler.html +++ b/www/doc/server/chatHandler.html @@ -3686,7 +3686,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/commandPreprocessor.html b/www/doc/server/commandPreprocessor.html similarity index 99% rename from www/doc/commandPreprocessor.html rename to www/doc/server/commandPreprocessor.html index dc93df3..999c40f 100644 --- a/www/doc/commandPreprocessor.html +++ b/www/doc/server/commandPreprocessor.html @@ -1246,7 +1246,7 @@ These arrays are used to handle further command/chat processing
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/commandProcessor.html b/www/doc/server/commandProcessor.html similarity index 99% rename from www/doc/commandProcessor.html rename to www/doc/server/commandProcessor.html index c983344..562c247 100644 --- a/www/doc/commandProcessor.html +++ b/www/doc/server/commandProcessor.html @@ -1831,7 +1831,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/connectedUser.html b/www/doc/server/connectedUser.html similarity index 99% rename from www/doc/connectedUser.html rename to www/doc/server/connectedUser.html index 75681f1..8e2281c 100644 --- a/www/doc/connectedUser.html +++ b/www/doc/server/connectedUser.html @@ -1879,7 +1879,7 @@ Having to crawl through these sockets is that. Because the other ways seem more
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/fonts/OpenSans-Bold-webfont.eot b/www/doc/server/fonts/OpenSans-Bold-webfont.eot similarity index 100% rename from www/doc/fonts/OpenSans-Bold-webfont.eot rename to www/doc/server/fonts/OpenSans-Bold-webfont.eot diff --git a/www/doc/fonts/OpenSans-Bold-webfont.svg b/www/doc/server/fonts/OpenSans-Bold-webfont.svg similarity index 100% rename from www/doc/fonts/OpenSans-Bold-webfont.svg rename to www/doc/server/fonts/OpenSans-Bold-webfont.svg diff --git a/www/doc/fonts/OpenSans-Bold-webfont.woff b/www/doc/server/fonts/OpenSans-Bold-webfont.woff similarity index 100% rename from www/doc/fonts/OpenSans-Bold-webfont.woff rename to www/doc/server/fonts/OpenSans-Bold-webfont.woff diff --git a/www/doc/fonts/OpenSans-BoldItalic-webfont.eot b/www/doc/server/fonts/OpenSans-BoldItalic-webfont.eot similarity index 100% rename from www/doc/fonts/OpenSans-BoldItalic-webfont.eot rename to www/doc/server/fonts/OpenSans-BoldItalic-webfont.eot diff --git a/www/doc/fonts/OpenSans-BoldItalic-webfont.svg b/www/doc/server/fonts/OpenSans-BoldItalic-webfont.svg similarity index 100% rename from www/doc/fonts/OpenSans-BoldItalic-webfont.svg rename to www/doc/server/fonts/OpenSans-BoldItalic-webfont.svg diff --git a/www/doc/fonts/OpenSans-BoldItalic-webfont.woff b/www/doc/server/fonts/OpenSans-BoldItalic-webfont.woff similarity index 100% rename from www/doc/fonts/OpenSans-BoldItalic-webfont.woff rename to www/doc/server/fonts/OpenSans-BoldItalic-webfont.woff diff --git a/www/doc/fonts/OpenSans-Italic-webfont.eot b/www/doc/server/fonts/OpenSans-Italic-webfont.eot similarity index 100% rename from www/doc/fonts/OpenSans-Italic-webfont.eot rename to www/doc/server/fonts/OpenSans-Italic-webfont.eot diff --git a/www/doc/fonts/OpenSans-Italic-webfont.svg b/www/doc/server/fonts/OpenSans-Italic-webfont.svg similarity index 100% rename from www/doc/fonts/OpenSans-Italic-webfont.svg rename to www/doc/server/fonts/OpenSans-Italic-webfont.svg diff --git a/www/doc/fonts/OpenSans-Italic-webfont.woff b/www/doc/server/fonts/OpenSans-Italic-webfont.woff similarity index 100% rename from www/doc/fonts/OpenSans-Italic-webfont.woff rename to www/doc/server/fonts/OpenSans-Italic-webfont.woff diff --git a/www/doc/fonts/OpenSans-Light-webfont.eot b/www/doc/server/fonts/OpenSans-Light-webfont.eot similarity index 100% rename from www/doc/fonts/OpenSans-Light-webfont.eot rename to www/doc/server/fonts/OpenSans-Light-webfont.eot diff --git a/www/doc/fonts/OpenSans-Light-webfont.svg b/www/doc/server/fonts/OpenSans-Light-webfont.svg similarity index 100% rename from www/doc/fonts/OpenSans-Light-webfont.svg rename to www/doc/server/fonts/OpenSans-Light-webfont.svg diff --git a/www/doc/fonts/OpenSans-Light-webfont.woff b/www/doc/server/fonts/OpenSans-Light-webfont.woff similarity index 100% rename from www/doc/fonts/OpenSans-Light-webfont.woff rename to www/doc/server/fonts/OpenSans-Light-webfont.woff diff --git a/www/doc/fonts/OpenSans-LightItalic-webfont.eot b/www/doc/server/fonts/OpenSans-LightItalic-webfont.eot similarity index 100% rename from www/doc/fonts/OpenSans-LightItalic-webfont.eot rename to www/doc/server/fonts/OpenSans-LightItalic-webfont.eot diff --git a/www/doc/fonts/OpenSans-LightItalic-webfont.svg b/www/doc/server/fonts/OpenSans-LightItalic-webfont.svg similarity index 100% rename from www/doc/fonts/OpenSans-LightItalic-webfont.svg rename to www/doc/server/fonts/OpenSans-LightItalic-webfont.svg diff --git a/www/doc/fonts/OpenSans-LightItalic-webfont.woff b/www/doc/server/fonts/OpenSans-LightItalic-webfont.woff similarity index 100% rename from www/doc/fonts/OpenSans-LightItalic-webfont.woff rename to www/doc/server/fonts/OpenSans-LightItalic-webfont.woff diff --git a/www/doc/fonts/OpenSans-Regular-webfont.eot b/www/doc/server/fonts/OpenSans-Regular-webfont.eot similarity index 100% rename from www/doc/fonts/OpenSans-Regular-webfont.eot rename to www/doc/server/fonts/OpenSans-Regular-webfont.eot diff --git a/www/doc/fonts/OpenSans-Regular-webfont.svg b/www/doc/server/fonts/OpenSans-Regular-webfont.svg similarity index 100% rename from www/doc/fonts/OpenSans-Regular-webfont.svg rename to www/doc/server/fonts/OpenSans-Regular-webfont.svg diff --git a/www/doc/fonts/OpenSans-Regular-webfont.woff b/www/doc/server/fonts/OpenSans-Regular-webfont.woff similarity index 100% rename from www/doc/fonts/OpenSans-Regular-webfont.woff rename to www/doc/server/fonts/OpenSans-Regular-webfont.woff diff --git a/www/doc/global.html b/www/doc/server/global.html similarity index 99% rename from www/doc/global.html rename to www/doc/server/global.html index fcb66b1..a6340bb 100644 --- a/www/doc/global.html +++ b/www/doc/server/global.html @@ -7377,7 +7377,7 @@ Warns server admin against unsafe config options.
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/index.html b/www/doc/server/index.html similarity index 67% rename from www/doc/index.html rename to www/doc/server/index.html index fdf486a..5d31476 100644 --- a/www/doc/index.html +++ b/www/doc/server/index.html @@ -42,6 +42,37 @@ +
+

Canopy - 0.3-INDEV

+

Canopy - /ˈkæ.nə.pi/:

+
    +
  • The upper layer of foliage and branches of a forest, containing the majority of animal life.
  • +
+

Canopy is a community chat & synced video embedding web application, intended to replace fore.st as the server software for ourfore.st. +This new codebase intends to solve the following issues with the current CyTube based software:

+
    +
  • Unmaintained upstream codebase.
  • +
  • Different goals.
  • +
  • Different coding styles.
  • +
  • Obsolete Technology and Dependencies.
  • +
  • General Clunk
  • +
  • Less Unique Community Identity
  • +
+

Canopy intends to be a simple node/express.js app. It leverages yt-dlp and the internet archive REST api for metadata gathering. Persistant storage is handled by mongodb, as it's document based nature inherintly works well for cleanly storing large config documents for user/channel settings, and the low use of inter-collection references within the canopy software. All hardcore security functions like server-side input sanatization, session handling, CSRF mitigation, and password hashing are handled by industry-standard open source libraries such as validator/express-validator, express-sessions, csrf-sync, and bcrypt, however it IS hobbiest software, and it should be treated as such.

+

The Canopy codebase does not, nor will it ever contain:

+
    +
  • Advertisements (targetted or otherwise)
  • +
  • Proprietary Code
  • +
  • Cryptocurrency/Blockchain integration
  • +
  • 'Analytics/Telemtry' spyware
  • +
  • The use of video sources which require proprietary 'Digital Rights Management Ristricitons Malware' such as Widevine.
  • +
+

Thirdparty media providers may or may not contain all of the above atrocities :P (though browser-side DRM extensions will never be required), always use an ad-blocker!

+

Our current goal is to create a cleaner, more modern, purpose-built codebase that has feature-parity with the current version of fore.st, while writing improvements where possible. Once this is accomplished, and ourfore.st has been migrated, work will continue to re-create features from TTN, while also building completely new ones as well.

+

License

+

Canopy is written by the community, and provided under the GNU Affero General Public License v3 in order to prevent Canopy from being used in proprietary software or shitcoin scams.

+
+ @@ -56,7 +87,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/media.html b/www/doc/server/media.html similarity index 99% rename from www/doc/media.html rename to www/doc/server/media.html index 9abb2cd..e0a1b6d 100644 --- a/www/doc/media.html +++ b/www/doc/server/media.html @@ -352,7 +352,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/playlistHandler.html b/www/doc/server/playlistHandler.html similarity index 99% rename from www/doc/playlistHandler.html rename to www/doc/server/playlistHandler.html index 971f6c8..3efb58f 100644 --- a/www/doc/playlistHandler.html +++ b/www/doc/server/playlistHandler.html @@ -5108,7 +5108,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/queue.html b/www/doc/server/queue.html similarity index 99% rename from www/doc/queue.html rename to www/doc/server/queue.html index a61f0f1..5166e19 100644 --- a/www/doc/queue.html +++ b/www/doc/server/queue.html @@ -5805,7 +5805,7 @@ Called auto-magically by the Synchronization Timer
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/queuedMedia.html b/www/doc/server/queuedMedia.html similarity index 99% rename from www/doc/queuedMedia.html rename to www/doc/server/queuedMedia.html index d57db67..fc2445c 100644 --- a/www/doc/queuedMedia.html +++ b/www/doc/server/queuedMedia.html @@ -936,7 +936,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_channelBanSchema.js.html b/www/doc/server/schemas_channel_channelBanSchema.js.html similarity index 99% rename from www/doc/schemas_channel_channelBanSchema.js.html rename to www/doc/server/schemas_channel_channelBanSchema.js.html index a676c99..52c503d 100644 --- a/www/doc/schemas_channel_channelBanSchema.js.html +++ b/www/doc/server/schemas_channel_channelBanSchema.js.html @@ -101,7 +101,7 @@ module.exports = channelBanSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_channelPermissionSchema.js.html b/www/doc/server/schemas_channel_channelPermissionSchema.js.html similarity index 99% rename from www/doc/schemas_channel_channelPermissionSchema.js.html rename to www/doc/server/schemas_channel_channelPermissionSchema.js.html index d563aa5..4beb50c 100644 --- a/www/doc/schemas_channel_channelPermissionSchema.js.html +++ b/www/doc/server/schemas_channel_channelPermissionSchema.js.html @@ -169,7 +169,7 @@ module.exports = channelPermissionSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_channelSchema.js.html b/www/doc/server/schemas_channel_channelSchema.js.html similarity index 99% rename from www/doc/schemas_channel_channelSchema.js.html rename to www/doc/server/schemas_channel_channelSchema.js.html index fb7b92f..7e669b8 100644 --- a/www/doc/schemas_channel_channelSchema.js.html +++ b/www/doc/server/schemas_channel_channelSchema.js.html @@ -934,7 +934,7 @@ module.exports = mongoose.model("channel", channelSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_chatSchema.js.html b/www/doc/server/schemas_channel_chatSchema.js.html similarity index 99% rename from www/doc/schemas_channel_chatSchema.js.html rename to www/doc/server/schemas_channel_chatSchema.js.html index 33d4e2d..7fc66fc 100644 --- a/www/doc/schemas_channel_chatSchema.js.html +++ b/www/doc/server/schemas_channel_chatSchema.js.html @@ -96,7 +96,7 @@ module.exports = chatSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_mediaSchema.js.html b/www/doc/server/schemas_channel_media_mediaSchema.js.html similarity index 99% rename from www/doc/schemas_channel_media_mediaSchema.js.html rename to www/doc/server/schemas_channel_media_mediaSchema.js.html index 7db4172..5cf4ba9 100644 --- a/www/doc/schemas_channel_media_mediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_mediaSchema.js.html @@ -96,7 +96,7 @@ module.exports = mediaSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_playlistMediaSchema.js.html b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html similarity index 99% rename from www/doc/schemas_channel_media_playlistMediaSchema.js.html rename to www/doc/server/schemas_channel_media_playlistMediaSchema.js.html index 8523ae3..c2e909d 100644 --- a/www/doc/schemas_channel_media_playlistMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html @@ -124,7 +124,7 @@ module.exports = mediaSchema.discriminator('saved', playlistMediaProperties);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_playlistSchema.js.html b/www/doc/server/schemas_channel_media_playlistSchema.js.html similarity index 99% rename from www/doc/schemas_channel_media_playlistSchema.js.html rename to www/doc/server/schemas_channel_media_playlistSchema.js.html index 601a46c..5d9cc3b 100644 --- a/www/doc/schemas_channel_media_playlistSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistSchema.js.html @@ -174,7 +174,7 @@ module.exports = playlistSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_channel_media_queuedMediaSchema.js.html b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html similarity index 99% rename from www/doc/schemas_channel_media_queuedMediaSchema.js.html rename to www/doc/server/schemas_channel_media_queuedMediaSchema.js.html index a384c5e..bd2431c 100644 --- a/www/doc/schemas_channel_media_queuedMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html @@ -113,7 +113,7 @@ module.exports = mediaSchema.discriminator('queued', queuedProperties);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_emoteSchema.js.html b/www/doc/server/schemas_emoteSchema.js.html similarity index 99% rename from www/doc/schemas_emoteSchema.js.html rename to www/doc/server/schemas_emoteSchema.js.html index aa89855..258c270 100644 --- a/www/doc/schemas_emoteSchema.js.html +++ b/www/doc/server/schemas_emoteSchema.js.html @@ -164,7 +164,7 @@ module.exports = mongoose.model("emote", emoteSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_flairSchema.js.html b/www/doc/server/schemas_flairSchema.js.html similarity index 99% rename from www/doc/schemas_flairSchema.js.html rename to www/doc/server/schemas_flairSchema.js.html index 8443064..3329d2a 100644 --- a/www/doc/schemas_flairSchema.js.html +++ b/www/doc/server/schemas_flairSchema.js.html @@ -118,7 +118,7 @@ module.exports = mongoose.model("flair", flairSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_permissionSchema.js.html b/www/doc/server/schemas_permissionSchema.js.html similarity index 99% rename from www/doc/schemas_permissionSchema.js.html rename to www/doc/server/schemas_permissionSchema.js.html index 5dfd02a..3659f64 100644 --- a/www/doc/schemas_permissionSchema.js.html +++ b/www/doc/server/schemas_permissionSchema.js.html @@ -356,7 +356,7 @@ module.exports = mongoose.model("permissions", permissionSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_statSchema.js.html b/www/doc/server/schemas_statSchema.js.html similarity index 99% rename from www/doc/schemas_statSchema.js.html rename to www/doc/server/schemas_statSchema.js.html index 1d420a8..3f276ed 100644 --- a/www/doc/schemas_statSchema.js.html +++ b/www/doc/server/schemas_statSchema.js.html @@ -240,7 +240,7 @@ module.exports = mongoose.model("statistics", statSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_tokebot_tokeCommandSchema.js.html b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html similarity index 99% rename from www/doc/schemas_tokebot_tokeCommandSchema.js.html rename to www/doc/server/schemas_tokebot_tokeCommandSchema.js.html index edfb17c..59a8416 100644 --- a/www/doc/schemas_tokebot_tokeCommandSchema.js.html +++ b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html @@ -160,7 +160,7 @@ module.exports = mongoose.model("tokeCommand", tokeCommandSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_emailChangeSchema.js.html b/www/doc/server/schemas_user_emailChangeSchema.js.html similarity index 99% rename from www/doc/schemas_user_emailChangeSchema.js.html rename to www/doc/server/schemas_user_emailChangeSchema.js.html index edb5d51..a4da1f1 100644 --- a/www/doc/schemas_user_emailChangeSchema.js.html +++ b/www/doc/server/schemas_user_emailChangeSchema.js.html @@ -222,7 +222,7 @@ module.exports = mongoose.model("emailChange", emailChangeSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_passwordResetSchema.js.html b/www/doc/server/schemas_user_passwordResetSchema.js.html similarity index 99% rename from www/doc/schemas_user_passwordResetSchema.js.html rename to www/doc/server/schemas_user_passwordResetSchema.js.html index 03a6c5d..16d75cc 100644 --- a/www/doc/schemas_user_passwordResetSchema.js.html +++ b/www/doc/server/schemas_user_passwordResetSchema.js.html @@ -198,7 +198,7 @@ module.exports = mongoose.model("passwordReset", passwordResetSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_userBanSchema.js.html b/www/doc/server/schemas_user_userBanSchema.js.html similarity index 99% rename from www/doc/schemas_user_userBanSchema.js.html rename to www/doc/server/schemas_user_userBanSchema.js.html index 3d4ae40..c33bec5 100644 --- a/www/doc/schemas_user_userBanSchema.js.html +++ b/www/doc/server/schemas_user_userBanSchema.js.html @@ -521,7 +521,7 @@ module.exports = mongoose.model("userBan", userBanSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/schemas_user_userSchema.js.html b/www/doc/server/schemas_user_userSchema.js.html similarity index 99% rename from www/doc/schemas_user_userSchema.js.html rename to www/doc/server/schemas_user_userSchema.js.html index 08d08b8..5b3dce6 100644 --- a/www/doc/schemas_user_userSchema.js.html +++ b/www/doc/server/schemas_user_userSchema.js.html @@ -888,7 +888,7 @@ module.exports.userModel = mongoose.model("user", userSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/scripts/linenumber.js b/www/doc/server/scripts/linenumber.js similarity index 100% rename from www/doc/scripts/linenumber.js rename to www/doc/server/scripts/linenumber.js diff --git a/www/doc/scripts/prettify/Apache-License-2.0.txt b/www/doc/server/scripts/prettify/Apache-License-2.0.txt similarity index 100% rename from www/doc/scripts/prettify/Apache-License-2.0.txt rename to www/doc/server/scripts/prettify/Apache-License-2.0.txt diff --git a/www/doc/scripts/prettify/lang-css.js b/www/doc/server/scripts/prettify/lang-css.js similarity index 100% rename from www/doc/scripts/prettify/lang-css.js rename to www/doc/server/scripts/prettify/lang-css.js diff --git a/www/doc/scripts/prettify/prettify.js b/www/doc/server/scripts/prettify/prettify.js similarity index 100% rename from www/doc/scripts/prettify/prettify.js rename to www/doc/server/scripts/prettify/prettify.js diff --git a/www/doc/styles/jsdoc-default.css b/www/doc/server/styles/jsdoc-default.css similarity index 100% rename from www/doc/styles/jsdoc-default.css rename to www/doc/server/styles/jsdoc-default.css diff --git a/www/doc/styles/prettify-jsdoc.css b/www/doc/server/styles/prettify-jsdoc.css similarity index 100% rename from www/doc/styles/prettify-jsdoc.css rename to www/doc/server/styles/prettify-jsdoc.css diff --git a/www/doc/styles/prettify-tomorrow.css b/www/doc/server/styles/prettify-tomorrow.css similarity index 100% rename from www/doc/styles/prettify-tomorrow.css rename to www/doc/server/styles/prettify-tomorrow.css diff --git a/www/doc/tokebot.html b/www/doc/server/tokebot.html similarity index 88% rename from www/doc/tokebot.html rename to www/doc/server/tokebot.html index 3df574f..f6696dc 100644 --- a/www/doc/tokebot.html +++ b/www/doc/server/tokebot.html @@ -215,134 +215,6 @@ -

Members

- - - -

chatHandler

- - - - -
- Parent chatHandler object -
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - -

server

- - - - -
- Parent channelManager object -
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - -

Methods

@@ -406,7 +278,7 @@ I would now, but I don't want to break shit in a comment-only commit.
Source:
@@ -494,7 +366,7 @@ I would now, but I don't want to break shit in a comment-only commit.
Source:
@@ -582,7 +454,7 @@ I would now, but I don't want to break shit in a comment-only commit.
Source:
@@ -670,7 +542,7 @@ I would now, but I don't want to break shit in a comment-only commit.
Source:
@@ -758,7 +630,7 @@ I would now, but I don't want to break shit in a comment-only commit.
Source:
@@ -895,7 +767,7 @@ I would now, but I don't want to break shit in a comment-only commit.
Source:
@@ -969,7 +841,7 @@ I would now, but I don't want to break shit in a comment-only commit.
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_altchaUtils.js.html b/www/doc/server/utils_altchaUtils.js.html similarity index 99% rename from www/doc/utils_altchaUtils.js.html rename to www/doc/server/utils_altchaUtils.js.html index 90b44bc..3ce1fb9 100644 --- a/www/doc/utils_altchaUtils.js.html +++ b/www/doc/server/utils_altchaUtils.js.html @@ -118,7 +118,7 @@ module.exports.verify = async function(payload, uniqueSecret = ''){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_configCheck.js.html b/www/doc/server/utils_configCheck.js.html similarity index 99% rename from www/doc/utils_configCheck.js.html rename to www/doc/server/utils_configCheck.js.html index 81cc912..0eb55a1 100644 --- a/www/doc/utils_configCheck.js.html +++ b/www/doc/server/utils_configCheck.js.html @@ -108,7 +108,7 @@ module.exports.securityCheck = function(){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_hashUtils.js.html b/www/doc/server/utils_hashUtils.js.html similarity index 99% rename from www/doc/utils_hashUtils.js.html rename to www/doc/server/utils_hashUtils.js.html index cc2e43e..18c927e 100644 --- a/www/doc/utils_hashUtils.js.html +++ b/www/doc/server/utils_hashUtils.js.html @@ -103,7 +103,7 @@ module.exports.hashIP = function(ip){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_linkUtils.js.html b/www/doc/server/utils_linkUtils.js.html similarity index 99% rename from www/doc/utils_linkUtils.js.html rename to www/doc/server/utils_linkUtils.js.html index d1c98ac..3eeb13f 100644 --- a/www/doc/utils_linkUtils.js.html +++ b/www/doc/server/utils_linkUtils.js.html @@ -146,7 +146,7 @@ module.exports.markLink = async function(link){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_loggerUtils.js.html b/www/doc/server/utils_loggerUtils.js.html similarity index 99% rename from www/doc/utils_loggerUtils.js.html rename to www/doc/server/utils_loggerUtils.js.html index 3b30489..233351f 100644 --- a/www/doc/utils_loggerUtils.js.html +++ b/www/doc/server/utils_loggerUtils.js.html @@ -207,7 +207,7 @@ module.exports.errorMiddleware = function(err, req, res, next){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_mailUtils.js.html b/www/doc/server/utils_mailUtils.js.html similarity index 99% rename from www/doc/utils_mailUtils.js.html rename to www/doc/server/utils_mailUtils.js.html index 3310c7d..e5abb54 100644 --- a/www/doc/utils_mailUtils.js.html +++ b/www/doc/server/utils_mailUtils.js.html @@ -140,7 +140,7 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_media_internetArchiveUtils.js.html b/www/doc/server/utils_media_internetArchiveUtils.js.html similarity index 99% rename from www/doc/utils_media_internetArchiveUtils.js.html rename to www/doc/server/utils_media_internetArchiveUtils.js.html index 6e8c8ff..5c28a5d 100644 --- a/www/doc/utils_media_internetArchiveUtils.js.html +++ b/www/doc/server/utils_media_internetArchiveUtils.js.html @@ -154,7 +154,7 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_media_yanker.js.html b/www/doc/server/utils_media_yanker.js.html similarity index 99% rename from www/doc/utils_media_yanker.js.html rename to www/doc/server/utils_media_yanker.js.html index 825203f..e34f4c7 100644 --- a/www/doc/utils_media_yanker.js.html +++ b/www/doc/server/utils_media_yanker.js.html @@ -193,7 +193,7 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_media_ytdlpUtils.js.html b/www/doc/server/utils_media_ytdlpUtils.js.html similarity index 99% rename from www/doc/utils_media_ytdlpUtils.js.html rename to www/doc/server/utils_media_ytdlpUtils.js.html index 9814973..17bb3d2 100644 --- a/www/doc/utils_media_ytdlpUtils.js.html +++ b/www/doc/server/utils_media_ytdlpUtils.js.html @@ -186,7 +186,7 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_regexUtils.js.html b/www/doc/server/utils_regexUtils.js.html similarity index 99% rename from www/doc/utils_regexUtils.js.html rename to www/doc/server/utils_regexUtils.js.html index 505d037..a3db8ae 100644 --- a/www/doc/utils_regexUtils.js.html +++ b/www/doc/server/utils_regexUtils.js.html @@ -69,7 +69,7 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_scheduler.js.html b/www/doc/server/utils_scheduler.js.html similarity index 99% rename from www/doc/utils_scheduler.js.html rename to www/doc/server/utils_scheduler.js.html index 90c8742..7161597 100644 --- a/www/doc/utils_scheduler.js.html +++ b/www/doc/server/utils_scheduler.js.html @@ -105,7 +105,7 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/utils_sessionUtils.js.html b/www/doc/server/utils_sessionUtils.js.html similarity index 99% rename from www/doc/utils_sessionUtils.js.html rename to www/doc/server/utils_sessionUtils.js.html index 55a03eb..05c32c5 100644 --- a/www/doc/utils_sessionUtils.js.html +++ b/www/doc/server/utils_sessionUtils.js.html @@ -236,7 +236,7 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:28:58 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time)
From ac06f839ead44a1b15973201b3f8f718b43eeae0 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 4 Sep 2025 05:45:33 -0400 Subject: [PATCH 078/209] Started work on JSDoc for www/js/channel --- package.json | 2 +- www/doc/client/channel.html | 1263 +++++++++++ www/doc/client/channel.js.html | 298 +++ www/doc/client/commandPreprocessor.html | 1921 +++++++++++++++++ www/doc/client/commandPreprocessor.js.html | 380 ++++ www/doc/client/commandProcessor.html | 430 ++++ .../client/fonts/OpenSans-Bold-webfont.eot | Bin 0 -> 19544 bytes .../client/fonts/OpenSans-Bold-webfont.svg | 1830 ++++++++++++++++ .../client/fonts/OpenSans-Bold-webfont.woff | Bin 0 -> 22432 bytes .../fonts/OpenSans-BoldItalic-webfont.eot | Bin 0 -> 20133 bytes .../fonts/OpenSans-BoldItalic-webfont.svg | 1830 ++++++++++++++++ .../fonts/OpenSans-BoldItalic-webfont.woff | Bin 0 -> 23048 bytes .../client/fonts/OpenSans-Italic-webfont.eot | Bin 0 -> 20265 bytes .../client/fonts/OpenSans-Italic-webfont.svg | 1830 ++++++++++++++++ .../client/fonts/OpenSans-Italic-webfont.woff | Bin 0 -> 23188 bytes .../client/fonts/OpenSans-Light-webfont.eot | Bin 0 -> 19514 bytes .../client/fonts/OpenSans-Light-webfont.svg | 1831 ++++++++++++++++ .../client/fonts/OpenSans-Light-webfont.woff | Bin 0 -> 22248 bytes .../fonts/OpenSans-LightItalic-webfont.eot | Bin 0 -> 20535 bytes .../fonts/OpenSans-LightItalic-webfont.svg | 1835 ++++++++++++++++ .../fonts/OpenSans-LightItalic-webfont.woff | Bin 0 -> 23400 bytes .../client/fonts/OpenSans-Regular-webfont.eot | Bin 0 -> 19836 bytes .../client/fonts/OpenSans-Regular-webfont.svg | 1831 ++++++++++++++++ .../fonts/OpenSans-Regular-webfont.woff | Bin 0 -> 22660 bytes www/doc/client/global.html | 217 ++ www/doc/client/index.html | 96 + www/doc/client/scripts/linenumber.js | 25 + .../scripts/prettify/Apache-License-2.0.txt | 202 ++ www/doc/client/scripts/prettify/lang-css.js | 2 + www/doc/client/scripts/prettify/prettify.js | 28 + www/doc/client/styles/jsdoc-default.css | 358 +++ www/doc/client/styles/prettify-jsdoc.css | 111 + www/doc/client/styles/prettify-tomorrow.css | 132 ++ www/doc/client/userList.html | 1200 ++++++++++ www/doc/client/userlist.js.html | 261 +++ www/doc/server/activeChannel.html | 2 +- .../server/app_channel_activeChannel.js.html | 2 +- .../server/app_channel_channelManager.js.html | 2 +- www/doc/server/app_channel_chat.js.html | 2 +- www/doc/server/app_channel_chatBuffer.js.html | 2 +- .../server/app_channel_chatHandler.js.html | 2 +- .../app_channel_commandPreprocessor.js.html | 2 +- .../server/app_channel_connectedUser.js.html | 2 +- .../server/app_channel_media_media.js.html | 2 +- .../app_channel_media_playlistHandler.js.html | 2 +- .../server/app_channel_media_queue.js.html | 2 +- .../app_channel_media_queuedMedia.js.html | 2 +- www/doc/server/app_channel_tokebot.js.html | 2 +- www/doc/server/channelManager.html | 2 +- www/doc/server/chat.html | 2 +- www/doc/server/chatBuffer.html | 2 +- www/doc/server/chatHandler.html | 2 +- www/doc/server/commandPreprocessor.html | 2 +- www/doc/server/commandProcessor.html | 2 +- www/doc/server/connectedUser.html | 2 +- www/doc/server/global.html | 2 +- www/doc/server/index.html | 2 +- www/doc/server/media.html | 2 +- www/doc/server/playlistHandler.html | 2 +- www/doc/server/queue.html | 2 +- www/doc/server/queuedMedia.html | 2 +- .../schemas_channel_channelBanSchema.js.html | 2 +- ...as_channel_channelPermissionSchema.js.html | 2 +- .../schemas_channel_channelSchema.js.html | 2 +- .../server/schemas_channel_chatSchema.js.html | 2 +- .../schemas_channel_media_mediaSchema.js.html | 2 +- ..._channel_media_playlistMediaSchema.js.html | 2 +- ...hemas_channel_media_playlistSchema.js.html | 2 +- ...as_channel_media_queuedMediaSchema.js.html | 2 +- www/doc/server/schemas_emoteSchema.js.html | 2 +- www/doc/server/schemas_flairSchema.js.html | 2 +- .../server/schemas_permissionSchema.js.html | 2 +- www/doc/server/schemas_statSchema.js.html | 2 +- .../schemas_tokebot_tokeCommandSchema.js.html | 2 +- .../schemas_user_emailChangeSchema.js.html | 2 +- .../schemas_user_passwordResetSchema.js.html | 2 +- .../server/schemas_user_userBanSchema.js.html | 2 +- .../server/schemas_user_userSchema.js.html | 2 +- www/doc/server/tokebot.html | 2 +- www/doc/server/utils_altchaUtils.js.html | 2 +- www/doc/server/utils_configCheck.js.html | 2 +- www/doc/server/utils_hashUtils.js.html | 2 +- www/doc/server/utils_linkUtils.js.html | 2 +- www/doc/server/utils_loggerUtils.js.html | 2 +- www/doc/server/utils_mailUtils.js.html | 2 +- .../utils_media_internetArchiveUtils.js.html | 2 +- www/doc/server/utils_media_yanker.js.html | 2 +- www/doc/server/utils_media_ytdlpUtils.js.html | 2 +- www/doc/server/utils_regexUtils.js.html | 2 +- www/doc/server/utils_scheduler.js.html | 2 +- www/doc/server/utils_sessionUtils.js.html | 2 +- www/js/channel/channel.js | 60 +- www/js/channel/chat.js | 46 +- www/js/channel/commandPreprocessor.js | 87 + www/js/channel/cpanel.js | 224 +- www/js/channel/player.js | 150 +- www/js/channel/userlist.js | 55 +- 97 files changed, 18556 insertions(+), 91 deletions(-) create mode 100644 www/doc/client/channel.html create mode 100644 www/doc/client/channel.js.html create mode 100644 www/doc/client/commandPreprocessor.html create mode 100644 www/doc/client/commandPreprocessor.js.html create mode 100644 www/doc/client/commandProcessor.html create mode 100644 www/doc/client/fonts/OpenSans-Bold-webfont.eot create mode 100644 www/doc/client/fonts/OpenSans-Bold-webfont.svg create mode 100644 www/doc/client/fonts/OpenSans-Bold-webfont.woff create mode 100644 www/doc/client/fonts/OpenSans-BoldItalic-webfont.eot create mode 100644 www/doc/client/fonts/OpenSans-BoldItalic-webfont.svg create mode 100644 www/doc/client/fonts/OpenSans-BoldItalic-webfont.woff create mode 100644 www/doc/client/fonts/OpenSans-Italic-webfont.eot create mode 100644 www/doc/client/fonts/OpenSans-Italic-webfont.svg create mode 100644 www/doc/client/fonts/OpenSans-Italic-webfont.woff create mode 100644 www/doc/client/fonts/OpenSans-Light-webfont.eot create mode 100644 www/doc/client/fonts/OpenSans-Light-webfont.svg create mode 100644 www/doc/client/fonts/OpenSans-Light-webfont.woff create mode 100644 www/doc/client/fonts/OpenSans-LightItalic-webfont.eot create mode 100644 www/doc/client/fonts/OpenSans-LightItalic-webfont.svg create mode 100644 www/doc/client/fonts/OpenSans-LightItalic-webfont.woff create mode 100644 www/doc/client/fonts/OpenSans-Regular-webfont.eot create mode 100644 www/doc/client/fonts/OpenSans-Regular-webfont.svg create mode 100644 www/doc/client/fonts/OpenSans-Regular-webfont.woff create mode 100644 www/doc/client/global.html create mode 100644 www/doc/client/index.html create mode 100644 www/doc/client/scripts/linenumber.js create mode 100644 www/doc/client/scripts/prettify/Apache-License-2.0.txt create mode 100644 www/doc/client/scripts/prettify/lang-css.js create mode 100644 www/doc/client/scripts/prettify/prettify.js create mode 100644 www/doc/client/styles/jsdoc-default.css create mode 100644 www/doc/client/styles/prettify-jsdoc.css create mode 100644 www/doc/client/styles/prettify-tomorrow.css create mode 100644 www/doc/client/userList.html create mode 100644 www/doc/client/userlist.js.html diff --git a/package.json b/package.json index 5c31afb..da43c50 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "start": "node ./src/server.js", "start:dev": "nodemon ./src/server.js", - "build": "node node_modules/jsdoc/jsdoc.js --verbose -r src/ -R README.md -d www/doc/server/ && node node_modules/jsdoc/jsdoc.js --verbose -r www/js/ -r README.md -d www/doc/client/" + "build": "node node_modules/jsdoc/jsdoc.js --verbose -r src/ -R README.md -d www/doc/server/ && node node_modules/jsdoc/jsdoc.js --verbose -r www/js/channel -r README.md -d www/doc/client/" }, "devDependencies": { "nodemon": "^3.1.10", diff --git a/www/doc/client/channel.html b/www/doc/client/channel.html new file mode 100644 index 0000000..ca741ff --- /dev/null +++ b/www/doc/client/channel.html @@ -0,0 +1,1263 @@ + + + + + JSDoc: Class: channel + + + + + + + + + + +
+ +

Class: channel

+ + + + + + +
+ +
+ +

channel()

+ +
Class for object containing base code for the Canopy channel client.
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new channel()

+ + + + + + +
+ Instantiates a new channel object +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

cPanel

+ + + + +
+ Child Canopy Panel Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

channelName

+ + + + +
+ Current connected channels name +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

chatBox

+ + + + +
+ Child Chat Box Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

defaultConfig

+ + + + +
+ Default channel config +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Child Video Player object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

userList

+ + + + +
+ Child User List Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

ytEmbedAPILoaded

+ + + + +
+ Returns true once the ytEmbed API has loaded in from google (eww) +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

connect()

+ + + + + + +
+ Handles initial client connection +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines network-related listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleClientInfo(data)

+ + + + + + +
+ Handles initial client-metadata ingestion from server upon connection +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Data glob from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processConfig(key, value)

+ + + + + + +
+ Run once every config change to ensure settings are properly set +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +String + + + + Setting to change
value + + +* + + + + Value to set setting to
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setDefaults(force, processConfig)

+ + + + + + +
+ Processes and applies default config on any unset settings +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
force + + +Boolean + + + + + + false + + Whether or not to forcefully reset already set settings
processConfig + + +Boolean + + + + + + false + + Whether or not to run the Process Config function once complete
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/channel.js.html b/www/doc/client/channel.js.html new file mode 100644 index 0000000..995b6e5 --- /dev/null +++ b/www/doc/client/channel.js.html @@ -0,0 +1,298 @@ + + + + + JSDoc: Source: channel.js + + + + + + + + + + +
+ +

Source: channel.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class for object containing base code for the Canopy channel client.
+ */
+class channel{
+    /**
+     * Instantiates a new channel object
+     */
+    constructor(){
+        //Establish connetion to the server via socket.io
+        this.connect();
+        //Define socket listeners
+        this.defineListeners();
+
+        /**
+         * Returns true once the ytEmbed API has loaded in from google (eww)
+         */
+        this.ytEmbedAPILoaded = false;
+
+        /**
+         * Current connected channels name
+         */
+        this.channelName = window.location.pathname.split('/c/')[1].split('/')[0];
+
+        /**
+         * Child Video Player object
+         */
+        this.player = new player(this);
+
+        /**
+         * Child Chat Box Object
+         */
+        this.chatBox = new chatBox(this);
+
+        /**
+         * Child User List Object
+         */
+        this.userList = new userList(this);
+        
+        /**
+         * Child Canopy Panel Object
+         */
+        this.cPanel = new cPanel(this);
+
+        //Set defaults for any unset settings and run any required process steps for the current config
+        this.setDefaults(false, true);
+
+        //Freak out any weirdos who take a peek in the dev console for shits n gigs
+        console.log("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇.");
+    }
+
+    /**
+     * Handles initial client connection
+     */
+    connect(){
+        this.socket = io({
+            extraHeaders: {
+                //Include CSRF token
+                'x-csrf-token': utils.ajax.getCSRFToken()
+            }
+        });
+    }
+
+    /**
+     * Defines network-related listeners
+     */
+    defineListeners(){
+        this.socket.on("connect", () => {
+            document.title = `${this.channelName} - Connected`
+        });
+
+        this.socket.on("kick", async (data) => {
+            if(data.reason == "Invalid CSRF Token!"){
+                //Reload the CSRF token
+                await utils.ajax.reloadCSRFToken();
+
+                //Retry the connection
+                this.connect();
+            }else{
+                new canopyUXUtils.popup(`You have been ${data.type} from the channel for the following reason:<br>${data.reason}`);
+            }
+        });
+
+        this.socket.on("clientMetadata", this.handleClientInfo.bind(this));
+
+        this.socket.on("error", utils.ux.displayResponseError);
+
+        this.socket.on("queue", (data) => {
+            this.queue = new Map(data.queue);
+        });
+
+        this.socket.on("lock", (data) => {
+            this.queueLock = data.locked;
+        });
+    }
+
+    /**
+     * Handles initial client-metadata ingestion from server upon connection
+     * @param {Object} data - Data glob from server
+     */
+    handleClientInfo(data){
+        //Ingest user data
+        this.user = data.user;
+
+        //Re-hydrate permission maps
+        this.user.permMap.site = new Map(data.user.permMap.site);
+        this.user.permMap.chan = new Map(data.user.permMap.chan);
+
+        //Tell the chatbox to handle client info 
+        //should it have its own event listener instead? Guess it's a stylistic choice :P
+        this.chatBox.handleClientInfo(data);
+
+        //Store queue for use by the queue panel
+        this.queue = new Map(data.queue);
+
+        //Store queue lock status
+        this.queueLock = data.queueLock;
+
+        //For each chat held in the chat buffer
+        for(let chat of data.chatBuffer){
+            //Display the chat
+            this.chatBox.displayChat(chat);
+        }
+    }
+
+    /**
+     * Processes and applies default config on any unset settings
+     * @param {Boolean} force - Whether or not to forcefully reset already set settings
+     * @param {Boolean} processConfig - Whether or not to run the Process Config function once complete
+     */
+    setDefaults(force = false, processConfig = false){
+        //Iterate through default config 
+        for(let [key, value] of channel.defaultConfig){
+            //If the setting is unset or function was called forcefully
+            if(force || localStorage.getItem(key) == null){
+                //Set item from default map
+                localStorage.setItem(key, value);
+            }
+
+            //If we're running process steps for the config
+            if(processConfig){
+                //Process the current config value
+                this.processConfig(key, localStorage.getItem(key));
+            }
+        }
+    }
+
+    /**
+     * Run once every config change to ensure settings are properly set
+     * @param {String} key - Setting to change
+     * @param {*} value - Value to set setting to
+     */
+    processConfig(key, value){
+        //Switch/case by config key
+        switch(key){
+            case 'ytPlayerType':
+                const embedScript = document.querySelector(".yt-embed-api");
+                //If the user is running the embedded player and we don't have en embed script loaded
+                if(value == 'embed' && embedScript == null){
+                    //Find our footer
+                    const footer = document.querySelector('footer');
+
+                    //Create new script tag
+                    const ytEmbedAPI = document.createElement('script');
+                    //Link the iframe api from youtube
+                    ytEmbedAPI.src = "https://www.youtube.com/iframe_api";
+                    //set the iframe api script id
+                    ytEmbedAPI.classList.add('yt-embed-api');
+
+                    //Append the script tag to the top of the footer to give everything else access
+                    footer.prepend(ytEmbedAPI);
+                //If we're not using the embed player but the script is loaded
+                }else if(embedScript != null){
+                    //Pull all scripts since the main one might have pulled others
+                    const scripts = document.querySelectorAll('script');
+
+                    //Iterate through all script tags on the page
+                    for(let script of scripts){
+                        //If the script came from youtube
+                        if(script.src.match(/youtube\.com|youtu\.be/)){
+                            //Rip it out
+                            script.remove();
+                        }
+                    }
+                }
+
+                //If the player or mediaHandler isn't loaded
+                if(this.player == null || this.player.mediaHandler == null){
+                    //We're fuggin done here
+                    return;
+                }
+
+
+                //Get current video
+                const nowPlaying = this.player.mediaHandler.nowPlaying;
+
+                //If we're playing a youtube video
+                if(nowPlaying != null && nowPlaying.type == 'yt'){
+                    //Restart the video
+                    this.player.start({media: nowPlaying});
+                }
+
+                //Stop while we're ahead
+                return;
+        }
+    }
+
+    /**
+     * Default channel config
+     */
+    static defaultConfig = new Map([
+        ["ytPlayerType","raw"]
+    ]);
+}
+
+/**
+ * Youtube iframe-embed API entry point
+ */
+function onYouTubeIframeAPIReady(){
+    //Set embed api to true
+    client.ytEmbedAPILoaded = true;
+
+    //Get currently playing item
+    const nowPlaying = client.player.mediaHandler.nowPlaying;
+
+    //If we're playing a youtube video and the official embeds are enabled
+    if(nowPlaying.type == 'yt' && localStorage.getItem('ytPlayerType') == "embed"){
+        //Restart the video now that the embed api has loaded
+        client.player.start({media: nowPlaying});
+    }
+}
+
+const client = new channel();
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/commandPreprocessor.html b/www/doc/client/commandPreprocessor.html new file mode 100644 index 0000000..21290cc --- /dev/null +++ b/www/doc/client/commandPreprocessor.html @@ -0,0 +1,1921 @@ + + + + + JSDoc: Class: commandPreprocessor + + + + + + + + + + +
+ +

Class: commandPreprocessor

+ + + + + + +
+ +
+ +

commandPreprocessor(client)

+ +
Class for object containing chat and command pre-processing logic
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new commandPreprocessor(client)

+ + + + + + +
+ Instantiates a new commandPreprocessor object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client mgmt object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

commandProcessor

+ + + + +
+ Child Command Processor object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

emotes

+ + + + +
+ Set of arrays containing site-wide, channel-wide, and user-specific emotes +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildAutocompleteDictionary() → {Object}

+ + + + + + +
+ Generates auto-complete dictionary from pre-written commands, emotes, and used tokes from servers for use with autocomplete +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Generated Dictionary object +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines Network-Related Listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Fetches emote by link +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
link + + +String + + + + Link to fetch emote with
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ found emote +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

getEmoteNames() → {Array}

+ + + + + + +
+ Generates flat list of emote names +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ List of strings containing emote names +
+ + + +
+
+ Type +
+
+ +Array + + +
+
+ + + + + + + + + + + + + +

preprocess(command)

+ + + + + + +
+ Pre-Processes a single chat/command before sending it off to the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
command + + +String + + + + Chat/Command to pre-process
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processEmotes()

+ + + + + + +
+ Processes emotes refrences in loaded message into links to be further processed by processLinks() +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Processes links into numbered file seperators, putting links into a dedicated array. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processLocalCommand()

+ + + + + + +
+ Processes local commands, starting with '/' +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sendRemoteCommand()

+ + + + + + +
+ Transmits message/command off to server +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setChanEmotes(data)

+ + + + + + +
+ Sets channel emotes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Emote data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPersonalEmotes(data)

+ + + + + + +
+ Sets personal emotes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Emote data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setSiteEmotes(data)

+ + + + + + +
+ Sets site emotes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Emote data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setUsedTokes(data)

+ + + + + + +
+ Sets used tokes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Used toke data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/commandPreprocessor.js.html b/www/doc/client/commandPreprocessor.js.html new file mode 100644 index 0000000..d2220c7 --- /dev/null +++ b/www/doc/client/commandPreprocessor.js.html @@ -0,0 +1,380 @@ + + + + + JSDoc: Source: commandPreprocessor.js + + + + + + + + + + +
+ +

Source: commandPreprocessor.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class for object containing chat and command pre-processing logic
+ */
+class commandPreprocessor{
+    /**
+     * Instantiates a new commandPreprocessor object
+     * @param {channel} client - Parent client mgmt object
+     */
+    constructor(client){
+        /**
+         * Parent Client Management object
+         */
+        this.client = client;
+
+        /**
+         * Child Command Processor object
+         */
+        this.commandProcessor = new commandProcessor(client);
+
+        /**
+         * Set of arrays containing site-wide, channel-wide, and user-specific emotes
+         */
+        this.emotes = {
+            site: [],
+            chan: [],
+            personal: []
+        }
+
+        //define listeners
+        this.defineListeners();
+    }
+
+    /**
+     * Defines Network-Related Listeners
+     */
+    defineListeners(){
+        //When we receive site-wide emote list
+        this.client.socket.on("siteEmotes", this.setSiteEmotes.bind(this));
+        this.client.socket.on("chanEmotes", this.setChanEmotes.bind(this));
+        this.client.socket.on("personalEmotes", this.setPersonalEmotes.bind(this));
+        this.client.socket.on("usedTokes", this.setUsedTokes.bind(this));
+    }
+
+    /**
+     * Pre-Processes a single chat/command before sending it off to the server
+     * @param {String} command - Chat/Command to pre-process
+     */
+    preprocess(command){
+        //Set command and sendFlag
+        this.command = command;
+        this.sendFlag = true;
+
+        //Attempt to process as local command
+        this.processLocalCommand();
+
+        //If we made it through the local command processor
+        if(this.sendFlag){
+            //Set the message to the command
+            this.message = command;
+            //Process message emotes into links
+            this.processEmotes();
+            //Process unmarked links into marked links
+            this.processLinks();
+            //Send command off to server
+            this.sendRemoteCommand();
+        }
+    }
+
+    /**
+     * Processes local commands, starting with '/'
+     */
+    processLocalCommand(){
+        //Create an empty array to hold the command
+        this.commandArray = [];
+        //Split string by words
+        this.commandArray = this.command.split(/\b/g);//Split by word-borders
+        this.argumentArray = this.command.match(/\b\w+\b/g);//Match by words surrounded by borders
+
+        //If this is a local command
+        if(this.commandArray[0] == '/'){
+            //If the command exists
+            if(this.argumentArray != null && this.commandProcessor[this.argumentArray[0].toLowerCase()] != null){
+                //Don't send it to the server
+                this.sendFlag = false;
+
+                //Call the command with the argument array
+                this.commandProcessor[this.argumentArray[0].toLowerCase()](this.argumentArray, this.commandArray);
+            }
+        }
+    }
+
+    /**
+     * Processes emotes refrences in loaded message into links to be further processed by processLinks()
+     */
+    processEmotes(){
+        //inject invisible whitespace in-between emotes to prevent from mushing links together
+        this.message = this.message.replaceAll('][',']ㅤ[');
+
+        //For each list of emotes
+        Object.keys(this.emotes).forEach((key) => {
+            //For each emote in the current list
+            this.emotes[key].forEach((emote) => {
+                //Inject emote links into the message, pad with invisible whitespace to keep link from getting mushed
+                this.message = this.message.replaceAll(`[${emote.name}]`, `ㅤ${emote.link}ㅤ`);
+            });
+        });
+    }
+
+    /**
+     * Processes links into numbered file seperators, putting links into a dedicated array.
+     */
+    processLinks(){
+        //Strip out file seperators in-case the user is being a smart-ass
+        this.message = this.message.replaceAll('␜','');
+        //Split message by links
+        var splitMessage = this.message.split(/(https?:\/\/[^\sㅤ]+)/g);
+        //Create an empty array to hold links
+        this.links = [];
+
+        splitMessage.forEach((chunk, chunkIndex) => {
+            //For each chunk that is a link
+            if(chunk.match(/(https?:\/\/[^\sㅤ]+)/g)){
+                //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}`
+
+                //push current chunk as link
+                this.links.push(chunk);
+            }
+        });
+
+        //Join the message back together
+        this.message = splitMessage.join('');
+    }
+
+    /**
+     * Transmits message/command off to server
+     */
+    sendRemoteCommand(){
+        this.client.socket.emit("chatMessage",{msg: this.message, links: this.links});
+    }
+
+    /**
+     * Sets site emotes
+     * @param {Object} data - Emote data from server
+     */
+    setSiteEmotes(data){
+        this.emotes.site = data;
+    }
+
+    /**
+     * Sets channel emotes
+     * @param {Object} data - Emote data from server
+     */
+    setChanEmotes(data){
+        this.emotes.chan = data;
+    }
+
+    /**
+     * Sets personal emotes
+     * @param {Object} data - Emote data from server
+     */
+    setPersonalEmotes(data){
+        this.emotes.personal = data;
+    }
+
+    /**
+     * Sets used tokes
+     * @param {Object} data - Used toke data from server
+     */
+    setUsedTokes(data){
+        this.usedTokes = data.tokes;
+    }
+
+    /**
+     * Fetches emote by link
+     * @param {String} link - Link to fetch emote with
+     * @returns {Object} found emote
+     */
+    getEmoteByLink(link){
+        //Create an empty variable to hold the found emote
+        var foundEmote = null;
+
+        //For each list of emotes
+        Object.keys(this.emotes).forEach((key) => {
+            //For each emote in the current list
+            this.emotes[key].forEach((emote) => {
+                //if we found a match
+                if(emote.link == link){
+                    //return the match
+                    foundEmote = emote;
+                }
+            });
+        });
+
+        return foundEmote;
+    }
+
+    /**
+     * Generates flat list of emote names
+     * @returns {Array} List of strings containing emote names
+     */
+    getEmoteNames(){
+        //Create an empty array to hold names
+        let names = [];
+
+        //For every set of emotes
+        for(let set of Object.keys(this.emotes)){
+            //for every emote in the current set of emotes
+            for(let emote of this.emotes[set]){
+                //push the name of the emote to the name list
+                names.push(emote.name);
+            }
+        }
+
+        //return our list of names
+        return names;
+    }
+
+    /**
+     * Generates auto-complete dictionary from pre-written commands, emotes, and used tokes from servers for use with autocomplete
+     * @returns {Object} Generated Dictionary object
+     */
+    buildAutocompleteDictionary(){
+        let dictionary = {
+            tokes: {
+                prefix: '!',
+                postfix: '',
+                cmds: [
+                    ['toke', true]
+                ].concat(injectPerms(this.usedTokes))
+            },
+            //Make sure to add spaces at the end for commands that take arguments
+            //Not necissary but definitely nice to have
+            serverCMD: {
+                prefix: '!',
+                postfix: '',
+                cmds: [
+                    ["whisper ", true],
+                    ["announce ", client.user.permMap.chan.get('announce')],
+                    ["serverannounce ", client.user.permMap.site.get('announce')],
+                    ["clear ", client.user.permMap.chan.get('clearChat')],
+                    ["kick ", client.user.permMap.chan.get('kickUser')],
+                ]
+            },
+            localCMD:{
+                prefix: '/',
+                postfix: '',
+                cmds: [
+                    ["high ", true]
+                ]
+            },
+            usernames:{
+                prefix: '',
+                postfix: '',
+                cmds: injectPerms(Array.from(client.userList.colorMap.keys()))
+            },
+            emotes:{
+                prefix:'[',
+                postfix:']',
+                cmds: injectPerms(this.getEmoteNames())
+            }
+        }; 
+
+        //return our dictionary object
+        return dictionary;
+    
+        function injectPerms(cmds, perm = true){
+            //Create empty array to hold cmds
+            let cmdSet = [];
+
+            //For each cmd
+            for(let cmd of cmds){
+                //Add the cmd with its perm to the cmdset
+                cmdSet.push([cmd, perm]);
+            }
+
+            //return the cmd set
+            return cmdSet;
+        }
+    }
+
+}
+
+/**
+ * Class for Object which contains logic for client-side commands
+ */
+class commandProcessor{
+    /**
+     * Instantiates a new Command Processor object
+     * @param {channel} client - Parent client mgmt object
+     */
+    constructor(client){
+        /**
+         * Parent Client Management object
+         */
+        this.client = client
+    }
+
+    /**
+     * Method handling /high client command
+     * @param {Array} argumentArray - Array of arguments passed down from Command Pre-Processor
+     */
+    high(argumentArray){
+        //If we have an argument
+        if(argumentArray[1]){
+            //Use it to set our high level
+            //Technically this is less of a local command than it would be if it where telling the select to do this
+            //but TTN used to treat this as a local command so fuck it
+            this.client.socket.emit("setHighLevel", {highLevel: argumentArray[1]}); 
+        }
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/commandProcessor.html b/www/doc/client/commandProcessor.html new file mode 100644 index 0000000..d4960e4 --- /dev/null +++ b/www/doc/client/commandProcessor.html @@ -0,0 +1,430 @@ + + + + + JSDoc: Class: commandProcessor + + + + + + + + + + +
+ +

Class: commandProcessor

+ + + + + + +
+ +
+ +

commandProcessor(client)

+ +
Class for Object which contains logic for client-side commands
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new commandProcessor(client)

+ + + + + + +
+ Instantiates a new Command Processor object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client mgmt object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

high(argumentArray)

+ + + + + + +
+ Method handling /high client command +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
argumentArray + + +Array + + + + Array of arguments passed down from Command Pre-Processor
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-Bold-webfont.eot b/www/doc/client/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..5d20d916338a5890a033952e2e07ba7380f5a7d3 GIT binary patch literal 19544 zcmZsBRZtvE7wqD@i!HFY1b24`kj35I-CYBL;O-Dy7Y*)i!Ciy9OMu`K2ubeuzujAP z&(u^;b@!=xJ5w`f^ppUAR7C&)@xOr#_z%&6s7NTth=|AtfF4A^f1HxqH6mcokP-l6 z{7?U16e0j9|A(M9nJ@pt|2J>}ssJ~DHNfRRlP19YKlJ?100c+?Tmeo1tN+$S0Gx`?s1CFN7eMUDk_WsHBTfGwNlSoSO;j5Y2+U^b7c?fa0Y^S_)w3$t3v&# z{~&TTlM zt?Lt*SHuem8SrEC@7zaU<-qSuQW-60?>}hkJOK8c63ZzHHJk8oZ^lJI@4J}J-UW#v z``};wWo2yOy5j-i>^G*aArwT)Vs*SHt6!%SuA2O<_J=(LpNDHvxaKhxXh#=~9&&Ym z(3h3}YEDIOIJiClxPx>szhB_|HF$A3M_(n`EZ{OfeopPhu5a!iV`!-MGz%=Z=6_KhH^># zc0eZ(i}Fam9zt=@^nI}P1TS0OA-NjllZr>npsHhjY^(twm8{D3gzMI3wz*wpNrf_@ z*a?QZ6Zge*92n!$$Tj4PYIXRs9DZwFAPAN5P1wKY;CH_ec^<;uNX&@i#260}94dT^ zt<=Np#*{u2jSWT-*MlH7@a5$;Wa{AyjRD3+-J*f z6&WMZwq>z5b$RG4+v&bc?4gk|zg$9}VoVrJ;Y}$~Y0v{16FHY4IxFkRaW%N-2|Ez= z_qUxB0-(|bh+%0a;3Ta?`XQ4zkOvWpkM=>=!Ky%oa>mUWp zD$PDk^y_cvj^9Y{zV+u>JQ0cidbEQJqsLJULLuYmMt{g`2A(e4Jx<)36FnSe9e>oE zxzOk@q#7!!I{#p>ubQPjK^X81+Uk6pgDIe@S%bvBM{r0gP<&p2HpJ{Dw?tBkQcYmf z)epzhSW{ofDYZ3@A~&Vc)p5lIB(G1Z(li%c#2C<(XdagusQ++&BM8?0j@5^olZU_% z=m7z5F=9%B3}Q*r?Z~~~QTicWnWMz%)ac2D(&K?a;ZmiIghUkmX^}3?DlhKXR*uytr?z?QgE=}; zOa!lz=(^W8!o_2yeZanFSf4l&pD~$9%qw3~q-JTwS{q=h8Z&*)#=pau`crUY8{{Xe zbG(-h4xKWAgfOI21Y+*SHvt*(jZOiBe~sW$i5tg5gJmQj!DRql3=`3nCTPe<85)Wv zDNcRZs>LpDMFIfBrMTi`Q=*uwc+(sNa(GH4V2;xllPE^eRd>%>?~<(DMkaHf*T4XQ z+U1nL|7aS>kOnGROHo}SZGERinov(cPMN+*C&qAc;KcZoErZ@htW9oyc8;-|!FrJq zWzc0=Z%7ImftY2Q1-AIz!2659@GzAk9Jg;F=}^jfq7YR0o}=6_?iu=(#FW0B7rvDm zn1c)hm^PqMaV$*U;T1f3Mq+R(f~gewI%O_(HCtJrr?aR}fm z^A5Nj&5bCD$&Zf4xcV+~Qxl;W7z!#yKm?fy{LsOD_z)&hz#E*1kcMLh{L3Pv46?s4 zdU|hZ!MYD2kv5!^pxI+?dVB71MvQ>)UiEJ@W37&wY1Frz(*jm6 zk|~Vew*ICqWr+{TfI1k%y(OI(S@~Ybjw34_tN3CkER8Wz-_7e@GSF5bBv56k)#w>4 zBJ&uc1o(x~|0<=JLj1+p9|#)e_9d6LEKN9K6?7Zwu+&cA2(Tf`G1&JnTKK;q|8>j2ztI4Bd}xKh$Ra!yFi$u>QQy2jhQuk%;V z8agmZLNW??oDq5&mtPbcc$hRlu<_ThWmGOqdt~T%1iy#AFDP1tgms>gw;8T?hb`>- zpN@N7#D#?I|Gg50kkVY{;9rb?KBbHtYoEAIxuhIL7e2Bsk5YeGX)!~AZ%NT z@&|>qOb$uDe$|(76~Ihc3bzsC+AjB$L*`YX<|&XOMtpbN4l0ut6#XN*X#vhU z+W6Gx3F=~fCf?=t_d~;Bdeqnz%~sZ;ekDKz4XwxFBddSrhzj3j1Jx`IIUD7y7M8-- z-9-|ccrC_9J}BI}K~etcC?%Lm7$E;WF#P(W9Zi2^2NJL14lA!Nnqs0@Ne^Y`t~emz zB2hvC!<7eO00Y@WTsb!3As(&f{2(ZZ5D=lqP_1J+;AFv#Xh&%UU^zhl(yskwZrrh+ z1Y!^Hp|{%zjqwuA`_$m);XzPJsr7e&oK+bW75~_?>-XkyGpurn*Ov-WXDxIF!;6a; zY-Rzp;&@DcWDuKI8W;90BZ=z^)~PWz?xdLaj?*X-U(m)W#`J;5_wz@sJtx``4)rL# zL&rY@x9GxIjC9gy0kve>w+5W);Q6CV7Fe>C&Xpu}y9Vz@x$_sEZSnSMr{M^gjfYei z4Lb-Z)j=!#Gdf15PpC8HP@nD~7jq9rpMR!R$FWbTnm&Qw| zBL@G`s*^SEq1DA>ns}cS_A&ZUva;SsX0Hy-uYli3k!hLB%m zorJ;k*m^ztGZh7lwDzBDWXH%&iJy8N%c}9$Kil z;I*C{Av2(ZOxfmo$P>uLtJg3|rJM=4da4&75^UCP4-RVvUM)jo-EI(FpHS*$V2U_@ zr`a0Xa*AQj!lE&v6M^TzPTem1DF8pYve zy>^orHFfarN*2R6;&Fl%pvuE%oo3g+v6L!wT+_d;>E7j8ep)$;7iBcIV#$v7gNOS; z!!V4jg30}|4l4jhf=N++7>kqop0bhFx0qJGFqto$2hsOAgXajjDV$l-1vOtt9z7pD z%UR9KT1HC2Xmv%LNiBW**YOQjYJZ**N4u*X|5;J1qjZ@M+O`0X*B#EL?%oV z=<4VYw>B%iK*J{E7=*En`lt!SIyyQocG0XUYRk?Sz#;>+MZmyHD}tFtVPj#OXgl432N05e@4`#Pra z7?)%r5rWZ3n@CmbgiK6azZ~#lSx9lkC(-B%dM?liI&R@-{N??}2=t;5D=kOdM{!Ys z;E(^B(6?fpxblMb-ePZ^Ow@4aaA*Ym+eU-B*OfnZj0KGOJhNU&sb;FwWe$wm=$AU+ zeIQHU7^-f8)Nrlyma2pcxs!K}!%1(11a1&DM&{SRI=zhLzqA-MW5g_rSOI!PeTCSB1V@ ze5`RMw(u1EoNxZf6c!%RlwjE+{w4agvwuZ!%)ZWe;m_>=FkC|uH+n9I5! zBObd>e}@6L>RXGvvNaHa7;_ymEU`+rJ7$n8uz$nuHC%YBB+nz}L9j^$A6#cwG!Fia zKgt)k+#A#80|9m(b!qE5iKFniV`82mQnwE=i46L{EE$C63p@ z1&V@Og*CSVFU^D_aAJp({4FeasEPR_ZU+MM*4+HagyvFnm8=*2aiWqG(kq^i6y9 zK9o~%mqLo^jdN0`4SDyMRQ+DizvAXDkH%SC1`{v-_^G*tU;#v3ZzUaPdQs|bqB}yi zFBYhuG}IG1{F?bu=BMR-nlmWhZ(jG}G6w^ejf+{OjANnCgJtiU7g8z$A!{$2Q60>_*AY^h^%3 zet=#D#2HqPia@kP1azEQ6PQ*BtH<5*9)o*`D7uNpNXqG_G@65yccncDNR&wvq8^T# zbQn<%?0SRg{$#fFGOA(3DqNG4=^UNn4WvpuT>E&R0QarW;0ld z$|U|uy2YYF`A`r<+ig8f_MUr)mh_MG3QLNODZrpY{AbgZ>)7C-Qu2~r9Ih)Ov+!Ia zuE#Y3aWo~S+;9aKW!Xcy{=XkxCeG%W`xvb6(Dm5E8z~!?a&*Yh*y77RvFe`kZcPfF z5z@rD$JQ&M#t(zX_-ya&iKs&BX~pSUkafVww)ym{?ig;xT{7ucGXy;6LXi2M*wJVW zhnO6L7JJ6TrRJf4oy+sFdw0$X?PmDUo4`R_;n_C4dS2~k%I4xEBMXN}cH?$9b_G5D zR4nV7LJMc?koICX{)5|5m=9>5{v#@_p58o-OeLsy6U6m5Rtc_7TYr|Ug)O#X-UGq@ zBvRTOiWMD$f+5Rfn#gFp!P>&0zaVyn|7`@7K;XDu{r z5#ymDq$&2BeA)XU2Qr$2+8S*NE0&9u2TvtBWA2I)ZhFPvUCbbzA|7qMzy9arvdZEP zzrIhYUFFJ3E_OGqe1(-MZs$YF{-tCA+c-=y_)w&z*bhY*8uETY*uRjts_e*Zm> z#X4q!T|V}5Rx<7LGq}QtCr;m4r$n8BtY3l=WqWOeq#82!twIBu)sWGLL^)3(&cjGM zUwfS&mh>T^!-F(kP_TI16N%k=A(^2bD)?9BH^g>TBRZ%+9*7-^f}R8UDofvwlsOr2 z#6(Gco__DIrTU8}>`=00_)gU5T8&haeZDXn86`otY)G&Vk(KLdt-#)_QkDl^$F-EA zfYe}zpa}86yJL#%gKaEj;&N2d|9AamL$8r5VM?$j!q^9ws4Q~j5fB^(X)xXpBPZpb zZQ zpO=8PS-{sKI;g}8ml2+lFmx<-I2PuOjDh%x;|M%1!PTw&^*n-eArC>mdGFPz!S&By z#=SiyQ$uF-(_D|80kf??b5#a5G;1~le8{Zv4&w&U3RqXZ9^h1>7DGPmfzjVy*m5!` zaD}I`Ow_{DE)twMGqD#tqf7LvO>`{gO=&1s6T7xE7B*om)eshq{JM*5u*L9a1aPpo z=+epa^`tIb%9Ew@A?QA3uJS$ZO75hy$I2sC@CIsiCUa%guB=h?l1+u;px_cgd3I^+ z9&WN@a8qCW#PAR80=!-D9X%rSoBLUX{%66>d?hDa`E`jjPw$uiq(&5bR(sVfMV8mGIBKX-)TfR_(3b9gX70B zNaSCKW_e}3Xypy7H`NccT{m~yeH-?F`qDIan#6ou5=``K5mra)aRGdhwUg*$Q~$d6 zD5FQRL0tn$q~tL}%nZEGj~cnGOJ89eW5t}> z@0A6;=QNnj_uUjxFXkL8SH%{PsavXCG>sX_-_wpOJx|IE=DUO&OQhb$n_H3rR0`BIukhCmxU^YjqQ`Q`RNf*DnAb0^=-uVUKg(fxVB1W7i3 zNXx*3IxRTVOhXspC7V|;(HpL4ju6c)+d2S$!a^3709WB84fUhL`{U13IEzpZgG%GOE>27OZH9Zx;8v10YJS_PuMP-SSy z@hb8;mB>V22sgWaE>r)ck|QLG8%qS#e&mh|a|Xv(&yWnXQTd4OgM)st6xkUhOpXmk zIe}ThDr(&LK>v>e;?ymsWQ2Js82J;(i&P7AX1+iKP*ufIY_zPy+_X%clOY$rG8K}3 zITj1C{lni?LHp=6TFfxJVJ#nNuby~c?_SbC>-q*c?5sIsTr&K|YtzAn)e^k%uXva@%|y7dICt9o$5nk($aa){E^) z%D(=0GY9d_&W-Q~yr1u|D4zoDkn*LBJ)7~@c%m}7SA~VbFzpI4^(@_jfLcc~gq7ZJ zi=pxzEzu0_Nhy@gIls@Y);UMB1OVHSwxm3&4U~{93qXW#v8)8;BjvXU1U{82xLl7N ze&kF|a}(a|UP3%rn~Kq;j30Gtw@^9NcMott3sv zS4~$V9oEy>lXPO*9$Qxwa!WCC4Wz>>p{kBJB-=BP@=-)Trv*vO9pe05&$S1lfPyGB zfb^eW)|RXG7z$2DdhGX3-!wPr826oG29$3&X$!0|jzTB`ii(E|0Zix`E&u*neyI9B zU5U1&I&fbpb}j>G0+ikqtK-~LlBn=ubci}C7*^kUez`*jPV5Ehzi?Z(&c#Y-X z&j1%Rmi_#T)|_vde52V!D51BdYuFVW2Xw4_HbMI>9q&ilzD)qt#*aOR^9;c9ufEq- zLNzyh8iO`BQCT*~rt>|GkO?gb(FA&uK(Kp7oQX~LLkDg{*XlwxmcU#Jb=EA}F$h-EvIyzO76 zjmLNnr&RR1XDGG7Z6+l&zc98A$pp)t<%#_Jgj`+LD5;WZ|2$Lksy0G?#24YMQX@Q% z8ahfr!cFn-Bd|3Yi3-u5CP8zJztxw^y0B8D@$YW%CnPmo_cocpe`fSZ8?H)plyFu4 z$W-Pz^PpyKH12~w33&kvo@GS}m_F5rfB8vBKk>kWSkr5gAC6WO^GH@jd7J!LRA1h8 z-PBMx>plM3hBZJfJKCgYAAoGu?|$XyeGMN>A&Zh&}7?JTI2?-MF1MTMivF#oKx z9#C-EDIlZ)_JsWLpqzC^+Uxb| zk2*~=5SW;gKG^aMy-)RTvShQ9e3#QonW+-5k-#GpeS7P}#OKASEJ{K0?LxQX3B5(s zCah5;$LH4{tR+{}@KuMa>$dUL9~xdv+j*$C7B4nsiX>KV)(5j7XM($`1K<}Tur5l> zn4y&dREx5rDQ0@ot6SKAv*C5&>c^DsumrXf1w`H3gaXH5jOMazHhIBdFrquOtHJIc zV>ubojQKtF4vXjyfx>+by#l%^_y|BR%8#;Fcv8L~2J2SfHZ+IccP2$4WaSUV9j=ny zXtD1AgvTn#>#(Ng=cSb2C(OQ7OU6#3hmC+-6*@(~YA(`O^w@~qk96WW#6fP6YeXW%#x>EBL>LX8mbVL*)cLcGYoWIxZ?T{nFH1I}u)u-elaKU^Y3T z%;Ft&iF|Yxg9E^E_h&u+81*x7LrCZ!edSV_0?lXEArHXMKb3nB?+v67oCLqLNjiPE zI|ZbfNEj$#VA5jhCKkO&wO=4_EAsJ5Z>*ANyds+#=u>L-ysutu!`&ro&Qf3>1X$H^ z;Z*?=4w#`xXATFp3lPv!ocA4{p9b(AS#TlT70PSlT1v)-dCOw-i*z<{y!am^=aT8e#k)=Um2u*1%^ zpu{A&EK!(#qWH$qqlN}LSs`4&&27+MRTLMkJf$<(RLq5f=H73q!- z36EksF&O3<+8Q-*lhG6#mxko5sGHPet|EKcC6+5074 zMNgbI$-rcOxp|OsEAsnHc=v^&SgFyjL-VLGHF^>oa~CN5r`nRm{jWmV6*xn`Z}rGB z_G#!x6}2Q@_F6~xhZ=pX3_U#0hC)d`A``H`E!`>x?#de8ld;Hrlb{6Zz z9Ml2%p-ctIF5+n^ek58Um*N)G+x6>E2fQIwZ~$bAISo3tY<6j(OoQcV{w8N7JpQR}h2|iw)$tMk0rdyZb=HD0IQD zj#pL~@lk~9GLmu61|JuYEsD&ST)*$)G-6fM%6@nGwd6H=4BKCwkdJLn4`(ab*tu{r z!tfQWvbTT_gb(AdYME3^nAc*E_l zQK+rDS?+S?u3-U~zm$!&AVy9^k9aDALo=S;Wl0F_?i(sZzllHnR}3PPY>yQ}b}a;s z*$7^43R8}sqSQ=-uX$5j_79}o#5UyO(SoC2j%-M%A9c$gEredV2iFcgq1%>@o(H9N zMAW0>EQ$$3H_a?1&j{DN{aeg)r_AGXe}?fz_TcKK&`+#zlX`ySK}+O>Vfj%8OSa~z#HMIXO}die4ICwC>%-QEDdxc(5s0Gy?x>! zBlW{zAn`tO-ff-FSGp+5cn`R;Thpd>Fl;|ss=$Pu4%{@9M%cO%Tmo01BD9Du{`Q%w z0EY8Zy?}VQ1jl_Odt>}aCY<*yI?Y=H`3#$)a{OV$#o4Kg8g*&7mttP3b7f+b&QV>? zDsrq&dM-V(+CK^a+7pl5wtaXKy2(e3Lzxnn{MtD%hVomjO;Wl zs#5qMGZ9;8xhLPEBcw1108zI~z0$#90(wuh1b?XKlHK*=A@h+6xwi~#)C%ozNGX-8 zS+m^d=Z5#Pg;t@H{4ArWqGSX`$^PIyy%BAK@yj2KV>YX!igE$_a1P`5h zp4Fb2;G66W5@n2tSn(}y@!8*x8hBEjd?ld!LD3=Mg?A3Y`N;;i>x1`oEn=HIGUVIGf`TofG?m4+W#Ej>yod>Q4Dowr}CW^=$M ztkLXFgXH4*xE|`jRij;ZaB>7r6BwPdDuv{HzGP*?rL_fQs}%P>M$q(O2Kgu{chae{ zBV(i`hMG6S+YuWvs^dDdvz59w*9_iR2M`_!XrGq48EleMtg!ll&)vKs4mLJyD@BoN z0|>oEz0bb^?P?l7=4@y77)5JZ;0II#KR^y->9T0E0Ot&#g!z zrfL{#lgA?m(H!Yad47GA94Rme#C$K=d9TX|J}*XK=CGn&lEWFjI#u@bsmtAgw(UCfg{I4{&8bNd)cdo)kdWz5mGV?wkDq|?y&-UHH z!Imsw#_ymHnlaZ3h?KSJjB+Av^uP%Y7?h&wf`7vfe};&-n0+`glRqxbn3~33Cc%K} zCjR-mgoT*t001+OCO z3w(H5c8WIm4Ne%3tHW&^%Qgb*Q-y{dp$f5}uxZcvr7^H(^Q}l5#0n`P|D%!Bov+29 z-bw47KR&9lcFr@Js&NaucP;?%&Mv3)4$}g7TY@$J;?oA(hz#)g0s`Okp5RQ2%|SvKgp>JMYD&_HTWV>pQy@M9$ru-)i>!v4XH{ zPp~I)d2F}5tf(z!59#CBIa0Obwkse?X9b~bxCSv?GQ$hv4@N&`XVD^*%!o4l8x<_a zA+k`RC`~r-p;t{WbJ0=}WhKRC6zg+^Wha`zXC`0ebzY5-)JWa;8uh2X`u`-j8yQ6v zOC3{vGZkLwIj|Ep_H>wZ?oeUIG_E{>IuPf+2<{TJGBO^nSW9!BBsW|NqBq2Sx}hY@ ztEyj!;@&O|I%E56EuqFKfpb(Ng|S zi6l~+SkYFpOD+uCJJ;It{a=)UlR*f-YZ{p%iI^yCmey>C9}vWdP-Y!>b26zo85;tY z8P`PLBoOhJRS9gVoeTQ3yZ=orJ0&8Mm+m7RYVJ+?D)PoD!@vv0Nw0>xoUeVRVY;Mv z9=ze0!9U#lZ^e9ivhuO)P#4$#H8tSoMnrtv9&7}r1M1r7kP)tZTPKBi<6NT9X>H6b zaQMA{nduha_d4f0EaKu|D6jzYW4&fPt~SvqEu)ujxmx|VyK@9&O^X;F3A=r6yeVu# zK&zj;MGq2tX})pC7pCF@hWc=*LA;;xGE7!`l^iFvu~%U4n!ea3eXPbrAeq%$+>#Yh z-IA0YhS&CLvwf!ls1+;OS*Q5&U2iuQaZ1cu-a6{=<`@3tyF5hLORT+nbnGxG z!>{As#j?;3Hu@=9{}n_Ml;iMU-9f$a9Vpj?9WEe16B{I(HRUSw)a)MziQ^~E*P}aI zHiM`i31(l$7HHU|XEUKx#5*b#?OR*OOe#^|?Rn)Iv3v2SJw_`rXSrjrwEMG5Ri?Qr z#f7lj`N9zNLZ_mLZ3U02yn%OWuH*=){kKl4S|GZ zJ5YIlRAAF2V7?`#Q(*iIuPnx%Aw4zfOoQ2^kmpGE51X~7-w`}5l?*%1ElC;I?GMdG zV*9k%%jl@zG%`WX@a%uU%vR&PKYP3VN@xa;^BOcNUpIUc{wr;Y*g^x&I)zx=ku$Q z(-j)=rQG-xTut9%k<5xv!K^$53m>Mv$ow7T{edMR-%pxWcw<;O+k^{DUhpc@E@{@F z#)cVx8bYfH3?jM^H#QyqT(Q?eW(wvUUuzJiqn|&STP#&(kpcwO!02v*40y^OMKt#h zv)SX2{ifd8Vs%)WI%6%j{<1m}@vIS(tum)C$gQP&`Fu#5g23PN(AQ6$nqQZ9v5s~= z`bGJ_E;3n_lPm@hE;(?jwl={A7z(k)R8cffljocpxYIPMb$>+@30)$fBYEwUjw#b9 z3XV^xp_At9dzbTpEL<+QG%1U%-%l94EG8;knb@F-TUbn>T1QzNl7bb@CPAuP!4@0? zj*!LVHBqqewA$pIe4m-~gDYY-dg_k1*OQtLI+LvBqc7gV`I7|1s9J0xO*bETcsnWX zkxtpCjKhy?FMIcZaU(wo{rMWVtGk3)EO$mqPyzO_VP=t0v1%e9c_Vd63iEy-8_@gTBdrIizyy3Z z+Mg(&J+XnU;&H-F$!PK;-=|sM4~33IXb$3uL5Y(;m=M~JZo_Uh#@_@z4-WYgPqZy5 zKrQeIT(fIb98(nrgobElbw-wS_~z;NX+1B_igY27EB@N5SS|I=OD)a!3rTWH!ND6Y zrcnzL$F||p05v=DPp#+kJhZc@`>DtG3Yb@BB;t^fkeTP@4D|JO8ezMS7U(B zx=@0?JrAca9 z_}FybrE%n+Z!(fjthd%-=y4lYVwW$RVL+T5@ItyBEnOWZIbGW#@T;wVxbELF%fCgo z@@+SJP;DtA@{R8Dlc0~^O8Oj~b!Fx!nCD#j1afR=cVfKje(dIGgU?W{rjh25PN zU}B5=S?lpic-Df`!!OyYvjL6uL7o;!vb^755rQ^b%>%3B_k97e7pZNg^530kHbmIA zm(EAi*};J4IPuoz%%X86mnA-ldN#X558mxTR5j)g?e4p{b*dlGa$rVmfXA{S`f{0T zfUR<4P3BqEYc8eBut`V=5=q(}uIeAR_m+gXJQyfN2rGljuC8E%R@!b;wX?&r*ADly zWITeso~Zx~2EDds7hWSx1n#gy&?N-a$C&!fuBkuv_~8AF94nmh@m4mHFq%T$3W#Rr za=-{X*=r)?LNfmETs4U;s-7St+d_3Z`~kr9^ezqkE~P!`-Mg%S+F|cVMX6T9KHi+e zQNAiyf-Q#P4a3IgBan%z#VhFN3ut~OU;*gek$)F58p(98B+C(v)h7wEYw7sE2+z~2qC5cHk8Xe{j+DPZ&p1Eoh9W^RU4d^Gb&TRq?J zi25fp(Z0<@^~bpByECH*O!o=y<2KP>c|M~34)m<@5c%uiL$HL!opW}|YIgUmfdmzv zlWJpmVdG^D7)t{rx*EHopm#@$u3mL!%UwNb6X#X3zLoH^@zN!xVJ;PNIb+EC;un86 z+5K1#X5kgneZ%N$*E_>R_<`+Sul6N@7+os8^aInlTKgI)dV4LcZvCA5J->*6J<%OK z6!&@=m53kb#BJR-vj4r4Gz5*8wCR+FKF0QVp-`^P4f5KBfc4Dm%&k9QLH~V__#G@$@%r4OW4%Vp7s1W7*)Oa9;|1dr+|FV0(Ym#xtd$$te(6nu-155nKBkC0@j z@2c#r!lJq1e@atM>4b-#L{aAQ;=7&a9;_erO^6Dl&4Z2mJ-a)diP59#rR4(oUC zIC&ib2x$R-jYd{PfALCl%Fcx6UY+Fpb}ECF*RPrFMW*+xzSvRcU63P7NFsS&(864M!S9aqZ1*dGyjTzm!xzewUADc1 z>2YXxP9i`Qel3cb#p^q@6K^Xn+$X=qcL;am*Xe7_WiEs43rtz^VQ2U>7mpVtI!NpU z3L^#_$Y=R^Y{U0MMN zThXIK_rbKd#V{y3x?1upDv}!|>pwur8pD8jukyYiSEIY=SAXL64d06M)h;WgVc)_` znC^PRMdbYerDr*jcm-|NHjNPAotqX~Z^gkNPUHydv@fbC9)pn)2NJqQIgPu6#5sey z7&P&1)K#ldPdi-lv; z)WcWpSKfX@!X34ga@gs@&#Y)M2UXIvaCh$J78^%2Nm~6Rh2%-Xv&>&^M%eH9h0NtM z09fqkz^_@qbW~W{!Q-C8Z^>G8+4-)zIxK_{p@Z2StD($PsyJneDH>UMMJC8`0V?j8 z269&NVpQdXDRdf!))G0Bks80FT*OQXW1m$b?)GX=5MHxbD~-L-wwZA!i`#)h`xrI6 z)Cmd}!yS!M_aVIRN;taqi}Whuc}y&L*jQ%_zB}H;Y(4(6@N;=itQOOAG%osygsJD* zef9Z?hrp)b>ba!%!?0PQh{zvyF)0+6Bn1J!rEld@c%U_D!u1}BwbU0YvZDkkyN>;@6f4A1 z0Vl!QO0vrEKKdH6o)gMCq}?&1@1N@7{k$JNqH8Bfk9G69DT zMtK_UEChKMb)+=xJ9V*sed12tw3`ZsBl?){!c6LaM}Ll_eM%;h<7Uh9`bA*)1-Ikl zS54H=FrW_fCW$uzz@RCyO zh+P85tK4!)5{ZuLTGEQ>v-ePgxif@o$T-cfC~b2ajF5_3JIl?Ylvu`?YU~_v6gFO6)T3ypp`Ccl_qoDukY+hi3;Ca#ie_q!DxqKaIsDH)svQrpD5T2%7bMd-E+zuZl8|m2k6rv>ycqm$2IF#FqQM{DO?ZzJF{T2g z9w1PqSsOln9d}reg6Kqc7LhD0Y(aIMBxz4CIPfE{ZfMco0ZMAwW`;w_lr2_>{tSl? zgN_wwrLvC9skr<9P|Hx!AJt9*GoKZ~0SQhlCRiUn^nWROnQ4r}qAFo-3MW>@%D=t} zMZiGE@aR)8PGaCJI3X&)Obpnh6r*v?05426F)Wl)AwRwri51ztJMICE3eO z=ryFWrTzfa{&lAxLT^hhZZD6iu^G7gb&f&MCMXqV<^OTEF~q}o%=iF#*vDG zE$sZXvmwFu!~C|Wo56r=1u*9}-2v&yT%P+ujZwC_x;Z_K(5$pGYAKtIvSM%|XG|{d zYK#?hRFVZ)(y4S3dvgyXWz`ah=uugangy*Q#GJ_4@RR(YDp^L@8?a&@FUwMSuQ+%x z6rF?2)^DNgmgu!s8Nu%nKCJMe{Awh!u^0nToUE*Eul9?7WMeyZU`)bitpbXzzZbLE zYxgo2Vg$#V7UaWX{L`!dSt{p)p+SghWwazC$FZKbZG>gHN_rp;FF8c*5=~i#Y5kjB z4_zzT7i(Xs=c4BPdQ`G+bqN=~?|)2;nPG4e`QEI)2eRh&4MU0(n9Xe8_aIBSzhtb| z*PXBUGEb0N`RkV0u@ zGX8{-*3J-p+fZae^U`Z}rulP}c{^If-7kd#q_Xt%HD^+YjPESii zWm_M5v^2ls)z`^2Jd77fZwo~z{Dhscefo`{1d+X1zzt7lP$}*!7aG`dc%dr?XE3jQ z(9N5j@MlK%O#9YjOp6LF_l8h#$T7MiiBGAFW3e$jNt}`4H>-wm1;kWv9tq9BSY%%M zt;qkrCVD+0FUbp6b4TPJv4niSpJYB+^+&Fd86iYJuzBXC0_InWxAz@#J34&TzC=Jh zGA|#6cy+ORwjh&ANqq+kTWeGtBEcQaGHaKMz!6aMm}x$kvhd^z!9bsbA~G+NBc1U` zBT9n>8@n)QjfWvl!)G3-JhAxr7J9c7{AL zsTohq6#D{uOsfrUj?%8T)8)B;N>F2hTNfUYscznjGzo6B(7(9Y*MutjJ7+ir|4xIR zUi($vyc=1xb?kz8}gf_O)_D54> zX3fJ~{bW#TR%I+|G91{NClMg!qt!YOT+|q$d%9I_GW8=ZKL03g29 z0rtUW3YJh$IcWzU8Iy6_C}IfD8f6(tGm7{fyHg5DKY%gUM)|=`WO;@CZ2KBwsnF%A&dRlYI+za zvxN*ygU(v986N+MpM#J162e8M`14tIOOGL2N^EvrY%`T8j;3v+5X4-{LI3a%btZ>v zH#!X&df)!W@e2=jY@KdAVdyQtJ)U4sJQ3hBXOCA8@J%{;#$mGOQIPtmLf%QpOA;L) zx?0!Z<3W@>93NN5;GeA^hk!(ekZxA1TnVbHRO@m5$cU~GvH%kSBQH+U*lV|GLXSqj z7Xg{C$v&+CpQu(~GNn3iWCymI=F{P57~o*cvpHyR6q@ygx8om0l zzR>IQZ2qkDSX|a36AmOHHskY(u@)6gcOgiQ9(kS#mfeREGc9Rk`m)}?+Kg^vCiQ*% zyE7uMc5$Tfi{WabhJq4bH=^5HdJ`=a5fw93eYhu~W^Kt{oJooIbNK9uD0SEe)eyPZ z5Q>5#uBAzjy;Nu=v(h-+Uggq|I)x0{%2yd=RQR-!xgPIf?OO#P?k;uOKyi!Y#bq0J zD@+keg%VlU#u4yIv*flA)6%+;3G$K@{IVV-LH>a!8(hmj8C30K^JtN?`8D0uoPjuJ zMlk>@i;cW_LAt$?ejjMmE`WrHS{wChP%DKo4JbKdrL+J^TT3+;>0EY43mwiGW|3?O zBu`J5MGbUxF3385CiwoCv8h7PdQM zSxA+6&hp4<%pFj$Qz}F9Ui}Gix`ccg7U=T(EL&(YiH4nl<(xScV@*_oF3XO1b=tkQ z71?5Et;JFwj2uG;HxvNyU5|8oOr|^3*~sPkb)j|i9MZDrseZl6cR5l=-?Vupla>4- zSno4Md5`-aaC~0k6-s8mD3DWRRItK^eM_m1f8UM7^Frz)f$-{C9LE6&Ly#Ii}?2*#498P zkeNK%4TV^!>cn5>XCO38o@OBsg(@9E1S3)mk&1e4tB%H&{{&-Zo5~ZK@CIF+qef;E z#bM+Q=gO04I0ty9H-?B(v+)?^uMe>YF%>-m7(3TAXPME|Yz)oDps;aD<$mlQ;U|{v zRCpa($hs_K24TSBVU0?5&V71u3xux0Xx0FhhVyh0mC6i573NVlt;QN(ZJh{gOm-qDPtPY~6~)A^KX;i44Oxa=zAB7z%I zO7X@OhQ9v_g=y0DA1A|_I(@)0Z?S@&fnW$jU`K2Aho6bC0Vfm5CBu~R zCy9^bL2U%7QAL8tW-NV_fQGrb+U2v0?YKv&;s$;nE8JDG90pb&03i#w1+>ancLH6F z1lkMjbHxy?i(e;xO9l#Ur;z|4zR17nN%OcVFbDt)m8~=Gn-+}Wh2728a5&6@p-gB9 zto;!k8AK7Ph;bkzgzN$qBql`qr){z$+!>7m$cVF~Rvg2XRk72Ox)_Eno0)?SSTkf5 zvLIt2+lnDIXuGat?WN{;`^HG=SlJz|n~lR`;(~Q5ZVoxY^$7qC_F;nKS3RS#DKs8$ zI!AWIy1!xj)cE%``Xe~r&AKb)F|gF$c0S*B8T=+>iufG#{p_pqvy9d zudlwlI1O9Z{7|xqPzB>ng3kf1ZLO>{)u35eV^#U+><}VHD8z{ilM5!@m2DW!1dE_> z5E_x6Y#`tOO+?2Jte_ZZ!_6gc=1fOfDMf**8ID1O=V!7(qn!$w@g){M!oXj`NJ4igaH?3ltH;0TeEQ$Y4_D|14~fgQBO zfTE&MQf(r10G?e40TwpI^PXQX2<<+2o$Sh%v=~#%o739L&hdGIVq$M|5p;FC|12QL z0a`scrA!d}ccxfK021(pn`32S&WcXw7~nfx&+z@pHy4pY;$zIg+VB50!EWb*V~)dB zcA&@=HKUEuQ9)!effMo>yYaq)^sh2tMn)HOGZhAV5;ebJ_-C*oTA9*j$5QKxpeHVP zMHv_+DK_x)KwJ0&^*MUr8veBx>uI%Ybuy4a98EJ7MTP7T%C6jsAS{v>T)(cdC+euk zYz`p`4?z2+I0ALUtDdKlL~1{43<1jhV`2UpLFkwN#5__wROh(?FNwMp25Eeryt*H~ zYPvL;h+>4wXWlB15tpop13tLlT?%x*vTt@p5bPCO2o<0$1bKFbak$^%xdq`-Sp@RP z!>9u@?9q!aN-9nDF{LeHY9DroQ}RedIY*eLPJNm~vxPh>L<9n&6HKZ^Mf!DZo{@gZly4ZtAf!u zPC8ilcR++GH8_Zb*@R#-N<%_orT#j}DVoUOIP>_XacM4s4f2^-v~LEoB-|H>J_u^kBN z`n0NgoQ8f$pn$nwKoo_+5=HQtHZZZglX5U=7SIeuf39`+x7`eu+dirX?L4o%azeHI zU^y#^S$Mhgfo>x!@)BJpIT*t%3SkLBPu!XU6wfZWln#)!vn-^#ww!r*Sq0l&Iya&7 zq$=gKg+X?O3rIfGK5S+qNXS8~$ajnkytXB3ghSRZH7-=tHRz->lMLIlYT5_E)LZ7z zG=2MF1nsPeEMk%;z@IXVNy;=EEBMTgr)Yo~Wf;w}7R#N(QL{|4(ad2sAyLk2q{l;z zGWclgWIz%X9VwG*vJV0neWo{;GRjn-8Cm!77%B((2r0QQreG$3m%PEEYx@P85O{m( zj&OXjmB{Tql0<0lV^vYvn+(We5D;X0Jf80ScA>LL0n(435RqaIK)`B?p7f8wBQ5aX zpEafAJIl#jK8TkZHS)tspx0DwYCMhO>_Etb*Fa1N1$&2Tr96D96-EixlLD%sa1cvJ zvDIZx*elZ>BS1P5cX`Pj=0A!92EOY(96oPa>ATkVP7V_?Ji;lVtn@^PlmKlm)zRg9 z`wjZk3??Lqse^mSAcXl+mSG_PMfqi{3lHGVNN3(9FF`|G{UL1EVq7vqJBs4O8QAr% zl!(iTELsbT%L?{eBm^3FmNeo?iE%kJu=JvD2I!hgChJxfhCuh&w|@<+uvP5!P{RtD z2-YaPidG;g(@Qqd4p0)fJ_VtdSQ_Zep%l$e@CeMuxn{kl*qAU#h?sVoGFip%Y^f3S z_1;|*MJ0g=9GH#h_o_lM07Z)PkCubs=jRE1bI-tVTDC$bxWF)P(~rPOq2-WRFCs(YN`snG z+z#;qq$pKcq}GCqu{0)1iGl6OiTXueo>emK{@Im9dy-tv2Yfs6y0y)M!esqTLK&lwl^FSZgwyDV*OW&Do7b62)h#&IIjOV=O^tZ=HT(~)0R<&6r@VQp%NrXIBR5yf*>G{kVnx$XXKG!b$+0y z_odiIvn8?}Pg{!R`I6`|9aSRt1iD8s9T#*ABdSYi3=CUn{OCHsyaDeSfzkqv5z5qL zhV;?~%L4>c%M_s<4w8JkW|SHLF}4ntk)hHGA?L9ExfEv&1Ua3!5{ain#8Cm@-+Ea| zW4yEmUr0!%p}P%=)+dpJPDWLmPtM2S#aKAI;&DGXI@{;$;=1N-!(?WV%;v-S#dz`o j!x{jHm-dM!L@tgKC!1~`DFP}XH6$TyA!EyeVAY!l>$s0Q literal 0 HcmV?d00001 diff --git a/www/doc/client/fonts/OpenSans-Bold-webfont.svg b/www/doc/client/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/www/doc/client/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-Bold-webfont.woff b/www/doc/client/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..1205787b0ed50db71ebd4f8a7f85d106721ff258 GIT binary patch literal 22432 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 ig3W + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-BoldItalic-webfont.woff b/www/doc/client/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..ed760c0628b6a0026041f5b8bba466a0471fd2e0 GIT binary patch literal 23048 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-Italic-webfont.woff b/www/doc/client/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..ff652e64356b538c001423b6aedefcf1ee66cd17 GIT binary patch literal 23188 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 literal 0 HcmV?d00001 diff --git a/www/doc/client/fonts/OpenSans-Light-webfont.svg b/www/doc/client/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/www/doc/client/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-Light-webfont.woff b/www/doc/client/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..e786074813a27d0a7a249047832988d5bf0fe756 GIT binary patch literal 22248 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 literal 0 HcmV?d00001 diff --git a/www/doc/client/fonts/OpenSans-LightItalic-webfont.eot b/www/doc/client/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..8f445929ffb03b50e98c2a2f7d831a0cb1b276a2 GIT binary patch literal 20535 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 Z%0#`NexefPFV?Xj51H#Y#AC7WXn+Jg($4?@ literal 0 HcmV?d00001 diff --git a/www/doc/client/fonts/OpenSans-LightItalic-webfont.svg b/www/doc/client/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/www/doc/client/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-LightItalic-webfont.woff b/www/doc/client/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..43e8b9e6cc061ff17fd2903075cbde12715512b3 GIT binary patch literal 23400 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 + + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/index.html b/www/doc/client/index.html new file mode 100644 index 0000000..c5f5bb4 --- /dev/null +++ b/www/doc/client/index.html @@ -0,0 +1,96 @@ + + + + + JSDoc: Home + + + + + + + + + + +
+ +

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + + +
+

Canopy - 0.3-INDEV

+

Canopy - /ˈkæ.nə.pi/:

+
    +
  • The upper layer of foliage and branches of a forest, containing the majority of animal life.
  • +
+

Canopy is a community chat & synced video embedding web application, intended to replace fore.st as the server software for ourfore.st. +This new codebase intends to solve the following issues with the current CyTube based software:

+
    +
  • Unmaintained upstream codebase.
  • +
  • Different goals.
  • +
  • Different coding styles.
  • +
  • Obsolete Technology and Dependencies.
  • +
  • General Clunk
  • +
  • Less Unique Community Identity
  • +
+

Canopy intends to be a simple node/express.js app. It leverages yt-dlp and the internet archive REST api for metadata gathering. Persistant storage is handled by mongodb, as it's document based nature inherintly works well for cleanly storing large config documents for user/channel settings, and the low use of inter-collection references within the canopy software. All hardcore security functions like server-side input sanatization, session handling, CSRF mitigation, and password hashing are handled by industry-standard open source libraries such as validator/express-validator, express-sessions, csrf-sync, and bcrypt, however it IS hobbiest software, and it should be treated as such.

+

The Canopy codebase does not, nor will it ever contain:

+
    +
  • Advertisements (targetted or otherwise)
  • +
  • Proprietary Code
  • +
  • Cryptocurrency/Blockchain integration
  • +
  • 'Analytics/Telemtry' spyware
  • +
  • The use of video sources which require proprietary 'Digital Rights Management Ristricitons Malware' such as Widevine.
  • +
+

Thirdparty media providers may or may not contain all of the above atrocities :P (though browser-side DRM extensions will never be required), always use an ad-blocker!

+

Our current goal is to create a cleaner, more modern, purpose-built codebase that has feature-parity with the current version of fore.st, while writing improvements where possible. Once this is accomplished, and ourfore.st has been migrated, work will continue to re-create features from TTN, while also building completely new ones as well.

+

License

+

Canopy is written by the community, and provided under the GNU Affero General Public License v3 in order to prevent Canopy from being used in proprietary software or shitcoin scams.

+
+ + + + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/scripts/linenumber.js b/www/doc/client/scripts/linenumber.js new file mode 100644 index 0000000..4354785 --- /dev/null +++ b/www/doc/client/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(() => { + const source = document.getElementsByClassName('prettyprint source linenums'); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/www/doc/client/scripts/prettify/Apache-License-2.0.txt b/www/doc/client/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/www/doc/client/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/www/doc/client/scripts/prettify/lang-css.js b/www/doc/client/scripts/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/www/doc/client/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/www/doc/client/scripts/prettify/prettify.js b/www/doc/client/scripts/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/www/doc/client/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p th:last-child { border-right: 1px solid #ddd; } + +.ancestors, .attribs { color: #999; } +.ancestors a, .attribs a +{ + color: #999 !important; + text-decoration: none; +} + +.clear +{ + clear: both; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.source +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.source code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td.description > p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/www/doc/client/styles/prettify-jsdoc.css b/www/doc/client/styles/prettify-jsdoc.css new file mode 100644 index 0000000..5a2526e --- /dev/null +++ b/www/doc/client/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/www/doc/client/styles/prettify-tomorrow.css b/www/doc/client/styles/prettify-tomorrow.css new file mode 100644 index 0000000..b6f92a7 --- /dev/null +++ b/www/doc/client/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/www/doc/client/userList.html b/www/doc/client/userList.html new file mode 100644 index 0000000..74f27cc --- /dev/null +++ b/www/doc/client/userList.html @@ -0,0 +1,1200 @@ + + + + + JSDoc: Class: userList + + + + + + + + + + +
+ +

Class: userList

+ + + + + + +
+ +
+ +

userList(client)

+ +
Class for object containing logic behind userlist UX
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new userList(client)

+ + + + + + +
+ Instantiates a new userList object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client mgmt object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

clickDragger

+ + + + +
+ Click Dragger object for handling userlist resizes +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

client

+ + + + +
+ Parent Client Management object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

colorMap

+ + + + +
+ Map of usernames to assigned username color +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

toggleIcon

+ + + + +
+ userlist toggle button +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

userColors

+ + + + +
+ Userlist color array (Maps to css classes) +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

userCount

+ + + + +
+ user count label +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

userDiv

+ + + + +
+ users div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

userList

+ + + + +
+ userlist div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

defineListeners()

+ + + + + + +
+ Defines network-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

renderUser(user, flair)

+ + + + + + +
+ Renders out a single username to the userlist +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
user + + +String + + + + Username to render
flair + + +String + + + + Flair to render as
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setupInput()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateList(list)

+ + + + + + +
+ Updates UX after user list change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
list + + +Array + + + + Userlist data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/userlist.js.html b/www/doc/client/userlist.js.html new file mode 100644 index 0000000..b1677df --- /dev/null +++ b/www/doc/client/userlist.js.html @@ -0,0 +1,261 @@ + + + + + JSDoc: Source: userlist.js + + + + + + + + + + +
+ +

Source: userlist.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class for object containing logic behind userlist UX
+ */
+class userList{
+    /**
+     * Instantiates a new userList object
+     * @param {channel} client - Parent client mgmt object
+     */
+    constructor(client){
+        /**
+         * Parent Client Management object
+         */
+        this.client = client
+
+        /**
+         * Click Dragger object for handling userlist resizes
+         */
+        this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-users-drag-handle", "#chat-panel-users-div", true, this.client.chatBox.clickDragger);
+        
+        /**
+         * Userlist color array (Maps to css classes)
+         */
+        this.userColors = [
+            "userlist-color0",
+            "userlist-color1",
+            "userlist-color2",
+            "userlist-color3", 
+            "userlist-color4",
+            "userlist-color5",
+            "userlist-color6"];
+        
+        /**
+         * Map of usernames to assigned username color
+         */
+        this.colorMap = new Map();
+
+        /**
+         * users div
+         */
+        this.userDiv = document.querySelector("#chat-panel-users-div");
+
+        /**
+         * userlist div
+         */
+        this.userList = document.querySelector("#chat-panel-users-list-div");
+
+        /**
+         * user count label
+         */
+        this.userCount = document.querySelector("#chat-panel-user-count");
+
+        /**
+         * userlist toggle button
+         */
+        this.toggleIcon = document.querySelector("#chat-panel-users-toggle");
+
+        //Call setup functions
+        this.setupInput();
+        this.defineListeners();
+    }
+
+    /**
+     * Defines input-related event listeners
+     */
+    setupInput(){
+        this.toggleIcon.addEventListener("click", ()=>{this.toggleUI()});
+        this.userCount.addEventListener("click", ()=>{this.toggleUI()});
+    }
+
+    /**
+     * Defines network-related event listeners
+     */
+    defineListeners(){
+        this.client.socket.on('userList', (data) => {
+            this.updateList(data);
+        });
+
+        this.client.socket.on("disconnect", () => {
+            this.updateList([]);
+        })
+    }
+
+    /**
+     * Updates UX after user list change
+     * @param {Array} list - Userlist data from server
+     */
+    updateList(list){
+        //Clear list and set user count
+        this.userCount.textContent = list.length == 1 ? '1 User' : `${list.length} Users`;
+        this.userList.innerHTML = null;
+
+        //create a new map
+        var newMap = new Map();
+
+        //for each user
+        list.forEach((user) => {
+            //randomly pick a color
+            var color = this.userColors[Math.floor(Math.random()*this.userColors.length)]
+
+            //if this user was in the previous colormap
+            if(this.colorMap.get(user.user) != null){
+                //Override with previous color
+                color = this.colorMap.get(user.user);
+            }
+
+            newMap.set(user.user, color);
+            this.renderUser(user, color);
+        });
+
+        this.colorMap = newMap;
+
+        //Make sure we're not cutting the ux off
+        this.clickDragger.fixCutoff();
+    }
+
+    /**
+     * Renders out a single username to the userlist
+     * @param {String} user - Username to render
+     * @param {String} flair - Flair to render as
+     */
+    renderUser(user, flair){
+
+        //Create user span
+        var userSpan = document.createElement('span');
+        userSpan.classList.add('chat-panel-users', 'user-entry');
+
+        //Create high-level label
+        var highLevel = document.createElement('p');
+        highLevel.classList.add("user-list-high-level","high-level");
+        highLevel.textContent = `${user.highLevel}`;
+
+        //Create nameplate
+        var userEntry = document.createElement('p');
+        userEntry.innerText = user.user;
+        userEntry.id = `user-entry-${user.user}`;
+
+        //Override color with flair
+        if(user.flair != "classic"){
+            flair = `flair-${user.flair}`;
+        }
+        //Add classes to classList
+        userEntry.classList.add("chat-panel-users","user-entry",flair); 
+
+        //Add high-level username to nameplate
+        userSpan.appendChild(highLevel);
+        userSpan.appendChild(userEntry);
+
+        //Setup profile tooltip
+        userSpan.addEventListener('mouseenter',(event)=>{utils.ux.displayTooltip(event, `profile?user=${user.user}`, true, null, true);});
+
+        //Setup profile context menu
+        userSpan.addEventListener('click', renderContextMenu.bind(this));
+        userSpan.addEventListener('contextmenu', renderContextMenu.bind(this));
+
+        this.userList.appendChild(userSpan);
+
+        function renderContextMenu(event){
+            //Setup menu map
+            let menuMap = new Map([
+                    ["Profile", ()=>{this.client.cPanel.setActivePanel(new panelObj(this.client, `${user.user}`, `/panel/profile?user=${user.user}`))}],
+                    ["Mention", ()=>{this.client.chatBox.catChat(`${user.user} `)}],
+                    ["Toke With", ()=>{this.client.chatBox.tokeWith(user.user)}],
+            ]);
+
+            if(user.user != "Tokebot" && user.user != this.client.user.user){
+                if(this.client.user.permMap.chan.get("kickUser")){
+                    menuMap.set("Kick", ()=>{this.client.chatBox.commandPreprocessor.preprocess(`!kick ${user.user}`)});
+                }
+
+                if(this.client.user.permMap.chan.get("banUser")){
+                    menuMap.set("Channel Ban", ()=>{new chanBanUserPopup(this.client.channelName, user.user);});
+                }
+
+                if(this.client.user.permMap.site.get("banUser")){
+                    menuMap.set("Site Ban", ()=>{new banUserPopup(user.user);});
+                }
+            }
+
+            //Display the menu
+            utils.ux.displayContextMenu(event, user.user, menuMap);
+        }
+    }
+
+    toggleUI(show = !this.userDiv.checkVisibility()){
+        if(show){
+            this.userDiv.style.display = "flex";
+            this.toggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill");
+            this.clickDragger.fixCutoff();
+        }else{
+            this.userDiv.style.display = "none";
+            this.toggleIcon.classList.replace("bi-caret-down-fill","bi-caret-left-fill");
+        }
+    }
+
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/server/activeChannel.html b/www/doc/server/activeChannel.html index e550d80..6e0f755 100644 --- a/www/doc/server/activeChannel.html +++ b/www/doc/server/activeChannel.html @@ -786,7 +786,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_activeChannel.js.html b/www/doc/server/app_channel_activeChannel.js.html index f351621..fbc5fbc 100644 --- a/www/doc/server/app_channel_activeChannel.js.html +++ b/www/doc/server/app_channel_activeChannel.js.html @@ -196,7 +196,7 @@ module.exports = activeChannel;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_channelManager.js.html b/www/doc/server/app_channel_channelManager.js.html index ac28645..51e0812 100644 --- a/www/doc/server/app_channel_channelManager.js.html +++ b/www/doc/server/app_channel_channelManager.js.html @@ -347,7 +347,7 @@ module.exports = channelManager;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chat.js.html b/www/doc/server/app_channel_chat.js.html index 12a97a2..d013229 100644 --- a/www/doc/server/app_channel_chat.js.html +++ b/www/doc/server/app_channel_chat.js.html @@ -81,7 +81,7 @@ module.exports = chat;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatBuffer.js.html b/www/doc/server/app_channel_chatBuffer.js.html index 039412f..11947f7 100644 --- a/www/doc/server/app_channel_chatBuffer.js.html +++ b/www/doc/server/app_channel_chatBuffer.js.html @@ -178,7 +178,7 @@ module.exports = chatBuffer;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatHandler.js.html b/www/doc/server/app_channel_chatHandler.js.html index 1fa9eaa..c7ffc23 100644 --- a/www/doc/server/app_channel_chatHandler.js.html +++ b/www/doc/server/app_channel_chatHandler.js.html @@ -376,7 +376,7 @@ module.exports = chatHandler;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_commandPreprocessor.js.html b/www/doc/server/app_channel_commandPreprocessor.js.html index 554d104..f2411c6 100644 --- a/www/doc/server/app_channel_commandPreprocessor.js.html +++ b/www/doc/server/app_channel_commandPreprocessor.js.html @@ -473,7 +473,7 @@ module.exports = commandPreprocessor;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_connectedUser.js.html b/www/doc/server/app_channel_connectedUser.js.html index b37162c..63bc23e 100644 --- a/www/doc/server/app_channel_connectedUser.js.html +++ b/www/doc/server/app_channel_connectedUser.js.html @@ -334,7 +334,7 @@ module.exports = connectedUser;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_media.js.html b/www/doc/server/app_channel_media_media.js.html index 08fd81b..1f4249a 100644 --- a/www/doc/server/app_channel_media_media.js.html +++ b/www/doc/server/app_channel_media_media.js.html @@ -83,7 +83,7 @@ module.exports = media;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_playlistHandler.js.html b/www/doc/server/app_channel_media_playlistHandler.js.html index 0a01917..02d6a5c 100644 --- a/www/doc/server/app_channel_media_playlistHandler.js.html +++ b/www/doc/server/app_channel_media_playlistHandler.js.html @@ -1180,7 +1180,7 @@ module.exports = playlistHandler;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queue.js.html b/www/doc/server/app_channel_media_queue.js.html index eb6d75f..7ffd44f 100644 --- a/www/doc/server/app_channel_media_queue.js.html +++ b/www/doc/server/app_channel_media_queue.js.html @@ -1795,7 +1795,7 @@ module.exports = queue;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queuedMedia.js.html b/www/doc/server/app_channel_media_queuedMedia.js.html index d8ba89f..6bf4702 100644 --- a/www/doc/server/app_channel_media_queuedMedia.js.html +++ b/www/doc/server/app_channel_media_queuedMedia.js.html @@ -165,7 +165,7 @@ module.exports = queuedMedia;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_tokebot.js.html b/www/doc/server/app_channel_tokebot.js.html index f272b14..14732da 100644 --- a/www/doc/server/app_channel_tokebot.js.html +++ b/www/doc/server/app_channel_tokebot.js.html @@ -273,7 +273,7 @@ module.exports = tokebot;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/channelManager.html b/www/doc/server/channelManager.html index 5cc831d..5e1c56b 100644 --- a/www/doc/server/channelManager.html +++ b/www/doc/server/channelManager.html @@ -1991,7 +1991,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chat.html b/www/doc/server/chat.html index 1103ae6..890fa38 100644 --- a/www/doc/server/chat.html +++ b/www/doc/server/chat.html @@ -329,7 +329,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatBuffer.html b/www/doc/server/chatBuffer.html index b33a017..6df7d3c 100644 --- a/www/doc/server/chatBuffer.html +++ b/www/doc/server/chatBuffer.html @@ -829,7 +829,7 @@ Left here since it seems like good form anywho, since this would be a private, o
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatHandler.html b/www/doc/server/chatHandler.html index 534792b..72a56ca 100644 --- a/www/doc/server/chatHandler.html +++ b/www/doc/server/chatHandler.html @@ -3686,7 +3686,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandPreprocessor.html b/www/doc/server/commandPreprocessor.html index 999c40f..5d60fbd 100644 --- a/www/doc/server/commandPreprocessor.html +++ b/www/doc/server/commandPreprocessor.html @@ -1246,7 +1246,7 @@ These arrays are used to handle further command/chat processing
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandProcessor.html b/www/doc/server/commandProcessor.html index 562c247..698151b 100644 --- a/www/doc/server/commandProcessor.html +++ b/www/doc/server/commandProcessor.html @@ -1831,7 +1831,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/connectedUser.html b/www/doc/server/connectedUser.html index 8e2281c..8e48e2f 100644 --- a/www/doc/server/connectedUser.html +++ b/www/doc/server/connectedUser.html @@ -1879,7 +1879,7 @@ Having to crawl through these sockets is that. Because the other ways seem more
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/global.html b/www/doc/server/global.html index a6340bb..9bc98d7 100644 --- a/www/doc/server/global.html +++ b/www/doc/server/global.html @@ -7377,7 +7377,7 @@ Warns server admin against unsafe config options.
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/index.html b/www/doc/server/index.html index 5d31476..91b924e 100644 --- a/www/doc/server/index.html +++ b/www/doc/server/index.html @@ -87,7 +87,7 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/media.html b/www/doc/server/media.html index e0a1b6d..05f1463 100644 --- a/www/doc/server/media.html +++ b/www/doc/server/media.html @@ -352,7 +352,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/playlistHandler.html b/www/doc/server/playlistHandler.html index 3efb58f..b507305 100644 --- a/www/doc/server/playlistHandler.html +++ b/www/doc/server/playlistHandler.html @@ -5108,7 +5108,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queue.html b/www/doc/server/queue.html index 5166e19..0dcfeda 100644 --- a/www/doc/server/queue.html +++ b/www/doc/server/queue.html @@ -5805,7 +5805,7 @@ Called auto-magically by the Synchronization Timer
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queuedMedia.html b/www/doc/server/queuedMedia.html index fc2445c..6dae2ba 100644 --- a/www/doc/server/queuedMedia.html +++ b/www/doc/server/queuedMedia.html @@ -936,7 +936,7 @@
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelBanSchema.js.html b/www/doc/server/schemas_channel_channelBanSchema.js.html index 52c503d..f75fc05 100644 --- a/www/doc/server/schemas_channel_channelBanSchema.js.html +++ b/www/doc/server/schemas_channel_channelBanSchema.js.html @@ -101,7 +101,7 @@ module.exports = channelBanSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelPermissionSchema.js.html b/www/doc/server/schemas_channel_channelPermissionSchema.js.html index 4beb50c..60a8dfb 100644 --- a/www/doc/server/schemas_channel_channelPermissionSchema.js.html +++ b/www/doc/server/schemas_channel_channelPermissionSchema.js.html @@ -169,7 +169,7 @@ module.exports = channelPermissionSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelSchema.js.html b/www/doc/server/schemas_channel_channelSchema.js.html index 7e669b8..143bfc4 100644 --- a/www/doc/server/schemas_channel_channelSchema.js.html +++ b/www/doc/server/schemas_channel_channelSchema.js.html @@ -934,7 +934,7 @@ module.exports = mongoose.model("channel", channelSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_chatSchema.js.html b/www/doc/server/schemas_channel_chatSchema.js.html index 7fc66fc..f2ecd6a 100644 --- a/www/doc/server/schemas_channel_chatSchema.js.html +++ b/www/doc/server/schemas_channel_chatSchema.js.html @@ -96,7 +96,7 @@ module.exports = chatSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_mediaSchema.js.html b/www/doc/server/schemas_channel_media_mediaSchema.js.html index 5cf4ba9..79f37b8 100644 --- a/www/doc/server/schemas_channel_media_mediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_mediaSchema.js.html @@ -96,7 +96,7 @@ module.exports = mediaSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html index c2e909d..f5e8950 100644 --- a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html @@ -124,7 +124,7 @@ module.exports = mediaSchema.discriminator('saved', playlistMediaProperties);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistSchema.js.html b/www/doc/server/schemas_channel_media_playlistSchema.js.html index 5d9cc3b..3bb51f6 100644 --- a/www/doc/server/schemas_channel_media_playlistSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistSchema.js.html @@ -174,7 +174,7 @@ module.exports = playlistSchema;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html index bd2431c..b1f0ac2 100644 --- a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html @@ -113,7 +113,7 @@ module.exports = mediaSchema.discriminator('queued', queuedProperties);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_emoteSchema.js.html b/www/doc/server/schemas_emoteSchema.js.html index 258c270..d08a919 100644 --- a/www/doc/server/schemas_emoteSchema.js.html +++ b/www/doc/server/schemas_emoteSchema.js.html @@ -164,7 +164,7 @@ module.exports = mongoose.model("emote", emoteSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_flairSchema.js.html b/www/doc/server/schemas_flairSchema.js.html index 3329d2a..657647f 100644 --- a/www/doc/server/schemas_flairSchema.js.html +++ b/www/doc/server/schemas_flairSchema.js.html @@ -118,7 +118,7 @@ module.exports = mongoose.model("flair", flairSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_permissionSchema.js.html b/www/doc/server/schemas_permissionSchema.js.html index 3659f64..8f40f49 100644 --- a/www/doc/server/schemas_permissionSchema.js.html +++ b/www/doc/server/schemas_permissionSchema.js.html @@ -356,7 +356,7 @@ module.exports = mongoose.model("permissions", permissionSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_statSchema.js.html b/www/doc/server/schemas_statSchema.js.html index 3f276ed..c61fbf9 100644 --- a/www/doc/server/schemas_statSchema.js.html +++ b/www/doc/server/schemas_statSchema.js.html @@ -240,7 +240,7 @@ module.exports = mongoose.model("statistics", statSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html index 59a8416..4c5a68e 100644 --- a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html +++ b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html @@ -160,7 +160,7 @@ module.exports = mongoose.model("tokeCommand", tokeCommandSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_emailChangeSchema.js.html b/www/doc/server/schemas_user_emailChangeSchema.js.html index a4da1f1..9a392c1 100644 --- a/www/doc/server/schemas_user_emailChangeSchema.js.html +++ b/www/doc/server/schemas_user_emailChangeSchema.js.html @@ -222,7 +222,7 @@ module.exports = mongoose.model("emailChange", emailChangeSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_passwordResetSchema.js.html b/www/doc/server/schemas_user_passwordResetSchema.js.html index 16d75cc..16be6b7 100644 --- a/www/doc/server/schemas_user_passwordResetSchema.js.html +++ b/www/doc/server/schemas_user_passwordResetSchema.js.html @@ -198,7 +198,7 @@ module.exports = mongoose.model("passwordReset", passwordResetSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userBanSchema.js.html b/www/doc/server/schemas_user_userBanSchema.js.html index c33bec5..6f532e3 100644 --- a/www/doc/server/schemas_user_userBanSchema.js.html +++ b/www/doc/server/schemas_user_userBanSchema.js.html @@ -521,7 +521,7 @@ module.exports = mongoose.model("userBan", userBanSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userSchema.js.html b/www/doc/server/schemas_user_userSchema.js.html index 5b3dce6..10ee9d0 100644 --- a/www/doc/server/schemas_user_userSchema.js.html +++ b/www/doc/server/schemas_user_userSchema.js.html @@ -888,7 +888,7 @@ module.exports.userModel = mongoose.model("user", userSchema);
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/tokebot.html b/www/doc/server/tokebot.html index f6696dc..5e3f64b 100644 --- a/www/doc/server/tokebot.html +++ b/www/doc/server/tokebot.html @@ -841,7 +841,7 @@ I would now, but I don't want to break shit in a comment-only commit.
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_altchaUtils.js.html b/www/doc/server/utils_altchaUtils.js.html index 3ce1fb9..3230072 100644 --- a/www/doc/server/utils_altchaUtils.js.html +++ b/www/doc/server/utils_altchaUtils.js.html @@ -118,7 +118,7 @@ module.exports.verify = async function(payload, uniqueSecret = ''){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_configCheck.js.html b/www/doc/server/utils_configCheck.js.html index 0eb55a1..f230ed1 100644 --- a/www/doc/server/utils_configCheck.js.html +++ b/www/doc/server/utils_configCheck.js.html @@ -108,7 +108,7 @@ module.exports.securityCheck = function(){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_hashUtils.js.html b/www/doc/server/utils_hashUtils.js.html index 18c927e..4ccb566 100644 --- a/www/doc/server/utils_hashUtils.js.html +++ b/www/doc/server/utils_hashUtils.js.html @@ -103,7 +103,7 @@ module.exports.hashIP = function(ip){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_linkUtils.js.html b/www/doc/server/utils_linkUtils.js.html index 3eeb13f..53c42b2 100644 --- a/www/doc/server/utils_linkUtils.js.html +++ b/www/doc/server/utils_linkUtils.js.html @@ -146,7 +146,7 @@ module.exports.markLink = async function(link){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_loggerUtils.js.html b/www/doc/server/utils_loggerUtils.js.html index 233351f..53e51ab 100644 --- a/www/doc/server/utils_loggerUtils.js.html +++ b/www/doc/server/utils_loggerUtils.js.html @@ -207,7 +207,7 @@ module.exports.errorMiddleware = function(err, req, res, next){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_mailUtils.js.html b/www/doc/server/utils_mailUtils.js.html index e5abb54..067fd69 100644 --- a/www/doc/server/utils_mailUtils.js.html +++ b/www/doc/server/utils_mailUtils.js.html @@ -140,7 +140,7 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_internetArchiveUtils.js.html b/www/doc/server/utils_media_internetArchiveUtils.js.html index 5c28a5d..da6f930 100644 --- a/www/doc/server/utils_media_internetArchiveUtils.js.html +++ b/www/doc/server/utils_media_internetArchiveUtils.js.html @@ -154,7 +154,7 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_yanker.js.html b/www/doc/server/utils_media_yanker.js.html index e34f4c7..63610fd 100644 --- a/www/doc/server/utils_media_yanker.js.html +++ b/www/doc/server/utils_media_yanker.js.html @@ -193,7 +193,7 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_ytdlpUtils.js.html b/www/doc/server/utils_media_ytdlpUtils.js.html index 17bb3d2..66c0dae 100644 --- a/www/doc/server/utils_media_ytdlpUtils.js.html +++ b/www/doc/server/utils_media_ytdlpUtils.js.html @@ -186,7 +186,7 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_regexUtils.js.html b/www/doc/server/utils_regexUtils.js.html index a3db8ae..fd9f1a1 100644 --- a/www/doc/server/utils_regexUtils.js.html +++ b/www/doc/server/utils_regexUtils.js.html @@ -69,7 +69,7 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_scheduler.js.html b/www/doc/server/utils_scheduler.js.html index 7161597..500ce72 100644 --- a/www/doc/server/utils_scheduler.js.html +++ b/www/doc/server/utils_scheduler.js.html @@ -105,7 +105,7 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_sessionUtils.js.html b/www/doc/server/utils_sessionUtils.js.html index 05c32c5..c2ca71d 100644 --- a/www/doc/server/utils_sessionUtils.js.html +++ b/www/doc/server/utils_sessionUtils.js.html @@ -236,7 +236,7 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Tue Sep 02 2025 07:43:33 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 3f9df64..3a9d511 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -14,26 +14,47 @@ 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 .*/ +/** + * Class for object containing base code for the Canopy channel client. + */ class channel{ + /** + * Instantiates a new channel object + */ constructor(){ //Establish connetion to the server via socket.io this.connect(); //Define socket listeners this.defineListeners(); - //Flag youtube iframe-embed api as unloaded + /** + * Returns true once the ytEmbed API has loaded in from google (eww) + */ this.ytEmbedAPILoaded = false; - //Scrape channel name off URL + /** + * Current connected channels name + */ this.channelName = window.location.pathname.split('/c/')[1].split('/')[0]; - //Create the Video Player Object + /** + * Child Video Player object + */ this.player = new player(this); - //Create the Chat Box Object + + /** + * Child Chat Box Object + */ this.chatBox = new chatBox(this); - //Create the User List Object + + /** + * Child User List Object + */ this.userList = new userList(this); - //Create the Canopy Panel Object + + /** + * Child Canopy Panel Object + */ this.cPanel = new cPanel(this); //Set defaults for any unset settings and run any required process steps for the current config @@ -43,6 +64,9 @@ class channel{ console.log("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇."); } + /** + * Handles initial client connection + */ connect(){ this.socket = io({ extraHeaders: { @@ -52,6 +76,9 @@ class channel{ }); } + /** + * Defines network-related listeners + */ defineListeners(){ this.socket.on("connect", () => { document.title = `${this.channelName} - Connected` @@ -82,6 +109,10 @@ class channel{ }); } + /** + * Handles initial client-metadata ingestion from server upon connection + * @param {Object} data - Data glob from server + */ handleClientInfo(data){ //Ingest user data this.user = data.user; @@ -107,6 +138,11 @@ class channel{ } } + /** + * Processes and applies default config on any unset settings + * @param {Boolean} force - Whether or not to forcefully reset already set settings + * @param {Boolean} processConfig - Whether or not to run the Process Config function once complete + */ setDefaults(force = false, processConfig = false){ //Iterate through default config for(let [key, value] of channel.defaultConfig){ @@ -124,6 +160,11 @@ class channel{ } } + /** + * Run once every config change to ensure settings are properly set + * @param {String} key - Setting to change + * @param {*} value - Value to set setting to + */ processConfig(key, value){ //Switch/case by config key switch(key){ @@ -179,12 +220,17 @@ class channel{ } } + /** + * Default channel config + */ static defaultConfig = new Map([ ["ytPlayerType","raw"] ]); } -//Youtube iframe-embed API load handler +/** + * Youtube iframe-embed API entry point + */ function onYouTubeIframeAPIReady(){ //Set embed api to true client.ytEmbedAPILoaded = true; diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index a6cfa84..81624fd 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -13,25 +13,59 @@ 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 .*/ + +/** + * Class for Object which represents Canopy Chat Box UI + */ class chatBox{ + /** + * Instantiates a new Chat Box object + * @param {channel} client - Parent client Management Object + */ constructor(client){ - //Client Object + /** + * Parent CLient Management Object + */ this.client = client - //Booleans + /** + * Whether or not chat-size should be locked to current media aspect ratio + */ this.aspectLock = true; + + /** + * Whether or not the chat box should auto-scroll on new chat + */ this.autoScroll = true; - //Numbers + /** + * Chat Buffer Scroll Top on last scroll + */ this.lastPos = 0; + + /** + * Height of Chat Buffer on last scroll + */ this.lastHeight = 0; + + /** + * Width of Chat Buffer on last scroll + */ this.lastWidth = 0; - //clickDragger object + /** + * Click-Dragger Object for handling dynamic chat/video split re-sizing + */ this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-drag-handle", "#chat-panel-div"); - //Preprocessor objects + /** + * Command Pre-Processor Object + */ this.commandPreprocessor = new commandPreprocessor(client); + + /** + * Chat Post-Processor Object + */ this.chatPostprocessor = new chatPostprocessor(client); //Element Nodes @@ -145,8 +179,6 @@ class chatBox{ chatBody.classList.add("chat-panel-buffer","chat-entry-body"); chatEntry.appendChild(chatBody); - console.log(data); - //Append the post-processed chat-body to the chat buffer this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data)); diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index 49f9fc4..6486cc9 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -13,10 +13,29 @@ 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 .*/ + +/** + * Class for object containing chat and command pre-processing logic + */ class commandPreprocessor{ + /** + * Instantiates a new commandPreprocessor object + * @param {channel} client - Parent client Management Object + */ constructor(client){ + /** + * Parent Client Management object + */ this.client = client; + + /** + * Child Command Processor object + */ this.commandProcessor = new commandProcessor(client); + + /** + * Set of arrays containing site-wide, channel-wide, and user-specific emotes + */ this.emotes = { site: [], chan: [], @@ -27,6 +46,9 @@ class commandPreprocessor{ this.defineListeners(); } + /** + * Defines Network-Related Listeners + */ defineListeners(){ //When we receive site-wide emote list this.client.socket.on("siteEmotes", this.setSiteEmotes.bind(this)); @@ -35,21 +57,34 @@ class commandPreprocessor{ this.client.socket.on("usedTokes", this.setUsedTokes.bind(this)); } + /** + * Pre-Processes a single chat/command before sending it off to the server + * @param {String} command - Chat/Command to pre-process + */ preprocess(command){ //Set command and sendFlag this.command = command; this.sendFlag = true; + //Attempt to process as local command this.processLocalCommand(); + //If we made it through the local command processor if(this.sendFlag){ + //Set the message to the command this.message = command; + //Process message emotes into links this.processEmotes(); + //Process unmarked links into marked links this.processLinks(); + //Send command off to server this.sendRemoteCommand(); } } + /** + * Processes local commands, starting with '/' + */ processLocalCommand(){ //Create an empty array to hold the command this.commandArray = []; @@ -70,6 +105,9 @@ class commandPreprocessor{ } } + /** + * Processes emotes refrences in loaded message into links to be further processed by processLinks() + */ processEmotes(){ //inject invisible whitespace in-between emotes to prevent from mushing links together this.message = this.message.replaceAll('][',']ㅤ['); @@ -84,6 +122,9 @@ class commandPreprocessor{ }); } + /** + * Processes links into numbered file seperators, putting links into a dedicated array. + */ processLinks(){ //Strip out file seperators in-case the user is being a smart-ass this.message = this.message.replaceAll('␜',''); @@ -109,26 +150,50 @@ class commandPreprocessor{ this.message = splitMessage.join(''); } + /** + * Transmits message/command off to server + */ sendRemoteCommand(){ this.client.socket.emit("chatMessage",{msg: this.message, links: this.links}); } + /** + * Sets site emotes + * @param {Object} data - Emote data from server + */ setSiteEmotes(data){ this.emotes.site = data; } + /** + * Sets channel emotes + * @param {Object} data - Emote data from server + */ setChanEmotes(data){ this.emotes.chan = data; } + /** + * Sets personal emotes + * @param {Object} data - Emote data from server + */ setPersonalEmotes(data){ this.emotes.personal = data; } + /** + * Sets used tokes + * @param {Object} data - Used toke data from server + */ setUsedTokes(data){ this.usedTokes = data.tokes; } + /** + * Fetches emote by link + * @param {String} link - Link to fetch emote with + * @returns {Object} found emote + */ getEmoteByLink(link){ //Create an empty variable to hold the found emote var foundEmote = null; @@ -148,6 +213,10 @@ class commandPreprocessor{ return foundEmote; } + /** + * Generates flat list of emote names + * @returns {Array} List of strings containing emote names + */ getEmoteNames(){ //Create an empty array to hold names let names = []; @@ -165,6 +234,10 @@ class commandPreprocessor{ return names; } + /** + * Generates auto-complete dictionary from pre-written commands, emotes, and used tokes from servers for use with autocomplete + * @returns {Object} Generated Dictionary object + */ buildAutocompleteDictionary(){ let dictionary = { tokes: { @@ -226,11 +299,25 @@ class commandPreprocessor{ } +/** + * Class for Object which contains logic for client-side commands + */ class commandProcessor{ + /** + * Instantiates a new Command Processor object + * @param {channel} client - Parent client mgmt object + */ constructor(client){ + /** + * Parent Client Management object + */ this.client = client } + /** + * Method handling /high client command + * @param {Array} argumentArray - Array of arguments passed down from Command Pre-Processor + */ high(argumentArray){ //If we have an argument if(argumentArray[1]){ diff --git a/www/js/channel/cpanel.js b/www/js/channel/cpanel.js index 3e5fa2c..53c2f44 100644 --- a/www/js/channel/cpanel.js +++ b/www/js/channel/cpanel.js @@ -14,39 +14,114 @@ 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 .*/ +/** + * Class for Object containing code for managing the Canopy Panel UX + */ class cPanel{ + /** + * Instantiates a new Canopy Panel Management object + * @param {channel} client - Parent client Management Object + */ constructor(client){ - //Client Object + /** + * Parent Client Management object + */ this.client = client; - //Panel Objects + /** + * Active Panel Object + */ this.activePanel = null; + + /** + * Pinned Panel Object + */ this.pinnedPanel = null; + + /** + * Popped Panel Objects + */ this.poppedPanels = []; - //ClickDragger Objects + /** + * Click-Dragger object for re-sizable active panel + */ this.activePanelDragger = new canopyUXUtils.clickDragger("#cpanel-active-drag-handle", "#cpanel-active-div", false, null, false); + + /** + * Click-Dragger object for re-sizable pinned panel + */ this.pinnedPanelDragger = new canopyUXUtils.clickDragger("#cpanel-pinned-drag-handle", "#cpanel-pinned-div", false, this.client.chatBox.clickDragger); //Element Nodes //Active Panel + /** + * Active Panel Container + */ this.activePanelDiv = document.querySelector("#cpanel-active-div"); + + /** + * Active Panel Title + */ this.activePanelTitle = document.querySelector("#cpanel-active-title"); + + /** + * Active Title Document Div + */ this.activePanelDoc = document.querySelector("#cpanel-active-doc"); + + /** + * Active Panel Pin Icon + */ this.activePanelPinIcon = document.querySelector("#cpanel-active-pin-icon"); + + /** + * Active Panel Pop-Out Icon + */ this.activePanelPopoutIcon = document.querySelector("#cpanel-active-popout-icon"); + + /** + * Active Panel Close Icon + */ this.activePanelCloseIcon = document.querySelector("#cpanel-active-close-icon"); + //Pinned Panel + /** + * Pinned Panel Contianer + */ this.pinnedPanelDiv = document.querySelector("#cpanel-pinned-div"); + + /** + * Pinned Panel Title + */ this.pinnedPanelTitle = document.querySelector("#cpanel-pinned-title"); + + /** + * Pinned Panel Document Div + */ this.pinnedPanelDoc = document.querySelector("#cpanel-pinned-doc"); + + /** + * Pinned Panel Un-Pin Icon + */ this.pinnedPanelUnpinIcon = document.querySelector("#cpanel-pinned-unpin-icon"); + + /** + * Pinned Panel Pop-Out Icon + */ this.pinnedPanelPopoutIcon = document.querySelector("#cpanel-pinned-popout-icon"); + + /** + * Pinned Panel Close Icon + */ this.pinnedPanelCloseIcon = document.querySelector("#cpanel-pinned-close-icon"); this.setupInput(); } + /** + * Defines input-related event listeners + */ setupInput(){ this.activePanelCloseIcon.addEventListener("click", this.hideActivePanel.bind(this)); this.activePanelPinIcon.addEventListener("click", this.pinPanel.bind(this)); @@ -56,6 +131,11 @@ class cPanel{ this.pinnedPanelPopoutIcon.addEventListener("click", this.popPinnedPanel.bind(this)); } + /** + * Sets Active Panel + * @param {panelObj} panel - Panel Object to set as active + * @param {String} panelBody - innerHTML of Panel, pulls from panelObj.getPage() if empty + */ async setActivePanel(panel, panelBody){ //Set active panel this.activePanel = panel; @@ -73,6 +153,11 @@ class cPanel{ this.activePanel.docSwitch(); } + /** + * Hides active panel + * @param {Event} event - Event passed down from Input Handler + * @param {Boolean} keepAlive - Prevents closing panel if true + */ hideActivePanel(event, keepAlive = false){ if(!keepAlive){ this.activePanel.closer(); @@ -86,16 +171,27 @@ class cPanel{ this.activePanel = null; } + /** + * Pins active panel + */ pinPanel(){ this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML); this.hideActivePanel(null, true); } + /** + * Pop's out active panel + */ popActivePanel(){ this.popPanel(this.activePanel, this.activePanelDoc.innerHTML); this.hideActivePanel(null, true); } + /** + * Sets pinned panel + * @param {panelObj} panel - Panel Object to apply to panel + * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null + */ async setPinnedPanel(panel, panelBody){ //Set pinned panel this.pinnedPanel = panel; @@ -117,6 +213,11 @@ class cPanel{ this.pinnedPanelDragger.fixCutoff(); } + /** + * Hides pinned panel + * @param {Event} event - Passed down input event + * @param {Boolean} keepAlive - Prevents panel.closer() from running if true + */ hidePinnedPanel(event, keepAlive = false){ this.pinnedPanelDiv.style.display = "none"; @@ -127,16 +228,27 @@ class cPanel{ this.pinnedPanel = null; } + /** + * Sets pinned panel to active + */ unpinPanel(){ this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); this.hidePinnedPanel(null, true); } + /** + * Pops pinned panel + */ popPinnedPanel(){ this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); this.hidePinnedPanel(null, true); } + /** + * Pops a new pop-out panel + * @param {panelObj} panel - panelObj to apply to the panel + * @param {String} panelBody - Raw HTML to inject into panel body, injects panel default if left to null + */ popPanel(panel, panelBody){ var newPanel = new poppedPanel(panel, panelBody, this) @@ -145,15 +257,48 @@ class cPanel{ } +/** + * Template Class for other Classes for Objects which represent a single Canopy Panel + */ class panelObj{ + /** + * Instantiates a new Panel Object + * @param {channel} client - Parent client Management Object + * @param {String} name - Panel Name + * @param {String} pageURL - Panel Default Page URL + * @param {Document} panelDocument - Panel Document + */ constructor(client, name = "Placeholder Panel", pageURL = "/panel/placeholder", panelDocument = window.document){ + /** + * Panel Name + */ this.name = name; + + /** + * Panel Default Page URL + */ this.pageURL = pageURL; + + /** + * Panel Document + */ this.panelDocument = panelDocument; + + /** + * Current root document panel doc lives within + */ this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; + + /** + * Parent Client Management object + */ this.client = client; } + /** + * Fetches panel page from the server + * @returns {String} Raw panel doc HTML + */ async getPage(){ var response = await fetch(this.pageURL,{ method: "GET", @@ -162,39 +307,84 @@ class panelObj{ return await response.text(); } + /** + * Handles Document/Panel Changes + */ docSwitch(){ //Set owner doc this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; } + /** + * Called upon panel close/exit + */ closer(){ } } +/** + * Class for Objects which represent a single instance of a popped-out panel + */ class poppedPanel{ + /** + * Instantiates a new Popped Panel Object + * @param {panelObj} panel - Panel Object to apply to Popped Panel + * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null + * @param {cPanel} cPanel - Parent Canopy Panel Management Object + */ constructor(panel, panelBody, cPanel){ - //Set Panel Object + /** + * Panel Object to apply to Popped Panel + */ this.panel = panel; - //Set Panel Body + + /** + * Raw HTML to inject into panel body, defaults to panel page if null + */ this.panelBody = panelBody; - //Set Window Placeholder + + /** + * Browser Window taken up by the Popped Panel + */ this.window = null; - //Element Node Placeholders + /** + * Popped Panel Container Div + */ this.pinnedPanelDiv = null; + + /** + * Popped Panel Title + */ this.pinnedPanelTitle = null; + + /** + * Popped Panel Document Div + */ this.pinnedPanelDoc = null; + + /** + * Popped Panel Close Icon + */ this.pinnedPanelCloseIcon = null; - //Functions + /** + * Parent Canopy Panel Management Object + */ this.cPanel = cPanel; + /** + * Disables this.panel.closer() calls from this.closer() + */ this.keepAlive = false; //Continue constructor asynchrnously this.asyncConstructor(); } + /** + * Continuation of constructor method for asynchronous function calls + */ async asyncConstructor(){ //Set panel body properly this.panelBody = (this.panelBody == null || this.panelBody == "") ? await this.panel.getPage() : this.panelBody; @@ -203,12 +393,18 @@ class poppedPanel{ this.popContainer(); } + /** + * Pops/Opens container window upon start + */ popContainer(){ //Set Window Object this.window = window.open("/panel/popoutContainer","",`menubar=no,height=850,width=600`); this.window.addEventListener("load", this.fillContainer.bind(this)); } + /** + * Fills container window with Popped Panel container elements + */ fillContainer(){ //Set Element Nodes this.panelDiv = this.window.document.querySelector("#cpanel-div"); @@ -231,12 +427,18 @@ class poppedPanel{ this.setupInput(); } + /** + * Defines default input-related popped-panel Event Listeners + */ setupInput(){ this.panelPopinIcon.addEventListener("click", this.unpop.bind(this)); this.panelPinIcon.addEventListener("click", this.pin.bind(this)); this.window.addEventListener("unload", this.closer.bind(this)); } + /** + * Called upon close/exit of panel + */ closer(){ if(!this.keepAlive){ this.panel.closer(); @@ -245,6 +447,9 @@ class poppedPanel{ this.cPanel.poppedPanels.splice(this.cPanel.poppedPanels.indexOf(this),1); } + /** + * Un-pops panel into active-panel slot + */ unpop(){ //Set active panel this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML); @@ -255,6 +460,9 @@ class poppedPanel{ this.window.close(); } + /** + * Pins panel next to chat + */ pin(){ this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML); diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 2b7ccd1..7982f73 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -14,37 +14,116 @@ 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 .*/ +/** + * Class for objects which represent Canopy Player UX + */ class player{ + /** + * Instantiates a new Canopy Player object + * @param {channel} client - Parent client Management Object + */ constructor (client){ - //client obj + /** + * Parent CLient Management Object + */ this.client = client; - //booleans + /** + * Whether or not the mouse cursor is floating over player UX + */ this.onUI = false; + + /** + * Whether or not player scrub is locked to sync signal from the server + */ this.syncLock = true; - //timers + /** + * Player UX Stow-Away timer + */ this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false); //elements + /** + * Top-Level Player Container Div + */ this.playerDiv = document.querySelector("#media-panel-div"); + + /** + * Player Element Container Div + */ this.videoContainer = document.querySelector("#media-panel-video-container") + + /** + * Page Nav-Par + */ this.navBar = document.querySelector("#navbar"); + + /** + * Auto-Hiding Player UI + */ this.uiBar = document.querySelector("#media-panel-head-div"); + + /** + * Player Title Label + */ this.title = document.querySelector("#media-panel-title-paragraph"); + + /** + * Player Show Video Icon + */ this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon"); + + /** + * Player Hide Video Icon + */ this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon"); + + /** + * Player Syncronization Icon + */ this.syncIcon = document.querySelector("#media-panel-sync-icon"); + + /** + * Player Cinema-Mode Icon + */ this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon"); + + /** + * Player Filp Video Y Icon + */ this.flipYIcon = document.querySelector("#media-panel-flip-vertical-icon") + + /** + * Player Flip Video X Icon + */ this.flipXIcon = document.querySelector("#media-panel-flip-horizontal-icon") + + /** + * Player Media Reload Icon + */ this.reloadIcon = document.querySelector("#media-panel-reload-icon"); - //Numbers + /** + * Tolerance between timestamp from server and actual media before corrective seek for pre-recorded media + */ this.syncTolerance = 0.4; - //Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future... + + /** + * Tolerance in livestream delay before corrective seek to live. + * + * Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future... + */ this.streamSyncTolerance = 2; + + /** + * Forced time to wait between sync checks, heavily decreases chance of seek-banging without reducing syncornization accuracy + */ this.syncDelta = 6; + + /** + * Current Player Volume + */ this.volume = 1; //run setup functions @@ -52,6 +131,9 @@ class player{ this.defineListeners(); } + /** + * Defines Input-Related Event Listeners for the player + */ setupInput(){ //UIBar Movement Detection this.playerDiv.addEventListener("mousemove", this.popUI.bind(this)); @@ -69,6 +151,9 @@ class player{ this.reloadIcon.addEventListener("click", this.reload.bind(this)); } + /** + * Define Network-Related Event Listeners for the player + */ defineListeners(){ this.client.socket.on("start", this.start.bind(this)); this.client.socket.on("sync", this.sync.bind(this)); @@ -76,6 +161,10 @@ class player{ this.client.socket.on("updateCurrentRawFile", this.updateCurrentRawFile.bind(this)); } + /** + * Handles command from server to start media + * @param {Object} data - Media Metadata from server + */ start(data){ //If we have an active media handler if(this.mediaHandler != null){ @@ -122,6 +211,10 @@ class player{ this.mediaHandler.sync(data.timestamp); } + /** + * Handles synchronization command from server + * @param {Object} data - Syncrhonization Data from Server + */ sync(data){ if(this.mediaHandler != null){ //Get timestamp @@ -142,12 +235,18 @@ class player{ } } + /** + * Reloads the media player + */ reload(){ if(this.mediaHandler != null){ this.mediaHandler.reload(); } } + /** + * Handles End-Media Commands from the Server + */ end(){ //Call the media handler finisher this.mediaHandler.end(); @@ -159,6 +258,10 @@ class player{ this.lockSync(); } + /** + * Handles Raw-File Metadata Updates from the Server + * @param {Object} data - Updadated Raw-File link from Server + */ updateCurrentRawFile(data){ //typecheck the media handler to see if we really need to do any of this shit, if not... if(this.mediaHandler.type == 'ytEmbed'){ @@ -176,6 +279,9 @@ class player{ this.start({media: currentItem}); } + /** + * Locks player seek to synced timestamp from the server + */ lockSync(){ //Enable syncing this.syncLock = true; @@ -195,6 +301,9 @@ class player{ } } + /** + * Un-locks player seek to synced timestamp from the server + */ unlockSync(){ //Unlight the sync icon since we're no longer actively synced this.syncIcon.classList.remove('positive'); @@ -203,6 +312,9 @@ class player{ this.syncLock = false; } + /** + * Flips the video horizontally + */ flipX(){ //I'm lazy const transform = this.videoContainer.style.transform; @@ -222,6 +334,9 @@ class player{ } } + /** + * Flips the video vertically + */ flipY(){ //I'm lazy const transform = this.videoContainer.style.transform; @@ -241,6 +356,10 @@ class player{ } } + /** + * Displays UI after player-related input + * @param {Event} event - Event passed through by event handler + */ popUI(event){ this.toggleUI(true); clearTimeout(this.uiTimer); @@ -249,10 +368,18 @@ class player{ } } + /** + * Toggles UI-Bar on or off + * @param {Boolean} show - Whether or not to show the UI-Bar. Defaults to toggle if left unspecified. + */ toggleUI(show = this.uiBar.style.display == "none"){ this.uiBar.style.display = show ? "flex" : "none"; } + /** + * Toggles video on or off + * @param {Boolean} show - Whether or not to show the video player. Defaults to toggle if left unspecified + */ toggleVideo(show = !this.playerDiv.checkVisibility()){ if(show){ this.playerDiv.style.display = "flex"; @@ -266,6 +393,10 @@ class player{ this.client.chatBox.handleVideoToggle(show); } + /** + * Toggles Cinema Mode on or off + * @param {Boolean} cinema - Whether or not to enter Cinema Mode. Defaults to toggle if left unspecified + */ toggleCinemaMode(cinema = !this.navBar.checkVisibility()){ if(cinema){ this.navBar.style.display = "flex"; @@ -277,12 +408,19 @@ class player{ this.client.chatBox.resizeAspect(); } + /** + * Informs the class when the user's mouse curosr enters and leaves the UI area + * @param {Boolean} onUI - Whether or not onUI should be toggled true + */ setOnUI(onUI){ this.onUI = onUI; this.popUI(); } - //This way other classes don't need to worry about media handler + /** + * Calculates ratio of current media object + * @returns {Number} Current media aspect ratio as a single floating point number + */ getRatio(){ //If we have no media handler if(this.mediaHandler == null){ diff --git a/www/js/channel/userlist.js b/www/js/channel/userlist.js index b72c394..d25be63 100644 --- a/www/js/channel/userlist.js +++ b/www/js/channel/userlist.js @@ -14,15 +14,28 @@ 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 .*/ +/** + * Class for object containing logic behind userlist UX + */ class userList{ + /** + * Instantiates a new userList object + * @param {channel} client - Parent client mgmt object + */ constructor(client){ - //Client object + /** + * Parent Client Management object + */ this.client = client - //Click Dragger Object + /** + * Click Dragger object for handling userlist resizes + */ this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-users-drag-handle", "#chat-panel-users-div", true, this.client.chatBox.clickDragger); - //Strings + /** + * Userlist color array (Maps to css classes) + */ this.userColors = [ "userlist-color0", "userlist-color1", @@ -32,13 +45,29 @@ class userList{ "userlist-color5", "userlist-color6"]; - //Maps + /** + * Map of usernames to assigned username color + */ this.colorMap = new Map(); - //Element Nodes + /** + * users div + */ this.userDiv = document.querySelector("#chat-panel-users-div"); + + /** + * userlist div + */ this.userList = document.querySelector("#chat-panel-users-list-div"); + + /** + * user count label + */ this.userCount = document.querySelector("#chat-panel-user-count"); + + /** + * userlist toggle button + */ this.toggleIcon = document.querySelector("#chat-panel-users-toggle"); //Call setup functions @@ -46,12 +75,17 @@ class userList{ this.defineListeners(); } - //Setup functions + /** + * Defines input-related event listeners + */ setupInput(){ this.toggleIcon.addEventListener("click", ()=>{this.toggleUI()}); this.userCount.addEventListener("click", ()=>{this.toggleUI()}); } + /** + * Defines network-related event listeners + */ defineListeners(){ this.client.socket.on('userList', (data) => { this.updateList(data); @@ -62,6 +96,10 @@ class userList{ }) } + /** + * Updates UX after user list change + * @param {Array} list - Userlist data from server + */ updateList(list){ //Clear list and set user count this.userCount.textContent = list.length == 1 ? '1 User' : `${list.length} Users`; @@ -91,6 +129,11 @@ class userList{ this.clickDragger.fixCutoff(); } + /** + * Renders out a single username to the userlist + * @param {String} user - Username to render + * @param {String} flair - Flair to render as + */ renderUser(user, flair){ //Create user span From c0f219276f4c8f43e5879ca3d251ebdf8f98a27f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 4 Sep 2025 20:11:23 -0400 Subject: [PATCH 079/209] JSDoc for www/js/channel/*.js complete. Just need to hadnle ww/js/channel/panels. --- www/doc/client/cPanel.html | 2611 +++++++++ www/doc/client/channel.html | 6 +- www/doc/client/channel.js.html | 6 +- www/doc/client/chat.js.html | 671 +++ www/doc/client/chatBox.html | 4659 +++++++++++++++++ www/doc/client/chatPostprocessor.html | 1803 +++++++ www/doc/client/chatPostprocessor.js.html | 680 +++ www/doc/client/commandPreprocessor.html | 8 +- www/doc/client/commandPreprocessor.js.html | 10 +- www/doc/client/commandProcessor.html | 6 +- www/doc/client/cpanel.js.html | 524 ++ www/doc/client/global.html | 4 +- www/doc/client/hlsBase.html | 2928 +++++++++++ www/doc/client/hlsLiveStreamHandler.html | 2905 ++++++++++ www/doc/client/index.html | 4 +- www/doc/client/mediaHandler.html | 2710 ++++++++++ www/doc/client/mediaHandler.js.html | 844 +++ www/doc/client/nullHandler.html | 2882 ++++++++++ www/doc/client/panelObj.html | 918 ++++ www/doc/client/player.html | 3383 ++++++++++++ www/doc/client/player.js.html | 483 ++ www/doc/client/poppedPanel.html | 1451 +++++ www/doc/client/rawFileBase.html | 2923 +++++++++++ www/doc/client/rawFileHandler.html | 2905 ++++++++++ www/doc/client/userList.html | 6 +- www/doc/client/userlist.js.html | 6 +- www/doc/client/youtubeEmbedHandler.html | 2900 ++++++++++ www/doc/server/activeChannel.html | 2 +- .../server/app_channel_activeChannel.js.html | 2 +- .../server/app_channel_channelManager.js.html | 2 +- www/doc/server/app_channel_chat.js.html | 2 +- www/doc/server/app_channel_chatBuffer.js.html | 2 +- .../server/app_channel_chatHandler.js.html | 2 +- .../app_channel_commandPreprocessor.js.html | 2 +- .../server/app_channel_connectedUser.js.html | 2 +- .../server/app_channel_media_media.js.html | 2 +- .../app_channel_media_playlistHandler.js.html | 2 +- .../server/app_channel_media_queue.js.html | 2 +- .../app_channel_media_queuedMedia.js.html | 2 +- www/doc/server/app_channel_tokebot.js.html | 2 +- www/doc/server/channelManager.html | 2 +- www/doc/server/chat.html | 2 +- www/doc/server/chatBuffer.html | 2 +- www/doc/server/chatHandler.html | 2 +- www/doc/server/commandPreprocessor.html | 2 +- www/doc/server/commandProcessor.html | 2 +- www/doc/server/connectedUser.html | 2 +- www/doc/server/global.html | 2 +- www/doc/server/index.html | 2 +- www/doc/server/media.html | 2 +- www/doc/server/playlistHandler.html | 2 +- www/doc/server/queue.html | 2 +- www/doc/server/queuedMedia.html | 2 +- .../schemas_channel_channelBanSchema.js.html | 2 +- ...as_channel_channelPermissionSchema.js.html | 2 +- .../schemas_channel_channelSchema.js.html | 2 +- .../server/schemas_channel_chatSchema.js.html | 2 +- .../schemas_channel_media_mediaSchema.js.html | 2 +- ..._channel_media_playlistMediaSchema.js.html | 2 +- ...hemas_channel_media_playlistSchema.js.html | 2 +- ...as_channel_media_queuedMediaSchema.js.html | 2 +- www/doc/server/schemas_emoteSchema.js.html | 2 +- www/doc/server/schemas_flairSchema.js.html | 2 +- .../server/schemas_permissionSchema.js.html | 2 +- www/doc/server/schemas_statSchema.js.html | 2 +- .../schemas_tokebot_tokeCommandSchema.js.html | 2 +- .../schemas_user_emailChangeSchema.js.html | 2 +- .../schemas_user_passwordResetSchema.js.html | 2 +- .../server/schemas_user_userBanSchema.js.html | 2 +- .../server/schemas_user_userSchema.js.html | 2 +- www/doc/server/tokebot.html | 2 +- www/doc/server/utils_altchaUtils.js.html | 2 +- www/doc/server/utils_configCheck.js.html | 2 +- www/doc/server/utils_hashUtils.js.html | 2 +- www/doc/server/utils_linkUtils.js.html | 2 +- www/doc/server/utils_loggerUtils.js.html | 2 +- www/doc/server/utils_mailUtils.js.html | 2 +- .../utils_media_internetArchiveUtils.js.html | 2 +- www/doc/server/utils_media_yanker.js.html | 2 +- www/doc/server/utils_media_ytdlpUtils.js.html | 2 +- www/doc/server/utils_regexUtils.js.html | 2 +- www/doc/server/utils_scheduler.js.html | 2 +- www/doc/server/utils_sessionUtils.js.html | 2 +- www/js/channel/channel.js | 2 +- www/js/channel/chat.js | 152 +- www/js/channel/chatPostprocessor.js | 67 +- www/js/channel/commandPreprocessor.js | 4 +- www/js/channel/cpanel.js | 4 +- www/js/channel/mediaHandler.js | 176 +- www/js/channel/player.js | 2 +- www/js/channel/userlist.js | 2 +- 91 files changed, 38653 insertions(+), 104 deletions(-) create mode 100644 www/doc/client/cPanel.html create mode 100644 www/doc/client/chat.js.html create mode 100644 www/doc/client/chatBox.html create mode 100644 www/doc/client/chatPostprocessor.html create mode 100644 www/doc/client/chatPostprocessor.js.html create mode 100644 www/doc/client/cpanel.js.html create mode 100644 www/doc/client/hlsBase.html create mode 100644 www/doc/client/hlsLiveStreamHandler.html create mode 100644 www/doc/client/mediaHandler.html create mode 100644 www/doc/client/mediaHandler.js.html create mode 100644 www/doc/client/nullHandler.html create mode 100644 www/doc/client/panelObj.html create mode 100644 www/doc/client/player.html create mode 100644 www/doc/client/player.js.html create mode 100644 www/doc/client/poppedPanel.html create mode 100644 www/doc/client/rawFileBase.html create mode 100644 www/doc/client/rawFileHandler.html create mode 100644 www/doc/client/youtubeEmbedHandler.html diff --git a/www/doc/client/cPanel.html b/www/doc/client/cPanel.html new file mode 100644 index 0000000..582e49c --- /dev/null +++ b/www/doc/client/cPanel.html @@ -0,0 +1,2611 @@ + + + + + JSDoc: Class: cPanel + + + + + + + + + + +
+ +

Class: cPanel

+ + + + + + +
+ +
+ +

cPanel(client)

+ +
Class containing code for managing the Canopy Panel UX
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new cPanel(client)

+ + + + + + +
+ Instantiates a new Canopy Panel Management object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client Management Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

activePanel

+ + + + +
+ Active Panel Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelCloseIcon

+ + + + +
+ Active Panel Close Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelDiv

+ + + + +
+ Active Panel Container +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelDoc

+ + + + +
+ Active Title Document Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelDragger

+ + + + +
+ Click-Dragger object for re-sizable active panel +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelPinIcon

+ + + + +
+ Active Panel Pin Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelPopoutIcon

+ + + + +
+ Active Panel Pop-Out Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

activePanelTitle

+ + + + +
+ Active Panel Title +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

client

+ + + + +
+ Parent Client Management object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanel

+ + + + +
+ Pinned Panel Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelCloseIcon

+ + + + +
+ Pinned Panel Close Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelDiv

+ + + + +
+ Pinned Panel Contianer +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelDoc

+ + + + +
+ Pinned Panel Document Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelDragger

+ + + + +
+ Click-Dragger object for re-sizable pinned panel +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelPopoutIcon

+ + + + +
+ Pinned Panel Pop-Out Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelTitle

+ + + + +
+ Pinned Panel Title +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelUnpinIcon

+ + + + +
+ Pinned Panel Un-Pin Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

poppedPanels

+ + + + +
+ Popped Panel Objects +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

hideActivePanel(event, keepAlive)

+ + + + + + +
+ Hides active panel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
event + + +Event + + + + + + Event passed down from Input Handler
keepAlive + + +Boolean + + + + + + false + + Prevents closing panel if true
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

hidePinnedPanel(event, keepAlive)

+ + + + + + +
+ Hides pinned panel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
event + + +Event + + + + + + Passed down input event
keepAlive + + +Boolean + + + + + + false + + Prevents panel.closer() from running if true
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pinPanel()

+ + + + + + +
+ Pins active panel +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

popActivePanel()

+ + + + + + +
+ Pop's out active panel +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

popPanel(panel, panelBody)

+ + + + + + +
+ Pops a new pop-out panel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
panel + + +panelObj + + + + panelObj to apply to the panel
panelBody + + +String + + + + Raw HTML to inject into panel body, injects panel default if left to null
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

popPinnedPanel()

+ + + + + + +
+ Pops pinned panel +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) setActivePanel(panel, panelBody)

+ + + + + + +
+ Sets Active Panel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
panel + + +panelObj + + + + Panel Object to set as active
panelBody + + +String + + + + innerHTML of Panel, pulls from panelObj.getPage() if empty
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) setPinnedPanel(panel, panelBody)

+ + + + + + +
+ Sets pinned panel +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
panel + + +panelObj + + + + Panel Object to apply to panel
panelBody + + +String + + + + Raw HTML to inject into panel body, defaults to panel page if null
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setupInput()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

unpinPanel()

+ + + + + + +
+ Sets pinned panel to active +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/channel.html b/www/doc/client/channel.html index ca741ff..d5f9a33 100644 --- a/www/doc/client/channel.html +++ b/www/doc/client/channel.html @@ -30,7 +30,7 @@

channel()

-
Class for object containing base code for the Canopy channel client.
+
Class containing base code for the Canopy channel client.
@@ -1248,13 +1248,13 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/channel.js.html b/www/doc/client/channel.js.html index 995b6e5..eb8ae9d 100644 --- a/www/doc/client/channel.js.html +++ b/www/doc/client/channel.js.html @@ -43,7 +43,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.*/ /** - * Class for object containing base code for the Canopy channel client. + * Class containing base code for the Canopy channel client. */ class channel{ /** @@ -283,13 +283,13 @@ const client = new channel();
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chat.js.html b/www/doc/client/chat.js.html new file mode 100644 index 0000000..cf1f5f0 --- /dev/null +++ b/www/doc/client/chat.js.html @@ -0,0 +1,671 @@ + + + + + JSDoc: Source: chat.js + + + + + + + + + + +
+ +

Source: chat.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class which represents Canopy Chat Box UI
+ */
+class chatBox{
+    /**
+     * Instantiates a new Chat Box object
+     * @param {channel} client - Parent client Management Object
+     */
+    constructor(client){
+        /**
+         * Parent Client Management Object
+         */
+        this.client = client
+
+        /**
+         * Whether or not chat-size should be locked to current media aspect ratio
+         */
+        this.aspectLock = true;
+
+        /**
+         * Whether or not the chat box should auto-scroll on new chat
+         */
+        this.autoScroll = true;
+
+        /**
+         * Chat Buffer Scroll Top on last scroll
+         */
+        this.lastPos = 0;
+
+        /**
+         * Height of Chat Buffer on last scroll
+         */
+        this.lastHeight = 0;
+
+        /**
+         * Width of Chat Buffer on last scroll
+         */
+        this.lastWidth = 0;
+
+        /**
+         * Click-Dragger Object for handling dynamic chat/video split re-sizing
+         */
+        this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-drag-handle", "#chat-panel-div");
+        
+        /**
+         * Command Pre-Processor Object
+         */
+        this.commandPreprocessor = new commandPreprocessor(client);
+
+        /**
+         * Chat Post-Processor Object
+         */
+        this.chatPostprocessor = new chatPostprocessor(client);
+
+        //Element Nodes
+        /**
+         * Chat Panel Container Div
+         */
+        this.chatPanel = document.querySelector("#chat-panel-div");
+
+        /**
+         * High Level Selector
+         */
+        this.highSelect = document.querySelector("#chat-panel-high-level-select");
+
+        /**
+         * Flair Selector
+         */
+        this.flairSelect = document.querySelector("#chat-panel-flair-select");
+
+        /**
+         * Chat Buffer Div
+         */
+        this.chatBuffer = document.querySelector("#chat-panel-buffer-div");
+
+        /**
+         * Chat Prompt
+         */
+        this.chatPrompt = document.querySelector("#chat-panel-prompt");
+
+        /**
+         * Auto-Complete Placeholder
+         */
+        this.autocompletePlaceholder = document.querySelector("#chat-panel-prompt-autocomplete-filler");
+
+        /**
+         * Auto-Complete Display
+         */
+        this.autocompleteDisplay = document.querySelector("#chat-panel-prompt-autocomplete-display");
+
+        /**
+         * Settings Panel Icon
+         */
+        this.settingsIcon = document.querySelector("#chat-panel-settings-icon");
+
+        /**
+         * Admin Panel Icon
+         */
+        this.adminIcon = document.querySelector("#chat-panel-admin-icon");
+
+        /**
+         * Emote Icon
+         */
+        this.emoteIcon = document.querySelector("#chat-panel-emote-icon");
+
+        /**
+         * Send Chat/Command Button
+         */
+        this.sendButton = document.querySelector("#chat-panel-send-button");
+
+        /**
+         * Aspect Lock Icon
+         * Seems weird to stick this in here, but the split is dictated by chat width :P
+         */
+        this.aspectLockIcon = document.querySelector("#media-panel-aspect-lock-icon");
+
+        /**
+         * Hide Chat Icon
+         */
+        this.hideChatIcon = document.querySelector("#chat-panel-div-hide");
+
+        /**
+         * Show Chat Icon
+         */
+        this.showChatIcon = document.querySelector("#media-panel-show-chat-icon");
+
+        //Setup functions
+        this.setupInput();
+        this.defineListeners();
+        this.sizeToAspect();
+    }
+
+    /**
+     * Defines input-related event listeners
+     */
+    setupInput(){
+        //Chat bar
+        this.chatPrompt.addEventListener("keydown", this.send.bind(this));
+        this.chatPrompt.addEventListener("keydown", this.tabComplete.bind(this));
+        this.chatPrompt.addEventListener("input", this.displayAutocomplete.bind(this));
+        this.autocompleteDisplay.addEventListener("click", this.tabComplete.bind(this));
+        this.sendButton.addEventListener("click", this.send.bind(this));
+        this.settingsIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new settingsPanel(client))});
+        this.adminIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new queuePanel(client))});
+        this.emoteIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new emotePanel(client))});
+
+        //Header icons
+        this.aspectLockIcon.addEventListener("click", this.lockAspect.bind(this));
+        this.showChatIcon.addEventListener("click", ()=>{this.toggleUI()});
+        this.hideChatIcon.addEventListener("click", ()=>{this.toggleUI()});
+        this.highSelect.addEventListener("change", this.setHighLevel.bind(this));
+        this.flairSelect.addEventListener("change", this.setFlair.bind(this));
+
+        //Clickdragger/Resize
+        this.clickDragger.handle.addEventListener("mousedown", this.unlockAspect.bind(this));
+        this.clickDragger.handle.addEventListener("clickdrag", this.handleAutoScroll.bind(this));
+        window.addEventListener("resize", this.resizeAspect.bind(this));
+
+        //chatbuffer
+        this.chatBuffer.addEventListener('scroll', this.scrollHandler.bind(this));
+    }
+
+    /**
+     * Defines network-related event listners
+     */
+    defineListeners(){
+        this.client.socket.on("chatMessage", this.displayChat.bind(this));
+        this.client.socket.on("clearChat", this.clearChat.bind(this));
+    }
+
+    /**
+     * Clears chat on command from server
+     * @param {Object} data - Data from server
+     */
+    clearChat(data){
+        //If we where passed a user to check
+        if(data.user != null){
+            var clearedChats = document.querySelectorAll(`.chat-entry-${data.user}`);
+        }else{
+            var clearedChats = document.querySelectorAll('.chat-entry');
+        }
+
+        //For each chat found
+        clearedChats.forEach((chat) => {
+            //fuckin' nukem!
+            chat.remove();
+        });
+    }
+
+    /**
+     * Receives, Post-Processes, and Displays chat messages from server
+     * @param {Object} data De-hydrated chat object from server
+     */
+    displayChat(data){
+        //Create chat-entry span
+        var chatEntry = document.createElement('span');
+        chatEntry.classList.add("chat-panel-buffer","chat-entry",`chat-entry-${data.user}`);
+
+        //Create high-level label
+        var highLevel = document.createElement('p');
+        highLevel.classList.add("chat-panel-buffer","chat-entry-high-level","high-level");
+        highLevel.textContent = utils.unescapeEntities(`${data.highLevel}`);
+        chatEntry.appendChild(highLevel);
+
+        //If we're not using classic flair
+        if(data.flair != "classic"){
+            //Use flair
+            var flair = `flair-${data.flair}`;
+        //Otherwise
+        }else{
+            //Pull user's assigned color from the color map
+            var flair =  this.client.userList.colorMap.get(data.user);
+        }
+
+        //Create username label
+        var userLabel = document.createElement('p');
+        userLabel.classList.add("chat-panel-buffer", "chat-entry-username", );
+        
+        //Create color span
+        var flairSpan = document.createElement('span');
+        flairSpan.classList.add("chat-entry-flair-span", flair);
+        flairSpan.innerHTML = data.user;
+
+        //Inject flair span into user label before the colon
+        userLabel.innerHTML = `${flairSpan.outerHTML}: `;
+
+        //Append user label
+        chatEntry.appendChild(userLabel);
+
+        //Create chat body
+        var chatBody = document.createElement('p');
+        chatBody.classList.add("chat-panel-buffer","chat-entry-body");
+        chatEntry.appendChild(chatBody);
+
+        //Append the post-processed chat-body to the chat buffer
+        this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data));
+
+        //Set size to aspect on launch
+        this.resizeAspect();
+    }
+
+    /**
+     * Concatinate Text into Chat Prompt
+     * @param {String} text - Text to Concatinate
+     */
+    catChat(text){
+        this.chatPrompt.value += text;
+        this.displayAutocomplete();
+    }
+
+    /**
+     * Calls a toke command out with a specified user
+     * @param {String} user - User to toke with
+     */
+    tokeWith(user){
+        this.commandPreprocessor.preprocess(user == this.client.user.user ? "!toke up fuckers" : `!toke up ${user}`);
+    }
+
+    /**
+     * Pre-processes and sends text from chat prompt to server
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    send(event){
+        if((!event || !event.key || event.key == "Enter") && this.chatPrompt.value){
+            this.commandPreprocessor.preprocess(this.chatPrompt.value);
+            //Clear our prompt and autocomplete nodes
+            this.chatPrompt.value = "";
+            this.autocompletePlaceholder.innerHTML = '';
+            this.autocompleteDisplay.innerHTML = '';
+        }
+    }
+
+    /**
+     * Displays auto-complete text against current prompt input
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    displayAutocomplete(event){
+        //Find current match
+        const match = this.checkAutocomplete();
+
+        //Set placeholder to space out the autocomplete display
+        //Use text content because it's unescaped, and while this only effects local users, it'll keep someone from noticing and whinging about it
+        this.autocompletePlaceholder.textContent = this.chatPrompt.value;
+        //Set the autocomplete display
+        this.autocompleteDisplay.textContent = match.match.replace(match.word, '');
+    }
+
+    /**
+     * Called upon tab-complete
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    tabComplete(event){
+        //If we hit tab or this isn't a keyboard event
+        if(event.key == "Tab" || event.key == null){
+            //Prevent default action
+            event.preventDefault();
+
+            //return focus to the chat prompt
+            this.chatPrompt.focus();
+
+            //Grab autocompletion match
+            const match = this.checkAutocomplete();
+
+            //If we have a match
+            if(match.match != ''){
+                //Autocomplete the current word
+                this.chatPrompt.value += match.match.replace(match.word, '');
+
+                //Clear out the autocomplete display
+                this.autocompleteDisplay.innerHTML = '';
+            }
+        }
+    }
+
+    /**
+     * Checks string input against auto-complete dictionary to generate the best guess as to what the user is typing
+     * @param {String} input - Current input from Chat Prompt
+     * @returns {Object} Object containing word we where handed and the match we found
+     */
+    checkAutocomplete(input = this.chatPrompt.value){
+        //Rebuild this fucker every time because it really doesn't take that much compute power and emotes/used tokes change
+        //Worst case we could store it persistantly and update as needed but I think that might be much
+        const dictionary = this.commandPreprocessor.buildAutocompleteDictionary();       
+
+        //Split our input by whitespace
+        const splitInput = input.split(/\s/g);
+        //Get the current word we're working on
+        const word = splitInput[splitInput.length - 1];
+        let matches = [];
+
+
+        //Run through dictionary sets
+        for(let set of Object.keys(dictionary)){
+            //Go through the current definitions of the current dictionary set
+            //I went with a for loop instead of a filter beacuse I wanted to pull the processed definition with pre/postfix
+            //and also directly push it into a shared array :P
+            for(let cmd of dictionary[set].cmds){
+
+                //Append the proper prefix/postfix to the current command
+                const definition = (`${dictionary[set].prefix}${cmd[0]}${dictionary[set].postfix}`);
+
+                //if definition starts with the current word and the command is enabled
+                if((word == '' ? false : definition.indexOf(word) == 0) && cmd[1]){
+                    //Add definition to match list
+                    matches.push(definition);
+                }
+            }
+        }
+
+        //If we found jack shit
+        if(matches.length == 0){
+            //Return jack shit
+            return {
+                match: '',
+                word
+            };
+        //If we got something
+        }else{
+            //return our top match
+            return {
+                match: matches[0],
+                word
+            };
+
+        }
+    }
+
+    /**
+     * Handles initial client meta-data dump from server upon connection
+     * @param {Object} data - Data dump from server
+     */
+    handleClientInfo(data){
+        this.updateFlairSelect(data.flairList, data.user.flair);
+        this.updateHighSelect(data.user.highLevel);
+    }
+
+    /**
+     * Sets user high-level
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    setHighLevel(event){
+        const highLevel = event.target.value;
+
+        this.client.socket.emit("setHighLevel", {highLevel});
+    }
+
+    /**
+     * Sets user flair
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    setFlair(event){
+        const flair = event.target.value;
+
+        this.client.socket.emit("setFlair", {flair});
+    }
+
+    /**
+     * Handles High-Level updates from the server
+     * @param {Number} highLevel - High Level to Set
+     */
+    updateHighSelect(highLevel){
+        this.highSelect.value = highLevel;
+    }
+
+    /**
+     * Handles flair updates from the server
+     * @param {Array} fliarList - List of flairs to put into flair select
+     * @param {String} fliar - Flair to set
+     */
+    updateFlairSelect(flairList, flair){
+        //clear current flair select
+        this.flairSelect.innerHTML = "";
+
+        //For each flair in flairlist
+        flairList.forEach((flair) => {
+            //Create an option
+            var flairOption = document.createElement('option');
+            //Set the name and innerHTML
+            flairOption.value = flair.name;
+            flairOption.textContent = utils.unescapeEntities(flair.displayName);
+
+            //Append it to the select
+            this.flairSelect.appendChild(flairOption);
+        });
+
+        //Set the selected flair in the UI
+        this.flairSelect.value = flair;
+        //Re-style the UI, do this in two seperate steps in-case we're running for the first time and have nothing to replace.
+        this.flairSelect.className = this.flairSelect.className.replace(/flair-\S*/, "");
+        this.flairSelect.classList.add(`flair-${flair}`);
+    }
+
+    /**
+     * Locks chat-size to aspect ratio of media
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    lockAspect(event){
+        //prevent the user from breaking shit :P
+        if(this.chatPanel.style.display != "none"){
+            this.aspectLock = true;
+            this.aspectLockIcon.style.display = "none";
+            this.sizeToAspect();
+        }
+    }
+
+    /**
+     * Un-locks chat-size to aspect ratio of media
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    unlockAspect(event){
+        //Disable aspect lock
+        this.aspectLock = false;
+
+        //Show aspect lock icon
+        this.aspectLockIcon.style.display = "inline";
+    }
+
+L    /**
+     * Re-sizes chat back to aspect ratio on window re-size when chat box is aspect locked
+     * Also prevents horizontal scroll-bars from chat/window resizing
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    resizeAspect(event){
+        const playerHidden = this.client.player.playerDiv.style.display == "none";
+
+        //If the aspect is locked and the player is hidden
+        if(this.aspectLock && !playerHidden){
+            this.sizeToAspect();
+        //Otherwise
+        }else{
+            //Fix the clickDragger on userlist
+            this.client.userList.clickDragger.fixCutoff();
+        }
+
+        //Autoscroll chat in-case we fucked it up
+        this.handleAutoScroll();
+    }
+
+L    /**
+     * Re-sizes chat box relative to media aspect ratio
+     */
+    sizeToAspect(){
+        if(this.chatPanel.style.display != "none"){
+            var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height;
+            const targetChatWidth = window.innerWidth - targetVidWidth;
+            //This should be changeable in settings later on, for now it defaults to 20%
+            const limit = window.innerWidth * .2;
+
+            //Set width to target or 20vh depending on whether or not we've hit the width limit
+            this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : '20vh';
+
+            //Fix busted layout
+            var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
+            this.chatPanel.style.flexBasis = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
+            //This sometimes gets called before userList ahs been initiated :p
+            if(this.client.userList != null){
+                this.client.userList.clickDragger.fixCutoff();
+            }
+        }
+    } 
+
+    /**
+     * Toggles Chat Box UX
+     * @param {Boolean} show - Whether or not to show Chat Box UX
+     */
+    toggleUI(show = !this.chatPanel.checkVisibility()){
+        if(show){
+            this.chatPanel.style.display = "flex";
+            this.showChatIcon.style.display = "none";
+            this.client.player.hideVideoIcon.style.display = "flex";
+            this.client.userList.clickDragger.fixCutoff();
+        }else{
+            this.chatPanel.style.display = "none";
+            this.showChatIcon.style.display = "flex";
+            this.client.player.hideVideoIcon.style.display = "none";
+        }
+    }
+
+    /**
+     * Handles Video Toggling
+     * @param {Boolean} show - Whether or not the video is currently being hidden
+     */
+    handleVideoToggle(show){
+        //If we're enabling the video
+        if(show){
+            //Show hide chat icon
+            this.hideChatIcon.style.display = "flex";
+
+            //Re-enable the click dragger
+            this.clickDragger.enabled = true;
+
+            //Lock the chat to aspect ratio of the video, to make sure the chat width isn't breaking shit
+            this.lockAspect();
+        //If we're disabling the video
+        }else{
+            //Hide hide hide hide hide hide chat icon
+            this.hideChatIcon.style.display = "none";
+
+            //Need to clear the width from the split, or else it doesn't display properly
+            this.chatPanel.style.flexBasis = "100%";
+
+            //Disable the click dragger
+            this.clickDragger.enabled = false;
+        }
+    }
+
+    /**
+     * Handles scrolling within the chat buffer
+     * @param {Event} event - Event passed down from Event Handler
+     */
+    scrollHandler(event){
+        //If we're just starting out
+        if(this.lastPos == 0){
+            //Set last pos for the first time
+            this.lastPos = this.chatBuffer.scrollTop;
+        }
+
+        //Calculate scroll delta
+        const deltaY = this.chatBuffer.scrollTop - this.lastPos;
+
+        //Grab visible bounding rect so we don't have to do it again (can't use offset because someone might zoom in :P)
+        const bufferRect = this.chatBuffer.getBoundingClientRect();
+        const bufferHeight = Math.round(bufferRect.height);
+        const bufferWidth = Math.round(bufferRect.width);
+
+        if(this.lastHeight == 0){
+            this.lastHeight = bufferHeight;
+        }
+
+        if(this.lastWidth == 0){
+            this.lastWidth = bufferWidth;
+        }
+
+        //If we're scrolling up
+        if(deltaY < 0){
+            //If we have room to scroll, and we didn't resize
+            if(this.chatBuffer.scrollHeight > bufferHeight && (this.lastWidth == bufferWidth && this.lastHeight == bufferHeight)){
+                //Disable auto scrolling
+                this.autoScroll = false;
+            }else{
+                this.handleAutoScroll();
+            }
+        //Otherwise if the difference between the chat buffers scroll height and offset height is equal to the scroll top
+        //(Because it is scrolled all the way down)
+        }else if((this.chatBuffer.scrollHeight - bufferHeight) == this.chatBuffer.scrollTop){
+            this.autoScroll = true;
+        }
+
+        //Set last post/size for next the run
+        this.lastPos = this.chatBuffer.scrollTop;
+        this.lastHeight = bufferHeight;
+        this.lastWidth = bufferWidth;
+    }
+
+    /**
+     * Auto-scrolls chat buffer when new chats are entered.
+     */
+    handleAutoScroll(){
+        //If autoscroll is enabled
+        if(this.autoScroll){
+            //Set chatBuffer scrollTop to the difference between scrollHeight and buffer height (scroll to the bottom)
+            this.chatBuffer.scrollTop = this.chatBuffer.scrollHeight - Math.round(this.chatBuffer.getBoundingClientRect().height);
+        }
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/chatBox.html b/www/doc/client/chatBox.html new file mode 100644 index 0000000..255128e --- /dev/null +++ b/www/doc/client/chatBox.html @@ -0,0 +1,4659 @@ + + + + + JSDoc: Class: chatBox + + + + + + + + + + +
+ +

Class: chatBox

+ + + + + + +
+ +
+ +

chatBox(client)

+ +
Class which represents Canopy Chat Box UI
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new chatBox(client)

+ + + + + + +
+ Instantiates a new Chat Box object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client Management Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

adminIcon

+ + + + +
+ Admin Panel Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

aspectLock

+ + + + +
+ Whether or not chat-size should be locked to current media aspect ratio +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

aspectLockIcon

+ + + + +
+ Aspect Lock Icon +Seems weird to stick this in here, but the split is dictated by chat width :P +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

autoScroll

+ + + + +
+ Whether or not the chat box should auto-scroll on new chat +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

autocompleteDisplay

+ + + + +
+ Auto-Complete Display +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

autocompletePlaceholder

+ + + + +
+ Auto-Complete Placeholder +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

chatBuffer

+ + + + +
+ Chat Buffer Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

chatPanel

+ + + + +
+ Chat Panel Container Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

chatPostprocessor

+ + + + +
+ Chat Post-Processor Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

chatPrompt

+ + + + +
+ Chat Prompt +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

clickDragger

+ + + + +
+ Click-Dragger Object for handling dynamic chat/video split re-sizing +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

commandPreprocessor

+ + + + +
+ Command Pre-Processor Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

emoteIcon

+ + + + +
+ Emote Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

flairSelect

+ + + + +
+ Flair Selector +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

hideChatIcon

+ + + + +
+ Hide Chat Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

highSelect

+ + + + +
+ High Level Selector +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

lastHeight

+ + + + +
+ Height of Chat Buffer on last scroll +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

lastPos

+ + + + +
+ Chat Buffer Scroll Top on last scroll +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

lastWidth

+ + + + +
+ Width of Chat Buffer on last scroll +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

sendButton

+ + + + +
+ Send Chat/Command Button +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

settingsIcon

+ + + + +
+ Settings Panel Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

showChatIcon

+ + + + +
+ Show Chat Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

catChat(text)

+ + + + + + +
+ Concatinate Text into Chat Prompt +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + + +String + + + + Text to Concatinate
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

checkAutocomplete(input) → {Object}

+ + + + + + +
+ Checks string input against auto-complete dictionary to generate the best guess as to what the user is typing +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
input + + +String + + + + Current input from Chat Prompt
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Object containing word we where handed and the match we found +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

clearChat(data)

+ + + + + + +
+ Clears chat on command from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines network-related event listners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

displayAutocomplete(event)

+ + + + + + +
+ Displays auto-complete text against current prompt input +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

displayChat(data)

+ + + + + + +
+ Receives, Post-Processes, and Displays chat messages from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + De-hydrated chat object from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleAutoScroll()

+ + + + + + +
+ Auto-scrolls chat buffer when new chats are entered. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleClientInfo(data)

+ + + + + + +
+ Handles initial client meta-data dump from server upon connection +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Data dump from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleVideoToggle(show)

+ + + + + + +
+ Handles Video Toggling +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
show + + +Boolean + + + + Whether or not the video is currently being hidden
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

lockAspect(event)

+ + + + + + +
+ Locks chat-size to aspect ratio of media +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

resizeAspect(event)

+ + + + + + +
+ Re-sizes chat back to aspect ratio on window re-size when chat box is aspect locked +Also prevents horizontal scroll-bars from chat/window resizing +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

scrollHandler(event)

+ + + + + + +
+ Handles scrolling within the chat buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

send(event)

+ + + + + + +
+ Pre-processes and sends text from chat prompt to server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setFlair(event)

+ + + + + + +
+ Sets user flair +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setHighLevel(event)

+ + + + + + +
+ Sets user high-level +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setupInput()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sizeToAspect()

+ + + + + + +
+ Re-sizes chat box relative to media aspect ratio +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

tabComplete(event)

+ + + + + + +
+ Called upon tab-complete +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toggleUI(show)

+ + + + + + +
+ Toggles Chat Box UX +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
show + + +Boolean + + + + Whether or not to show Chat Box UX
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

tokeWith(user)

+ + + + + + +
+ Calls a toke command out with a specified user +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
user + + +String + + + + User to toke with
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

unlockAspect(event)

+ + + + + + +
+ Un-locks chat-size to aspect ratio of media +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down from Event Handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateFlairSelect(fliarList, fliar)

+ + + + + + +
+ Handles flair updates from the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
fliarList + + +Array + + + + List of flairs to put into flair select
fliar + + +String + + + + Flair to set
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateHighSelect(highLevel)

+ + + + + + +
+ Handles High-Level updates from the server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
highLevel + + +Number + + + + High Level to Set
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/chatPostprocessor.html b/www/doc/client/chatPostprocessor.html new file mode 100644 index 0000000..4673ea7 --- /dev/null +++ b/www/doc/client/chatPostprocessor.html @@ -0,0 +1,1803 @@ + + + + + JSDoc: Class: chatPostprocessor + + + + + + + + + + +
+ +

Class: chatPostprocessor

+ + + + + + +
+ +
+ +

chatPostprocessor(client)

+ +
Class contianing client-side message post-processing code
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new chatPostprocessor(client)

+ + + + + + +
+ Instantiates a new Chat Post-Processor object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client Management Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

addWhitespace()

+ + + + + + +
+ Injects invisible whitespace in long-ass words to prevent fucking up the chat buffer size +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleChatType()

+ + + + + + +
+ Marks chat nodes in-case of non-standard chat types +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

injectBody()

+ + + + + + +
+ Injects word objects into chat-entry as proper DOM Nodes +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

postprocess(chatEntry, rawData) → {Node}

+ + + + + + +
+ Post-Processes a single message from the server and returns a presntable DOM Node +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chatEntry + + +Node + + + + Chat entry generated by initial chatBox method
rawData + + +Object + + + + Raw data from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Post-Processed Chat Entry +
+ + + +
+
+ Type +
+
+ +Node + + +
+
+ + + + + + + + + + + + + +

processBold()

+ + + + + + +
+ Processes in-line Bold/Strong text +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processChannelNames()

+ + + + + + +
+ Processes clickable channel names in chat +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processCommandExamples()

+ + + + + + +
+ Processes clickable command examples in chat +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processFilter(delimiter, cb) → {Array}

+ + + + + + +
+ Searches for text in-between a specific delimiter and runs a given callback against it + +Internal command used by several text filters to prevent code re-writes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
delimiter + + +String + + + + delimiter to search string by
cb + + +function + + + + Callback function to run against found strings
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - list of found instances of filter +
+ + + +
+
+ Type +
+
+ +Array + + +
+
+ + + + + + + + + + + + + +

processItalics()

+ + + + + + +
+ Processes in-line Italics +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Processes clickable links and embedded media +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processQoute()

+ + + + + + +
+ Processes qouted text in chat +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processSpoilers()

+ + + + + + +
+ Processes in-line spoilers +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processStrikethrough()

+ + + + + + +
+ Processes in-line Strike-through +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

processUsernames()

+ + + + + + +
+ Processes clickable username callouts in chat +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

splitMessage()

+ + + + + + +
+ Splits message into an array of Word Objects for further processing +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/chatPostprocessor.js.html b/www/doc/client/chatPostprocessor.js.html new file mode 100644 index 0000000..ae4b63c --- /dev/null +++ b/www/doc/client/chatPostprocessor.js.html @@ -0,0 +1,680 @@ + + + + + JSDoc: Source: chatPostprocessor.js + + + + + + + + + + +
+ +

Source: chatPostprocessor.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class contianing client-side message post-processing code
+ */
+class chatPostprocessor{
+    /**
+     * Instantiates a new Chat Post-Processor object
+     * @param {channel} client - Parent client Management Object
+     */
+    constructor(client){
+        /**
+         * Parent Client Management Object
+         */
+        this.client = client;
+    }
+
+    /**
+     * Post-Processes a single message from the server and returns a presntable DOM Node
+     * @param {Node} chatEntry - Chat entry generated by initial chatBox method
+     * @param {Object} rawData - Raw data from server
+     * @returns {Node} Post-Processed Chat Entry
+     */
+    postprocess(chatEntry, rawData){
+        //Create empty array to hold filter spans
+        this.filterSpans = [];
+        //Set raw message data
+        this.rawData = rawData;
+        //Set current chat nodes
+        this.chatEntry = chatEntry;
+        this.chatBody = this.chatEntry.querySelector(".chat-entry-body");
+
+        //Split the chat message into an array of objects representing each word/chunk
+        this.splitMessage();
+
+        //Process Qoutes
+        this.processQoute();
+
+        //Re-Hydrate and Inject links and embedded media into un-processed placeholders
+        this.processLinks();
+
+        //Inject clickable command examples
+        this.processCommandExamples();
+
+        //Inject clickable channel names
+        this.processChannelNames();
+
+        //Inject clickable usernames
+        this.processUsernames();
+
+        //Detect inline spoilers
+        this.processSpoilers();
+
+        //Detect inline strikethrough
+        this.processStrikethrough();
+
+        //Detect inline bold text
+        this.processBold();
+
+        //Detect inline italics
+        this.processItalics();
+
+        //Inject whitespace into long ass-words
+        this.addWhitespace();
+
+        //Handle non-standard chat types
+        this.handleChatType();
+                
+        //Inject the pre-processed chat hyper-text into the chatEntry node
+        this.injectBody();
+
+        //Return the pre-processed node
+        return this.chatEntry;
+    }
+
+    /**
+     * Splits message into an array of Word Objects for further processing
+     */
+    splitMessage(){
+        //Create an empty array to hold the body
+        this.messageArray = [];
+
+        //Unescape any sanatized char codes as we use .textContent for double-safety, and to prevent splitting of char codes
+        //Split string by word-boundries on words and non-word boundries around whitespace, with negative lookaheads to exclude file seperators so we don't split link placeholders, and dashes so we dont split usernames and other things
+        //Also split by any invisble whitespace as a crutch to handle mushed links/emotes
+        //If we can one day figure out how to split non-repeating special chars instead of special chars with whitespace, that would be perf, unfortunately my brain hasn't rotted enough to understand regex like that just yet.
+        const splitString = utils.unescapeEntities(this.rawData.msg).split(/(?<!-)(?<!␜)(?=\w)\b|(?!-)(?<=\w)\b|(?=\s)\B|(?<=\s)\B|ㅤ/g);
+
+        //for each word in the splitstring
+        splitString.forEach((string) => {
+            //create a word object
+            const wordObj = {
+                string: string,
+                filterClasses: [],
+                type: "word"
+            }
+
+            //Add it to our body array
+            this.messageArray.push(wordObj);
+        });
+    }
+
+    /**
+     * Injects word objects into chat-entry as proper DOM Nodes
+     */
+    injectBody(){
+        //Create an empty array to hold the objects to inject
+        const injectionArray = [];
+
+        //For each word object
+        this.messageArray.forEach((wordObj) => {
+            if(wordObj.type == 'word'){
+                //Create span node
+                const span = document.createElement('span');
+
+                //Set span filter classes
+                span.classList.add(...wordObj.filterClasses);
+
+                //Set span text
+                span.textContent = wordObj.string;
+                
+                //Inject node into array
+                injectionArray.push(span);
+            }else if(wordObj.type == 'link'){
+                //Create a link node from our link
+                const link = document.createElement('a');
+                link.classList.add('chat-link', ...wordObj.filterClasses);
+                link.href = wordObj.link;
+                link.target = "_blank";
+                //Use textContent to be safe since links can't be escaped serverside
+                link.textContent = wordObj.link;
+
+                //Append node to chatBody
+                combineNode(wordObj, link);
+            }else if(wordObj.type == 'deadLink'){
+                //Create a text span node from our link
+                const badLink = document.createElement('a');
+                badLink.classList.add('chat-dead-link', 'danger-link', ...wordObj.filterClasses);
+                badLink.href = wordObj.link;
+                badLink.target = "_blank";
+                //Use textContent to be safe since links can't be escaped serverside
+                badLink.textContent = wordObj.link;
+
+                //Append node to chatBody
+                combineNode(wordObj, badLink);
+            }else if(wordObj.type == 'malformedLink'){
+                //Create a text span node from our link
+                const malformedLink = document.createElement('span');
+                malformedLink.classList.add('chat-malformed-link', ...wordObj.filterClasses);
+                //Use textContent to be safe since links can't be escaped (this is why we don't just add it using injectString)
+                //arguably we could sanatize malformed links serverside since they're never actually used as links
+                malformedLink.textContent = wordObj.link;
+
+                //Append node to chatBody
+                combineNode(wordObj, malformedLink);
+            }else if(wordObj.type == 'image'){
+                //Create an img node from our link
+                const img = document.createElement('img');
+                img.classList.add('chat-img', ...wordObj.filterClasses);
+                img.src = wordObj.link;
+
+                //Look for an emote by link since emotes are tx'd as bare links
+                const emote = this.client.chatBox.commandPreprocessor.getEmoteByLink(wordObj.link);
+
+                //If this is a known emote
+                if(emote != null){
+                    //Set the hover text to the emote's name
+                    img.title = `[${emote.name}]`;
+                }
+
+                //Append node to chatBody
+                combineNode(wordObj, img);
+            }else if(wordObj.type == 'video'){
+                //Create a video node from our link
+                const vid = document.createElement('video');
+                vid.classList.add('chat-video', ...wordObj.filterClasses);
+                vid.src = wordObj.link;
+                vid.controls = false;
+                vid.autoplay = true;
+                vid.loop = true;
+                vid.muted = true;
+
+                //Look for an emote by link since emotes are tx'd as bare links
+                const emote = this.client.chatBox.commandPreprocessor.getEmoteByLink(wordObj.link);
+
+                //If this is a known emote
+                if(emote != null){
+                    //Set the hover text to the emote's name
+                    vid.title = `[${emote.name}]`;
+                }
+
+                combineNode(wordObj, vid);
+            }else if(wordObj.type == 'command'){
+                //Create link node
+                const link = document.createElement('a');
+                //Set class
+                link.classList.add('chat-link', ...wordObj.filterClasses);
+                //Set href and inner text
+                link.href = "javascript:";
+                link.textContent = wordObj.command;
+
+                //Add chatbox functionality
+                link.addEventListener('click', () => {this.client.chatBox.commandPreprocessor.preprocess(wordObj.command)});
+
+                //We don't have to worry about injecting this into whitespace since there shouldn't be any here.
+                injectionArray.push(link);
+            }else if(wordObj.type == "username"){
+                //Create link node
+                const link = document.createElement('a');
+                //set class
+                link.classList.add(wordObj.color, ...wordObj.filterClasses);
+                //Set href and inner text
+                link.href = "javascript:";
+                link.textContent = wordObj.string;
+
+                //add chatbox functionality
+                link.addEventListener('click', () => {this.client.chatBox.chatPrompt.value += `${wordObj.string} `});
+
+                //We don't have to worry about injecting this into whitespace since there shouldn't be any here.
+                injectionArray.push(link);
+            }else if(wordObj.type == "channel"){
+                //Create link node
+                const link = document.createElement('a');
+                //set class
+                link.classList.add('chat-link', ...wordObj.filterClasses);
+                //Set href and inner text
+                link.href = `/c/${wordObj.chan}`;
+                link.target = "_blank"
+                link.textContent = wordObj.string;
+
+                //We don't have to worry about injecting this into whitespace since there shouldn't be any here.
+                injectionArray.push(link);
+            }else{
+                console.warn("Unknown chat postprocessor word type:");
+                console.warn(wordObj);
+            }
+
+        });
+
+        //For each item found in the injection array
+        for(let itemIndex in injectionArray){
+            const item = injectionArray[itemIndex];
+
+            //Currently this doesnt support multiple overlapping span-type filters
+            //not a huge deal since we only have once (spoiler)
+            //All others can be applied per-node without any usability side effects
+            const curFilter = this.filterSpans.filter(filterFilters)[0];
+            let appendBody = this.chatBody;
+
+            //If we have a filter span
+            if(curFilter != null){
+                //If we're beggining the array
+                if(itemIndex == curFilter.index[0]){
+                    //Create the span
+                    appendBody = document.createElement('span');
+                    //Label it for what it is
+                    appendBody.classList.add(curFilter.class);
+                    //Add it to the chat body
+                    this.chatBody.appendChild(appendBody);
+                //Otherwise
+                }else{
+                    //Use the existing span
+                    appendBody = (this.chatBody.children[this.chatBody.children.length - 1]);
+                }
+            }
+
+            //Append the node to our chat body
+            appendBody.appendChild(item);
+
+            function filterFilters(filter){
+                //If the index is within the filter span
+                return filter.index[0] <= itemIndex && filter.index[1] >= itemIndex;
+            }
+        }
+
+        //Like string.replace except it actually injects the node so we can keep things like event handlers
+        function combineNode(wordObj, node, placeholder = '␜'){
+            //Split string by the placeholder so we can keep surrounding whitespace
+            const splitWord = wordObj.string.split(placeholder, 2);
+
+            //Create combined node
+            const combinedSpan = document.createElement('span');
+
+            //Add the first part of the text
+            combinedSpan.textContent = splitWord[0];
+
+            //Add in the requestd node
+            combinedSpan.appendChild(node);
+
+            //Finish it off with the last bit of text
+            combinedSpan.insertAdjacentText('beforeend', splitWord[1]);
+
+            //Add to injection array as three nested items to keep arrays lined up
+            injectionArray.push(combinedSpan);
+        } 
+    }
+
+    /**
+     * Processes qouted text in chat
+     */
+    processQoute(){
+        //If the message starts off with '>'
+        if(this.messageArray[0].string[0] == '>'){
+            this.chatBody.classList.add("qoute");
+        }
+    }
+
+    /**
+     * Processes clickable command examples in chat
+     */
+    processCommandExamples(){
+        //for each word object in the body
+        this.messageArray.forEach((wordObj, wordIndex) => {
+            //if the word object hasn't been pre-processed elsewhere
+            if(wordObj.type == "word"){
+                //Get last char of current word
+                const lastChar = wordObj.string[wordObj.string.length - 1];
+
+                //if the last char is !
+                if(lastChar == '!' || lastChar == '/'){
+                    //get next word
+                    const nextWord = this.messageArray[wordIndex + 1];
+                    //if we have another word
+                    if(nextWord != null){
+                        const command = lastChar + nextWord.string;
+                        //Take out the command marker
+                        this.messageArray[wordIndex].string = wordObj.string.slice(0,-1);
+
+                        const commandObj = {
+                            type: "command",
+                            string: nextWord.string,
+                            filterClasses: [],
+                            command: command
+                        }
+
+                        this.messageArray[wordIndex + 1] = commandObj;
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Processes clickable channel names in chat
+     */
+    processChannelNames(){
+        //for each word object in the body
+        this.messageArray.forEach((wordObj, wordIndex) => {
+            //if the word object hasn't been pre-processed elsewhere
+            if(wordObj.type == "word"){
+                //Get last char of current word with slashes pounds
+                const lastChar = wordObj.string[wordObj.string.length - 1];
+                const secondLastChar = wordObj.string[wordObj.string.length - 2];
+
+                //if the last char is # and the second to last char isn't & or # (avoid spoilers)
+                if(lastChar == '#' && secondLastChar != '#'){
+                    //get next word
+                    const nextWord = this.messageArray[wordIndex + 1];
+                    //if we have another word
+                    if(nextWord != null){
+                        //Take out the chan marker
+                        this.messageArray[wordIndex].string = wordObj.string.slice(0,-1);
+
+                        const commandObj = {
+                            type: "channel",
+                            string: lastChar + nextWord.string,
+                            filterClasses: [],
+                            chan: nextWord.string
+                        }
+
+                        this.messageArray[wordIndex + 1] = commandObj;
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Processes clickable username callouts in chat
+     */
+    processUsernames(){
+        //for each word object in the body
+        this.messageArray.forEach((wordObj, wordIndex) => {
+            //if the word object hasn't been pre-processed elsewhere
+            if(wordObj.type == "word"){
+                //Check for user and get their color
+                const color = this.client.userList.colorMap.get(wordObj.string);
+
+                //If the current word is the username of a connected user
+                if(color != null){
+                    //Mark it as so
+                    this.messageArray[wordIndex].type = "username";
+                    //Store their color
+                    this.messageArray[wordIndex].color = color;
+                }
+            }
+        });
+    }
+
+    /**
+     * Injects invisible whitespace in long-ass words to prevent fucking up the chat buffer size
+     */
+    addWhitespace(){
+        //for each word object in the body
+        this.messageArray.forEach((wordObj, wordIndex) => {
+            //if the word object hasn't been pre-processed elsewhere
+            if(wordObj.type == "word"){
+                //Create an empty array to hold our word
+                var wordArray = [];
+                //For each character in the string of the current word object
+                this.messageArray[wordIndex].string.split("").forEach((char, charIndex) => {
+                    //push the current character to the wordArray
+                    wordArray.push(char);
+                    //After eight characters
+                    if(charIndex > 8){
+                        //Push an invisible line-break character between every character
+                        wordArray.push("ㅤ");
+                    }
+
+                });
+
+                //Join the wordArray into a single string, and use it to set the current wordObject's string
+                this.messageArray[wordIndex].string = wordArray.join("");
+            }
+        });
+    }
+
+    /**
+     * Searches for text in-between a specific delimiter and runs a given callback against it
+     * 
+     * Internal command used by several text filters to prevent code re-writes
+     * @param {String} delimiter - delimiter to search string by
+     * @param {Function} cb - Callback function to run against found strings
+     * @returns {Array} - list of found instances of filter
+     */
+    processFilter(delimiter, cb){
+        //Create empty array to hold spoilers (keep this seperate at first for internal function use)
+        const foundFilters = [];
+        //Spoiler detection stage
+        //For each word object in the message array
+        main: for(let wordIndex = 0; wordIndex < this.messageArray.length; wordIndex++){
+            //Get the current word object
+            const wordObj = this.messageArray[wordIndex];
+            
+            //If its a regular word and contains '##'
+            if(wordObj.type == 'word' && wordObj.string.match(utils.escapeRegex(delimiter))){
+
+                //Crawl through detected spoilers
+                for(let spoiler of foundFilters){
+                    //If the current word object is part of a detected spoiler
+                    if(wordIndex == spoiler[0] || wordIndex == spoiler[1]){
+                        //ignore it and continue on to the next word object
+                        continue main;
+                    }
+                }
+
+                //Crawl throw word objects after the current one
+                for(let endIndex = (wordIndex + 1); endIndex < this.messageArray.length; endIndex++){
+                    //Get the current end object
+                    const endObj = this.messageArray[endIndex];
+
+                    //If its a regular word and contains '##'
+                    if(endObj.type == 'word' && endObj.string.match(utils.escapeRegex(delimiter))){                   
+                        //Setup the found filter array
+                        const foundFilter = [wordIndex, endIndex];
+
+                        //Scrape out delimiters
+                        wordObj.string = wordObj.string.replaceAll(delimiter,'');
+                        endObj.string = endObj.string.replaceAll(delimiter,'');
+
+                        //Add it to the list of detected filters
+                        foundFilters.push(foundFilter);
+
+                        //Run the filter callback
+                        cb(foundFilter)
+
+                        //Break the nested end-detection loop
+                        break;
+                    }
+                }
+            }
+        }
+
+        return foundFilters;
+    }
+
+    /**
+     * Processes in-line spoilers
+     */
+    processSpoilers(){
+        //Process spoilers using '##' delimiter
+        this.processFilter('##', (foundSpoiler)=>{
+            //For each found spoiler add it to the list of found filter spans
+            this.filterSpans.push({class: "spoiler", index: [foundSpoiler[0] + 1, foundSpoiler[1] - 1], delimiters: [foundSpoiler[0], foundSpoiler[1]]});
+        });
+    }
+
+    /**
+     * Processes in-line Strike-through
+     */
+    processStrikethrough(){
+        //Process strikethrough's using '~~' delimiter
+        this.processFilter('~~', (foundStrikethrough)=>{
+            for(let wordIndex = foundStrikethrough[0]; wordIndex < foundStrikethrough[1]; wordIndex++){
+                this.messageArray[wordIndex].filterClasses.push("strikethrough");
+            }
+        })
+    }
+
+    /**
+     * Processes in-line Bold/Strong text
+     */
+    processBold(){
+        //Process strong text using '*' delimiter
+        this.processFilter('**', (foundStrikethrough)=>{
+            for(let wordIndex = foundStrikethrough[0]; wordIndex < foundStrikethrough[1]; wordIndex++){
+                this.messageArray[wordIndex].filterClasses.push("bold");
+            }
+        })
+    }
+
+    /**
+     * Processes in-line Italics
+     */
+    processItalics(){
+        //Process italics using '__' delimiter
+        this.processFilter('*', (foundStrikethrough)=>{
+            for(let wordIndex = foundStrikethrough[0]; wordIndex < foundStrikethrough[1]; wordIndex++){
+                this.messageArray[wordIndex].filterClasses.push("italics");
+            }
+        })
+    }
+
+    /**
+     * Processes clickable links and embedded media
+     */
+    processLinks(){
+        //If we don't have links
+        if(this.rawData.links == null){
+            //Don't bother
+            return;
+        }
+
+        //For every link received in this message
+        this.rawData.links.forEach((link, linkIndex) => {
+            //For every word obj in the message array
+            this.messageArray.forEach((wordObj, wordIndex) => {
+                //Check current wordobj for link (placeholder may contain whitespace with it)
+                if(wordObj.string.match(`␜${linkIndex}`)){
+                    //Set current word object in the body array to the new link object
+                    this.messageArray[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}`, '␜'),
+                        link: link.link,
+                        type: link.type,
+                        filterClasses: []
+                    }
+                }
+            })
+        });
+    }
+
+    /**
+     * Marks chat nodes in-case of non-standard chat types
+     */
+    handleChatType(){
+        if(this.rawData.type == "whisper"){
+            //add whisper class
+            this.chatBody.classList.add('whisper');
+        }else if(this.rawData.type == "announcement"){
+            //Squash the high-level
+            this.chatEntry.querySelector('.high-level').remove();
+
+            //Get the username and make it into an announcement title (little hacky but this *IS* postprocessing)
+            const userNode = this.chatEntry.querySelector('.chat-entry-username');
+            userNode.textContent = `${userNode.textContent.slice(0,-2)} Announcement`;
+
+            //Add/remove relevant classes
+            userNode.classList.remove('chat-entry-username');
+            userNode.classList.add('announcement-title');
+            this.chatBody.classList.add('announcement-body');
+            this.chatEntry.classList.add('announcement');
+        }else if(this.rawData.type == "toke"){
+            //Squash the high-level
+            this.chatEntry.querySelector('.high-level').remove();
+
+            //remove the username
+            this.chatEntry.querySelector('.chat-entry-username').remove();
+
+            //Add toke/tokewhisper class
+            this.chatBody.classList.add("toke");
+        }else if(this.rawData.type == "tokewhisper"){
+            //Squash the high-level
+            this.chatEntry.querySelector('.high-level').remove();
+
+            //remove the username
+            this.chatEntry.querySelector('.chat-entry-username').remove();
+
+            //Add toke/tokewhisper class
+            this.chatBody.classList.add("tokewhisper","serverwhisper");
+        }else if(this.rawData.type == "spoiler"){
+            //Set whole-body spoiler
+            this.chatBody.classList.add("spoiler");
+        }else if(this.rawData.type == "strikethrough"){
+            //Set whole-body spoiler
+            this.chatBody.classList.add("strikethrough");
+        }else if(this.rawData.type == "bold"){
+            //Set whole-body spoiler
+            this.chatBody.classList.add("bold");
+        }else if(this.rawData.type == "italics"){
+            //Set whole-body spoiler
+            this.chatBody.classList.add("italics");
+        }
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/commandPreprocessor.html b/www/doc/client/commandPreprocessor.html index 21290cc..99d9d3f 100644 --- a/www/doc/client/commandPreprocessor.html +++ b/www/doc/client/commandPreprocessor.html @@ -30,7 +30,7 @@

commandPreprocessor(client)

-
Class for object containing chat and command pre-processing logic
+
Class containing chat and command pre-processing logic
@@ -105,7 +105,7 @@ - Parent client mgmt object + Parent client Management Object @@ -1906,13 +1906,13 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandPreprocessor.js.html b/www/doc/client/commandPreprocessor.js.html index d2220c7..9e6a44c 100644 --- a/www/doc/client/commandPreprocessor.js.html +++ b/www/doc/client/commandPreprocessor.js.html @@ -43,12 +43,12 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.*/ /** - * Class for object containing chat and command pre-processing logic + * Class containing chat and command pre-processing logic */ class commandPreprocessor{ /** * Instantiates a new commandPreprocessor object - * @param {channel} client - Parent client mgmt object + * @param {channel} client - Parent client Management Object */ constructor(client){ /** @@ -328,7 +328,7 @@ class commandPreprocessor{ } /** - * Class for Object which contains logic for client-side commands + * Class which contains logic for client-side commands */ class commandProcessor{ /** @@ -365,13 +365,13 @@ class commandProcessor{
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandProcessor.html b/www/doc/client/commandProcessor.html index d4960e4..e8bf2a6 100644 --- a/www/doc/client/commandProcessor.html +++ b/www/doc/client/commandProcessor.html @@ -30,7 +30,7 @@

commandProcessor(client)

-
Class for Object which contains logic for client-side commands
+
Class which contains logic for client-side commands
@@ -415,13 +415,13 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/cpanel.js.html b/www/doc/client/cpanel.js.html new file mode 100644 index 0000000..8056e0a --- /dev/null +++ b/www/doc/client/cpanel.js.html @@ -0,0 +1,524 @@ + + + + + JSDoc: Source: cpanel.js + + + + + + + + + + +
+ +

Source: cpanel.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class containing code for managing the Canopy Panel UX
+ */
+class cPanel{
+    /**
+     * Instantiates a new Canopy Panel Management object
+     * @param {channel} client - Parent client Management Object
+     */
+    constructor(client){
+        /**
+         * Parent Client Management object
+         */
+        this.client = client;
+
+        /**
+         * Active Panel Object
+         */
+        this.activePanel = null;
+
+        /**
+         * Pinned Panel Object
+         */
+        this.pinnedPanel = null;
+
+        /**
+         * Popped Panel Objects
+         */
+        this.poppedPanels = [];
+
+        /**
+         * Click-Dragger object for re-sizable active panel
+         */
+        this.activePanelDragger = new canopyUXUtils.clickDragger("#cpanel-active-drag-handle", "#cpanel-active-div", false, null, false);
+
+        /**
+         * Click-Dragger object for re-sizable pinned panel
+         */
+        this.pinnedPanelDragger = new canopyUXUtils.clickDragger("#cpanel-pinned-drag-handle", "#cpanel-pinned-div", false, this.client.chatBox.clickDragger);
+
+        //Element Nodes
+        //Active Panel
+        /**
+         * Active Panel Container
+         */
+        this.activePanelDiv = document.querySelector("#cpanel-active-div");
+
+        /**
+         * Active Panel Title
+         */
+        this.activePanelTitle = document.querySelector("#cpanel-active-title");
+
+        /**
+         * Active Title Document Div
+         */
+        this.activePanelDoc = document.querySelector("#cpanel-active-doc");
+
+        /**
+         * Active Panel Pin Icon
+         */
+        this.activePanelPinIcon = document.querySelector("#cpanel-active-pin-icon");
+
+        /**
+         * Active Panel Pop-Out Icon
+         */
+        this.activePanelPopoutIcon = document.querySelector("#cpanel-active-popout-icon");
+
+        /**
+         * Active Panel Close Icon
+         */
+        this.activePanelCloseIcon = document.querySelector("#cpanel-active-close-icon");
+
+        //Pinned Panel
+        /**
+         * Pinned Panel Contianer
+         */
+        this.pinnedPanelDiv = document.querySelector("#cpanel-pinned-div");
+
+        /**
+         * Pinned Panel Title
+         */
+        this.pinnedPanelTitle = document.querySelector("#cpanel-pinned-title");
+
+        /**
+         * Pinned Panel Document Div
+         */
+        this.pinnedPanelDoc = document.querySelector("#cpanel-pinned-doc");
+
+        /**
+         * Pinned Panel Un-Pin Icon
+         */
+        this.pinnedPanelUnpinIcon = document.querySelector("#cpanel-pinned-unpin-icon");
+
+        /**
+         * Pinned Panel Pop-Out Icon
+         */
+        this.pinnedPanelPopoutIcon = document.querySelector("#cpanel-pinned-popout-icon");
+
+        /**
+         * Pinned Panel Close Icon
+         */
+        this.pinnedPanelCloseIcon = document.querySelector("#cpanel-pinned-close-icon");
+
+        this.setupInput();
+    }
+
+    /**
+     * Defines input-related event listeners
+     */
+    setupInput(){
+        this.activePanelCloseIcon.addEventListener("click", this.hideActivePanel.bind(this));
+        this.activePanelPinIcon.addEventListener("click", this.pinPanel.bind(this));
+        this.activePanelPopoutIcon.addEventListener("click", this.popActivePanel.bind(this));
+        this.pinnedPanelCloseIcon.addEventListener("click", this.hidePinnedPanel.bind(this));
+        this.pinnedPanelUnpinIcon.addEventListener("click", this.unpinPanel.bind(this));
+        this.pinnedPanelPopoutIcon.addEventListener("click", this.popPinnedPanel.bind(this));
+    }
+
+    /**
+     * Sets Active Panel
+     * @param {panelObj} panel - Panel Object to set as active
+     * @param {String} panelBody - innerHTML of Panel, pulls from panelObj.getPage() if empty
+     */
+    async setActivePanel(panel, panelBody){
+        //Set active panel
+        this.activePanel = panel;
+
+        //Grab panel hypertext content and load it into div
+        this.activePanelDoc.innerHTML = (panelBody == null || panelBody == "") ? await this.activePanel.getPage() : panelBody;
+          
+
+        //Display panel
+        this.activePanelDiv.style.display = "flex";
+        this.activePanelTitle.textContent = this.activePanel.name;
+
+        //Call panel initialization function
+        this.activePanel.panelDocument = this.activePanelDoc;
+        this.activePanel.docSwitch();
+    }
+
+    /**
+     * Hides active panel
+     * @param {Event} event - Event passed down from Input Handler
+     * @param {Boolean} keepAlive - Prevents closing panel if true
+     */
+    hideActivePanel(event, keepAlive = false){
+        if(!keepAlive){
+            this.activePanel.closer();
+        }
+
+        //Hide the panel
+        this.activePanelDiv.style.display = "none";
+        //Clear out the panel
+        this.activePanelDoc.innerHTML = '';
+        //Set active panel to null
+        this.activePanel = null;
+    }
+
+    /**
+     * Pins active panel
+     */
+    pinPanel(){
+        this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML);
+        this.hideActivePanel(null, true);
+    }
+
+    /**
+     * Pop's out active panel
+     */
+    popActivePanel(){
+        this.popPanel(this.activePanel, this.activePanelDoc.innerHTML);
+        this.hideActivePanel(null, true);
+    }
+
+    /**
+     * Sets pinned panel
+     * @param {panelObj} panel - Panel Object to apply to panel
+     * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null
+     */
+    async setPinnedPanel(panel, panelBody){
+        //Set pinned panel
+        this.pinnedPanel = panel;
+
+        //Set Title
+        this.pinnedPanelTitle.textContent = this.pinnedPanel.name;
+
+        //Grab panel hypertext content and load it into div
+        this.pinnedPanelDoc.innerHTML = (panelBody == null || panelBody == "") ? await this.pinnedPanel.getPage() : panelBody;
+
+        //Display panel
+        this.pinnedPanelDiv.style.display = "flex";
+
+        //Call panel initialization function
+        this.pinnedPanel.panelDocument = this.pinnedPanelDoc;
+        this.pinnedPanel.docSwitch();
+
+        //Resize to window/content
+        this.pinnedPanelDragger.fixCutoff();
+    }
+
+    /**
+     * Hides pinned panel
+     * @param {Event} event - Passed down input event
+     * @param {Boolean} keepAlive - Prevents panel.closer() from running if true
+     */
+    hidePinnedPanel(event, keepAlive = false){
+        this.pinnedPanelDiv.style.display = "none";
+
+        if(!keepAlive){
+            this.pinnedPanel.closer();
+        }
+
+        this.pinnedPanel = null;
+    }
+
+    /**
+     * Sets pinned panel to active
+     */
+    unpinPanel(){
+        this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML);
+        this.hidePinnedPanel(null, true);
+    }
+
+    /**
+     * Pops pinned panel
+     */
+    popPinnedPanel(){
+        this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML);
+        this.hidePinnedPanel(null, true);
+    }
+
+    /**
+     * Pops a new pop-out panel
+     * @param {panelObj} panel - panelObj to apply to the panel
+     * @param {String} panelBody - Raw HTML to inject into panel body, injects panel default if left to null
+     */
+    popPanel(panel, panelBody){
+        var newPanel = new poppedPanel(panel, panelBody, this)
+
+        this.poppedPanels.push(newPanel);
+    }
+
+}
+
+/**
+ * Template Class for other Classes for Objects which represent a single Canopy Panel
+ */
+class panelObj{
+    /**
+     * Instantiates a new Panel Object
+     * @param {channel} client - Parent client Management Object
+     * @param {String} name - Panel Name
+     * @param {String} pageURL - Panel Default Page URL
+     * @param {Document} panelDocument - Panel Document
+     */
+    constructor(client, name = "Placeholder Panel", pageURL = "/panel/placeholder", panelDocument = window.document){
+        /**
+         * Panel Name
+         */
+        this.name = name;
+
+        /**
+         * Panel Default Page URL
+         */
+        this.pageURL = pageURL;
+
+        /**
+         * Panel Document
+         */
+        this.panelDocument = panelDocument;
+
+        /**
+         * Current root document panel doc lives within
+         */
+        this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; 
+
+        /**
+         * Parent Client Management object
+         */
+        this.client = client;
+    }
+
+    /**
+     * Fetches panel page from the server
+     * @returns {String} Raw panel doc HTML
+     */
+    async getPage(){
+        var response = await fetch(this.pageURL,{
+            method: "GET",
+        });
+
+        return await response.text();
+    }
+
+    /**
+     * Handles Document/Panel Changes
+     */
+    docSwitch(){
+        //Set owner doc
+        this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; 
+    }
+
+    /**
+     * Called upon panel close/exit
+     */
+    closer(){
+    }
+}
+
+/**
+ * Class which represents a single instance of a popped-out panel
+ */
+class poppedPanel{
+    /**
+     * Instantiates a new Popped Panel Object
+     * @param {panelObj} panel - Panel Object to apply to Popped Panel
+     * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null
+     * @param {cPanel} cPanel - Parent Canopy Panel Management Object
+     */
+    constructor(panel, panelBody, cPanel){
+        /**
+         * Panel Object to apply to Popped Panel
+         */
+        this.panel = panel;
+
+        /**
+         * Raw HTML to inject into panel body, defaults to panel page if null
+         */
+        this.panelBody = panelBody;
+
+        /**
+         * Browser Window taken up by the Popped Panel
+         */
+        this.window = null;
+
+        /**
+         * Popped Panel Container Div
+         */
+        this.pinnedPanelDiv = null;
+
+        /**
+         * Popped Panel Title
+         */
+        this.pinnedPanelTitle = null;
+
+        /**
+         * Popped Panel Document Div
+         */
+        this.pinnedPanelDoc = null;
+
+        /**
+         * Popped Panel Close Icon
+         */
+        this.pinnedPanelCloseIcon = null;
+
+        /**
+         * Parent Canopy Panel Management Object
+         */
+        this.cPanel = cPanel;
+
+        /**
+         * Disables this.panel.closer() calls from this.closer()
+         */
+        this.keepAlive = false;
+
+        //Continue constructor asynchrnously
+        this.asyncConstructor();
+    }
+
+    /**
+     * Continuation of constructor method for asynchronous function calls
+     */
+    async asyncConstructor(){
+        //Set panel body properly
+        this.panelBody = (this.panelBody == null || this.panelBody == "") ? await this.panel.getPage() : this.panelBody;
+
+        //Pop the panel
+        this.popContainer();
+    }
+
+    /**
+     * Pops/Opens container window upon start
+     */
+    popContainer(){
+        //Set Window Object
+        this.window = window.open("/panel/popoutContainer","",`menubar=no,height=850,width=600`);
+        this.window.addEventListener("load", this.fillContainer.bind(this)); 
+    }
+
+    /**
+     * Fills container window with Popped Panel container elements
+     */
+    fillContainer(){
+        //Set Element Nodes
+        this.panelDiv = this.window.document.querySelector("#cpanel-div");
+        this.panelTitle = this.window.document.querySelector("#cpanel-title");
+        this.panelDoc = this.window.document.querySelector("#cpanel-doc");
+        this.panelPopinIcon = this.window.document.querySelector("#cpanel-popin-icon");
+        this.panelPinIcon = this.window.document.querySelector("#cpanel-pin-icon");
+
+        //Set Window Title
+        this.window.document.title = this.window.document.title.replace("NULL_POPOUT", `${this.panel.name} (${client.channelName})`);
+
+        //Set Panel Content
+        this.panelTitle.innerText = this.panel.name;
+        this.panelDoc.innerHTML = this.panelBody;
+
+        //Set panel object document and call the related function
+        this.panel.panelDocument = this.window.document;
+        this.panel.docSwitch();
+
+        this.setupInput();
+    }
+
+    /**
+     * Defines default input-related popped-panel Event Listeners
+     */
+    setupInput(){
+        this.panelPopinIcon.addEventListener("click", this.unpop.bind(this));
+        this.panelPinIcon.addEventListener("click", this.pin.bind(this));
+        this.window.addEventListener("unload", this.closer.bind(this));
+    }
+
+    /**
+     * Called upon close/exit of panel
+     */
+    closer(){
+        if(!this.keepAlive){
+            this.panel.closer();
+        }
+
+        this.cPanel.poppedPanels.splice(this.cPanel.poppedPanels.indexOf(this),1);
+    }
+
+    /**
+     * Un-pops panel into active-panel slot
+     */
+    unpop(){
+        //Set active panel
+        this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML);
+
+        this.keepAlive = true;
+
+        //Close the popped window
+        this.window.close();
+    }
+
+    /**
+     * Pins panel next to chat
+     */
+    pin(){
+        this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML);
+
+        this.keepAlive = true;
+
+        this.window.close();
+    }
+
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/global.html b/www/doc/client/global.html index d122a61..281baf5 100644 --- a/www/doc/client/global.html +++ b/www/doc/client/global.html @@ -202,13 +202,13 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/hlsBase.html b/www/doc/client/hlsBase.html new file mode 100644 index 0000000..570794e --- /dev/null +++ b/www/doc/client/hlsBase.html @@ -0,0 +1,2928 @@ + + + + + JSDoc: Class: hlsBase + + + + + + + + + + +
+ +

Class: hlsBase

+ + + + + + +
+ +
+ +

hlsBase(client, player, media, type)

+ +
Base HLS Media handler for handling all HLS related media
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new hlsBase(client, player, media, type)

+ + + + + + +
+ Instantiates a new HLS Base object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
media + + +Object + + + + De-hydrated media object from server
type + + +String + + + + Media Handler Source Type
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/hlsLiveStreamHandler.html b/www/doc/client/hlsLiveStreamHandler.html new file mode 100644 index 0000000..be4378b --- /dev/null +++ b/www/doc/client/hlsLiveStreamHandler.html @@ -0,0 +1,2905 @@ + + + + + JSDoc: Class: hlsLiveStreamHandler + + + + + + + + + + +
+ +

Class: hlsLiveStreamHandler

+ + + + + + +
+ +
+ +

hlsLiveStreamHandler(client, player, media)

+ +
HLS Livestream Handler
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new hlsLiveStreamHandler(client, player, media)

+ + + + + + +
+ Instantiates a new HLS Live Stream Handler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
media + + +Object + + + + De-hydrated media object from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/index.html b/www/doc/client/index.html index c5f5bb4..eefcca4 100644 --- a/www/doc/client/index.html +++ b/www/doc/client/index.html @@ -81,13 +81,13 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/mediaHandler.html b/www/doc/client/mediaHandler.html new file mode 100644 index 0000000..64a1729 --- /dev/null +++ b/www/doc/client/mediaHandler.html @@ -0,0 +1,2710 @@ + + + + + JSDoc: Class: mediaHandler + + + + + + + + + + +
+ +

Class: mediaHandler

+ + + + + + +
+ +
+ +

mediaHandler(client, player, media, type)

+ + +
+ +
+
+ + + + + + +

new mediaHandler(client, player, media, type)

+ + + + + + +
+ Instantiates a new Media Handler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
media + + +Object + + + + De-hydrated media object from server
type + + +String + + + + Media Handler Source Type
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/mediaHandler.js.html b/www/doc/client/mediaHandler.js.html new file mode 100644 index 0000000..d1d7982 --- /dev/null +++ b/www/doc/client/mediaHandler.js.html @@ -0,0 +1,844 @@ + + + + + JSDoc: Source: mediaHandler.js + + + + + + + + + + +
+ +

Source: mediaHandler.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/*
+ * Base class for all Canopy Media Handlers
+ *
+ * This is little more than a interface class
+ */
+class mediaHandler{ 
+    /**
+     * Instantiates a new Media Handler object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player  - Parent Canopy Player Object
+     * @param {Object} media - De-hydrated media object from server
+     * @param {String} type - Media Handler Source Type
+     */
+    constructor(client, player, media, type){
+        /**
+         * Parent Client Management Object
+         */
+        this.client = client;
+
+        /**
+         * Parent Canopy Player Object
+         */
+        this.player = player;
+
+        /**
+         * Media Handler Source Type
+         */
+        this.type = type
+
+        /*
+         * Denotes wether a seek call was made by the syncing function
+         */
+        this.selfAct = false;
+
+        /*
+         * Contains the last received time stamp
+         */
+        this.lastTimestamp = 0;
+
+        //Ingest media object from server
+        this.startMedia(media);
+    }
+
+    /**
+     * Ingests media nd starts playback
+     * @param {Object} media - Media object from server
+     */
+    startMedia(media){
+        //If we properly ingested the media
+        if(this.ingestMedia(media)){
+            //Build the video player
+            this.buildPlayer();
+
+            //Call the start function
+            this.start();
+        }
+    }
+
+    /**
+     * Builds video player element
+     */
+    buildPlayer(){
+        //Reset player lock
+        this.lock = false;
+    }
+
+    /**
+     * Destroys video player element
+     */
+    destroyPlayer(){
+        //Null out video property
+        this.video = null;
+    }
+
+    /**
+     * Ingests media object from server
+     * @param {Object} media - Media object from the server
+     * @returns {Boolean} True upon success
+     */
+    ingestMedia(media){
+        //Set now playing
+        this.nowPlaying = media;
+        
+        //return true to signify success
+        return true;
+    }
+
+    /**
+     * Starts video playback
+     */
+    start(){
+        this.setVideoTitle(this.nowPlaying.title);
+    }
+
+    /**
+     * Syncronizes timestamp based on timestamp received from server
+     * @param {Number} timestamp - Current video timestamp in seconds
+     */
+    sync(timestamp = this.lastTimestamp){
+        //Skip sync calls that won't seek so we don't pointlessly throw selfAct
+        if(timestamp != this.video.currentTime){
+            //Set self act flag
+            this.selfAct = true;
+        }
+    }
+
+    /**
+     * Reloads media player
+     */
+    reload(){
+        //Get current timestamp
+        const timestamp = this.video.currentTime;
+
+        //Throw self act flag to make sure we don't un-sync the player
+        this.selfAct = true;
+    }
+
+    /**
+     * Handles media end
+     */
+    end(){
+        //Null out current media
+        this.nowPlaying = null;
+
+        //Throw self act to prevent unlock on video end
+        this.selfAct = true;
+
+        //Destroy the player
+        this.destroyPlayer();
+    }
+
+    /**
+     * Plays video
+     */
+    play(){
+    }
+
+    /**
+     * Pauses video
+     */
+    pause(){
+    }
+
+    /**
+     * Toggles player control lockout
+     * @param {Boolean} lock - Whether or not to lock-out user control of video
+     */
+    setPlayerLock(lock){
+        //set lock property
+        this.lock = lock;
+    }
+
+    /**
+     * Calculates Aspect Ratio of media
+     * @returns {Number} Media Aspect Ratio as Floating Point number
+     */
+    getRatio(){
+        return 4/3;
+    } 
+
+    /**
+     * Gets current timestamp from video
+     * @returns {Number} Media Timestamp in seconds
+     */
+    getTimestamp(){
+        return 0;
+    }
+
+    /**
+     * Sets player title
+     * @param {String} title - Title to set
+     */
+    setVideoTitle(title){
+        this.player.title.textContent = `Currently Playing: ${title}`;
+    }
+
+    /**
+     * Called once all video metadata has properly been fetched
+     * @param {Event} event - Event passed down by event handler
+     */
+    onMetadataLoad(event){
+        //Resize aspect (if locked), since the video doesn't properly report it's resolution until it's been loaded
+        this.client.chatBox.resizeAspect();
+    }
+
+    /**
+     * Called on media pause
+     * @param {Event} event - Event passed down by event handler
+     */
+    onPause(event){
+        //If the video was paused out-side of code
+        if(!this.selfAct){
+            this.player.unlockSync();
+        }
+
+        this.selfAct = false;
+    }
+
+    /**
+     * Called on media volume change
+     * @param {Event} event - Event passed down by event handler
+     */
+    onVolumeChange(event){
+    }
+
+    /**
+     * Called on media seek
+     * @param {Event} event - Event passed down by event handler
+     */
+    onSeek(event){
+        //If the video was seeked out-side of code
+        if(!this.selfAct){
+            this.player.unlockSync();
+        }
+
+        //reset self act flag
+        this.selfAct = false;
+    }
+
+    /**
+     * Called on media buffer
+     * @param {Event} event - Event passed down by event handler
+     */
+    onBuffer(){
+        this.selfAct = true;
+    }
+}
+
+/**
+ * Class containing basic building blocks for anything that touches a <video> tag
+ * @extends mediaHandler
+ */
+class rawFileBase extends mediaHandler{
+    /**
+     * Instantiates a new rawFileBase object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player  - Parent Canopy Player Object
+     * @param {Object} media - De-hydrated media object from server
+     * @param {String} type - Media Handler Source Type
+     */
+    constructor(client, player, media, type){
+        super(client, player, media, type);
+    }
+
+    /**
+     * Defines input-related event listeners
+     */
+    defineListeners(){
+        //Resize to aspect on metadata load
+        this.video.addEventListener('loadedmetadata', this.onMetadataLoad.bind(this));
+        this.video.addEventListener('volumechange', this.onVolumeChange.bind(this));
+    }
+
+    buildPlayer(){
+        //Create player
+        this.video = document.createElement('video');
+
+        //Enable controls
+        this.video.controls = true;
+
+        //Append it to page
+        this.player.videoContainer.appendChild(this.video);
+
+        //Run derived method
+        super.buildPlayer();
+    }
+
+    destroyPlayer(){
+        //Stops playback
+        this.video.pause();
+        //Remove player from page
+        this.video.remove();
+        //Run derived method
+        super.destroyPlayer();
+    }
+
+    reload(){
+        //Call derived method
+        super.reload();
+
+        //Load video from source
+        this.video.load();
+
+        //Set it back to the proper time
+        this.video.currentTime = this.lastTimestamp;
+
+        //Play the video
+        this.video.play();
+    }
+
+    setPlayerLock(lock){
+            //toggle controls
+            this.video.controls = !lock;
+            //Only toggle mute if we're locking, or if we're unlocking after being locked
+            //If this is ran twice without locking we don't want to surprise unmute on the user
+            if(lock || this.lock){
+                //toggle mute
+                this.video.muted = lock;
+            }
+            //toggle looping
+            this.video.loop = lock;
+
+            //Run derived method
+            super.setPlayerLock(lock);
+    }
+
+    getRatio(){
+        return this.video.videoWidth / this.video.videoHeight;
+    } 
+
+    onVolumeChange(event){
+        //Pull volume from video
+        this.player.volume = this.video.volume;
+    }
+}
+
+/** 
+ * Off air static 'player'
+ * @extends rawFileBase
+ */
+class nullHandler extends rawFileBase{
+    /**
+     * Instantiates a new Null Handler object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player  - Parent Canopy Player Object
+     */
+    constructor(client, player){
+        //Call derived constructor
+        super(client, player, {}, null);
+
+        this.defineListeners();
+    }
+
+    defineListeners(){
+        //Run derived method
+        super.defineListeners();
+
+        //Disable right clicking
+        this.video.addEventListener('contextmenu', (e)=>{e.preventDefault()});
+    }
+
+    start(){
+        //call derived start function
+        super.start();        
+
+        //Lock the player
+        this.setPlayerLock(true);
+
+        //Set the static placeholder
+        this.video.src = '/video/static.webm';
+
+        //play the placeholder video
+        this.video.play();
+    }
+
+    setVideoTitle(title){
+        this.player.title.textContent = `Channel Off Air`;
+    }
+}
+
+/**
+ * Basic building blocks needed for proper time-synchronized raw-file playback
+ * @extends rawFileBase
+ */
+class rawFileHandler extends rawFileBase{
+    /**
+     * Instantiates a new Null Handler object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player  - Parent Canopy Player Object
+     * @param {Object} media - De-hydrated media object from server
+     */
+    constructor(client, player, media){
+        //Call derived constructor
+        super(client, player, media, 'raw');
+
+        //Define listeners
+        this.defineListeners();
+    }
+
+    defineListeners(){
+        //Run derived method
+        super.defineListeners();
+
+        this.video.addEventListener('pause', this.onPause.bind(this));
+        this.video.addEventListener('seeked', this.onSeek.bind(this));
+        this.video.addEventListener('waiting', this.onBuffer.bind(this));
+    }
+
+    start(){
+        //Call derived start
+        super.start();
+
+        //Set video
+        this.video.src = this.nowPlaying.rawLink;
+
+        //Set video volume
+        this.video.volume = this.player.volume;
+
+        //Unlock player
+        this.setPlayerLock(false);
+
+        //play video
+        this.video.play();
+    }
+
+    play(){
+        this.video.play();
+    }
+
+    pause(){
+        this.video.pause();
+    }
+
+    sync(timestamp = this.lastTimestamp){
+        //Call derived sync
+        super.sync(timestamp);
+
+        //Skip sync calls that won't seek so we don't pointlessly throw selfAct
+        if(timestamp != this.video.currentTime){
+            //Set current video time based on timestamp received from server
+            this.video.currentTime = timestamp;
+        }
+    }
+
+    getTimestamp(){
+        //Return current timestamp
+        return this.video.currentTime;
+    }
+}
+
+/**
+ * Handles Youtube playback via the official YT embed (gross)
+ * @extends mediaHandler
+ */
+class youtubeEmbedHandler extends mediaHandler{
+    /**
+     * Instantiates a new  Youtube Embed Handler object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player  - Parent Canopy Player Object
+     * @param {Object} media - De-hydrated media object from server
+     */
+    constructor(client, player, media){
+        //Call derived constructor
+        super(client, player, media, 'ytEmbed');
+
+        //Set flag to notify functions when the player is actually ready
+        this.ready = false;
+
+        //Create property to hold video iframe for easy access
+        this.iframe = null;
+    }
+
+    //custom start media function since we want the youtube player to call the start function once it's ready
+    startMedia(media){
+        //If we properly ingested the media
+        if(this.ingestMedia(media)){
+            //Build the video player
+            this.buildPlayer();
+        }
+    }
+
+    buildPlayer(){
+        //If the embed API hasn't loaded
+        if(!this.client.ytEmbedAPILoaded){
+            //Complain and stop
+            return console.warn("youtubeEmbedHandler.buildPlayer() Called before YT Iframe API Loaded, waiting on refresh to rebuild...");
+        }
+
+        //Create temp div for yt api to replace
+        const tempDiv = document.createElement('div');
+        //Name the div
+        tempDiv.id = "youtube-embed-player"
+        //Append it to the video container
+        this.player.videoContainer.appendChild(tempDiv);
+
+        //Create a new youtube player using the official YT iframe-embed api
+        this.video = new YT.Player('youtube-embed-player', {
+            //Inject video id
+            videoId: this.nowPlaying.id,
+            events: {
+                'onReady': this.start.bind(this),
+                'onStateChange': this.onStateChange.bind(this)
+            }
+        }); 
+
+        //Call derived function
+        super.buildPlayer();
+    }
+
+    start(){
+        //Call derived start function
+        super.start();
+
+        //Set volume based on player volume
+        this.video.setVolume(this.player.volume * 100);
+
+        //Kick the video off
+        this.video.playVideo();
+
+        //Pull iframe
+        this.iframe = this.video.getIframe()
+
+        //Throw the ready flag
+        this.ready = true;
+    }
+
+    destroyPlayer(){
+        //If we've had enough time to create a player frame
+        if(this.ready){
+            //Pull volume from player before destroying since google didn't give us a volume change event like a bunch of dicks
+            this.player.volume = (this.video.getVolume() / 100);
+
+            //Use the embed api's built in destroy function
+            this.video.destroy();
+        }
+
+        //Check the f̶r̶i̶d̶g̶e video container for leftovers
+        const leftovers = this.player.videoContainer.querySelector("#youtube-embed-player");
+
+        //If we have any leftovers
+        if(leftovers != null){
+            //Nukem like last nights chicken
+            leftovers.remove();
+        }
+
+        //Call derived destroy function
+        super.destroyPlayer();
+    }
+
+    sync(timestamp = this.lastTimestamp){
+        //If we're not ready
+        if(!this.ready){
+            //Kick off a timer to wait it out and try again l8r
+            setTimeout(this.sync.bind(this), 100);
+
+            //If it failed, tell randy to fuck off
+            return;
+        }
+
+        //Seek to timestamp, allow buffering
+        this.video.seekTo(timestamp, true);
+    }
+
+    reload(){
+        //if we're ready
+        if(this.ready){
+            //re-load the video by id
+            this.video.loadVideoById(this.nowPlaying.id);
+        }
+    }
+
+    play(){
+        //If we're ready
+        if(this.ready){
+            //play the video
+            this.video.playVideo();
+        }
+    }
+
+    pause(){
+        //If we're ready
+        if(this.ready){
+            //pause the video
+            this.video.pauseVideo();
+        }
+    }
+
+    getRatio(){
+        //TODO: Implement a type-specific metadata property object in the media class to hold type-sepecifc meta-data
+        //Alternatively we could fill in resolution information from the raw link 
+        //However keeping embedded functionality dependant on raw-links seems like bad practice
+    }
+
+    getTimestamp(){
+        //If we're ready
+        if(this.ready){
+            //Return the timestamp
+            return this.video.getCurrentTime();
+        }
+        
+        //If we fall through, simply report that the video hasn't gone anywhere yet
+        return 0;
+    }
+
+    setVideoTitle(){
+        //Clear out the player title so that youtube's baked in title can do it's thing.
+        //This will be replaced once we complete the full player control and remove the defualt youtube UI
+        this.player.title.textContent = "";
+    }
+
+    /**
+     * Generic handler for state changes since google is a dick
+     */
+    onStateChange(event){
+        switch(event.data){
+            //video unstarted
+            case -1:
+                return;
+            //video ended
+            case 0:
+                return;
+            //video playing
+            case 1:
+                return;
+            //video paused
+            case 2:
+                super.onPause(event);
+                return;
+            //video buffering
+            case 3:
+                //There is no good way to tell slow connections apart from user seeking
+                //This will be easier to implement once we get custom player controls up
+                //super.onSeek(event);
+                return;
+            //video queued
+            case 5:
+                return;
+            //bad status code
+            default:
+                return;
+        }
+    }
+
+    setPlayerLock(lock){
+        super.setPlayerLock(lock);
+
+        if(this.ready){
+            this.iframe.style.pointerEvents = (lock ? "none" : "");
+        }
+    }
+}
+
+/**
+ * Base HLS Media handler for handling all HLS related media
+ * @extends rawFileBase
+ */
+class hlsBase extends rawFileBase{
+    /**
+     * Instantiates a new HLS Base object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player - Parent Canopy Player Object
+     * @param {Object} media - De-hydrated media object from server
+     * @param {String} type - Media Handler Source Type
+     */
+    constructor(client, player, media, type){
+        //Call derived constructor
+        super(client, player, media, type);
+    }
+
+    buildPlayer(){
+        //Call derived buildPlayer function
+        super.buildPlayer();
+
+        //Instantiate HLS object
+        this.hls = new Hls();
+
+        //Load HLS Stream
+        this.hls.loadSource(this.nowPlaying.url);
+
+        //Attatch hls object to video element
+        this.hls.attachMedia(this.video);
+
+        //Bind onMetadataLoad to MANIFEST_PARSED
+        this.hls.on(Hls.Events.MANIFEST_PARSED, this.onMetadataLoad.bind(this));
+    }
+
+    end(){
+        //Stop hls.js from loading any more of the stream
+        this.hls.stopLoad();
+
+        //Call derived method
+        super.end();
+    }
+
+    onMetadataLoad(){
+        //Call derived method
+        super.onMetadataLoad();
+    }
+
+    start(){
+        //Call derived method
+        super.start();
+
+        //Start the video
+        this.video.play();
+    }
+}
+
+/**
+ * HLS Livestream Handler
+ * @extends hlsBase
+ */
+class hlsLiveStreamHandler extends hlsBase{
+    /**
+     * Instantiates a new HLS Live Stream Handler object
+     * @param {channel} client - Parent Client Management Object
+     * @param {player} player - Parent Canopy Player Object
+     * @param {Object} media - De-hydrated media object from server
+     */
+    constructor(client, player, media){
+        //Call derived constructor
+        super(client, player, media, "livehls");
+
+        //Create variable to determine if we need to resync after next seek
+        this.reSync = false;
+
+        this.video.addEventListener('pause', this.onPause.bind(this));
+        this.video.addEventListener('seeked', this.onSeek.bind(this));
+        this.video.addEventListener('waiting', this.onBuffer.bind(this));
+    }
+
+    sync(){
+        //Kick the video back on if it was paused
+        this.video.play();
+
+        //Pull video duration
+        const duration = this.video.duration;
+
+        //Ignore bad timestamps
+        if(duration > 0){
+            //Seek to the end to sync up w/ the livestream
+            this.video.currentTime = duration;
+        }
+    }
+
+    setVideoTitle(title){
+        //Add title as text content for security :P
+        this.player.title.textContent = `: ${title}`;
+
+        //Create glow span
+        const glowSpan = document.createElement('span');
+        //Fill glow span content
+        glowSpan.textContent = "🔴LIVE";
+        //Set glowspan class
+        glowSpan.classList.add('critical-danger-text');
+
+        //Inject glowspan into title in a way that allows it to be easily replaced
+        this.player.title.prepend(glowSpan);
+    }
+
+    onBuffer(event){
+        //Call derived function
+        super.onBuffer(event);
+
+
+        //If we're synced by the end of buffering
+        if(this.player.syncLock){
+            //Throw flag to manually sync since this works entirely differently from literally every other fucking media source
+            this.reSync = true;
+        }
+    }
+
+    onSeek(event){
+        //Call derived method
+        super.onSeek(event);
+
+        //If we stopped playing the video
+        if(this.video == null){
+            //Don't worry about it
+            return;
+        }
+
+        //Calculate distance to end of stream
+        const difference = this.video.duration - this.video.currentTime;
+
+        //If we where buffering under sync lock
+        if(this.reSync){
+            //Set reSync to false
+            this.reSync = false;
+
+            //If the difference is bigger than streamSyncTolerance
+            if(difference > this.player.streamSyncTolerance){
+                //Sync manually since we have no timestamp, and therefore the player won't do it for us
+                this.sync();
+            }
+        }
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/nullHandler.html b/www/doc/client/nullHandler.html new file mode 100644 index 0000000..6f54084 --- /dev/null +++ b/www/doc/client/nullHandler.html @@ -0,0 +1,2882 @@ + + + + + JSDoc: Class: nullHandler + + + + + + + + + + +
+ +

Class: nullHandler

+ + + + + + +
+ +
+ +

nullHandler(client, player)

+ +
Off air static 'player'
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new nullHandler(client, player)

+ + + + + + +
+ Instantiates a new Null Handler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/panelObj.html b/www/doc/client/panelObj.html new file mode 100644 index 0000000..114225a --- /dev/null +++ b/www/doc/client/panelObj.html @@ -0,0 +1,918 @@ + + + + + JSDoc: Class: panelObj + + + + + + + + + + +
+ +

Class: panelObj

+ + + + + + +
+ +
+ +

panelObj(client, name, pageURL, panelDocument)

+ +
Template Class for other Classes for Objects which represent a single Canopy Panel
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new panelObj(client, name, pageURL, panelDocument)

+ + + + + + +
+ Instantiates a new Panel Object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
client + + +channel + + + + + + Parent client Management Object
name + + +String + + + + + + Placeholder Panel + + Panel Name
pageURL + + +String + + + + + + /panel/placeholder + + Panel Default Page URL
panelDocument + + +Document + + + + + + Panel Document
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

name

+ + + + +
+ Panel Name +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

ownerDoc

+ + + + +
+ Current root document panel doc lives within +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pageURL

+ + + + +
+ Panel Default Page URL +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

panelDocument

+ + + + +
+ Panel Document +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

closer()

+ + + + + + +
+ Called upon panel close/exit +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

docSwitch()

+ + + + + + +
+ Handles Document/Panel Changes +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) getPage() → {String}

+ + + + + + +
+ Fetches panel page from the server +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Raw panel doc HTML +
+ + + +
+
+ Type +
+
+ +String + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/player.html b/www/doc/client/player.html new file mode 100644 index 0000000..e96c2d9 --- /dev/null +++ b/www/doc/client/player.html @@ -0,0 +1,3383 @@ + + + + + JSDoc: Class: player + + + + + + + + + + +
+ +

Class: player

+ + + + + + +
+ +
+ +

player(client)

+ +
Class which represents Canopy Player UX
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new player(client)

+ + + + + + +
+ Instantiates a new Canopy Player object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent client Management Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

cinemaModeIcon

+ + + + +
+ Player Cinema-Mode Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

client

+ + + + +
+ Parent CLient Management Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

flipXIcon

+ + + + +
+ Player Flip Video X Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

flipYIcon

+ + + + +
+ Player Filp Video Y Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

hideVideoIcon

+ + + + +
+ Player Hide Video Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
+ Page Nav-Par +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

onUI

+ + + + +
+ Whether or not the mouse cursor is floating over player UX +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

playerDiv

+ + + + +
+ Top-Level Player Container Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

reloadIcon

+ + + + +
+ Player Media Reload Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

showVideoIcon

+ + + + +
+ Player Show Video Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

streamSyncTolerance

+ + + + +
+ Tolerance in livestream delay before corrective seek to live. + +Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future... +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

syncDelta

+ + + + +
+ Forced time to wait between sync checks, heavily decreases chance of seek-banging without reducing syncornization accuracy +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

syncIcon

+ + + + +
+ Player Syncronization Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

syncLock

+ + + + +
+ Whether or not player scrub is locked to sync signal from the server +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

syncTolerance

+ + + + +
+ Tolerance between timestamp from server and actual media before corrective seek for pre-recorded media +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

title

+ + + + +
+ Player Title Label +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

uiBar

+ + + + +
+ Auto-Hiding Player UI +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

uiTimer

+ + + + +
+ Player UX Stow-Away timer +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

videoContainer

+ + + + +
+ Player Element Container Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

volume

+ + + + +
+ Current Player Volume +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

defineListeners()

+ + + + + + +
+ Define Network-Related Event Listeners for the player +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles End-Media Commands from the Server +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

flipX()

+ + + + + + +
+ Flips the video horizontally +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

flipY()

+ + + + + + +
+ Flips the video vertically +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates ratio of current media object +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Current media aspect ratio as a single floating point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

lockSync()

+ + + + + + +
+ Locks player seek to synced timestamp from the server +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

popUI(event)

+ + + + + + +
+ Displays UI after player-related input +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed through by event handler
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads the media player +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setOnUI(onUI)

+ + + + + + +
+ Informs the class when the user's mouse curosr enters and leaves the UI area +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
onUI + + +Boolean + + + + Whether or not onUI should be toggled true
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setupInput()

+ + + + + + +
+ Defines Input-Related Event Listeners for the player +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start(data)

+ + + + + + +
+ Handles command from server to start media +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Media Metadata from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(data)

+ + + + + + +
+ Handles synchronization command from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Syncrhonization Data from Server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toggleCinemaMode(cinema)

+ + + + + + +
+ Toggles Cinema Mode on or off +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cinema + + +Boolean + + + + Whether or not to enter Cinema Mode. Defaults to toggle if left unspecified
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toggleUI(show)

+ + + + + + +
+ Toggles UI-Bar on or off +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
show + + +Boolean + + + + Whether or not to show the UI-Bar. Defaults to toggle if left unspecified.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toggleVideo(show)

+ + + + + + +
+ Toggles video on or off +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
show + + +Boolean + + + + Whether or not to show the video player. Defaults to toggle if left unspecified
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

unlockSync()

+ + + + + + +
+ Un-locks player seek to synced timestamp from the server +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateCurrentRawFile(data)

+ + + + + + +
+ Handles Raw-File Metadata Updates from the Server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + + Updadated Raw-File link from Server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/player.js.html b/www/doc/client/player.js.html new file mode 100644 index 0000000..25a1ace --- /dev/null +++ b/www/doc/client/player.js.html @@ -0,0 +1,483 @@ + + + + + JSDoc: Source: player.js + + + + + + + + + + +
+ +

Source: player.js

+ + + + + + +
+
+
/*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 <https://www.gnu.org/licenses/>.*/
+
+/**
+ * Class which represents Canopy Player UX
+ */
+class player{
+    /**
+     * Instantiates a new Canopy Player object
+     * @param {channel} client - Parent client Management Object
+     */
+    constructor (client){
+        /**
+         * Parent CLient Management Object
+         */
+        this.client = client;
+
+        /**
+         * Whether or not the mouse cursor is floating over player UX
+         */
+        this.onUI = false;
+
+        /**
+         * Whether or not player scrub is locked to sync signal from the server
+         */
+        this.syncLock = true;
+        
+        /**
+         * Player UX Stow-Away timer
+         */
+        this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false);
+
+        //elements
+        /**
+         * Top-Level Player Container Div
+         */
+        this.playerDiv = document.querySelector("#media-panel-div");
+
+        /**
+         * Player Element Container Div
+         */
+        this.videoContainer = document.querySelector("#media-panel-video-container")
+
+        /**
+         * Page Nav-Par
+         */
+        this.navBar = document.querySelector("#navbar");
+
+        /**
+         * Auto-Hiding Player UI
+         */
+        this.uiBar = document.querySelector("#media-panel-head-div");
+
+        /**
+         * Player Title Label
+         */
+        this.title = document.querySelector("#media-panel-title-paragraph");
+
+        /**
+         * Player Show Video Icon
+         */
+        this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon");
+
+        /**
+         * Player Hide Video Icon
+         */
+        this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon");
+
+        /**
+         * Player Syncronization Icon
+         */
+        this.syncIcon = document.querySelector("#media-panel-sync-icon");
+
+        /**
+         * Player Cinema-Mode Icon
+         */
+        this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon");
+
+        /**
+         * Player Filp Video Y Icon
+         */
+        this.flipYIcon = document.querySelector("#media-panel-flip-vertical-icon")
+
+        /**
+         * Player Flip Video X Icon
+         */
+        this.flipXIcon = document.querySelector("#media-panel-flip-horizontal-icon")
+
+        /**
+         * Player Media Reload Icon
+         */
+        this.reloadIcon = document.querySelector("#media-panel-reload-icon");
+
+        /**
+         * Tolerance between timestamp from server and actual media before corrective seek for pre-recorded media
+         */
+        this.syncTolerance = 0.4;
+
+        /**
+         * Tolerance in livestream delay before corrective seek to live.
+         * 
+         * Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future...
+         */
+        this.streamSyncTolerance = 2;
+
+        /**
+         * Forced time to wait between sync checks, heavily decreases chance of seek-banging without reducing syncornization accuracy
+         */
+        this.syncDelta = 6;
+
+        /**
+         * Current Player Volume
+         */
+        this.volume = 1;
+
+        //run setup functions
+        this.setupInput();
+        this.defineListeners();
+    }
+
+    /**
+     * Defines Input-Related Event Listeners for the player
+     */
+    setupInput(){
+        //UIBar Movement Detection
+        this.playerDiv.addEventListener("mousemove", this.popUI.bind(this));
+        this.uiBar.addEventListener("mouseenter", ()=>{this.setOnUI(true)});
+        this.uiBar.addEventListener("mouseleave", ()=>{this.setOnUI(false)});
+
+        //UIBar/header icons
+        //Don't bind these, they want an argument that isn't an event :P
+        this.showVideoIcon.addEventListener("click", ()=>{this.toggleVideo()});
+        this.hideVideoIcon.addEventListener("click", ()=>{this.toggleVideo()});
+        this.syncIcon.addEventListener("click", this.lockSync.bind(this));
+        this.cinemaModeIcon.addEventListener("click", ()=>{this.toggleCinemaMode()});
+        this.flipYIcon.addEventListener('click', this.flipY.bind(this));
+        this.flipXIcon.addEventListener('click', this.flipX.bind(this));
+        this.reloadIcon.addEventListener("click", this.reload.bind(this));
+    }
+
+    /**
+     * Define Network-Related Event Listeners for the player
+     */
+    defineListeners(){
+        this.client.socket.on("start", this.start.bind(this));
+        this.client.socket.on("sync", this.sync.bind(this));
+        this.client.socket.on("end", this.end.bind(this));
+        this.client.socket.on("updateCurrentRawFile", this.updateCurrentRawFile.bind(this));
+    }
+
+    /**
+     * Handles command from server to start media
+     * @param {Object} data - Media Metadata from server
+     */
+    start(data){
+        //If we have an active media handler
+        if(this.mediaHandler != null){
+            //End the media handler
+            this.mediaHandler.end();
+        }
+
+        //Ignore null media
+        if(data.media == null){
+            //Set null handler
+            this.mediaHandler = new nullHandler(client, this);
+        //Otherwise
+        }else{
+            //If we have a youtube video and the official embedded iframe player is selected
+            if(data.media.type == 'yt' && localStorage.getItem("ytPlayerType") == 'embed'){
+                //Create a new yt handler for it
+                this.mediaHandler = new youtubeEmbedHandler(this.client, this, data.media);
+                //Sync to time stamp
+                this.mediaHandler.sync(data.timestamp);
+            //If we have an HLS Livestream
+            }else if(data.media.type == "livehls"){
+                //Create a new HLS Livestream Handler for it
+                this.mediaHandler = new hlsLiveStreamHandler(this.client, this, data.media);
+            }else if(data.media.type == 'dm'){
+                this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media);
+            //Otherwise, if we have a raw-file compatible source
+            }else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){
+                //Create a new raw file handler for it
+                this.mediaHandler = new rawFileHandler(client, this, data.media);
+                //Sync to time stamp
+                this.mediaHandler.sync(data.timestamp);   
+            }else{
+                this.mediaHandler = new nullHandler(client, this);
+            }
+        }
+
+        //Lock synchronization since everyone starts at 0, and update the UI
+        this.lockSync();
+
+        //Re-size to aspect since video may now be a different size
+        this.client.chatBox.resizeAspect();
+
+        //Sync off of starter time stamp
+        this.mediaHandler.sync(data.timestamp);
+    }
+
+    /**
+     * Handles synchronization command from server
+     * @param {Object} data - Syncrhonization Data from Server
+     */
+    sync(data){
+        if(this.mediaHandler != null){
+            //Get timestamp
+            const timestamp = data.timestamp;
+            //Get difference between server and local timestamp
+            const difference = Math.abs(timestamp - this.mediaHandler.getTimestamp());
+
+            //Check if timestamp evenly devides into sync delta, effectively only checking for sync every X seconds
+            //Check if the difference between timestamps is larger than the sync tolerance
+            //Lastly, check to make sure we have sync lock
+            if(timestamp % this.syncDelta == 0 && difference > this.syncTolerance && this.syncLock){
+                //If we need to sync, then sync the video!
+                this.mediaHandler.sync(timestamp);
+            }
+
+            //Collect last timestamp
+            this.mediaHandler.lastTimestamp = timestamp;
+        }
+    }
+
+    /**
+     * Reloads the media player
+     */
+    reload(){
+        if(this.mediaHandler != null){
+            this.mediaHandler.reload();
+        }
+    }
+
+    /**
+     * Handles End-Media Commands from the Server
+     */
+    end(){
+        //Call the media handler finisher
+        this.mediaHandler.end();
+
+        //Replace it with a null handler
+        this.mediaHandler = new nullHandler(client, this);
+
+        //Re-lock sync since we're probably gonna start new media soon anywho, and we need to update the UI anywho
+        this.lockSync();
+    }
+
+    /**
+     * Handles Raw-File Metadata Updates from the Server
+     * @param {Object} data - Updadated Raw-File link from Server
+     */
+    updateCurrentRawFile(data){
+        //typecheck the media handler to see if we really need to do any of this shit, if not...
+        if(this.mediaHandler.type == 'ytEmbed'){
+            //Ignore it
+            return;
+        }
+
+        //Grab current item from media handler
+        const currentItem = this.mediaHandler.nowPlaying;
+
+        //Update raw link
+        currentItem.rawLink = data.file;
+
+        //Re-start the item
+        this.start({media: currentItem});
+    }
+
+    /**
+     * Locks player seek to synced timestamp from the server
+     */
+    lockSync(){
+        //Enable syncing
+        this.syncLock = true;
+
+        if(this.mediaHandler != null && this.mediaHandler.type != null){
+            //Light up the sync icon to show that we're actively synchronized
+            this.syncIcon.classList.add('positive');
+
+            //Sync to last timestamp
+            this.mediaHandler.sync();
+
+            //Play
+            this.mediaHandler.play();
+        }else{
+            //Unlight the sync icon since there is nothing to sync
+            this.syncIcon.classList.remove('positive');
+        }
+    }
+
+    /**
+     * Un-locks player seek to synced timestamp from the server
+     */
+    unlockSync(){
+        //Unlight the sync icon since we're no longer actively synced
+        this.syncIcon.classList.remove('positive');
+
+        //Disable syncing
+        this.syncLock = false;
+    }
+
+    /**
+     * Flips the video horizontally
+     */
+    flipX(){
+        //I'm lazy
+        const transform = this.videoContainer.style.transform;
+
+        //If we we're specifically set to un-mirrored
+        if(transform.match("scaleX(1)")){
+            //mirror it
+            this.videoContainer.style.transfrom = transform.replace('scaleX(1)', 'scaleX(-1)');
+        //If we're currently mirrored
+        }else if(transform.match("scaleX(-1)")){
+            //Un-mirror
+            this.videoContainer.style.transfrom = transform.replace('scaleX(-1)', 'scaleX(1)');
+        //Otherwise, if it's untouched
+        }else{
+            //Mirror it
+            this.videoContainer.style.transform += 'scaleX(-1)';
+        }
+    }
+
+    /**
+     * Flips the video vertically
+     */
+    flipY(){
+        //I'm lazy
+        const transform = this.videoContainer.style.transform;
+
+        //If we we're specifically set to un-mirrored
+        if(transform.match("scaleY(1)")){
+            //mirror it
+            this.videoContainer.style.transfrom = transform.replace('scaleY(1)', 'scaleY(-1)');
+        //If we're currently mirrored
+        }else if(transform.match("scaleY(-1)")){
+            //Un-mirror
+            this.videoContainer.style.transfrom = transform.replace('scaleY(-1)', 'scaleY(1)');
+        //Otherwise, if it's untouched
+        }else{
+            //Mirror it
+            this.videoContainer.style.transform += 'scaleY(-1)';
+        }
+    }
+
+    /**
+     * Displays UI after player-related input
+     * @param {Event} event - Event passed through by event handler
+     */
+    popUI(event){
+        this.toggleUI(true);
+        clearTimeout(this.uiTimer);
+        if(!this.onUI){
+            this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false);
+        }
+    }
+
+    /**
+     * Toggles UI-Bar on or off
+     * @param {Boolean} show - Whether or not to show the UI-Bar. Defaults to toggle if left unspecified.
+     */
+    toggleUI(show = this.uiBar.style.display == "none"){
+        this.uiBar.style.display = show ? "flex" : "none";
+    }
+
+    /**
+     * Toggles video on or off
+     * @param {Boolean} show - Whether or not to show the video player. Defaults to toggle if left unspecified
+     */
+    toggleVideo(show = !this.playerDiv.checkVisibility()){
+        if(show){
+            this.playerDiv.style.display = "flex";
+            this.showVideoIcon.style.display = "none";
+        }else{
+            this.playerDiv.style.display = "none";
+            this.showVideoIcon.style.display = "flex";
+        }
+
+        //Tell chatbox to handle this shit
+        this.client.chatBox.handleVideoToggle(show);
+    }
+
+    /**
+     * Toggles Cinema Mode on or off
+     * @param {Boolean} cinema - Whether or not to enter Cinema Mode. Defaults to toggle if left unspecified
+     */
+    toggleCinemaMode(cinema = !this.navBar.checkVisibility()){
+        if(cinema){
+            this.navBar.style.display = "flex";
+        }else{
+            this.navBar.style.display = "none";
+        }
+
+        //Resize the video if we're aspect locked
+        this.client.chatBox.resizeAspect();
+    }
+
+    /**
+     * Informs the class when the user's mouse curosr enters and leaves the UI area
+     * @param {Boolean} onUI - Whether or not onUI should be toggled true
+     */
+    setOnUI(onUI){
+        this.onUI = onUI;
+        this.popUI();
+    }
+
+    /**
+     * Calculates ratio of current media object
+     * @returns {Number} Current media aspect ratio as a single floating point number
+     */
+    getRatio(){
+        //If we have no media handler
+        if(this.mediaHandler == null){
+            //Return a 4/3 aspect to get a decent chat size
+            return 4/3;
+        }else{
+            return this.mediaHandler.getRatio();
+        }
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/www/doc/client/poppedPanel.html b/www/doc/client/poppedPanel.html new file mode 100644 index 0000000..b9f77ad --- /dev/null +++ b/www/doc/client/poppedPanel.html @@ -0,0 +1,1451 @@ + + + + + JSDoc: Class: poppedPanel + + + + + + + + + + +
+ +

Class: poppedPanel

+ + + + + + +
+ +
+ +

poppedPanel(panel, panelBody, cPanel)

+ +
Class which represents a single instance of a popped-out panel
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new poppedPanel(panel, panelBody, cPanel)

+ + + + + + +
+ Instantiates a new Popped Panel Object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
panel + + +panelObj + + + + Panel Object to apply to Popped Panel
panelBody + + +String + + + + Raw HTML to inject into panel body, defaults to panel page if null
cPanel + + +cPanel + + + + Parent Canopy Panel Management Object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

cPanel

+ + + + +
+ Parent Canopy Panel Management Object +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

keepAlive

+ + + + +
+ Disables this.panel.closer() calls from this.closer() +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

panel

+ + + + +
+ Panel Object to apply to Popped Panel +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

panelBody

+ + + + +
+ Raw HTML to inject into panel body, defaults to panel page if null +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelCloseIcon

+ + + + +
+ Popped Panel Close Icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelDiv

+ + + + +
+ Popped Panel Container Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelDoc

+ + + + +
+ Popped Panel Document Div +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

pinnedPanelTitle

+ + + + +
+ Popped Panel Title +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

window

+ + + + +
+ Browser Window taken up by the Popped Panel +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(async) asyncConstructor()

+ + + + + + +
+ Continuation of constructor method for asynchronous function calls +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

closer()

+ + + + + + +
+ Called upon close/exit of panel +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

fillContainer()

+ + + + + + +
+ Fills container window with Popped Panel container elements +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pin()

+ + + + + + +
+ Pins panel next to chat +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

popContainer()

+ + + + + + +
+ Pops/Opens container window upon start +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setupInput()

+ + + + + + +
+ Defines default input-related popped-panel Event Listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

unpop()

+ + + + + + +
+ Un-pops panel into active-panel slot +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/rawFileBase.html b/www/doc/client/rawFileBase.html new file mode 100644 index 0000000..ab77579 --- /dev/null +++ b/www/doc/client/rawFileBase.html @@ -0,0 +1,2923 @@ + + + + + JSDoc: Class: rawFileBase + + + + + + + + + + +
+ +

Class: rawFileBase

+ + + + + + +
+ +
+ +

rawFileBase(client, player, media, type)

+ +
Class containing basic building blocks for anything that touches a
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new rawFileBase(client, player, media, type)

+ + + + + + +
+ Instantiates a new rawFileBase object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
media + + +Object + + + + De-hydrated media object from server
type + + +String + + + + Media Handler Source Type
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/rawFileHandler.html b/www/doc/client/rawFileHandler.html new file mode 100644 index 0000000..64939ea --- /dev/null +++ b/www/doc/client/rawFileHandler.html @@ -0,0 +1,2905 @@ + + + + + JSDoc: Class: rawFileHandler + + + + + + + + + + +
+ +

Class: rawFileHandler

+ + + + + + +
+ +
+ +

rawFileHandler(client, player, media)

+ +
Basic building blocks needed for proper time-synchronized raw-file playback
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new rawFileHandler(client, player, media)

+ + + + + + +
+ Instantiates a new Null Handler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
media + + +Object + + + + De-hydrated media object from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defineListeners()

+ + + + + + +
+ Defines input-related event listeners +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/client/userList.html b/www/doc/client/userList.html index 74f27cc..8c2accc 100644 --- a/www/doc/client/userList.html +++ b/www/doc/client/userList.html @@ -30,7 +30,7 @@

userList(client)

-
Class for object containing logic behind userlist UX
+
Class containing logic behind userlist UX
@@ -1185,13 +1185,13 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/userlist.js.html b/www/doc/client/userlist.js.html index b1677df..d93908b 100644 --- a/www/doc/client/userlist.js.html +++ b/www/doc/client/userlist.js.html @@ -43,7 +43,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.*/ /** - * Class for object containing logic behind userlist UX + * Class containing logic behind userlist UX */ class userList{ /** @@ -246,13 +246,13 @@ class userList{
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/youtubeEmbedHandler.html b/www/doc/client/youtubeEmbedHandler.html new file mode 100644 index 0000000..cd60f4b --- /dev/null +++ b/www/doc/client/youtubeEmbedHandler.html @@ -0,0 +1,2900 @@ + + + + + JSDoc: Class: youtubeEmbedHandler + + + + + + + + + + +
+ +

Class: youtubeEmbedHandler

+ + + + + + +
+ +
+ +

youtubeEmbedHandler(client, player, media)

+ +
Handles Youtube playback via the official YT embed (gross)
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new youtubeEmbedHandler(client, player, media)

+ + + + + + +
+ Instantiates a new Youtube Embed Handler object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
client + + +channel + + + + Parent Client Management Object
player + + +player + + + + Parent Canopy Player Object
media + + +Object + + + + De-hydrated media object from server
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

Extends

+ + + + + + + + + + + + + + + + + + + + +

Members

+ + + +

client

+ + + + +
+ Parent Client Management Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

player

+ + + + +
+ Parent Canopy Player Object +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + +
+ Media Handler Source Type +
+ + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

buildPlayer()

+ + + + + + +
+ Builds video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

destroyPlayer()

+ + + + + + +
+ Destroys video player element +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

end()

+ + + + + + +
+ Handles media end +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getRatio() → {Number}

+ + + + + + +
+ Calculates Aspect Ratio of media +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Aspect Ratio as Floating Point number +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

getTimestamp() → {Number}

+ + + + + + +
+ Gets current timestamp from video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Media Timestamp in seconds +
+ + + +
+
+ Type +
+
+ +Number + + +
+
+ + + + + + + + + + + + + +

ingestMedia(media) → {Boolean}

+ + + + + + +
+ Ingests media object from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from the server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ True upon success +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

onBuffer(event)

+ + + + + + +
+ Called on media buffer +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onMetadataLoad(event)

+ + + + + + +
+ Called once all video metadata has properly been fetched +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onPause(event)

+ + + + + + +
+ Called on media pause +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onSeek(event)

+ + + + + + +
+ Called on media seek +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onStateChange()

+ + + + + + +
+ Generic handler for state changes since google is a dick +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

onVolumeChange(event)

+ + + + + + +
+ Called on media volume change +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event passed down by event handler
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

pause()

+ + + + + + +
+ Pauses video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

play()

+ + + + + + +
+ Plays video +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

reload()

+ + + + + + +
+ Reloads media player +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setPlayerLock(lock)

+ + + + + + +
+ Toggles player control lockout +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
lock + + +Boolean + + + + Whether or not to lock-out user control of video
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setVideoTitle(title)

+ + + + + + +
+ Sets player title +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +String + + + + Title to set
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

start()

+ + + + + + +
+ Starts video playback +
+ + + + + + + + + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

startMedia(media)

+ + + + + + +
+ Ingests media nd starts playback +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
media + + +Object + + + + Media object from server
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

sync(timestamp)

+ + + + + + +
+ Syncronizes timestamp based on timestamp received from server +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
timestamp + + +Number + + + + Current video timestamp in seconds
+ + + + + + +
+ + + + + + + + +
Overrides:
+
+ + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:44 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/www/doc/server/activeChannel.html b/www/doc/server/activeChannel.html index 6e0f755..2504c7e 100644 --- a/www/doc/server/activeChannel.html +++ b/www/doc/server/activeChannel.html @@ -786,7 +786,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_activeChannel.js.html b/www/doc/server/app_channel_activeChannel.js.html index fbc5fbc..348ddf8 100644 --- a/www/doc/server/app_channel_activeChannel.js.html +++ b/www/doc/server/app_channel_activeChannel.js.html @@ -196,7 +196,7 @@ module.exports = activeChannel;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_channelManager.js.html b/www/doc/server/app_channel_channelManager.js.html index 51e0812..ced5aee 100644 --- a/www/doc/server/app_channel_channelManager.js.html +++ b/www/doc/server/app_channel_channelManager.js.html @@ -347,7 +347,7 @@ module.exports = channelManager;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chat.js.html b/www/doc/server/app_channel_chat.js.html index d013229..e1e4fa2 100644 --- a/www/doc/server/app_channel_chat.js.html +++ b/www/doc/server/app_channel_chat.js.html @@ -81,7 +81,7 @@ module.exports = chat;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatBuffer.js.html b/www/doc/server/app_channel_chatBuffer.js.html index 11947f7..3c09baf 100644 --- a/www/doc/server/app_channel_chatBuffer.js.html +++ b/www/doc/server/app_channel_chatBuffer.js.html @@ -178,7 +178,7 @@ module.exports = chatBuffer;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatHandler.js.html b/www/doc/server/app_channel_chatHandler.js.html index c7ffc23..44b3036 100644 --- a/www/doc/server/app_channel_chatHandler.js.html +++ b/www/doc/server/app_channel_chatHandler.js.html @@ -376,7 +376,7 @@ module.exports = chatHandler;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_commandPreprocessor.js.html b/www/doc/server/app_channel_commandPreprocessor.js.html index f2411c6..6514a1e 100644 --- a/www/doc/server/app_channel_commandPreprocessor.js.html +++ b/www/doc/server/app_channel_commandPreprocessor.js.html @@ -473,7 +473,7 @@ module.exports = commandPreprocessor;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_connectedUser.js.html b/www/doc/server/app_channel_connectedUser.js.html index 63bc23e..a520193 100644 --- a/www/doc/server/app_channel_connectedUser.js.html +++ b/www/doc/server/app_channel_connectedUser.js.html @@ -334,7 +334,7 @@ module.exports = connectedUser;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_media.js.html b/www/doc/server/app_channel_media_media.js.html index 1f4249a..f4553bb 100644 --- a/www/doc/server/app_channel_media_media.js.html +++ b/www/doc/server/app_channel_media_media.js.html @@ -83,7 +83,7 @@ module.exports = media;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_playlistHandler.js.html b/www/doc/server/app_channel_media_playlistHandler.js.html index 02d6a5c..0fbd48f 100644 --- a/www/doc/server/app_channel_media_playlistHandler.js.html +++ b/www/doc/server/app_channel_media_playlistHandler.js.html @@ -1180,7 +1180,7 @@ module.exports = playlistHandler;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queue.js.html b/www/doc/server/app_channel_media_queue.js.html index 7ffd44f..61488cf 100644 --- a/www/doc/server/app_channel_media_queue.js.html +++ b/www/doc/server/app_channel_media_queue.js.html @@ -1795,7 +1795,7 @@ module.exports = queue;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queuedMedia.js.html b/www/doc/server/app_channel_media_queuedMedia.js.html index 6bf4702..186edec 100644 --- a/www/doc/server/app_channel_media_queuedMedia.js.html +++ b/www/doc/server/app_channel_media_queuedMedia.js.html @@ -165,7 +165,7 @@ module.exports = queuedMedia;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_tokebot.js.html b/www/doc/server/app_channel_tokebot.js.html index 14732da..730b904 100644 --- a/www/doc/server/app_channel_tokebot.js.html +++ b/www/doc/server/app_channel_tokebot.js.html @@ -273,7 +273,7 @@ module.exports = tokebot;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/channelManager.html b/www/doc/server/channelManager.html index 5e1c56b..f4a7404 100644 --- a/www/doc/server/channelManager.html +++ b/www/doc/server/channelManager.html @@ -1991,7 +1991,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chat.html b/www/doc/server/chat.html index 890fa38..421f210 100644 --- a/www/doc/server/chat.html +++ b/www/doc/server/chat.html @@ -329,7 +329,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatBuffer.html b/www/doc/server/chatBuffer.html index 6df7d3c..1c88a99 100644 --- a/www/doc/server/chatBuffer.html +++ b/www/doc/server/chatBuffer.html @@ -829,7 +829,7 @@ Left here since it seems like good form anywho, since this would be a private, o
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatHandler.html b/www/doc/server/chatHandler.html index 72a56ca..8a2b548 100644 --- a/www/doc/server/chatHandler.html +++ b/www/doc/server/chatHandler.html @@ -3686,7 +3686,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandPreprocessor.html b/www/doc/server/commandPreprocessor.html index 5d60fbd..191b74c 100644 --- a/www/doc/server/commandPreprocessor.html +++ b/www/doc/server/commandPreprocessor.html @@ -1246,7 +1246,7 @@ These arrays are used to handle further command/chat processing
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandProcessor.html b/www/doc/server/commandProcessor.html index 698151b..c54ee7b 100644 --- a/www/doc/server/commandProcessor.html +++ b/www/doc/server/commandProcessor.html @@ -1831,7 +1831,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/connectedUser.html b/www/doc/server/connectedUser.html index 8e48e2f..0bc268c 100644 --- a/www/doc/server/connectedUser.html +++ b/www/doc/server/connectedUser.html @@ -1879,7 +1879,7 @@ Having to crawl through these sockets is that. Because the other ways seem more
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/global.html b/www/doc/server/global.html index 9bc98d7..8c21ffa 100644 --- a/www/doc/server/global.html +++ b/www/doc/server/global.html @@ -7377,7 +7377,7 @@ Warns server admin against unsafe config options.
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/index.html b/www/doc/server/index.html index 91b924e..a5fac30 100644 --- a/www/doc/server/index.html +++ b/www/doc/server/index.html @@ -87,7 +87,7 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/media.html b/www/doc/server/media.html index 05f1463..33b9fa0 100644 --- a/www/doc/server/media.html +++ b/www/doc/server/media.html @@ -352,7 +352,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/playlistHandler.html b/www/doc/server/playlistHandler.html index b507305..98c99a5 100644 --- a/www/doc/server/playlistHandler.html +++ b/www/doc/server/playlistHandler.html @@ -5108,7 +5108,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queue.html b/www/doc/server/queue.html index 0dcfeda..7813882 100644 --- a/www/doc/server/queue.html +++ b/www/doc/server/queue.html @@ -5805,7 +5805,7 @@ Called auto-magically by the Synchronization Timer
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queuedMedia.html b/www/doc/server/queuedMedia.html index 6dae2ba..90c33bb 100644 --- a/www/doc/server/queuedMedia.html +++ b/www/doc/server/queuedMedia.html @@ -936,7 +936,7 @@
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelBanSchema.js.html b/www/doc/server/schemas_channel_channelBanSchema.js.html index f75fc05..dd2b77b 100644 --- a/www/doc/server/schemas_channel_channelBanSchema.js.html +++ b/www/doc/server/schemas_channel_channelBanSchema.js.html @@ -101,7 +101,7 @@ module.exports = channelBanSchema;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelPermissionSchema.js.html b/www/doc/server/schemas_channel_channelPermissionSchema.js.html index 60a8dfb..750b88c 100644 --- a/www/doc/server/schemas_channel_channelPermissionSchema.js.html +++ b/www/doc/server/schemas_channel_channelPermissionSchema.js.html @@ -169,7 +169,7 @@ module.exports = channelPermissionSchema;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelSchema.js.html b/www/doc/server/schemas_channel_channelSchema.js.html index 143bfc4..f392729 100644 --- a/www/doc/server/schemas_channel_channelSchema.js.html +++ b/www/doc/server/schemas_channel_channelSchema.js.html @@ -934,7 +934,7 @@ module.exports = mongoose.model("channel", channelSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_chatSchema.js.html b/www/doc/server/schemas_channel_chatSchema.js.html index f2ecd6a..c1ed22f 100644 --- a/www/doc/server/schemas_channel_chatSchema.js.html +++ b/www/doc/server/schemas_channel_chatSchema.js.html @@ -96,7 +96,7 @@ module.exports = chatSchema;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_mediaSchema.js.html b/www/doc/server/schemas_channel_media_mediaSchema.js.html index 79f37b8..81a1f2c 100644 --- a/www/doc/server/schemas_channel_media_mediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_mediaSchema.js.html @@ -96,7 +96,7 @@ module.exports = mediaSchema;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html index f5e8950..e0e0de1 100644 --- a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html @@ -124,7 +124,7 @@ module.exports = mediaSchema.discriminator('saved', playlistMediaProperties);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistSchema.js.html b/www/doc/server/schemas_channel_media_playlistSchema.js.html index 3bb51f6..e369231 100644 --- a/www/doc/server/schemas_channel_media_playlistSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistSchema.js.html @@ -174,7 +174,7 @@ module.exports = playlistSchema;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html index b1f0ac2..a7152ba 100644 --- a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html @@ -113,7 +113,7 @@ module.exports = mediaSchema.discriminator('queued', queuedProperties);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_emoteSchema.js.html b/www/doc/server/schemas_emoteSchema.js.html index d08a919..c943ef9 100644 --- a/www/doc/server/schemas_emoteSchema.js.html +++ b/www/doc/server/schemas_emoteSchema.js.html @@ -164,7 +164,7 @@ module.exports = mongoose.model("emote", emoteSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_flairSchema.js.html b/www/doc/server/schemas_flairSchema.js.html index 657647f..89a923b 100644 --- a/www/doc/server/schemas_flairSchema.js.html +++ b/www/doc/server/schemas_flairSchema.js.html @@ -118,7 +118,7 @@ module.exports = mongoose.model("flair", flairSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_permissionSchema.js.html b/www/doc/server/schemas_permissionSchema.js.html index 8f40f49..15ef881 100644 --- a/www/doc/server/schemas_permissionSchema.js.html +++ b/www/doc/server/schemas_permissionSchema.js.html @@ -356,7 +356,7 @@ module.exports = mongoose.model("permissions", permissionSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_statSchema.js.html b/www/doc/server/schemas_statSchema.js.html index c61fbf9..d3bac04 100644 --- a/www/doc/server/schemas_statSchema.js.html +++ b/www/doc/server/schemas_statSchema.js.html @@ -240,7 +240,7 @@ module.exports = mongoose.model("statistics", statSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html index 4c5a68e..53b32c0 100644 --- a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html +++ b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html @@ -160,7 +160,7 @@ module.exports = mongoose.model("tokeCommand", tokeCommandSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_emailChangeSchema.js.html b/www/doc/server/schemas_user_emailChangeSchema.js.html index 9a392c1..5d491e4 100644 --- a/www/doc/server/schemas_user_emailChangeSchema.js.html +++ b/www/doc/server/schemas_user_emailChangeSchema.js.html @@ -222,7 +222,7 @@ module.exports = mongoose.model("emailChange", emailChangeSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_passwordResetSchema.js.html b/www/doc/server/schemas_user_passwordResetSchema.js.html index 16be6b7..38156eb 100644 --- a/www/doc/server/schemas_user_passwordResetSchema.js.html +++ b/www/doc/server/schemas_user_passwordResetSchema.js.html @@ -198,7 +198,7 @@ module.exports = mongoose.model("passwordReset", passwordResetSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userBanSchema.js.html b/www/doc/server/schemas_user_userBanSchema.js.html index 6f532e3..4c2954d 100644 --- a/www/doc/server/schemas_user_userBanSchema.js.html +++ b/www/doc/server/schemas_user_userBanSchema.js.html @@ -521,7 +521,7 @@ module.exports = mongoose.model("userBan", userBanSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userSchema.js.html b/www/doc/server/schemas_user_userSchema.js.html index 10ee9d0..385a7f4 100644 --- a/www/doc/server/schemas_user_userSchema.js.html +++ b/www/doc/server/schemas_user_userSchema.js.html @@ -888,7 +888,7 @@ module.exports.userModel = mongoose.model("user", userSchema);
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/tokebot.html b/www/doc/server/tokebot.html index 5e3f64b..39cedd1 100644 --- a/www/doc/server/tokebot.html +++ b/www/doc/server/tokebot.html @@ -841,7 +841,7 @@ I would now, but I don't want to break shit in a comment-only commit.
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_altchaUtils.js.html b/www/doc/server/utils_altchaUtils.js.html index 3230072..9b888ce 100644 --- a/www/doc/server/utils_altchaUtils.js.html +++ b/www/doc/server/utils_altchaUtils.js.html @@ -118,7 +118,7 @@ module.exports.verify = async function(payload, uniqueSecret = ''){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_configCheck.js.html b/www/doc/server/utils_configCheck.js.html index f230ed1..408a772 100644 --- a/www/doc/server/utils_configCheck.js.html +++ b/www/doc/server/utils_configCheck.js.html @@ -108,7 +108,7 @@ module.exports.securityCheck = function(){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_hashUtils.js.html b/www/doc/server/utils_hashUtils.js.html index 4ccb566..af4d8ba 100644 --- a/www/doc/server/utils_hashUtils.js.html +++ b/www/doc/server/utils_hashUtils.js.html @@ -103,7 +103,7 @@ module.exports.hashIP = function(ip){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_linkUtils.js.html b/www/doc/server/utils_linkUtils.js.html index 53c42b2..4bee12a 100644 --- a/www/doc/server/utils_linkUtils.js.html +++ b/www/doc/server/utils_linkUtils.js.html @@ -146,7 +146,7 @@ module.exports.markLink = async function(link){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_loggerUtils.js.html b/www/doc/server/utils_loggerUtils.js.html index 53e51ab..20cafd3 100644 --- a/www/doc/server/utils_loggerUtils.js.html +++ b/www/doc/server/utils_loggerUtils.js.html @@ -207,7 +207,7 @@ module.exports.errorMiddleware = function(err, req, res, next){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_mailUtils.js.html b/www/doc/server/utils_mailUtils.js.html index 067fd69..f9ff380 100644 --- a/www/doc/server/utils_mailUtils.js.html +++ b/www/doc/server/utils_mailUtils.js.html @@ -140,7 +140,7 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_internetArchiveUtils.js.html b/www/doc/server/utils_media_internetArchiveUtils.js.html index da6f930..60d0abe 100644 --- a/www/doc/server/utils_media_internetArchiveUtils.js.html +++ b/www/doc/server/utils_media_internetArchiveUtils.js.html @@ -154,7 +154,7 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_yanker.js.html b/www/doc/server/utils_media_yanker.js.html index 63610fd..9381dc7 100644 --- a/www/doc/server/utils_media_yanker.js.html +++ b/www/doc/server/utils_media_yanker.js.html @@ -193,7 +193,7 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_ytdlpUtils.js.html b/www/doc/server/utils_media_ytdlpUtils.js.html index 66c0dae..400a92b 100644 --- a/www/doc/server/utils_media_ytdlpUtils.js.html +++ b/www/doc/server/utils_media_ytdlpUtils.js.html @@ -186,7 +186,7 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_regexUtils.js.html b/www/doc/server/utils_regexUtils.js.html index fd9f1a1..b1e7099 100644 --- a/www/doc/server/utils_regexUtils.js.html +++ b/www/doc/server/utils_regexUtils.js.html @@ -69,7 +69,7 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_scheduler.js.html b/www/doc/server/utils_scheduler.js.html index 500ce72..e067e4f 100644 --- a/www/doc/server/utils_scheduler.js.html +++ b/www/doc/server/utils_scheduler.js.html @@ -105,7 +105,7 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_sessionUtils.js.html b/www/doc/server/utils_sessionUtils.js.html index c2ca71d..c4b04a3 100644 --- a/www/doc/server/utils_sessionUtils.js.html +++ b/www/doc/server/utils_sessionUtils.js.html @@ -236,7 +236,7 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Thu Sep 04 2025 20:09:42 GMT-0400 (Eastern Daylight Time)
diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 3a9d511..49f9dc9 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ /** - * Class for object containing base code for the Canopy channel client. + * Class containing base code for the Canopy channel client. */ class channel{ /** diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index 81624fd..0f2eae4 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ /** - * Class for Object which represents Canopy Chat Box UI + * Class which represents Canopy Chat Box UI */ class chatBox{ /** @@ -24,7 +24,7 @@ class chatBox{ */ constructor(client){ /** - * Parent CLient Management Object + * Parent Client Management Object */ this.client = client @@ -69,20 +69,75 @@ class chatBox{ this.chatPostprocessor = new chatPostprocessor(client); //Element Nodes + /** + * Chat Panel Container Div + */ this.chatPanel = document.querySelector("#chat-panel-div"); + + /** + * High Level Selector + */ this.highSelect = document.querySelector("#chat-panel-high-level-select"); + + /** + * Flair Selector + */ this.flairSelect = document.querySelector("#chat-panel-flair-select"); + + /** + * Chat Buffer Div + */ this.chatBuffer = document.querySelector("#chat-panel-buffer-div"); + + /** + * Chat Prompt + */ this.chatPrompt = document.querySelector("#chat-panel-prompt"); + + /** + * Auto-Complete Placeholder + */ this.autocompletePlaceholder = document.querySelector("#chat-panel-prompt-autocomplete-filler"); + + /** + * Auto-Complete Display + */ this.autocompleteDisplay = document.querySelector("#chat-panel-prompt-autocomplete-display"); + + /** + * Settings Panel Icon + */ this.settingsIcon = document.querySelector("#chat-panel-settings-icon"); + + /** + * Admin Panel Icon + */ this.adminIcon = document.querySelector("#chat-panel-admin-icon"); + + /** + * Emote Icon + */ this.emoteIcon = document.querySelector("#chat-panel-emote-icon"); + + /** + * Send Chat/Command Button + */ this.sendButton = document.querySelector("#chat-panel-send-button"); - //Seems weird to stick this in here, but the split is dictated by chat width :P + + /** + * Aspect Lock Icon + * Seems weird to stick this in here, but the split is dictated by chat width :P + */ this.aspectLockIcon = document.querySelector("#media-panel-aspect-lock-icon"); + + /** + * Hide Chat Icon + */ this.hideChatIcon = document.querySelector("#chat-panel-div-hide"); + + /** + * Show Chat Icon + */ this.showChatIcon = document.querySelector("#media-panel-show-chat-icon"); //Setup functions @@ -91,6 +146,9 @@ class chatBox{ this.sizeToAspect(); } + /** + * Defines input-related event listeners + */ setupInput(){ //Chat bar this.chatPrompt.addEventListener("keydown", this.send.bind(this)); @@ -118,11 +176,18 @@ class chatBox{ this.chatBuffer.addEventListener('scroll', this.scrollHandler.bind(this)); } + /** + * Defines network-related event listners + */ defineListeners(){ this.client.socket.on("chatMessage", this.displayChat.bind(this)); this.client.socket.on("clearChat", this.clearChat.bind(this)); } + /** + * Clears chat on command from server + * @param {Object} data - Data from server + */ clearChat(data){ //If we where passed a user to check if(data.user != null){ @@ -138,6 +203,10 @@ class chatBox{ }); } + /** + * Receives, Post-Processes, and Displays chat messages from server + * @param {Object} data De-hydrated chat object from server + */ displayChat(data){ //Create chat-entry span var chatEntry = document.createElement('span'); @@ -186,15 +255,27 @@ class chatBox{ this.resizeAspect(); } + /** + * Concatinate Text into Chat Prompt + * @param {String} text - Text to Concatinate + */ catChat(text){ this.chatPrompt.value += text; this.displayAutocomplete(); } + /** + * Calls a toke command out with a specified user + * @param {String} user - User to toke with + */ tokeWith(user){ this.commandPreprocessor.preprocess(user == this.client.user.user ? "!toke up fuckers" : `!toke up ${user}`); } + /** + * Pre-processes and sends text from chat prompt to server + * @param {Event} event - Event passed down from Event Handler + */ send(event){ if((!event || !event.key || event.key == "Enter") && this.chatPrompt.value){ this.commandPreprocessor.preprocess(this.chatPrompt.value); @@ -205,6 +286,10 @@ class chatBox{ } } + /** + * Displays auto-complete text against current prompt input + * @param {Event} event - Event passed down from Event Handler + */ displayAutocomplete(event){ //Find current match const match = this.checkAutocomplete(); @@ -216,6 +301,10 @@ class chatBox{ this.autocompleteDisplay.textContent = match.match.replace(match.word, ''); } + /** + * Called upon tab-complete + * @param {Event} event - Event passed down from Event Handler + */ tabComplete(event){ //If we hit tab or this isn't a keyboard event if(event.key == "Tab" || event.key == null){ @@ -239,6 +328,11 @@ class chatBox{ } } + /** + * Checks string input against auto-complete dictionary to generate the best guess as to what the user is typing + * @param {String} input - Current input from Chat Prompt + * @returns {Object} Object containing word we where handed and the match we found + */ checkAutocomplete(input = this.chatPrompt.value){ //Rebuild this fucker every time because it really doesn't take that much compute power and emotes/used tokes change //Worst case we could store it persistantly and update as needed but I think that might be much @@ -287,27 +381,48 @@ class chatBox{ } } + /** + * Handles initial client meta-data dump from server upon connection + * @param {Object} data - Data dump from server + */ handleClientInfo(data){ this.updateFlairSelect(data.flairList, data.user.flair); this.updateHighSelect(data.user.highLevel); } + /** + * Sets user high-level + * @param {Event} event - Event passed down from Event Handler + */ setHighLevel(event){ const highLevel = event.target.value; this.client.socket.emit("setHighLevel", {highLevel}); } + /** + * Sets user flair + * @param {Event} event - Event passed down from Event Handler + */ setFlair(event){ const flair = event.target.value; this.client.socket.emit("setFlair", {flair}); } + /** + * Handles High-Level updates from the server + * @param {Number} highLevel - High Level to Set + */ updateHighSelect(highLevel){ this.highSelect.value = highLevel; } + /** + * Handles flair updates from the server + * @param {Array} fliarList - List of flairs to put into flair select + * @param {String} fliar - Flair to set + */ updateFlairSelect(flairList, flair){ //clear current flair select this.flairSelect.innerHTML = ""; @@ -331,6 +446,10 @@ class chatBox{ this.flairSelect.classList.add(`flair-${flair}`); } + /** + * Locks chat-size to aspect ratio of media + * @param {Event} event - Event passed down from Event Handler + */ lockAspect(event){ //prevent the user from breaking shit :P if(this.chatPanel.style.display != "none"){ @@ -340,6 +459,10 @@ class chatBox{ } } + /** + * Un-locks chat-size to aspect ratio of media + * @param {Event} event - Event passed down from Event Handler + */ unlockAspect(event){ //Disable aspect lock this.aspectLock = false; @@ -348,6 +471,11 @@ class chatBox{ this.aspectLockIcon.style.display = "inline"; } +L /** + * Re-sizes chat back to aspect ratio on window re-size when chat box is aspect locked + * Also prevents horizontal scroll-bars from chat/window resizing + * @param {Event} event - Event passed down from Event Handler + */ resizeAspect(event){ const playerHidden = this.client.player.playerDiv.style.display == "none"; @@ -364,6 +492,9 @@ class chatBox{ this.handleAutoScroll(); } +L /** + * Re-sizes chat box relative to media aspect ratio + */ sizeToAspect(){ if(this.chatPanel.style.display != "none"){ var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height; @@ -384,6 +515,10 @@ class chatBox{ } } + /** + * Toggles Chat Box UX + * @param {Boolean} show - Whether or not to show Chat Box UX + */ toggleUI(show = !this.chatPanel.checkVisibility()){ if(show){ this.chatPanel.style.display = "flex"; @@ -397,6 +532,10 @@ class chatBox{ } } + /** + * Handles Video Toggling + * @param {Boolean} show - Whether or not the video is currently being hidden + */ handleVideoToggle(show){ //If we're enabling the video if(show){ @@ -421,6 +560,10 @@ class chatBox{ } } + /** + * Handles scrolling within the chat buffer + * @param {Event} event - Event passed down from Event Handler + */ scrollHandler(event){ //If we're just starting out if(this.lastPos == 0){ @@ -465,6 +608,9 @@ class chatBox{ this.lastWidth = bufferWidth; } + /** + * Auto-scrolls chat buffer when new chats are entered. + */ handleAutoScroll(){ //If autoscroll is enabled if(this.autoScroll){ diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 4b8759d..6bc03c0 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -13,11 +13,28 @@ 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 .*/ + +/** + * Class contianing client-side message post-processing code + */ class chatPostprocessor{ + /** + * Instantiates a new Chat Post-Processor object + * @param {channel} client - Parent client Management Object + */ constructor(client){ + /** + * Parent Client Management Object + */ this.client = client; } + /** + * Post-Processes a single message from the server and returns a presntable DOM Node + * @param {Node} chatEntry - Chat entry generated by initial chatBox method + * @param {Object} rawData - Raw data from server + * @returns {Node} Post-Processed Chat Entry + */ postprocess(chatEntry, rawData){ //Create empty array to hold filter spans this.filterSpans = []; @@ -30,6 +47,7 @@ class chatPostprocessor{ //Split the chat message into an array of objects representing each word/chunk this.splitMessage(); + //Process Qoutes this.processQoute(); //Re-Hydrate and Inject links and embedded media into un-processed placeholders @@ -62,13 +80,16 @@ class chatPostprocessor{ //Handle non-standard chat types this.handleChatType(); - //Inject the pre-processed chat into the chatEntry node + //Inject the pre-processed chat hyper-text into the chatEntry node this.injectBody(); //Return the pre-processed node return this.chatEntry; } + /** + * Splits message into an array of Word Objects for further processing + */ splitMessage(){ //Create an empty array to hold the body this.messageArray = []; @@ -93,6 +114,9 @@ class chatPostprocessor{ }); } + /** + * Injects word objects into chat-entry as proper DOM Nodes + */ injectBody(){ //Create an empty array to hold the objects to inject const injectionArray = []; @@ -285,6 +309,9 @@ class chatPostprocessor{ } } + /** + * Processes qouted text in chat + */ processQoute(){ //If the message starts off with '>' if(this.messageArray[0].string[0] == '>'){ @@ -292,6 +319,9 @@ class chatPostprocessor{ } } + /** + * Processes clickable command examples in chat + */ processCommandExamples(){ //for each word object in the body this.messageArray.forEach((wordObj, wordIndex) => { @@ -324,6 +354,9 @@ class chatPostprocessor{ }); } + /** + * Processes clickable channel names in chat + */ processChannelNames(){ //for each word object in the body this.messageArray.forEach((wordObj, wordIndex) => { @@ -356,6 +389,9 @@ class chatPostprocessor{ }); } + /** + * Processes clickable username callouts in chat + */ processUsernames(){ //for each word object in the body this.messageArray.forEach((wordObj, wordIndex) => { @@ -375,6 +411,9 @@ class chatPostprocessor{ }); } + /** + * Injects invisible whitespace in long-ass words to prevent fucking up the chat buffer size + */ addWhitespace(){ //for each word object in the body this.messageArray.forEach((wordObj, wordIndex) => { @@ -400,6 +439,14 @@ class chatPostprocessor{ }); } + /** + * Searches for text in-between a specific delimiter and runs a given callback against it + * + * Internal command used by several text filters to prevent code re-writes + * @param {String} delimiter - delimiter to search string by + * @param {Function} cb - Callback function to run against found strings + * @returns {Array} - list of found instances of filter + */ processFilter(delimiter, cb){ //Create empty array to hold spoilers (keep this seperate at first for internal function use) const foundFilters = []; @@ -451,6 +498,9 @@ class chatPostprocessor{ return foundFilters; } + /** + * Processes in-line spoilers + */ processSpoilers(){ //Process spoilers using '##' delimiter this.processFilter('##', (foundSpoiler)=>{ @@ -459,6 +509,9 @@ class chatPostprocessor{ }); } + /** + * Processes in-line Strike-through + */ processStrikethrough(){ //Process strikethrough's using '~~' delimiter this.processFilter('~~', (foundStrikethrough)=>{ @@ -468,6 +521,9 @@ class chatPostprocessor{ }) } + /** + * Processes in-line Bold/Strong text + */ processBold(){ //Process strong text using '*' delimiter this.processFilter('**', (foundStrikethrough)=>{ @@ -477,6 +533,9 @@ class chatPostprocessor{ }) } + /** + * Processes in-line Italics + */ processItalics(){ //Process italics using '__' delimiter this.processFilter('*', (foundStrikethrough)=>{ @@ -486,6 +545,9 @@ class chatPostprocessor{ }) } + /** + * Processes clickable links and embedded media + */ processLinks(){ //If we don't have links if(this.rawData.links == null){ @@ -513,6 +575,9 @@ class chatPostprocessor{ }); } + /** + * Marks chat nodes in-case of non-standard chat types + */ handleChatType(){ if(this.rawData.type == "whisper"){ //add whisper class diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index 6486cc9..ddfb9f6 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ /** - * Class for object containing chat and command pre-processing logic + * Class containing chat and command pre-processing logic */ class commandPreprocessor{ /** @@ -300,7 +300,7 @@ class commandPreprocessor{ } /** - * Class for Object which contains logic for client-side commands + * Class which contains logic for client-side commands */ class commandProcessor{ /** diff --git a/www/js/channel/cpanel.js b/www/js/channel/cpanel.js index 53c2f44..3a180a0 100644 --- a/www/js/channel/cpanel.js +++ b/www/js/channel/cpanel.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ /** - * Class for Object containing code for managing the Canopy Panel UX + * Class containing code for managing the Canopy Panel UX */ class cPanel{ /** @@ -323,7 +323,7 @@ class panelObj{ } /** - * Class for Objects which represent a single instance of a popped-out panel + * Class which represents a single instance of a popped-out panel */ class poppedPanel{ /** diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 5432ebd..0fde0d1 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -14,26 +14,53 @@ 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 .*/ -//This is little more than a interface class +/* + * Base class for all Canopy Media Handlers + * + * This is little more than a interface class + */ class mediaHandler{ + /** + * Instantiates a new Media Handler object + * @param {channel} client - Parent Client Management Object + * @param {player} player - Parent Canopy Player Object + * @param {Object} media - De-hydrated media object from server + * @param {String} type - Media Handler Source Type + */ constructor(client, player, media, type){ - //Get parents + /** + * Parent Client Management Object + */ this.client = client; + + /** + * Parent Canopy Player Object + */ this.player = player; - //Set handler type + /** + * Media Handler Source Type + */ this.type = type - //Denotes wether a seek call was made by the syncing function + /* + * Denotes wether a seek call was made by the syncing function + */ this.selfAct = false; - //Set last received timestamp to 0 + /* + * Contains the last received time stamp + */ this.lastTimestamp = 0; //Ingest media object from server this.startMedia(media); } + /** + * Ingests media nd starts playback + * @param {Object} media - Media object from server + */ startMedia(media){ //If we properly ingested the media if(this.ingestMedia(media)){ @@ -45,16 +72,27 @@ class mediaHandler{ } } + /** + * Builds video player element + */ buildPlayer(){ //Reset player lock this.lock = false; } + /** + * Destroys video player element + */ destroyPlayer(){ //Null out video property this.video = null; } + /** + * Ingests media object from server + * @param {Object} media - Media object from the server + * @returns {Boolean} True upon success + */ ingestMedia(media){ //Set now playing this.nowPlaying = media; @@ -63,10 +101,17 @@ class mediaHandler{ return true; } + /** + * Starts video playback + */ start(){ this.setVideoTitle(this.nowPlaying.title); } + /** + * Syncronizes timestamp based on timestamp received from server + * @param {Number} timestamp - Current video timestamp in seconds + */ sync(timestamp = this.lastTimestamp){ //Skip sync calls that won't seek so we don't pointlessly throw selfAct if(timestamp != this.video.currentTime){ @@ -75,6 +120,9 @@ class mediaHandler{ } } + /** + * Reloads media player + */ reload(){ //Get current timestamp const timestamp = this.video.currentTime; @@ -83,6 +131,9 @@ class mediaHandler{ this.selfAct = true; } + /** + * Handles media end + */ end(){ //Null out current media this.nowPlaying = null; @@ -94,32 +145,64 @@ class mediaHandler{ this.destroyPlayer(); } + /** + * Plays video + */ play(){ } + /** + * Pauses video + */ pause(){ } + /** + * Toggles player control lockout + * @param {Boolean} lock - Whether or not to lock-out user control of video + */ setPlayerLock(lock){ //set lock property this.lock = lock; } + /** + * Calculates Aspect Ratio of media + * @returns {Number} Media Aspect Ratio as Floating Point number + */ getRatio(){ + return 4/3; } + /** + * Gets current timestamp from video + * @returns {Number} Media Timestamp in seconds + */ getTimestamp(){ + return 0; } + /** + * Sets player title + * @param {String} title - Title to set + */ setVideoTitle(title){ this.player.title.textContent = `Currently Playing: ${title}`; } + /** + * Called once all video metadata has properly been fetched + * @param {Event} event - Event passed down by event handler + */ onMetadataLoad(event){ //Resize aspect (if locked), since the video doesn't properly report it's resolution until it's been loaded this.client.chatBox.resizeAspect(); } + /** + * Called on media pause + * @param {Event} event - Event passed down by event handler + */ onPause(event){ //If the video was paused out-side of code if(!this.selfAct){ @@ -129,9 +212,17 @@ class mediaHandler{ this.selfAct = false; } + /** + * Called on media volume change + * @param {Event} event - Event passed down by event handler + */ onVolumeChange(event){ } + /** + * Called on media seek + * @param {Event} event - Event passed down by event handler + */ onSeek(event){ //If the video was seeked out-side of code if(!this.selfAct){ @@ -142,17 +233,34 @@ class mediaHandler{ this.selfAct = false; } + /** + * Called on media buffer + * @param {Event} event - Event passed down by event handler + */ onBuffer(){ this.selfAct = true; } } -//Basic building blocks for anything that touches a

)CdQ3F!eKyF|9D2V!F-rhUpJ8J+l7g0OBBVM#THTI&O8)>EGR%u6Gd9D9pm5!S}%&6DxW}^f|7=Y zPoO3(pTZY#?(7(|!5}5Nn!D%DotZmlW)?smSMcEE<^aT$6gw#LlwubPI9BYTffL0! zyu-EPCnz{Y#ZR&1d{F!hr_NW!&#~mXis$jseXDo@U)-kR7sMBeUt-T&RQw9By@BF9 z3f?cpmw4m-R{RHncaC**(V--ipJ<~6LkW2fi6RVfh%vcYt9@z>&M0LBSf-Q|Et8wU zCt43_*JB)mHR71wb`K@~5Cizwp{`A2uuJ^_Bcl3k{7ree$8&@l?;^2nagS+NqCDBfkB?pJws=PbK~+A7|2 z{gCDJKI-i%m4LD$n{WIwWR|c+NRy`C1#)1sSBI7FiH6z-QkhY&Q_|%I3exQ zQ`X1M?cZH4^M&BSyr;2z$+^SZUMA*0001Z+HKHROw(}?!13=vX`$@Br+fGR zZ%e`5O6%Txi$Yrz0gF{}p>fY>OnlS0Uevf}oDXW;D{d2gcE<2)oFcV80@g$H)63L{HN*d{8kVzKVW(;E)$9N_%kx5Ku3R9WJbY?JW^G#k0Wdx>E$NBBVtKRLiL?sA*s%w`TdsNz1=+~FRNdB8&+@iBD0 zXFTC4C-8-Cwv(4U=LLQ~^Oa4^rG|OTr5?ItoaPMYxxh`%a*kVU z;HYGAjq6;IY{`*awo0DlOMw(hkrYdb(O28l;MYvSx*ChcQW4f^QL5UdE3HbqvbxB$pfSg`>Cj#;?~00;nMAg}==M6d%RaIhCe zARtS)01i=0um)3FSgr#ump{<1pq_<0a34Kp8x=7I1^|9 literal 0 HcmV?d00001 diff --git a/www/doc/client/fonts/OpenSans-Regular-webfont.eot b/www/doc/client/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..6bbc3cf58cb011a6b4bf3cb1612ce212608f7274 GIT binary patch literal 19836 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/doc/client/fonts/OpenSans-Regular-webfont.woff b/www/doc/client/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..e231183dce4c7b452afc9e7799586fd285e146f4 GIT binary patch literal 22660 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 ML;wL%uV=LY00kR;TmS$7 literal 0 HcmV?d00001 diff --git a/www/doc/client/global.html b/www/doc/client/global.html new file mode 100644 index 0000000..d122a61 --- /dev/null +++ b/www/doc/client/global.html @@ -0,0 +1,217 @@ + + + + + JSDoc: Global + + + + + + + + + + +

@@ -207,7 +221,7 @@ module.exports.errorMiddleware = function(err, req, res, next){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:23:36 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_mailUtils.js.html b/www/doc/server/utils_mailUtils.js.html index 0ad5f5c..f73cd30 100644 --- a/www/doc/server/utils_mailUtils.js.html +++ b/www/doc/server/utils_mailUtils.js.html @@ -134,13 +134,13 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_internetArchiveUtils.js.html b/www/doc/server/utils_media_internetArchiveUtils.js.html index 060afb1..063a1e0 100644 --- a/www/doc/server/utils_media_internetArchiveUtils.js.html +++ b/www/doc/server/utils_media_internetArchiveUtils.js.html @@ -57,7 +57,6 @@ const loggerUtils = require('../loggerUtils.js') * @returns {Array} Generated list of media objects from given upload path */ module.exports.fetchMetadata = async function(fullID, title){ - console.log("FUCK"); //Split fullID by first slash const [itemID, requestedPath] = decodeURIComponent(fullID).split(/\/(.*)/); //Create empty list to hold media objects @@ -127,8 +126,6 @@ module.exports.fetchMetadata = async function(fullID, title){ } } - console.log(mediaList); - //return media object list return mediaList; @@ -151,13 +148,13 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_yanker.js.html b/www/doc/server/utils_media_yanker.js.html index 5815fa4..68bd14e 100644 --- a/www/doc/server/utils_media_yanker.js.html +++ b/www/doc/server/utils_media_yanker.js.html @@ -187,13 +187,13 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_ytdlpUtils.js.html b/www/doc/server/utils_media_ytdlpUtils.js.html index a36a65a..e7a2e2b 100644 --- a/www/doc/server/utils_media_ytdlpUtils.js.html +++ b/www/doc/server/utils_media_ytdlpUtils.js.html @@ -180,13 +180,13 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_regexUtils.js.html b/www/doc/server/utils_regexUtils.js.html index 60a8f6d..bd5e072 100644 --- a/www/doc/server/utils_regexUtils.js.html +++ b/www/doc/server/utils_regexUtils.js.html @@ -63,13 +63,13 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_scheduler.js.html b/www/doc/server/utils_scheduler.js.html index 2f67d9f..2a53346 100644 --- a/www/doc/server/utils_scheduler.js.html +++ b/www/doc/server/utils_scheduler.js.html @@ -99,13 +99,13 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_sessionUtils.js.html b/www/doc/server/utils_sessionUtils.js.html index 5eb7e6e..40a5087 100644 --- a/www/doc/server/utils_sessionUtils.js.html +++ b/www/doc/server/utils_sessionUtils.js.html @@ -230,13 +230,13 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 00:47:13 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time)
From e9c474eaf0bf29e358a23e706e2a5fbe6891b92c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 6 Sep 2025 09:08:07 -0400 Subject: [PATCH 087/209] Added basic option to specify alternative IA CDN servers within the settings panel. --- .gitignore | 2 +- src/views/partial/panels/settings.ejs | 5 ++++ www/css/panel/settings.css | 13 ++++++++++ www/js/channel/channel.js | 29 ++++++++++++++++++---- www/js/channel/panels/settingsPanel.js | 21 ++++++++++++++++ www/js/channel/player.js | 33 ++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 4d019d4..b7bef3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ -log/ +log/crash/* package-lock.json config.json state.json diff --git a/src/views/partial/panels/settings.ejs b/src/views/partial/panels/settings.ejs index 5b0eab5..ad14ec5 100644 --- a/src/views/partial/panels/settings.ejs +++ b/src/views/partial/panels/settings.ejs @@ -16,6 +16,7 @@ along with this program. If not, see . %>

Client Settings

+

Player Settings

Youtube Player Type:

+ +

Internet Archive CDN Server:

+ +
\ No newline at end of file diff --git a/www/css/panel/settings.css b/www/css/panel/settings.css index 95bfa62..869e0d5 100644 --- a/www/css/panel/settings.css +++ b/www/css/panel/settings.css @@ -17,6 +17,7 @@ along with this program. If not, see .*/ display: flex; flex-direction: column; gap: 1em; + align-items: center; } #settings-panel h2{ @@ -24,10 +25,22 @@ along with this program. If not, see .*/ margin: 1em 0 0; } +#settings-panel h4{ + text-align: center; + margin: 0; +} + .settings-panel-setting{ display: flex; flex-direction: row; text-wrap: nowrap; height: 1em; align-items: center; + max-width: 30em; + width: 100% +} + +.settings-panel-setting :is(input, select){ + width: 1px; + flex: 1 1 auto; } \ No newline at end of file diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 49f9dc9..3e4ff1a 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -166,6 +166,9 @@ class channel{ * @param {*} value - Value to set setting to */ processConfig(key, value){ + //Unfortunately we can't scope constants to switch-cases so this is the best we got if we wanna re-use the name + let nowPlaying; + //Switch/case by config key switch(key){ case 'ytPlayerType': @@ -205,18 +208,35 @@ class channel{ return; } - //Get current video - const nowPlaying = this.player.mediaHandler.nowPlaying; + nowPlaying = this.player.mediaHandler.nowPlaying; //If we're playing a youtube video if(nowPlaying != null && nowPlaying.type == 'yt'){ //Restart the video - this.player.start({media: nowPlaying}); + this.player.hardReload(); } //Stop while we're ahead return; + + case 'IACDN': + //If the player or mediaHandler isn't loaded + if(this.player == null || this.player.mediaHandler == null){ + //We're fuggin done here + return; + } + + //Get current video + nowPlaying = this.player.mediaHandler.nowPlaying; + + //If we're playing a video from Internet Archive + if(nowPlaying != null && nowPlaying.type == 'ia'){ + //Hard reload the media, forcing media handler re-creation + this.player.hardReload(); + } + + return; } } @@ -224,7 +244,8 @@ class channel{ * Default channel config */ static defaultConfig = new Map([ - ["ytPlayerType","raw"] + ["ytPlayerType","raw"], + ["IACDN",""] ]); } diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index 73849a7..3f9ca54 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -32,8 +32,16 @@ class settingsPanel extends panelObj{ } docSwitch(){ + /** + * Youtube Source Selector + */ this.youtubeSource = this.panelDocument.querySelector("#settings-panel-youtube-source select"); + /** + * Internet Archive CDN Server Input + */ + this.IACDNInput = this.panelDocument.querySelector("#settings-panel-ia-server input"); + this.renderSettings(); this.setupInput(); } @@ -43,6 +51,7 @@ class settingsPanel extends panelObj{ */ setupInput(){ this.youtubeSource.addEventListener('change', this.updateYoutubeSource.bind(this)); + this.IACDNInput.addEventListener('keydown', this.updateIACDN.bind(this)); } /** @@ -50,6 +59,7 @@ class settingsPanel extends panelObj{ */ renderSettings(){ this.youtubeSource.value = localStorage.getItem("ytPlayerType"); + this.IACDNInput.value = localStorage.getItem("IACDN"); } /** @@ -59,4 +69,15 @@ class settingsPanel extends panelObj{ localStorage.setItem("ytPlayerType", this.youtubeSource.value); client.processConfig("ytPlayerType", this.youtubeSource.value); } + + /** + * Event handler for Internet Archive CDN Server input + * @param {Event} event - Event handed down by event listener + */ + updateIACDN(event){ + if(event.key == "Enter"){ + localStorage.setItem("IACDN", this.IACDNInput.value); + client.processConfig("IACDN", this.IACDNInput.value); + } + } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 5db4734..ec218f0 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -192,6 +192,18 @@ class player{ this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media); //Otherwise, if we have a raw-file compatible source }else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){ + //If we're running a source from IA + if(data.media.type == 'ia'){ + //Replace specified CDN with generic URL, in-case of hard reload + data.media.rawLink = data.media.rawLink.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download") + + //If we have an IA source and a custom IA CDN Server set + if(data.media.type == 'ia' && localStorage.getItem("IACDN") != ""){ + //Generate and set new link + data.media.rawLink = data.media.rawLink.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`); + } + } + //Create a new raw file handler for it this.mediaHandler = new rawFileHandler(client, this, data.media); //Sync to time stamp @@ -244,6 +256,27 @@ class player{ } } + /** + * Destroys and Re-Creates media handler + */ + hardReload(){ + if(this.mediaHandler != null){ + //Re-create data we'd get from server + const data = { + media: this.mediaHandler.nowPlaying, + timestamp: this.mediaHandler.getTimestamp() + } + + //End current media handler + this.end(); + + console.log(data); + + //Restart from last media handlers + this.start(data); + } + } + /** * Handles End-Media Commands from the Server */ From 306f22aa9337e1fe4467be5708eb6a2a6103fd25 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 6 Sep 2025 09:16:40 -0400 Subject: [PATCH 088/209] Added validation step to IA CDN server setting --- www/js/channel/panels/settingsPanel.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index 3f9ca54..d8ff081 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -75,7 +75,17 @@ class settingsPanel extends panelObj{ * @param {Event} event - Event handed down by event listener */ updateIACDN(event){ + //If we hit enter if(event.key == "Enter"){ + //If we have an invalid server string + if(!(this.IACDNInput.value.match(/^ia[0-9]{6}\...$/g) || this.IACDNInput.value == "")){ + //reset back to what was set before + this.IACDNInput.value = localStorage.getItem('IACDN'); + + //BAIL! + return; + } + localStorage.setItem("IACDN", this.IACDNInput.value); client.processConfig("IACDN", this.IACDNInput.value); } From 132fdabb2985cd69c163612f260b16e2042aaa45 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 6 Sep 2025 10:34:06 -0400 Subject: [PATCH 089/209] Added improved settings panel. --- src/views/partial/panels/settings.ejs | 21 +- www/css/panel/settings.css | 5 + www/doc/client/addURLPopup.html | 2 +- www/doc/client/cPanel.html | 2 +- www/doc/client/channel.html | 4 +- www/doc/client/channel.js.html | 75 +- www/doc/client/chat.js.html | 13 +- www/doc/client/chatBox.html | 150 +- www/doc/client/chatPostprocessor.html | 2 +- www/doc/client/chatPostprocessor.js.html | 2 +- www/doc/client/clearPopup.html | 2 +- www/doc/client/commandPreprocessor.html | 2 +- www/doc/client/commandPreprocessor.js.html | 2 +- www/doc/client/commandProcessor.html | 2 +- www/doc/client/cpanel.js.html | 2 +- www/doc/client/defaultTitlesPopup.html | 2 +- www/doc/client/emotePanel.html | 2 +- www/doc/client/global.html | 4 +- www/doc/client/hlsBase.html | 2 +- www/doc/client/hlsLiveStreamHandler.html | 2 +- www/doc/client/index.html | 2 +- www/doc/client/mediaHandler.html | 2 +- www/doc/client/mediaHandler.js.html | 4 +- www/doc/client/newPlaylistPopup.html | 2 +- www/doc/client/nullHandler.html | 2 +- www/doc/client/panelObj.html | 2 +- www/doc/client/panels_emotePanel.js.html | 2 +- .../panels_queuePanel_playlistManager.js.html | 2 +- .../panels_queuePanel_queuePanel.js.html | 2 +- www/doc/client/panels_settingsPanel.js.html | 129 +- www/doc/client/player.html | 118 +- www/doc/client/player.js.html | 41 +- www/doc/client/playlistManager.html | 2 +- www/doc/client/poppedPanel.html | 2 +- www/doc/client/queuePanel.html | 2 +- www/doc/client/rawFileBase.html | 2 +- www/doc/client/rawFileHandler.html | 2 +- www/doc/client/renamePopup.html | 2 +- www/doc/client/reschedulePopup.html | 2 +- www/doc/client/schedulePopup.html | 2 +- www/doc/client/settingsPanel.html | 869 +++++++- www/doc/client/userList.html | 2 +- www/doc/client/userlist.js.html | 2 +- www/doc/client/youtubeEmbedHandler.html | 2 +- www/doc/server/activeChannel.html | 4 +- .../server/app_channel_activeChannel.js.html | 6 +- .../server/app_channel_channelManager.js.html | 4 +- www/doc/server/app_channel_chat.js.html | 4 +- www/doc/server/app_channel_chatBuffer.js.html | 4 +- .../server/app_channel_chatHandler.js.html | 4 +- .../app_channel_commandPreprocessor.js.html | 4 +- .../server/app_channel_connectedUser.js.html | 4 +- .../server/app_channel_media_media.js.html | 4 +- .../app_channel_media_playlistHandler.js.html | 4 +- .../server/app_channel_media_queue.js.html | 4 +- .../app_channel_media_queuedMedia.js.html | 4 +- www/doc/server/app_channel_tokebot.js.html | 4 +- www/doc/server/channelManager.html | 4 +- www/doc/server/chat.html | 4 +- www/doc/server/chatBuffer.html | 4 +- www/doc/server/chatHandler.html | 4 +- www/doc/server/commandPreprocessor.html | 4 +- www/doc/server/commandProcessor.html | 4 +- www/doc/server/connectedUser.html | 4 +- www/doc/server/global.html | 1785 ++++++++++++++++- www/doc/server/index.html | 4 +- www/doc/server/media.html | 4 +- www/doc/server/playlistHandler.html | 4 +- www/doc/server/queue.html | 4 +- www/doc/server/queuedMedia.html | 4 +- .../schemas_channel_channelBanSchema.js.html | 4 +- ...as_channel_channelPermissionSchema.js.html | 4 +- .../schemas_channel_channelSchema.js.html | 4 +- .../server/schemas_channel_chatSchema.js.html | 4 +- .../schemas_channel_media_mediaSchema.js.html | 4 +- ..._channel_media_playlistMediaSchema.js.html | 4 +- ...hemas_channel_media_playlistSchema.js.html | 4 +- ...as_channel_media_queuedMediaSchema.js.html | 4 +- www/doc/server/schemas_emoteSchema.js.html | 4 +- www/doc/server/schemas_flairSchema.js.html | 4 +- .../server/schemas_permissionSchema.js.html | 4 +- www/doc/server/schemas_statSchema.js.html | 4 +- .../schemas_tokebot_tokeCommandSchema.js.html | 4 +- .../schemas_user_emailChangeSchema.js.html | 4 +- .../schemas_user_passwordResetSchema.js.html | 4 +- .../server/schemas_user_userBanSchema.js.html | 4 +- .../server/schemas_user_userSchema.js.html | 4 +- www/doc/server/tokebot.html | 4 +- www/doc/server/utils_altchaUtils.js.html | 4 +- www/doc/server/utils_configCheck.js.html | 4 +- www/doc/server/utils_hashUtils.js.html | 4 +- www/doc/server/utils_linkUtils.js.html | 4 +- www/doc/server/utils_loggerUtils.js.html | 24 +- www/doc/server/utils_mailUtils.js.html | 4 +- .../utils_media_internetArchiveUtils.js.html | 4 +- www/doc/server/utils_media_yanker.js.html | 4 +- www/doc/server/utils_media_ytdlpUtils.js.html | 4 +- www/doc/server/utils_regexUtils.js.html | 4 +- www/doc/server/utils_scheduler.js.html | 4 +- www/doc/server/utils_sessionUtils.js.html | 4 +- www/js/channel/channel.js | 46 +- www/js/channel/chat.js | 11 +- www/js/channel/mediaHandler.js | 2 +- www/js/channel/panels/settingsPanel.js | 110 +- www/js/channel/player.js | 6 +- 105 files changed, 3447 insertions(+), 252 deletions(-) diff --git a/src/views/partial/panels/settings.ejs b/src/views/partial/panels/settings.ejs index ad14ec5..ac3cb93 100644 --- a/src/views/partial/panels/settings.ejs +++ b/src/views/partial/panels/settings.ejs @@ -18,15 +18,32 @@ along with this program. If not, see . %>

Client Settings

Player Settings

-

Youtube Player Type:

+

Youtube Player Type:

-

Internet Archive CDN Server:

+

Internet Archive CDN Server:

+ +

Syncronization Tolerance:

+ +
+ +

Livestream Sync Tolerance:

+ +
+ +

Syncronization Delta:

+ +
+

Chat Settings

+ +

Aspect-Ratio Lock Chat Width Minimum:

+ +
\ No newline at end of file diff --git a/www/css/panel/settings.css b/www/css/panel/settings.css index 869e0d5..56b0790 100644 --- a/www/css/panel/settings.css +++ b/www/css/panel/settings.css @@ -36,10 +36,15 @@ along with this program. If not, see .*/ text-wrap: nowrap; height: 1em; align-items: center; + justify-content: space-between; max-width: 30em; width: 100% } +.settings-panel-setting input[type=number]{ + flex: 0 1 4em; +} + .settings-panel-setting :is(input, select){ width: 1px; flex: 1 1 auto; diff --git a/www/doc/client/addURLPopup.html b/www/doc/client/addURLPopup.html index 9afb3a0..e59bd9d 100644 --- a/www/doc/client/addURLPopup.html +++ b/www/doc/client/addURLPopup.html @@ -875,7 +875,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/cPanel.html b/www/doc/client/cPanel.html index 6e28375..66be9d8 100644 --- a/www/doc/client/cPanel.html +++ b/www/doc/client/cPanel.html @@ -2602,7 +2602,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/channel.html b/www/doc/client/channel.html index 030583f..12d5a85 100644 --- a/www/doc/client/channel.html +++ b/www/doc/client/channel.html @@ -377,7 +377,7 @@
Source:
@@ -1254,7 +1254,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/channel.js.html b/www/doc/client/channel.js.html index c713518..99fe36d 100644 --- a/www/doc/client/channel.js.html +++ b/www/doc/client/channel.js.html @@ -194,6 +194,9 @@ class channel{ * @param {*} value - Value to set setting to */ processConfig(key, value){ + //Unfortunately we can't scope constants to switch-cases so this is the best we got if we wanna re-use the name + let nowPlaying; + //Switch/case by config key switch(key){ case 'ytPlayerType': @@ -233,18 +236,75 @@ class channel{ return; } - //Get current video - const nowPlaying = this.player.mediaHandler.nowPlaying; + nowPlaying = this.player.mediaHandler.nowPlaying; //If we're playing a youtube video if(nowPlaying != null && nowPlaying.type == 'yt'){ //Restart the video - this.player.start({media: nowPlaying}); + this.player.hardReload(); } //Stop while we're ahead return; + + case 'IACDN': + //If the player or mediaHandler isn't loaded + if(this.player == null || this.player.mediaHandler == null){ + //We're fuggin done here + return; + } + + //Get current video + nowPlaying = this.player.mediaHandler.nowPlaying; + + //If we're playing a video from Internet Archive + if(nowPlaying != null && nowPlaying.type == 'ia'){ + //Hard reload the media, forcing media handler re-creation + this.player.hardReload(); + } + + return; + case 'syncTolerance': + //If the player isn't loaded + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Set syncronization tolerance + this.player.syncTolerance = value; + return; + case 'liveSyncTolerance': + //If the player isn't loaded + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Set syncronization tolerance + this.player.streamSyncTolerance = value; + return; + case 'syncDelta': + //If the player isn't loaded + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Set syncronization delta + this.player.syncDelta = value; + return; + case 'chatWidthMin': + //If the chat isn't loaded + if(this.chatBox == null){ + //We're fuckin' done here + return; + } + + //Set Chat Box Width minimum while Locked to Aspect-Ratio + this.chatBox.chatWidthMinimum = value / 100; + return; } } @@ -252,7 +312,12 @@ class channel{ * Default channel config */ static defaultConfig = new Map([ - ["ytPlayerType","raw"] + ["ytPlayerType","raw"], + ["IACDN",""], + ["syncTolerance",0.4], + ["liveSyncTolerance", 2], + ["syncDelta", 6], + ["chatWidthMin", 20] ]); } @@ -289,7 +354,7 @@ const client = new channel();
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chat.js.html b/www/doc/client/chat.js.html index f5991b6..be80679 100644 --- a/www/doc/client/chat.js.html +++ b/www/doc/client/chat.js.html @@ -66,6 +66,11 @@ class chatBox{ */ this.autoScroll = true; + /** + * Chat-Width Minimum while sized to media Aspect-Ratio + */ + this.chatWidthMinimum = localStorage.getItem('chatWidthMin') / 100; + /** * Chat Buffer Scroll Top on last scroll */ @@ -528,10 +533,10 @@ L /** var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height; const targetChatWidth = window.innerWidth - targetVidWidth; //This should be changeable in settings later on, for now it defaults to 20% - const limit = window.innerWidth * .2; + const limit = window.innerWidth * this.chatWidthMinimum; - //Set width to target or 20vh depending on whether or not we've hit the width limit - this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : '20vh'; + //Set width to target or 20vw depending on whether or not we've hit the width limit + this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : `${this.chatWidthMinimum * 100}vw`; //Fix busted layout var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width; @@ -662,7 +667,7 @@ L /**
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chatBox.html b/www/doc/client/chatBox.html index 81a0bc4..caddc0c 100644 --- a/www/doc/client/chatBox.html +++ b/www/doc/client/chatBox.html @@ -240,7 +240,7 @@
Source:
@@ -365,7 +365,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -489,7 +489,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -551,7 +551,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -613,7 +613,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -675,7 +675,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -737,7 +737,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -799,7 +799,69 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
+ + + + + + + + + + + + + + + + +

chatWidthMinimum

+ + + + +
+ Chat-Width Minimum while sized to media Aspect-Ratio +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -861,7 +923,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -985,7 +1047,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1047,7 +1109,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1109,7 +1171,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1171,7 +1233,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1233,7 +1295,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1295,7 +1357,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1357,7 +1419,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1419,7 +1481,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1481,7 +1543,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1543,7 +1605,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1605,7 +1667,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1734,7 +1796,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -1871,7 +1933,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2030,7 +2092,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2118,7 +2180,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2255,7 +2317,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2392,7 +2454,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2480,7 +2542,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2617,7 +2679,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2754,7 +2816,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -2891,7 +2953,7 @@ Seems weird to stick this in here, but the split is dictated by chat width :P
Source:
@@ -3029,7 +3091,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3166,7 +3228,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3303,7 +3365,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3440,7 +3502,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3577,7 +3639,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3665,7 +3727,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3753,7 +3815,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -3890,7 +3952,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -4027,7 +4089,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -4164,7 +4226,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -4301,7 +4363,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -4461,7 +4523,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -4598,7 +4660,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
Source:
@@ -4650,7 +4712,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chatPostprocessor.html b/www/doc/client/chatPostprocessor.html index f2f06ee..bb1dc90 100644 --- a/www/doc/client/chatPostprocessor.html +++ b/www/doc/client/chatPostprocessor.html @@ -1794,7 +1794,7 @@ Internal command used by several text filters to prevent code re-writes
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chatPostprocessor.js.html b/www/doc/client/chatPostprocessor.js.html index bbaf312..fe217a0 100644 --- a/www/doc/client/chatPostprocessor.js.html +++ b/www/doc/client/chatPostprocessor.js.html @@ -671,7 +671,7 @@ class chatPostprocessor{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/clearPopup.html b/www/doc/client/clearPopup.html index cf6e243..0717d03 100644 --- a/www/doc/client/clearPopup.html +++ b/www/doc/client/clearPopup.html @@ -790,7 +790,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandPreprocessor.html b/www/doc/client/commandPreprocessor.html index 6e246c5..fa0d109 100644 --- a/www/doc/client/commandPreprocessor.html +++ b/www/doc/client/commandPreprocessor.html @@ -1912,7 +1912,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandPreprocessor.js.html b/www/doc/client/commandPreprocessor.js.html index 7b12efd..e9f4fa2 100644 --- a/www/doc/client/commandPreprocessor.js.html +++ b/www/doc/client/commandPreprocessor.js.html @@ -371,7 +371,7 @@ class commandProcessor{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandProcessor.html b/www/doc/client/commandProcessor.html index 3c0c1cb..a5325c6 100644 --- a/www/doc/client/commandProcessor.html +++ b/www/doc/client/commandProcessor.html @@ -421,7 +421,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/cpanel.js.html b/www/doc/client/cpanel.js.html index f457257..b992b2c 100644 --- a/www/doc/client/cpanel.js.html +++ b/www/doc/client/cpanel.js.html @@ -515,7 +515,7 @@ class poppedPanel{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/defaultTitlesPopup.html b/www/doc/client/defaultTitlesPopup.html index 32b3bc8..6baa3fe 100644 --- a/www/doc/client/defaultTitlesPopup.html +++ b/www/doc/client/defaultTitlesPopup.html @@ -960,7 +960,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/emotePanel.html b/www/doc/client/emotePanel.html index 2c10c4e..ad9a98e 100644 --- a/www/doc/client/emotePanel.html +++ b/www/doc/client/emotePanel.html @@ -2249,7 +2249,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/global.html b/www/doc/client/global.html index 8e96f76..65242c2 100644 --- a/www/doc/client/global.html +++ b/www/doc/client/global.html @@ -156,7 +156,7 @@
Source:
@@ -208,7 +208,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/hlsBase.html b/www/doc/client/hlsBase.html index b7ca232..7647acb 100644 --- a/www/doc/client/hlsBase.html +++ b/www/doc/client/hlsBase.html @@ -2919,7 +2919,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/hlsLiveStreamHandler.html b/www/doc/client/hlsLiveStreamHandler.html index e224c82..2cfd09b 100644 --- a/www/doc/client/hlsLiveStreamHandler.html +++ b/www/doc/client/hlsLiveStreamHandler.html @@ -2896,7 +2896,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/index.html b/www/doc/client/index.html index e4fd518..5b9523a 100644 --- a/www/doc/client/index.html +++ b/www/doc/client/index.html @@ -87,7 +87,7 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/mediaHandler.html b/www/doc/client/mediaHandler.html index 19ff7f9..b3d8708 100644 --- a/www/doc/client/mediaHandler.html +++ b/www/doc/client/mediaHandler.html @@ -2701,7 +2701,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/mediaHandler.js.html b/www/doc/client/mediaHandler.js.html index 681285e..71350a6 100644 --- a/www/doc/client/mediaHandler.js.html +++ b/www/doc/client/mediaHandler.js.html @@ -787,7 +787,7 @@ class hlsLiveStreamHandler extends hlsBase{ super.onBuffer(event); - //If we're synced by the end of buffering + //If we're supposed to be synced by the end of buffering if(this.player.syncLock){ //Throw flag to manually sync since this works entirely differently from literally every other fucking media source this.reSync = true; @@ -835,7 +835,7 @@ class hlsLiveStreamHandler extends hlsBase{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/newPlaylistPopup.html b/www/doc/client/newPlaylistPopup.html index bb512fe..c33d0c9 100644 --- a/www/doc/client/newPlaylistPopup.html +++ b/www/doc/client/newPlaylistPopup.html @@ -705,7 +705,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/nullHandler.html b/www/doc/client/nullHandler.html index f3cc015..30b6fd5 100644 --- a/www/doc/client/nullHandler.html +++ b/www/doc/client/nullHandler.html @@ -2873,7 +2873,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panelObj.html b/www/doc/client/panelObj.html index 5def7f1..5b4e567 100644 --- a/www/doc/client/panelObj.html +++ b/www/doc/client/panelObj.html @@ -909,7 +909,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_emotePanel.js.html b/www/doc/client/panels_emotePanel.js.html index b37fd8d..23a022f 100644 --- a/www/doc/client/panels_emotePanel.js.html +++ b/www/doc/client/panels_emotePanel.js.html @@ -353,7 +353,7 @@ class emotePanel extends panelObj{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_queuePanel_playlistManager.js.html b/www/doc/client/panels_queuePanel_playlistManager.js.html index 532ca0a..10dcabb 100644 --- a/www/doc/client/panels_queuePanel_playlistManager.js.html +++ b/www/doc/client/panels_queuePanel_playlistManager.js.html @@ -983,7 +983,7 @@ class renamePopup{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_queuePanel_queuePanel.js.html b/www/doc/client/panels_queuePanel_queuePanel.js.html index d9bfe08..dff9be9 100644 --- a/www/doc/client/panels_queuePanel_queuePanel.js.html +++ b/www/doc/client/panels_queuePanel_queuePanel.js.html @@ -1679,7 +1679,7 @@ class clearPopup{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_settingsPanel.js.html b/www/doc/client/panels_settingsPanel.js.html index 74b8727..72232f3 100644 --- a/www/doc/client/panels_settingsPanel.js.html +++ b/www/doc/client/panels_settingsPanel.js.html @@ -60,8 +60,36 @@ class settingsPanel extends panelObj{ } docSwitch(){ + /** + * Youtube Source Selector + */ this.youtubeSource = this.panelDocument.querySelector("#settings-panel-youtube-source select"); + /** + * Internet Archive CDN Server Input + */ + this.iaCDN = this.panelDocument.querySelector("#settings-panel-ia-server input"); + + /** + * Syncronization Tolerance Input + */ + this.syncTolerance = this.panelDocument.querySelector("#settings-panel-sync-tolerance input"); + + /** + * Livestream Syncronization Tolerance Input + */ + this.liveSyncTolerance = this.panelDocument.querySelector("#settings-panel-live-sync-tolerance input"); + + /** + * Syncronization Tolerance Delta + */ + this.syncDelta = this.panelDocument.querySelector("#settings-panel-sync-delta input"); + + /** + * Chat Width Minimum while Size-Locked to Media Aspect Ratio + */ + this.chatWidthMinimum = this.panelDocument.querySelector("#settings-panel-min-chat-width input"); + this.renderSettings(); this.setupInput(); } @@ -71,6 +99,11 @@ class settingsPanel extends panelObj{ */ setupInput(){ this.youtubeSource.addEventListener('change', this.updateYoutubeSource.bind(this)); + this.iaCDN.addEventListener('keydown', this.updateIACDN.bind(this)); + this.syncTolerance.addEventListener('change', this.updateSyncTolerance.bind(this)); + this.liveSyncTolerance.addEventListener('change', this.updateLiveSyncTolerance.bind(this)); + this.syncDelta.addEventListener('change', this.updateSyncDelta.bind(this)); + this.chatWidthMinimum.addEventListener('change', this.updateChatWidthMinimum.bind(this)); } /** @@ -78,6 +111,11 @@ class settingsPanel extends panelObj{ */ renderSettings(){ this.youtubeSource.value = localStorage.getItem("ytPlayerType"); + this.iaCDN.value = localStorage.getItem("IACDN"); + this.syncTolerance.value = localStorage.getItem("syncTolerance"); + this.liveSyncTolerance.value = localStorage.getItem("liveSyncTolerance"); + this.syncDelta.value = localStorage.getItem("syncDelta"); + this.chatWidthMinimum.value = localStorage.getItem("chatWidthMin"); } /** @@ -87,6 +125,95 @@ class settingsPanel extends panelObj{ localStorage.setItem("ytPlayerType", this.youtubeSource.value); client.processConfig("ytPlayerType", this.youtubeSource.value); } + + /** + * Event handler for Internet Archive CDN Server input + * @param {Event} event - Event handed down by event listener + */ + updateIACDN(event){ + //If we hit enter + if(event.key == "Enter"){ + //If we have an invalid server string + if(!(this.iaCDN.value.match(/^ia[0-9]{6}\...$/g) || this.iaCDN.value == "")){ + //reset back to what was set before + this.iaCDN.value = localStorage.getItem('IACDN'); + + //BAIL! + return; + } + + localStorage.setItem("IACDN", this.iaCDN.value); + client.processConfig("IACDN", this.iaCDN.value); + } + } + + /** + * Handles Sync Tolerance Changes + */ + updateSyncTolerance(){ + //If sync tolerance was set to a negative number + if(this.syncTolerance.value < 0){ + //Reset setting back to stored value + this.syncTolerance.value = localStorage.getItem('syncTolerance'); + + //BAIL! + return; + } + + localStorage.setItem("syncTolerance", this.syncTolerance.value); + client.processConfig("syncTolerance", this.syncTolerance.value); + } + + /** + * Handles Live Sync Tolerance Changes + */ + updateLiveSyncTolerance(){ + //If sync tolerance was set to a negative number + if(this.liveSyncTolerance.value < 0){ + //Reset setting back to stored value + this.liveSyncTolerance.value = localStorage.getItem('liveSyncTolerance'); + + //BAIL! + return; + } + + localStorage.setItem("liveSyncTolerance", this.liveSyncTolerance.value); + client.processConfig("liveSyncTolerance", this.liveSyncTolerance.value); + } + + /** + * Handles Sync Delta Changes + */ + updateSyncDelta(){ + //If sync tolerance was set to a negative number + if(this.syncDelta.value < 0){ + //Reset setting back to stored value + this.syncDelta.value = localStorage.getItem('syncDelta'); + + //BAIL! + return; + } + + localStorage.setItem("syncDelta", this.syncDelta.value); + client.processConfig("syncDelta", this.syncDelta.value); + } + + /** + * Handles Chat Width minimum Changes + */ + updateChatWidthMinimum(){ + //If sync tolerance was set to a negative number + if(this.chatWidthMinimum.value < 0 || this.chatWidthMinimum > 100){ + //Reset setting back to stored value + this.syncDelta.value = localStorage.getItem('chatWidthMin'); + + //BAIL! + return; + } + + localStorage.setItem("chatWidthMin", this.chatWidthMinimum.value); + client.processConfig("chatWidthMin", this.chatWidthMinimum.value); + } } @@ -103,7 +230,7 @@ class settingsPanel extends panelObj{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/player.html b/www/doc/client/player.html index efdac7f..6804765 100644 --- a/www/doc/client/player.html +++ b/www/doc/client/player.html @@ -1588,7 +1588,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -1676,7 +1676,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -1764,7 +1764,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -1852,7 +1852,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -1904,6 +1904,94 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m + + + + + + +

hardReload()

+ + + + + + +
+ Destroys and Re-Creates media handler +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -1962,7 +2050,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2099,7 +2187,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2187,7 +2275,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2324,7 +2412,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2686,7 +2774,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2823,7 +2911,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2960,7 +3048,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -3097,7 +3185,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -3185,7 +3273,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -3322,7 +3410,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -3374,7 +3462,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/player.js.html b/www/doc/client/player.js.html index 3de47cd..adbc5a9 100644 --- a/www/doc/client/player.js.html +++ b/www/doc/client/player.js.html @@ -135,19 +135,19 @@ class player{ /** * Tolerance between timestamp from server and actual media before corrective seek for pre-recorded media */ - this.syncTolerance = 0.4; + this.syncTolerance = localStorage.getItem('syncTolerance'); /** * Tolerance in livestream delay before corrective seek to live. * * Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future... */ - this.streamSyncTolerance = 2; + this.streamSyncTolerance = localStorage.getItem('liveSyncTolerance'); /** * Forced time to wait between sync checks, heavily decreases chance of seek-banging without reducing syncornization accuracy */ - this.syncDelta = 6; + this.syncDelta = localStorage.getItem('syncDelta'); /** * Current Player Volume @@ -220,6 +220,18 @@ class player{ this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media); //Otherwise, if we have a raw-file compatible source }else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){ + //If we're running a source from IA + if(data.media.type == 'ia'){ + //Replace specified CDN with generic URL, in-case of hard reload + data.media.rawLink = data.media.rawLink.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download") + + //If we have an IA source and a custom IA CDN Server set + if(data.media.type == 'ia' && localStorage.getItem("IACDN") != ""){ + //Generate and set new link + data.media.rawLink = data.media.rawLink.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`); + } + } + //Create a new raw file handler for it this.mediaHandler = new rawFileHandler(client, this, data.media); //Sync to time stamp @@ -272,6 +284,27 @@ class player{ } } + /** + * Destroys and Re-Creates media handler + */ + hardReload(){ + if(this.mediaHandler != null){ + //Re-create data we'd get from server + const data = { + media: this.mediaHandler.nowPlaying, + timestamp: this.mediaHandler.getTimestamp() + } + + //End current media handler + this.end(); + + console.log(data); + + //Restart from last media handlers + this.start(data); + } + } + /** * Handles End-Media Commands from the Server */ @@ -474,7 +507,7 @@ class player{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/playlistManager.html b/www/doc/client/playlistManager.html index 0bc1b4d..1d0ccce 100644 --- a/www/doc/client/playlistManager.html +++ b/www/doc/client/playlistManager.html @@ -3631,7 +3631,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/poppedPanel.html b/www/doc/client/poppedPanel.html index 4305c59..1803f2b 100644 --- a/www/doc/client/poppedPanel.html +++ b/www/doc/client/poppedPanel.html @@ -1442,7 +1442,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/queuePanel.html b/www/doc/client/queuePanel.html index 2347fd2..3c17906 100644 --- a/www/doc/client/queuePanel.html +++ b/www/doc/client/queuePanel.html @@ -5313,7 +5313,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/rawFileBase.html b/www/doc/client/rawFileBase.html index e0feed8..f7b27fb 100644 --- a/www/doc/client/rawFileBase.html +++ b/www/doc/client/rawFileBase.html @@ -2914,7 +2914,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/rawFileHandler.html b/www/doc/client/rawFileHandler.html index cca00c9..75a7113 100644 --- a/www/doc/client/rawFileHandler.html +++ b/www/doc/client/rawFileHandler.html @@ -2896,7 +2896,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/renamePopup.html b/www/doc/client/renamePopup.html index 83e29b5..b94cc7c 100644 --- a/www/doc/client/renamePopup.html +++ b/www/doc/client/renamePopup.html @@ -875,7 +875,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/reschedulePopup.html b/www/doc/client/reschedulePopup.html index cca142d..0aae8da 100644 --- a/www/doc/client/reschedulePopup.html +++ b/www/doc/client/reschedulePopup.html @@ -1050,7 +1050,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/schedulePopup.html b/www/doc/client/schedulePopup.html index 8255821..1706ecc 100644 --- a/www/doc/client/schedulePopup.html +++ b/www/doc/client/schedulePopup.html @@ -960,7 +960,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/settingsPanel.html b/www/doc/client/settingsPanel.html index 7890d02..7fcaff6 100644 --- a/www/doc/client/settingsPanel.html +++ b/www/doc/client/settingsPanel.html @@ -230,6 +230,68 @@ +

chatWidthMinimum

+ + + + +
+ Chat Width Minimum while Size-Locked to Media Aspect Ratio +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + +

client

@@ -297,6 +359,130 @@ +

iaCDN

+ + + + +
+ Internet Archive CDN Server Input +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

liveSyncTolerance

+ + + + +
+ Livestream Syncronization Tolerance Input +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + +

name

@@ -564,6 +750,192 @@ + +

syncDelta

+ + + + +
+ Syncronization Tolerance Delta +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

syncTolerance

+ + + + +
+ Syncronization Tolerance Input +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

youtubeSource

+ + + + +
+ Youtube Source Selector +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + @@ -928,7 +1300,7 @@
Source:
@@ -1016,7 +1388,496 @@
Source:
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateChatWidthMinimum()

+ + + + + + +
+ Handles Chat Width minimum Changes +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateIACDN(event)

+ + + + + + +
+ Event handler for Internet Archive CDN Server input +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +Event + + + + Event handed down by event listener
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateLiveSyncTolerance()

+ + + + + + +
+ Handles Live Sync Tolerance Changes +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateSyncDelta()

+ + + + + + +
+ Handles Sync Delta Changes +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

updateSyncTolerance()

+ + + + + + +
+ Handles Sync Tolerance Changes +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -1104,7 +1965,7 @@
Source:
@@ -1156,7 +2017,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/userList.html b/www/doc/client/userList.html index 1d1ecca..29f4bdd 100644 --- a/www/doc/client/userList.html +++ b/www/doc/client/userList.html @@ -1191,7 +1191,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/userlist.js.html b/www/doc/client/userlist.js.html index 0234aa8..6089037 100644 --- a/www/doc/client/userlist.js.html +++ b/www/doc/client/userlist.js.html @@ -252,7 +252,7 @@ class userList{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/youtubeEmbedHandler.html b/www/doc/client/youtubeEmbedHandler.html index dc4981e..44e2470 100644 --- a/www/doc/client/youtubeEmbedHandler.html +++ b/www/doc/client/youtubeEmbedHandler.html @@ -2891,7 +2891,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:36:01 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/activeChannel.html b/www/doc/server/activeChannel.html index dc170a0..8a1e364 100644 --- a/www/doc/server/activeChannel.html +++ b/www/doc/server/activeChannel.html @@ -1218,13 +1218,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_activeChannel.js.html b/www/doc/server/app_channel_activeChannel.js.html index 5054c52..85ae37d 100644 --- a/www/doc/server/app_channel_activeChannel.js.html +++ b/www/doc/server/app_channel_activeChannel.js.html @@ -88,7 +88,7 @@ class activeChannel{ /** * Child Playlist Handler Object */ - this.playlistHandler = new playlistHandler(server, chanDB, this); + this.playlistHandler = new playlistHandler(server, this); /** * Child Chat Buffer Object @@ -215,13 +215,13 @@ module.exports = activeChannel;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_channelManager.js.html b/www/doc/server/app_channel_channelManager.js.html index 21502ba..b4f54c0 100644 --- a/www/doc/server/app_channel_channelManager.js.html +++ b/www/doc/server/app_channel_channelManager.js.html @@ -346,13 +346,13 @@ module.exports = channelManager;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chat.js.html b/www/doc/server/app_channel_chat.js.html index 0b0e21b..29b3323 100644 --- a/www/doc/server/app_channel_chat.js.html +++ b/www/doc/server/app_channel_chat.js.html @@ -98,13 +98,13 @@ module.exports = chat;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatBuffer.js.html b/www/doc/server/app_channel_chatBuffer.js.html index df11724..58a9823 100644 --- a/www/doc/server/app_channel_chatBuffer.js.html +++ b/www/doc/server/app_channel_chatBuffer.js.html @@ -194,13 +194,13 @@ module.exports = chatBuffer;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatHandler.js.html b/www/doc/server/app_channel_chatHandler.js.html index 56a8b8a..a6890d3 100644 --- a/www/doc/server/app_channel_chatHandler.js.html +++ b/www/doc/server/app_channel_chatHandler.js.html @@ -378,13 +378,13 @@ module.exports = chatHandler;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_commandPreprocessor.js.html b/www/doc/server/app_channel_commandPreprocessor.js.html index 39c90f2..f89d27f 100644 --- a/www/doc/server/app_channel_commandPreprocessor.js.html +++ b/www/doc/server/app_channel_commandPreprocessor.js.html @@ -482,13 +482,13 @@ module.exports = commandPreprocessor;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_connectedUser.js.html b/www/doc/server/app_channel_connectedUser.js.html index f61df5d..9e065d8 100644 --- a/www/doc/server/app_channel_connectedUser.js.html +++ b/www/doc/server/app_channel_connectedUser.js.html @@ -360,13 +360,13 @@ module.exports = connectedUser;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_media.js.html b/www/doc/server/app_channel_media_media.js.html index 77f92ca..f85f6a9 100644 --- a/www/doc/server/app_channel_media_media.js.html +++ b/www/doc/server/app_channel_media_media.js.html @@ -104,13 +104,13 @@ module.exports = media;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_playlistHandler.js.html b/www/doc/server/app_channel_media_playlistHandler.js.html index 1e5d0c5..4e7ae08 100644 --- a/www/doc/server/app_channel_media_playlistHandler.js.html +++ b/www/doc/server/app_channel_media_playlistHandler.js.html @@ -1179,13 +1179,13 @@ module.exports = playlistHandler;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queue.js.html b/www/doc/server/app_channel_media_queue.js.html index b3c1e64..62b12db 100644 --- a/www/doc/server/app_channel_media_queue.js.html +++ b/www/doc/server/app_channel_media_queue.js.html @@ -1818,13 +1818,13 @@ module.exports = queue;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queuedMedia.js.html b/www/doc/server/app_channel_media_queuedMedia.js.html index 2574a84..ff2434c 100644 --- a/www/doc/server/app_channel_media_queuedMedia.js.html +++ b/www/doc/server/app_channel_media_queuedMedia.js.html @@ -174,13 +174,13 @@ module.exports = queuedMedia;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_tokebot.js.html b/www/doc/server/app_channel_tokebot.js.html index 34f5dcf..6816142 100644 --- a/www/doc/server/app_channel_tokebot.js.html +++ b/www/doc/server/app_channel_tokebot.js.html @@ -293,13 +293,13 @@ module.exports = tokebot;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/channelManager.html b/www/doc/server/channelManager.html index 76ffbba..101b81d 100644 --- a/www/doc/server/channelManager.html +++ b/www/doc/server/channelManager.html @@ -2175,13 +2175,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chat.html b/www/doc/server/chat.html index 81d5896..53a0ebe 100644 --- a/www/doc/server/chat.html +++ b/www/doc/server/chat.html @@ -699,13 +699,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatBuffer.html b/www/doc/server/chatBuffer.html index cec6f96..d52127b 100644 --- a/www/doc/server/chatBuffer.html +++ b/www/doc/server/chatBuffer.html @@ -1323,13 +1323,13 @@ Left here since it seems like good form anywho, since this would be a private, o
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatHandler.html b/www/doc/server/chatHandler.html index a5705fb..7d120e7 100644 --- a/www/doc/server/chatHandler.html +++ b/www/doc/server/chatHandler.html @@ -3870,13 +3870,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandPreprocessor.html b/www/doc/server/commandPreprocessor.html index c2eb0f5..75c0e76 100644 --- a/www/doc/server/commandPreprocessor.html +++ b/www/doc/server/commandPreprocessor.html @@ -1492,13 +1492,13 @@ These arrays are used to handle further command/chat processing
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandProcessor.html b/www/doc/server/commandProcessor.html index d44d232..cba8ebe 100644 --- a/www/doc/server/commandProcessor.html +++ b/www/doc/server/commandProcessor.html @@ -1825,13 +1825,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/connectedUser.html b/www/doc/server/connectedUser.html index 4482395..05fe784 100644 --- a/www/doc/server/connectedUser.html +++ b/www/doc/server/connectedUser.html @@ -2435,13 +2435,13 @@ Having to crawl through these sockets is that. Because the other ways seem more
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/global.html b/www/doc/server/global.html index e1a32c2..378d9bd 100644 --- a/www/doc/server/global.html +++ b/www/doc/server/global.html @@ -2218,6 +2218,737 @@ This is important, as reducing authentication endpoints reduces attack surface. + + + + + + +

consoleWarn(string)

+ + + + + + +
+ Prints warning text to server console +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
string + + +String + + + + String to print to console
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

dumpError(err, date)

+ + + + + + +
+ Dumps unexpected server crashes to dedicated log files +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
err + + +Error + + + + error to dump to file
date + + +Date + + + + Date of error, defaults to now
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

errorHandler(res, msg, type, status) → {Express.Response}

+ + + + + + +
+ Main error handling function +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
res + + +Express.Response + + + + Response being sent out to the client who caused the issue
msg + + +String + + + + Error message to send the client
type + + +String + + + + Error type to send back to the client
status + + +Number + + + + HTTP(s) Status Code to send back to the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ If we have a usable Express Response object, return it back after it's been cashed +
+ + + +
+
+ Type +
+
+ +Express.Response + + +
+
+ + + + + + + + + + + + + +

errorMiddleware(err, req, res, next)

+ + + + + + +
+ Basic error-handling middleware to ensure we're not dumping stack traces to the client, as that would be insecure +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
err + + +Error + + + + Error to handle
req + + +Express.Request + + + + Express Request
res + + +Express.Response + + + + Express Response
next + + +function + + + + Next function in the Express middleware chain (Not that it's getting called XP)
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -2386,6 +3117,350 @@ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects +

exceptionHandler(res, err)

+ + + + + + +
+ Handles exceptions which where directly the fault of user action >:( +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
res + + +Express.Response + + + + Express Response object to bitch at
err + + +Error + + + + Error created by the jerk in question
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

exceptionSmith(msg, type) → {Error}

+ + + + + + +
+ Creates and returns a custom exception, tagged as a 'custom' exception, using the 'custom' boolean property. +This is used to denote that this error was generated on purpose, with a human readable message, that can be securely sent to the client. +Unexpected exceptions should only be logged internally, however, as they may contain sensitive data. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +String + + + + Error message to send the client
type + + +String + + + + Error type to send back to the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The exception to smith +
+ + + +
+
+ Type +
+
+ +Error + + +
+
+ + + + + + + + + + + + +

fetchMetadata(fullID, title) → {Array}

@@ -4180,6 +5255,143 @@ Provides a basic level of privacy by only logging salted hashes of IP's + + + + + + +

localExceptionHandler(err)

+ + + + + + +
+ Handles local exceptions which where not directly created by user interaction +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
err + + +Error + + + + Exception to handle
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -5176,6 +6388,575 @@ Warns server admin against unsafe config options. + + + + + + +

socketCriticalExceptionHandler(socket, err) → {Boolean}

+ + + + + + +
+ Generates error messages and drops connection for critical errors caused by socket.io interaction +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket error originated from
err + + +Error + + + + Error created by the jerk in question
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - Passthrough from socket.disconnect +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

socketErrorHandler(socket, msg, type) → {Boolean}

+ + + + + + +
+ Basic error-handling for socket.io so we don't just silently swallow errors. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket error originated from
msg + + +String + + + + Error message to send the client
type + + +String + + + + Error type to send back to the client
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - Passthrough from socket.emit +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + + + + + + + +

socketExceptionHandler(socket, err) → {Boolean}

+ + + + + + +
+ Generates error messages for simple errors generated by socket.io interaction +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
socket + + +Socket + + + + Socket error originated from
err + + +Error + + + + Error created by the jerk in question
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - Passthrough from socket.emit +
+ + + +
+
+ Type +
+
+ +Boolean + + +
+
+ + + + + + + @@ -5750,13 +7531,13 @@ Warns server admin against unsafe config options.
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/index.html b/www/doc/server/index.html index 78830f0..f66d8ba 100644 --- a/www/doc/server/index.html +++ b/www/doc/server/index.html @@ -81,13 +81,13 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/media.html b/www/doc/server/media.html index 6ae18e4..8a084b1 100644 --- a/www/doc/server/media.html +++ b/www/doc/server/media.html @@ -784,13 +784,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/playlistHandler.html b/www/doc/server/playlistHandler.html index ffebf64..a8b2db8 100644 --- a/www/doc/server/playlistHandler.html +++ b/www/doc/server/playlistHandler.html @@ -5230,13 +5230,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queue.html b/www/doc/server/queue.html index ea579b9..c9d5ef5 100644 --- a/www/doc/server/queue.html +++ b/www/doc/server/queue.html @@ -6671,13 +6671,13 @@ Called auto-magically by the Synchronization Timer
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queuedMedia.html b/www/doc/server/queuedMedia.html index 1216275..80f4833 100644 --- a/www/doc/server/queuedMedia.html +++ b/www/doc/server/queuedMedia.html @@ -1713,13 +1713,13 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelBanSchema.js.html b/www/doc/server/schemas_channel_channelBanSchema.js.html index cf51fc8..0c153ee 100644 --- a/www/doc/server/schemas_channel_channelBanSchema.js.html +++ b/www/doc/server/schemas_channel_channelBanSchema.js.html @@ -95,13 +95,13 @@ module.exports = channelBanSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelPermissionSchema.js.html b/www/doc/server/schemas_channel_channelPermissionSchema.js.html index ec2d7b6..e38a92b 100644 --- a/www/doc/server/schemas_channel_channelPermissionSchema.js.html +++ b/www/doc/server/schemas_channel_channelPermissionSchema.js.html @@ -163,13 +163,13 @@ module.exports = channelPermissionSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelSchema.js.html b/www/doc/server/schemas_channel_channelSchema.js.html index 35d0a91..74bd89b 100644 --- a/www/doc/server/schemas_channel_channelSchema.js.html +++ b/www/doc/server/schemas_channel_channelSchema.js.html @@ -928,13 +928,13 @@ module.exports = mongoose.model("channel", channelSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_chatSchema.js.html b/www/doc/server/schemas_channel_chatSchema.js.html index e60700a..0c1b259 100644 --- a/www/doc/server/schemas_channel_chatSchema.js.html +++ b/www/doc/server/schemas_channel_chatSchema.js.html @@ -90,13 +90,13 @@ module.exports = chatSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_mediaSchema.js.html b/www/doc/server/schemas_channel_media_mediaSchema.js.html index aebddd9..3712212 100644 --- a/www/doc/server/schemas_channel_media_mediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_mediaSchema.js.html @@ -90,13 +90,13 @@ module.exports = mediaSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html index 960fdbe..54b985e 100644 --- a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html @@ -118,13 +118,13 @@ module.exports = mediaSchema.discriminator('saved', playlistMediaProperties);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistSchema.js.html b/www/doc/server/schemas_channel_media_playlistSchema.js.html index 0947ca1..8d54516 100644 --- a/www/doc/server/schemas_channel_media_playlistSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistSchema.js.html @@ -168,13 +168,13 @@ module.exports = playlistSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html index 52e8741..d81d4f5 100644 --- a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html @@ -107,13 +107,13 @@ module.exports = mediaSchema.discriminator('queued', queuedProperties);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_emoteSchema.js.html b/www/doc/server/schemas_emoteSchema.js.html index 08ae363..1d30a63 100644 --- a/www/doc/server/schemas_emoteSchema.js.html +++ b/www/doc/server/schemas_emoteSchema.js.html @@ -158,13 +158,13 @@ module.exports = mongoose.model("emote", emoteSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_flairSchema.js.html b/www/doc/server/schemas_flairSchema.js.html index 6add2c3..ea28ff4 100644 --- a/www/doc/server/schemas_flairSchema.js.html +++ b/www/doc/server/schemas_flairSchema.js.html @@ -112,13 +112,13 @@ module.exports = mongoose.model("flair", flairSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_permissionSchema.js.html b/www/doc/server/schemas_permissionSchema.js.html index d368671..39a48e7 100644 --- a/www/doc/server/schemas_permissionSchema.js.html +++ b/www/doc/server/schemas_permissionSchema.js.html @@ -350,13 +350,13 @@ module.exports = mongoose.model("permissions", permissionSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_statSchema.js.html b/www/doc/server/schemas_statSchema.js.html index 37ce9f9..eacaf80 100644 --- a/www/doc/server/schemas_statSchema.js.html +++ b/www/doc/server/schemas_statSchema.js.html @@ -234,13 +234,13 @@ module.exports = mongoose.model("statistics", statSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html index 6ff9741..8413927 100644 --- a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html +++ b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html @@ -154,13 +154,13 @@ module.exports = mongoose.model("tokeCommand", tokeCommandSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_emailChangeSchema.js.html b/www/doc/server/schemas_user_emailChangeSchema.js.html index f4b10e2..cbae2e1 100644 --- a/www/doc/server/schemas_user_emailChangeSchema.js.html +++ b/www/doc/server/schemas_user_emailChangeSchema.js.html @@ -216,13 +216,13 @@ module.exports = mongoose.model("emailChange", emailChangeSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_passwordResetSchema.js.html b/www/doc/server/schemas_user_passwordResetSchema.js.html index 482a004..67ffc94 100644 --- a/www/doc/server/schemas_user_passwordResetSchema.js.html +++ b/www/doc/server/schemas_user_passwordResetSchema.js.html @@ -198,13 +198,13 @@ module.exports = mongoose.model("passwordReset", passwordResetSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userBanSchema.js.html b/www/doc/server/schemas_user_userBanSchema.js.html index 7b8e6b5..aa94c9d 100644 --- a/www/doc/server/schemas_user_userBanSchema.js.html +++ b/www/doc/server/schemas_user_userBanSchema.js.html @@ -515,13 +515,13 @@ module.exports = mongoose.model("userBan", userBanSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userSchema.js.html b/www/doc/server/schemas_user_userSchema.js.html index 4faa7bf..d14856e 100644 --- a/www/doc/server/schemas_user_userSchema.js.html +++ b/www/doc/server/schemas_user_userSchema.js.html @@ -882,13 +882,13 @@ module.exports.userModel = mongoose.model("user", userSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/tokebot.html b/www/doc/server/tokebot.html index bc50283..243dc15 100644 --- a/www/doc/server/tokebot.html +++ b/www/doc/server/tokebot.html @@ -1397,13 +1397,13 @@ I would now, but I don't want to break shit in a comment-only commit.
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_altchaUtils.js.html b/www/doc/server/utils_altchaUtils.js.html index 50a509d..9eea7b0 100644 --- a/www/doc/server/utils_altchaUtils.js.html +++ b/www/doc/server/utils_altchaUtils.js.html @@ -112,13 +112,13 @@ module.exports.verify = async function(payload, uniqueSecret = ''){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_configCheck.js.html b/www/doc/server/utils_configCheck.js.html index 31a96d4..54d8ec1 100644 --- a/www/doc/server/utils_configCheck.js.html +++ b/www/doc/server/utils_configCheck.js.html @@ -102,13 +102,13 @@ module.exports.securityCheck = function(){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_hashUtils.js.html b/www/doc/server/utils_hashUtils.js.html index f683cb3..4928c8c 100644 --- a/www/doc/server/utils_hashUtils.js.html +++ b/www/doc/server/utils_hashUtils.js.html @@ -97,13 +97,13 @@ module.exports.hashIP = function(ip){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_linkUtils.js.html b/www/doc/server/utils_linkUtils.js.html index 021b1a9..a2a1f12 100644 --- a/www/doc/server/utils_linkUtils.js.html +++ b/www/doc/server/utils_linkUtils.js.html @@ -140,13 +140,13 @@ module.exports.markLink = async function(link){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_loggerUtils.js.html b/www/doc/server/utils_loggerUtils.js.html index a818d99..b4fd6fa 100644 --- a/www/doc/server/utils_loggerUtils.js.html +++ b/www/doc/server/utils_loggerUtils.js.html @@ -42,8 +42,8 @@ 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 <https://www.gnu.org/licenses/>.*/ -//Node Imports -const fs = require('node:fs/promises') +//Node +const fs = require('node:fs/promises'); //Config const config = require('../../config.json'); @@ -196,14 +196,20 @@ module.exports.errorMiddleware = function(err, req, res, next){ module.exports.errorHandler(res, err.message, reason, err.status); } +/** + * Dumps unexpected server crashes to dedicated log files + * @param {Error} err - error to dump to file + * @param {Date} date - Date of error, defaults to now + */ module.exports.dumpError = function(err, date = new Date()){ try{ - console.log(err); - console.log(JSON.stringify(err, null, 2)); - fs.writeFile(`log/crash/${date.getTime()}.log`, JSON.stringify(err, null, 2)); - }catch(err){ - module.exports.consoleWarn("Hey Dawg, I Heard you liked errors, so I got you an error while dumping your error to the error logs:"); + const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`; + + fs.writeFile(`log/crash/${date.getTime()}.log`, content); + }catch(doubleErr){ + module.exports.consoleWarn("Yo Dawg, I herd you like errors, so I put an error in your error dump, so you can dump while you dump:"); module.exports.consoleWarn(err); + module.exports.consoleWarn(doubleErr); } } @@ -215,13 +221,13 @@ module.exports.dumpError = function(err, date = new Date()){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:23:36 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_mailUtils.js.html b/www/doc/server/utils_mailUtils.js.html index f73cd30..554ffb8 100644 --- a/www/doc/server/utils_mailUtils.js.html +++ b/www/doc/server/utils_mailUtils.js.html @@ -134,13 +134,13 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_internetArchiveUtils.js.html b/www/doc/server/utils_media_internetArchiveUtils.js.html index 063a1e0..1478f36 100644 --- a/www/doc/server/utils_media_internetArchiveUtils.js.html +++ b/www/doc/server/utils_media_internetArchiveUtils.js.html @@ -148,13 +148,13 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_yanker.js.html b/www/doc/server/utils_media_yanker.js.html index 68bd14e..29324f9 100644 --- a/www/doc/server/utils_media_yanker.js.html +++ b/www/doc/server/utils_media_yanker.js.html @@ -187,13 +187,13 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_ytdlpUtils.js.html b/www/doc/server/utils_media_ytdlpUtils.js.html index e7a2e2b..31aa839 100644 --- a/www/doc/server/utils_media_ytdlpUtils.js.html +++ b/www/doc/server/utils_media_ytdlpUtils.js.html @@ -180,13 +180,13 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_regexUtils.js.html b/www/doc/server/utils_regexUtils.js.html index bd5e072..00b0571 100644 --- a/www/doc/server/utils_regexUtils.js.html +++ b/www/doc/server/utils_regexUtils.js.html @@ -63,13 +63,13 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_scheduler.js.html b/www/doc/server/utils_scheduler.js.html index 2a53346..3474b81 100644 --- a/www/doc/server/utils_scheduler.js.html +++ b/www/doc/server/utils_scheduler.js.html @@ -99,13 +99,13 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_sessionUtils.js.html b/www/doc/server/utils_sessionUtils.js.html index 40a5087..9ecd5b8 100644 --- a/www/doc/server/utils_sessionUtils.js.html +++ b/www/doc/server/utils_sessionUtils.js.html @@ -230,13 +230,13 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 01:35:59 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time)
diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 3e4ff1a..b8b5305 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -236,6 +236,46 @@ class channel{ this.player.hardReload(); } + return; + case 'syncTolerance': + //If the player isn't loaded + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Set syncronization tolerance + this.player.syncTolerance = value; + return; + case 'liveSyncTolerance': + //If the player isn't loaded + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Set syncronization tolerance + this.player.streamSyncTolerance = value; + return; + case 'syncDelta': + //If the player isn't loaded + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Set syncronization delta + this.player.syncDelta = value; + return; + case 'chatWidthMin': + //If the chat isn't loaded + if(this.chatBox == null){ + //We're fuckin' done here + return; + } + + //Set Chat Box Width minimum while Locked to Aspect-Ratio + this.chatBox.chatWidthMinimum = value / 100; return; } } @@ -245,7 +285,11 @@ class channel{ */ static defaultConfig = new Map([ ["ytPlayerType","raw"], - ["IACDN",""] + ["IACDN",""], + ["syncTolerance",0.4], + ["liveSyncTolerance", 2], + ["syncDelta", 6], + ["chatWidthMin", 20] ]); } diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index a9f475d..abca6f8 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -38,6 +38,11 @@ class chatBox{ */ this.autoScroll = true; + /** + * Chat-Width Minimum while sized to media Aspect-Ratio + */ + this.chatWidthMinimum = localStorage.getItem('chatWidthMin') / 100; + /** * Chat Buffer Scroll Top on last scroll */ @@ -500,10 +505,10 @@ L /** var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height; const targetChatWidth = window.innerWidth - targetVidWidth; //This should be changeable in settings later on, for now it defaults to 20% - const limit = window.innerWidth * .2; + const limit = window.innerWidth * this.chatWidthMinimum; - //Set width to target or 20vh depending on whether or not we've hit the width limit - this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : '20vh'; + //Set width to target or 20vw depending on whether or not we've hit the width limit + this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : `${this.chatWidthMinimum * 100}vw`; //Fix busted layout var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width; diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 0fde0d1..4c4f491 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -759,7 +759,7 @@ class hlsLiveStreamHandler extends hlsBase{ super.onBuffer(event); - //If we're synced by the end of buffering + //If we're supposed to be synced by the end of buffering if(this.player.syncLock){ //Throw flag to manually sync since this works entirely differently from literally every other fucking media source this.reSync = true; diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index d8ff081..7f6b707 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -40,7 +40,27 @@ class settingsPanel extends panelObj{ /** * Internet Archive CDN Server Input */ - this.IACDNInput = this.panelDocument.querySelector("#settings-panel-ia-server input"); + this.iaCDN = this.panelDocument.querySelector("#settings-panel-ia-server input"); + + /** + * Syncronization Tolerance Input + */ + this.syncTolerance = this.panelDocument.querySelector("#settings-panel-sync-tolerance input"); + + /** + * Livestream Syncronization Tolerance Input + */ + this.liveSyncTolerance = this.panelDocument.querySelector("#settings-panel-live-sync-tolerance input"); + + /** + * Syncronization Tolerance Delta + */ + this.syncDelta = this.panelDocument.querySelector("#settings-panel-sync-delta input"); + + /** + * Chat Width Minimum while Size-Locked to Media Aspect Ratio + */ + this.chatWidthMinimum = this.panelDocument.querySelector("#settings-panel-min-chat-width input"); this.renderSettings(); this.setupInput(); @@ -51,7 +71,11 @@ class settingsPanel extends panelObj{ */ setupInput(){ this.youtubeSource.addEventListener('change', this.updateYoutubeSource.bind(this)); - this.IACDNInput.addEventListener('keydown', this.updateIACDN.bind(this)); + this.iaCDN.addEventListener('keydown', this.updateIACDN.bind(this)); + this.syncTolerance.addEventListener('change', this.updateSyncTolerance.bind(this)); + this.liveSyncTolerance.addEventListener('change', this.updateLiveSyncTolerance.bind(this)); + this.syncDelta.addEventListener('change', this.updateSyncDelta.bind(this)); + this.chatWidthMinimum.addEventListener('change', this.updateChatWidthMinimum.bind(this)); } /** @@ -59,7 +83,11 @@ class settingsPanel extends panelObj{ */ renderSettings(){ this.youtubeSource.value = localStorage.getItem("ytPlayerType"); - this.IACDNInput.value = localStorage.getItem("IACDN"); + this.iaCDN.value = localStorage.getItem("IACDN"); + this.syncTolerance.value = localStorage.getItem("syncTolerance"); + this.liveSyncTolerance.value = localStorage.getItem("liveSyncTolerance"); + this.syncDelta.value = localStorage.getItem("syncDelta"); + this.chatWidthMinimum.value = localStorage.getItem("chatWidthMin"); } /** @@ -78,16 +106,84 @@ class settingsPanel extends panelObj{ //If we hit enter if(event.key == "Enter"){ //If we have an invalid server string - if(!(this.IACDNInput.value.match(/^ia[0-9]{6}\...$/g) || this.IACDNInput.value == "")){ + if(!(this.iaCDN.value.match(/^ia[0-9]{6}\...$/g) || this.iaCDN.value == "")){ //reset back to what was set before - this.IACDNInput.value = localStorage.getItem('IACDN'); + this.iaCDN.value = localStorage.getItem('IACDN'); //BAIL! return; } - localStorage.setItem("IACDN", this.IACDNInput.value); - client.processConfig("IACDN", this.IACDNInput.value); + localStorage.setItem("IACDN", this.iaCDN.value); + client.processConfig("IACDN", this.iaCDN.value); } } + + /** + * Handles Sync Tolerance Changes + */ + updateSyncTolerance(){ + //If sync tolerance was set to a negative number + if(this.syncTolerance.value < 0){ + //Reset setting back to stored value + this.syncTolerance.value = localStorage.getItem('syncTolerance'); + + //BAIL! + return; + } + + localStorage.setItem("syncTolerance", this.syncTolerance.value); + client.processConfig("syncTolerance", this.syncTolerance.value); + } + + /** + * Handles Live Sync Tolerance Changes + */ + updateLiveSyncTolerance(){ + //If sync tolerance was set to a negative number + if(this.liveSyncTolerance.value < 0){ + //Reset setting back to stored value + this.liveSyncTolerance.value = localStorage.getItem('liveSyncTolerance'); + + //BAIL! + return; + } + + localStorage.setItem("liveSyncTolerance", this.liveSyncTolerance.value); + client.processConfig("liveSyncTolerance", this.liveSyncTolerance.value); + } + + /** + * Handles Sync Delta Changes + */ + updateSyncDelta(){ + //If sync tolerance was set to a negative number + if(this.syncDelta.value < 0){ + //Reset setting back to stored value + this.syncDelta.value = localStorage.getItem('syncDelta'); + + //BAIL! + return; + } + + localStorage.setItem("syncDelta", this.syncDelta.value); + client.processConfig("syncDelta", this.syncDelta.value); + } + + /** + * Handles Chat Width minimum Changes + */ + updateChatWidthMinimum(){ + //If sync tolerance was set to a negative number + if(this.chatWidthMinimum.value < 0 || this.chatWidthMinimum > 100){ + //Reset setting back to stored value + this.syncDelta.value = localStorage.getItem('chatWidthMin'); + + //BAIL! + return; + } + + localStorage.setItem("chatWidthMin", this.chatWidthMinimum.value); + client.processConfig("chatWidthMin", this.chatWidthMinimum.value); + } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index ec218f0..fb117c6 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -107,19 +107,19 @@ class player{ /** * Tolerance between timestamp from server and actual media before corrective seek for pre-recorded media */ - this.syncTolerance = 0.4; + this.syncTolerance = localStorage.getItem('syncTolerance'); /** * Tolerance in livestream delay before corrective seek to live. * * Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future... */ - this.streamSyncTolerance = 2; + this.streamSyncTolerance = localStorage.getItem('liveSyncTolerance'); /** * Forced time to wait between sync checks, heavily decreases chance of seek-banging without reducing syncornization accuracy */ - this.syncDelta = 6; + this.syncDelta = localStorage.getItem('syncDelta'); /** * Current Player Volume From 770bdc47943052ef4a996ded52e9e34993f91f2a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 6 Sep 2025 10:42:12 -0400 Subject: [PATCH 090/209] L --- www/js/channel/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index abca6f8..cae6fac 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -476,7 +476,7 @@ class chatBox{ this.aspectLockIcon.style.display = "inline"; } -L /** + /** * Re-sizes chat back to aspect ratio on window re-size when chat box is aspect locked * Also prevents horizontal scroll-bars from chat/window resizing * @param {Event} event - Event passed down from Event Handler @@ -497,7 +497,7 @@ L /** this.handleAutoScroll(); } -L /** + /** * Re-sizes chat box relative to media aspect ratio */ sizeToAspect(){ From 9650caeed0a3662047be1079dc7dd2dfb3d6bc6b Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 6 Sep 2025 19:08:10 -0400 Subject: [PATCH 091/209] Userlist collapse and cinema mode now state now persistant. --- www/doc/client/addURLPopup.html | 2 +- www/doc/client/cPanel.html | 2 +- www/doc/client/channel.html | 4 +-- www/doc/client/channel.js.html | 26 +++++++++++++++++-- www/doc/client/chat.js.html | 6 ++--- www/doc/client/chatBox.html | 2 +- www/doc/client/chatPostprocessor.html | 2 +- www/doc/client/chatPostprocessor.js.html | 2 +- www/doc/client/clearPopup.html | 2 +- www/doc/client/commandPreprocessor.html | 2 +- www/doc/client/commandPreprocessor.js.html | 2 +- www/doc/client/commandProcessor.html | 2 +- www/doc/client/cpanel.js.html | 2 +- www/doc/client/defaultTitlesPopup.html | 2 +- www/doc/client/emotePanel.html | 2 +- www/doc/client/global.html | 4 +-- www/doc/client/hlsBase.html | 2 +- www/doc/client/hlsLiveStreamHandler.html | 2 +- www/doc/client/index.html | 2 +- www/doc/client/mediaHandler.html | 2 +- www/doc/client/mediaHandler.js.html | 2 +- www/doc/client/newPlaylistPopup.html | 2 +- www/doc/client/nullHandler.html | 2 +- www/doc/client/panelObj.html | 2 +- www/doc/client/panels_emotePanel.js.html | 2 +- .../panels_queuePanel_playlistManager.js.html | 2 +- .../panels_queuePanel_queuePanel.js.html | 2 +- www/doc/client/panels_settingsPanel.js.html | 2 +- www/doc/client/player.html | 6 ++--- www/doc/client/player.js.html | 10 ++++--- www/doc/client/playlistManager.html | 2 +- www/doc/client/poppedPanel.html | 2 +- www/doc/client/queuePanel.html | 2 +- www/doc/client/rawFileBase.html | 2 +- www/doc/client/rawFileHandler.html | 2 +- www/doc/client/renamePopup.html | 2 +- www/doc/client/reschedulePopup.html | 2 +- www/doc/client/schedulePopup.html | 2 +- www/doc/client/settingsPanel.html | 2 +- www/doc/client/userList.html | 2 +- www/doc/client/userlist.js.html | 4 ++- www/doc/client/youtubeEmbedHandler.html | 2 +- www/doc/server/activeChannel.html | 2 +- .../server/app_channel_activeChannel.js.html | 2 +- .../server/app_channel_channelManager.js.html | 2 +- www/doc/server/app_channel_chat.js.html | 2 +- www/doc/server/app_channel_chatBuffer.js.html | 2 +- .../server/app_channel_chatHandler.js.html | 2 +- .../app_channel_commandPreprocessor.js.html | 2 +- .../server/app_channel_connectedUser.js.html | 2 +- .../server/app_channel_media_media.js.html | 2 +- .../app_channel_media_playlistHandler.js.html | 2 +- .../server/app_channel_media_queue.js.html | 2 +- .../app_channel_media_queuedMedia.js.html | 2 +- www/doc/server/app_channel_tokebot.js.html | 2 +- www/doc/server/channelManager.html | 2 +- www/doc/server/chat.html | 2 +- www/doc/server/chatBuffer.html | 2 +- www/doc/server/chatHandler.html | 2 +- www/doc/server/commandPreprocessor.html | 2 +- www/doc/server/commandProcessor.html | 2 +- www/doc/server/connectedUser.html | 2 +- www/doc/server/global.html | 2 +- www/doc/server/index.html | 2 +- www/doc/server/media.html | 2 +- www/doc/server/playlistHandler.html | 2 +- www/doc/server/queue.html | 2 +- www/doc/server/queuedMedia.html | 2 +- .../schemas_channel_channelBanSchema.js.html | 2 +- ...as_channel_channelPermissionSchema.js.html | 2 +- .../schemas_channel_channelSchema.js.html | 2 +- .../server/schemas_channel_chatSchema.js.html | 2 +- .../schemas_channel_media_mediaSchema.js.html | 2 +- ..._channel_media_playlistMediaSchema.js.html | 2 +- ...hemas_channel_media_playlistSchema.js.html | 2 +- ...as_channel_media_queuedMediaSchema.js.html | 2 +- www/doc/server/schemas_emoteSchema.js.html | 2 +- www/doc/server/schemas_flairSchema.js.html | 2 +- .../server/schemas_permissionSchema.js.html | 2 +- www/doc/server/schemas_statSchema.js.html | 2 +- .../schemas_tokebot_tokeCommandSchema.js.html | 2 +- .../schemas_user_emailChangeSchema.js.html | 2 +- .../schemas_user_passwordResetSchema.js.html | 2 +- .../server/schemas_user_userBanSchema.js.html | 2 +- .../server/schemas_user_userSchema.js.html | 2 +- www/doc/server/tokebot.html | 2 +- www/doc/server/utils_altchaUtils.js.html | 2 +- www/doc/server/utils_configCheck.js.html | 2 +- www/doc/server/utils_hashUtils.js.html | 2 +- www/doc/server/utils_linkUtils.js.html | 2 +- www/doc/server/utils_loggerUtils.js.html | 2 +- www/doc/server/utils_mailUtils.js.html | 2 +- .../utils_media_internetArchiveUtils.js.html | 2 +- www/doc/server/utils_media_yanker.js.html | 2 +- www/doc/server/utils_media_ytdlpUtils.js.html | 2 +- www/doc/server/utils_regexUtils.js.html | 2 +- www/doc/server/utils_scheduler.js.html | 2 +- www/doc/server/utils_sessionUtils.js.html | 2 +- www/js/channel/channel.js | 24 ++++++++++++++++- www/js/channel/player.js | 8 +++--- www/js/channel/userlist.js | 2 ++ 101 files changed, 164 insertions(+), 112 deletions(-) diff --git a/www/doc/client/addURLPopup.html b/www/doc/client/addURLPopup.html index e59bd9d..7c0a910 100644 --- a/www/doc/client/addURLPopup.html +++ b/www/doc/client/addURLPopup.html @@ -875,7 +875,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/cPanel.html b/www/doc/client/cPanel.html index 66be9d8..680689b 100644 --- a/www/doc/client/cPanel.html +++ b/www/doc/client/cPanel.html @@ -2602,7 +2602,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/channel.html b/www/doc/client/channel.html index 12d5a85..a4d4d6f 100644 --- a/www/doc/client/channel.html +++ b/www/doc/client/channel.html @@ -377,7 +377,7 @@
Source:
@@ -1254,7 +1254,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/channel.js.html b/www/doc/client/channel.js.html index 99fe36d..33e74ff 100644 --- a/www/doc/client/channel.js.html +++ b/www/doc/client/channel.js.html @@ -305,6 +305,26 @@ class channel{ //Set Chat Box Width minimum while Locked to Aspect-Ratio this.chatBox.chatWidthMinimum = value / 100; return; + case 'userlistHidden': + //If the userlist class isn't loaded in yet + if(this.userList == null){ + //We're fuckin' done here + return; + } + + //Pass value down to UI toggle, making sure to allow for string conversion + this.userList.toggleUI(!(value == true || value == "true")); + return; + case 'cinemaMode': + //If the userlist class isn't loaded in yet + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Pass value down to UI toggle, making sure to allow for string conversion + this.player.toggleCinemaMode(value == true || value == "true"); + return; } } @@ -317,7 +337,9 @@ class channel{ ["syncTolerance",0.4], ["liveSyncTolerance", 2], ["syncDelta", 6], - ["chatWidthMin", 20] + ["chatWidthMin", 20], + ["userlistHidden", false], + ["cinemaMode", false] ]); } @@ -354,7 +376,7 @@ const client = new channel();
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chat.js.html b/www/doc/client/chat.js.html index be80679..7304a74 100644 --- a/www/doc/client/chat.js.html +++ b/www/doc/client/chat.js.html @@ -504,7 +504,7 @@ class chatBox{ this.aspectLockIcon.style.display = "inline"; } -L /** + /** * Re-sizes chat back to aspect ratio on window re-size when chat box is aspect locked * Also prevents horizontal scroll-bars from chat/window resizing * @param {Event} event - Event passed down from Event Handler @@ -525,7 +525,7 @@ L /** this.handleAutoScroll(); } -L /** + /** * Re-sizes chat box relative to media aspect ratio */ sizeToAspect(){ @@ -667,7 +667,7 @@ L /**
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chatBox.html b/www/doc/client/chatBox.html index caddc0c..f19126b 100644 --- a/www/doc/client/chatBox.html +++ b/www/doc/client/chatBox.html @@ -4712,7 +4712,7 @@ Also prevents horizontal scroll-bars from chat/window resizing
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chatPostprocessor.html b/www/doc/client/chatPostprocessor.html index bb1dc90..4037169 100644 --- a/www/doc/client/chatPostprocessor.html +++ b/www/doc/client/chatPostprocessor.html @@ -1794,7 +1794,7 @@ Internal command used by several text filters to prevent code re-writes
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/chatPostprocessor.js.html b/www/doc/client/chatPostprocessor.js.html index fe217a0..717a4b8 100644 --- a/www/doc/client/chatPostprocessor.js.html +++ b/www/doc/client/chatPostprocessor.js.html @@ -671,7 +671,7 @@ class chatPostprocessor{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/clearPopup.html b/www/doc/client/clearPopup.html index 0717d03..5207cf4 100644 --- a/www/doc/client/clearPopup.html +++ b/www/doc/client/clearPopup.html @@ -790,7 +790,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandPreprocessor.html b/www/doc/client/commandPreprocessor.html index fa0d109..59c0c4f 100644 --- a/www/doc/client/commandPreprocessor.html +++ b/www/doc/client/commandPreprocessor.html @@ -1912,7 +1912,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandPreprocessor.js.html b/www/doc/client/commandPreprocessor.js.html index e9f4fa2..01b02f0 100644 --- a/www/doc/client/commandPreprocessor.js.html +++ b/www/doc/client/commandPreprocessor.js.html @@ -371,7 +371,7 @@ class commandProcessor{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/commandProcessor.html b/www/doc/client/commandProcessor.html index a5325c6..1d9876c 100644 --- a/www/doc/client/commandProcessor.html +++ b/www/doc/client/commandProcessor.html @@ -421,7 +421,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/cpanel.js.html b/www/doc/client/cpanel.js.html index b992b2c..10ea65f 100644 --- a/www/doc/client/cpanel.js.html +++ b/www/doc/client/cpanel.js.html @@ -515,7 +515,7 @@ class poppedPanel{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/defaultTitlesPopup.html b/www/doc/client/defaultTitlesPopup.html index 6baa3fe..39333cd 100644 --- a/www/doc/client/defaultTitlesPopup.html +++ b/www/doc/client/defaultTitlesPopup.html @@ -960,7 +960,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/emotePanel.html b/www/doc/client/emotePanel.html index ad9a98e..f8623e5 100644 --- a/www/doc/client/emotePanel.html +++ b/www/doc/client/emotePanel.html @@ -2249,7 +2249,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/global.html b/www/doc/client/global.html index 65242c2..0372855 100644 --- a/www/doc/client/global.html +++ b/www/doc/client/global.html @@ -156,7 +156,7 @@
Source:
@@ -208,7 +208,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/hlsBase.html b/www/doc/client/hlsBase.html index 7647acb..fb16373 100644 --- a/www/doc/client/hlsBase.html +++ b/www/doc/client/hlsBase.html @@ -2919,7 +2919,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/hlsLiveStreamHandler.html b/www/doc/client/hlsLiveStreamHandler.html index 2cfd09b..ceb9fd7 100644 --- a/www/doc/client/hlsLiveStreamHandler.html +++ b/www/doc/client/hlsLiveStreamHandler.html @@ -2896,7 +2896,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/index.html b/www/doc/client/index.html index 5b9523a..122a3a8 100644 --- a/www/doc/client/index.html +++ b/www/doc/client/index.html @@ -87,7 +87,7 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/mediaHandler.html b/www/doc/client/mediaHandler.html index b3d8708..9a575a6 100644 --- a/www/doc/client/mediaHandler.html +++ b/www/doc/client/mediaHandler.html @@ -2701,7 +2701,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/mediaHandler.js.html b/www/doc/client/mediaHandler.js.html index 71350a6..1aaac20 100644 --- a/www/doc/client/mediaHandler.js.html +++ b/www/doc/client/mediaHandler.js.html @@ -835,7 +835,7 @@ class hlsLiveStreamHandler extends hlsBase{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/newPlaylistPopup.html b/www/doc/client/newPlaylistPopup.html index c33d0c9..893fa8d 100644 --- a/www/doc/client/newPlaylistPopup.html +++ b/www/doc/client/newPlaylistPopup.html @@ -705,7 +705,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/nullHandler.html b/www/doc/client/nullHandler.html index 30b6fd5..c01b1a0 100644 --- a/www/doc/client/nullHandler.html +++ b/www/doc/client/nullHandler.html @@ -2873,7 +2873,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panelObj.html b/www/doc/client/panelObj.html index 5b4e567..b043fd5 100644 --- a/www/doc/client/panelObj.html +++ b/www/doc/client/panelObj.html @@ -909,7 +909,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_emotePanel.js.html b/www/doc/client/panels_emotePanel.js.html index 23a022f..00a3590 100644 --- a/www/doc/client/panels_emotePanel.js.html +++ b/www/doc/client/panels_emotePanel.js.html @@ -353,7 +353,7 @@ class emotePanel extends panelObj{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_queuePanel_playlistManager.js.html b/www/doc/client/panels_queuePanel_playlistManager.js.html index 10dcabb..a5202b1 100644 --- a/www/doc/client/panels_queuePanel_playlistManager.js.html +++ b/www/doc/client/panels_queuePanel_playlistManager.js.html @@ -983,7 +983,7 @@ class renamePopup{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_queuePanel_queuePanel.js.html b/www/doc/client/panels_queuePanel_queuePanel.js.html index dff9be9..35c1181 100644 --- a/www/doc/client/panels_queuePanel_queuePanel.js.html +++ b/www/doc/client/panels_queuePanel_queuePanel.js.html @@ -1679,7 +1679,7 @@ class clearPopup{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/panels_settingsPanel.js.html b/www/doc/client/panels_settingsPanel.js.html index 72232f3..30966cc 100644 --- a/www/doc/client/panels_settingsPanel.js.html +++ b/www/doc/client/panels_settingsPanel.js.html @@ -230,7 +230,7 @@ class settingsPanel extends panelObj{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/player.html b/www/doc/client/player.html index 6804765..07e70e7 100644 --- a/www/doc/client/player.html +++ b/www/doc/client/player.html @@ -1852,7 +1852,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -2412,7 +2412,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
Source:
@@ -3462,7 +3462,7 @@ Might seem weird to keep this here instead of the HLS handler, but remember we m
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/player.js.html b/www/doc/client/player.js.html index adbc5a9..e50e665 100644 --- a/www/doc/client/player.js.html +++ b/www/doc/client/player.js.html @@ -458,11 +458,13 @@ class player{ * Toggles Cinema Mode on or off * @param {Boolean} cinema - Whether or not to enter Cinema Mode. Defaults to toggle if left unspecified */ - toggleCinemaMode(cinema = !this.navBar.checkVisibility()){ + toggleCinemaMode(cinema = this.navBar.checkVisibility()){ + localStorage.setItem("cinemaMode", cinema); + if(cinema){ - this.navBar.style.display = "flex"; - }else{ this.navBar.style.display = "none"; + }else{ + this.navBar.style.display = "flex"; } //Resize the video if we're aspect locked @@ -507,7 +509,7 @@ class player{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/playlistManager.html b/www/doc/client/playlistManager.html index 1d0ccce..1b16f7a 100644 --- a/www/doc/client/playlistManager.html +++ b/www/doc/client/playlistManager.html @@ -3631,7 +3631,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/poppedPanel.html b/www/doc/client/poppedPanel.html index 1803f2b..4ec2e3b 100644 --- a/www/doc/client/poppedPanel.html +++ b/www/doc/client/poppedPanel.html @@ -1442,7 +1442,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/queuePanel.html b/www/doc/client/queuePanel.html index 3c17906..2e82372 100644 --- a/www/doc/client/queuePanel.html +++ b/www/doc/client/queuePanel.html @@ -5313,7 +5313,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/rawFileBase.html b/www/doc/client/rawFileBase.html index f7b27fb..fe9e240 100644 --- a/www/doc/client/rawFileBase.html +++ b/www/doc/client/rawFileBase.html @@ -2914,7 +2914,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/rawFileHandler.html b/www/doc/client/rawFileHandler.html index 75a7113..33c713e 100644 --- a/www/doc/client/rawFileHandler.html +++ b/www/doc/client/rawFileHandler.html @@ -2896,7 +2896,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/renamePopup.html b/www/doc/client/renamePopup.html index b94cc7c..9108d99 100644 --- a/www/doc/client/renamePopup.html +++ b/www/doc/client/renamePopup.html @@ -875,7 +875,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/reschedulePopup.html b/www/doc/client/reschedulePopup.html index 0aae8da..0a1a26e 100644 --- a/www/doc/client/reschedulePopup.html +++ b/www/doc/client/reschedulePopup.html @@ -1050,7 +1050,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/schedulePopup.html b/www/doc/client/schedulePopup.html index 1706ecc..91afd6a 100644 --- a/www/doc/client/schedulePopup.html +++ b/www/doc/client/schedulePopup.html @@ -960,7 +960,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/settingsPanel.html b/www/doc/client/settingsPanel.html index 7fcaff6..d7315a7 100644 --- a/www/doc/client/settingsPanel.html +++ b/www/doc/client/settingsPanel.html @@ -2017,7 +2017,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/userList.html b/www/doc/client/userList.html index 29f4bdd..4b4b6b2 100644 --- a/www/doc/client/userList.html +++ b/www/doc/client/userList.html @@ -1191,7 +1191,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/userlist.js.html b/www/doc/client/userlist.js.html index 6089037..f59a133 100644 --- a/www/doc/client/userlist.js.html +++ b/www/doc/client/userlist.js.html @@ -226,6 +226,8 @@ class userList{ } toggleUI(show = !this.userDiv.checkVisibility()){ + localStorage.setItem("userlistHidden", !show); + if(show){ this.userDiv.style.display = "flex"; this.toggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill"); @@ -252,7 +254,7 @@ class userList{
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:57 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/client/youtubeEmbedHandler.html b/www/doc/client/youtubeEmbedHandler.html index 44e2470..fb50e5e 100644 --- a/www/doc/client/youtubeEmbedHandler.html +++ b/www/doc/client/youtubeEmbedHandler.html @@ -2891,7 +2891,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:49 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:58 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/activeChannel.html b/www/doc/server/activeChannel.html index 8a1e364..4282bba 100644 --- a/www/doc/server/activeChannel.html +++ b/www/doc/server/activeChannel.html @@ -1224,7 +1224,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_activeChannel.js.html b/www/doc/server/app_channel_activeChannel.js.html index 85ae37d..7f8e700 100644 --- a/www/doc/server/app_channel_activeChannel.js.html +++ b/www/doc/server/app_channel_activeChannel.js.html @@ -221,7 +221,7 @@ module.exports = activeChannel;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_channelManager.js.html b/www/doc/server/app_channel_channelManager.js.html index b4f54c0..cc074cd 100644 --- a/www/doc/server/app_channel_channelManager.js.html +++ b/www/doc/server/app_channel_channelManager.js.html @@ -352,7 +352,7 @@ module.exports = channelManager;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chat.js.html b/www/doc/server/app_channel_chat.js.html index 29b3323..cb04f9f 100644 --- a/www/doc/server/app_channel_chat.js.html +++ b/www/doc/server/app_channel_chat.js.html @@ -104,7 +104,7 @@ module.exports = chat;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatBuffer.js.html b/www/doc/server/app_channel_chatBuffer.js.html index 58a9823..58c7959 100644 --- a/www/doc/server/app_channel_chatBuffer.js.html +++ b/www/doc/server/app_channel_chatBuffer.js.html @@ -200,7 +200,7 @@ module.exports = chatBuffer;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_chatHandler.js.html b/www/doc/server/app_channel_chatHandler.js.html index a6890d3..e4924c3 100644 --- a/www/doc/server/app_channel_chatHandler.js.html +++ b/www/doc/server/app_channel_chatHandler.js.html @@ -384,7 +384,7 @@ module.exports = chatHandler;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_commandPreprocessor.js.html b/www/doc/server/app_channel_commandPreprocessor.js.html index f89d27f..a4b1af5 100644 --- a/www/doc/server/app_channel_commandPreprocessor.js.html +++ b/www/doc/server/app_channel_commandPreprocessor.js.html @@ -488,7 +488,7 @@ module.exports = commandPreprocessor;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_connectedUser.js.html b/www/doc/server/app_channel_connectedUser.js.html index 9e065d8..9f2e19e 100644 --- a/www/doc/server/app_channel_connectedUser.js.html +++ b/www/doc/server/app_channel_connectedUser.js.html @@ -366,7 +366,7 @@ module.exports = connectedUser;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_media.js.html b/www/doc/server/app_channel_media_media.js.html index f85f6a9..65fc3d5 100644 --- a/www/doc/server/app_channel_media_media.js.html +++ b/www/doc/server/app_channel_media_media.js.html @@ -110,7 +110,7 @@ module.exports = media;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_playlistHandler.js.html b/www/doc/server/app_channel_media_playlistHandler.js.html index 4e7ae08..282bb1a 100644 --- a/www/doc/server/app_channel_media_playlistHandler.js.html +++ b/www/doc/server/app_channel_media_playlistHandler.js.html @@ -1185,7 +1185,7 @@ module.exports = playlistHandler;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queue.js.html b/www/doc/server/app_channel_media_queue.js.html index 62b12db..0b49033 100644 --- a/www/doc/server/app_channel_media_queue.js.html +++ b/www/doc/server/app_channel_media_queue.js.html @@ -1824,7 +1824,7 @@ module.exports = queue;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_media_queuedMedia.js.html b/www/doc/server/app_channel_media_queuedMedia.js.html index ff2434c..632fcc1 100644 --- a/www/doc/server/app_channel_media_queuedMedia.js.html +++ b/www/doc/server/app_channel_media_queuedMedia.js.html @@ -180,7 +180,7 @@ module.exports = queuedMedia;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/app_channel_tokebot.js.html b/www/doc/server/app_channel_tokebot.js.html index 6816142..3f08f2c 100644 --- a/www/doc/server/app_channel_tokebot.js.html +++ b/www/doc/server/app_channel_tokebot.js.html @@ -299,7 +299,7 @@ module.exports = tokebot;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/channelManager.html b/www/doc/server/channelManager.html index 101b81d..521158a 100644 --- a/www/doc/server/channelManager.html +++ b/www/doc/server/channelManager.html @@ -2181,7 +2181,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chat.html b/www/doc/server/chat.html index 53a0ebe..56d1a41 100644 --- a/www/doc/server/chat.html +++ b/www/doc/server/chat.html @@ -705,7 +705,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatBuffer.html b/www/doc/server/chatBuffer.html index d52127b..8e4e49f 100644 --- a/www/doc/server/chatBuffer.html +++ b/www/doc/server/chatBuffer.html @@ -1329,7 +1329,7 @@ Left here since it seems like good form anywho, since this would be a private, o
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/chatHandler.html b/www/doc/server/chatHandler.html index 7d120e7..d8b7e8f 100644 --- a/www/doc/server/chatHandler.html +++ b/www/doc/server/chatHandler.html @@ -3876,7 +3876,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandPreprocessor.html b/www/doc/server/commandPreprocessor.html index 75c0e76..2810dbf 100644 --- a/www/doc/server/commandPreprocessor.html +++ b/www/doc/server/commandPreprocessor.html @@ -1498,7 +1498,7 @@ These arrays are used to handle further command/chat processing
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/commandProcessor.html b/www/doc/server/commandProcessor.html index cba8ebe..ad7ab52 100644 --- a/www/doc/server/commandProcessor.html +++ b/www/doc/server/commandProcessor.html @@ -1831,7 +1831,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/connectedUser.html b/www/doc/server/connectedUser.html index 05fe784..215e6b0 100644 --- a/www/doc/server/connectedUser.html +++ b/www/doc/server/connectedUser.html @@ -2441,7 +2441,7 @@ Having to crawl through these sockets is that. Because the other ways seem more
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/global.html b/www/doc/server/global.html index 378d9bd..814d57e 100644 --- a/www/doc/server/global.html +++ b/www/doc/server/global.html @@ -7537,7 +7537,7 @@ Warns server admin against unsafe config options.
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/index.html b/www/doc/server/index.html index f66d8ba..91a9b2f 100644 --- a/www/doc/server/index.html +++ b/www/doc/server/index.html @@ -87,7 +87,7 @@ This new codebase intends to solve the following issues with the current CyTube
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/media.html b/www/doc/server/media.html index 8a084b1..8563f4d 100644 --- a/www/doc/server/media.html +++ b/www/doc/server/media.html @@ -790,7 +790,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/playlistHandler.html b/www/doc/server/playlistHandler.html index a8b2db8..35802bb 100644 --- a/www/doc/server/playlistHandler.html +++ b/www/doc/server/playlistHandler.html @@ -5236,7 +5236,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queue.html b/www/doc/server/queue.html index c9d5ef5..7413f61 100644 --- a/www/doc/server/queue.html +++ b/www/doc/server/queue.html @@ -6677,7 +6677,7 @@ Called auto-magically by the Synchronization Timer
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/queuedMedia.html b/www/doc/server/queuedMedia.html index 80f4833..43f597b 100644 --- a/www/doc/server/queuedMedia.html +++ b/www/doc/server/queuedMedia.html @@ -1719,7 +1719,7 @@
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelBanSchema.js.html b/www/doc/server/schemas_channel_channelBanSchema.js.html index 0c153ee..a6b5545 100644 --- a/www/doc/server/schemas_channel_channelBanSchema.js.html +++ b/www/doc/server/schemas_channel_channelBanSchema.js.html @@ -101,7 +101,7 @@ module.exports = channelBanSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelPermissionSchema.js.html b/www/doc/server/schemas_channel_channelPermissionSchema.js.html index e38a92b..b30ac6a 100644 --- a/www/doc/server/schemas_channel_channelPermissionSchema.js.html +++ b/www/doc/server/schemas_channel_channelPermissionSchema.js.html @@ -169,7 +169,7 @@ module.exports = channelPermissionSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_channelSchema.js.html b/www/doc/server/schemas_channel_channelSchema.js.html index 74bd89b..74c6706 100644 --- a/www/doc/server/schemas_channel_channelSchema.js.html +++ b/www/doc/server/schemas_channel_channelSchema.js.html @@ -934,7 +934,7 @@ module.exports = mongoose.model("channel", channelSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_chatSchema.js.html b/www/doc/server/schemas_channel_chatSchema.js.html index 0c1b259..17dc25c 100644 --- a/www/doc/server/schemas_channel_chatSchema.js.html +++ b/www/doc/server/schemas_channel_chatSchema.js.html @@ -96,7 +96,7 @@ module.exports = chatSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_mediaSchema.js.html b/www/doc/server/schemas_channel_media_mediaSchema.js.html index 3712212..e228fc2 100644 --- a/www/doc/server/schemas_channel_media_mediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_mediaSchema.js.html @@ -96,7 +96,7 @@ module.exports = mediaSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html index 54b985e..d42947a 100644 --- a/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistMediaSchema.js.html @@ -124,7 +124,7 @@ module.exports = mediaSchema.discriminator('saved', playlistMediaProperties);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_playlistSchema.js.html b/www/doc/server/schemas_channel_media_playlistSchema.js.html index 8d54516..5e087f0 100644 --- a/www/doc/server/schemas_channel_media_playlistSchema.js.html +++ b/www/doc/server/schemas_channel_media_playlistSchema.js.html @@ -174,7 +174,7 @@ module.exports = playlistSchema;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html index d81d4f5..dcdbd8f 100644 --- a/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html +++ b/www/doc/server/schemas_channel_media_queuedMediaSchema.js.html @@ -113,7 +113,7 @@ module.exports = mediaSchema.discriminator('queued', queuedProperties);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_emoteSchema.js.html b/www/doc/server/schemas_emoteSchema.js.html index 1d30a63..269b558 100644 --- a/www/doc/server/schemas_emoteSchema.js.html +++ b/www/doc/server/schemas_emoteSchema.js.html @@ -164,7 +164,7 @@ module.exports = mongoose.model("emote", emoteSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_flairSchema.js.html b/www/doc/server/schemas_flairSchema.js.html index ea28ff4..2dc800f 100644 --- a/www/doc/server/schemas_flairSchema.js.html +++ b/www/doc/server/schemas_flairSchema.js.html @@ -118,7 +118,7 @@ module.exports = mongoose.model("flair", flairSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_permissionSchema.js.html b/www/doc/server/schemas_permissionSchema.js.html index 39a48e7..50e0257 100644 --- a/www/doc/server/schemas_permissionSchema.js.html +++ b/www/doc/server/schemas_permissionSchema.js.html @@ -356,7 +356,7 @@ module.exports = mongoose.model("permissions", permissionSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_statSchema.js.html b/www/doc/server/schemas_statSchema.js.html index eacaf80..4ad3647 100644 --- a/www/doc/server/schemas_statSchema.js.html +++ b/www/doc/server/schemas_statSchema.js.html @@ -240,7 +240,7 @@ module.exports = mongoose.model("statistics", statSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html index 8413927..2c9a8e8 100644 --- a/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html +++ b/www/doc/server/schemas_tokebot_tokeCommandSchema.js.html @@ -160,7 +160,7 @@ module.exports = mongoose.model("tokeCommand", tokeCommandSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_emailChangeSchema.js.html b/www/doc/server/schemas_user_emailChangeSchema.js.html index cbae2e1..211da9d 100644 --- a/www/doc/server/schemas_user_emailChangeSchema.js.html +++ b/www/doc/server/schemas_user_emailChangeSchema.js.html @@ -222,7 +222,7 @@ module.exports = mongoose.model("emailChange", emailChangeSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_passwordResetSchema.js.html b/www/doc/server/schemas_user_passwordResetSchema.js.html index 67ffc94..4cb3383 100644 --- a/www/doc/server/schemas_user_passwordResetSchema.js.html +++ b/www/doc/server/schemas_user_passwordResetSchema.js.html @@ -204,7 +204,7 @@ module.exports = mongoose.model("passwordReset", passwordResetSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userBanSchema.js.html b/www/doc/server/schemas_user_userBanSchema.js.html index aa94c9d..9b68f85 100644 --- a/www/doc/server/schemas_user_userBanSchema.js.html +++ b/www/doc/server/schemas_user_userBanSchema.js.html @@ -521,7 +521,7 @@ module.exports = mongoose.model("userBan", userBanSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/schemas_user_userSchema.js.html b/www/doc/server/schemas_user_userSchema.js.html index d14856e..e54a80f 100644 --- a/www/doc/server/schemas_user_userSchema.js.html +++ b/www/doc/server/schemas_user_userSchema.js.html @@ -888,7 +888,7 @@ module.exports.userModel = mongoose.model("user", userSchema);
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/tokebot.html b/www/doc/server/tokebot.html index 243dc15..8ce590a 100644 --- a/www/doc/server/tokebot.html +++ b/www/doc/server/tokebot.html @@ -1403,7 +1403,7 @@ I would now, but I don't want to break shit in a comment-only commit.
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_altchaUtils.js.html b/www/doc/server/utils_altchaUtils.js.html index 9eea7b0..4d2664b 100644 --- a/www/doc/server/utils_altchaUtils.js.html +++ b/www/doc/server/utils_altchaUtils.js.html @@ -118,7 +118,7 @@ module.exports.verify = async function(payload, uniqueSecret = ''){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_configCheck.js.html b/www/doc/server/utils_configCheck.js.html index 54d8ec1..1b3adc5 100644 --- a/www/doc/server/utils_configCheck.js.html +++ b/www/doc/server/utils_configCheck.js.html @@ -108,7 +108,7 @@ module.exports.securityCheck = function(){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_hashUtils.js.html b/www/doc/server/utils_hashUtils.js.html index 4928c8c..4b1b7de 100644 --- a/www/doc/server/utils_hashUtils.js.html +++ b/www/doc/server/utils_hashUtils.js.html @@ -103,7 +103,7 @@ module.exports.hashIP = function(ip){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_linkUtils.js.html b/www/doc/server/utils_linkUtils.js.html index a2a1f12..7af678c 100644 --- a/www/doc/server/utils_linkUtils.js.html +++ b/www/doc/server/utils_linkUtils.js.html @@ -146,7 +146,7 @@ module.exports.markLink = async function(link){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_loggerUtils.js.html b/www/doc/server/utils_loggerUtils.js.html index b4fd6fa..94184ad 100644 --- a/www/doc/server/utils_loggerUtils.js.html +++ b/www/doc/server/utils_loggerUtils.js.html @@ -227,7 +227,7 @@ module.exports.dumpError = function(err, date = new Date()){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_mailUtils.js.html b/www/doc/server/utils_mailUtils.js.html index 554ffb8..382c15f 100644 --- a/www/doc/server/utils_mailUtils.js.html +++ b/www/doc/server/utils_mailUtils.js.html @@ -140,7 +140,7 @@ module.exports.sendAddressVerification = async function(requestDB, userDB, newEm
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_internetArchiveUtils.js.html b/www/doc/server/utils_media_internetArchiveUtils.js.html index 1478f36..64e1c04 100644 --- a/www/doc/server/utils_media_internetArchiveUtils.js.html +++ b/www/doc/server/utils_media_internetArchiveUtils.js.html @@ -154,7 +154,7 @@ module.exports.fetchMetadata = async function(fullID, title){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_yanker.js.html b/www/doc/server/utils_media_yanker.js.html index 29324f9..0f0f9e9 100644 --- a/www/doc/server/utils_media_yanker.js.html +++ b/www/doc/server/utils_media_yanker.js.html @@ -193,7 +193,7 @@ module.exports.getMediaType = async function(url){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_media_ytdlpUtils.js.html b/www/doc/server/utils_media_ytdlpUtils.js.html index 31aa839..c22eaa7 100644 --- a/www/doc/server/utils_media_ytdlpUtils.js.html +++ b/www/doc/server/utils_media_ytdlpUtils.js.html @@ -186,7 +186,7 @@ async function ytdlpFetch(link, format = 'b'){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_regexUtils.js.html b/www/doc/server/utils_regexUtils.js.html index 00b0571..82e6bea 100644 --- a/www/doc/server/utils_regexUtils.js.html +++ b/www/doc/server/utils_regexUtils.js.html @@ -69,7 +69,7 @@ module.exports.escapeRegex = function(string){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_scheduler.js.html b/www/doc/server/utils_scheduler.js.html index 3474b81..d01450e 100644 --- a/www/doc/server/utils_scheduler.js.html +++ b/www/doc/server/utils_scheduler.js.html @@ -105,7 +105,7 @@ module.exports.kickoff = function(){
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:48 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/doc/server/utils_sessionUtils.js.html b/www/doc/server/utils_sessionUtils.js.html index 9ecd5b8..af1c60f 100644 --- a/www/doc/server/utils_sessionUtils.js.html +++ b/www/doc/server/utils_sessionUtils.js.html @@ -236,7 +236,7 @@ module.exports.maxAttempts = maxAttempts;
- Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 10:33:47 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 4.0.4 on Sat Sep 06 2025 19:07:56 GMT-0400 (Eastern Daylight Time)
diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index b8b5305..ba4f433 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -277,6 +277,26 @@ class channel{ //Set Chat Box Width minimum while Locked to Aspect-Ratio this.chatBox.chatWidthMinimum = value / 100; return; + case 'userlistHidden': + //If the userlist class isn't loaded in yet + if(this.userList == null){ + //We're fuckin' done here + return; + } + + //Pass value down to UI toggle, making sure to allow for string conversion + this.userList.toggleUI(!(value == true || value == "true")); + return; + case 'cinemaMode': + //If the userlist class isn't loaded in yet + if(this.player == null){ + //We're fuckin' done here + return; + } + + //Pass value down to UI toggle, making sure to allow for string conversion + this.player.toggleCinemaMode(value == true || value == "true"); + return; } } @@ -289,7 +309,9 @@ class channel{ ["syncTolerance",0.4], ["liveSyncTolerance", 2], ["syncDelta", 6], - ["chatWidthMin", 20] + ["chatWidthMin", 20], + ["userlistHidden", false], + ["cinemaMode", false] ]); } diff --git a/www/js/channel/player.js b/www/js/channel/player.js index fb117c6..568886f 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -430,11 +430,13 @@ class player{ * Toggles Cinema Mode on or off * @param {Boolean} cinema - Whether or not to enter Cinema Mode. Defaults to toggle if left unspecified */ - toggleCinemaMode(cinema = !this.navBar.checkVisibility()){ + toggleCinemaMode(cinema = this.navBar.checkVisibility()){ + localStorage.setItem("cinemaMode", cinema); + if(cinema){ - this.navBar.style.display = "flex"; - }else{ this.navBar.style.display = "none"; + }else{ + this.navBar.style.display = "flex"; } //Resize the video if we're aspect locked diff --git a/www/js/channel/userlist.js b/www/js/channel/userlist.js index 67c2a8f..7db5a5e 100644 --- a/www/js/channel/userlist.js +++ b/www/js/channel/userlist.js @@ -198,6 +198,8 @@ class userList{ } toggleUI(show = !this.userDiv.checkVisibility()){ + localStorage.setItem("userlistHidden", !show); + if(show){ this.userDiv.style.display = "flex"; this.toggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill"); From cbd2136ca6dacc8adc31602c3de447201f771132 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 6 Sep 2025 21:26:14 -0400 Subject: [PATCH 092/209] Made emotes in emote pallette only enlargen on search to improve UX. --- www/js/channel/panels/emotePanel.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/www/js/channel/panels/emotePanel.js b/www/js/channel/panels/emotePanel.js index 464fd40..ded82f5 100644 --- a/www/js/channel/panels/emotePanel.js +++ b/www/js/channel/panels/emotePanel.js @@ -179,13 +179,15 @@ class emotePanel extends panelObj{ var search = this.searchPrompt.value; } + var searching = (search != null && search != ''); + //pull emote lists from the command preprocessor var siteEmotes = this.client.chatBox.commandPreprocessor.emotes.site; var chanEmotes = this.client.chatBox.commandPreprocessor.emotes.chan; var personalEmotes = this.client.chatBox.commandPreprocessor.emotes.personal; //If we have a search bar and a search in the search bar - if(search != null && search != ''){ + if(searching){ //filter emote lists using the filterQuery function siteEmotes = siteEmotes.filter(filterQuery); chanEmotes = chanEmotes.filter(filterQuery); @@ -198,9 +200,9 @@ class emotePanel extends panelObj{ } //render out the emote lists - this.renderEmotes(siteEmotes, this.siteEmoteList); - this.renderEmotes(chanEmotes, this.chanEmoteList); - this.renderEmotes(personalEmotes, this.personalEmoteList, true); + this.renderEmotes(siteEmotes, this.siteEmoteList, searching); + this.renderEmotes(chanEmotes, this.chanEmoteList, searching); + this.renderEmotes(personalEmotes, this.personalEmoteList, searching, true); } /** @@ -209,12 +211,12 @@ class emotePanel extends panelObj{ * @param {Node} container - Container to render emotes out to * @param {Boolean} personal - Denotes whether or not we're rendering personal emotes */ - renderEmotes(emoteList, container, personal = false){ + renderEmotes(emoteList, container, searchRender = false, personal = false){ //Clear out the container container.innerHTML = ''; //If we have two or less emotes - if(emoteList.length <= 2){ + if(emoteList.length <= 2 && searchRender){ //Set the container display to flex container.style.display = 'flex'; //otherwise @@ -260,7 +262,7 @@ class emotePanel extends panelObj{ emoteMedia.classList.add('emote-list-media'); //if we have a low emote count - if(emoteList.length <= 2){ + if(emoteList.length <= 2 && searchRender){ //render them huuuuuge emoteMedia.classList.add('emote-list-big-media'); } From 3ab6c6c7152e874287b43240d932f28215e1d336 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 7 Sep 2025 09:43:45 -0400 Subject: [PATCH 093/209] Changed out spliced ID's for dataset in admin panel. Removed auto-generated documentation from build step. --- src/views/partial/adminPanel/permList.ejs | 8 +- src/views/partial/adminPanel/userList.ejs | 22 +- www/doc/client/addURLPopup.html | 884 -- www/doc/client/cPanel.html | 2611 ------ www/doc/client/channel.html | 1263 --- www/doc/client/channel.js.html | 385 - www/doc/client/chat.js.html | 676 -- www/doc/client/chatBox.html | 4721 ----------- www/doc/client/chatPostprocessor.html | 1803 ---- www/doc/client/chatPostprocessor.js.html | 680 -- www/doc/client/clearPopup.html | 799 -- www/doc/client/commandPreprocessor.html | 1921 ----- www/doc/client/commandPreprocessor.js.html | 380 - www/doc/client/commandProcessor.html | 430 - www/doc/client/cpanel.js.html | 524 -- www/doc/client/defaultTitlesPopup.html | 969 --- www/doc/client/emotePanel.html | 2258 ----- .../client/fonts/OpenSans-Bold-webfont.eot | Bin 19544 -> 0 bytes .../client/fonts/OpenSans-Bold-webfont.svg | 1830 ---- .../client/fonts/OpenSans-Bold-webfont.woff | Bin 22432 -> 0 bytes .../fonts/OpenSans-BoldItalic-webfont.eot | Bin 20133 -> 0 bytes .../fonts/OpenSans-BoldItalic-webfont.svg | 1830 ---- .../fonts/OpenSans-BoldItalic-webfont.woff | Bin 23048 -> 0 bytes .../client/fonts/OpenSans-Italic-webfont.eot | Bin 20265 -> 0 bytes .../client/fonts/OpenSans-Italic-webfont.svg | 1830 ---- .../client/fonts/OpenSans-Italic-webfont.woff | Bin 23188 -> 0 bytes .../client/fonts/OpenSans-Light-webfont.eot | Bin 19514 -> 0 bytes .../client/fonts/OpenSans-Light-webfont.svg | 1831 ---- .../client/fonts/OpenSans-Light-webfont.woff | Bin 22248 -> 0 bytes .../fonts/OpenSans-LightItalic-webfont.eot | Bin 20535 -> 0 bytes .../fonts/OpenSans-LightItalic-webfont.svg | 1835 ---- .../fonts/OpenSans-LightItalic-webfont.woff | Bin 23400 -> 0 bytes .../client/fonts/OpenSans-Regular-webfont.eot | Bin 19836 -> 0 bytes .../client/fonts/OpenSans-Regular-webfont.svg | 1831 ---- .../fonts/OpenSans-Regular-webfont.woff | Bin 22660 -> 0 bytes www/doc/client/global.html | 217 - www/doc/client/hlsBase.html | 2928 ------- www/doc/client/hlsLiveStreamHandler.html | 2905 ------- www/doc/client/index.html | 96 - www/doc/client/mediaHandler.html | 2710 ------ www/doc/client/mediaHandler.js.html | 844 -- www/doc/client/newPlaylistPopup.html | 714 -- www/doc/client/nullHandler.html | 2882 ------- www/doc/client/panelObj.html | 918 -- www/doc/client/panels_emotePanel.js.html | 362 - .../panels_queuePanel_playlistManager.js.html | 992 --- .../panels_queuePanel_queuePanel.js.html | 1688 ---- www/doc/client/panels_settingsPanel.js.html | 239 - www/doc/client/player.html | 3471 -------- www/doc/client/player.js.html | 518 -- www/doc/client/playlistManager.html | 3640 -------- www/doc/client/poppedPanel.html | 1451 ---- www/doc/client/queuePanel.html | 5322 ------------ www/doc/client/rawFileBase.html | 2923 ------- www/doc/client/rawFileHandler.html | 2905 ------- www/doc/client/renamePopup.html | 884 -- www/doc/client/reschedulePopup.html | 1059 --- www/doc/client/schedulePopup.html | 969 --- www/doc/client/scripts/linenumber.js | 25 - .../scripts/prettify/Apache-License-2.0.txt | 202 - www/doc/client/scripts/prettify/lang-css.js | 2 - www/doc/client/scripts/prettify/prettify.js | 28 - www/doc/client/settingsPanel.html | 2026 ----- www/doc/client/styles/jsdoc-default.css | 358 - www/doc/client/styles/prettify-jsdoc.css | 111 - www/doc/client/styles/prettify-tomorrow.css | 132 - www/doc/client/userList.html | 1200 --- www/doc/client/userlist.js.html | 263 - www/doc/client/youtubeEmbedHandler.html | 2900 ------- www/doc/server/activeChannel.html | 1233 --- .../server/app_channel_activeChannel.js.html | 230 - .../server/app_channel_channelManager.js.html | 361 - www/doc/server/app_channel_chat.js.html | 113 - www/doc/server/app_channel_chatBuffer.js.html | 209 - .../server/app_channel_chatHandler.js.html | 393 - .../app_channel_commandPreprocessor.js.html | 497 -- .../server/app_channel_connectedUser.js.html | 375 - .../server/app_channel_media_media.js.html | 119 - .../app_channel_media_playlistHandler.js.html | 1194 --- .../server/app_channel_media_queue.js.html | 1833 ---- .../app_channel_media_queuedMedia.js.html | 189 - www/doc/server/app_channel_tokebot.js.html | 308 - www/doc/server/channelManager.html | 2190 ----- www/doc/server/chat.html | 714 -- www/doc/server/chatBuffer.html | 1338 --- www/doc/server/chatHandler.html | 3885 --------- www/doc/server/commandPreprocessor.html | 1507 ---- www/doc/server/commandProcessor.html | 1840 ---- www/doc/server/connectedUser.html | 2450 ------ .../server/fonts/OpenSans-Bold-webfont.eot | Bin 19544 -> 0 bytes .../server/fonts/OpenSans-Bold-webfont.svg | 1830 ---- .../server/fonts/OpenSans-Bold-webfont.woff | Bin 22432 -> 0 bytes .../fonts/OpenSans-BoldItalic-webfont.eot | Bin 20133 -> 0 bytes .../fonts/OpenSans-BoldItalic-webfont.svg | 1830 ---- .../fonts/OpenSans-BoldItalic-webfont.woff | Bin 23048 -> 0 bytes .../server/fonts/OpenSans-Italic-webfont.eot | Bin 20265 -> 0 bytes .../server/fonts/OpenSans-Italic-webfont.svg | 1830 ---- .../server/fonts/OpenSans-Italic-webfont.woff | Bin 23188 -> 0 bytes .../server/fonts/OpenSans-Light-webfont.eot | Bin 19514 -> 0 bytes .../server/fonts/OpenSans-Light-webfont.svg | 1831 ---- .../server/fonts/OpenSans-Light-webfont.woff | Bin 22248 -> 0 bytes .../fonts/OpenSans-LightItalic-webfont.eot | Bin 20535 -> 0 bytes .../fonts/OpenSans-LightItalic-webfont.svg | 1835 ---- .../fonts/OpenSans-LightItalic-webfont.woff | Bin 23400 -> 0 bytes .../server/fonts/OpenSans-Regular-webfont.eot | Bin 19836 -> 0 bytes .../server/fonts/OpenSans-Regular-webfont.svg | 1831 ---- .../fonts/OpenSans-Regular-webfont.woff | Bin 22660 -> 0 bytes www/doc/server/global.html | 7546 ----------------- www/doc/server/index.html | 96 - www/doc/server/media.html | 799 -- www/doc/server/playlistHandler.html | 5245 ------------ www/doc/server/queue.html | 6686 --------------- www/doc/server/queuedMedia.html | 1728 ---- .../schemas_channel_channelBanSchema.js.html | 110 - ...as_channel_channelPermissionSchema.js.html | 178 - .../schemas_channel_channelSchema.js.html | 943 -- .../server/schemas_channel_chatSchema.js.html | 105 - .../schemas_channel_media_mediaSchema.js.html | 105 - ..._channel_media_playlistMediaSchema.js.html | 133 - ...hemas_channel_media_playlistSchema.js.html | 183 - ...as_channel_media_queuedMediaSchema.js.html | 122 - www/doc/server/schemas_emoteSchema.js.html | 173 - www/doc/server/schemas_flairSchema.js.html | 127 - .../server/schemas_permissionSchema.js.html | 365 - www/doc/server/schemas_statSchema.js.html | 249 - .../schemas_tokebot_tokeCommandSchema.js.html | 169 - .../schemas_user_emailChangeSchema.js.html | 231 - .../schemas_user_passwordResetSchema.js.html | 213 - .../server/schemas_user_userBanSchema.js.html | 530 -- .../server/schemas_user_userSchema.js.html | 897 -- www/doc/server/scripts/linenumber.js | 25 - .../scripts/prettify/Apache-License-2.0.txt | 202 - www/doc/server/scripts/prettify/lang-css.js | 2 - www/doc/server/scripts/prettify/prettify.js | 28 - www/doc/server/styles/jsdoc-default.css | 358 - www/doc/server/styles/prettify-jsdoc.css | 111 - www/doc/server/styles/prettify-tomorrow.css | 132 - www/doc/server/tokebot.html | 1412 --- www/doc/server/utils_altchaUtils.js.html | 127 - www/doc/server/utils_configCheck.js.html | 117 - www/doc/server/utils_hashUtils.js.html | 112 - www/doc/server/utils_linkUtils.js.html | 155 - www/doc/server/utils_loggerUtils.js.html | 236 - www/doc/server/utils_mailUtils.js.html | 149 - .../utils_media_internetArchiveUtils.js.html | 163 - www/doc/server/utils_media_yanker.js.html | 202 - www/doc/server/utils_media_ytdlpUtils.js.html | 195 - www/doc/server/utils_regexUtils.js.html | 78 - www/doc/server/utils_scheduler.js.html | 114 - www/doc/server/utils_sessionUtils.js.html | 245 - www/js/adminPanel.js | 32 +- 151 files changed, 31 insertions(+), 142267 deletions(-) delete mode 100644 www/doc/client/addURLPopup.html delete mode 100644 www/doc/client/cPanel.html delete mode 100644 www/doc/client/channel.html delete mode 100644 www/doc/client/channel.js.html delete mode 100644 www/doc/client/chat.js.html delete mode 100644 www/doc/client/chatBox.html delete mode 100644 www/doc/client/chatPostprocessor.html delete mode 100644 www/doc/client/chatPostprocessor.js.html delete mode 100644 www/doc/client/clearPopup.html delete mode 100644 www/doc/client/commandPreprocessor.html delete mode 100644 www/doc/client/commandPreprocessor.js.html delete mode 100644 www/doc/client/commandProcessor.html delete mode 100644 www/doc/client/cpanel.js.html delete mode 100644 www/doc/client/defaultTitlesPopup.html delete mode 100644 www/doc/client/emotePanel.html delete mode 100644 www/doc/client/fonts/OpenSans-Bold-webfont.eot delete mode 100644 www/doc/client/fonts/OpenSans-Bold-webfont.svg delete mode 100644 www/doc/client/fonts/OpenSans-Bold-webfont.woff delete mode 100644 www/doc/client/fonts/OpenSans-BoldItalic-webfont.eot delete mode 100644 www/doc/client/fonts/OpenSans-BoldItalic-webfont.svg delete mode 100644 www/doc/client/fonts/OpenSans-BoldItalic-webfont.woff delete mode 100644 www/doc/client/fonts/OpenSans-Italic-webfont.eot delete mode 100644 www/doc/client/fonts/OpenSans-Italic-webfont.svg delete mode 100644 www/doc/client/fonts/OpenSans-Italic-webfont.woff delete mode 100644 www/doc/client/fonts/OpenSans-Light-webfont.eot delete mode 100644 www/doc/client/fonts/OpenSans-Light-webfont.svg delete mode 100644 www/doc/client/fonts/OpenSans-Light-webfont.woff delete mode 100644 www/doc/client/fonts/OpenSans-LightItalic-webfont.eot delete mode 100644 www/doc/client/fonts/OpenSans-LightItalic-webfont.svg delete mode 100644 www/doc/client/fonts/OpenSans-LightItalic-webfont.woff delete mode 100644 www/doc/client/fonts/OpenSans-Regular-webfont.eot delete mode 100644 www/doc/client/fonts/OpenSans-Regular-webfont.svg delete mode 100644 www/doc/client/fonts/OpenSans-Regular-webfont.woff delete mode 100644 www/doc/client/global.html delete mode 100644 www/doc/client/hlsBase.html delete mode 100644 www/doc/client/hlsLiveStreamHandler.html delete mode 100644 www/doc/client/index.html delete mode 100644 www/doc/client/mediaHandler.html delete mode 100644 www/doc/client/mediaHandler.js.html delete mode 100644 www/doc/client/newPlaylistPopup.html delete mode 100644 www/doc/client/nullHandler.html delete mode 100644 www/doc/client/panelObj.html delete mode 100644 www/doc/client/panels_emotePanel.js.html delete mode 100644 www/doc/client/panels_queuePanel_playlistManager.js.html delete mode 100644 www/doc/client/panels_queuePanel_queuePanel.js.html delete mode 100644 www/doc/client/panels_settingsPanel.js.html delete mode 100644 www/doc/client/player.html delete mode 100644 www/doc/client/player.js.html delete mode 100644 www/doc/client/playlistManager.html delete mode 100644 www/doc/client/poppedPanel.html delete mode 100644 www/doc/client/queuePanel.html delete mode 100644 www/doc/client/rawFileBase.html delete mode 100644 www/doc/client/rawFileHandler.html delete mode 100644 www/doc/client/renamePopup.html delete mode 100644 www/doc/client/reschedulePopup.html delete mode 100644 www/doc/client/schedulePopup.html delete mode 100644 www/doc/client/scripts/linenumber.js delete mode 100644 www/doc/client/scripts/prettify/Apache-License-2.0.txt delete mode 100644 www/doc/client/scripts/prettify/lang-css.js delete mode 100644 www/doc/client/scripts/prettify/prettify.js delete mode 100644 www/doc/client/settingsPanel.html delete mode 100644 www/doc/client/styles/jsdoc-default.css delete mode 100644 www/doc/client/styles/prettify-jsdoc.css delete mode 100644 www/doc/client/styles/prettify-tomorrow.css delete mode 100644 www/doc/client/userList.html delete mode 100644 www/doc/client/userlist.js.html delete mode 100644 www/doc/client/youtubeEmbedHandler.html delete mode 100644 www/doc/server/activeChannel.html delete mode 100644 www/doc/server/app_channel_activeChannel.js.html delete mode 100644 www/doc/server/app_channel_channelManager.js.html delete mode 100644 www/doc/server/app_channel_chat.js.html delete mode 100644 www/doc/server/app_channel_chatBuffer.js.html delete mode 100644 www/doc/server/app_channel_chatHandler.js.html delete mode 100644 www/doc/server/app_channel_commandPreprocessor.js.html delete mode 100644 www/doc/server/app_channel_connectedUser.js.html delete mode 100644 www/doc/server/app_channel_media_media.js.html delete mode 100644 www/doc/server/app_channel_media_playlistHandler.js.html delete mode 100644 www/doc/server/app_channel_media_queue.js.html delete mode 100644 www/doc/server/app_channel_media_queuedMedia.js.html delete mode 100644 www/doc/server/app_channel_tokebot.js.html delete mode 100644 www/doc/server/channelManager.html delete mode 100644 www/doc/server/chat.html delete mode 100644 www/doc/server/chatBuffer.html delete mode 100644 www/doc/server/chatHandler.html delete mode 100644 www/doc/server/commandPreprocessor.html delete mode 100644 www/doc/server/commandProcessor.html delete mode 100644 www/doc/server/connectedUser.html delete mode 100644 www/doc/server/fonts/OpenSans-Bold-webfont.eot delete mode 100644 www/doc/server/fonts/OpenSans-Bold-webfont.svg delete mode 100644 www/doc/server/fonts/OpenSans-Bold-webfont.woff delete mode 100644 www/doc/server/fonts/OpenSans-BoldItalic-webfont.eot delete mode 100644 www/doc/server/fonts/OpenSans-BoldItalic-webfont.svg delete mode 100644 www/doc/server/fonts/OpenSans-BoldItalic-webfont.woff delete mode 100644 www/doc/server/fonts/OpenSans-Italic-webfont.eot delete mode 100644 www/doc/server/fonts/OpenSans-Italic-webfont.svg delete mode 100644 www/doc/server/fonts/OpenSans-Italic-webfont.woff delete mode 100644 www/doc/server/fonts/OpenSans-Light-webfont.eot delete mode 100644 www/doc/server/fonts/OpenSans-Light-webfont.svg delete mode 100644 www/doc/server/fonts/OpenSans-Light-webfont.woff delete mode 100644 www/doc/server/fonts/OpenSans-LightItalic-webfont.eot delete mode 100644 www/doc/server/fonts/OpenSans-LightItalic-webfont.svg delete mode 100644 www/doc/server/fonts/OpenSans-LightItalic-webfont.woff delete mode 100644 www/doc/server/fonts/OpenSans-Regular-webfont.eot delete mode 100644 www/doc/server/fonts/OpenSans-Regular-webfont.svg delete mode 100644 www/doc/server/fonts/OpenSans-Regular-webfont.woff delete mode 100644 www/doc/server/global.html delete mode 100644 www/doc/server/index.html delete mode 100644 www/doc/server/media.html delete mode 100644 www/doc/server/playlistHandler.html delete mode 100644 www/doc/server/queue.html delete mode 100644 www/doc/server/queuedMedia.html delete mode 100644 www/doc/server/schemas_channel_channelBanSchema.js.html delete mode 100644 www/doc/server/schemas_channel_channelPermissionSchema.js.html delete mode 100644 www/doc/server/schemas_channel_channelSchema.js.html delete mode 100644 www/doc/server/schemas_channel_chatSchema.js.html delete mode 100644 www/doc/server/schemas_channel_media_mediaSchema.js.html delete mode 100644 www/doc/server/schemas_channel_media_playlistMediaSchema.js.html delete mode 100644 www/doc/server/schemas_channel_media_playlistSchema.js.html delete mode 100644 www/doc/server/schemas_channel_media_queuedMediaSchema.js.html delete mode 100644 www/doc/server/schemas_emoteSchema.js.html delete mode 100644 www/doc/server/schemas_flairSchema.js.html delete mode 100644 www/doc/server/schemas_permissionSchema.js.html delete mode 100644 www/doc/server/schemas_statSchema.js.html delete mode 100644 www/doc/server/schemas_tokebot_tokeCommandSchema.js.html delete mode 100644 www/doc/server/schemas_user_emailChangeSchema.js.html delete mode 100644 www/doc/server/schemas_user_passwordResetSchema.js.html delete mode 100644 www/doc/server/schemas_user_userBanSchema.js.html delete mode 100644 www/doc/server/schemas_user_userSchema.js.html delete mode 100644 www/doc/server/scripts/linenumber.js delete mode 100644 www/doc/server/scripts/prettify/Apache-License-2.0.txt delete mode 100644 www/doc/server/scripts/prettify/lang-css.js delete mode 100644 www/doc/server/scripts/prettify/prettify.js delete mode 100644 www/doc/server/styles/jsdoc-default.css delete mode 100644 www/doc/server/styles/prettify-jsdoc.css delete mode 100644 www/doc/server/styles/prettify-tomorrow.css delete mode 100644 www/doc/server/tokebot.html delete mode 100644 www/doc/server/utils_altchaUtils.js.html delete mode 100644 www/doc/server/utils_configCheck.js.html delete mode 100644 www/doc/server/utils_hashUtils.js.html delete mode 100644 www/doc/server/utils_linkUtils.js.html delete mode 100644 www/doc/server/utils_loggerUtils.js.html delete mode 100644 www/doc/server/utils_mailUtils.js.html delete mode 100644 www/doc/server/utils_media_internetArchiveUtils.js.html delete mode 100644 www/doc/server/utils_media_yanker.js.html delete mode 100644 www/doc/server/utils_media_ytdlpUtils.js.html delete mode 100644 www/doc/server/utils_regexUtils.js.html delete mode 100644 www/doc/server/utils_scheduler.js.html delete mode 100644 www/doc/server/utils_sessionUtils.js.html diff --git a/src/views/partial/adminPanel/permList.ejs b/src/views/partial/adminPanel/permList.ejs index 9412de1..d0e9ce1 100644 --- a/src/views/partial/adminPanel/permList.ejs +++ b/src/views/partial/adminPanel/permList.ejs @@ -20,8 +20,8 @@ along with this program. If not, see . %> <% Object.keys(permList).forEach((key)=>{ %> <% if(key != "channelOverrides"){ %> - - <%rankEnum.slice().reverse().forEach((rank)=>{ %> <% }); %> @@ -33,8 +33,8 @@ along with this program. If not, see . %> <% Object.keys(permList.channelOverrides).forEach((key)=>{ %> <% if(key != "channelOverrides"){ %> - - <%rankEnum.slice().reverse().forEach((rank)=>{ %> <% }); %> diff --git a/src/views/partial/adminPanel/userList.ejs b/src/views/partial/adminPanel/userList.ejs index fe827d4..d94142a 100644 --- a/src/views/partial/adminPanel/userList.ejs +++ b/src/views/partial/adminPanel/userList.ejs @@ -41,23 +41,23 @@ along with this program. If not, see . %> <% userList.forEach((curUser) => { %> - - + + - + - + <%- curUser.id %> - - + + <%- curUser.user %> - + <% if(rankEnum.indexOf(curUser.rank) < rankEnum.indexOf(user.rank)){%> + + class="channel-preference-list-item" value="<%- channel.settings[key] %>"> + <% break; default: %> - class="channel-preference-list-item" type="checkbox" <% if(channel.settings[key]){ %> checked <% } %>> + checked <% } %>> <% break; } %> diff --git a/www/js/channel/panels/emotePanel.js b/www/js/channel/panels/emotePanel.js index ded82f5..98a412b 100644 --- a/www/js/channel/panels/emotePanel.js +++ b/www/js/channel/panels/emotePanel.js @@ -284,7 +284,6 @@ class emotePanel extends panelObj{ //Create trash icon const trashIcon = document.createElement('i'); trashIcon.classList.add('emote-list-trash-icon', 'bi-trash-fill'); - trashIcon.id = `emote-list-trash-icon-${emote.name}`; //add deletePersonalEmote event listener trashIcon.addEventListener('click', ()=>{this.deletePersonalEmote(emote.name)}); diff --git a/www/js/channelSettings.js b/www/js/channelSettings.js index ef94eeb..1af50fc 100644 --- a/www/js/channelSettings.js +++ b/www/js/channelSettings.js @@ -163,7 +163,7 @@ class rankList{ } async submitUpdate(event){ - const user = event.target.id.replace("channel-rank-select-",""); + const user = event.target.dataset.user; const rank = event.target.value; await this.submitUserRank(user, rank); @@ -203,7 +203,7 @@ class rankList{ }else{ //Create rank select var rankContent = document.createElement('select'); - rankContent.id = `channel-rank-select-${user.user}` + rankContent.dataset.user = user.user; rankContent.classList.add("channel-rank-select") rankContent.addEventListener("change", this.submitUpdate.bind(this)); @@ -273,7 +273,7 @@ class banList{ async unban(event){ //Rip user outta the target id - const user = event.target.id.replace("admin-user-list-unban-icon-",""); + const user = event.target.dataset.name; //Tell the server to unban them and get the list returned const list = await utils.ajax.chanUnban(this.channel, user); @@ -297,7 +297,7 @@ class banList{ //Create unban icon node const unbanIcon = document.createElement('i'); unbanIcon.classList.add("bi-emoji-smile-fill","admin-user-list-icon","admin-user-list-unban-icon"); - unbanIcon.id = `admin-user-list-unban-icon-${ban.user.user}`; + unbanIcon.dataset.name = ban.user.user; unbanIcon.title = `Unban ${ban.user.user}`; unbanIcon.addEventListener("click", this.unban.bind(this)); @@ -334,7 +334,7 @@ class prefrenceList{ async submitUpdate(event){ //Get key from event target - const key = event.target.id.replace("channel-preference-",""); + const key = event.target.dataset.key; //Pull value from event target let value = event.target.value; @@ -378,7 +378,7 @@ class permList{ } async submitUpdate(event){ - const key = event.target.id.replace("admin-perm-list-rank-select-",""); + const key = event.target.dataset.key; const value = event.target.value; const permMap = new Map([ [key, value] @@ -429,7 +429,7 @@ class tokeCommandList{ } async deleteToke(event){ - const name = event.target.id.replace("toke-command-delete-",""); + const name = event.target.dataset.name; const tokeList = await utils.ajax.deleteChanToke(this.channel, name); @@ -462,7 +462,7 @@ class tokeCommandList{ //Create toke command delete icon const tokeDelete = document.createElement('i'); tokeDelete.classList.add('toke-command-list', 'bi-trash-fill', 'toke-command-delete'); - tokeDelete.id = `toke-command-delete-${toke}`; + tokeDelete.dataset.name = toke; tokeDelete.addEventListener('click', this.deleteToke.bind(this)); //append span contents to tokeSpan @@ -495,7 +495,7 @@ class emoteList{ async deleteEmote(event){ //Strip name from element id - const name = event.target.id.replace('emote-list-delete-',''); + const name = event.target.dataset.name; //Delete emote and pull list const list = await utils.ajax.deleteChanEmote(this.channel, name); @@ -572,7 +572,7 @@ class emoteList{ const deleteIcon = document.createElement('i'); //Set delete icon id and class deleteIcon.classList.add('bi-trash-fill', 'emote-list-delete'); - deleteIcon.id = `emote-list-delete-${emote.name}`; + deleteIcon.dataset.name = emote.name; //Add delete icon event listener deleteIcon.addEventListener('click',this.deleteEmote.bind(this)); From 9eeed591addcfde2793f89deb3a222dac464fd10 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 9 Sep 2025 08:19:46 -0400 Subject: [PATCH 096/209] Re-scheduling currently playing item now stops currnet playback, and re-schedules a clone. Allowing for DB-Friendly schedule2scrub functionality. --- src/app/channel/media/queue.js | 41 +++++---- src/app/channel/media/queuedMedia.js | 19 ++++ .../channel/panels/queuePanel/queuePanel.js | 19 +++- www/js/utils.js | 91 ++++++++++++------- 4 files changed, 112 insertions(+), 58 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index f4cfe3e..83eb236 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -596,7 +596,7 @@ class queue{ } //Find our media, don't remove it yet since we want to do some more testing first - const media = this.getItemByUUID(uuid); + let media = this.getItemByUUID(uuid); //If we got a bad request if(media == null){ @@ -611,27 +611,31 @@ class queue{ //If someone is trying to re-schedule something that starts in the past if(media.startTime < new Date().getTime()){ - //If an originating socket was provided for this request - if(socket != null){ - //If the item is currently playing - if(media.getEndTime() > new Date().getTime()){ - //Yell at the user for being an asshole - loggerUtils.socketErrorHandler(socket, "You cannot move an actively playing video!", "queue"); - //Otherwise, if it's already ended - }else{ + //If the item is currently playing + if(media.getEndTime() > new Date().getTime()){ + //Dupe media for the rest of the function + media = media.clone(); + + //Stop current item + await this.stop(socket, chanDB); + + //Otherwise, if it's already ended + }else{ + //If an originating socket was provided for this request + if(socket != null){ //Yell at the user for being an asshole loggerUtils.socketErrorHandler(socket, "You cannot alter the past!", "queue"); } + + //Ignore it + return; } - - - //Ignore it - return; + //If the item has yet to be played + }else{ + //Remove the media from the schedule + await this.removeMedia(uuid, socket, chanDB); } - //Remove the media from the schedule - await this.removeMedia(uuid, socket, chanDB); - //Grab the old start time for safe keeping const oldStart = media.startTime; @@ -1438,9 +1442,10 @@ class queue{ /** * Stops currently playing media item * @param {Socket} socket - Requesting Socket + * @param {Mongoose.Document} chanDB - Pass through Channel Document to save on DB Transactions * @returns returns false if there is nothing to stop */ - stop(socket){ + async stop(socket, chanDB){ //If we're not currently playing anything if(this.nowPlaying == null){ //If an originating socket was provided for this request @@ -1462,7 +1467,7 @@ class queue{ } //End the media - this.end(); + await this.end(false, false, false, chanDB); } /** diff --git a/src/app/channel/media/queuedMedia.js b/src/app/channel/media/queuedMedia.js index c92d7d0..863c770 100644 --- a/src/app/channel/media/queuedMedia.js +++ b/src/app/channel/media/queuedMedia.js @@ -119,6 +119,25 @@ class queuedMedia extends media{ this.uuid = crypto.randomUUID(); } + /** + * Generates a unique clone of a given media object + * @returns unique clone of media object + */ + clone(){ + return new queuedMedia( + this.title, + this.fileName, + this.url, + this.id, + this.type, + this.duration, + this.rawLink, + this.startTime, + this.startTimeStamp, + this.earlyEnd + ); + } + /** * return the end time of a given queuedMedia object * @param {boolean} fullTime - Overrides early ends diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index 9bb91f4..5fb159e 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -956,16 +956,25 @@ class queuePanel extends panelObj{ //Get current start time const start = this.dateByOffset(target.offsetTop); + const end = new Date(start.getTime() + (target.dataset['duration'] * 1000)); //Position timetip timetip.moveToMouse(event); - //Inject timetip label - //Normally wouldn't do innerHTML but these values are calculated serverside and it saves us making a
element - timetip.tooltip.innerHTML = [ + //Normally wouldn't do innerHTML but these values are calculated serverside and it saves us making a
dom node + let timetipContents = [ `Start Time: ${utils.ux.timeStringFromDate(start, true)}`, - `End Time: ${utils.ux.timeStringFromDate(new Date(start.getTime() + (target.dataset['duration'] * 1000)), true)}` - ].join('
'); + `End Time: ${utils.ux.timeStringFromDate(end, true)}` + ]; + + //If the current time is after the start date, but before the end (we're scheduling to start now) + if(start.getTime() < date.getTime() && end.getTime() > date.getTime()){ + //Add start timestamp to item + timetipContents.push(`Start Timestamp: ${utils.ux.humieFriendlyDuration((date.getTime() - start.getTime()) / 1000, true)}`); + } + + //Inject timetip label + timetip.tooltip.innerHTML = timetipContents.join('
') //Calculate offset from rest of window const windowOffset = this.queueContainer.offsetTop + this.ownerDoc.defaultView.scrollY; diff --git a/www/js/utils.js b/www/js/utils.js index e7bfce2..c8d7508 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -99,7 +99,7 @@ class canopyUXUtils{ return outString; } - humieFriendlyDuration(seconds){ + humieFriendlyDuration(seconds, compact = false){ //If we have an invalid duration if(seconds <= 0){ //bitch, moan, and complain! @@ -119,41 +119,62 @@ class canopyUXUtils{ //Remove recorded minutes seconds -= minutes * 60; - //If we have an hour - if(hours == 1){ - //Add the string - timeStrings.push('1 Hour'); - //If we have hours - }else if(hours > 0){ - //Add the string - timeStrings.push(`${hours} Hours`); + //If we're rendering compact, alarm-clock style duration + if(compact){ + if(hours > 0){ + //Push hours, pad start with 0 + timeStrings.push(String(hours).padStart(2, '0')); + } + + if(hours > 0 || minutes > 0){ + //Push minutes, pad start with 0 + timeStrings.push(String(minutes).padStart(2, '0')); + } + + if(hours > 0 || minutes > 0 || seconds > 0){ + //Push seconds, pre-fix a 00: if hours and minutes are empty, round to nearest int and pad start with 0 + timeStrings.push(`${(hours == 0 && minutes == 0) ? '00:' : ''}${String(Math.round(seconds)).padStart(2, '0')}`); + } + + return timeStrings.join(':'); + //If we're rendering out using our words + }else{ + //If we have an hour + if(hours == 1){ + //Add the string + timeStrings.push('1 Hour'); + //If we have hours + }else if(hours > 0){ + //Add the string + timeStrings.push(`${hours} Hours`); + } + + //If we have a minute + if(minutes == 1){ + //Add the string + timeStrings.push('1 Minute'); + //If we have minutes + }else if(minutes > 0){ + //Add the string + timeStrings.push(`${minutes} Minutes`); + } + + //Add the 'and ' if we need it + const secondsPrefix = timeStrings.length > 0 ? 'and ' : ''; + + //If we have a second + if(seconds == 1){ + //Add the string + timeStrings.push(`${secondsPrefix}1 Second`); + //If we have more than a second + }else if(seconds > 1){ + //Add the string + timeStrings.push(`${secondsPrefix}${Math.round(seconds)} Seconds`); + } + + //Join the time strings together + return timeStrings.join(', '); } - - //If we have a minute - if(minutes == 1){ - //Add the string - timeStrings.push('1 Minute'); - //If we have minutes - }else if(minutes > 0){ - //Add the string - timeStrings.push(`${minutes} Minutes`); - } - - //Add the 'and ' if we need it - const secondsPrefix = timeStrings.length > 0 ? 'and ' : ''; - - //If we have a second - if(seconds == 1){ - //Add the string - timeStrings.push(`${secondsPrefix}1 Second`); - //If we have more than a second - }else if(seconds > 1){ - //Add the string - timeStrings.push(`${secondsPrefix}${Math.round(seconds)} Seconds`); - } - - //Join the time strings together - return timeStrings.join(', '); } //Update this and popup class to use nodes From de11803cea3b5d81d2b084aafab4c3489aed62fa Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 10 Sep 2025 03:43:15 -0400 Subject: [PATCH 097/209] Fixed !clear command to clear server-side chatBuffer as well as client buffers. --- src/app/channel/chatBuffer.js | 47 +++++++++++++++++++++++++++++++--- src/app/channel/chatHandler.js | 4 +++ src/utils/loggerUtils.js | 5 +++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/app/channel/chatBuffer.js b/src/app/channel/chatBuffer.js index 6968e36..3a4e466 100644 --- a/src/app/channel/chatBuffer.js +++ b/src/app/channel/chatBuffer.js @@ -118,16 +118,55 @@ class chatBuffer{ } /** - * Saves RAM-Based buffer to Channel Document in DB - * @param {String} reason - Reason for DB save, formatted as 'x minutes/seconds of in/activity', used for logging purposes - * @param {Mongoose.Document} chanDB - Channel Doc to work with, can be left empty for method to auto-find through channel name. + * Clears out buffer timers to prevent saving */ - async saveDB(reason, chanDB){ + clearTimers(){ //clear existing timers clearTimeout(this.inactivityTimer); clearTimeout(this.busyTimer); this.inactivityTimer = null; this.busyTimer = null; + } + + /** + * Clears RAM-Based chat buffer and saves the result to DB + * @param {String} name - Name of user to clear chats from. Left as null or an empty string, it will clear the entire buffer. + */ + async clearBuffer(name){ + //Clear out DB Timers + this.clearTimers(); + + let reason = "clearing chat"; + + //If we have a null or empty string passed as name + if(name == null || name == ""){ + //Nuke that fcker + this.buffer = []; + //Otherwise + }else{ + reason = `clearing ${name}'s chats` + + //Iterate through chat buffer by index + for(let chatIndex in this.buffer){ + //If the current chat we're looking at was submitted by the given user + if(this.buffer[chatIndex].user.toLowerCase() == name.toLowerCase()){ + //Splice that fcker out + this.buffer.splice(chatIndex, 1); + } + } + } + + await this.saveDB(reason); + } + + /** + * Saves RAM-Based buffer to Channel Document in DB + * @param {String} reason - Reason for DB save, formatted as 'x minutes/seconds of in/activity', used for logging purposes + * @param {Mongoose.Document} chanDB - Channel Doc to work with, can be left empty for method to auto-find through channel name. + */ + async saveDB(reason, chanDB){ + //Clear out DB Timers + this.clearTimers(); //if the server is in screamy boi mode if(config.verbose){ diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index e1d2f9b..4e4c14f 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -334,7 +334,11 @@ class chatHandler{ //If no user was entered OR the user was found if(user == null || target != null){ + //Send command out to browsers to drop chats from buffer this.server.io.in(chan).emit("clearChat", {user}); + + //Clear serverside buffer, down to the DB + activeChan.chatBuffer.clearBuffer(user); } } } diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 46443af..4c6b2cf 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -176,8 +176,11 @@ module.exports.errorMiddleware = function(err, req, res, next){ module.exports.dumpError = function(err, date = new Date()){ try{ const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`; + const path = `log/crash/${date.getTime()}.log`; - fs.writeFile(`log/crash/${date.getTime()}.log`, content); + fs.writeFile(path, content); + + module.exports.consoleWarn(`Warning: Unexpected Server Crash gracefully dumped to '${path}'... SOMETHING MAY BE VERY BROKEN!!!!`); }catch(doubleErr){ module.exports.consoleWarn("Yo Dawg, I herd you like errors, so I put an error in your error dump, so you can dump while you dump:"); module.exports.consoleWarn(err); From eb8ada7fa0f7d5e3ecfebf1e5108e2d2055751fb Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 10 Sep 2025 08:02:36 -0400 Subject: [PATCH 098/209] Fixed initial default toke not loading into DB properly on server first boot. --- .gitignore | 1 + config.example.jsonc | 2 +- src/schemas/tokebot/tokeCommandSchema.js | 13 +++++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 6530cd8..d15d9ec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ log/crash/* www/doc/*/* package-lock.json config.json +config.json.old state.json chatexamples.txt server.cert diff --git a/config.example.jsonc b/config.example.jsonc index 818ca58..544e906 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -17,7 +17,7 @@ //Dailymotion and Vimeo could work using official apis w/o keys, but you wouldn't have any raw file playback options :P "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", //Be careful with what you keep in secrets, you should use special chars, but test your deployment, as some chars may break account registration - //An update to either kill the server and bitch it's planned so it's not so confusing for new admins + //An update to either kill the server and bitch about the issue in console is planned so it's not so confusing for new admins //Session secret used to secure session keys "sessionSecret": "CHANGE_ME", //Altacha secret used to generate altcha challenges diff --git a/src/schemas/tokebot/tokeCommandSchema.js b/src/schemas/tokebot/tokeCommandSchema.js index 164127f..a6e9715 100644 --- a/src/schemas/tokebot/tokeCommandSchema.js +++ b/src/schemas/tokebot/tokeCommandSchema.js @@ -20,6 +20,7 @@ const {mongoose} = require('mongoose'); //Local Imports const defaultTokes = require("../../../defaultTokes.json"); const server = require('../../server'); +const loggerUtils = require('../../utils/loggerUtils'); /** * Mongoose Schema representing a toke command @@ -40,8 +41,11 @@ tokeCommandSchema.pre('save', async function (next){ //Get server tokebot object const tokebot = server.channelManager.chatHandler.commandPreprocessor.tokebot; - //Pop the command on to the end - tokebot.tokeCommands.push(this.command); + //If tokebot is up and running + if(tokebot != null && tokebot.tokeCommands != null){ + //Pop the command on to the end + tokebot.tokeCommands.push(this.command); + } } @@ -92,6 +96,7 @@ tokeCommandSchema.statics.loadDefaults = async function(){ //Ensure default comes first (.bind(this) doesn't seem to work here...) await registerToke(defaultTokes.default); + //For each entry in the defaultTokes.json file defaultTokes.array.forEach(registerToke); @@ -108,9 +113,9 @@ tokeCommandSchema.statics.loadDefaults = async function(){ }catch(err){ if(toke != null){ - console.log(`Error loading toke command: '!${toke}'`); + loggerUtils.dumpError(err); }else{ - console.log("Error, null toke!"); + loggerUtils.consoleWarn("Error, null toke!"); } } } From 3d4e0a662116cfb05705b2d0020dfa2562068193 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 12 Sep 2025 03:38:54 -0400 Subject: [PATCH 099/209] Fixed startTimeStamp not being added to earlyEnd when calculating resume time stamp after livestream end in pushback mode. --- src/app/channel/media/queue.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 83eb236..7866783 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -1230,6 +1230,7 @@ class queue{ this.streamLock = false; //We don't have to save here since someone else will do it for us :) + //Reminder for those of us reading this in the future since I'm a dipshit: this only clears the DB liveRemainder, NOT the RAM backed variable chanDB.media.liveRemainder = null; //Get current epoch @@ -1266,7 +1267,7 @@ class queue{ /** * Overwrites livestream over scheduled media content after it has ended - * @param {queuedMedia} wasPlaying - Media object that was playing while we started the Livestream + * @param {queuedMedia} wasPlaying - Media object that was playing (from the current livestream) * @param {Mongoose.Document} chanDB - Pass through Channel Document to save on DB Transactions */ async livestreamOverwriteSchedule(wasPlaying, chanDB){ @@ -1355,7 +1356,7 @@ class queue{ /** * Pushes back any missed content scheduled during Livestream after Livestream has ended. - * @param {queuedMedia} wasPlaying - Media object that was playing while we started the Livestream + * @param {queuedMedia} wasPlaying - Media object that was playing (from the current livestream) * @param {Mongoose.Document} chanDB - Pass through Channel Document to save on DB Transactions */ async livestreamPushbackSchedule(wasPlaying, chanDB){ @@ -1385,7 +1386,8 @@ class queue{ //if we have a live remainder if(this.liveRemainder != null){ //Set item to continue where it left off - this.liveRemainder.startTimeStamp = this.liveRemainder.earlyEnd; + //TODO: Account for liveRemainder + this.liveRemainder.startTimeStamp = this.liveRemainder.startTimeStamp + this.liveRemainder.earlyEnd; //Rip out the early end so it finish up this.liveRemainder.earlyEnd = null; From dca788dfc0ca6c88112348b46399b12953baca33 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 13 Sep 2025 11:36:08 -0400 Subject: [PATCH 100/209] Improved IA Compatibility. --- src/utils/media/internetArchiveUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/media/internetArchiveUtils.js b/src/utils/media/internetArchiveUtils.js index 8f7debf..9db7321 100644 --- a/src/utils/media/internetArchiveUtils.js +++ b/src/utils/media/internetArchiveUtils.js @@ -103,7 +103,7 @@ module.exports.fetchMetadata = async function(fullID, title){ function compatibilityFilter(file){ //return true for all files that match for web-safe formats - return file.format == "h.264 IA" || file.format == "h.264" || file.format == "Ogg Video" || file.format.match("MPEG4"); + return file.format.match(/^h\.264/) || file.format == "Ogg Video" || file.format.match("MPEG4"); } function pathFilter(file){ From dd39f76725cd0ba4726f2c408631eef9751bc60a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 13 Sep 2025 11:51:10 -0400 Subject: [PATCH 101/209] Client now only injects chat buffer once. --- www/js/channel/channel.js | 6 ------ www/js/channel/chat.js | 10 ++++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index ba4f433..fd72349 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -130,12 +130,6 @@ class channel{ //Store queue lock status this.queueLock = data.queueLock; - - //For each chat held in the chat buffer - for(let chat of data.chatBuffer){ - //Display the chat - this.chatBox.displayChat(chat); - } } /** diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index cae6fac..a8eb996 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -393,6 +393,16 @@ class chatBox{ handleClientInfo(data){ this.updateFlairSelect(data.flairList, data.user.flair); this.updateHighSelect(data.user.highLevel); + + //If the chatbox is empty + if(this.chatBuffer.childElementCount <= 0){ + //For each chat held in the chat buffer + for(let chat of data.chatBuffer){ + //Display the chat + this.displayChat(chat); + } + } + } /** From 261dce7b29eb0f204de68d1dcd838a10d17bd1ec Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 13 Sep 2025 11:53:28 -0400 Subject: [PATCH 102/209] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4101a76..65c8ec5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Canopy - 0.3-INDEV +Canopy - 0.3-INDEV - Hotfix 1 ====== Canopy - /ˈkæ.nə.pi/: From 6222535c4790d37b263407a43e950e011179e394 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 17 Sep 2025 05:11:45 -0400 Subject: [PATCH 103/209] channelManager now tracks all active connectedUser objects for a given user. --- .gitignore | 3 + README.md | 4 +- package.json | 4 +- src/app/channel/activeChannel.js | 18 ++++-- src/app/channel/channelManager.js | 98 ++++++++++++++++++++++++++----- src/app/channel/connectedUser.js | 5 ++ src/app/channel/message.js | 52 ++++++++++++++++ 7 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 src/app/channel/message.js diff --git a/.gitignore b/.gitignore index d15d9ec..bd8bfad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules/ log/crash/* +!log/crash www/doc/*/* +!www/doc/client +!www/doc/server package-lock.json config.json config.json.old diff --git a/README.md b/README.md index 65c8ec5..34a3c2e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Canopy - 0.3-INDEV - Hotfix 1 Canopy - /ˈkæ.nə.pi/: - The upper layer of foliage and branches of a forest, containing the majority of animal life. + - An honest attempt at an freedom/privacy respecting, libre, and open-source refrence implementation of what a stoner streaming service can be. Canopy is a community chat & synced video embedding web application, intended to replace fore.st as the server software for ourfore.st. This new codebase intends to solve the following issues with the current CyTube based software: @@ -22,10 +23,11 @@ The Canopy codebase does not, nor will it ever contain: - Cryptocurrency/Blockchain integration - 'Analytics/Telemtry' spyware - The use of video sources which require proprietary 'Digital ~~Rights Management~~ Ristricitons Malware' such as Widevine. + - The use of large language models, stable diffusion, or generative AI in either development or function. Thirdparty media providers may or may not contain all of the above atrocities :P (though browser-side DRM extensions will never be required), always use an ad-blocker! - Our current goal is to create a cleaner, more modern, purpose-built codebase that has feature-parity with the current version of fore.st, while writing improvements where possible. Once this is accomplished, and ourfore.st has been migrated, work will continue to re-create features from TTN, while also building completely new ones as well. + Our current goal is to create a cleaner, more modern, purpose-built back-end that has feature-parity with the current version of fore.st, writing improvements where possible. Paired with this functionality, are a mix of engineering and artistic choices which attempt to re-create the power-user friendly UX of desktop sites from the early 2010's, with the 'aged like wine' looks that late oughts/early web 2.0 designs graced us with. Making sure that pageloads are low, and GPU use is non-existant along the way, to ensure everything is usable, even on low-end machines. ## License Canopy is written by the community, and provided under the GNU Affero General Public License v3 in order to prevent Canopy from being used in proprietary software or shitcoin scams. \ No newline at end of file diff --git a/package.json b/package.json index da43c50..0c49972 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "canopy-of", - "version": "0.3", + "name": "canopy-of-indev", + "version": "0.4", "license": "AGPL-3.0-only", "dependencies": { "altcha": "^1.0.7", diff --git a/src/app/channel/activeChannel.js b/src/app/channel/activeChannel.js index 8c8374a..566e0c7 100644 --- a/src/app/channel/activeChannel.js +++ b/src/app/channel/activeChannel.js @@ -74,6 +74,7 @@ class activeChannel{ * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access * @param {Socket} socket - Requesting Socket + * @returns {activeUser} active user object generated by the new connection */ async handleConnection(userDB, chanDB, socket){ //get current user object from the userlist @@ -104,10 +105,13 @@ class activeChannel{ this.playlistHandler.defineListeners(socket); //Hand off the connection initiation to it's user object - await userObj.handleConnection(userDB, chanDB, socket) + const activeUser = await userObj.handleConnection(userDB, chanDB, socket) //Send out the userlist this.broadcastUserList(socket.chan); + + //Return active user connection object for use by the channelManager object + return activeUser; } /** @@ -115,11 +119,11 @@ class activeChannel{ * @param {Socket} socket - Requesting Socket */ handleDisconnect(socket){ - //If we have more than one active connection - if(this.userList.get(socket.user.user).sockets.length > 1){ - //temporarily store userObj - var userObj = this.userList.get(socket.user.user); + //temporarily store userObj + let userObj = this.userList.get(socket.user.user); + //If we have more than one active connection + if(userObj.sockets.length > 1){ //Filter out disconnecting socket from socket list, and set as current socket list for user userObj.sockets = userObj.sockets.filter((id) => { return id != socket.id; @@ -127,7 +131,11 @@ class activeChannel{ //Update the userlist this.userList.set(socket.user.user, userObj); + //If this is the last one }else{ + //Tell the server to handle the disconnection of this user object + this.server.handleUserDisconnect(userObj); + //If this is the last connection for this user, remove them from the userlist this.userList.delete(socket.user.user); } diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index f264204..66a14cd 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -33,7 +33,7 @@ const chatHandler = require('./chatHandler'); class channelManager{ /** * Instantiates object containing global server-side channel conection management logic - * @param {Server} io - Socket.io server instanced passed down from server.js + * @param {Socket.io} io - Socket.io server instanced passed down from server.js */ constructor(io){ /** @@ -46,6 +46,11 @@ class channelManager{ */ this.activeChannels = new Map; + /** + * Map containing all active users. This may be redundant, however it improves preformance for user-specific inter-channel functionality + */ + this.activeUsers = new Map; + /** * Global Chat Handler Object */ @@ -88,13 +93,48 @@ class channelManager{ return; } + //Connection accepted past this point + //Define listeners for inter-channel classes this.defineListeners(socket); this.chatHandler.defineListeners(socket); //Hand off the connection to it's given active channel object //Lil' hacky to pass chanDB like that, but why double up on DB calls? - activeChan.handleConnection(userDB, chanDB, socket); + const activeUser = await activeChan.handleConnection(userDB, chanDB, socket); + + //Pull status from server-wide activeUsers map + let status = this.activeUsers.get(activeUser.user); + + //If this user isn't connected anywhere else + if(status == null){ + //initiate the entry + this.activeUsers.set(activeUser.user, [activeUser]); + //otherwise + }else{ + //Push user to array by default + let pushUser = true; + + //For each active connection within the status map + for(let curUser of status){ + //If we're already listing this active user (we're splitting a user connection amongst several sockets) + if(curUser.channel.name == activeUser.channel.name){ + //don't need to push it again + pushUser = false; + } + } + + //if the user is flagged as un-added + if(pushUser){ + //Add their connection object into the status array we pulled + status.push(activeUser); + + //Set status entry to updated array + this.activeUsers.set(activeUser.set, status); + } + } + + }else{ //Toss out anon's socket.emit("kick", {type: "disconnected", reason: "You must log-in to join this channel!"}); @@ -217,6 +257,34 @@ class channelManager{ activeChan.handleDisconnect(socket, reason); } + /** + * Handles a disconnection event for a single active user within a given channel (when all sockets disconnect) + * @param {*} userObj + */ + handleUserDisconnect(userObj){ + //Create array to hold + let stillConnected = []; + + //Crawl through all known user connections + this.crawlConnections(userObj.user, (curUser)=>{ + //If we have a matching username from a different channel + if(curUser.user == userObj.user && userObj.channel.name != curUser.channel.name){ + //Keep current user + stillConnected.push(curUser); + } + }); + + //If we have anyone left + if(stillConnected.length > 0){ + //save the remainder to the status map, otherwise unset the value. + this.activeUsers.set(userObj.user, stillConnected); + //Otherwise + }else{ + //Delete the user from the status map + this.activeUsers.delete(userObj.user); + } + } + /** * Pulls user information by socket * @param {Socket} socket - Socket to check @@ -257,30 +325,28 @@ class channelManager{ * @param {Function} cb - Callback function to run active connections of a given user against */ crawlConnections(user, cb){ - //For each channel - this.activeChannels.forEach((channel) => { - //Check and see if the user is connected - const foundUser = channel.userList.get(user); + //Pull connection list from status map + const list = this.activeUsers.get(user); - //If we found a user and this channel hasn't been added to the list - if(foundUser){ - cb(foundUser); + //If we have active connections + if(list != null){ + //For each connection + for(let user of list){ + //Run the callback against it + cb(user); } - }); + } + } /** * Iterates through connections by a given username, and runs them through a given callback function/method + * This function is deprecated. Instead use channelManager.activeUsers.get(user) * @param {String} user - Username to crawl connections against * @param {Function} cb - Callback function to run active connections of a given user against */ getConnections(user){ - //Create a list to store our connections - var connections = []; - - //crawl through connections - //this.crawlConnections(user,(foundUser)=>{connections.push(foundUser)}); - this.crawlConnections(user,(foundUser)=>{connections.push(foundUser)}); + const connections = this.activeUsers.get(user); //return connects return connections; diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 3fe59d5..81b3220 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -91,6 +91,7 @@ class connectedUser{ * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access * @param {Socket} socket - Requesting Socket + * @returns {activeUser} active user object generated by the new connection */ async handleConnection(userDB, chanDB, socket){ //send metadata to client @@ -115,6 +116,10 @@ class connectedUser{ //Tattoo hashed IP address to user account for seven days await userDB.tattooIPRecord(socket.handshake.address); } + + + //Return active user object for use by activeChannel and channelManager objects + return this; } /** diff --git a/src/app/channel/message.js b/src/app/channel/message.js new file mode 100644 index 0000000..eef6658 --- /dev/null +++ b/src/app/channel/message.js @@ -0,0 +1,52 @@ +/*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 .*/ + +/** + * Class representing a single chat message + */ +class message{ + /** + * Instantiates a chat message object + * @param {connectedUser} sender - User who sent the message + * @param {Array} recipients - Array of connected users who are supposed to receive the message + * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers + * @param {Array} links - Array of URLs/Links included in the message. + */ + constructor(sender, recipients, msg, links){ + + /** + * User who sent the message + */ + this.sender = sender; + + /** + * Array of strings containing usernames to send message to + */ + this.recipients = recipients; + + /** + * Contenst of the messages, with links replaced with numbered file-seperator markers + */ + this.msg = msg; + + /** + * Array of URLs/Links included in the message. + */ + this.links = links; + } +} + +module.exports = message; \ No newline at end of file From 6445950f9029f75fab1ff03e2e888b0110644801 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 17 Sep 2025 20:17:41 -0400 Subject: [PATCH 104/209] Last user activity now marked on humie-friendly page-loads and last-socket disconnects, ensuring accurate 'online' status when disconnected from a channel. --- src/app/channel/channelManager.js | 6 ++- src/routers/adminPanelRouter.js | 3 +- src/routers/channelRouter.js | 4 ++ src/routers/indexRouter.js | 4 ++ src/routers/newChannelRouter.js | 4 ++ src/schemas/user/userSchema.js | 5 +++ src/utils/presenceUtils.js | 75 +++++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/utils/presenceUtils.js diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 66a14cd..4b594ad 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -24,6 +24,7 @@ const {userModel} = require('../../schemas/user/userSchema'); const userBanModel = require('../../schemas/user/userBanSchema'); const loggerUtils = require('../../utils/loggerUtils'); const csrfUtils = require('../../utils/csrfUtils'); +const presenceUtils = require('../../utils/presenceUtils'); const activeChannel = require('./activeChannel'); const chatHandler = require('./chatHandler'); @@ -259,7 +260,7 @@ class channelManager{ /** * Handles a disconnection event for a single active user within a given channel (when all sockets disconnect) - * @param {*} userObj + * @param {connectedUser} userObj - Connected user object to handle disconnection of */ handleUserDisconnect(userObj){ //Create array to hold @@ -282,6 +283,9 @@ class channelManager{ }else{ //Delete the user from the status map this.activeUsers.delete(userObj.user); + + //Mark last disconnection as user activity, as they'll no longer be marked as streaming. + presenceUtils.handlePresence(userObj.user); } } diff --git a/src/routers/adminPanelRouter.js b/src/routers/adminPanelRouter.js index 1d54411..129b403 100644 --- a/src/routers/adminPanelRouter.js +++ b/src/routers/adminPanelRouter.js @@ -17,16 +17,17 @@ along with this program. If not, see .*/ //npm imports const { Router } = require('express'); - //local imports const permissionSchema = require("../schemas/permissionSchema"); const adminPanelController = require("../controllers/adminPanelController"); +const presenceUtils = require("../utils/presenceUtils"); //globals const router = Router(); //Use authentication middleware router.use(permissionSchema.reqPermCheck("adminPanel")) +router.use(presenceUtils.presenceMiddleware); //routing functions router.get('/', adminPanelController.get); diff --git a/src/routers/channelRouter.js b/src/routers/channelRouter.js index c8343ad..fb32167 100644 --- a/src/routers/channelRouter.js +++ b/src/routers/channelRouter.js @@ -22,6 +22,7 @@ const { Router } = require('express'); const channelModel = require("../schemas/channel/channelSchema"); const channelController = require("../controllers/channelController"); const channelSettingsController = require("../controllers/channelSettingsController"); +const presenceUtils = require("../utils/presenceUtils"); //globals const router = Router(); @@ -29,6 +30,9 @@ const router = Router(); //User authentication middleware router.use("/*/settings",channelModel.reqPermCheck("manageChannel","/c/")); +//Use presence middleware +router.use(presenceUtils.presenceMiddleware); + //routing functions router.get('/*/settings', channelSettingsController.get); router.get('/*/', channelController.get); diff --git a/src/routers/indexRouter.js b/src/routers/indexRouter.js index 0b3528b..92135dc 100644 --- a/src/routers/indexRouter.js +++ b/src/routers/indexRouter.js @@ -20,10 +20,14 @@ const { Router } = require('express'); //local imports const indexController = require("../controllers/indexController"); +const presenceUtils = require("../utils/presenceUtils"); //globals const router = Router(); +//Use presence middleware +router.use(presenceUtils.presenceMiddleware); + //routing functions router.get('/', indexController.get); diff --git a/src/routers/newChannelRouter.js b/src/routers/newChannelRouter.js index 0dd998c..e77808b 100644 --- a/src/routers/newChannelRouter.js +++ b/src/routers/newChannelRouter.js @@ -21,6 +21,7 @@ const { Router } = require('express'); //local imports const permissionSchema = require("../schemas/permissionSchema"); const newChannelController = require("../controllers/newChannelController"); +const presenceUtils = require("../utils/presenceUtils"); //globals const router = Router(); @@ -28,6 +29,9 @@ const router = Router(); //user authentication middleware router.use("/",permissionSchema.reqPermCheck("registerChannel")); +//Use presence middleware +router.use(presenceUtils.presenceMiddleware); + //routing functions router.get('/', newChannelController.get); diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 40a3e6e..5f718ed 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -60,6 +60,11 @@ const userSchema = new mongoose.Schema({ required: true, default: new Date() }, + lastActive: { + type: mongoose.SchemaTypes.Date, + required: true, + default: new Date() + }, rank: { type: mongoose.SchemaTypes.String, required: true, diff --git a/src/utils/presenceUtils.js b/src/utils/presenceUtils.js new file mode 100644 index 0000000..603b5c3 --- /dev/null +++ b/src/utils/presenceUtils.js @@ -0,0 +1,75 @@ +/*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 .*/ + +//local includes +const userSchema = require('../schemas/user/userSchema'); + +//User activity map to keep us from constantly reading off of the DB +let activityMap = new Map(); + +//How much difference between last write and now until we hit the DB again (in millis) +//Defaults to two minutes +const tolerance = 2 * (60 * 1000); + +module.exports.presenceMiddleware = function(req, res, next){ + //Pull user from session + const user = req.session.user; + + //if we have a user object + if(user != null){ + //Handle Presence + module.exports.handlePresence(user.user); + } + + //Go on to next part of the middleware chain + next(); +} + +module.exports.handlePresence = async function(user, userDB, noSave = false){ + //If we don't have a user + if(user == null || user == ''){ + //Drop that shit + return; + } + + //Get current date as epoch (millis) + const now = new Date(); + const millis = now.getTime(); + + //Check last user activity + const activity = activityMap.get(user); + + //If we have no recorded activity, or if the the time between now and the last activity is greater than two minutes + if(activity == null || millis - activity > tolerance){ + //Set last user activity + activityMap.set(user, millis); + + //If we wheren't handed a free user doc + if(userDB == null){ + //Pull one from the username + userDB = await userSchema.userModel.findOne({user: user}); + } + + //Set last active in user's DB document + userDB.lastActive = now; + + //If saving is enabled + if(!noSave){ + //Save document to + await userDB.save(); + } + } +} \ No newline at end of file From 1384b02f4dd6911f4e27e66291de0a3fc993768e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 18 Sep 2025 02:43:43 -0400 Subject: [PATCH 105/209] Profile pages now display user status. --- README.md | 6 +-- src/controllers/panel/profileController.js | 6 ++- src/controllers/profileController.js | 6 +++ src/schemas/user/userSchema.js | 3 +- src/utils/presenceUtils.js | 59 ++++++++++++++++++++++ src/views/partial/panels/profile.ejs | 1 + src/views/partial/profile/status.ejs | 22 ++++++++ src/views/profile.ejs | 1 + www/css/panel/profile.css | 6 +++ www/css/profile.css | 5 ++ www/css/theme/movie-night.css | 9 ++++ 11 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/views/partial/profile/status.ejs diff --git a/README.md b/README.md index 34a3c2e..2f346eb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -Canopy - 0.3-INDEV - Hotfix 1 +Canopy - 0.4-INDEV ====== Canopy - /ˈkæ.nə.pi/: - The upper layer of foliage and branches of a forest, containing the majority of animal life. - - An honest attempt at an freedom/privacy respecting, libre, and open-source refrence implementation of what a stoner streaming service can be. + - An honest attempt at a freedom/privacy respecting, libre, and open-source refrence implementation of what a stoner streaming service can be. Canopy is a community chat & synced video embedding web application, intended to replace fore.st as the server software for ourfore.st. This new codebase intends to solve the following issues with the current CyTube based software: @@ -15,7 +15,7 @@ This new codebase intends to solve the following issues with the current CyTube - General Clunk - Less Unique Community Identity -Canopy intends to be a simple node/express.js app. It leverages yt-dlp and the internet archive REST api for metadata gathering. Persistant storage is handled by mongodb, as it's document based nature inherintly works well for cleanly storing large config documents for user/channel settings, and the low use of inter-collection references within the canopy software. All hardcore security functions like server-side input sanatization, session handling, CSRF mitigation, and password hashing are handled by industry-standard open source libraries such as validator/express-validator, express-sessions, csrf-sync, and bcrypt, however it IS hobbiest software, and it should be treated as such. +Canopy is a simple node/express.js app, leveraging yt-dlp and the internet archive REST api for metadata gathering. Persistant storage is handled by mongodb, as it's document based nature inherintly works well for cleanly storing large config documents for user/channel settings, and the low use of inter-collection references within the canopy software. All hardcore security functions like server-side input sanatization, session handling, CSRF mitigation, and password hashing are handled by industry-standard open source libraries such as validator/express-validator, express-sessions, csrf-sync, and bcrypt, however it IS hobbiest software, and it should be treated as such. The Canopy codebase does not, nor will it ever contain: - Advertisements (targetted or otherwise) diff --git a/src/controllers/panel/profileController.js b/src/controllers/panel/profileController.js index ac58d44..4a58c7d 100644 --- a/src/controllers/panel/profileController.js +++ b/src/controllers/panel/profileController.js @@ -18,6 +18,7 @@ along with this program. If not, see .*/ const {validationResult, matchedData} = require('express-validator'); //local imports +const presenceUtils = require('../../utils/presenceUtils'); const {userModel} = require('../../schemas/user/userSchema'); const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils'); @@ -30,7 +31,10 @@ module.exports.get = async function(req, res){ const data = matchedData(req); const profile = await userModel.findProfile({user: data.user}); - return res.render('partial/panels/profile', {profile}); + //Pull presence (should be quick since everyone whos been on since last startup will be backed in RAM) + const presence = await presenceUtils.getPresence(profile.user); + + return res.render('partial/panels/profile', {profile, presence}); }else{ res.status(400); return res.send({errors: validResult.array()}) diff --git a/src/controllers/profileController.js b/src/controllers/profileController.js index c37fdf9..fd13cac 100644 --- a/src/controllers/profileController.js +++ b/src/controllers/profileController.js @@ -17,6 +17,7 @@ along with this program. If not, see .*/ //Local Imports const {userModel} = require('../schemas/user/userSchema'); const csrfUtils = require('../utils/csrfUtils'); +const presenceUtils = require('../utils/presenceUtils'); const {exceptionHandler, errorHandler} = require('../utils/loggerUtils'); //Config @@ -34,11 +35,15 @@ module.exports.get = async function(req, res){ //If we have a user, check if the is looking at their own profile const selfProfile = req.session.user ? profile.user == req.session.user.user : false; + //Pull presence (should be quick since everyone whos been on since last startup will be backed in RAM) + const presence = await presenceUtils.getPresence(profile.user); + res.render('profile', { instance: config.instanceName, user: req.session.user, profile, selfProfile, + presence, csrfToken: csrfUtils.generateToken(req) }); }else{ @@ -47,6 +52,7 @@ module.exports.get = async function(req, res){ user: req.session.user, profile: null, selfProfile: false, + presence: null, csrfToken: csrfUtils.generateToken(req) }); } diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 5f718ed..434447c 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -63,7 +63,7 @@ const userSchema = new mongoose.Schema({ lastActive: { type: mongoose.SchemaTypes.Date, required: true, - default: new Date() + default: new Date(0) }, rank: { type: mongoose.SchemaTypes.String, @@ -505,6 +505,7 @@ userSchema.methods.getProfile = function(includeEmail = false){ id: this.id, user: this.user, date: this.date, + lastActive: this.lastActive, tokes: this.tokes, tokeCount: this.getTokeCount(), img: this.img, diff --git a/src/utils/presenceUtils.js b/src/utils/presenceUtils.js index 603b5c3..0096b69 100644 --- a/src/utils/presenceUtils.js +++ b/src/utils/presenceUtils.js @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //local includes +const server = require('../server'); const userSchema = require('../schemas/user/userSchema'); //User activity map to keep us from constantly reading off of the DB @@ -23,6 +24,64 @@ let activityMap = new Map(); //How much difference between last write and now until we hit the DB again (in millis) //Defaults to two minutes const tolerance = 2 * (60 * 1000); +//How long a user has to be in-active to be considered offline +//Defaults to five minutes +const offlineTimeout = 5 * (60 * 1000); + +module.exports.getPresence = async function(user, userDB){ + //If we don't have a user + if(user == null || user == '' || user == 'Tokebot'){ + //Drop that shit + return; + } + + //Set status as offline + let status = "Offline" + //Attempt to pull from activity map to save on DB pull + let activity = activityMap.get(user); + //Pull current epoch in millis + const now = new Date().getTime(); + + //If we couldn't find anything in RAM + if(activity == null){ + //If we wheren't handed a free user doc + if(userDB == null){ + //Pull one from the username + userDB = await userSchema.userModel.findOne({user: user}); + } + + //If for some reason we can't find a user doc + if(userDB == null){ + //Bail with empty status object + return { + status, + activeConnections: [], + lastActive: 0 + } + } + + //Pull last active date from userDB + activity = userDB.lastActive.getTime(); + } + + //Pull active connections for user from the channel manager + const activeConnections = server.channelManager.getConnections(user); + + //If the user is connected to at least one channel + if(activeConnections != null && activeConnections.length > 0){ + status = "Streaming"; + //Otherwise, if it's been five minutes + }else if(now - activity < offlineTimeout){ + status = "Recently Active"; + } + + //Assemble and return status object + return { + status, + activeConnections, + lastActive: activity + } +} module.exports.presenceMiddleware = function(req, res, next){ //Pull user from session diff --git a/src/views/partial/panels/profile.ejs b/src/views/partial/panels/profile.ejs index c2ca2db..b5728c6 100644 --- a/src/views/partial/panels/profile.ejs +++ b/src/views/partial/panels/profile.ejs @@ -20,6 +20,7 @@ along with this program. If not, see . %> <% }else{ %> View Full Profile

<%- profile.user %>

+ <%- include('../profile/status', {profile, presence, auxClass:"panel"}); %>

Toke Count: <%- profile.tokeCount %>

<% if(profile.pronouns != '' && profile.pronouns != null){ %> diff --git a/src/views/partial/profile/status.ejs b/src/views/partial/profile/status.ejs new file mode 100644 index 0000000..d2bdff5 --- /dev/null +++ b/src/views/partial/profile/status.ejs @@ -0,0 +1,22 @@ +<%# 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 . %> +<% if(profile.user == "Tokebot"){ %> +

Perma-Couched

+<% }else{ %> + <% const statusClass = (presence.status == "Streaming") ? "positive" : ((presence.status == "Offline") ? "inactive" : "positive-low");%> + <% const curChan = (presence.activeConnections == null || presence.activeConnections.length <= 0) ? '' : (presence.activeConnections.length == 1 ? ` - /c/${presence.activeConnections[0].channel.name}` : " - Multiple Channels"); %> +

<%- presence.status %><%-curChan%>

+<% } %> \ No newline at end of file diff --git a/src/views/profile.ejs b/src/views/profile.ejs index ffb964b..3c956f0 100644 --- a/src/views/profile.ejs +++ b/src/views/profile.ejs @@ -33,6 +33,7 @@ along with this program. If not, see . %>

<%- profile.user %>

+ <%- include('partial/profile/status', {profile, presence, auxClass: ""}); %> <%- include('partial/profile/image', {profile, selfProfile}); %> <%- include('partial/profile/pronouns', {profile, selfProfile}); %> <%- include('partial/profile/signature', {profile, selfProfile}); %> diff --git a/www/css/panel/profile.css b/www/css/panel/profile.css index 299a2de..5ee912c 100644 --- a/www/css/panel/profile.css +++ b/www/css/panel/profile.css @@ -24,6 +24,12 @@ along with this program. If not, see .*/ .panel.profile-name{ text-align: center; + margin-bottom: 0; +} + +.panel.profile-status{ + text-align: center; + margin-bottom: 1em; } .panel.profile-img{ diff --git a/www/css/profile.css b/www/css/profile.css index 55c00fb..84a4088 100644 --- a/www/css/profile.css +++ b/www/css/profile.css @@ -56,6 +56,11 @@ along with this program. If not, see .*/ margin: 0; } +#profile-status{ + margin: 0; + text-wrap: nowrap; +} + #profile-img{ position: relative; display: flex; diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index d7443ae..fee6c3e 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -31,6 +31,7 @@ along with this program. If not, see .*/ --accent0-alt1: rgb(70, 70, 70); --accent1: rgb(245, 245, 245); --accent1-alt0: rgb(185, 185, 185); + --accent1-alt1: rgb(124, 124, 124); --accent2: var(--accent0-alt0); --focus0: rgb(51, 153, 51); @@ -157,6 +158,14 @@ textarea{ text-shadow: var(--focus-glow0); } +.positive-low{ + color: var(--focus0); +} + +.inactive{ + color: var(--accent1-alt1); +} + .danger-button{ background-color: var(--danger0); color: var(--accent1); From d541dce8c45490fb108d26048f4d6c9641faac23 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 18 Sep 2025 03:42:52 -0400 Subject: [PATCH 106/209] Starting work on private messaging back-end. --- src/app/{channel => pm}/message.js | 0 src/app/pm/pmHandler.js | 51 ++++++++++++++++++++++++++++++ src/server.js | 2 ++ www/js/channel/channel.js | 9 ++++-- 4 files changed, 59 insertions(+), 3 deletions(-) rename src/app/{channel => pm}/message.js (100%) create mode 100644 src/app/pm/pmHandler.js diff --git a/src/app/channel/message.js b/src/app/pm/message.js similarity index 100% rename from src/app/channel/message.js rename to src/app/pm/message.js diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js new file mode 100644 index 0000000..29b4ad1 --- /dev/null +++ b/src/app/pm/pmHandler.js @@ -0,0 +1,51 @@ +/*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 .*/ + +//local includes +const config = require("../../../config.json"); +const csrfUtils = require("../../utils/csrfUtils"); +const userBanModel = require("../../schemas/user/userBanSchema"); + +/** + * Class containg global server-side private message relay logic + */ +class pmHandler{ + /** + * Instantiates object containing global server-side private message relay logic + * @param {Socket.io} io - Socket.io server instanced passed down from server.js + */ + constructor(io){ + /** + * Socket.io server instance passed down from server.js + */ + this.io = io; + + /** + * Socket.io server namespace for handling messaging + */ + this.namespace = io.of('/pm'); + + //Handle connections from private messaging namespace + this.namespace.on("connection", this.handleConnection.bind(this) ); + } + + async handleConnection(socket){ + + } + +} + +module.exports = pmHandler; \ No newline at end of file diff --git a/src/server.js b/src/server.js index f584830..6005d30 100644 --- a/src/server.js +++ b/src/server.js @@ -33,6 +33,7 @@ const mongoose = require('mongoose'); //Define Local Imports //Application const channelManager = require('./app/channel/channelManager'); +const pmHandler = require('./app/pm/pmHandler'); //Util const configCheck = require('./utils/configCheck'); const scheduler = require('./utils/scheduler'); @@ -196,6 +197,7 @@ scheduler.kickoff(); //Hand over general-namespace socket.io connections to the channel manager module.exports.channelManager = new channelManager(io) +module.exports.pmHandler = new pmHandler(io) //Listen Function webServer.listen(port, () => { diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index fd72349..f7955d1 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -68,12 +68,15 @@ class channel{ * Handles initial client connection */ connect(){ - this.socket = io({ - extraHeaders: { + const clientOptions = { + extraHeaders: { //Include CSRF token 'x-csrf-token': utils.ajax.getCSRFToken() } - }); + }; + + this.socket = io(clientOptions); + this.pmSocket = io("/pm", clientOptions); } /** From 7da07c8717a5cde134761fdcb03c340e56ab9cc3 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 18 Sep 2025 04:13:19 -0400 Subject: [PATCH 107/209] Seperated out socket validation/authorization from channel mangement logic. --- src/app/channel/channelManager.js | 71 ++-------------------- src/utils/presenceUtils.js | 6 +- src/utils/socketUtils.js | 97 +++++++++++++++++++++++++++++++ www/js/channel/channel.js | 8 +-- 4 files changed, 108 insertions(+), 74 deletions(-) create mode 100644 src/utils/socketUtils.js diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 4b594ad..b36d7b5 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -22,6 +22,7 @@ const channelModel = require('../../schemas/channel/channelSchema'); const emoteModel = require('../../schemas/emoteSchema'); const {userModel} = require('../../schemas/user/userSchema'); const userBanModel = require('../../schemas/user/userBanSchema'); +const socketUtils = require('../../utils/socketUtils'); const loggerUtils = require('../../utils/loggerUtils'); const csrfUtils = require('../../utils/csrfUtils'); const presenceUtils = require('../../utils/presenceUtils'); @@ -68,7 +69,7 @@ class channelManager{ async handleConnection(socket){ try{ //ensure unbanned ip and valid CSRF token - if(!(await this.validateSocket(socket))){ + if(!(await socketUtils.validateSocket(socket))){ socket.disconnect(); return; } @@ -76,7 +77,7 @@ class channelManager{ //Prevent logged out connections and authenticate socket if(socket.request.session.user != null){ //Authenticate socket - const userDB = await this.authSocket(socket); + const userDB = await socketUtils.authSocket(socket); //Get the active channel based on the socket var {activeChan, chanDB} = await this.getActiveChan(socket); @@ -146,71 +147,7 @@ class channelManager{ //Flip a table if something fucks up return loggerUtils.socketCriticalExceptionHandler(socket, err); } - } - - /** - * Global server-side validation logic for new connections to any channel - * @param {Socket} socket - Requesting Socket - * @returns {Boolean} true on success - */ - async validateSocket(socket){ - //If we're proxied use passthrough IP - const ip = config.proxied ? socket.handshake.headers['x-forwarded-for'] : socket.handshake.address; - - //Look for ban by IP - const ipBanDB = await userBanModel.checkBanByIP(ip); - - //If this ip is randy bobandy - if(ipBanDB != null){ - //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P - const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration(); - - //If the ban is permanent - if(ipBanDB.permanent){ - //tell it to fuck off - socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`}); - //Otherwise - }else{ - //tell it to fuck off - socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`}); - } - - - return false; - } - - - //Check for Cross-Site Request Forgery - if(!csrfUtils.isRequestValid(socket.request)){ - socket.emit("kick", {type: "disconnected", reason: "Invalid CSRF Token!"}); - return false; - } - - - return true; - } - - /** - * Global server-side authorization logic for new connections to any channel - * @param {Socket} socket - Requesting Socket - * @returns {Mongoose.Document} - Authorized User Document upon success - */ - async authSocket(socket){ - //Find the user in the Database since the session won't store enough data to fulfill our needs :P - const userDB = await userModel.findOne({user: socket.request.session.user.user}); - - if(userDB == null){ - throw loggerUtils.exceptionSmith("User not found!", "unauthorized"); - } - - //Set socket user and channel values - socket.user = { - id: userDB.id, - user: userDB.user, - }; - - return userDB; - } + } /** * Gets active channel from a given socket diff --git a/src/utils/presenceUtils.js b/src/utils/presenceUtils.js index 0096b69..310db6c 100644 --- a/src/utils/presenceUtils.js +++ b/src/utils/presenceUtils.js @@ -16,7 +16,7 @@ along with this program. If not, see .*/ //local includes const server = require('../server'); -const userSchema = require('../schemas/user/userSchema'); +const {userModel} = require('../schemas/user/userSchema'); //User activity map to keep us from constantly reading off of the DB let activityMap = new Map(); @@ -47,7 +47,7 @@ module.exports.getPresence = async function(user, userDB){ //If we wheren't handed a free user doc if(userDB == null){ //Pull one from the username - userDB = await userSchema.userModel.findOne({user: user}); + userDB = await userModel.findOne({user: user}); } //If for some reason we can't find a user doc @@ -119,7 +119,7 @@ module.exports.handlePresence = async function(user, userDB, noSave = false){ //If we wheren't handed a free user doc if(userDB == null){ //Pull one from the username - userDB = await userSchema.userModel.findOne({user: user}); + userDB = await userModel.findOne({user: user}); } //Set last active in user's DB document diff --git a/src/utils/socketUtils.js b/src/utils/socketUtils.js new file mode 100644 index 0000000..68fc46a --- /dev/null +++ b/src/utils/socketUtils.js @@ -0,0 +1,97 @@ +/*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 .*/ + +const config = require('../../config.json'); +const csrfUtils = require('./csrfUtils'); +const {userModel} = require('../schemas/user/userSchema'); +const userBanModel = require('../schemas/user/userBanSchema'); + +module.exports.validateSocket = async function(socket, quiet = false){ + //If we're proxied use passthrough IP + const ip = config.proxied ? socket.handshake.headers['x-forwarded-for'] : socket.handshake.address; + + //Look for ban by IP + const ipBanDB = await userBanModel.checkBanByIP(ip); + + //If this ip is randy bobandy + if(ipBanDB != null){ + //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P + const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration(); + + if(quiet){ + socket.disconnect(); + }else{ + //If the ban is permanent + if(ipBanDB.permanent){ + //tell it to fuck off + socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`}); + //Otherwise + }else{ + //tell it to fuck off + socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`}); + } + } + + return false; + } + + + //Check for Cross-Site Request Forgery + if(!csrfUtils.isRequestValid(socket.request)){ + if(quiet){ + socket.disconnect(); + }else{ + socket.emit("kick", {type: "disconnected", reason: "Invalid CSRF Token!"}); + } + + return false; + } + + + return true; +} + +//socket.request.session is already trusted, we don't actually need to verify against DB for authorzation +//It's just a useful place to grab the DB doc, and is mostly a stand-in from when the only socket-related code was in the channel folder +module.exports.authSocketLite = async function(socket){ + const user = socket.request.session.user; + + //Set socket user and channel values + socket.user = { + id: user.id, + user: user.user, + }; + + //return user object from session + return user; +} + +module.exports.authSocket = async function(socket){ + //Find the user in the Database since the session won't store enough data to fulfill our needs :P + const userDB = await userModel.findOne({user: socket.request.session.user.user}); + + if(userDB == null){ + throw loggerUtils.exceptionSmith("User not found!", "unauthorized"); + } + + //Set socket user and channel values + socket.user = { + id: userDB.id, + user: userDB.user, + }; + + return userDB; +} \ No newline at end of file diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index f7955d1..0377d1e 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -89,11 +89,11 @@ class channel{ this.socket.on("kick", async (data) => { if(data.reason == "Invalid CSRF Token!"){ - //Reload the CSRF token - await utils.ajax.reloadCSRFToken(); + //Warn the user + new canopyUXUtils.popup('Invalid CSRF Token detected, reloading client...'); - //Retry the connection - this.connect(); + //Just reload the fucker + setTimeout(()=>{location.reload();}, 1000); }else{ new canopyUXUtils.popup(`You have been ${data.type} from the channel for the following reason:
${data.reason}`); } From 67edef9035932b0ae5c6e383329acbf4b0d170c7 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 19 Sep 2025 03:47:19 -0400 Subject: [PATCH 108/209] Base implementation of PM back-end started. --- src/app/pm/message.js | 8 +-- src/app/pm/pmHandler.js | 125 +++++++++++++++++++++++++++++++++++- src/utils/loggerUtils.js | 29 ++++++++- src/utils/socketUtils.js | 8 ++- www/js/channel/pmHandler.js | 24 +++++++ 5 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 www/js/channel/pmHandler.js diff --git a/src/app/pm/message.js b/src/app/pm/message.js index eef6658..f2c216c 100644 --- a/src/app/pm/message.js +++ b/src/app/pm/message.js @@ -20,20 +20,20 @@ along with this program. If not, see .*/ class message{ /** * Instantiates a chat message object - * @param {connectedUser} sender - User who sent the message - * @param {Array} recipients - Array of connected users who are supposed to receive the message + * @param {String} sender - Name of user who sent the message + * @param {Array} recipients - Array of usernames who are supposed to receive the message * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers * @param {Array} links - Array of URLs/Links included in the message. */ constructor(sender, recipients, msg, links){ /** - * User who sent the message + * Name of user who sent the message */ this.sender = sender; /** - * Array of strings containing usernames to send message to + * Array of usernames who are supposed to receive the message */ this.recipients = recipients; diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index 29b4ad1..01af129 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -14,10 +14,13 @@ 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 validator = require('validator');//No express here, so regular validator it is! + //local includes -const config = require("../../../config.json"); -const csrfUtils = require("../../utils/csrfUtils"); -const userBanModel = require("../../schemas/user/userBanSchema"); +const loggerUtils = require("../../utils/loggerUtils"); +const socketUtils = require("../../utils/socketUtils"); +const message = require("./message"); /** * Class containg global server-side private message relay logic @@ -43,7 +46,123 @@ class pmHandler{ } async handleConnection(socket){ + try{ + //ensure unbanned ip and valid CSRF token + if(!(await socketUtils.validateSocket(socket))){ + socket.disconnect(); + return; + } + //If the socket wasn't authorized + if(await socketUtils.authSocketLite(socket) == null){ + socket.disconnect(); + return; + } + + //Throw socket into room named after it's user + socket.join(socket.user.user); + + //Define network related event listeners against socket + this.defineListeners(socket); + }catch(err){ + //Flip a table if something fucks up + return loggerUtils.socketCriticalExceptionHandler(socket, err); + } + } + + defineListeners(socket){ + socket.on("pm", (data)=>{this.handlePM(data, socket)}); + } + + async handlePM(data, socket){ + try{ + //Create empty list of recipients + let recipients = []; + + //For each requested recipient + for(let user of data.recipients){ + //If the given user is online and didn't send the message + if(this.checkPresence(user) && user != socket.user.user){ + //Add the recipient to the list + recipients.push(user); + } + } + + //If we don't have any valid recipients + if(recipients.length <= 0){ + //Drop that shit + return; + } + + //Sanatize Message + const msg = this.sanatizeMessage(data.msg); + + //If we have an invalid message + if(msg == null){ + //Drop that shit + return; + } + + //Create message object and relay it off to the recipients + this.relayPMObj(new message( + socket.user.user, + recipients, + msg, + [] + )); + + //If something fucked up + }catch(err){ + //Bitch and moan + return loggerUtils.socketExceptionHandler(socket, err); + } + } + + relayPMObj(msg){ + //For each recipient + for(let user of msg.recipients){ + //Send the message + this.namespace.to(user).emit("message", msg); + } + + //Acknowledge the sent message + this.namespace.to(msg.sender).emit("sent", msg); + } + + /** + * Basic function for checking presence + * This could be done using Channel Presence, but running off of bare Socket.io functionality makes this easier to implement outside the channel if need be + * @param {String} user - Username to check presence of + * @returns {Boolean} Whether or not the user is currently able to accept messages + */ + checkPresence(user){ + //Pull room map from the guts of socket.io and run a null check against the given username + return this.namespace.adapter.rooms.get(user) != null; + } + + /** + * Sanatizes and Validates a single message, Temporary until we get commandPreprocessor split up. + * @param {String} msg - message to validate/sanatize + * @returns {String} sanatized/validates message, returns null on validation failure + */ + sanatizeMessage(msg){ + //if msg is empty or null + if(msg == null || msg == ''){ + //Pimp slap that shit into fucking oblivion + return null; + } + + //Trim and Sanatize for XSS + msg = validator.trim(validator.escape(msg)); + + //Return whether or not the shit was long enough + if(validator.isLength(msg, {min: 1, max: 255})){ + //If it's valid return the message + return msg; + } + + //if not return nothing + return null; } } diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 4c6b2cf..a3100e8 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -173,17 +173,40 @@ module.exports.errorMiddleware = function(err, req, res, next){ * @param {Error} err - error to dump to file * @param {Date} date - Date of error, defaults to now */ -module.exports.dumpError = function(err, date = new Date()){ +module.exports.dumpError = async function(err, date = new Date()){ try{ - const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`; - const path = `log/crash/${date.getTime()}.log`; + //Crash directory + const dir = "./log/crash/" + //Double check crash folder exists + try{ + await fs.stat(dir); + //If we caught an error (most likely it's missing) + }catch(err){ + //Shout about it + module.exports.consoleWarn("Log folder missing, mking dir!") + + //Make it if doesn't + await fs.mkdir(dir, {recursive: true}); + } + + //Assemble log file path + const path = `${dir}${date.getTime()}.log`; + //Generate error file content + const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`; + + //Write content to file fs.writeFile(path, content); + //Whine about the error module.exports.consoleWarn(`Warning: Unexpected Server Crash gracefully dumped to '${path}'... SOMETHING MAY BE VERY BROKEN!!!!`); + //If somethine went really really wrong }catch(doubleErr){ + //Use humor to cope with the pain module.exports.consoleWarn("Yo Dawg, I herd you like errors, so I put an error in your error dump, so you can dump while you dump:"); + //Dump the original error to console module.exports.consoleWarn(err); + //Dump the error we had saving that error to file to console module.exports.consoleWarn(doubleErr); } } \ No newline at end of file diff --git a/src/utils/socketUtils.js b/src/utils/socketUtils.js index 68fc46a..765ea02 100644 --- a/src/utils/socketUtils.js +++ b/src/utils/socketUtils.js @@ -67,7 +67,11 @@ module.exports.validateSocket = async function(socket, quiet = false){ //socket.request.session is already trusted, we don't actually need to verify against DB for authorzation //It's just a useful place to grab the DB doc, and is mostly a stand-in from when the only socket-related code was in the channel folder module.exports.authSocketLite = async function(socket){ - const user = socket.request.session.user; + const user = socket.request.session.user; + + if(user == null){ + return null; + } //Set socket user and channel values socket.user = { @@ -84,7 +88,7 @@ module.exports.authSocket = async function(socket){ const userDB = await userModel.findOne({user: socket.request.session.user.user}); if(userDB == null){ - throw loggerUtils.exceptionSmith("User not found!", "unauthorized"); + return null; } //Set socket user and channel values diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js new file mode 100644 index 0000000..94f392b --- /dev/null +++ b/www/js/channel/pmHandler.js @@ -0,0 +1,24 @@ +/*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 .*/ + +/** + * Class for handling incoming Private Messages + */ +class pmHandler{ + constructor(client){ + this.client = client; + } +} \ No newline at end of file From e19ae744121aabd4478ab2341f754cdbee57bbef Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 25 Sep 2025 23:32:04 -0400 Subject: [PATCH 109/209] Started work on PM Panel layout --- src/app/pm/pmHandler.js | 4 ++ src/controllers/panel/pmController.js | 20 +++++++++ src/routers/panelRouter.js | 2 + src/views/channel.ejs | 2 + src/views/partial/channel/chatPanel.ejs | 1 + src/views/partial/panels/pm.ejs | 37 +++++++++++++++++ www/css/panel/pm.css | 54 +++++++++++++++++++++++++ www/css/theme/movie-night.css | 17 ++++++++ www/js/channel/channel.js | 5 +++ www/js/channel/panels/pmPanel.js | 43 ++++++++++++++++++++ www/js/channel/pmHandler.js | 13 ++++++ 11 files changed, 198 insertions(+) create mode 100644 src/controllers/panel/pmController.js create mode 100644 src/views/partial/panels/pm.ejs create mode 100644 www/css/panel/pm.css create mode 100644 www/js/channel/panels/pmPanel.js diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index 01af129..cf8daab 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -45,6 +45,10 @@ class pmHandler{ this.namespace.on("connection", this.handleConnection.bind(this) ); } + /** + * Handles global server-side initialization for new connections to the private messaging system + * @param {Socket} socket - Requesting Socket + */ async handleConnection(socket){ try{ //ensure unbanned ip and valid CSRF token diff --git a/src/controllers/panel/pmController.js b/src/controllers/panel/pmController.js new file mode 100644 index 0000000..7b46625 --- /dev/null +++ b/src/controllers/panel/pmController.js @@ -0,0 +1,20 @@ +/*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 .*/ + +//root index functions +module.exports.get = async function(req, res){ + res.render('partial/panels/pm', {}); +} \ No newline at end of file diff --git a/src/routers/panelRouter.js b/src/routers/panelRouter.js index 7cafe14..59888c1 100644 --- a/src/routers/panelRouter.js +++ b/src/routers/panelRouter.js @@ -25,6 +25,7 @@ const popoutContainerController = require("../controllers/panel/popoutContainerC const profileController = require("../controllers/panel/profileController"); const queueController = require("../controllers/panel/queueController"); const settingsController = require("../controllers/panel/settingsController"); +const pmController = require("../controllers/panel/pmController"); //Validators const accountValidator = require("../validators/accountValidator"); @@ -38,5 +39,6 @@ router.get('/popoutContainer', popoutContainerController.get); router.get('/profile', accountValidator.user(), profileController.get); router.get('/queue', queueController.get); router.get('/settings', settingsController.get); +router.get('/pm', pmController.get); module.exports = router; diff --git a/src/views/channel.ejs b/src/views/channel.ejs index 561702d..07d9f36 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -47,12 +47,14 @@ along with this program. If not, see . %> + <%# panels %> + <%# main client %> diff --git a/src/views/partial/channel/chatPanel.ejs b/src/views/partial/channel/chatPanel.ejs index b5c4bef..e56c28a 100644 --- a/src/views/partial/channel/chatPanel.ejs +++ b/src/views/partial/channel/chatPanel.ejs @@ -74,6 +74,7 @@ along with this program. If not, see . %>
+ diff --git a/src/views/partial/panels/pm.ejs b/src/views/partial/panels/pm.ejs new file mode 100644 index 0000000..af3e372 --- /dev/null +++ b/src/views/partial/panels/pm.ejs @@ -0,0 +1,37 @@ +<%# 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 . %> + +
+
+
+ + Start Sesh +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/www/css/panel/pm.css b/www/css/panel/pm.css new file mode 100644 index 0000000..09b3c56 --- /dev/null +++ b/www/css/panel/pm.css @@ -0,0 +1,54 @@ +/*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 .*/ +#pm-panel-main-div{ + display: flex; + flex-direction: horizontal; + height: 100%; +} + +#pm-panel-sesh-list-container{ + flex: 1; + max-width: 10em; +} + +#pm-panel-sesh-container{ + display: flex; + flex-direction: column; + flex: 1; + height: calc(100% - 0.25em); + margin-top: 0; +} + +#pm-panel-start-sesh{ + width: calc(100% - 1.25em); + margin-left: 0.25em; + padding: 0 0.5em; + text-wrap: nowrap; + display: flex; +} + +#pm-panel-start-sesh span{ + flex: 1; + text-align: center; +} + +#pm-panel-sesh-buffer{ + flex: 1; +} + +#pm-panel-sesh-control-div{ + margin: 0.5em; +} \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index fee6c3e..1cfda84 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -103,6 +103,14 @@ a:active, i:active:not(button i), .interactive:active{ text-shadow: var(--focus-glow0-alt0); } +div.interactive:hover{ + background-color: var(--bg2); +} + +div.interactive:active{ + background-color: var(--bg1); +} + button i{ margin: 0.05em; text-wrap: nowrap; @@ -610,6 +618,15 @@ div.archived p{ border-left: var(--accent1-alt0) solid 1px; } +/* PM Panel */ +#pm-panel-sesh-container{ + border-left: 1px solid var(--accent0); +} + +#pm-panel-start-sesh{ + border-bottom: 1px solid var(--accent0); +} + /* altcha theming*/ div.altcha{ box-shadow: 4px 4px 1px var(--bg1-alt0) inset; diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 0377d1e..523cf6b 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -51,6 +51,11 @@ class channel{ * Child User List Object */ this.userList = new userList(this); + + /** + * Child PM Handler + */ + this.pmHandler = new pmHandler(this); /** * Child Canopy Panel Object diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js new file mode 100644 index 0000000..761c2d8 --- /dev/null +++ b/www/js/channel/panels/pmPanel.js @@ -0,0 +1,43 @@ +/*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 .*/ + +/** + * Class representing the settings panel + * @extends panelObj + */ +class pmPanel extends panelObj{ + /** + * Instantiates a new Panel Object + * @param {channel} client - Parent client Management Object + * @param {Document} panelDocument - Panel Document + */ + constructor(client, panelDocument){ + super(client, "Private Messaging", "/panel/pm", panelDocument); + } + + closer(){ + } + + docSwitch(){ + this.setupInput(); + } + + /** + * Defines input-related event handlers + */ + setupInput(){ + } +} \ No newline at end of file diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index 94f392b..956bd57 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -20,5 +20,18 @@ along with this program. If not, see .*/ class pmHandler{ constructor(client){ this.client = client; + + this.pmIcon = document.querySelector('#chat-panel-pm-icon'); + + this.defineListeners(); + this.setupInput(); + } + + defineListeners(){ + + } + + setupInput(){ + this.pmIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new pmPanel(client))}); } } \ No newline at end of file From d8e5c64c139ac524f236a2632639a7b07705075a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 30 Sep 2025 03:25:15 -0400 Subject: [PATCH 110/209] Starting work on client-side chat sesh handling --- www/js/channel/pmHandler.js | 81 ++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index 956bd57..c26f47c 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -15,23 +15,100 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ /** - * Class for handling incoming Private Messages + * Class for handling incoming Private Messages for the entire client */ class pmHandler{ + /** + * Instantiates a new Private Message Handler object + * @param {channel} client - Parent client Management Object + */ constructor(client){ + /** + * Parent client management object + */ this.client = client; + /** + * PM Icon in the main chat bar + */ this.pmIcon = document.querySelector('#chat-panel-pm-icon'); + /** + * List of PM Sessions + */ + this.seshList = []; + this.defineListeners(); this.setupInput(); } + /** + * Defines network related event listeners for PM Handler + */ defineListeners(){ - + this.client.pmSocket.on("message", this.handlePM.bind(this)); + this.client.pmSocket.on("sent", this.handlePM.bind(this)); } + /** + * Defines inpet related event listeners for PM handler + */ setupInput(){ this.pmIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new pmPanel(client))}); } + + /** + * Handles received Private Messages from the PM service on the server, organizing it into the proper session from the sesh list + * Or creating a new sesh where a matching one does not a exist + * @param {object} data - Private Message data from the server + */ + handlePM(data){ + //Store whether or not current message has been consumed by an existing sesh + let consumed = false; + + //For each existing sesh + for(let seshIndex in this.seshList){ + //Get current sesh + const sesh = this.seshList[seshIndex]; + + //Check to see if the length of sesh recipients equals current length (only check on arrays that actually make sense to save time) + if(sesh.recipients.length == data.recipients.length){ + /*Feels like cheating to have the JS engine to the hard bits by just telling it to sort them. + That being said, since the function is implemented into the JS Engine itself + It will be quicker than any custom comparison code we can write*/ + + //Sort recipient lists so lists with the same user will be equal when joined together in a string and compare, if they're the same... + if(sesh.recipients.sort().join() == data.recipients.sort().join()){ + //Dump collected message into the matching session + this.seshList[seshIndex].messages.push(data); + + //Let the rest of the method know that we've consumed this message + consumed = true; + } + } + } + + //If we made it through the loop without consuming the message + if(!consumed){ + //Add it to it's own fresh new sesh + this.seshList.push(new pmSesh(data)); + } + } +} + +/** + * Class which represents an existing Private Messaging session between two or more users + */ +class pmSesh{ + /** + * Instatiates a new pmSession object + * @param {Object} message - Initial Private Message object from server that initiated the session + */ + constructor(message){ + //Add recipients from message + this.recipients = message.recipients; + + //Add message to messages array + this.messages = [message]; + } } \ No newline at end of file From e2020406a7884815b8b83bd1758455faadf88081 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 30 Sep 2025 04:39:17 -0400 Subject: [PATCH 111/209] Improved Private Message Session Management --- www/js/channel/pmHandler.js | 56 ++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index c26f47c..09e1a3a 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -66,19 +66,37 @@ class pmHandler{ //Store whether or not current message has been consumed by an existing sesh let consumed = false; + //Create members array from scratch to avoid changing the input data for further processing + const members = []; + + //Manually iterate through recipients + for(const member of data.recipients){ + //check to make sure we're not adding ourselves + if(member != this.client.user.user){ + //Copy relevant array members by value instead of reference + members.push(member); + } + } + + //If this wasn't our message + if(data.sender != this.client.user.user){ + //Push sender onto members list + members.push(data.sender); + } + //For each existing sesh for(let seshIndex in this.seshList){ //Get current sesh const sesh = this.seshList[seshIndex]; //Check to see if the length of sesh recipients equals current length (only check on arrays that actually make sense to save time) - if(sesh.recipients.length == data.recipients.length){ + if(sesh.recipients.length == members.length){ /*Feels like cheating to have the JS engine to the hard bits by just telling it to sort them. That being said, since the function is implemented into the JS Engine itself It will be quicker than any custom comparison code we can write*/ //Sort recipient lists so lists with the same user will be equal when joined together in a string and compare, if they're the same... - if(sesh.recipients.sort().join() == data.recipients.sort().join()){ + if(sesh.recipients.sort().join() == members.sort().join()){ //Dump collected message into the matching session this.seshList[seshIndex].messages.push(data); @@ -91,7 +109,7 @@ class pmHandler{ //If we made it through the loop without consuming the message if(!consumed){ //Add it to it's own fresh new sesh - this.seshList.push(new pmSesh(data)); + this.seshList.push(new pmSesh(data, client)); } } } @@ -104,11 +122,35 @@ class pmSesh{ * Instatiates a new pmSession object * @param {Object} message - Initial Private Message object from server that initiated the session */ - constructor(message){ - //Add recipients from message - this.recipients = message.recipients; + constructor(message, client){ + /** + * Parent client management object + */ + this.client = client; - //Add message to messages array + /** + * Members of session excluding the currently logged in user + */ + this.recipients = []; + + //Manually iterate through recipients + for(const member of message.recipients){ + //check to make sure we're not adding ourselves + if(member != this.client.user.user){ + //Copy relevant array members by value instead of reference + this.recipients.push(member); + } + } + + //If this wasn't our message + if(message.sender != this.client.user.user){ + //Push sender onto members list + this.recipients.push(message.sender); + } + + /** + * Array containing all session messages + */ this.messages = [message]; } } \ No newline at end of file From a681bddbf78002a4f4f1bfafe5d88d5400c5a39f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 30 Sep 2025 04:39:39 -0400 Subject: [PATCH 112/209] Cleaned up emotePanel.js --- www/js/channel/panels/emotePanel.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/js/channel/panels/emotePanel.js b/www/js/channel/panels/emotePanel.js index 98a412b..877b4f7 100644 --- a/www/js/channel/panels/emotePanel.js +++ b/www/js/channel/panels/emotePanel.js @@ -27,6 +27,13 @@ class emotePanel extends panelObj{ constructor(client, panelDocument){ super(client, "Emote Palette", "/panel/emote", panelDocument); + this.defineListeners(); + } + + /** + * Defines network related listeners + */ + defineListeners(){ this.client.socket.on("personalEmotes", this.renderEmoteLists.bind(this)); } From f109314163d5f7907b7c8e460c15f500e03c9d7f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 30 Sep 2025 05:03:36 -0400 Subject: [PATCH 113/209] Added basic sesh list rendering to PM panel UX --- www/css/panel/pm.css | 18 +++++++++++-- www/css/theme/movie-night.css | 5 ++++ www/js/channel/panels/pmPanel.js | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/www/css/panel/pm.css b/www/css/panel/pm.css index 09b3c56..df2db42 100644 --- a/www/css/panel/pm.css +++ b/www/css/panel/pm.css @@ -22,6 +22,8 @@ along with this program. If not, see .*/ #pm-panel-sesh-list-container{ flex: 1; max-width: 10em; + width: calc(100% - 1.25em); + margin-left: 0.25em; } #pm-panel-sesh-container{ @@ -33,11 +35,11 @@ along with this program. If not, see .*/ } #pm-panel-start-sesh{ - width: calc(100% - 1.25em); - margin-left: 0.25em; padding: 0 0.5em; text-wrap: nowrap; display: flex; + font-size: 1.2em; + font-weight: bold; } #pm-panel-start-sesh span{ @@ -51,4 +53,16 @@ along with this program. If not, see .*/ #pm-panel-sesh-control-div{ margin: 0.5em; +} + +div.pm-panel-sesh-list-entry{ + padding: 0 0.5em; + display: flex; + flex-direction: row; +} + +div.pm-panel-sesh-list-entry, div.pm-panel-sesh-list-entry p{ + margin: 0; + text-wrap: nowrap; + text-align: center; } \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 1cfda84..96e5793 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -627,6 +627,11 @@ div.archived p{ border-bottom: 1px solid var(--accent0); } + +div.pm-panel-sesh-list-entry{ + border-bottom: 1px solid var(--accent0); +} + /* altcha theming*/ div.altcha{ box-shadow: 4px 4px 1px var(--bg1-alt0) inset; diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 761c2d8..e502f4f 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -26,13 +26,26 @@ class pmPanel extends panelObj{ */ constructor(client, panelDocument){ super(client, "Private Messaging", "/panel/pm", panelDocument); + + this.defineListeners(); } closer(){ } docSwitch(){ + this.seshList = this.panelDocument.querySelector('#pm-panel-sesh-list'); + this.setupInput(); + + this.renderSeshList(); + } + + /** + * Defines network related event listeners + */ + defineListeners(){ + } /** @@ -40,4 +53,36 @@ class pmPanel extends panelObj{ */ setupInput(){ } + + /** + * Render out current sesh array to sesh list UI + */ + renderSeshList(){ + //For each session tracked by the pmHandler + for(const sesh of this.client.pmHandler.seshList){ + this.renderSeshListEntry(sesh); + } + } + + /** + * Renders out a given messaging sesh to the sesh list UI + */ + renderSeshListEntry(sesh){ + //Create container div + const entryDiv = document.createElement('div'); + //Set conatiner div classes + entryDiv.classList.add('pm-panel-sesh-list-entry','interactive'); + + + //Create sesh label + const seshLabel = document.createElement('p'); + //Create human-readable label out of members array + seshLabel.textContent = utils.unescapeEntities(sesh.recipients.sort().join(', ')); + + //append sesh label to entry div + entryDiv.appendChild(seshLabel); + + //Append entry div to sesh list + this.seshList.appendChild(entryDiv); + } } \ No newline at end of file From e81a4c0973f6a17b4f2feff33b791ab470ee8cca Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 1 Oct 2025 04:33:24 -0400 Subject: [PATCH 114/209] Basic chat UI complete. --- src/app/pm/pmHandler.js | 11 +- src/views/partial/panels/pm.ejs | 8 +- www/css/panel/pm.css | 24 ++ www/css/popup/startChatSesh.css | 27 ++ www/css/theme/movie-night.css | 12 +- www/js/channel/panels/emotePanel.js | 3 + www/js/channel/panels/pmPanel.js | 246 +++++++++++++++++- .../channel/panels/queuePanel/queuePanel.js | 2 +- www/js/channel/panels/settingsPanel.js | 3 + www/js/channel/pmHandler.js | 105 ++++---- www/popup/startChatSesh.html | 23 ++ 11 files changed, 393 insertions(+), 71 deletions(-) create mode 100644 www/css/popup/startChatSesh.css create mode 100644 www/popup/startChatSesh.html diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index cf8daab..0014ec2 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -150,17 +150,14 @@ class pmHandler{ * @returns {String} sanatized/validates message, returns null on validation failure */ sanatizeMessage(msg){ - //if msg is empty or null - if(msg == null || msg == ''){ - //Pimp slap that shit into fucking oblivion - return null; - } + //Normally I'd kill empty messages here + //But instead we're allowing them for sesh startups //Trim and Sanatize for XSS msg = validator.trim(validator.escape(msg)); - //Return whether or not the shit was long enough - if(validator.isLength(msg, {min: 1, max: 255})){ + //Return whether or not the shit was too long + if(validator.isLength(msg, {min: 0, max: 255})){ //If it's valid return the message return msg; } diff --git a/src/views/partial/panels/pm.ejs b/src/views/partial/panels/pm.ejs index af3e372..958a18c 100644 --- a/src/views/partial/panels/pm.ejs +++ b/src/views/partial/panels/pm.ejs @@ -26,11 +26,13 @@ along with this program. If not, see . %>
- +
+

Start a sesh to start chatting!

+
- - + +
diff --git a/www/css/panel/pm.css b/www/css/panel/pm.css index df2db42..2e6663c 100644 --- a/www/css/panel/pm.css +++ b/www/css/panel/pm.css @@ -61,8 +61,32 @@ div.pm-panel-sesh-list-entry{ flex-direction: row; } + +div.pm-panel-sesh-list-entry p{ + pointer-events: none; +} + div.pm-panel-sesh-list-entry, div.pm-panel-sesh-list-entry p{ margin: 0; text-wrap: nowrap; text-align: center; +} + +#pm-panel-sesh-buffer span{ + display: flex; + flex-direction: row; + margin: 0; +} + +.pm-panel-sesh-message-sender, .pm-panel-sesh-message-content{ + margin: 0; + font-size: 10pt; +} + +#pm-panel-sesh-welcome{ + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + height: 100%; } \ No newline at end of file diff --git a/www/css/popup/startChatSesh.css b/www/css/popup/startChatSesh.css new file mode 100644 index 0000000..86cd92f --- /dev/null +++ b/www/css/popup/startChatSesh.css @@ -0,0 +1,27 @@ +/*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 .*/ +#pm-sesh-popup-div{ + display: flex; +} + + +#pm-sesh-popup-div p{ + margin: 0; +} + +#pm-sesh-popup-sup{ + font-size: 0.7em +} \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 96e5793..00e2f24 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -129,13 +129,13 @@ button{ border-radius: 0.5em; } -button:hover{ +button:hover:not([disabled]){ color: var(--focus0-alt1); background-color: var(--focus0-alt0); box-shadow: var(--focus-glow0); } -button:active{ +button:active:not([disabled]){ color: var(--focus0-alt0); background-color: var(--focus0-alt1); box-shadow: var(--focus-glow0-alt0); @@ -179,13 +179,13 @@ textarea{ color: var(--accent1); } -.danger-button:hover, .critical-danger-button, .critical-danger-button:hover{ +.danger-button:hover:not([disabled]), .critical-danger-button, .critical-danger-button:hover{ background-color: var(--danger0-alt1); color: var(--danger0-alt0); box-shadow: var(--danger-glow0); } -.critical-danger-button:hover{ +.critical-danger-button:hover:not([disabled]){ background-color: var(--danger0-alt2); } @@ -219,12 +219,12 @@ textarea{ color: white; } -.positive-button:hover{ +.positive-button:hover:not([disabled]){ color: var(--focus0-alt1); background-color: var(--focus0-alt0); } -.positive-button:active{ +.positive-button:active:not([disabled]){ color: var(--focus0-alt0); background-color: var(--focus0-alt1); } diff --git a/www/js/channel/panels/emotePanel.js b/www/js/channel/panels/emotePanel.js index 877b4f7..ee19936 100644 --- a/www/js/channel/panels/emotePanel.js +++ b/www/js/channel/panels/emotePanel.js @@ -64,6 +64,9 @@ class emotePanel extends panelObj{ this.setupInput(); this.renderEmoteLists(); + + //Call derived method + super.docSwitch(); } /** diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index e502f4f..85e191e 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -27,6 +27,11 @@ class pmPanel extends panelObj{ constructor(client, panelDocument){ super(client, "Private Messaging", "/panel/pm", panelDocument); + /** + * String to hold name of currently active sesh + */ + this.activeSesh = ""; + this.defineListeners(); } @@ -34,33 +39,112 @@ class pmPanel extends panelObj{ } docSwitch(){ + this.startSeshButton = this.panelDocument.querySelector('#pm-panel-start-sesh'); this.seshList = this.panelDocument.querySelector('#pm-panel-sesh-list'); + this.seshBuffer = this.panelDocument.querySelector('#pm-panel-sesh-buffer'); + this.seshPrompt = this.panelDocument.querySelector('#pm-panel-message-prompt'); + this.seshSendButton = this.panelDocument.querySelector('#pm-panel-send-button'); this.setupInput(); this.renderSeshList(); + + //If we have an active sesh + if(this.activeSesh != null && this.activeSesh != ""){ + //Render messages + this.renderMessages(); + } + + //Call derived method + super.docSwitch(); } /** * Defines network related event listeners */ defineListeners(){ - + this.client.pmSocket.on("message", this.handlePM.bind(this)); + this.client.pmSocket.on("sent", this.handlePM.bind(this)); } /** * Defines input-related event handlers */ setupInput(){ + this.startSeshButton.addEventListener('click', this.startSesh.bind(this)); + this.seshPrompt.addEventListener("keydown", this.send.bind(this)); + this.seshSendButton.addEventListener("click", this.send.bind(this)); + } + + startSesh(event){ + new startSeshPopup(event, this.client, this.renderSeshList.bind(this), this.ownerDoc); + } + + handlePM(data){ + const nameObj = pmHandler.genSeshName(data); + + //If this message is for the active sesh + if(nameObj.name == this.activeSesh){ + //Render out the newest message + this.renderMessage(data); + }else{ + //pull current session entry if it exists + const curEntry = this.panelDocument.querySelector(`[data-id="${nameObj.name}"]`); + + //If it doesn't exist + if(curEntry == null){ + //Re-render out the sesh list + this.renderSeshList(); + } + } + } + + /** + * sends private message from sesh prompt to server + * @param {Event} event - Event passed down from Event Handler + */ + send(event){ + if((!event || !event.key || event.key == "Enter") && this.seshPrompt.value && this.activeSesh != ''){ + //Pull current sesh from sesh list + const sesh = this.client.pmHandler.seshList.get(this.activeSesh); + + //Send message out to server + this.client.pmSocket.emit("pm", { + recipients: sesh.recipients, + msg: this.seshPrompt.value + }); + + //Clear our prompt + this.seshPrompt.value = ""; + } } /** * Render out current sesh array to sesh list UI */ renderSeshList(){ + //Clear out the sesh list + this.seshList.innerHTML = ""; + + //Assemble temporary array from client PM Handler sesh list + const seshList = Array.from(this.client.pmHandler.seshList); + + //If we have existing sessions and no active sesh + if(this.activeSesh == "" && seshList[0] != null){ + //Enable UI elements + this.seshPrompt.disabled = false; + this.seshSendButton.disabled = false; + + //Render out messages + this.renderMessages(); + + //Set the first one as active + this.activeSesh = seshList[0][1].id; + } + //For each session tracked by the pmHandler - for(const sesh of this.client.pmHandler.seshList){ - this.renderSeshListEntry(sesh); + for(const seshEntry of seshList){ + this.renderSeshListEntry(seshEntry[1]); } } @@ -72,17 +156,171 @@ class pmPanel extends panelObj{ const entryDiv = document.createElement('div'); //Set conatiner div classes entryDiv.classList.add('pm-panel-sesh-list-entry','interactive'); + //Set dataset sesh name + entryDiv.dataset.id = sesh.id; + //If the current entry is the active sesh + if(sesh.id == this.activeSesh){ + //mark it as such + entryDiv.classList.add('positive'); + } //Create sesh label const seshLabel = document.createElement('p'); //Create human-readable label out of members array - seshLabel.textContent = utils.unescapeEntities(sesh.recipients.sort().join(', ')); + seshLabel.textContent = utils.unescapeEntities(sesh.id); //append sesh label to entry div entryDiv.appendChild(seshLabel); //Append entry div to sesh list this.seshList.appendChild(entryDiv); + + //Add input-related event listener for entry div + entryDiv.addEventListener('click', this.selectSesh.bind(this)); + } + + selectSesh(event){ + //Attempt to pull previously active sesh item from list + const wasActive = this.panelDocument.querySelector('.positive'); + + //If there was an active sesh + if(wasActive != null){ + //Remove active sesh class from old item + wasActive.classList.remove('positive'); + } + + //Pull new active sesh name from target dataset + this.activeSesh = event.target.dataset.id; + + //Set new sesh as active sesh + event.target.classList.add('positive'); + + //Re-render message buffer + this.renderMessages(); + } + + renderMessages(){ + //Empty out the sesh buffer + this.seshBuffer.innerHTML = ""; + + //Pull sesh from pmHandler + const sesh = this.client.pmHandler.seshList.get(this.activeSesh); + + //If the sesh is real + if(sesh != null){ + //for each message in the sesh + for(const message of sesh.messages){ + //Render out messages to the buffer + this.renderMessage(message); + } + } + } + + renderMessage(message){ + const msgSpan = document.createElement('span'); + + const msgSender = document.createElement('p'); + msgSender.innerText = utils.unescapeEntities(`${message.sender}:`); + msgSender.classList.add('pm-panel-sesh-message-sender'); + + const msgContent = document.createElement('p'); + msgContent.innerText = utils.unescapeEntities(message.msg); + msgContent.classList.add('pm-panel-sesh-message-content'); + + msgSpan.appendChild(msgSender); + msgSpan.appendChild(msgContent); + + this.seshBuffer.appendChild(msgSpan); + } +} + +/** + * Class representing pop-up dialogue to start a private messaging sesh + */ +class startSeshPopup{ + /** + * Instantiates a new schedule media Pop-up + * @param {Event} event - Event passed down from Event Listener + * @param {channel} client - Parent Client Management Object + * @param {String} url - URL/link to media to queue + * @param {String} title - Title of media to queue + * @param {Function} cb - Callback function, passed upon pop-up creation + * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up + */ + constructor(event, client, cb, doc){ + /** + * Parent Client Management Object + */ + this.client = client; + + /** + * Callback function, passed upon pop-up creation + */ + this.cb = cb; + + //Create media popup and call async constructor when done + //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :( + /** + * canopyUXUtils.popup() object + */ + this.popup = new canopyUXUtils.popup('/startChatSesh', true, this.asyncConstructor.bind(this), doc); + } + + /** + * Continuation of object construction, called after child popup object construction + */ + asyncConstructor(){ + //Grab required UI elements + this.startSeshButton = this.popup.contentDiv.querySelector('#pm-sesh-popup-button'); + this.usernamePrompt = this.popup.contentDiv.querySelector('#pm-sesh-popup-prompt'); + + //Setup input + this.setupInput(); + } + + /** + * Defines input-related Event Handlers + */ + setupInput(){ + //Setup input + this.startSeshButton.addEventListener('click', this.startSesh.bind(this)); + this.popup.popupDiv.addEventListener('keydown', this.startSesh.bind(this)); + } + + /** + * Handles sending request to schedule item to the queue + * @param {Event} event - Event passed down from Event Listener + */ + startSesh(event){ + //If we clicked or hit enter + if(event.key == null || event.key == "Enter"){ + /* + //Cook a new sesh from + const newSesh = new pmSesh({ + //Split usernames by space + sender: this.client.user.user, + recipients: this.usernamePrompt.value.split(" ") + }); + + //Pop new sesh into pmHandler + this.client.pmHandler.seshList.set(newSesh.id, newSesh); + */ + + //Send message out to server + this.client.pmSocket.emit("pm", { + recipients: this.usernamePrompt.value.split(" "), + msg: "" + }); + + //If we have a function + if(typeof this.cb == "function"){ + //Call any callbacks we where given + this.cb(); + } + + //Close the popup + this.popup.closePopup(); + } } } \ No newline at end of file diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index 5fb159e..2b05766 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -1542,7 +1542,7 @@ class reschedulePopup extends schedulePopup{ this.media = media; } - schedule(event){ + startSesh(event){ //If we clicked or hit enter if(event.key == null || event.key == "Enter"){ //Get localized input date diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index 7f6b707..976afee 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -64,6 +64,9 @@ class settingsPanel extends panelObj{ this.renderSettings(); this.setupInput(); + + //Call derived method + super.docSwitch(); } /** diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index 09e1a3a..c3562f5 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -36,7 +36,7 @@ class pmHandler{ /** * List of PM Sessions */ - this.seshList = []; + this.seshList = new Map(); this.defineListeners(); this.setupInput(); @@ -66,50 +66,63 @@ class pmHandler{ //Store whether or not current message has been consumed by an existing sesh let consumed = false; + const nameObj = pmHandler.genSeshName(data); + //Create members array from scratch to avoid changing the input data for further processing - const members = []; - - //Manually iterate through recipients - for(const member of data.recipients){ - //check to make sure we're not adding ourselves - if(member != this.client.user.user){ - //Copy relevant array members by value instead of reference - members.push(member); - } - } - - //If this wasn't our message - if(data.sender != this.client.user.user){ - //Push sender onto members list - members.push(data.sender); - } + const members = nameObj.recipients; //For each existing sesh - for(let seshIndex in this.seshList){ - //Get current sesh - const sesh = this.seshList[seshIndex]; + for(const seshEntry of this.seshList){ + //Pull sesh object from map entry + const sesh = seshEntry[1]; - //Check to see if the length of sesh recipients equals current length (only check on arrays that actually make sense to save time) - if(sesh.recipients.length == members.length){ - /*Feels like cheating to have the JS engine to the hard bits by just telling it to sort them. - That being said, since the function is implemented into the JS Engine itself - It will be quicker than any custom comparison code we can write*/ + //If currently checked sesh ID matches calculated message sesh id + if(sesh.id == nameObj.name){ + //Dump collected message into the matching session + sesh.messages.push(data); - //Sort recipient lists so lists with the same user will be equal when joined together in a string and compare, if they're the same... - if(sesh.recipients.sort().join() == members.sort().join()){ - //Dump collected message into the matching session - this.seshList[seshIndex].messages.push(data); + //Add sesh to sesh map + this.seshList.set(sesh.id, sesh); - //Let the rest of the method know that we've consumed this message - consumed = true; - } + //Let the rest of the method know that we've consumed this message + consumed = true; } } //If we made it through the loop without consuming the message if(!consumed){ - //Add it to it's own fresh new sesh - this.seshList.push(new pmSesh(data, client)); + //Generate a new sesh + const sesh = new pmSesh(data, client); + + //Add it to the sesh list + this.seshList.set(sesh.id, sesh); + } + } + + static genSeshName(message){ + const recipients = []; + + //Manually iterate through recipients + for(const member of message.recipients){ + //check to make sure we're not adding ourselves + if(member != client.user.user){ + //Copy relevant array members by value instead of reference + recipients.push(member); + } + } + + //If this wasn't our message + if(message.sender != client.user.user){ + //Push sender onto members list + recipients.push(message.sender); + } + + //Sort recipients + recipients.sort(); + + return { + name: recipients.join(', '), + recipients } } } @@ -128,29 +141,21 @@ class pmSesh{ */ this.client = client; + const nameObj = pmHandler.genSeshName(message); + /** * Members of session excluding the currently logged in user */ - this.recipients = []; + this.recipients = nameObj.recipients - //Manually iterate through recipients - for(const member of message.recipients){ - //check to make sure we're not adding ourselves - if(member != this.client.user.user){ - //Copy relevant array members by value instead of reference - this.recipients.push(member); - } - } - - //If this wasn't our message - if(message.sender != this.client.user.user){ - //Push sender onto members list - this.recipients.push(message.sender); - } + /** + * Name of the chat sesh, named after out-going recipients + */ + this.id = nameObj.name; /** * Array containing all session messages */ - this.messages = [message]; + this.messages = (message.msg == "" || message.msg == null) ? [] : [message]; } } \ No newline at end of file diff --git a/www/popup/startChatSesh.html b/www/popup/startChatSesh.html new file mode 100644 index 0000000..b8bb3cc --- /dev/null +++ b/www/popup/startChatSesh.html @@ -0,0 +1,23 @@ + + + +
+

Enter user(s) to chat with:

+ + +
+Users must be online and connected to a channel (it doesn't have to be the same one.) \ No newline at end of file From d465863ee6512ee90136f5860d010d3b99c0aa97 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 1 Oct 2025 19:43:46 -0400 Subject: [PATCH 115/209] Split commandPreprocessor in preperation for formatted private messaging. --- src/app/channel/commandPreprocessor.js | 145 ++------------------- src/app/chatPreprocessor.js | 171 +++++++++++++++++++++++++ src/schemas/channel/chatSchema.js | 3 +- 3 files changed, 182 insertions(+), 137 deletions(-) create mode 100644 src/app/chatPreprocessor.js diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index 8b75261..c08c653 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -18,6 +18,7 @@ along with this program. If not, see .*/ const validator = require('validator');//No express here, so regular validator it is! //Local Imports +const chatPreprocessor = require('../chatPreprocessor'); const tokebot = require('./tokebot'); const linkUtils = require('../../utils/linkUtils'); const permissionModel = require('../../schemas/permissionSchema'); @@ -26,148 +27,20 @@ const channelModel = require('../../schemas/channel/channelSchema'); /** * Class containing global server-side chat/command pre-processing logic */ -class commandPreprocessor{ +class commandPreprocessor extends chatPreprocessor{ /** * Instantiates a commandPreprocessor object * @param {channelManager} server - Parent Server Object * @param {chatHandler} chatHandler - Parent Chat Handler Object */ constructor(server, chatHandler){ - /** - * Parent Server Object - */ - this.server = server; - - /** - * Parent Chat Handler Object - */ - this.chatHandler = chatHandler; - - /** - * Child Command Processor Object - */ - this.commandProcessor = new commandProcessor(server, chatHandler); - - /** - * Child Tokebot Object - */ - this.tokebot = new tokebot(server, chatHandler); - } - - /** - * Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly - * @param {Socket} socket - Socket we're receiving the request from - * @param {Object} data - Event payload - */ - async preprocess(socket, data){ - //Set command object - const commandObj = { - socket, - sendFlag: true, - rawData: data, - chatType: 'chat' - } - - //If we don't pass sanatization/validation turn this car around - if(!this.sanatizeCommand(commandObj)){ - return; - } - - //split the command - this.splitCommand(commandObj); - - //Process the command - await this.processServerCommand(commandObj); - - //If we're going to relay this command as a message, continue on to chat processing - if(commandObj.sendFlag){ - //Prep the message - await this.prepMessage(commandObj); - - //Send the chat - this.sendChat(commandObj); - } - } - - /** - * Sanatizes and Validates a single user chat message/command - * @param {Object} commandObj - Object representing a single given command/chat request - * @returns {Boolean} false if Command/Message is too long to send - */ - sanatizeCommand(commandObj){ - //Trim and Sanatize for XSS - commandObj.command = validator.trim(validator.escape(commandObj.rawData.msg)); - - //Return whether or not the shit was long enough - return (validator.isLength(commandObj.rawData.msg, {min: 1, max: 255})); - } - - /** - * Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders - * These arrays are used to handle further command/chat processing - * @param {Object} commandObj - Object representing a single given command/chat request - */ - splitCommand(commandObj){ - //Split string by words - commandObj.commandArray = commandObj.command.split(/\b/g);//Split by word-borders - commandObj.argumentArray = commandObj.command.match(/\b\w+\b/g);//Match by words surrounded by borders - } - - /** - * Uses the server's Command Processor object to process the chat/command request. - * @param {Object} commandObj - Object representing a single given command/chat request - */ - async processServerCommand(commandObj){ - //If the raw message starts with '!' (skip commands that start with whitespace so people can send example commands in chat) - if(commandObj.rawData.msg[0] == '!'){ - //if it isn't just an exclimation point, and we have a real command - if(commandObj.argumentArray != null){ - //If the command processor knows what to do with whatever the fuck the user sent us - if(this.commandProcessor[commandObj.argumentArray[0].toLowerCase()] != null){ - //Process the command and use the return value to set the sendflag (true if command valid) - commandObj.sendFlag = await this.commandProcessor[commandObj.argumentArray[0].toLowerCase()](commandObj, this); - }else{ - //Process as toke command if we didnt get a match from the standard server-side command processor - commandObj.sendFlag = await this.tokebot.tokeProcessor(commandObj); - } - } - } - } - - /** - * Iterates through links in message and marks them by link type for later use by client-side post-processing - * @param {Object} commandObj - Object representing a single given command/chat request - */ - async markLinks(commandObj){ - //Setup the links array - commandObj.links = []; - - //For each link sent from the client - //this.rawData.links.forEach((link) => { - for (const link of commandObj.rawData.links){ - //Add a marked link object to our links array - commandObj.links.push(await linkUtils.markLink(link)); - } - } - - /** - * Re-creates message string from processed Command Array - * @param {Object} commandObj - Object representing a single given command/chat request - */ - async prepMessage(commandObj){ - //Create message from commandArray - commandObj.message = commandObj.commandArray.join('').trimStart(); - //Validate links and mark them by embed type - await this.markLinks(commandObj); - } - - /** - * Relays chat to channel via parent Chat Handler object - * @param {Object} commandObj - Object representing a single given command/chat request - */ - sendChat(commandObj){ - //FUCKIN' SEND IT! - this.chatHandler.relayUserChat(commandObj.socket, commandObj.message, commandObj.chatType, commandObj.links); + //Call derived constructor + super( + server, + chatHandler, + new commandProcessor(server, chatHandler), + new tokebot(server, chatHandler) + ); } } diff --git a/src/app/chatPreprocessor.js b/src/app/chatPreprocessor.js new file mode 100644 index 0000000..43e77fc --- /dev/null +++ b/src/app/chatPreprocessor.js @@ -0,0 +1,171 @@ +/*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 validator = require('validator');//No express here, so regular validator it is! + +//Local Imports +const linkUtils = require('../utils/linkUtils'); + +/** + * Class containing global server-side chat/command pre-processing logic + */ +class chatPreprocessor{ + /** + * Instantiates a commandPreprocessor object + * @param {channelManager} server - Parent Server Object + * @param {chatHandler} chatHandler - Parent Chat Handler Object + */ + constructor(server, chatHandler, commandProcessor, tokebot){ + /** + * Parent Server Object + */ + this.server = server; + + /** + * Parent Chat Handler Object + */ + this.chatHandler = chatHandler; + + /** + * Child Command Processor Object. Contains functions named after commands. + */ + this.commandProcessor = commandProcessor; + + /** + * Child Tokebot Object + */ + this.tokebot = tokebot; + } + + /** + * Ingests a command/chat request from Chat Handler and pre-processes and processes it accordingly + * @param {Socket} socket - Socket we're receiving the request from + * @param {Object} data - Event payload + */ + async preprocess(socket, data){ + //Set command object + const commandObj = { + socket, + sendFlag: true, + rawData: data, + chatType: 'chat' + } + + //If we don't pass sanatization/validation turn this car around + if(!this.sanatizeCommand(commandObj)){ + return; + } + + //split the command + this.splitCommand(commandObj); + + //Process the command + await this.processServerCommand(commandObj); + + //If we're going to relay this command as a message, continue on to chat processing + if(commandObj.sendFlag){ + //Prep the message + await this.prepMessage(commandObj); + + //Send the chat + this.sendChat(commandObj); + } + } + + /** + * Sanatizes and Validates a single user chat message/command + * @param {Object} commandObj - Object representing a single given command/chat request + * @returns {Boolean} false if Command/Message is too long to send + */ + sanatizeCommand(commandObj){ + //Trim and Sanatize for XSS + commandObj.command = validator.trim(validator.escape(commandObj.rawData.msg)); + + //Return whether or not the shit was long enough + return (validator.isLength(commandObj.rawData.msg, {min: 1, max: 255})); + } + + /** + * Splits raw chat/command data into seperate arrays, one by word-borders and words surrounded by word-borders + * These arrays are used to handle further command/chat processing + * @param {Object} commandObj - Object representing a single given command/chat request + */ + splitCommand(commandObj){ + //Split string by words + commandObj.commandArray = commandObj.command.split(/\b/g);//Split by word-borders + commandObj.argumentArray = commandObj.command.match(/\b\w+\b/g);//Match by words surrounded by borders + } + + /** + * Uses the server's Command Processor object to process the chat/command request. + * @param {Object} commandObj - Object representing a single given command/chat request + */ + async processServerCommand(commandObj){ + //If the raw message starts with '!' (skip commands that start with whitespace so people can send example commands in chat) + if(commandObj.rawData.msg[0] == '!'){ + //if it isn't just an exclimation point, and we have a real command + if(commandObj.argumentArray != null){ + //If the command processor knows what to do with whatever the fuck the user sent us + if(this.commandProcessor[commandObj.argumentArray[0].toLowerCase()] != null){ + //Process the command and use the return value to set the sendflag (true if command valid) + commandObj.sendFlag = await this.commandProcessor[commandObj.argumentArray[0].toLowerCase()](commandObj, this); + }else{ + //Process as toke command if we didnt get a match from the standard server-side command processor + commandObj.sendFlag = await this.tokebot.tokeProcessor(commandObj); + } + } + } + } + + /** + * Iterates through links in message and marks them by link type for later use by client-side post-processing + * @param {Object} commandObj - Object representing a single given command/chat request + */ + async markLinks(commandObj){ + //Setup the links array + commandObj.links = []; + + //For each link sent from the client + //this.rawData.links.forEach((link) => { + for (const link of commandObj.rawData.links){ + //Add a marked link object to our links array + commandObj.links.push(await linkUtils.markLink(link)); + } + } + + /** + * Re-creates message string from processed Command Array + * @param {Object} commandObj - Object representing a single given command/chat request + */ + async prepMessage(commandObj){ + //Create message from commandArray + commandObj.message = commandObj.commandArray.join('').trimStart(); + //Validate links and mark them by embed type + await this.markLinks(commandObj); + } + + /** + * Relays chat to channel via parent Chat Handler object + * @param {Object} commandObj - Object representing a single given command/chat request + */ + sendChat(commandObj){ + //FUCKIN' SEND IT! + this.chatHandler.relayUserChat(commandObj.socket, commandObj.message, commandObj.chatType, commandObj.links); + } +} + +module.exports = chatPreprocessor; \ No newline at end of file diff --git a/src/schemas/channel/chatSchema.js b/src/schemas/channel/chatSchema.js index 537afd0..c0d81b0 100644 --- a/src/schemas/channel/chatSchema.js +++ b/src/schemas/channel/chatSchema.js @@ -32,10 +32,11 @@ const chatSchema = new mongoose.Schema({ }, flair: { type: mongoose.SchemaTypes.String, - required: true, + //Leave this as unreq'd for internal type chats that have no flair }, highLevel: { type: mongoose.SchemaTypes.Number, + default: 0, required: true, }, msg: { From b26dd1094ce342ff3ccf88eec507ac04d667e0a0 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 1 Oct 2025 20:38:16 -0400 Subject: [PATCH 116/209] Modified commandPreprocessor to be easily shared between chat.js and pmHandler.js --- www/js/channel/chat.js | 17 +++++++++++++++-- www/js/channel/commandPreprocessor.js | 17 ++++++++--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index a8eb996..0402a4b 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -274,7 +274,7 @@ class chatBox{ * @param {String} user - User to toke with */ tokeWith(user){ - this.commandPreprocessor.preprocess(user == this.client.user.user ? "!toke up fuckers" : `!toke up ${user}`); + this.transmit(user == this.client.user.user ? "!toke up fuckers" : `!toke up ${user}`); } /** @@ -283,7 +283,9 @@ class chatBox{ */ send(event){ if((!event || !event.key || event.key == "Enter") && this.chatPrompt.value){ - this.commandPreprocessor.preprocess(this.chatPrompt.value); + //Transmit the chat + this.transmit(this.chatPrompt.value); + //Clear our prompt and autocomplete nodes this.chatPrompt.value = ""; this.autocompletePlaceholder.innerHTML = ''; @@ -291,6 +293,17 @@ class chatBox{ } } + transmit(msg){ + //Pre-process chat string + const preprocessedChat = this.commandPreprocessor.preprocess(msg); + + //If we passed through pre-processing + if(preprocessedChat != false){ + //Send pre-processed chat data off to server + this.client.socket.emit("chatMessage", preprocessedChat); + } + } + /** * Displays auto-complete text against current prompt input * @param {Event} event - Event passed down from Event Handler diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index ddfb9f6..af05cfe 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -73,13 +73,19 @@ class commandPreprocessor{ if(this.sendFlag){ //Set the message to the command this.message = command; + //Process message emotes into links this.processEmotes(); + //Process unmarked links into marked links this.processLinks(); - //Send command off to server - this.sendRemoteCommand(); + + //Return pre-processed message data + return {msg: this.message, links: this.links}; } + + //Return false for bad message/command on fall-through + return false; } /** @@ -150,13 +156,6 @@ class commandPreprocessor{ this.message = splitMessage.join(''); } - /** - * Transmits message/command off to server - */ - sendRemoteCommand(){ - this.client.socket.emit("chatMessage",{msg: this.message, links: this.links}); - } - /** * Sets site emotes * @param {Object} data - Emote data from server From 57db26a827f124fae4d15ef4d7796b9c04c757c3 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 1 Oct 2025 21:41:49 -0400 Subject: [PATCH 117/209] Quick Cleanup --- src/app/channel/channelManager.js | 3 --- www/css/theme/movie-night.css | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index b36d7b5..289c227 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -20,11 +20,8 @@ const config = require('../../../config.json'); //Local Imports const channelModel = require('../../schemas/channel/channelSchema'); const emoteModel = require('../../schemas/emoteSchema'); -const {userModel} = require('../../schemas/user/userSchema'); -const userBanModel = require('../../schemas/user/userBanSchema'); const socketUtils = require('../../utils/socketUtils'); const loggerUtils = require('../../utils/loggerUtils'); -const csrfUtils = require('../../utils/csrfUtils'); const presenceUtils = require('../../utils/presenceUtils'); const activeChannel = require('./activeChannel'); const chatHandler = require('./chatHandler'); diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 00e2f24..fa3d836 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -214,7 +214,7 @@ textarea{ text-shadow: var(--danger-glow0-alt1); } -.positive-button{ +.positive-button:not([disabled]){ background-color: var(--focus0); color: white; } From 23ad6794738c928ebcec5cf37e1e23173ee46b92 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 2 Oct 2025 02:29:29 -0400 Subject: [PATCH 118/209] Client-side PM Pre-processing complete. --- www/js/channel/panels/pmPanel.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 85e191e..261c694 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -108,14 +108,20 @@ class pmPanel extends panelObj{ //Pull current sesh from sesh list const sesh = this.client.pmHandler.seshList.get(this.activeSesh); - //Send message out to server - this.client.pmSocket.emit("pm", { - recipients: sesh.recipients, - msg: this.seshPrompt.value - }); + //Preprocess message from prompt + const preprocessedMessage = this.client.chatBox.commandPreprocessor.preprocess(this.seshPrompt.value); - //Clear our prompt - this.seshPrompt.value = ""; + //If preprocessedMessage had it's send flag thrown as false + if(preprocessedMessage != false){ + //Stick recipients into the pre-processed message + preprocessedMessage.recipients = sesh.recipients; + + //Send message out to server + this.client.pmSocket.emit("pm", preprocessedMessage); + } + + //Clear our prompt + this.seshPrompt.value = ""; } } From 0ed1c0dd8980d4aeda17c80e2e68fece8063449c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 2 Oct 2025 03:38:40 -0400 Subject: [PATCH 119/209] More work decoupling chatPreprocessor from src/app/channel. --- src/app/channel/chatHandler.js | 21 +++++++++++--- ...andPreprocessor.js => commandProcessor.js} | 28 +------------------ src/app/chatPreprocessor.js | 21 +++----------- src/schemas/tokebot/tokeCommandSchema.js | 4 +-- 4 files changed, 24 insertions(+), 50 deletions(-) rename src/app/channel/{commandPreprocessor.js => commandProcessor.js} (92%) diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index 4e4c14f..df21492 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -18,7 +18,9 @@ along with this program. If not, see .*/ const validator = require('validator') //local imports -const commandPreprocessor = require('./commandPreprocessor'); +const chatPreprocessor = require('../chatPreprocessor'); +const commandProcessor = require('./commandProcessor'); +const tokebot = require('./tokebot'); const loggerUtils = require('../../utils/loggerUtils'); const linkUtils = require('../../utils/linkUtils'); const emoteValidator = require('../../validators/emoteValidator'); @@ -42,7 +44,11 @@ class chatHandler{ /** * Child Command Pre-Processor Object */ - this.commandPreprocessor = new commandPreprocessor(server, this) + this.chatPreprocessor = new chatPreprocessor( + server, + new commandProcessor(server, this), + new tokebot(server, this) + ); /** * Max chat buffer message count @@ -67,8 +73,15 @@ class chatHandler{ * @param {Socket} socket - Socket we're receiving the request from * @param {Object} data - Event payload */ - handleChat(socket, data){ - this.commandPreprocessor.preprocess(socket, data); + async handleChat(socket, data){ + //Preprocess chat data + const preprocessedChat = await this.chatPreprocessor.preprocess(socket, data); + + //If send flag wasn't set to false + if(preprocessedChat != false){ + //Send that shit! + this.relayUserChat(socket, preprocessedChat.message, preprocessedChat.chatType, preprocessedChat.links); + } } /** diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandProcessor.js similarity index 92% rename from src/app/channel/commandPreprocessor.js rename to src/app/channel/commandProcessor.js index c08c653..213ec1c 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandProcessor.js @@ -14,36 +14,10 @@ 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 validator = require('validator');//No express here, so regular validator it is! - //Local Imports -const chatPreprocessor = require('../chatPreprocessor'); -const tokebot = require('./tokebot'); -const linkUtils = require('../../utils/linkUtils'); const permissionModel = require('../../schemas/permissionSchema'); const channelModel = require('../../schemas/channel/channelSchema'); -/** - * Class containing global server-side chat/command pre-processing logic - */ -class commandPreprocessor extends chatPreprocessor{ - /** - * Instantiates a commandPreprocessor object - * @param {channelManager} server - Parent Server Object - * @param {chatHandler} chatHandler - Parent Chat Handler Object - */ - constructor(server, chatHandler){ - //Call derived constructor - super( - server, - chatHandler, - new commandProcessor(server, chatHandler), - new tokebot(server, chatHandler) - ); - } -} - /** * Class representing global server-side chat/command processing logic */ @@ -317,4 +291,4 @@ class commandProcessor{ } } -module.exports = commandPreprocessor; \ No newline at end of file +module.exports = commandProcessor; \ No newline at end of file diff --git a/src/app/chatPreprocessor.js b/src/app/chatPreprocessor.js index 43e77fc..cbd1cea 100644 --- a/src/app/chatPreprocessor.js +++ b/src/app/chatPreprocessor.js @@ -27,19 +27,13 @@ class chatPreprocessor{ /** * Instantiates a commandPreprocessor object * @param {channelManager} server - Parent Server Object - * @param {chatHandler} chatHandler - Parent Chat Handler Object */ - constructor(server, chatHandler, commandProcessor, tokebot){ + constructor(server, commandProcessor, tokebot){ /** * Parent Server Object */ this.server = server; - /** - * Parent Chat Handler Object - */ - this.chatHandler = chatHandler; - /** * Child Command Processor Object. Contains functions named after commands. */ @@ -82,8 +76,10 @@ class chatPreprocessor{ await this.prepMessage(commandObj); //Send the chat - this.sendChat(commandObj); + return commandObj; } + + return false; } /** @@ -157,15 +153,6 @@ class chatPreprocessor{ //Validate links and mark them by embed type await this.markLinks(commandObj); } - - /** - * Relays chat to channel via parent Chat Handler object - * @param {Object} commandObj - Object representing a single given command/chat request - */ - sendChat(commandObj){ - //FUCKIN' SEND IT! - this.chatHandler.relayUserChat(commandObj.socket, commandObj.message, commandObj.chatType, commandObj.links); - } } module.exports = chatPreprocessor; \ No newline at end of file diff --git a/src/schemas/tokebot/tokeCommandSchema.js b/src/schemas/tokebot/tokeCommandSchema.js index a6e9715..892b7d0 100644 --- a/src/schemas/tokebot/tokeCommandSchema.js +++ b/src/schemas/tokebot/tokeCommandSchema.js @@ -39,7 +39,7 @@ tokeCommandSchema.pre('save', async function (next){ //if the command was changed if(this.isModified("command")){ //Get server tokebot object - const tokebot = server.channelManager.chatHandler.commandPreprocessor.tokebot; + const tokebot = server.channelManager.chatHandler.chatPreprocessor.tokebot; //If tokebot is up and running if(tokebot != null && tokebot.tokeCommands != null){ @@ -58,7 +58,7 @@ tokeCommandSchema.pre('save', async function (next){ */ tokeCommandSchema.pre('deleteOne', {document: true}, async function (next){ //Get server tokebot object (isn't this a fun dot crawler? Why hasn't anyone asked me to stop writing software yet?) - const tokebot = server.channelManager.chatHandler.commandPreprocessor.tokebot; + const tokebot = server.channelManager.chatHandler.chatPreprocessor.tokebot; //Get the index of the command within tokeCommand and splice it out tokebot.tokeCommands.splice(tokebot.tokeCommands.indexOf(this.command),1); From ad3cdd38a33a7c6f2225beadce02520980a91b82 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 2 Oct 2025 04:05:13 -0400 Subject: [PATCH 120/209] More work decoupling chatPreprocessor.js from chatHandler.js --- src/app/channel/chatHandler.js | 1 - src/app/chatPreprocessor.js | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index df21492..c4d22c2 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -45,7 +45,6 @@ class chatHandler{ * Child Command Pre-Processor Object */ this.chatPreprocessor = new chatPreprocessor( - server, new commandProcessor(server, this), new tokebot(server, this) ); diff --git a/src/app/chatPreprocessor.js b/src/app/chatPreprocessor.js index cbd1cea..23bf559 100644 --- a/src/app/chatPreprocessor.js +++ b/src/app/chatPreprocessor.js @@ -19,6 +19,7 @@ const validator = require('validator');//No express here, so regular validator i //Local Imports const linkUtils = require('../utils/linkUtils'); +const commandProcessor = require('./channel/commandProcessor'); /** * Class containing global server-side chat/command pre-processing logic @@ -26,14 +27,9 @@ const linkUtils = require('../utils/linkUtils'); class chatPreprocessor{ /** * Instantiates a commandPreprocessor object - * @param {channelManager} server - Parent Server Object + * @param {commandProcessor} - Child Command Processor Object. Contains functions named after commands. */ - constructor(server, commandProcessor, tokebot){ - /** - * Parent Server Object - */ - this.server = server; - + constructor(commandProcessor, tokebot){ /** * Child Command Processor Object. Contains functions named after commands. */ From 6f89f36fb8fb113a804d8eb0b68a0ce14c648273 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 2 Oct 2025 04:56:47 -0400 Subject: [PATCH 121/209] Integrated server-side chatPreprocessor.js with pmHandler.js --- src/app/chatPreprocessor.js | 4 +-- src/app/pm/message.js | 8 ++++- src/app/pm/pmHandler.js | 62 ++++++++++++++----------------------- 3 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/app/chatPreprocessor.js b/src/app/chatPreprocessor.js index 23bf559..be7cf26 100644 --- a/src/app/chatPreprocessor.js +++ b/src/app/chatPreprocessor.js @@ -112,10 +112,10 @@ class chatPreprocessor{ //if it isn't just an exclimation point, and we have a real command if(commandObj.argumentArray != null){ //If the command processor knows what to do with whatever the fuck the user sent us - if(this.commandProcessor[commandObj.argumentArray[0].toLowerCase()] != null){ + if(this.commandProcessor != null && this.commandProcessor[commandObj.argumentArray[0].toLowerCase()] != null){ //Process the command and use the return value to set the sendflag (true if command valid) commandObj.sendFlag = await this.commandProcessor[commandObj.argumentArray[0].toLowerCase()](commandObj, this); - }else{ + }else if(this.tokebot != null){ //Process as toke command if we didnt get a match from the standard server-side command processor commandObj.sendFlag = await this.tokebot.tokeProcessor(commandObj); } diff --git a/src/app/pm/message.js b/src/app/pm/message.js index f2c216c..1ccb88d 100644 --- a/src/app/pm/message.js +++ b/src/app/pm/message.js @@ -23,9 +23,10 @@ class message{ * @param {String} sender - Name of user who sent the message * @param {Array} recipients - Array of usernames who are supposed to receive the message * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers + * @param {String} type - Message Type Identifier, used for client-side processing. * @param {Array} links - Array of URLs/Links included in the message. */ - constructor(sender, recipients, msg, links){ + constructor(sender, recipients, msg, type, links){ /** * Name of user who sent the message @@ -42,6 +43,11 @@ class message{ */ this.msg = msg; + /** + * Message Type Identifier, used for client-side processing. + */ + this.type = type; + /** * Array of URLs/Links included in the message. */ diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index 0014ec2..0e80206 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -18,6 +18,7 @@ along with this program. If not, see .*/ const validator = require('validator');//No express here, so regular validator it is! //local includes +const chatPreprocessor = require('../chatPreprocessor'); const loggerUtils = require("../../utils/loggerUtils"); const socketUtils = require("../../utils/socketUtils"); const message = require("./message"); @@ -41,6 +42,8 @@ class pmHandler{ */ this.namespace = io.of('/pm'); + this.chatPreprocessor = new chatPreprocessor(null, null); + //Handle connections from private messaging namespace this.namespace.on("connection", this.handleConnection.bind(this) ); } @@ -80,39 +83,25 @@ class pmHandler{ async handlePM(data, socket){ try{ - //Create empty list of recipients - let recipients = []; - - //For each requested recipient - for(let user of data.recipients){ - //If the given user is online and didn't send the message - if(this.checkPresence(user) && user != socket.user.user){ - //Add the recipient to the list - recipients.push(user); - } - } + //Check recipients + const recipients = this.checkRecipients(data.recipients, socket); //If we don't have any valid recipients if(recipients.length <= 0){ //Drop that shit - return; + return false; } - //Sanatize Message - const msg = this.sanatizeMessage(data.msg); - - //If we have an invalid message - if(msg == null){ - //Drop that shit - return; - } + //preprocess message + const preprocessedMessage = await this.chatPreprocessor.preprocess(socket, data); //Create message object and relay it off to the recipients this.relayPMObj(new message( socket.user.user, recipients, - msg, - [] + preprocessedMessage.message, + preprocessedMessage.chatType, + preprocessedMessage.links )); //If something fucked up @@ -144,26 +133,21 @@ class pmHandler{ return this.namespace.adapter.rooms.get(user) != null; } - /** - * Sanatizes and Validates a single message, Temporary until we get commandPreprocessor split up. - * @param {String} msg - message to validate/sanatize - * @returns {String} sanatized/validates message, returns null on validation failure - */ - sanatizeMessage(msg){ - //Normally I'd kill empty messages here - //But instead we're allowing them for sesh startups + checkRecipients(input, socket){ + //Create empty recipients array + let recipients = []; - //Trim and Sanatize for XSS - msg = validator.trim(validator.escape(msg)); - - //Return whether or not the shit was too long - if(validator.isLength(msg, {min: 0, max: 255})){ - //If it's valid return the message - return msg; + //For each requested recipient + for(let user of input){ + //If the given user is online and didn't send the message + if(this.checkPresence(user) && user != socket.user.user){ + //Add the recipient to the list + recipients.push(user); + } } - //if not return nothing - return null; + //return recipients + return recipients; } } From e1cdca2b96ffc0526f1172b26a52b20e21877fc0 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 2 Oct 2025 05:09:45 -0400 Subject: [PATCH 122/209] Fixed bug with sesh starting --- src/app/channel/chatHandler.js | 18 ++++++++++++------ src/app/pm/pmHandler.js | 12 ++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index c4d22c2..bc1e751 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -73,13 +73,19 @@ class chatHandler{ * @param {Object} data - Event payload */ async handleChat(socket, data){ - //Preprocess chat data - const preprocessedChat = await this.chatPreprocessor.preprocess(socket, data); + try{ + //Preprocess chat data + const preprocessedChat = await this.chatPreprocessor.preprocess(socket, data); - //If send flag wasn't set to false - if(preprocessedChat != false){ - //Send that shit! - this.relayUserChat(socket, preprocessedChat.message, preprocessedChat.chatType, preprocessedChat.links); + //If send flag wasn't set to false + if(preprocessedChat != false){ + //Send that shit! + this.relayUserChat(socket, preprocessedChat.message, preprocessedChat.chatType, preprocessedChat.links); + } + //If something fucked up + }catch(err){ + //Bitch and moan + return loggerUtils.socketExceptionHandler(socket, err); } } diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index 0e80206..1414914 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -92,6 +92,18 @@ class pmHandler{ return false; } + //If this is a sesh starter + if(data.msg == '' || data.msg == null){ + //Skip pre-processing and send a cooked message + return this.relayPMObj(new message( + socket.user.user, + recipients, + '', + 'chat', + [] + )); + } + //preprocess message const preprocessedMessage = await this.chatPreprocessor.preprocess(socket, data); From faf72fd7a5a60a0079fc90ca8b774733905bd61a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 2 Oct 2025 08:56:54 -0400 Subject: [PATCH 123/209] Started work on client-side private message post-processing --- www/js/channel/chat.js | 42 +------------------------- www/js/channel/chatPostprocessor.js | 46 +++++++++++++++++++++++++++-- www/js/channel/panels/pmPanel.js | 21 +++++-------- 3 files changed, 53 insertions(+), 56 deletions(-) diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index 0402a4b..b9aa843 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -213,48 +213,8 @@ class chatBox{ * @param {Object} data De-hydrated chat object from server */ displayChat(data){ - //Create chat-entry span - var chatEntry = document.createElement('span'); - chatEntry.classList.add("chat-panel-buffer","chat-entry",`chat-entry-${data.user}`); - - //Create high-level label - var highLevel = document.createElement('p'); - highLevel.classList.add("chat-panel-buffer","chat-entry-high-level","high-level"); - highLevel.textContent = utils.unescapeEntities(`${data.highLevel}`); - chatEntry.appendChild(highLevel); - - //If we're not using classic flair - if(data.flair != "classic"){ - //Use flair - var flair = `flair-${data.flair}`; - //Otherwise - }else{ - //Pull user's assigned color from the color map - var flair = this.client.userList.colorMap.get(data.user); - } - - //Create username label - var userLabel = document.createElement('p'); - userLabel.classList.add("chat-panel-buffer", "chat-entry-username", ); - - //Create color span - var flairSpan = document.createElement('span'); - flairSpan.classList.add("chat-entry-flair-span", flair); - flairSpan.innerHTML = data.user; - - //Inject flair span into user label before the colon - userLabel.innerHTML = `${flairSpan.outerHTML}: `; - - //Append user label - chatEntry.appendChild(userLabel); - - //Create chat body - var chatBody = document.createElement('p'); - chatBody.classList.add("chat-panel-buffer","chat-entry-body"); - chatEntry.appendChild(chatBody); - //Append the post-processed chat-body to the chat buffer - this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data)); + this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(data)); //Set size to aspect on launch this.resizeAspect(); diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 6bc03c0..80d29a7 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -35,13 +35,13 @@ class chatPostprocessor{ * @param {Object} rawData - Raw data from server * @returns {Node} Post-Processed Chat Entry */ - postprocess(chatEntry, rawData){ + postprocess(rawData){ //Create empty array to hold filter spans this.filterSpans = []; //Set raw message data this.rawData = rawData; //Set current chat nodes - this.chatEntry = chatEntry; + this.buildEntry(); this.chatBody = this.chatEntry.querySelector(".chat-entry-body"); //Split the chat message into an array of objects representing each word/chunk @@ -87,6 +87,48 @@ class chatPostprocessor{ return this.chatEntry; } + buildEntry(){ + //Create chat-entry span + this.chatEntry = document.createElement('span'); + this.chatEntry.classList.add("chat-panel-buffer","chat-entry",`chat-entry-${this.rawData.user}`); + + //Create high-level label + var highLevel = document.createElement('p'); + highLevel.classList.add("chat-panel-buffer","chat-entry-high-level","high-level"); + highLevel.textContent = utils.unescapeEntities(`${this.rawData.highLevel}`); + this.chatEntry.appendChild(highLevel); + + //If we're not using classic flair + if(this.rawData.flair != "classic"){ + //Use flair + var flair = `flair-${this.rawData.flair}`; + //Otherwise + }else{ + //Pull user's assigned color from the color map + var flair = this.client.userList.colorMap.get(this.rawData.user); + } + + //Create username label + var userLabel = document.createElement('p'); + userLabel.classList.add("chat-panel-buffer", "chat-entry-username", ); + + //Create color span + var flairSpan = document.createElement('span'); + flairSpan.classList.add("chat-entry-flair-span", flair); + flairSpan.innerHTML = this.rawData.user; + + //Inject flair span into user label before the colon + userLabel.innerHTML = `${flairSpan.outerHTML}: `; + + //Append user label + this.chatEntry.appendChild(userLabel); + + //Create chat body + var chatBody = document.createElement('p'); + chatBody.classList.add("chat-panel-buffer","chat-entry-body"); + this.chatEntry.appendChild(chatBody); + } + /** * Splits message into an array of Word Objects for further processing */ diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 261c694..13d5349 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -223,21 +223,16 @@ class pmPanel extends panelObj{ } } + /** + * Renders message out to PM Panel Message Buffer + * @param {Object} message - Message to render + */ renderMessage(message){ - const msgSpan = document.createElement('span'); + //Run postprocessing functions on chat message + const postprocessedMessage = client.chatBox.chatPostprocessor.postprocess(message); - const msgSender = document.createElement('p'); - msgSender.innerText = utils.unescapeEntities(`${message.sender}:`); - msgSender.classList.add('pm-panel-sesh-message-sender'); - - const msgContent = document.createElement('p'); - msgContent.innerText = utils.unescapeEntities(message.msg); - msgContent.classList.add('pm-panel-sesh-message-content'); - - msgSpan.appendChild(msgSender); - msgSpan.appendChild(msgContent); - - this.seshBuffer.appendChild(msgSpan); + //Append message to buffer + this.seshBuffer.appendChild(postprocessedMessage); } } From 2feea726944aa3b95438777283769b5430443a92 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 3 Oct 2025 08:58:43 -0400 Subject: [PATCH 124/209] Finished up work on advanced formatted private messaging. --- src/app/channel/chat.js | 38 ++++--------------- src/app/chatMetadata.js | 57 +++++++++++++++++++++++++++++ src/app/pm/message.js | 32 +++++----------- src/app/pm/pmHandler.js | 40 +++++++++++++++----- src/server.js | 2 +- www/css/channel.css | 8 ++-- www/css/panel/pm.css | 43 ++++++++++++++++++---- www/css/theme/movie-night.css | 7 ++-- www/js/channel/chatPostprocessor.js | 25 +++++++------ www/js/channel/panels/pmPanel.js | 2 +- www/js/channel/pmHandler.js | 4 +- 11 files changed, 164 insertions(+), 94 deletions(-) create mode 100644 src/app/chatMetadata.js diff --git a/src/app/channel/chat.js b/src/app/channel/chat.js index b26fc17..c9c5853 100644 --- a/src/app/channel/chat.js +++ b/src/app/channel/chat.js @@ -14,49 +14,25 @@ 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 .*/ +//local imports +const chatMetadata = require("../chatMetadata"); + /** * Class representing a single chat message */ -class chat{ +class chat extends chatMetadata{ /** * Instantiates a chat message object * @param {connectedUser} user - User who sent the message - * @param {String} flair - Flair ID String for the flair used to send the message - * @param {Number} highLevel - Number representing current high level - * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers - * @param {String} type - Message Type Identifier, used for client-side processing. - * @param {Array} links - Array of URLs/Links included in the message. */ constructor(user, flair, highLevel, msg, type, links){ + //Call derived constructor + super(flair, highLevel, msg, type, links); + /** * User who sent the message */ this.user = user; - - /** - * Flair ID String for the flair used to send the message - */ - this.flair = flair; - - /** - * Number representing current high level - */ - this.highLevel = highLevel; - - /** - * COntents of the message, with links replaced with numbered file-seperator marks - */ - this.msg = msg; - - /** - * Message Type Identifier, used for client-side processing. - */ - this.type = type; - - /** - * Array of URLs/Links included in the message. - */ - this.links = links; } } diff --git a/src/app/chatMetadata.js b/src/app/chatMetadata.js new file mode 100644 index 0000000..df5fbd6 --- /dev/null +++ b/src/app/chatMetadata.js @@ -0,0 +1,57 @@ +/*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 .*/ + +/** + * Class representing a the metadata of a single message + */ +class chatMetadata{ + /** + * Instantiates a chat metadata object + * @param {String} flair - Flair ID String for the flair used to send the message + * @param {Number} highLevel - Number representing current high level + * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers + * @param {String} type - Message Type Identifier, used for client-side processing. + * @param {Array} links - Array of URLs/Links included in the message. + */ + constructor(flair, highLevel, msg, type, links){ + /** + * Flair ID String for the flair used to send the message + */ + this.flair = flair; + + /** + * Number representing current high level + */ + this.highLevel = highLevel; + + /** + * COntents of the message, with links replaced with numbered file-seperator marks + */ + this.msg = msg; + + /** + * Message Type Identifier, used for client-side processing. + */ + this.type = type; + + /** + * Array of URLs/Links included in the message. + */ + this.links = links; + } +} + +module.exports = chatMetadata; \ No newline at end of file diff --git a/src/app/pm/message.js b/src/app/pm/message.js index 1ccb88d..28dc46f 100644 --- a/src/app/pm/message.js +++ b/src/app/pm/message.js @@ -14,44 +14,30 @@ 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 .*/ +//localImports +const chatMetadata = require("../chatMetadata"); + /** * Class representing a single chat message */ -class message{ +class message extends chatMetadata{ /** - * Instantiates a chat message object - * @param {String} sender - Name of user who sent the message + * @param {String} user - Name of user who sent the message * @param {Array} recipients - Array of usernames who are supposed to receive the message - * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers - * @param {String} type - Message Type Identifier, used for client-side processing. - * @param {Array} links - Array of URLs/Links included in the message. */ - constructor(sender, recipients, msg, type, links){ + constructor(user, recipients, flair, highLevel, msg, type, links){ + //Call derived constructor + super(flair, highLevel, msg, type, links); /** * Name of user who sent the message */ - this.sender = sender; + this.user = user; /** * Array of usernames who are supposed to receive the message */ this.recipients = recipients; - - /** - * Contenst of the messages, with links replaced with numbered file-seperator markers - */ - this.msg = msg; - - /** - * Message Type Identifier, used for client-side processing. - */ - this.type = type; - - /** - * Array of URLs/Links included in the message. - */ - this.links = links; } } diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index 1414914..b783be8 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -30,13 +30,19 @@ class pmHandler{ /** * Instantiates object containing global server-side private message relay logic * @param {Socket.io} io - Socket.io server instanced passed down from server.js + * @param {channelManager} chanServer - Sister channel management server object */ - constructor(io){ + constructor(io, chanServer){ /** * Socket.io server instance passed down from server.js */ this.io = io; + /** + * Sister channel management server object + */ + this.chanServer = chanServer; + /** * Socket.io server namespace for handling messaging */ @@ -107,14 +113,28 @@ class pmHandler{ //preprocess message const preprocessedMessage = await this.chatPreprocessor.preprocess(socket, data); - //Create message object and relay it off to the recipients - this.relayPMObj(new message( - socket.user.user, - recipients, - preprocessedMessage.message, - preprocessedMessage.chatType, - preprocessedMessage.links - )); + //If the send flag wasnt thrown false + if(preprocessedMessage != false){ + //Pull an active user profile from the first channel that gives it in the chan server + const senderProfile = this.chanServer.activeUsers.get(socket.user.user); + + //If user isn't actively connected to a channel + if(senderProfile == null || senderProfile.length == 0){ + //They don't get to send shit lol + return; + } + + //Create message object and relay it off to the recipients + this.relayPMObj(new message( + socket.user.user, + recipients, + senderProfile[0].flair, + senderProfile[0].highLevel, + preprocessedMessage.message, + preprocessedMessage.chatType, + preprocessedMessage.links + )); + } //If something fucked up }catch(err){ @@ -131,7 +151,7 @@ class pmHandler{ } //Acknowledge the sent message - this.namespace.to(msg.sender).emit("sent", msg); + this.namespace.to(msg.user).emit("sent", msg); } /** diff --git a/src/server.js b/src/server.js index 6005d30..091bbe0 100644 --- a/src/server.js +++ b/src/server.js @@ -197,7 +197,7 @@ scheduler.kickoff(); //Hand over general-namespace socket.io connections to the channel manager module.exports.channelManager = new channelManager(io) -module.exports.pmHandler = new pmHandler(io) +module.exports.pmHandler = new pmHandler(io, module.exports.channelManager); //Listen Function webServer.listen(port, () => { diff --git a/www/css/channel.css b/www/css/channel.css index e7644c4..a5fb710 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -162,18 +162,18 @@ p.panel-head-element{ height: 1.5em; } -.chat-entry{ +.chat-entry, .pm-panel-sesh-entry{ display: flex; align-content: center; font-size: 10pt; } -.chat-entry-username{ +.chat-entry-username, .pm-panel-sesh-entry-username{ margin: auto 0.2em auto 0; text-wrap: nowrap; } -.chat-entry-body{ +.chat-entry-body, .pm-panel-sesh-entry-body{ margin: 0.2em; align-content: center; } @@ -182,7 +182,7 @@ p.panel-head-element{ margin: auto; } -.chat-entry-high-level{ +.chat-entry-high-level, .pm-panel-sesh-entry-high-level{ margin: auto 0 auto 0.2em; } diff --git a/www/css/panel/pm.css b/www/css/panel/pm.css index 2e6663c..54a5e9c 100644 --- a/www/css/panel/pm.css +++ b/www/css/panel/pm.css @@ -17,13 +17,14 @@ along with this program. If not, see .*/ display: flex; flex-direction: horizontal; height: 100%; + overflow: hidden; } #pm-panel-sesh-list-container{ flex: 1; max-width: 10em; - width: calc(100% - 1.25em); margin-left: 0.25em; + overflow-y: auto; } #pm-panel-sesh-container{ @@ -32,6 +33,7 @@ along with this program. If not, see .*/ flex: 1; height: calc(100% - 0.25em); margin-top: 0; + margin-right: 0.25em; } #pm-panel-start-sesh{ @@ -49,6 +51,7 @@ along with this program. If not, see .*/ #pm-panel-sesh-buffer{ flex: 1; + overflow-y: auto; } #pm-panel-sesh-control-div{ @@ -72,12 +75,6 @@ div.pm-panel-sesh-list-entry, div.pm-panel-sesh-list-entry p{ text-align: center; } -#pm-panel-sesh-buffer span{ - display: flex; - flex-direction: row; - margin: 0; -} - .pm-panel-sesh-message-sender, .pm-panel-sesh-message-content{ margin: 0; font-size: 10pt; @@ -89,4 +86,36 @@ div.pm-panel-sesh-list-entry, div.pm-panel-sesh-list-entry p{ justify-content: center; text-align: center; height: 100%; +} + +.pm-panel-sesh-entry{ + display: flex; + align-content: center; + font-size: 10pt; +} + +.pm-panel-sesh-entry-username{ + margin: auto 0.2em auto 0; + text-wrap: nowrap; +} + +.pm-panel-sesh-entry-body{ + margin: 0.2em; + align-content: center; +} + +.pm-panel-sesh-entry-high-level{ + margin: auto 0 auto 0.2em; +} + +.high-level{ + z-index: 2; + background-image: url("/img/sweet_leaf_simple.png"); + background-size: 1.3em; + background-repeat: no-repeat; + background-position-x: center; + background-position-y: top; + width: 1.5em; + text-align: center; + flex-shrink: 0; } \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index fa3d836..5ce7db6 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -623,13 +623,12 @@ div.archived p{ border-left: 1px solid var(--accent0); } -#pm-panel-start-sesh{ +#pm-panel-start-sesh, div.pm-panel-sesh-list-entry{ border-bottom: 1px solid var(--accent0); } - -div.pm-panel-sesh-list-entry{ - border-bottom: 1px solid var(--accent0); +span.pm-panel-sesh-entry{ + border-bottom: 1px solid var(--accent1-alt1); } /* altcha theming*/ diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 80d29a7..e6daf7b 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -35,14 +35,13 @@ class chatPostprocessor{ * @param {Object} rawData - Raw data from server * @returns {Node} Post-Processed Chat Entry */ - postprocess(rawData){ + postprocess(rawData, pm = false){ //Create empty array to hold filter spans this.filterSpans = []; //Set raw message data this.rawData = rawData; //Set current chat nodes - this.buildEntry(); - this.chatBody = this.chatEntry.querySelector(".chat-entry-body"); + this.buildEntry(pm); //Split the chat message into an array of objects representing each word/chunk this.splitMessage(); @@ -87,14 +86,16 @@ class chatPostprocessor{ return this.chatEntry; } - buildEntry(){ + buildEntry(pm){ + const classSuffix = pm ? 'pm-panel-sesh' : 'chat'; + const classSuffixAlt = pm ? classSuffix : 'chat-panel'; //Create chat-entry span this.chatEntry = document.createElement('span'); - this.chatEntry.classList.add("chat-panel-buffer","chat-entry",`chat-entry-${this.rawData.user}`); + this.chatEntry.classList.add(`${classSuffixAlt}-buffer`,`${classSuffix}-entry`,`${classSuffix}-entry-${this.rawData.user}`); //Create high-level label var highLevel = document.createElement('p'); - highLevel.classList.add("chat-panel-buffer","chat-entry-high-level","high-level"); + highLevel.classList.add(`${classSuffixAlt}-buffer`,`${classSuffix}-entry-high-level`,"high-level"); highLevel.textContent = utils.unescapeEntities(`${this.rawData.highLevel}`); this.chatEntry.appendChild(highLevel); @@ -110,11 +111,11 @@ class chatPostprocessor{ //Create username label var userLabel = document.createElement('p'); - userLabel.classList.add("chat-panel-buffer", "chat-entry-username", ); + userLabel.classList.add(`${classSuffixAlt}-buffer`, `${classSuffix}-entry-username`, ); //Create color span var flairSpan = document.createElement('span'); - flairSpan.classList.add("chat-entry-flair-span", flair); + flairSpan.classList.add(`${classSuffix}-entry-flair-span`, flair); flairSpan.innerHTML = this.rawData.user; //Inject flair span into user label before the colon @@ -124,9 +125,11 @@ class chatPostprocessor{ this.chatEntry.appendChild(userLabel); //Create chat body - var chatBody = document.createElement('p'); - chatBody.classList.add("chat-panel-buffer","chat-entry-body"); - this.chatEntry.appendChild(chatBody); + this.chatBody = document.createElement('p'); + this.chatBody.classList.add(`${classSuffixAlt}-buffer`,`${classSuffix}-entry-body`); + + //Append chat body to chat entry + this.chatEntry.appendChild(this.chatBody); } /** diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 13d5349..4af334f 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -229,7 +229,7 @@ class pmPanel extends panelObj{ */ renderMessage(message){ //Run postprocessing functions on chat message - const postprocessedMessage = client.chatBox.chatPostprocessor.postprocess(message); + const postprocessedMessage = client.chatBox.chatPostprocessor.postprocess(message, true); //Append message to buffer this.seshBuffer.appendChild(postprocessedMessage); diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index c3562f5..ac5417b 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -112,9 +112,9 @@ class pmHandler{ } //If this wasn't our message - if(message.sender != client.user.user){ + if(message.user != client.user.user){ //Push sender onto members list - recipients.push(message.sender); + recipients.push(message.user); } //Sort recipients From 64f9d713daf0f7b5c87eb3ae61335bb023c62612 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 6 Oct 2025 04:48:17 -0400 Subject: [PATCH 125/209] PMHandler now tracks unread messages and lights pm icon to notify user. --- www/css/theme/movie-night.css | 1 + www/js/channel/panels/pmPanel.js | 31 ++++++++----- www/js/channel/pmHandler.js | 75 ++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 5ce7db6..439002d 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -168,6 +168,7 @@ textarea{ .positive-low{ color: var(--focus0); + text-shadow: var(--focus-glow0-alt0); } .inactive{ diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 4af334f..b908858 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -32,10 +32,23 @@ class pmPanel extends panelObj{ */ this.activeSesh = ""; + /** + * Random UUID to identify the panel with the pmHandler + */ + this.uuid = crypto.randomUUID(); + + //Tell PMHandler to start tracking this panel + this.client.pmHandler.panelList.set(this.uuid, null); + this.defineListeners(); } closer(){ + //Tell PMHandler to start tracking this panel + this.client.pmHandler.panelList.delete(this.uuid); + + //Run derived closer + super.closer(); } docSwitch(){ @@ -146,6 +159,9 @@ class pmPanel extends panelObj{ //Set the first one as active this.activeSesh = seshList[0][1].id; + + //Tell PMHandler what sesh we have open for notification reasons + this.client.pmHandler.readSesh(this.uuid, this.activeSesh); } //For each session tracked by the pmHandler @@ -202,6 +218,9 @@ class pmPanel extends panelObj{ //Set new sesh as active sesh event.target.classList.add('positive'); + //Tell PMHandler what sesh we have open for notification reasons + this.client.pmHandler.readSesh(this.uuid, this.activeSesh); + //Re-render message buffer this.renderMessages(); } @@ -296,18 +315,6 @@ class startSeshPopup{ startSesh(event){ //If we clicked or hit enter if(event.key == null || event.key == "Enter"){ - /* - //Cook a new sesh from - const newSesh = new pmSesh({ - //Split usernames by space - sender: this.client.user.user, - recipients: this.usernamePrompt.value.split(" ") - }); - - //Pop new sesh into pmHandler - this.client.pmHandler.seshList.set(newSesh.id, newSesh); - */ - //Send message out to server this.client.pmSocket.emit("pm", { recipients: this.usernamePrompt.value.split(" "), diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index ac5417b..6553edb 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -38,6 +38,11 @@ class pmHandler{ */ this.seshList = new Map(); + /** + * List of open PM Panels + */ + this.panelList = new Map(); + this.defineListeners(); this.setupInput(); } @@ -66,10 +71,11 @@ class pmHandler{ //Store whether or not current message has been consumed by an existing sesh let consumed = false; - const nameObj = pmHandler.genSeshName(data); + //Store whether or not we have this sesh open in an existing PMPanel + let displayed = false; - //Create members array from scratch to avoid changing the input data for further processing - const members = nameObj.recipients; + //Pull session name from static generation method + const nameObj = pmHandler.genSeshName(data); //For each existing sesh for(const seshEntry of this.seshList){ @@ -81,6 +87,24 @@ class pmHandler{ //Dump collected message into the matching session sesh.messages.push(data); + //Set sesh to unread + sesh.unread = true; + + //For each open PM Panel + for(const panel of this.panelList){ + //If sesh ID matches an open sesh in an existing panel + if(sesh.id == panel[1]){ + //Set sesh to read + sesh.unread = false; + } + } + + //If the message was unread + if(sesh.unread){ + //Notify user of new message/sesh + this.handlePing(); + } + //Add sesh to sesh map this.seshList.set(sesh.id, sesh); @@ -94,11 +118,51 @@ class pmHandler{ //Generate a new sesh const sesh = new pmSesh(data, client); + //Notify user of new message/sesh + this.handlePing(); + //Add it to the sesh list this.seshList.set(sesh.id, sesh); } } + handlePing(){ + //Light up the icon + this.pmIcon.classList.add('positive-low'); + } + + //Handles UI updates after reading all messages + checkAllRead(){ + //For each sesh + for(const sesh of this.seshList){ + //If a sesh is unread + if(sesh.unread){ + //LOOK OUT BOYS, THIS ONE'S BEEN READ! CHEESE IT! + return; + } + } + + //Unlight the icon + this.pmIcon.classList.remove('positive-low'); + } + + readSesh(panelID, seshID){ + //Set current sesh for panel + this.panelList.set(panelID, seshID); + + //Get requested session + const sesh = this.seshList.get(seshID); + + //Set it to unread + sesh.unread = false; + + //Commit sesh back to sesh list + this.seshList.set(sesh.id, sesh); + + //Check if all messages are read and handle em' if they are + this.checkAllRead(); + } + static genSeshName(message){ const recipients = []; @@ -153,6 +217,11 @@ class pmSesh{ */ this.id = nameObj.name; + /** + * Whether or not we have unread messages within the session + */ + this.unread = true; + /** * Array containing all session messages */ From 53fc46adc38cffe1d075edf4ae52da2f9aec8451 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 6 Oct 2025 05:00:35 -0400 Subject: [PATCH 126/209] Started seperating non-free images/audio from mian codebase. --- .gitignore | 4 +++- src/schemas/channel/channelSchema.js | 2 +- src/schemas/user/userSchema.js | 4 ++-- www/img/johnny.png | Bin 15906 -> 0 bytes 4 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 www/img/johnny.png diff --git a/.gitignore b/.gitignore index bd8bfad..6bb63d2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ config.json.old state.json chatexamples.txt server.cert -server.key \ No newline at end of file +server.key +www/nonfree/* +www/nonfree/README.md \ No newline at end of file diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index c3055ca..bbf72c5 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -60,7 +60,7 @@ const channelSchema = new mongoose.Schema({ thumbnail: { type: mongoose.SchemaTypes.String, required: true, - default: "/img/johnny.png" + default: "/nonfree/johnny.png" }, settings: { hidden: { diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 434447c..d456127 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -79,7 +79,7 @@ const userSchema = new mongoose.Schema({ img: { type: mongoose.SchemaTypes.String, required: true, - default: "/img/johnny.png" + default: "/nonfree/johnny.png" }, bio: { type: mongoose.SchemaTypes.String, @@ -318,7 +318,7 @@ userSchema.statics.findProfile = async function(user, includeEmail = false){ date: (await statModel.getStats()).firstLaunch, tokes: await statModel.getTokeCommandCounts(), tokeCount: await statModel.getTokeCount(), - img: "/img/johnny.png", + img: "/nonfree/johnny.png", signature: "!TOKE", bio: "!TOKE OR DIE!" }; diff --git a/www/img/johnny.png b/www/img/johnny.png deleted file mode 100644 index b1335553295db0583e1a1d15b3f411ba62bf0871..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15906 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 Ef^d>MqW}N^ From 976e157cf1db177cf13e135e9aea67fa2167806d Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 6 Oct 2025 19:06:47 -0400 Subject: [PATCH 127/209] Added settings for PM-related audio pings. --- .gitignore | 3 +- src/views/partial/panels/settings.ejs | 21 ++++++++++ www/js/channel/channel.js | 6 ++- www/js/channel/panels/settingsPanel.js | 56 ++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 6bb63d2..928c465 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ state.json chatexamples.txt server.cert server.key -www/nonfree/* -www/nonfree/README.md \ No newline at end of file +www/nonfree/* \ No newline at end of file diff --git a/src/views/partial/panels/settings.ejs b/src/views/partial/panels/settings.ejs index ac3cb93..5fd29d7 100644 --- a/src/views/partial/panels/settings.ejs +++ b/src/views/partial/panels/settings.ejs @@ -45,5 +45,26 @@ along with this program. If not, see . %>

Aspect-Ratio Lock Chat Width Minimum:

+

Notification Settings

+ +

Play Sound for received PMs:

+ +
+ +

Play sound for sent PMs:

+ +
+ +

Play sound on new PM sesh:

+ +
+ +

Play sound on PM sesh end:

+ +
\ No newline at end of file diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 523cf6b..9198ee1 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -313,7 +313,11 @@ class channel{ ["syncDelta", 6], ["chatWidthMin", 20], ["userlistHidden", false], - ["cinemaMode", false] + ["cinemaMode", false], + ["rxPMSound", 'unread'], + ["txPMSound", false], + ["newSeshSound", true], + ["endSeshSound", true] ]); } diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index 976afee..d3949ef 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -62,6 +62,26 @@ class settingsPanel extends panelObj{ */ this.chatWidthMinimum = this.panelDocument.querySelector("#settings-panel-min-chat-width input"); + /** + * Audible Ping on PM Recieved + */ + this.rxPMSound = this.panelDocument.querySelector("#settings-panel-ping-on-pm-rx select"); + + /** + * Audible Ping on PM Transmit + */ + this.txPMSound = this.panelDocument.querySelector("#settings-panel-ping-on-pm-tx input"); + + /** + * Audible Ping on new PM sesh + */ + this.newSeshSound = this.panelDocument.querySelector("#settings-panel-ping-on-new-sesh input"); + + /** + * Audible Ping on old PM sesh + */ + this.endSeshSound = this.panelDocument.querySelector("#settings-panel-ping-on-end-sesh input"); + this.renderSettings(); this.setupInput(); @@ -79,6 +99,10 @@ class settingsPanel extends panelObj{ this.liveSyncTolerance.addEventListener('change', this.updateLiveSyncTolerance.bind(this)); this.syncDelta.addEventListener('change', this.updateSyncDelta.bind(this)); this.chatWidthMinimum.addEventListener('change', this.updateChatWidthMinimum.bind(this)); + this.rxPMSound.addEventListener('change', this.updateRXPMSound.bind(this)); + this.txPMSound.addEventListener('change', this.updateTXPMSound.bind(this)); + this.newSeshSound.addEventListener('change', this.updateNewPMSeshSound.bind(this)); + this.endSeshSound.addEventListener('change', this.updateEndPMSeshSound.bind(this)); } /** @@ -91,6 +115,10 @@ class settingsPanel extends panelObj{ this.liveSyncTolerance.value = localStorage.getItem("liveSyncTolerance"); this.syncDelta.value = localStorage.getItem("syncDelta"); this.chatWidthMinimum.value = localStorage.getItem("chatWidthMin"); + this.rxPMSound.value = localStorage.getItem('rxPMSound'); + this.txPMSound.checked = localStorage.getItem('txPMSound') == 'true'; + this.newSeshSound.checked = localStorage.getItem('newSeshSound') == 'true'; + this.endSeshSound.checked = localStorage.getItem('endSeshSound') == 'true'; } /** @@ -189,4 +217,32 @@ class settingsPanel extends panelObj{ localStorage.setItem("chatWidthMin", this.chatWidthMinimum.value); client.processConfig("chatWidthMin", this.chatWidthMinimum.value); } + + /** + * Handles changes to RX PM Sound setting + */ + updateRXPMSound(){ + localStorage.setItem('rxPMSound', this.rxPMSound.value); + } + + /** + * Handles changes to TX PM Sound setting + */ + updateTXPMSound(){ + localStorage.setItem('txPMSound', this.txPMSound.checked); + } + + /** + * Handles changes to New PM Sesh Sound setting + */ + updateNewPMSeshSound(){ + localStorage.setItem('newSeshSound', this.newSeshSound.checked); + } + + /** + * Handles changes to Old PM Sesh Sound setting + */ + updateEndPMSeshSound(){ + localStorage.setItem('endSeshSound', this.endSeshSound.checked); + } } \ No newline at end of file From e85fb18ce507d64abcaa5c56a175af216a2af251 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 6 Oct 2025 21:21:54 -0400 Subject: [PATCH 128/209] Messages now play notification sounds. --- www/js/channel/panels/pmPanel.js | 16 ++++++++++++++++ www/js/channel/pmHandler.js | 32 ++++++++++++++++++++++++++++++-- www/js/utils.js | 5 +++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index b908858..041ec5c 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -37,6 +37,11 @@ class pmPanel extends panelObj{ */ this.uuid = crypto.randomUUID(); + /** + * PM TX Sound + */ + this.txSound = '/nonfree/imsend.ogg'; + //Tell PMHandler to start tracking this panel this.client.pmHandler.panelList.set(this.uuid, null); @@ -131,6 +136,10 @@ class pmPanel extends panelObj{ //Send message out to server this.client.pmSocket.emit("pm", preprocessedMessage); + + if(localStorage.getItem('txPMSound') == 'true'){ + utils.ux.playSound(this.txSound); + } } //Clear our prompt @@ -247,6 +256,13 @@ class pmPanel extends panelObj{ * @param {Object} message - Message to render */ renderMessage(message){ + //If we have an empty message + console.log(message); + if(message.msg == null || message.msg == ''){ + //BAIL!! + return; + } + //Run postprocessing functions on chat message const postprocessedMessage = client.chatBox.chatPostprocessor.postprocess(message, true); diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index 6553edb..aaa887d 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -43,6 +43,21 @@ class pmHandler{ */ this.panelList = new Map(); + /** + * PM RX Sound + */ + this.rxSound = '/nonfree/imrecv.ogg'; + + /** + * Open Sesh Sound + */ + this.openSeshSound = '/nonfree/opensesh.ogg'; + + /** + * End Sesh Sound + */ + this.endSeshSound = '/nonfree/closesesh.ogg'; + this.defineListeners(); this.setupInput(); } @@ -118,17 +133,30 @@ class pmHandler{ //Generate a new sesh const sesh = new pmSesh(data, client); + //Notify user of new message/sesh - this.handlePing(); + this.handlePing((data.msg == '' || data.msg == null)); //Add it to the sesh list this.seshList.set(sesh.id, sesh); } + + //If this isn't an empty message (sesh-starter), and PM's always make noise, and we didn't send the message + if(data.msg != '' && data.msg != null && localStorage.getItem('rxPMSound') == 'all' && data.user != this.client.user.user){ + //make sum noize! + utils.ux.playSound(this.rxSound); + } } - handlePing(){ + handlePing(newSesh = false){ //Light up the icon this.pmIcon.classList.add('positive-low'); + + if(newSesh && (localStorage.getItem('newSeshSound') == 'true')){ + utils.ux.playSound(this.openSeshSound); + }else if(localStorage.getItem('rxPMSound') == 'unread'){ + utils.ux.playSound(this.rxSound); + } } //Handles UI updates after reading all messages diff --git a/www/js/utils.js b/www/js/utils.js index c8d7508..9f652b1 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -297,6 +297,11 @@ class canopyUXUtils{ } + playSound(url){ + const audio = new Audio(url); + audio.play(); + } + newTableRow(cellContent){ //Create an empty table row to hold the cells const entryRow = document.createElement('tr'); From 64348b848605edde1b015d3d3f0e4966f4f7378d Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 7 Oct 2025 01:00:13 -0400 Subject: [PATCH 129/209] Sesh titles now glow while unread, fixed chat icon un-lighting pre-emptively. --- www/css/theme/movie-night.css | 8 ++++++++ www/js/channel/panels/pmPanel.js | 17 ++++++++--------- www/js/channel/pmHandler.js | 6 +++--- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 439002d..0f517b3 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -166,8 +166,16 @@ textarea{ text-shadow: var(--focus-glow0); } +.positive-inverse{ + color: var(--focus0); + text-shadow: var(--focus-glow0-alt0); +} + .positive-low{ color: var(--focus0); +} + +.positive-afterglow{ text-shadow: var(--focus-glow0-alt0); } diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 041ec5c..92e44ef 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -106,14 +106,8 @@ class pmPanel extends panelObj{ //Render out the newest message this.renderMessage(data); }else{ - //pull current session entry if it exists - const curEntry = this.panelDocument.querySelector(`[data-id="${nameObj.name}"]`); - - //If it doesn't exist - if(curEntry == null){ - //Re-render out the sesh list - this.renderSeshList(); - } + //Re-render out the sesh list + this.renderSeshList(); } } @@ -194,6 +188,9 @@ class pmPanel extends panelObj{ if(sesh.id == this.activeSesh){ //mark it as such entryDiv.classList.add('positive'); + //If it contains something unread + }else if(sesh.unread){ + entryDiv.classList.add('positive-afterglow'); } //Create sesh label @@ -232,6 +229,9 @@ class pmPanel extends panelObj{ //Re-render message buffer this.renderMessages(); + + //Re-Render Sesh List + this.renderSeshList(); } renderMessages(){ @@ -257,7 +257,6 @@ class pmPanel extends panelObj{ */ renderMessage(message){ //If we have an empty message - console.log(message); if(message.msg == null || message.msg == ''){ //BAIL!! return; diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js index aaa887d..3ff3c5b 100644 --- a/www/js/channel/pmHandler.js +++ b/www/js/channel/pmHandler.js @@ -150,7 +150,7 @@ class pmHandler{ handlePing(newSesh = false){ //Light up the icon - this.pmIcon.classList.add('positive-low'); + this.pmIcon.classList.add('positive-inverse'); if(newSesh && (localStorage.getItem('newSeshSound') == 'true')){ utils.ux.playSound(this.openSeshSound); @@ -164,14 +164,14 @@ class pmHandler{ //For each sesh for(const sesh of this.seshList){ //If a sesh is unread - if(sesh.unread){ + if(sesh[1].unread){ //LOOK OUT BOYS, THIS ONE'S BEEN READ! CHEESE IT! return; } } //Unlight the icon - this.pmIcon.classList.remove('positive-low'); + this.pmIcon.classList.remove('positive-inverse'); } readSesh(panelID, seshID){ From 3d2b40b3c8334377850d180cb66b1eccbce1c191 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 7 Oct 2025 01:14:08 -0400 Subject: [PATCH 130/209] Upgraded IP-Hashing Algorithm to SHA-512 --- src/utils/hashUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index 52616c6..082cd73 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -52,7 +52,7 @@ module.exports.comparePassword = function(pass, hash){ */ module.exports.hashIP = function(ip){ //Create hash object - const hashObj = crypto.createHash('md5'); + const hashObj = crypto.createHash('sha512'); //add IP and salt to the hash hashObj.update(`${ip}${config.ipSecret}`); From 4698ba4122c3cc2e1e6592b8a295e7f9bc7b696f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 7 Oct 2025 03:17:16 -0400 Subject: [PATCH 131/209] Added auto-scrolling to private message panel. Fixed auto-scrolling w/ laggy assets in chat and PM. --- www/js/channel/chat.js | 10 +++ www/js/channel/panels/pmPanel.js | 118 +++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index b9aa843..e733bdf 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -145,6 +145,12 @@ class chatBox{ */ this.showChatIcon = document.querySelector("#media-panel-show-chat-icon"); + /** + * re-occuring auto-scroll call + * cope for the fact that postprocessedMessage objects in renderMessage aren't throwing load events + */ + this.scrollInterval = setInterval(this.handleAutoScroll.bind(this), 200); + //Setup functions this.setupInput(); this.defineListeners(); @@ -567,11 +573,15 @@ class chatBox{ const bufferHeight = Math.round(bufferRect.height); const bufferWidth = Math.round(bufferRect.width); + //If last height was unset if(this.lastHeight == 0){ + //Set it based on buffer Height this.lastHeight = bufferHeight; } + //if last width is unset if(this.lastWidth == 0){ + //Set it based on buffer width this.lastWidth = bufferWidth; } diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 92e44ef..7751d59 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -42,6 +42,32 @@ class pmPanel extends panelObj{ */ this.txSound = '/nonfree/imsend.ogg'; + /** + * Message Buffer Scroll Top on last scroll + */ + this.lastPos = 0; + + /** + * Height of Message Buffer on last scroll + */ + this.lastHeight = 0; + + /** + * Width of Message Buffer on last scroll + */ + this.lastWidth = 0; + + /** + * Whether or not auto-scroll is enabled + */ + this.autoScroll = true; + + /** + * re-occuring auto-scroll call + * cope for the fact that postprocessedMessage objects in renderMessage aren't throwing load events + */ + this.scrollInterval = setInterval(this.handleAutoScroll.bind(this), 200); + //Tell PMHandler to start tracking this panel this.client.pmHandler.panelList.set(this.uuid, null); @@ -49,20 +75,29 @@ class pmPanel extends panelObj{ } closer(){ - //Tell PMHandler to start tracking this panel + //Tell PMHandler to stop tracking this panel this.client.pmHandler.panelList.delete(this.uuid); + //Clear the scroll interval + clearInterval(this.scrollInterval); + //Run derived closer super.closer(); } - docSwitch(){ + async docSwitch(){ + //Call derived method + super.docSwitch(); + this.startSeshButton = this.panelDocument.querySelector('#pm-panel-start-sesh'); this.seshList = this.panelDocument.querySelector('#pm-panel-sesh-list'); this.seshBuffer = this.panelDocument.querySelector('#pm-panel-sesh-buffer'); this.seshPrompt = this.panelDocument.querySelector('#pm-panel-message-prompt'); this.seshSendButton = this.panelDocument.querySelector('#pm-panel-send-button'); + //reset auto-scroll + this.autoScroll = true; + this.setupInput(); this.renderSeshList(); @@ -72,9 +107,6 @@ class pmPanel extends panelObj{ //Render messages this.renderMessages(); } - - //Call derived method - super.docSwitch(); } /** @@ -92,6 +124,9 @@ class pmPanel extends panelObj{ this.startSeshButton.addEventListener('click', this.startSesh.bind(this)); this.seshPrompt.addEventListener("keydown", this.send.bind(this)); this.seshSendButton.addEventListener("click", this.send.bind(this)); + this.seshBuffer.addEventListener('scroll', this.scrollHandler.bind(this)); + this.ownerDoc.defaultView.addEventListener('resize', this.handleAutoScroll.bind(this)); + } startSesh(event){ @@ -227,6 +262,9 @@ class pmPanel extends panelObj{ //Tell PMHandler what sesh we have open for notification reasons this.client.pmHandler.readSesh(this.uuid, this.activeSesh); + //Reset auto scroll to scroll newly selected sesh down to the bottom + this.autoScroll = true; + //Re-render message buffer this.renderMessages(); @@ -255,7 +293,7 @@ class pmPanel extends panelObj{ * Renders message out to PM Panel Message Buffer * @param {Object} message - Message to render */ - renderMessage(message){ + async renderMessage(message){ //If we have an empty message if(message.msg == null || message.msg == ''){ //BAIL!! @@ -267,6 +305,74 @@ class pmPanel extends panelObj{ //Append message to buffer this.seshBuffer.appendChild(postprocessedMessage); + + //Auto-scroll buffer on content load + this.handleAutoScroll(); + } + + /** + * Handles scrolling within the message buffer + * @param {Event} event - Event passed down from Event Handler + */ + scrollHandler(event){ + //If we're just starting out + if(this.lastPos == 0){ + //Set last pos for the first time + this.lastPos = this.seshBuffer.scrollTop; + } + + //Calculate scroll delta + const deltaY = this.seshBuffer.scrollTop - this.lastPos; + + //Grab visible bounding rect so we don't have to do it again (can't use offset because someone might zoom in :P) + const bufferRect = this.seshBuffer.getBoundingClientRect(); + const bufferHeight = Math.round(bufferRect.height); + const bufferWidth = Math.round(bufferRect.width); + + //If last height was unset + if(this.lastHeight == 0){ + //Set it based on buffer Height + this.lastHeight = bufferHeight; + } + + //if last width is unset + if(this.lastWidth == 0){ + //Set it based on buffer width + this.lastWidth = bufferWidth; + } + + //If we're scrolling up + if(deltaY < 0){ + //If we have room to scroll, and we didn't resize + if(this.seshBuffer.scrollHeight > bufferHeight && (this.lastWidth == bufferWidth && this.lastHeight == bufferHeight)){ + //Disable auto scrolling + this.autoScroll = false; + //We probably resized + }else{ + this.handleAutoScroll(); + } + //Otherwise if the difference between the message buffers scroll height and offset height is equal to the scroll top + //(Because it is scrolled all the way down) + }else if((this.seshBuffer.scrollHeight - bufferHeight) == this.seshBuffer.scrollTop){ + this.autoScroll = true; + } + + //Set last post/size for next the run + this.lastPos = this.seshBuffer.scrollTop; + this.lastHeight = bufferHeight; + this.lastWidth = bufferWidth; + } + + /** + // * Auto-scrolls sesh chat buffer when new chats are entered. + */ + handleAutoScroll(){ + //If autoscroll is enabled + if(this.autoScroll){ + console.log("SCROLLME"); + //Set seshBuffer scrollTop to the difference between scrollHeight and buffer height (scroll to the bottom) + this.seshBuffer.scrollTop = this.seshBuffer.scrollHeight - Math.round(this.seshBuffer.getBoundingClientRect().height); + } } } From 9fda308306178860b0d224a5d58de31f9351a568 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 9 Oct 2025 03:50:05 -0400 Subject: [PATCH 132/209] Started work on legacy account migration. --- .gitignore | 3 +- config.example.json | 1 + config.example.jsonc | 4 + src/schemas/user/migrationSchema.js | 190 ++++++++++++++++++++++++++++ src/server.js | 4 + 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/schemas/user/migrationSchema.js diff --git a/.gitignore b/.gitignore index 928c465..9868f22 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ state.json chatexamples.txt server.cert server.key -www/nonfree/* \ No newline at end of file +www/nonfree/* +migration/* \ No newline at end of file diff --git a/config.example.json b/config.example.json index 22c388c..372a5ac 100644 --- a/config.example.json +++ b/config.example.json @@ -9,6 +9,7 @@ "sessionSecret": "CHANGE_ME", "altchaSecret": "CHANGE_ME", "ipSecret": "CHANGE_ME", + "migrate": false, "ssl":{ "cert": "./server.cert", "key": "./server.key" diff --git a/config.example.jsonc b/config.example.jsonc index 544e906..fea8c16 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -24,6 +24,10 @@ "altchaSecret": "CHANGE_ME", //IP Secret used to salt IP Hashes "ipSecret": "CHANGE_ME", + //Enable to migrate legacy DB and toke files dumped into the ./migration/ directory + //WARNING: The migration folder is cleared after server boot, whether or not a migration took place or this option is enabled. + //Keep your backups in a safe place, preferably a machine that DOESN'T have open inbound ports exposed to the internet/a publically accessible reverse proxy! + "migrate": false, //SSL cert and key locations "ssl":{ "cert": "./server.cert", diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js new file mode 100644 index 0000000..6c4bad9 --- /dev/null +++ b/src/schemas/user/migrationSchema.js @@ -0,0 +1,190 @@ +/*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 .*/ + +//Node Imports +const fs = require('node:fs/promises'); + +//NPM Imports +const {mongoose} = require('mongoose'); + +//local imports +const config = require('../../../config.json'); +const {userModel} = require('../user/userSchema'); +const permissionModel = require('../permissionSchema'); + + +/** + * DB Schema for documents representing legacy fore.st migration data for a single user account + */ +const migrationSchema = new mongoose.Schema({ + user:{ + type: mongoose.SchemaTypes.String, + unique: true, + required: true + }, + pass: { + type: mongoose.SchemaTypes.String, + required: true + }, + rank: { + type: mongoose.SchemaTypes.Number, + required: true + }, + email: { + type: mongoose.SchemaTypes.String, + default: '' + }, + bio: { + type: mongoose.SchemaTypes.String, + default: 'Bio not set!' + }, + image: { + type: mongoose.SchemaTypes.String, + default: "/nonfree/johnny.png" + }, + date: { + type: mongoose.SchemaTypes.Date, + required: true + }, + tokes: { + type: mongoose.SchemaTypes.Map, + default: new Map(), + required: true + }, +}); + +//statics +/** + * Static method for ingesting data dump from legacy cytube/fore.st server + */ +migrationSchema.statics.ingestLegacyDump = async function(){ + //If migration is disabled + if(!config.migrate){ + //BAIL! + return; + } + + //Crash directory + const dir = "./migration/" + const userDump = `${dir}users.sql` + + //Double check migration files + try{ + //Pull dump stats + await fs.stat(userDump); + //If we caught an error (most likely it's missing) + }catch(err){ + //BAIL! + return; + } + + //Pull raw dump from file + const rawDump = await fs.readFile(userDump, 'binary'); + + //Split dump by line + const splitDump = rawDump.split('\n'); + + //For each line in the user dump + for(const line of splitDump){ + //Ingest the legacy user profile + this.ingestLegacyUser(line); + } +} + +/** + * Ingests a single line containing a single profile out of an .sql data dump from a legacy cytube/fore.st server + * @param {String} rawProfile - Line of text contianing raw profile dump + */ +migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ + //If migration is disabled + if(!config.migrate){ + //BAIL! + return; + } + + //Filter out the entry from any extra guff on the line + const profileMatches = rawProfile.match(/^\((.*?(?=,),){9}.*?(?=\))\)/g); + + //If we have an invalid line + if(profileMatches <= 0){ + //BAIL! + return; + } + + //Set filtered profile to the match we found + let filteredProfile = profileMatches[0]; + + //cook the filtered profile in order to trick the JSON interpreter into thinking it's an array + filteredProfile = `[${filteredProfile.substring(1, filteredProfile.length - 1)}]`; + + //Replace single qoutes with double to match JSON strings + filteredProfile = filteredProfile.replaceAll(",'",',"'); + filteredProfile = filteredProfile.replaceAll("',",'",'); + + //Make sure single qoutes are escaped + filteredProfile = filteredProfile.replaceAll("\'",'\\\''); + + + //Dupe the JSON interpreter like the rube that it is + const profileArray = JSON.parse(filteredProfile); + + //If profile array is the wrong length + if(profileArray.length != 10){ + //BAIL! + return; + } + + //Look for user in migration table + const foundMigration = await this.findOne({user:profileArray[1]}); + const foundUser = await userModel.findOne({user: profileArray[1]}); + + //If we found the user in the database + if(foundMigration != null || foundUser != null){ + //Scream + console.log(`Found legacy user ${profileArray[1]} in database, skipping migration!`); + //BAIL! + return; + } + + + //Create migration profile object from scraped info + const migrationProfile = new this({ + user: profileArray[1], + pass: profileArray[2], + //Clamp rank to 0 and the max setting allowed by the rank enum + rank: Math.min(Math.max(0, profileArray[3]), permissionModel.rankEnum.length - 1), + email: profileArray[4], + date: profileArray[7], + }) + + //If our profile array isn't empty + if(profileArray[5] != ''){ + //Make sure single qoutes are escaped, and parse bio JSON + const bioObject = JSON.parse(profileArray[5].replaceAll("\'",'\\\'')); + + //Inject bio information into migration profile, only if they're present; + migrationProfile.bio = bioObject.text == '' ? undefined : bioObject.text; + migrationProfile.image = bioObject.image == '' ? undefined : bioObject.image; + } + + //Build DB Doc from migration Profile hashtable and dump it into the DB + await this.create(migrationProfile); + + //Let the world know of our triumph! + console.log(`Legacy user profile ${migrationProfile.user} migrated successfully!`); +} + +module.exports = mongoose.model("migration", migrationSchema); \ No newline at end of file diff --git a/src/server.js b/src/server.js index 091bbe0..8a13955 100644 --- a/src/server.js +++ b/src/server.js @@ -43,6 +43,7 @@ const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); const emoteModel = require('./schemas/emoteSchema'); const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema'); +const migrationModel = require('./schemas/user/migrationSchema'); //Controller const fileNotFoundController = require('./controllers/404Controller'); //Router @@ -192,6 +193,9 @@ emoteModel.loadDefaults(); //Load default toke commands tokeCommandModel.loadDefaults(); +//Run legacy migration +migrationModel.ingestLegacyDump(); + //Kick off scheduled-jobs scheduler.kickoff(); From ad0dd6bdbba66cac23075bdafde11fb1e001c385 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 9 Oct 2025 18:02:04 -0400 Subject: [PATCH 133/209] Added better error checking to migration schema statics. --- src/schemas/user/migrationSchema.js | 223 +++++++++++++++------------- 1 file changed, 120 insertions(+), 103 deletions(-) diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 6c4bad9..aab54bb 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -24,6 +24,7 @@ const {mongoose} = require('mongoose'); const config = require('../../../config.json'); const {userModel} = require('../user/userSchema'); const permissionModel = require('../permissionSchema'); +const loggerUtils = require('../../utils/loggerUtils'); /** @@ -66,41 +67,49 @@ const migrationSchema = new mongoose.Schema({ }, }); +//TODO: before next commit, add error checking to the ingestLegacy statics down below +//Also add a warning for the fail condition in ingestLegacyDump that bails out when missing files + //statics /** * Static method for ingesting data dump from legacy cytube/fore.st server */ migrationSchema.statics.ingestLegacyDump = async function(){ - //If migration is disabled - if(!config.migrate){ - //BAIL! - return; - } - - //Crash directory - const dir = "./migration/" - const userDump = `${dir}users.sql` - - //Double check migration files try{ - //Pull dump stats - await fs.stat(userDump); - //If we caught an error (most likely it's missing) + //If migration is disabled + if(!config.migrate){ + //BAIL! + return; + } + + //Crash directory + const dir = "./migration/" + const userDump = `${dir}users.sql` + + //Double check migration files + try{ + //Pull dump stats + await fs.stat(userDump); + //If we caught an error (most likely it's missing) + }catch(err){ + loggerUtils.consoleWarn("No migration files detected! Pleas provide legacy migration files or disable migration from config.json!"); + //BAIL! + return; + } + + //Pull raw dump from file + const rawDump = await fs.readFile(userDump, 'binary'); + + //Split dump by line + const splitDump = rawDump.split('\n'); + + //For each line in the user dump + for(const line of splitDump){ + //Ingest the legacy user profile + this.ingestLegacyUser(line); + } }catch(err){ - //BAIL! - return; - } - - //Pull raw dump from file - const rawDump = await fs.readFile(userDump, 'binary'); - - //Split dump by line - const splitDump = rawDump.split('\n'); - - //For each line in the user dump - for(const line of splitDump){ - //Ingest the legacy user profile - this.ingestLegacyUser(line); + return loggerUtils.localExceptionHandler(err); } } @@ -109,82 +118,90 @@ migrationSchema.statics.ingestLegacyDump = async function(){ * @param {String} rawProfile - Line of text contianing raw profile dump */ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ - //If migration is disabled - if(!config.migrate){ - //BAIL! - return; + try{ + //If migration is disabled + if(!config.migrate){ + //BAIL! + return; + } + + //Filter out the entry from any extra guff on the line + const profileMatches = rawProfile.match(/^\((.*?(?=,),){9}.*?(?=\))\)/g); + + //If we have an invalid line + if(profileMatches <= 0){ + loggerUtils.consoleWarn('Bad profile detected in legacy dump:'); + loggerUtils.consoleWarn(rawProfile); + //BAIL! + return; + } + + //Set filtered profile to the match we found + let filteredProfile = profileMatches[0]; + + //cook the filtered profile in order to trick the JSON interpreter into thinking it's an array + filteredProfile = `[${filteredProfile.substring(1, filteredProfile.length - 1)}]`; + + //Replace single qoutes with double to match JSON strings + filteredProfile = filteredProfile.replaceAll(",'",',"'); + filteredProfile = filteredProfile.replaceAll("',",'",'); + + //Make sure single qoutes are escaped + filteredProfile = filteredProfile.replaceAll("\'",'\\\''); + + + //Dupe the JSON interpreter like the rube that it is + const profileArray = JSON.parse(filteredProfile); + + //If profile array is the wrong length + if(profileArray.length != 10){ + loggerUtils.consoleWarn('Bad profile detected in legacy dump:'); + loggerUtils.consoleWarn(profileArray); + //BAIL! + return; + } + + //Look for user in migration table + const foundMigration = await this.findOne({user:profileArray[1]}); + const foundUser = await userModel.findOne({user: profileArray[1]}); + + //If we found the user in the database + if(foundMigration != null || foundUser != null){ + //Scream + loggerUtils.consoleWarn(`Found legacy user ${profileArray[1]} in database, skipping migration!`); + //BAIL! + return; + } + + + //Create migration profile object from scraped info + const migrationProfile = new this({ + user: profileArray[1], + pass: profileArray[2], + //Clamp rank to 0 and the max setting allowed by the rank enum + rank: Math.min(Math.max(0, profileArray[3]), permissionModel.rankEnum.length - 1), + email: profileArray[4], + date: profileArray[7], + }) + + //If our profile array isn't empty + if(profileArray[5] != ''){ + //Make sure single qoutes are escaped, and parse bio JSON + const bioObject = JSON.parse(profileArray[5].replaceAll("\'",'\\\'')); + + //Inject bio information into migration profile, only if they're present; + migrationProfile.bio = bioObject.text == '' ? undefined : bioObject.text; + migrationProfile.image = bioObject.image == '' ? undefined : bioObject.image; + } + + //Build DB Doc from migration Profile hashtable and dump it into the DB + await this.create(migrationProfile); + + //Let the world know of our triumph! + console.log(`Legacy user profile ${migrationProfile.user} migrated successfully!`); + }catch(err){ + return loggerUtils.localExceptionHandler(err); } - - //Filter out the entry from any extra guff on the line - const profileMatches = rawProfile.match(/^\((.*?(?=,),){9}.*?(?=\))\)/g); - - //If we have an invalid line - if(profileMatches <= 0){ - //BAIL! - return; - } - - //Set filtered profile to the match we found - let filteredProfile = profileMatches[0]; - - //cook the filtered profile in order to trick the JSON interpreter into thinking it's an array - filteredProfile = `[${filteredProfile.substring(1, filteredProfile.length - 1)}]`; - - //Replace single qoutes with double to match JSON strings - filteredProfile = filteredProfile.replaceAll(",'",',"'); - filteredProfile = filteredProfile.replaceAll("',",'",'); - - //Make sure single qoutes are escaped - filteredProfile = filteredProfile.replaceAll("\'",'\\\''); - - - //Dupe the JSON interpreter like the rube that it is - const profileArray = JSON.parse(filteredProfile); - - //If profile array is the wrong length - if(profileArray.length != 10){ - //BAIL! - return; - } - - //Look for user in migration table - const foundMigration = await this.findOne({user:profileArray[1]}); - const foundUser = await userModel.findOne({user: profileArray[1]}); - - //If we found the user in the database - if(foundMigration != null || foundUser != null){ - //Scream - console.log(`Found legacy user ${profileArray[1]} in database, skipping migration!`); - //BAIL! - return; - } - - - //Create migration profile object from scraped info - const migrationProfile = new this({ - user: profileArray[1], - pass: profileArray[2], - //Clamp rank to 0 and the max setting allowed by the rank enum - rank: Math.min(Math.max(0, profileArray[3]), permissionModel.rankEnum.length - 1), - email: profileArray[4], - date: profileArray[7], - }) - - //If our profile array isn't empty - if(profileArray[5] != ''){ - //Make sure single qoutes are escaped, and parse bio JSON - const bioObject = JSON.parse(profileArray[5].replaceAll("\'",'\\\'')); - - //Inject bio information into migration profile, only if they're present; - migrationProfile.bio = bioObject.text == '' ? undefined : bioObject.text; - migrationProfile.image = bioObject.image == '' ? undefined : bioObject.image; - } - - //Build DB Doc from migration Profile hashtable and dump it into the DB - await this.create(migrationProfile); - - //Let the world know of our triumph! - console.log(`Legacy user profile ${migrationProfile.user} migrated successfully!`); } module.exports = mongoose.model("migration", migrationSchema); \ No newline at end of file From bb2a1369a3c41c7d3e42f135b010c07b09a763fe Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 10 Oct 2025 08:42:02 -0400 Subject: [PATCH 134/209] Added profile toke count ingestion to migration schema. --- src/schemas/user/migrationSchema.js | 97 ++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index aab54bb..c32b46d 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -61,10 +61,9 @@ const migrationSchema = new mongoose.Schema({ required: true }, tokes: { - type: mongoose.SchemaTypes.Map, - default: new Map(), - required: true - }, + type: mongoose.SchemaTypes.Number, + default: 0, + } }); //TODO: before next commit, add error checking to the ingestLegacy statics down below @@ -82,20 +81,28 @@ migrationSchema.statics.ingestLegacyDump = async function(){ return; } - //Crash directory + //Migration directories/file const dir = "./migration/" const userDump = `${dir}users.sql` + const tokeDir = `./migration/tokebot/` + + //Create array to hold list of toke dump files + let tokeDumps = []; //Double check migration files try{ //Pull dump stats await fs.stat(userDump); + + //Pull toke related files + tokeDumps = await fs.readdir(tokeDir) + //If we caught an error (most likely it's missing) }catch(err){ loggerUtils.consoleWarn("No migration files detected! Pleas provide legacy migration files or disable migration from config.json!"); //BAIL! return; - } + } //Pull raw dump from file const rawDump = await fs.readFile(userDump, 'binary'); @@ -106,8 +113,34 @@ migrationSchema.statics.ingestLegacyDump = async function(){ //For each line in the user dump for(const line of splitDump){ //Ingest the legacy user profile - this.ingestLegacyUser(line); + //Waiting on this is a lot less effecient... + //But I'm too lazy to write a while loop that waits on every promise to return gracefully to make something that will run like once preform better. + await this.ingestLegacyUser(line); } + + + //Create arrays to hold toke dumps contents + const tokeMaps = []; + const tokeLogs = []; + + //For every toke related file + for(const file of tokeDumps){ + //Read toke related file + const rawContents = await fs.readFile(`${tokeDir}${file}`, 'binary'); + + //If its a toke file containing a list of toke counts per profile + if(file.match(/\_tokefile/) != null){ + //Push raw toke map into toke maps array + tokeMaps.push(rawContents); + //If its a toke log containing a list of tokes + }else if(file.match(/\_toke\.log/)){ + //Push file contents into toke log array + tokeLogs.push(rawContents); + } + } + + //Ingest toke maps + this.ingestTokeMaps(tokeMaps); }catch(err){ return loggerUtils.localExceptionHandler(err); } @@ -168,7 +201,7 @@ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ //If we found the user in the database if(foundMigration != null || foundUser != null){ //Scream - loggerUtils.consoleWarn(`Found legacy user ${profileArray[1]} in database, skipping migration!`); + //loggerUtils.consoleWarn(`Found legacy user ${profileArray[1]} in database, skipping migration!`); //BAIL! return; } @@ -204,4 +237,52 @@ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ } } +/** + * Ingests array of raw toke map data ripped from the migrations folder and injects it on-top of the existing migration profile collection in the DB + * @param {Array} rawTokeMaps - List of raw content ripped from legacy cytube/fore.st toke files + */ +migrationSchema.statics.ingestTokeMaps = async function(rawTokeMaps){ + try{ + //If server migration is disabled + if(!config.migrate){ + //BAIL!! + return; + } + + //Create new map to hold total toke count + const tokeMap = new Map(); + + //For each raw map handed to us by the main ingestion method + for(const rawMap of rawTokeMaps){ + //Parse map into dehydrated map array + const dehydratedMap = JSON.parse(rawMap); + + //We don't need to re-hydrate a map we're just going to fucking iterate through like an array... + for(const curCount of dehydratedMap.value){ + //Get current toke count for user + const total = tokeMap.get(curCount[0]); + + //If this user isn't counted + if(total == null || total == 0){ + //Set users toke count to parsed count + tokeMap.set(curCount[0], curCount[1]); + //Otherwise + }else{ + //Add parsed count to users total + tokeMap.set(curCount[0], curCount[1] + total); + } + } + } + + //For each toking user + for(const toker of tokeMap){ + //Update migration profile to include total tokes + await this.updateOne({user: toker[0]},{$set:{tokes: toker[1]}}); + console.log(`${toker[1]} tokes injected into user profile ${toker[0]}!`); + } + }catch(err){ + return loggerUtils.localExceptionHandler(err); + } +} + module.exports = mongoose.model("migration", migrationSchema); \ No newline at end of file From a231c8fc4c8b2848f196235a397e389fe6e63d0e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 11 Oct 2025 08:19:53 -0400 Subject: [PATCH 135/209] Started server-wide legacy cytube/fore.st toke count ingestion. --- config.example.json | 1 + config.example.jsonc | 4 + src/schemas/statSchema.js | 98 ++++++++++++++++++++++++- src/schemas/user/migrationSchema.js | 9 ++- src/views/partial/profile/tokeCount.ejs | 2 +- 5 files changed, 110 insertions(+), 4 deletions(-) diff --git a/config.example.json b/config.example.json index 372a5ac..5e3eb01 100644 --- a/config.example.json +++ b/config.example.json @@ -10,6 +10,7 @@ "altchaSecret": "CHANGE_ME", "ipSecret": "CHANGE_ME", "migrate": false, + "dropLegacyTokes": false, "ssl":{ "cert": "./server.cert", "key": "./server.key" diff --git a/config.example.jsonc b/config.example.jsonc index fea8c16..01eeab1 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -28,6 +28,10 @@ //WARNING: The migration folder is cleared after server boot, whether or not a migration took place or this option is enabled. //Keep your backups in a safe place, preferably a machine that DOESN'T have open inbound ports exposed to the internet/a publically accessible reverse proxy! "migrate": false, + //Drops all legacy tokes out of the statistics file since doing so manually from mongosh is a lot of typing out obnoxious parentha-glyphics. + //Requires migration to be disabled before it takes effect. + //WARNING: this does NOT affect user toke counts, migrated or otherwise. Use carefully! + "dropLegacyTokes": false, //SSL cert and key locations "ssl":{ "cert": "./server.cert", diff --git a/src/schemas/statSchema.js b/src/schemas/statSchema.js index f0a2c3f..f615703 100644 --- a/src/schemas/statSchema.js +++ b/src/schemas/statSchema.js @@ -187,7 +187,7 @@ statSchema.statics.getTokeCommandCounts = async function(){ count.set(command, 1); }else{ //Set it to ++curCount - count.set(command, ++curCount); + count.set(command, curCount + 1); } }); }); @@ -196,4 +196,100 @@ statSchema.statics.getTokeCommandCounts = async function(){ return count; } +/** + * Ingests legacy tokes handed over by the migration model + * @param {Array} rawLegacyTokes - List of strings containing contents of legacy cytube/fore.st toke logs + */ +statSchema.statics.ingestLegacyTokes = async function(rawLegacyTokes){ + //If migration is disabled + if(!config.migrate){ + //BAIL! + return; + } + + try{ + const statDB = await this.getStats(); + + //For each toke log + for(const tokeLog of rawLegacyTokes){ + //Split and iterate toke log by new line + for(const tokeLine of tokeLog.split('\n')){ + //Ensure line is a valid toke log line (this will break if your tokes take place after 12:46:40PM on Nov 20th 2286... Or before 21:46:40 Sep 08 2001) + //You'll probably want to have migrated from cytube/fore.st to canopy by then :) + //Also splits tokers array off for easier processing + const splitToke = tokeLine.match(/^\[.+\]|,[0-9]{1,4},|[0-9]{13}$/g) + if(splitToke != null){ + + //Create empty tokers map + const toke = new Map(); + + //Add qoutes around strings in the tokers line + let tokersLine = splitToke[0].replaceAll('[', '["'); + tokersLine = tokersLine.replaceAll(']','"]'); + tokersLine = tokersLine.replaceAll(',','","'); + + //Force feed doctored line into the JSON parser, and iterate by the array it shits out + for(const toker of JSON.parse(tokersLine)){ + toke.set(toker,"Legacy Tokes"); + } + + const date = new Date(Number(splitToke[2])); + + //Push toke on to statDB + statDB.tokes.push({ + toke, + date + }); + + console.log(`Adding legacy toke: ${tokersLine} from: ${date.toLocaleString()}`); + } + } + } + + //Save toke to file + await statDB.save(); + + console.log("Legacy tokes commited to server-wide database statistics file!"); + }catch(err){ + return loggerutils.localexceptionhandler(err); + } +} + +statSchema.statics.dropLegacyTokes = async function(){ + try{ + //If legacy toke dropping is disabled or migration is enabled + if(!config.dropLegacyTokes || config.migrate){ + //return + return; + } + + //pull stat doc + const statDB = await this.getStats(); + + //Create temporary toke array + const tokes = []; + + //Iterate through server toke history + for(const toke of statDB.tokes){ + //If it's not a legacy toke + if(Array.from(toke.toke)[0][1] != "Legacy Tokes"){ + //Add it to the temp array + tokes.push(toke); + } + } + + //Replace the server-wide toke log with our newly doctored one + statDB.tokes = tokes; + + //Save the stat document + statDB.save(); + + //Tell of our success + console.log("Removed migration tokes!"); + }catch(err){ + return loggerutils.localexceptionhandler(err); + } + +} + module.exports = mongoose.model("statistics", statSchema); \ No newline at end of file diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index c32b46d..1be7b73 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -24,6 +24,7 @@ const {mongoose} = require('mongoose'); const config = require('../../../config.json'); const {userModel} = require('../user/userSchema'); const permissionModel = require('../permissionSchema'); +const statModel = require('../statSchema'); const loggerUtils = require('../../utils/loggerUtils'); @@ -77,6 +78,7 @@ migrationSchema.statics.ingestLegacyDump = async function(){ try{ //If migration is disabled if(!config.migrate){ + statModel.dropLegacyTokes(); //BAIL! return; } @@ -140,7 +142,10 @@ migrationSchema.statics.ingestLegacyDump = async function(){ } //Ingest toke maps - this.ingestTokeMaps(tokeMaps); + await this.ingestTokeMaps(tokeMaps); + + //Pass toke logs over to the stat model for further ingestion + await statModel.ingestLegacyTokes(tokeLogs); }catch(err){ return loggerUtils.localExceptionHandler(err); } @@ -281,7 +286,7 @@ migrationSchema.statics.ingestTokeMaps = async function(rawTokeMaps){ console.log(`${toker[1]} tokes injected into user profile ${toker[0]}!`); } }catch(err){ - return loggerUtils.localExceptionHandler(err); + return loggerutils.localexceptionhandler(err); } } diff --git a/src/views/partial/profile/tokeCount.ejs b/src/views/partial/profile/tokeCount.ejs index d9a2094..9c1ab81 100644 --- a/src/views/partial/profile/tokeCount.ejs +++ b/src/views/partial/profile/tokeCount.ejs @@ -19,6 +19,6 @@ along with this program. If not, see . %>
<% profile.tokes.forEach((count, toke) => { %> -

!<%- toke %>: <%- count %>

+

<%- toke == "Legacy Tokes" ? '
' : '!' %><%- toke %>: <%- count %>

<% }); %>
\ No newline at end of file From e9b95394778552f01e772507ec4c1c0afbfa07a1 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 11 Oct 2025 09:38:44 -0400 Subject: [PATCH 136/209] Added completion time to migration procedure. --- src/schemas/user/migrationSchema.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 1be7b73..8ae54f7 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -146,6 +146,8 @@ migrationSchema.statics.ingestLegacyDump = async function(){ //Pass toke logs over to the stat model for further ingestion await statModel.ingestLegacyTokes(tokeLogs); + + loggerUtils.consoleWarn(`Legacy Server Migration Completed at: ${new Date().toLocaleString()}`); }catch(err){ return loggerUtils.localExceptionHandler(err); } From 42ad17072f81641180e8b541e78b7ed3e68c43d1 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 11 Oct 2025 09:43:26 -0400 Subject: [PATCH 137/209] Refactored userSchema.statics.findProfile()'s tokebot profile generation procedure. --- src/schemas/statSchema.js | 78 ++++++++++++++-------------------- src/schemas/user/userSchema.js | 9 ++-- www/nonfree | 1 + 3 files changed, 40 insertions(+), 48 deletions(-) create mode 160000 www/nonfree diff --git a/src/schemas/statSchema.js b/src/schemas/statSchema.js index f615703..ab737a0 100644 --- a/src/schemas/statSchema.js +++ b/src/schemas/statSchema.js @@ -152,50 +152,6 @@ statSchema.statics.tattooToke = async function(toke){ await stats.save(); } -/** - * Gets toke count from statistics document - * @returns {Number} Number of tokes across the entire site - */ -statSchema.statics.getTokeCount = async function(){ - //get stats doc - const stats = await this.getStats(); - - //return toke count - return stats.tokes.length; -} - -/** - * Gets toke counts for each individual callout in a map - * @returns {Map} Map of toke counts for each individual callout registered to the server - */ -statSchema.statics.getTokeCommandCounts = async function(){ - //get stats doc - const stats = await this.getStats() - //Create empty map to hold toke command counts - const count = new Map(); - - //for each toke - stats.tokes.forEach((toke) => { - //For each toke command called in the current toke - toke.toke.forEach((command) => { - //Get the current count for the current command - var curCount = count.get(command); - - //if the current count is null - if(curCount == null){ - //Set it to one - count.set(command, 1); - }else{ - //Set it to ++curCount - count.set(command, curCount + 1); - } - }); - }); - - //return the toke command count - return count; -} - /** * Ingests legacy tokes handed over by the migration model * @param {Array} rawLegacyTokes - List of strings containing contents of legacy cytube/fore.st toke logs @@ -282,7 +238,7 @@ statSchema.statics.dropLegacyTokes = async function(){ statDB.tokes = tokes; //Save the stat document - statDB.save(); + await statDB.save(); //Tell of our success console.log("Removed migration tokes!"); @@ -292,4 +248,36 @@ statSchema.statics.dropLegacyTokes = async function(){ } +//Methods + +/** + * Gets toke counts for each individual callout in a map + * @returns {Map} Map of toke counts for each individual callout registered to the server + */ +statSchema.methods.calculateTokeCommandCounts = async function(){ + //Create empty map to hold toke command counts + const count = new Map(); + + //for each toke + this.tokes.forEach((toke) => { + //For each toke command called in the current toke + toke.toke.forEach((command) => { + //Get the current count for the current command + var curCount = count.get(command); + + //if the current count is null + if(curCount == null){ + //Set it to one + count.set(command, 1); + }else{ + //Set it to ++curCount + count.set(command, curCount + 1); + } + }); + }); + + //return the toke command count + return count; +} + module.exports = mongoose.model("statistics", statSchema); \ No newline at end of file diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index d456127..bf7af99 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -311,13 +311,16 @@ userSchema.statics.findProfile = async function(user, includeEmail = false){ return null; //If someone's looking for tokebot }else if(user.user.toLowerCase() == "tokebot"){ + //Pull statistics document from the database + const statDB = await statModel.getStats(); + //fake a profile hashtable for tokebot const profile = { id: -420, user: "Tokebot", - date: (await statModel.getStats()).firstLaunch, - tokes: await statModel.getTokeCommandCounts(), - tokeCount: await statModel.getTokeCount(), + date: statDB.firstLaunch, + tokes: await statDB.calculateTokeCommandCounts(), + tokeCount: statDB.tokes.length, img: "/nonfree/johnny.png", signature: "!TOKE", bio: "!TOKE OR DIE!" diff --git a/www/nonfree b/www/nonfree new file mode 160000 index 0000000..8f3f78b --- /dev/null +++ b/www/nonfree @@ -0,0 +1 @@ +Subproject commit 8f3f78be454a156aa7b6a9a811cd656cf4bd80b2 From a1b602efdb32f22d827130cac208d933d6d3ad66 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 12 Oct 2025 23:48:45 -0400 Subject: [PATCH 138/209] Moved toke log to it's own DB collection w/ cached statistics. Tokebot statistics page-load time decreased by up to 20-30x --- src/app/channel/tokebot.js | 6 +- src/schemas/statSchema.js | 168 ++---------------- src/schemas/tokebot/tokeSchema.js | 219 ++++++++++++++++++++++++ src/schemas/user/migrationSchema.js | 6 +- src/schemas/user/userSchema.js | 11 +- src/server.js | 53 ++++-- src/views/partial/profile/tokeCount.ejs | 7 +- 7 files changed, 281 insertions(+), 189 deletions(-) create mode 100644 src/schemas/tokebot/tokeSchema.js diff --git a/src/app/channel/tokebot.js b/src/app/channel/tokebot.js index 7d1f005..6e41471 100644 --- a/src/app/channel/tokebot.js +++ b/src/app/channel/tokebot.js @@ -16,8 +16,8 @@ along with this program. If not, see .*/ //Local Imports const tokeCommandModel = require('../../schemas/tokebot/tokeCommandSchema'); +const tokeModel = require('../../schemas/tokebot/tokeSchema'); const {userModel} = require('../../schemas/user/userSchema'); -const statSchema = require('../../schemas/statSchema'); /** @@ -190,8 +190,8 @@ class tokebot{ //Asynchronously tattoo the toke into the users documents within the database so that tokebot doesn't have to wait or worry about DB transactions userModel.tattooToke(this.tokers); - //Do the same for the global stat schema - statSchema.tattooToke(this.tokers); + //Do the same for the global toke statistics collection + tokeModel.tattooToke(this.tokers); //Set the toke cooldown this.cooldownCounter = this.cooldownTime; diff --git a/src/schemas/statSchema.js b/src/schemas/statSchema.js index ab737a0..d1af85e 100644 --- a/src/schemas/statSchema.js +++ b/src/schemas/statSchema.js @@ -44,22 +44,16 @@ const statSchema = new mongoose.Schema({ type: mongoose.SchemaTypes.Date, required: true, default: new Date() - }, - tokes: [{ - toke: { - type: mongoose.SchemaTypes.Map, - required: true, - default: new Map() - }, - date: { - type: mongoose.SchemaTypes.Date, - required: true, - default: new Date() - } - }] + } }); //statics + +/** + * Set placeholder variable to hold cached firstLaunch date from stat document + */ +statSchema.statics.firstLaunch = null; + /** * Get's servers sole stat document from the DB * @returns {Mongoose.Document} Server's sole statistics document @@ -67,7 +61,7 @@ const statSchema = new mongoose.Schema({ statSchema.statics.getStats = async function(){ //Get the first document we find var stats = await this.findOne({}); - + if(stats){ //If we found something then the statistics document exist and this is it, //So long as no one else has fucked with the database it should be the only one. (is this forshadowing for a future bug?) @@ -96,6 +90,9 @@ statSchema.statics.incrementLaunchCount = async function(){ stats.launchCount++; stats.save(); + //Cache first launch + this.firstLaunch = stats.firstLaunch; + //print bootup message to console. console.log(`${config.instanceName}(Powered by Canopy) initialized. This server has booted ${stats.launchCount} time${stats.launchCount == 1 ? '' : 's'}.`) console.log(`First booted on ${stats.firstLaunch}.`); @@ -137,147 +134,4 @@ statSchema.statics.incrementChannelCount = async function(){ return oldCount; } -/** - * Tattoo's toke to the server statistics document - * @param {Map} toke - Tokemap handed down from Tokebot - */ -statSchema.statics.tattooToke = async function(toke){ - //Get the statistics document - const stats = await this.getStats(); - - //Add the toke to the stat document - stats.tokes.push({toke}); - - //Save the stat document - await stats.save(); -} - -/** - * Ingests legacy tokes handed over by the migration model - * @param {Array} rawLegacyTokes - List of strings containing contents of legacy cytube/fore.st toke logs - */ -statSchema.statics.ingestLegacyTokes = async function(rawLegacyTokes){ - //If migration is disabled - if(!config.migrate){ - //BAIL! - return; - } - - try{ - const statDB = await this.getStats(); - - //For each toke log - for(const tokeLog of rawLegacyTokes){ - //Split and iterate toke log by new line - for(const tokeLine of tokeLog.split('\n')){ - //Ensure line is a valid toke log line (this will break if your tokes take place after 12:46:40PM on Nov 20th 2286... Or before 21:46:40 Sep 08 2001) - //You'll probably want to have migrated from cytube/fore.st to canopy by then :) - //Also splits tokers array off for easier processing - const splitToke = tokeLine.match(/^\[.+\]|,[0-9]{1,4},|[0-9]{13}$/g) - if(splitToke != null){ - - //Create empty tokers map - const toke = new Map(); - - //Add qoutes around strings in the tokers line - let tokersLine = splitToke[0].replaceAll('[', '["'); - tokersLine = tokersLine.replaceAll(']','"]'); - tokersLine = tokersLine.replaceAll(',','","'); - - //Force feed doctored line into the JSON parser, and iterate by the array it shits out - for(const toker of JSON.parse(tokersLine)){ - toke.set(toker,"Legacy Tokes"); - } - - const date = new Date(Number(splitToke[2])); - - //Push toke on to statDB - statDB.tokes.push({ - toke, - date - }); - - console.log(`Adding legacy toke: ${tokersLine} from: ${date.toLocaleString()}`); - } - } - } - - //Save toke to file - await statDB.save(); - - console.log("Legacy tokes commited to server-wide database statistics file!"); - }catch(err){ - return loggerutils.localexceptionhandler(err); - } -} - -statSchema.statics.dropLegacyTokes = async function(){ - try{ - //If legacy toke dropping is disabled or migration is enabled - if(!config.dropLegacyTokes || config.migrate){ - //return - return; - } - - //pull stat doc - const statDB = await this.getStats(); - - //Create temporary toke array - const tokes = []; - - //Iterate through server toke history - for(const toke of statDB.tokes){ - //If it's not a legacy toke - if(Array.from(toke.toke)[0][1] != "Legacy Tokes"){ - //Add it to the temp array - tokes.push(toke); - } - } - - //Replace the server-wide toke log with our newly doctored one - statDB.tokes = tokes; - - //Save the stat document - await statDB.save(); - - //Tell of our success - console.log("Removed migration tokes!"); - }catch(err){ - return loggerutils.localexceptionhandler(err); - } - -} - -//Methods - -/** - * Gets toke counts for each individual callout in a map - * @returns {Map} Map of toke counts for each individual callout registered to the server - */ -statSchema.methods.calculateTokeCommandCounts = async function(){ - //Create empty map to hold toke command counts - const count = new Map(); - - //for each toke - this.tokes.forEach((toke) => { - //For each toke command called in the current toke - toke.toke.forEach((command) => { - //Get the current count for the current command - var curCount = count.get(command); - - //if the current count is null - if(curCount == null){ - //Set it to one - count.set(command, 1); - }else{ - //Set it to ++curCount - count.set(command, curCount + 1); - } - }); - }); - - //return the toke command count - return count; -} - module.exports = mongoose.model("statistics", statSchema); \ No newline at end of file diff --git a/src/schemas/tokebot/tokeSchema.js b/src/schemas/tokebot/tokeSchema.js new file mode 100644 index 0000000..ca186af --- /dev/null +++ b/src/schemas/tokebot/tokeSchema.js @@ -0,0 +1,219 @@ +/*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 {mongoose} = require('mongoose'); + +//Local Imports +const config = require('./../../../config.json'); +const loggerUtils = require('../../utils/loggerUtils'); + +/** + * DB Schema for single document for keeping track of a single toke + */ +const tokeSchema = new mongoose.Schema({ + toke: { + type: mongoose.SchemaTypes.Map, + required: true, + default: new Map() + }, + date: { + type: mongoose.SchemaTypes.Date, + required: true, + default: new Date() + } +}); + +//statics +/** + * Cached map containing counts of individual toke commands + */ +tokeSchema.statics.tokeMap = new Map(); + +/** + * Cached number of total tokes + */ +tokeSchema.statics.count = 0; + +/** + * Cached number of times a user has successfully ran a '!toke' command + * Not to be confused with tokeSchema.statics.count, which counts total amount of tokes called out + */ +tokeSchema.statics.commandCount = 0; + +/** + * Calculates cached toke map from existing + */ +tokeSchema.statics.calculateTokeMap = async function(){ + //Pull full toke collection + const tokes = await this.find(); + + //Drop existing toke map + this.tokeMap = new Map(); + + //Iterate through DB of tokes + for(const toke of tokes){ + //Increment toke count + this.count++; + + //For each command callout + for(const command of toke.toke){ + //Increment Command Count + this.commandCount++; + + //Pull current count of respective toke command + let curCount = this.tokeMap.get(command[1]); + + //If this is an unset toke command + if(curCount == null){ + //Set it to one + this.tokeMap.set(command[1], 1); + //Otherwise + }else{ + //Increment the existing count + this.tokeMap.set(command[1], ++curCount); + } + } + } + + //Display calculated toke sats for funsies + if(config.verbose){ + console.log(`Processed ${this.commandCount} toke command callouts accross ${await this.estimatedDocumentCount()} tokes.`); + } +} + +/** + * Tattoos toke into toke DB colleciton + * + * We use this instead of a pre-save function as we need to fuck w/ statics + */ +tokeSchema.statics.tattooToke = async function(toke){ + //Write toke to DB + await this.create({toke}); + + //Increment RAM-backed toke count + this.count++; + + //Iterate through tokers + for(const curToke of toke){ + //Pull current toke count + let curCount = this.tokeMap.get(curToke[1]); + + //If this command hasn't been counted + if(curCount == null){ + //Set new command count to one + this.tokeMap.set(curToke[1], 1); + }else{ + //Increment current toke count and commit it to the RAM-backed tokeMap + this.tokeMap.set(curToke[1], ++curCount); + } + + //Increment RAM-Backed command count + this.commandCount++; + } +} + +/** + * Ingests legacy tokes handed over by the migration model + * @param {Array} rawLegacyTokes - List of strings containing contents of legacy cytube/fore.st toke logs + */ +tokeSchema.statics.ingestLegacyTokes = async function(rawLegacyTokes){ + //If migration is disabled + if(!config.migrate){ + //BAIL! + return; + } + + try{ + //For each toke log + for(const tokeLog of rawLegacyTokes){ + //Split and iterate toke log by new line + for(const tokeLine of tokeLog.split('\n')){ + //Ensure line is a valid toke log line (this will break if your tokes take place after 12:46:40PM on Nov 20th 2286... Or before 21:46:40 Sep 08 2001) + //You'll probably want to have migrated from cytube/fore.st to canopy by then :) + //Also splits tokers array off for easier processing + const splitToke = tokeLine.match(/^\[.+\]|,[0-9]{1,4},|[0-9]{13}$/g) + if(splitToke != null){ + + //Create empty tokers map + const toke = new Map(); + + //Add qoutes around strings in the tokers line + let tokersLine = splitToke[0].replaceAll('[', '["'); + tokersLine = tokersLine.replaceAll(']','"]'); + tokersLine = tokersLine.replaceAll(',','","'); + + //Force feed doctored line into the JSON parser, and iterate by the array it shits out + for(const toker of JSON.parse(tokersLine)){ + toke.set(toker,"Legacy Tokes"); + } + + const date = new Date(Number(splitToke[2])); + + //Push toke on to statDB + this.create({ + toke, + date + }); + + console.log(`Adding legacy toke: ${tokersLine} from: ${date.toLocaleString()}`); + } + } + } + + console.log("Legacy tokes commited to server-wide database statistics file!"); + }catch(err){ + return loggerUtils.localExceptionHandler(err); + } +} + +tokeSchema.statics.dropLegacyTokes = async function(){ + try{ + //If legacy toke dropping is disabled or migration is enabled + if(!config.dropLegacyTokes || config.migrate){ + //return + return; + } + //Pull tokes from DB + const oldTokes = await this.find(); + + //Create temporary toke array + const tokes = []; + + //Nuke the toke collection + await this.deleteMany({}); + + //Iterate through server toke history + for(const toke of oldTokes){ + //If it's not a legacy toke or a dupe + if(Array.from(toke.toke)[0][1] != "Legacy Tokes"){ + //Re-add it to the database, scraping out the old ID + this.create({ + toke: toke.toke, + date: toke.date + }); + } + } + + //Tell of our success + console.log("Removed migration tokes!"); + }catch(err){ + return loggerUtils.localExceptionHandler(err); + } + +} + +module.exports = mongoose.model("toke", tokeSchema); \ No newline at end of file diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 8ae54f7..b03b1dc 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -24,7 +24,7 @@ const {mongoose} = require('mongoose'); const config = require('../../../config.json'); const {userModel} = require('../user/userSchema'); const permissionModel = require('../permissionSchema'); -const statModel = require('../statSchema'); +const tokeModel = require('../tokebot/tokeSchema'); const loggerUtils = require('../../utils/loggerUtils'); @@ -78,7 +78,7 @@ migrationSchema.statics.ingestLegacyDump = async function(){ try{ //If migration is disabled if(!config.migrate){ - statModel.dropLegacyTokes(); + await tokeModel.dropLegacyTokes(); //BAIL! return; } @@ -145,7 +145,7 @@ migrationSchema.statics.ingestLegacyDump = async function(){ await this.ingestTokeMaps(tokeMaps); //Pass toke logs over to the stat model for further ingestion - await statModel.ingestLegacyTokes(tokeLogs); + await tokeModel.ingestLegacyTokes(tokeLogs); loggerUtils.consoleWarn(`Legacy Server Migration Completed at: ${new Date().toLocaleString()}`); }catch(err){ diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index bf7af99..dd13b24 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -22,6 +22,7 @@ const {mongoose} = require('mongoose'); const server = require('../../server'); //DB Models const statModel = require('../statSchema'); +const tokeModel = require('../tokebot/tokeSchema'); const flairModel = require('../flairSchema'); const permissionModel = require('../permissionSchema'); const emoteModel = require('../emoteSchema'); @@ -311,16 +312,14 @@ userSchema.statics.findProfile = async function(user, includeEmail = false){ return null; //If someone's looking for tokebot }else if(user.user.toLowerCase() == "tokebot"){ - //Pull statistics document from the database - const statDB = await statModel.getStats(); - //fake a profile hashtable for tokebot const profile = { id: -420, user: "Tokebot", - date: statDB.firstLaunch, - tokes: await statDB.calculateTokeCommandCounts(), - tokeCount: statDB.tokes.length, + //Look ma, no DB calls! + date: statModel.firstLaunch, + tokes: tokeModel.tokeMap, + tokeCount: tokeModel.count, img: "/nonfree/johnny.png", signature: "!TOKE", bio: "!TOKE OR DIE!" diff --git a/src/server.js b/src/server.js index 8a13955..c1b2e2f 100644 --- a/src/server.js +++ b/src/server.js @@ -42,6 +42,7 @@ const {errorMiddleware} = require('./utils/loggerUtils'); const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); const emoteModel = require('./schemas/emoteSchema'); +const tokeModel = require('./schemas/tokebot/tokeSchema'); const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema'); const migrationModel = require('./schemas/user/migrationSchema'); //Controller @@ -181,29 +182,43 @@ app.use(errorMiddleware); //Basic 404 handler app.use(fileNotFoundController); -//Increment launch counter -statModel.incrementLaunchCount(); +asyncKickStart(); -//Load default flairs -flairModel.loadDefaults(); +/*Asyncronous Kickstarter function +Allows us to force server startup to wait on the DB to be ready. +Might be better if she kicked off everything at once, and ran a while loop to check when they where all done. +This runs once at server startup, and most startups will run fairly quickly so... Not worth it?*/ +async function asyncKickStart(){ + //Lettum fuckin' know wassup + console.log(`${config.instanceName}(Powered by Canopy) is booting up!`); -//Load default emotes -emoteModel.loadDefaults(); + //Run legacy migration + await migrationModel.ingestLegacyDump(); -//Load default toke commands -tokeCommandModel.loadDefaults(); + //Calculate Toke Map + await tokeModel.calculateTokeMap(); -//Run legacy migration -migrationModel.ingestLegacyDump(); + //Load default toke commands + await tokeCommandModel.loadDefaults(); -//Kick off scheduled-jobs -scheduler.kickoff(); + //Load default flairs + await flairModel.loadDefaults(); -//Hand over general-namespace socket.io connections to the channel manager -module.exports.channelManager = new channelManager(io) -module.exports.pmHandler = new pmHandler(io, module.exports.channelManager); + //Load default emotes + await emoteModel.loadDefaults(); -//Listen Function -webServer.listen(port, () => { - console.log(`Opening port ${port}`); -}); \ No newline at end of file + //Kick off scheduled-jobs + scheduler.kickoff(); + + //Increment launch counter + await statModel.incrementLaunchCount(); + + //Hand over general-namespace socket.io connections to the channel manager + module.exports.channelManager = new channelManager(io) + module.exports.pmHandler = new pmHandler(io, module.exports.channelManager); + + //Listen Function + webServer.listen(port, () => { + console.log(`Tokes up on port ${port}!`); + }); +} \ No newline at end of file diff --git a/src/views/partial/profile/tokeCount.ejs b/src/views/partial/profile/tokeCount.ejs index 9c1ab81..ccf9ac5 100644 --- a/src/views/partial/profile/tokeCount.ejs +++ b/src/views/partial/profile/tokeCount.ejs @@ -19,6 +19,11 @@ along with this program. If not, see . %>
<% profile.tokes.forEach((count, toke) => { %> -

<%- toke == "Legacy Tokes" ? '
' : '!' %><%- toke %>: <%- count %>

+ <% if(toke != "Legacy Tokes"){ %> +

!<%- toke %>: <%- count %>

+ <% } %> <% }); %> + <% if(profile.tokes.get("Legacy Tokes") != null){ %> +


Legacy Tokes: <%- profile.tokes.get("Legacy Tokes") %>

+ <% } %>
\ No newline at end of file From da9428205f2a0fdd2e21a7e81a14dc62c0380255 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 14 Oct 2025 02:10:34 -0400 Subject: [PATCH 139/209] Prevented users from signing up with an email/username which was already present in the migration database. --- .../account/emailChangeRequestController.js | 17 +++++++-- src/schemas/user/migrationSchema.js | 30 ++++++++++++++-- src/schemas/user/userSchema.js | 36 +++++++++++++++++-- src/server.js | 3 ++ 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/account/emailChangeRequestController.js b/src/controllers/api/account/emailChangeRequestController.js index 1b82d74..4791ba2 100644 --- a/src/controllers/api/account/emailChangeRequestController.js +++ b/src/controllers/api/account/emailChangeRequestController.js @@ -43,7 +43,7 @@ module.exports.post = async function(req, res){ //Check to make sure the user is logged in if(req.session.user == null){ - errorHandler(res, "Invalid user!"); + return errorHandler(res, "Invalid user!"); } //Authenticate and find user model from DB @@ -51,11 +51,22 @@ module.exports.post = async function(req, res){ //If we have an invalid user if(userDB == null){ - errorHandler(res, "Invalid user!"); + return errorHandler(res, "Invalid user!"); } if(userDB.email == email){ - errorHandler(res, "Cannot set current email!"); + return errorHandler(res, "Cannot set current email!"); + } + + + //Look through DB and migration cache for existing email + const existingDB = await userModel.findOne({email: new RegExp(email, 'i')}); + const needsMigration = userModel.migrationCache.emails.includes(email.toLowerCase()); + + //If the email is in use + if(existingDB != null || needsMigration){ + //Complain + return errorHandler(res, "Email already in use!"); } //Generate the password reset link diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index b03b1dc..2856094 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -27,7 +27,6 @@ const permissionModel = require('../permissionSchema'); const tokeModel = require('../tokebot/tokeSchema'); const loggerUtils = require('../../utils/loggerUtils'); - /** * DB Schema for documents representing legacy fore.st migration data for a single user account */ @@ -208,7 +207,7 @@ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ //If we found the user in the database if(foundMigration != null || foundUser != null){ //Scream - //loggerUtils.consoleWarn(`Found legacy user ${profileArray[1]} in database, skipping migration!`); + loggerUtils.consoleWarn(`Found legacy user ${profileArray[1]} in database, skipping migration!`); //BAIL! return; } @@ -292,4 +291,31 @@ migrationSchema.statics.ingestTokeMaps = async function(rawTokeMaps){ } } +migrationSchema.statics.buildMigrationCache = async function(){ + //Pull all profiles from the Legacy Profile Migration DB collection + const legacyProfiles = await this.find(); + + //For each profile in the migration collection + for(const profile of legacyProfiles){ + //Push the username into the migration cache + userModel.migrationCache.users.push(profile.user.toLowerCase()); + //If the profile has an email address set + if(profile.email != null && profile.email != ''){ + //Add the email to the migration cache + userModel.migrationCache.emails.push(profile.email.toLowerCase()); + } + } +} + +//Methods +/** + * Consumes a migration profile and creates a new, modern canopy profile from the original. + * @param {String} oldPass - Original password to authenticate migration against + * @param {String} newPass - New password to re-hash with modern hashing algo + * @param {String} confirmPass - Confirmation for the new pass + */ +migrationSchema.methods.consume = async function(oldPass, newPass, confirmPass){ + +} + module.exports = mongoose.model("migration", migrationSchema); \ No newline at end of file diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index dd13b24..003f2e6 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -226,6 +226,18 @@ userSchema.post('deleteOne', {document: true}, async function (){ }); //statics +/** + * Holds cache of usernames of profiles stored in the Legacy Profile Migration collection + * + * We can't directly reference migrationSchema, as it would cause a circular reference + * To deal with this, migration schema caches it's regestered users into this array on startup. + * Bonus pts for improved performance on registration calls + */ +userSchema.statics.migrationCache = { + users: [], + emails: [] +}; + /** * Registers a new user account with given information * @param {Object} userObj - Object representing user to register, generated by the client @@ -237,11 +249,31 @@ userSchema.statics.register = async function(userObj, ip){ //Check password confirmation matches if(pass == passConfirm){ + //Setup user query + let userQuery = {user: new RegExp(user, 'i')}; + + //If we have an email + if(email != null && email != ""){ + userQuery = {$or: [ + userQuery, + {email: new RegExp(email, 'i')} + ]}; + } + //Look for a user (case insensitive) - var userDB = await this.findOne({user: new RegExp(user, 'i')}); + var userDB = await this.findOne(userQuery); + + //Look for a legacy profile + let needsMigration = this.migrationCache.users.includes(user.toLowerCase()); + + //If the email isn't null and we didnt hit a migration username + if(email != null && !needsMigration){ + //Check for migration email + needsMigration = this.migrationCache.emails.includes(email.toLowerCase()); + } //If the user is found or someones trying to impersonate tokeboi - if(userDB || user.toLowerCase() == "tokebot"){ + if(userDB || needsMigration || user.toLowerCase() == "tokebot"){ throw loggerUtils.exceptionSmith("User name/email already taken!", "validation"); }else{ //Increment the user count, pulling the id to tattoo to the user diff --git a/src/server.js b/src/server.js index c1b2e2f..333dfc3 100644 --- a/src/server.js +++ b/src/server.js @@ -195,6 +195,9 @@ async function asyncKickStart(){ //Run legacy migration await migrationModel.ingestLegacyDump(); + //Build migration cache + await migrationModel.buildMigrationCache(); + //Calculate Toke Map await tokeModel.calculateTokeMap(); From 6cbb72676488533a530086991dfd9c61594c7c30 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 05:25:08 -0400 Subject: [PATCH 140/209] Implemented Profile Migration Backend --- .../api/account/migrationController.js | 96 +++++++++++++++++++ src/routers/api/accountRouter.js | 23 ++++- src/schemas/user/migrationSchema.js | 55 ++++++++++- src/schemas/user/userSchema.js | 2 +- src/utils/csrfUtils.js | 3 - src/utils/hashUtils.js | 12 ++- src/utils/mailUtils.js | 36 +++++-- www/js/utils.js | 18 ++++ 8 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 src/controllers/api/account/migrationController.js diff --git a/src/controllers/api/account/migrationController.js b/src/controllers/api/account/migrationController.js new file mode 100644 index 0000000..61762ed --- /dev/null +++ b/src/controllers/api/account/migrationController.js @@ -0,0 +1,96 @@ +/*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 .*/ + +//Config +const config = require('../../../../config.json'); + +//NPM Imports +const {validationResult, matchedData} = require('express-validator'); + +//local imports +const userBanModel = require('../../../schemas/user/userBanSchema'); +const altchaUtils = require('../../../utils/altchaUtils'); +const migrationModel = require('../../../schemas/user/migrationSchema'); +const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); + +module.exports.post = async function(req, res){ + try{ + //Check for validation errors + const validResult = validationResult(req); + + //If there are none + if(validResult.isEmpty()){ + //Get sanatized/validated data + const migration = matchedData(req); + //Verify Altcha Payload + const verified = await altchaUtils.verify(req.body.verification); + + //If altcha verification failed + if(!verified){ + return errorHandler(res, 'Altcha verification failed, Please refresh the page!', 'unauthorized'); + } + + //If we're proxied use passthrough IP + const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; + + //Look for ban by IP + const ipBanDB = await userBanModel.checkBanByIP(ip); + + //If this ip is randy bobandy + if(ipBanDB != null){ + //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P + const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration(); + let banMsg = []; + + //If the ban is permanent + if(ipBanDB.permanent){ + //tell it to fuck off + //Make the code and message look pretty (kinda) at the same time + banMsg = [ + 'The IP address you are trying to migrate an account from has been permanently banned.', + 'Your cleartext IP has been saved to the database.', + `Any accounts associated will be nuked in ${expiration} day(s).`, + 'If you beleive this to be an error feel free to reach out to your server administrator.', + 'Otherwise, fuck off :)' + ]; + }else{ + //tell it to fuck off + //Make the code and message look pretty (kinda) at the same time + banMsg = [ + 'The IP address you are trying to migrate an account from has been temporarily banned.', + `Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`, + 'If you beleive this to be an error feel free to reach out to your server administrator.', + 'Otherwise, fuck off :)' + ]; + } + + //tell it to fuck off + return errorHandler(res, banMsg.join('
'), 'unauthorized'); + } + + //Find and consume migration document + await migrationModel.consumeByUsername(ip, migration); + + //tell of our success + return res.sendStatus(200); + }else{ + 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/accountRouter.js b/src/routers/api/accountRouter.js index 95aafd5..5975453 100644 --- a/src/routers/api/accountRouter.js +++ b/src/routers/api/accountRouter.js @@ -22,6 +22,7 @@ const accountValidator = require("../../validators/accountValidator"); const loginController = require("../../controllers/api/account/loginController"); const logoutController = require("../../controllers/api/account/logoutController"); const registerController = require("../../controllers/api/account/registerController"); +const migrationController = require("../../controllers/api/account/migrationController"); const updateController = require("../../controllers/api/account/updateController"); const rankEnumController = require("../../controllers/api/account/rankEnumController"); const passwordResetRequestController = require("../../controllers/api/account/passwordResetRequestController"); @@ -38,18 +39,32 @@ router.post('/login', accountValidator.user(), accountValidator.pass(), loginCon //logout router.post('/logout', logoutController.post); //register -router.post('/register', accountValidator.user(), +router.post('/register', + accountValidator.user(), accountValidator.securePass(), accountValidator.pass('passConfirm'), - accountValidator.email(), registerController.post); + accountValidator.email(), + registerController.post); + +//migrate legacy profile +router.post('/migrate', + accountValidator.user(), + accountValidator.pass('oldPass'), + accountValidator.securePass('newPass'), + accountValidator.pass('passConfirm'), + migrationController.post); + //update profile -router.post('/update', accountValidator.img(), +router.post('/update', + accountValidator.img(), accountValidator.bio(), accountValidator.signature(), accountValidator.pronouns(), accountValidator.pass('passChange.oldPass'), accountValidator.securePass('passChange.newPass'), - accountValidator.pass('passChange.confirmPass'), updateController.post); + accountValidator.pass('passChange.confirmPass'), + updateController.post); + //rankEnum //This might seem silly, but it allows us to cleanly get the current rank list to compare against, without storing it in multiple places router.get('/rankEnum', rankEnumController.get); diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 2856094..1cc3b1a 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -25,7 +25,11 @@ const config = require('../../../config.json'); const {userModel} = require('../user/userSchema'); const permissionModel = require('../permissionSchema'); const tokeModel = require('../tokebot/tokeSchema'); +const statModel = require('../statSchema'); +const emailChangeModel = require('../user/emailChangeSchema'); const loggerUtils = require('../../utils/loggerUtils'); +const hashUtils = require('../../utils/hashUtils'); +const mailUtils = require('../../utils/mailUtils'); /** * DB Schema for documents representing legacy fore.st migration data for a single user account @@ -307,15 +311,62 @@ migrationSchema.statics.buildMigrationCache = async function(){ } } +migrationSchema.statics.consumeByUsername = async function(ip, migration){ + //Pull migration doc by case-insensitive username + const migrationDB = await this.findOne({user: new RegExp(migration.user, 'i')}); + + //Wait on the miration DB token to be consumed + await migrationDB.consume(ip, migration); +} + //Methods /** * Consumes a migration profile and creates a new, modern canopy profile from the original. * @param {String} oldPass - Original password to authenticate migration against * @param {String} newPass - New password to re-hash with modern hashing algo - * @param {String} confirmPass - Confirmation for the new pass + * @param {String} passConfirm - Confirmation for the new pass */ -migrationSchema.methods.consume = async function(oldPass, newPass, confirmPass){ +migrationSchema.methods.consume = async function(ip, migration){ + //If we where handed a bad password + if(!hashUtils.compareLegacyPassword(migration.oldPass, this.pass)){ + //Complain + throw loggerUtils.exceptionSmith("Incorrect username/password.", "migration"); + } + //If we where handed a mismatched confirmation password + if(migration.newPass != migration.passConfirm){ + //Complain + throw loggerUtils.exceptionSmith("New password does not match confirmation password.", "migration"); + } + + //Increment user count + const id = await statModel.incrementUserCount(); + + //Create new user from profile info + const newUser = await userModel.create({ + id, + user: this.user, + pass: migration.newPass, + rank: permissionModel.rankEnum[this.rank], + bio: this.bio, + img: this.image, + date: this.date, + tokes: new Map([["Legacy Tokes", this.tokes]]) + }); + + //Tattoo hashed IP use to migrate to the new user account + await newUser.tattooIPRecord(ip); + + //if we submitted an email + if(this.email != null && this.email != ''){ + //Generate new request + const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: this.email, ipHash: ip}); + + //Send confirmation email + mailUtils.sendAddressVerification(requestDB, newUser, this.email, false, true); + } + + await this.deleteOne(); } module.exports = mongoose.model("migration", migrationSchema); \ No newline at end of file diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 003f2e6..bac2434 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -289,7 +289,7 @@ userSchema.statics.register = async function(userObj, ip){ if(email != null){ const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: email, ipHash: ip}); - await mailUtil.sendAddressVerification(requestDB, newUser, email) + await mailUtil.sendAddressVerification(requestDB, newUser, email, true); } } }else{ diff --git a/src/utils/csrfUtils.js b/src/utils/csrfUtils.js index 5a898b3..457e48b 100644 --- a/src/utils/csrfUtils.js +++ b/src/utils/csrfUtils.js @@ -17,9 +17,6 @@ along with this program. If not, see .*/ //NPM Imports const { csrfSync } = require('csrf-sync'); -//Local Imports -const {errorHandler} = require('./loggerUtils'); - //Pull needed methods from csrfSync const {generateToken, revokeToken, csrfSynchronisedProtection, isRequestValid} = csrfSync(); diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index 082cd73..95aaf71 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -34,7 +34,7 @@ module.exports.hashPassword = function(pass){ } /** - * Sitewide password for authenticating/comparing passwords agianst hashes + * Sitewide method for authenticating/comparing passwords agianst hashes * @param {String} pass - Plaintext Password * @param {String} hash - Salty Hash * @returns {Boolean} True if authentication success @@ -43,6 +43,16 @@ module.exports.comparePassword = function(pass, hash){ return bcrypt.compareSync(pass, hash); } +/** + * Sitewide method for authenticating/comparing passwords agianst hashes for legacy profiles + * @param {String} pass - Plaintext Password + * @param {String} hash - Salty Hash + * @returns {Boolean} True if authentication success + */ +module.exports.compareLegacyPassword = function(pass, hash){ + return bcrypt.compareSync(pass, hash); +} + /** * Site-wide IP hashing/salting function * diff --git a/src/utils/mailUtils.js b/src/utils/mailUtils.js index 3df123b..ec825a1 100644 --- a/src/utils/mailUtils.js +++ b/src/utils/mailUtils.js @@ -72,16 +72,40 @@ module.exports.mailem = async function(to, subject, body, htmlBody = false){ * @param {Mongoose.Document} requestDB - DB Document Object for the current email change request token * @param {Mongoose.Document} userDB - DB Document Object for the user we're verifying email against * @param {String} newEmail - New email address to send to + * @param {Boolean} newUser - Denotes an email going out to a new account + * @param {Boolean} migration - Denotes an email going out to an account which was just mirated */ -module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail){ +module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail, newUser = false, migration = false,){ + let subject = `Email Change Request - ${userDB.user}`; + let content = `

email change request

+

a request to change the email associated with the ${config.instanceName} account '${userDB.user}' to this address has been requested.
+ click here to confirm this change.

+ if you received this email without request, feel free to ignore and delete it! -tokebot`; + + if(newUser){ + subject = `New User Email Confirmation - ${userDB.user}`; + + content = `

New user email confirmation

+

a new ${config.instanceName} account '${userDB.user}' was created with this email address.
+ click here to confirm this change.

+ if you received this email without request, feel free to ignore and delete it! -tokebot`; + } + + if(migration){ + subject = `User Migration Email Confirmation - ${userDB.user}`; + + content = `

User migration email confirmation

+

The ${config.instanceName} account '${userDB.user}' was successfully migrated to our fancy new codebase.
+ click here to confirm this change.

+ if you received this email without request, reach out to an admin, as your old account might be getting jacked! -tokebot`; + } + + //Send the reset url via email await module.exports.mailem( newEmail, - `Email Change Request - ${userDB.user}`, - `

Email Change Request

-

A request to change the email associated with the ${config.instanceName} account '${userDB.user}' to this address has been requested.
- Click here to confirm this change.

- If you received this email without request, feel free to ignore and delete it! -Tokebot`, + subject, + content, true ); diff --git a/www/js/utils.js b/www/js/utils.js index 9f652b1..a2c0d8b 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -737,6 +737,24 @@ class canopyAjaxUtils{ } } + async migrate(user, oldPass, newPass, passConfirm, verification){ + var response = await fetch(`/api/account/migrate`,{ + method: "POST", + headers: { + "Content-Type": "application/json", + //It's either this or find and bind all event listeners :P + "x-csrf-token": utils.ajax.getCSRFToken() + }, + body: JSON.stringify({user, oldPass, newPass, passConfirm, verification}) + }); + + if(response.ok){ + location = "/"; + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + async login(user, pass, verification){ var response = await fetch(`/api/account/login`,{ method: "POST", From 66ec2fabc5851cd1f80769fd8d4b0cb655b9e1f9 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 05:48:12 -0400 Subject: [PATCH 141/209] Added basic profile sanatization for legacy migration data. --- src/schemas/user/migrationSchema.js | 12 +++++++----- src/schemas/user/userSchema.js | 4 +++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 1cc3b1a..3cdad49 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -19,6 +19,7 @@ const fs = require('node:fs/promises'); //NPM Imports const {mongoose} = require('mongoose'); +const validator = require('validator'); //local imports const config = require('../../../config.json'); @@ -223,7 +224,7 @@ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ pass: profileArray[2], //Clamp rank to 0 and the max setting allowed by the rank enum rank: Math.min(Math.max(0, profileArray[3]), permissionModel.rankEnum.length - 1), - email: profileArray[4], + email: validator.normalizeEmail(profileArray[4]), date: profileArray[7], }) @@ -233,8 +234,8 @@ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){ const bioObject = JSON.parse(profileArray[5].replaceAll("\'",'\\\'')); //Inject bio information into migration profile, only if they're present; - migrationProfile.bio = bioObject.text == '' ? undefined : bioObject.text; - migrationProfile.image = bioObject.image == '' ? undefined : bioObject.image; + migrationProfile.bio = bioObject.text == '' ? undefined : validator.escape(bioObject.text); + migrationProfile.image = bioObject.image == '' ? undefined : validator.escape(bioObject.image); } //Build DB Doc from migration Profile hashtable and dump it into the DB @@ -359,13 +360,14 @@ migrationSchema.methods.consume = async function(ip, migration){ //if we submitted an email if(this.email != null && this.email != ''){ - //Generate new request + //Generate new email change request const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: this.email, ipHash: ip}); - //Send confirmation email + //Send tokenized confirmation email mailUtils.sendAddressVerification(requestDB, newUser, this.email, false, true); } + //Nuke out miration entry await this.deleteOne(); } diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index bac2434..209312e 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -287,9 +287,11 @@ userSchema.statics.register = async function(userObj, ip){ //if we submitted an email if(email != null){ + //Generate email request token const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: email, ipHash: ip}); - await mailUtil.sendAddressVerification(requestDB, newUser, email, true); + //Send tokenized confirmation link to users email address + mailUtil.sendAddressVerification(requestDB, newUser, email, true); } } }else{ From 6ae652b47c39f32f3c1aefa3aa09fbdfa61b007d Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 06:55:36 -0400 Subject: [PATCH 142/209] Updated login API to throw 301 when an un-migrated user attempts to login. --- .../api/account/loginController.js | 33 ++++++++++++++----- src/utils/sessionUtils.js | 2 +- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index 00d62c4..fd926b0 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -21,10 +21,10 @@ const config = require('../../../../config.json'); const {validationResult, matchedData} = require('express-validator'); //local imports +const migrationModel = require('../../../schemas/user/migrationSchema.js'); const sessionUtils = require('../../../utils/sessionUtils'); +const hashUtils = require('../../../utils/hashUtils.js'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); -const altchaUtils = require('../../../utils/altchaUtils'); -const session = require('express-session'); //api account functions module.exports.post = async function(req, res){ @@ -51,20 +51,35 @@ module.exports.post = async function(req, res){ //if we don't have errors if(validResult.isEmpty()){ //Get login attempts for current user - const {user} = matchedData(req); - const attempts = sessionUtils.getLoginAttempts(user) + const {user, pass} = matchedData(req); - //if we've gone over max attempts and - if(attempts.count > sessionUtils.throttleAttempts){ - //tell client it needs a captcha - return res.sendStatus(429); + //Look for the username in the migration DB + const migrationDB = await migrationModel.findOne({user}); + + //If this isn't a migration + if(migrationDB == null){ + //Get login attempts + const attempts = sessionUtils.getLoginAttempts(user) + + //if we've gone over max attempts + if(attempts.count > sessionUtils.throttleAttempts){ + //tell client it needs a captcha + return res.sendStatus(429); + } + //otherwise + }else{ + //If the user has a good password + if(hashUtils.compareLegacyPassword(pass, migrationDB.pass)){ + //Redirect to migrate + return res.sendStatus(301); + } } }else{ res.status(400); return res.send({errors: validResult.array()}) } - // + //Scream about any un-caught errors return exceptionHandler(res, err); } diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 6aeae3b..b1b15cd 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -17,7 +17,7 @@ along with this program. If not, see .*/ //Local Imports const config = require('../../config.json'); const {userModel} = require('../schemas/user/userSchema.js'); -const userBanModel = require('../schemas/user/userBanSchema.js') +const userBanModel = require('../schemas/user/userBanSchema.js'); const altchaUtils = require('../utils/altchaUtils.js'); const loggerUtils = require('../utils/loggerUtils.js'); From cb3fc9bb9137dd0d3b4b856479a3345605a721fc Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 07:36:32 -0400 Subject: [PATCH 143/209] Beautified launch printout --- src/schemas/statSchema.js | 4 ++-- src/server.js | 2 +- src/utils/loggerUtils.js | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/schemas/statSchema.js b/src/schemas/statSchema.js index d1af85e..e7da67a 100644 --- a/src/schemas/statSchema.js +++ b/src/schemas/statSchema.js @@ -19,6 +19,7 @@ const {mongoose} = require('mongoose'); //Local Imports const config = require('./../../config.json'); +const loggerUtils = require('./../utils/loggerUtils'); /** * DB Schema for single document for keeping track of server stats @@ -94,8 +95,7 @@ statSchema.statics.incrementLaunchCount = async function(){ this.firstLaunch = stats.firstLaunch; //print bootup message to console. - console.log(`${config.instanceName}(Powered by Canopy) initialized. This server has booted ${stats.launchCount} time${stats.launchCount == 1 ? '' : 's'}.`) - console.log(`First booted on ${stats.firstLaunch}.`); + loggerUtils.welcomeWagon(stats.launchCount, stats.firstLaunch); } /** diff --git a/src/server.js b/src/server.js index 333dfc3..1799b24 100644 --- a/src/server.js +++ b/src/server.js @@ -222,6 +222,6 @@ async function asyncKickStart(){ //Listen Function webServer.listen(port, () => { - console.log(`Tokes up on port ${port}!`); + console.log(`Tokes up on port \x1b[4m\x1b[35m${port}\x1b[0m!`); }); } \ No newline at end of file diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index a3100e8..2ae399e 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -209,4 +209,27 @@ module.exports.dumpError = async function(err, date = new Date()){ //Dump the error we had saving that error to file to console module.exports.consoleWarn(doubleErr); } +} + +module.exports.welcomeWagon = function(count, date){ + //Inject values into ascii art + const art = ` +\x1b[32m ! \x1b[0m +\x1b[32m 420 \x1b[0m \x1b[32m\x1b[40m${config.instanceName}\x1b[0m\x1b[2m, Powered By:\x1b[0m +\x1b[32m 420 \x1b[0m +\x1b[32m WEEED \x1b[0m CCCC AAA NN N OOO PPPP Y Y +\x1b[32m! WEEED !\x1b[0m C A A NN N O O P P Y Y +\x1b[32mWEE EEEEE EED\x1b[0m C A A N N N O O P P Y Y +\x1b[32m WEE EEEEE EED\x1b[0m C AAAAA N N N O O PPPP Y +\x1b[32m WEE EEE EED\x1b[0m C A A N N N O O P Y +\x1b[32m WEE EEE EED\x1b[0m C A A N NN O O P Y +\x1b[32m WEEEEED\x1b[0m CCCC A A N NN OOO P Y +\x1b[32m WEEE ! EEED\x1b[0m +\x1b[32m !\x1b[0m \x1b[34mInitialization Complete!\x1b[0m This server has booted \x1b[4m${count}\x1b[0m time${count == 1 ? '' : 's'}. +\x1b[32m !\x1b[0m This server was first booted on \x1b[4m${date}\x1b[0m.` + + //Dump art to console + console.log(art); + //Add some extra padding for the port printout from server.js + process.stdout.write(' '); } \ No newline at end of file From bddbd9cd36fd9bc88ad86056e05606724ce0e8db Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 07:41:13 -0400 Subject: [PATCH 144/209] More welcome wagon beautification. --- src/schemas/statSchema.js | 3 ++- src/server.js | 2 +- src/utils/loggerUtils.js | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/schemas/statSchema.js b/src/schemas/statSchema.js index e7da67a..8c680c3 100644 --- a/src/schemas/statSchema.js +++ b/src/schemas/statSchema.js @@ -19,6 +19,7 @@ const {mongoose} = require('mongoose'); //Local Imports const config = require('./../../config.json'); +const tokeSchema = require('./tokebot/tokeSchema'); const loggerUtils = require('./../utils/loggerUtils'); /** @@ -95,7 +96,7 @@ statSchema.statics.incrementLaunchCount = async function(){ this.firstLaunch = stats.firstLaunch; //print bootup message to console. - loggerUtils.welcomeWagon(stats.launchCount, stats.firstLaunch); + loggerUtils.welcomeWagon(stats.launchCount, stats.firstLaunch, tokeSchema.count); } /** diff --git a/src/server.js b/src/server.js index 1799b24..43a1f57 100644 --- a/src/server.js +++ b/src/server.js @@ -222,6 +222,6 @@ async function asyncKickStart(){ //Listen Function webServer.listen(port, () => { - console.log(`Tokes up on port \x1b[4m\x1b[35m${port}\x1b[0m!`); + console.log(`Tokes up on port \x1b[4m\x1b[35m${port}\x1b[0m!\n`); }); } \ No newline at end of file diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 2ae399e..eb51977 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -211,7 +211,7 @@ module.exports.dumpError = async function(err, date = new Date()){ } } -module.exports.welcomeWagon = function(count, date){ +module.exports.welcomeWagon = function(count, date, tokes){ //Inject values into ascii art const art = ` \x1b[32m ! \x1b[0m @@ -225,7 +225,7 @@ module.exports.welcomeWagon = function(count, date){ \x1b[32m WEE EEE EED\x1b[0m C A A N NN O O P Y \x1b[32m WEEEEED\x1b[0m CCCC A A N NN OOO P Y \x1b[32m WEEE ! EEED\x1b[0m -\x1b[32m !\x1b[0m \x1b[34mInitialization Complete!\x1b[0m This server has booted \x1b[4m${count}\x1b[0m time${count == 1 ? '' : 's'}. +\x1b[32m !\x1b[0m \x1b[34mInitialization Complete!\x1b[0m This server has booted \x1b[4m${count}\x1b[0m time${count == 1 ? '' : 's'} and taken ${tokes} \x1b[4mtoke${tokes == 1 ? '' : 's'}\x1b[0m. \x1b[32m !\x1b[0m This server was first booted on \x1b[4m${date}\x1b[0m.` //Dump art to console From eb48b925512ec54075a5c4d119f1247023a11e2a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 08:25:13 -0400 Subject: [PATCH 145/209] Started work on migration UI. Improved email handling. --- src/controllers/migrateController.js | 31 +++++++++++++ src/routers/migrateRouter.js | 30 ++++++++++++ src/schemas/user/migrationSchema.js | 8 +++- src/server.js | 2 + src/utils/loggerUtils.js | 4 +- src/utils/mailUtils.js | 55 ++++++++++++++-------- src/views/migrate.ejs | 46 +++++++++++++++++++ www/css/migrate.css | 31 +++++++++++++ www/js/migrate.js | 68 ++++++++++++++++++++++++++++ 9 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 src/controllers/migrateController.js create mode 100644 src/routers/migrateRouter.js create mode 100644 src/views/migrate.ejs create mode 100644 www/css/migrate.css create mode 100644 www/js/migrate.js diff --git a/src/controllers/migrateController.js b/src/controllers/migrateController.js new file mode 100644 index 0000000..ea82e5e --- /dev/null +++ b/src/controllers/migrateController.js @@ -0,0 +1,31 @@ +/*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 .*/ + +//Config +const config = require('../../config.json'); + +//Local Imports +const altchaUtils = require('../utils/altchaUtils'); +const csrfUtils = require('../utils/csrfUtils'); + +//register page functions +module.exports.get = async function(req, res){ + //Generate captcha + const challenge = await altchaUtils.genCaptcha(); + + //Render page + return res.render('migrate', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)}); +} \ No newline at end of file diff --git a/src/routers/migrateRouter.js b/src/routers/migrateRouter.js new file mode 100644 index 0000000..8ee67be --- /dev/null +++ b/src/routers/migrateRouter.js @@ -0,0 +1,30 @@ +/*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 { Router } = require('express'); + + +//local imports +const migrateController = require("../controllers/migrateController"); + +//globals +const router = Router(); + +//routing functions +router.get('/', migrateController.get); + +module.exports = router; diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 3cdad49..c17ed51 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -316,6 +316,12 @@ migrationSchema.statics.consumeByUsername = async function(ip, migration){ //Pull migration doc by case-insensitive username const migrationDB = await this.findOne({user: new RegExp(migration.user, 'i')}); + //If we have no migration document + if(migrationDB == null){ + //Bitch and moan + throw loggerUtils.exceptionSmith("Incorrect username/password.", "migration"); + } + //Wait on the miration DB token to be consumed await migrationDB.consume(ip, migration); } @@ -359,7 +365,7 @@ migrationSchema.methods.consume = async function(ip, migration){ await newUser.tattooIPRecord(ip); //if we submitted an email - if(this.email != null && this.email != ''){ + if(this.email != null && validator.isEmail(this.email)){ //Generate new email change request const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: this.email, ipHash: ip}); diff --git a/src/server.js b/src/server.js index 43a1f57..336071c 100644 --- a/src/server.js +++ b/src/server.js @@ -58,6 +58,7 @@ const channelRouter = require('./routers/channelRouter'); const newChannelRouter = require('./routers/newChannelRouter'); const passwordResetRouter = require('./routers/passwordResetRouter'); const emailChangeRouter = require('./routers/emailChangeController'); +const migrateRouter = require('./routers/migrateRouter'); //Panel const panelRouter = require('./routers/panelRouter'); //Popup @@ -161,6 +162,7 @@ app.use('/c', channelRouter); app.use('/newChannel', newChannelRouter); app.use('/passwordReset', passwordResetRouter); app.use('/emailChange', emailChangeRouter); +app.use('/migrate', migrateRouter); //Panel app.use('/panel', panelRouter); //tooltip diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index eb51977..12e5ad8 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -173,10 +173,10 @@ module.exports.errorMiddleware = function(err, req, res, next){ * @param {Error} err - error to dump to file * @param {Date} date - Date of error, defaults to now */ -module.exports.dumpError = async function(err, date = new Date()){ +module.exports.dumpError = async function(err, date = new Date(), subDir){ try{ //Crash directory - const dir = "./log/crash/" + const dir = `./log/crash/${subDir}` //Double check crash folder exists try{ diff --git a/src/utils/mailUtils.js b/src/utils/mailUtils.js index ec825a1..fce92ca 100644 --- a/src/utils/mailUtils.js +++ b/src/utils/mailUtils.js @@ -19,6 +19,11 @@ const config = require('../../config.json'); //NPM imports const nodeMailer = require("nodemailer"); +const validator = require('validator'); + +//local imports +const loggerUtils = require('./loggerUtils'); + //Setup mail transport /** @@ -43,28 +48,38 @@ const transporter = nodeMailer.createTransport({ * @returns {Object} Sent mail info */ module.exports.mailem = async function(to, subject, body, htmlBody = false){ - //Create mail object - const mailObj = { - from: `"Tokebot🤖💨"<${config.mail.address}>`, - to, - subject - }; + try{ + //If we have a bad email address + if(!validator.isEmail(to)){ + //fuck off + return; + } - //If we're sending HTML - if(htmlBody){ - //set body as html - mailObj.html = body; - //If we're sending plaintext - }else{ - //Set body as plaintext - mailObj.text = body + //Create mail object + const mailObj = { + from: `"Tokebot🤖💨"<${config.mail.address}>`, + to, + subject + }; + + //If we're sending HTML + if(htmlBody){ + //set body as html + mailObj.html = body; + //If we're sending plaintext + }else{ + //Set body as plaintext + mailObj.text = body + } + + //Send mail based on mail object + const sentMail = await transporter.sendMail(mailObj); + + //return the mail info + return sentMail; + }catch(err){ + loggerUtils.dumpError(err, new Date(), 'mail/'); } - - //Send mail based on mail object - const sentMail = await transporter.sendMail(mailObj); - - //return the mail info - return sentMail; } /** diff --git a/src/views/migrate.ejs b/src/views/migrate.ejs new file mode 100644 index 0000000..3744efb --- /dev/null +++ b/src/views/migrate.ejs @@ -0,0 +1,46 @@ +<%# 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 . %> + + + + + <%- include('partial/styles', {instance, user}); %> + <%- include('partial/csrfToken', {csrfToken}); %> + + + <%= instance %> - Account Migration + + + <%- include('partial/navbar', {user}); %> +
+ + + + + + + + + + +
+ +
+ <%- include('partial/scripts', {user}); %> + + +
+ diff --git a/www/css/migrate.css b/www/css/migrate.css new file mode 100644 index 0000000..02ef485 --- /dev/null +++ b/www/css/migrate.css @@ -0,0 +1,31 @@ +/*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 .*/ +form{ + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5em; + margin: 5% 17%; +} + +.migrate-prompt{ + width: 100% +} + +#migrate-button{ + width: 6em; + height: 2em; +} \ No newline at end of file diff --git a/www/js/migrate.js b/www/js/migrate.js new file mode 100644 index 0000000..ed4a1ee --- /dev/null +++ b/www/js/migrate.js @@ -0,0 +1,68 @@ +/*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 .*/ + +class migratePrompt{ + constructor(){ + //Grab user prompt + this.user = document.querySelector("#migrate-username"); + //Grab pass prompts + this.oldPass = document.querySelector("#migrate-password-old"); + this.pass = document.querySelector("#migrate-password"); + this.passConfirm = document.querySelector("#migrate-password-confirm"); + //Grab migrate button + this.button = document.querySelector("#migrate-button"); + //Grab altcha widget + this.altcha = document.querySelector("altcha-widget"); + //Setup null property to hold verification payload from altcha widget + this.verification = null + + //Run input setup after DOM content has completely loaded to ensure altcha event listeners work + document.addEventListener('DOMContentLoaded', this.setupInput.bind(this)); + } + + setupInput(){ + //Add verification event listener to altcha widget + this.altcha.addEventListener("verified", this.verify.bind(this)); + + //Add migrate event listener to migrate button + this.button.addEventListener("click", this.migrate.bind(this)); + } + + verify(event){ + //pull verification payload from event + this.verification = event.detail.payload; + } + + migrate(){ + //If altcha verification isn't complete + if(this.verification == null){ + //don't bother + return; + } + + //if the confirmation password doesn't match + if(this.pass.value != this.passConfirm.value){ + //Scream and shout + new canopyUXUtils.popup(`

Confirmation password does not match!

`); + return; + } + + //Send the registration informaiton off to the server + utils.ajax.migrate(this.user.value , this.oldPass.value, this.pass.value , this.passConfirm.value , this.verification); + } +} + +const migrateForm = new migratePrompt(); \ No newline at end of file From 6bab5b4723b32069fe6b1f383df9839a769a0bd2 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 21:22:37 -0400 Subject: [PATCH 146/209] Migration UI complete. --- .../api/account/loginController.js | 23 +++++++++---------- src/utils/loggerUtils.js | 2 +- src/views/migrate.ejs | 3 +++ src/views/register.ejs | 2 ++ www/css/migrate.css | 8 +++++++ www/css/register.css | 4 ++++ www/js/login.js | 1 + www/js/migrate.js | 3 +++ www/js/utils.js | 13 ++++++++++- 9 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index fd926b0..f5f2130 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -56,24 +56,23 @@ module.exports.post = async function(req, res){ //Look for the username in the migration DB const migrationDB = await migrationModel.findOne({user}); - //If this isn't a migration - if(migrationDB == null){ - //Get login attempts - const attempts = sessionUtils.getLoginAttempts(user) - - //if we've gone over max attempts - if(attempts.count > sessionUtils.throttleAttempts){ - //tell client it needs a captcha - return res.sendStatus(429); - } - //otherwise - }else{ + //If we found a migration profile + if(migrationDB != null){ //If the user has a good password if(hashUtils.compareLegacyPassword(pass, migrationDB.pass)){ //Redirect to migrate return res.sendStatus(301); } } + + //Get login attempts + const attempts = sessionUtils.getLoginAttempts(user) + + //if we've gone over max attempts + if(attempts.count > sessionUtils.throttleAttempts){ + //tell client it needs a captcha + return res.sendStatus(429); + } }else{ res.status(400); return res.send({errors: validResult.array()}) diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 12e5ad8..b3e30c3 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -225,7 +225,7 @@ module.exports.welcomeWagon = function(count, date, tokes){ \x1b[32m WEE EEE EED\x1b[0m C A A N NN O O P Y \x1b[32m WEEEEED\x1b[0m CCCC A A N NN OOO P Y \x1b[32m WEEE ! EEED\x1b[0m -\x1b[32m !\x1b[0m \x1b[34mInitialization Complete!\x1b[0m This server has booted \x1b[4m${count}\x1b[0m time${count == 1 ? '' : 's'} and taken ${tokes} \x1b[4mtoke${tokes == 1 ? '' : 's'}\x1b[0m. +\x1b[32m !\x1b[0m \x1b[34mInitialization Complete!\x1b[0m This server has booted \x1b[4m${count}\x1b[0m time${count == 1 ? '' : 's'} and taken \x1b[4m${tokes}\x1b[0m toke${tokes == 1 ? '' : 's'}. \x1b[32m !\x1b[0m This server was first booted on \x1b[4m${date}\x1b[0m.` //Dump art to console diff --git a/src/views/migrate.ejs b/src/views/migrate.ejs index 3744efb..63e8f87 100644 --- a/src/views/migrate.ejs +++ b/src/views/migrate.ejs @@ -25,6 +25,9 @@ along with this program. If not, see . %> <%- include('partial/navbar', {user}); %> +

Welcome Back!

+

<%= instance%> has received an update, and your account needs one too!

+

Remember your new password, you will need it for your first login!

diff --git a/src/views/register.ejs b/src/views/register.ejs index 3848eac..58e8af3 100644 --- a/src/views/register.ejs +++ b/src/views/register.ejs @@ -25,6 +25,8 @@ along with this program. If not, see . %> <%- include('partial/navbar', {user}); %> +

Account Registration

+

Remember your password, you will need it for your first login!

diff --git a/www/css/migrate.css b/www/css/migrate.css index 02ef485..7c56ebc 100644 --- a/www/css/migrate.css +++ b/www/css/migrate.css @@ -28,4 +28,12 @@ form{ #migrate-button{ width: 6em; height: 2em; +} + +h1, h2{ + text-align: center; +} + +h2{ + margin-bottom: 0; } \ No newline at end of file diff --git a/www/css/register.css b/www/css/register.css index 0d25816..d070af8 100644 --- a/www/css/register.css +++ b/www/css/register.css @@ -28,4 +28,8 @@ form{ #register-button{ width: 6em; height: 2em; +} + +h1, h2{ + text-align: center; } \ No newline at end of file diff --git a/www/js/login.js b/www/js/login.js index a2a4903..c5a3408 100644 --- a/www/js/login.js +++ b/www/js/login.js @@ -18,6 +18,7 @@ class registerPrompt{ constructor(){ //Grab user prompt this.user = document.querySelector("#login-page-username"); + this.user.value = window.location.search.replace("?user=",''); //Grab pass prompts this.pass = document.querySelector("#login-page-password"); //Grab register button diff --git a/www/js/migrate.js b/www/js/migrate.js index ed4a1ee..d714ce7 100644 --- a/www/js/migrate.js +++ b/www/js/migrate.js @@ -18,6 +18,7 @@ class migratePrompt{ constructor(){ //Grab user prompt this.user = document.querySelector("#migrate-username"); + this.user.value = window.location.search.replace("?user=",''); //Grab pass prompts this.oldPass = document.querySelector("#migrate-password-old"); this.pass = document.querySelector("#migrate-password"); @@ -37,6 +38,8 @@ class migratePrompt{ //Add verification event listener to altcha widget this.altcha.addEventListener("verified", this.verify.bind(this)); + console.log(this.button); + //Add migrate event listener to migrate button this.button.addEventListener("click", this.migrate.bind(this)); } diff --git a/www/js/utils.js b/www/js/utils.js index a2c0d8b..9e023f1 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -756,7 +756,7 @@ class canopyAjaxUtils{ } async login(user, pass, verification){ - var response = await fetch(`/api/account/login`,{ + const response = await fetch(`/api/account/login`,{ method: "POST", headers: { "Content-Type": "application/json", @@ -769,6 +769,17 @@ class canopyAjaxUtils{ location.reload(); }else if(response.status == 429){ location = `/login?user=${user}`; + }else if(response.status == 301){ + /* + * So this is gross but I don't know that theres a better way to do this + * Reloading the page would mean either sending the pass to the server as a URL query string which is insecure + * Or the server pre-loading it from the request, however sending passwords back to users seems like a bad idea too, even if it's just an echo + * Using fetch API to load the page assets in dynamically fucks up too, because register.js waits for DOM to load + * + * We could try an iframe and inject the password into that, however that seems really fucking dirty + * Sometimes it might just be better to make the user re-enter it... + */ + location = `/migrate?user=${user}`; }else{ utils.ux.displayResponseError(await response.json()); } From 06f552a9ec2a034f37c21ae9a67e8b6857acadd4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 18 Oct 2025 07:21:17 -0400 Subject: [PATCH 147/209] Improved link validation and sanatization, in order to mitigate CVE-2025-56200 from validator.js NPM package. --- package.json | 8 +++++--- src/app/channel/media/playlistHandler.js | 4 ++-- src/app/channel/media/queue.js | 4 ++-- src/utils/linkUtils.js | 9 ++++++--- src/utils/loggerUtils.js | 2 +- src/utils/media/yanker.js | 10 +++++++--- src/validators/accountValidator.js | 9 +++++++-- src/validators/channelValidator.js | 6 +++++- src/validators/emoteValidator.js | 5 +++-- 9 files changed, 38 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 0c49972..86045a3 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,10 @@ "version": "0.4", "license": "AGPL-3.0-only", "dependencies": { + "@braintree/sanitize-url": "^7.1.1", "altcha": "^1.0.7", "altcha-lib": "^1.2.0", + "argon2": "^0.44.0", "bcrypt": "^5.1.1", "bootstrap-icons": "^1.11.3", "connect-mongo": "^5.1.0", @@ -16,7 +18,7 @@ "hls.js": "^1.6.2", "mongoose": "^8.4.3", "node-cron": "^3.0.3", - "nodemailer": "^6.9.16", + "nodemailer": "^7.0.9", "socket.io": "^4.8.1", "youtube-dl-exec": "^3.0.20" }, @@ -26,7 +28,7 @@ "build": "node node_modules/jsdoc/jsdoc.js --verbose -r src/ -R README.md -d www/doc/server/ && node node_modules/jsdoc/jsdoc.js --verbose -r www/js/channel -r README.md -d www/doc/client/" }, "devDependencies": { - "nodemon": "^3.1.10", - "jsdoc": "^4.0.4" + "jsdoc": "^4.0.4", + "nodemon": "^3.1.10" } } diff --git a/src/app/channel/media/playlistHandler.js b/src/app/channel/media/playlistHandler.js index 9345590..70b8c2b 100644 --- a/src/app/channel/media/playlistHandler.js +++ b/src/app/channel/media/playlistHandler.js @@ -120,12 +120,12 @@ class playlistHandler{ */ async addToPlaylistValidator(socket, url){ //If we where given a bad URL - if(typeof url != 'string' || !validator.isURL(url)){ + if(typeof url != 'string' || !validator.isURL(url,{require_valid_protocol: true})){ //Attempt to fix the situation by encoding it url = encodeURI(url); //If it's still bad - if(typeof url != 'string' || !validator.isURL(url)){ + if(typeof url != 'string' || !validator.isURL(url,{require_valid_protocol: true})){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation"); //and ignore it! diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 7866783..9f68021 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -132,12 +132,12 @@ class queue{ let url = data.url; //If we where given a bad URL - if(!validator.isURL(url)){ + if(!validator.isURL(url,{require_valid_protocol: true})){ //Attempt to fix the situation by encoding it url = encodeURI(url); //If it's still bad - if(!validator.isURL(url)){ + if(!validator.isURL(url,{require_valid_protocol: true})){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation"); //and ignore it! diff --git a/src/utils/linkUtils.js b/src/utils/linkUtils.js index 6452db1..9e85870 100644 --- a/src/utils/linkUtils.js +++ b/src/utils/linkUtils.js @@ -16,6 +16,7 @@ along with this program. If not, see .*/ //NPM Imports const validator = require('validator');//No express here, so regular validator it is! +const {sanitizeUrl} = require("@braintree/sanitize-url"); //Create link cache /** @@ -25,10 +26,12 @@ module.exports.cache = new Map(); /** * Validates links and returns a marked link object that can be returned to the client to format/embed accordingly - * @param {String} link - URL to Validate + * @param {String} dirtyLink - URL to Validate * @returns {Object} Marked link object */ -module.exports.markLink = async function(link){ +module.exports.markLink = async function(dirtyLink){ + const link = sanitizeUrl(dirtyLink); + //Check link cache for the requested link const cachedLink = module.exports.cache.get(link); @@ -44,7 +47,7 @@ module.exports.markLink = async function(link){ var type = "malformedLink" //Make sure we have an actual, factual URL - if(validator.isURL(link)){ + if(validator.isURL(link,{require_valid_protocol: true, protocols: ['http', 'https']})){ //The URL is valid, so this is at least a dead link type = 'deadLink'; diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index b3e30c3..3e9c0aa 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -173,7 +173,7 @@ module.exports.errorMiddleware = function(err, req, res, next){ * @param {Error} err - error to dump to file * @param {Date} date - Date of error, defaults to now */ -module.exports.dumpError = async function(err, date = new Date(), subDir){ +module.exports.dumpError = async function(err, date = new Date(), subDir = ''){ try{ //Crash directory const dir = `./log/crash/${subDir}` diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index f16640d..8072712 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -17,6 +17,7 @@ along with this program. If not, see .*/ //NPM Imports //const url = require("node:url"); const validator = require('validator');//No express here, so regular validator it is! +const {sanitizeUrl} = require("@braintree/sanitize-url"); //local import const iaUtil = require('./internetArchiveUtils'); @@ -96,12 +97,15 @@ module.exports.refreshRawLink = async function(mediaObj){ * Still this has some improvements like url pre-checks and the fact that it's handled serverside, recuing possibility of bad requests. * Some of the regex expressions for certain services have also been improved, such as youtube, and the fore.st-unique archive.org * - * @param {String} url - URL to determine media type of + * @param {String} dirtyURL - URL to determine media type of * @returns {Object} containing URL type and clipped ID string */ -module.exports.getMediaType = async function(url){ +module.exports.getMediaType = async function(dirtyURL){ + //Sanatize our URL + const url = sanitizeUrl(dirtyURL); + //Check if we have a valid url, encode it on the fly in case it's too humie-friendly - if(!validator.isURL(encodeURI(url))){ + if(!validator.isURL(encodeURI(url,{require_valid_protocol: true}))){ //If not toss the fucker out return { type: null, diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 38f08b7..aa1d942 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -16,6 +16,7 @@ along with this program. If not, see .*/ //NPM Imports const { checkSchema } = require('express-validator'); +const {sanitizeUrl} = require("@braintree/sanitize-url"); //local imports const {isRank} = require('./permissionsValidator'); @@ -99,11 +100,15 @@ module.exports.img = function(field = 'img'){ isURL: { options: { require_tld: false, - require_host: false + require_host: false, + require_valid_protocol: true }, errorMessage: "Invalid URL." }, - trim: true + trim: true, + customSanitizer: { + options: sanitizeUrl + } } }); } diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index 60c9170..350dd87 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -83,7 +83,11 @@ module.exports.settingsMap = function(){ }, 'settingsMap.streamURL': { optional: true, - isURL: true, + isURL: { + options:{ + require_valid_protocol: true + } + }, errorMessage: "Invalid Stream URL" } }) diff --git a/src/validators/emoteValidator.js b/src/validators/emoteValidator.js index 3c4ee74..6416516 100644 --- a/src/validators/emoteValidator.js +++ b/src/validators/emoteValidator.js @@ -48,7 +48,8 @@ module.exports.link = function(field = 'link'){ isURL: { options: { require_tld: false, - require_host: false + require_host: false, + require_valid_protocol: true }, errorMessage: "Invalid URL." }, @@ -76,7 +77,7 @@ module.exports.manualLink = function(input){ const clean = validator.trim(input) //If we have a URL return the trimmed input - if(validator.isURL(clean)){ + if(validator.isURL(clean,{require_valid_protocol: true})){ return clean; } From 7f6abdf8e2c56de63755adfed8ab585925adf23b Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 18 Oct 2025 08:36:05 -0400 Subject: [PATCH 148/209] Improved Email Change and Password Reset token security by increasing token size. --- src/schemas/user/emailChangeSchema.js | 2 +- src/schemas/user/passwordResetSchema.js | 2 +- src/validators/accountValidator.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/schemas/user/emailChangeSchema.js b/src/schemas/user/emailChangeSchema.js index bffd13f..5e839ba 100644 --- a/src/schemas/user/emailChangeSchema.js +++ b/src/schemas/user/emailChangeSchema.js @@ -52,7 +52,7 @@ const emailChangeSchema = new mongoose.Schema({ type: mongoose.SchemaTypes.String, required: true, //Use a cryptographically secure algorythm to create a random hex string from 16 bytes as our change/cancel token - default: ()=>{return crypto.randomBytes(16).toString('hex')} + default: ()=>{return crypto.randomBytes(32).toString('hex')} }, ipHash: { type: mongoose.SchemaTypes.String, diff --git a/src/schemas/user/passwordResetSchema.js b/src/schemas/user/passwordResetSchema.js index ecba77f..9391bee 100644 --- a/src/schemas/user/passwordResetSchema.js +++ b/src/schemas/user/passwordResetSchema.js @@ -48,7 +48,7 @@ const passwordResetSchema = new mongoose.Schema({ type: mongoose.SchemaTypes.String, required: true, //Use a cryptographically secure algorythm to create a random hex string from 16 bytes as our reset token - default: ()=>{return crypto.randomBytes(16).toString('hex')} + default: ()=>{return crypto.randomBytes(32).toString('hex')} }, ipHash: { type: mongoose.SchemaTypes.String, diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index aa1d942..4e031d3 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -185,8 +185,8 @@ module.exports.securityToken = function(field = 'token'){ isHexadecimal: true, isLength: { options: { - min: 32, - max: 32 + min: 64, + max: 64 } }, errorMessage: "Invalid security token." From 895a8201a5a00a07675b4931e19807951f1c4454 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 18 Oct 2025 09:15:00 -0400 Subject: [PATCH 149/209] Started work on Remember Me Tokens. --- src/schemas/user/rememberMeSchema.js | 103 +++++++++++++++++++++++++++ src/utils/hashUtils.js | 21 ++++++ 2 files changed, 124 insertions(+) create mode 100644 src/schemas/user/rememberMeSchema.js diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js new file mode 100644 index 0000000..6b63942 --- /dev/null +++ b/src/schemas/user/rememberMeSchema.js @@ -0,0 +1,103 @@ +/*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 .*/ + +//You could make an argument for making this part of the userModel +//However, this is so rarely used the preformance benefits aren't worth the extra clutter + +//Config +const config = require('../../../config.json'); + +//Node Imports +const crypto = require("node:crypto"); + +//NPM Imports +const {mongoose} = require('mongoose'); + +//Local Imports +const userSchema = require('./userSchema'); +const hashUtil = require('../../utils/hashUtils'); +const loggerUtils = require('../../utils/loggerUtils'); + +/** + * Password reset token retention time + * + * Lasts about half a year + */ +const daysToExpire = 182; + +/** + * DB Schema for documents containing a single expiring password reset token + */ +const rememberMeToken = new mongoose.Schema({ + id: { + type: mongoose.SchemaTypes.UUID, + required: true, + default: crypto.randomUUID() + }, + user: { + type: mongoose.SchemaTypes.ObjectID, + ref: "user", + required: true + }, + token: { + type: mongoose.SchemaTypes.String, + required: true + }, + date: { + type: mongoose.SchemaTypes.Date, + required: true, + default: new Date() + } +}); + +/** + * Pre-Save function for rememberMeSchema + */ +rememberMeToken.pre('save', async function (next){ + //If the token was changed + if(this.isModified("token")){ + //Hash that sunnovabitch, no questions asked. + this.token = hashUtil.hashRememberMeToken(this.token); + } + + //All is good, continue on saving. + next(); +}); + +//statics +rememberMeToken.statics.genToken = async function(user, pass){ + try{ + //Authenticate user and pull document + const userDB = await userSchema.authenticate(user, pass); + + //Generate a cryptographically secure string of 32 bytes in hexidecimal + const token = crypto.randomBytes(32).toString('hex'); + + //Create token document off of user and token string + const tokenDB = await this.create({user: userDB._id, token}); + + //Return token document UUID w/ plaintext token for browser consumption + return { + id: tokenDB.id, + token + }; + //If we failed (most likely for bad login) + }catch(err){ + return loggerUtils.localExceptionHandler(err); + } +} + +module.exports = mongoose.model("rememberMe", rememberMeToken); \ No newline at end of file diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index 95aaf71..b9086cc 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -21,6 +21,7 @@ const config = require('../../config.json'); const crypto = require('node:crypto'); //NPM Imports +const argon2 = require('argon2'); const bcrypt = require('bcrypt'); /** @@ -69,4 +70,24 @@ module.exports.hashIP = function(ip){ //return the IP hash as a string return hashObj.digest('hex'); +} + +/** + * Site-wide remember-me token hashing function + * @param {String} token - Token to hash + * @returns {String} - Hashed token + */ +module.exports.hashRememberMeToken = async function(token){ + return await argon2.hash(token); +} + +/** + * Site-wide remember-me token hash comparison function + * @param {String} token - Token to compare + * @param {String} hash - Hash to compare + * @returns {String} - Comparison results + */ +module.exports.compareRememberMeToken = async function(token, hash){ + //Compare hash and return result + return await argon2.verify(hash, token); } \ No newline at end of file From 5caa679b9230b81a9a0ba3c6dfd4039865402123 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 18 Oct 2025 09:42:08 -0400 Subject: [PATCH 150/209] Upgraded password hashing algo to argon2id. --- config.example.json | 10 +++++++--- config.example.jsonc | 23 +++++++++++++++-------- src/schemas/user/userSchema.js | 12 ++++++------ src/server.js | 2 +- src/utils/altchaUtils.js | 4 ++-- src/utils/configCheck.js | 16 +++++++++++++--- src/utils/hashUtils.js | 22 ++++++++++++---------- 7 files changed, 56 insertions(+), 33 deletions(-) diff --git a/config.example.json b/config.example.json index 5e3eb01..cb43df6 100644 --- a/config.example.json +++ b/config.example.json @@ -6,11 +6,15 @@ "protocol": "http", "domain": "localhost", "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", - "sessionSecret": "CHANGE_ME", - "altchaSecret": "CHANGE_ME", - "ipSecret": "CHANGE_ME", "migrate": false, "dropLegacyTokes": false, + "secrets":{ + "passwordSecret": "CHANGE_ME", + "rememberMeSecret": "CHANGE_ME", + "sessionSecret": "CHANGE_ME", + "altchaSecret": "CHANGE_ME", + "ipSecret": "CHANGE_ME" + }, "ssl":{ "cert": "./server.cert", "key": "./server.key" diff --git a/config.example.jsonc b/config.example.jsonc index 01eeab1..02fb88e 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -16,14 +16,6 @@ //Path to YT-DLP Executable for scraping youtube, dailymotion, and vimeo //Dailymotion and Vimeo could work using official apis w/o keys, but you wouldn't have any raw file playback options :P "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", - //Be careful with what you keep in secrets, you should use special chars, but test your deployment, as some chars may break account registration - //An update to either kill the server and bitch about the issue in console is planned so it's not so confusing for new admins - //Session secret used to secure session keys - "sessionSecret": "CHANGE_ME", - //Altacha secret used to generate altcha challenges - "altchaSecret": "CHANGE_ME", - //IP Secret used to salt IP Hashes - "ipSecret": "CHANGE_ME", //Enable to migrate legacy DB and toke files dumped into the ./migration/ directory //WARNING: The migration folder is cleared after server boot, whether or not a migration took place or this option is enabled. //Keep your backups in a safe place, preferably a machine that DOESN'T have open inbound ports exposed to the internet/a publically accessible reverse proxy! @@ -32,6 +24,21 @@ //Requires migration to be disabled before it takes effect. //WARNING: this does NOT affect user toke counts, migrated or otherwise. Use carefully! "dropLegacyTokes": false, + //Server Secrets + //Be careful with what you keep in secrets, you should use special chars, but test your deployment, as some chars may break account registration + //An update to either kill the server and bitch about the issue in console is planned so it's not so confusing for new admins + "secrets":{ + //Password secret used to pepper password hashes + "passwordSecret": "CHANGE_ME", + //Password secret used to pepper rememberMe token hashes + "rememberMeSecret": "CHANGE_ME", + //Session secret used to secure session keys + "sessionSecret": "CHANGE_ME", + //Altacha secret used to generate altcha challenges + "altchaSecret": "CHANGE_ME", + //IP Secret used to pepper IP Hashes + "ipSecret": "CHANGE_ME" + }, //SSL cert and key locations "ssl":{ "cert": "./server.cert", diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 209312e..e9368a3 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -164,7 +164,7 @@ userSchema.pre('save', async function (next){ //If the password was changed if(this.isModified("pass")){ //Hash that sunnovabitch, no questions asked. - this.pass = hashUtil.hashPassword(this.pass); + this.pass = await hashUtil.hashPassword(this.pass); } //If the flair was changed @@ -321,7 +321,7 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use } //Check our password is correct - if(userDB.checkPass(pass)){ + if(await userDB.checkPass(pass)){ return userDB; }else{ //if not scream and shout @@ -492,8 +492,8 @@ userSchema.statics.processAgedIPRecords = async function(){ * @param {String} pass - Password to authenticate * @returns {Boolean} True if authenticated */ -userSchema.methods.checkPass = function(pass){ - return hashUtil.comparePassword(pass, this.pass) +userSchema.methods.checkPass = async function(pass){ + return await hashUtil.comparePassword(pass, this.pass) } /** @@ -824,7 +824,7 @@ userSchema.methods.killAllSessions = async function(reason = "A full log-out fro * @param {Object} passChange - passChange object handed down from Browser */ userSchema.methods.changePassword = async function(passChange){ - if(this.checkPass(passChange.oldPass)){ + if(await this.checkPass(passChange.oldPass)){ if(passChange.newPass == passChange.confirmPass){ //Note: We don't have to worry about hashing here because the schema is written to do it auto-magically this.pass = passChange.newPass; @@ -877,7 +877,7 @@ userSchema.methods.nuke = async function(pass){ } //Check that the password is correct - if(this.checkPass(pass)){ + if(await this.checkPass(pass)){ //delete the user var oldUser = await this.deleteOne(); }else{ diff --git a/src/server.js b/src/server.js index 336071c..8aedf91 100644 --- a/src/server.js +++ b/src/server.js @@ -84,7 +84,7 @@ module.exports.store = mongoStore.create({mongoUrl: dbUrl}); //define sessionMiddleware const sessionMiddleware = session({ - secret: config.sessionSecret, + secret: config.secrets.sessionSecret, resave: false, saveUninitialized: false, store: module.exports.store diff --git a/src/utils/altchaUtils.js b/src/utils/altchaUtils.js index 4c7754f..68ed7e1 100644 --- a/src/utils/altchaUtils.js +++ b/src/utils/altchaUtils.js @@ -44,7 +44,7 @@ module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){ //Generate Altcha Challenge return await createChallenge({ - hmacKey: [config.altchaSecret, uniqueSecret].join(''), + hmacKey: [config.secrets.altchaSecret, uniqueSecret].join(''), maxNumber: 100000 * difficulty, expires: expiration }); @@ -73,5 +73,5 @@ module.exports.verify = async function(payload, uniqueSecret = ''){ setTimeout(() => {spent.splice(payloadIndex,1);}, lifetime * 60 * 1000); //Return verification results - return await verifySolution(payload, [config.altchaSecret, uniqueSecret].join('')); + return await verifySolution(payload, [config.secrets.altchaSecret, uniqueSecret].join('')); } \ No newline at end of file diff --git a/src/utils/configCheck.js b/src/utils/configCheck.js index a705195..d979b32 100644 --- a/src/utils/configCheck.js +++ b/src/utils/configCheck.js @@ -40,18 +40,28 @@ module.exports.securityCheck = function(){ loggerUtil.consoleWarn("Mail transport security disabled! This server should be used for development purposes only!"); } + //check password pepper + if(!validator.isStrongPassword(config.secrets.passwordSecret) || config.secrets.passwordSecret == "CHANGE_ME"){ + loggerUtil.consoleWarn("Insecure Password Secret! Change Password Secret!"); + } + + //check RememberMe pepper + if(!validator.isStrongPassword(config.secrets.rememberMeSecret) || config.secrets.rememberMeSecret == "CHANGE_ME"){ + loggerUtil.consoleWarn("Insecure RememberMe Secret! Change RememberMe Secret!"); + } + //check session secret - if(!validator.isStrongPassword(config.sessionSecret) || config.sessionSecret == "CHANGE_ME"){ + if(!validator.isStrongPassword(config.secrets.sessionSecret) || config.secrets.sessionSecret == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure Session Secret! Change Session Secret!"); } //check altcha secret - if(!validator.isStrongPassword(config.altchaSecret) || config.altchaSecret == "CHANGE_ME"){ + if(!validator.isStrongPassword(config.secrets.altchaSecret) || config.secrets.altchaSecret == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure Altcha Secret! Change Altcha Secret!"); } //check ipHash secret - if(!validator.isStrongPassword(config.ipSecret) || config.ipSecret == "CHANGE_ME"){ + if(!validator.isStrongPassword(config.secrets.ipSecret) || config.secrets.ipSecret == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure IP Hashing Secret! Change IP Hashing Secret!"); } diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index b9086cc..e60d81e 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -29,9 +29,9 @@ const bcrypt = require('bcrypt'); * @param {String} pass - Password to hash * @returns {String} Hashed/Salted password */ -module.exports.hashPassword = function(pass){ - const salt = bcrypt.genSaltSync(); - return bcrypt.hashSync(pass, salt); +module.exports.hashPassword = async function(pass){ + //Hash password with argon2id + return await argon2.hash(pass, {secret: Buffer.from(config.secrets.passwordSecret)}); } /** @@ -40,8 +40,9 @@ module.exports.hashPassword = function(pass){ * @param {String} hash - Salty Hash * @returns {Boolean} True if authentication success */ -module.exports.comparePassword = function(pass, hash){ - return bcrypt.compareSync(pass, hash); +module.exports.comparePassword = async function(pass, hash){ + //Verify password against argon2 hash + return await argon2.verify(hash, pass, {secret: Buffer.from(config.secrets.passwordSecret)}); } /** @@ -59,14 +60,14 @@ module.exports.compareLegacyPassword = function(pass, hash){ * * Provides a basic level of privacy by only logging salted hashes of IP's * @param {String} ip - IP to hash - * @returns {String} Hashed/Salted IP Adress + * @returns {String} Hashed/Peppered IP Adress */ module.exports.hashIP = function(ip){ //Create hash object const hashObj = crypto.createHash('sha512'); - //add IP and salt to the hash - hashObj.update(`${ip}${config.ipSecret}`); + //add IP and pepper to the hash + hashObj.update(`${ip}${config.secrets.ipSecret}`); //return the IP hash as a string return hashObj.digest('hex'); @@ -78,7 +79,8 @@ module.exports.hashIP = function(ip){ * @returns {String} - Hashed token */ module.exports.hashRememberMeToken = async function(token){ - return await argon2.hash(token); + //hash token with argon2id + return await argon2.hash(token, {secret: Buffer.from(config.secrets.rememberMeSecret)}); } /** @@ -89,5 +91,5 @@ module.exports.hashRememberMeToken = async function(token){ */ module.exports.compareRememberMeToken = async function(token, hash){ //Compare hash and return result - return await argon2.verify(hash, token); + return await argon2.verify(hash, token, {secret: Buffer.from(config.secrets.rememberMeSecret)}); } \ No newline at end of file From 95ed2fa40311f54e6735a73c64e288a5560ac7ee Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 20 Oct 2025 05:15:34 -0400 Subject: [PATCH 151/209] Prepped session utils for remember me tokens. --- src/utils/sessionUtils.js | 73 ++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index b1b15cd..584411d 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -41,16 +41,19 @@ const maxAttempts = 200; * Sole and Singular Session Authentication method. * All logins should happen through here, all other site-wide authentication should happen by sessions authenticated by this model. * This is important, as reducing authentication endpoints reduces attack surface. - * @param {String} user - Username to login as - * @param {String} pass - Password to authenticat session with + * + * Ended up not splitting this in two/three for remember-me tokens. Kind of fucked up it was actually easier this way... + * @param {String} identifier - Identifer used to identify account, either username or token UUID + * @param {String} secret - Secret to authenticate session with, either password or token secret * @param {express.Request} req - Express request object w/ session to authenticate + * @param {Boolean} useRememberMeToken - Whether or not we're using username/pass or remember-me tokens * @returns Username of authticated user upon success */ -module.exports.authenticateSession = async function(user, pass, req){ +module.exports.authenticateSession = async function(identifier, secret, req, useRememberMeToken = false){ //Fuck you yoda try{ //Grab previous attempts - const attempt = failedAttempts.get(user); + const attempt = failedAttempts.get(identifier); //If we're proxied use passthrough IP const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip; @@ -74,7 +77,7 @@ module.exports.authenticateSession = async function(user, pass, req){ } //If we have failed attempts - if(attempt != null){ + if(!useRememberMeToken && attempt != null){ //If we have more failed attempts than allowed if(attempt.count > maxAttempts){ throw loggerUtils.exceptionSmith("This account has been locked for at 24 hours due to a large amount of failed log-in attempts", "unauthorized"); @@ -86,14 +89,23 @@ module.exports.authenticateSession = async function(user, pass, req){ //Since we've already got access to the request and dont need to import anything, why bother getting it from a parameter? if(req.body.verification == null){ throw loggerUtils.exceptionSmith("Verification failed!", "unauthorized"); - }else if(!altchaUtils.verify(req.body.verification, user)){ + }else if(!altchaUtils.verify(req.body.verification, identifier)){ throw loggerUtils.exceptionSmith("Verification failed!", ""); } } } - //Authenticate the session - const userDB = await userModel.authenticate(user, pass); + //define/scope empty userDB variable + let userDB = null; + + //If we're using remember me tokens + if(useRememberMeToken){ + + //Otherwise + }else{ + //Fallback on to username/password authentication + userDB = await userModel.authenticate(identifier, secret); + } //Check for user ban const userBanDB = await userBanModel.checkBanByUserDoc(userDB); @@ -123,33 +135,40 @@ module.exports.authenticateSession = async function(user, pass, req){ //Tattoo hashed IP address to user account for seven days userDB.tattooIPRecord(ip); - //If we got to here then the log-in was successful. We should clear-out any failed attempts. - failedAttempts.delete(user); + if(!useRememberMeToken){ + //If we got to here then the log-in was successful. We should clear-out any failed attempts. + failedAttempts.delete(identifier); + } //return user return userDB.user; }catch(err){ - //Look for previous failed attempts - var attempt = failedAttempts.get(user); + //Failed attempts at good tokens are handled by the token schema by dropping the users effected tokens and screaming bloody murder + //Failed attempts with bad tokens don't need to be handled as it's not like attacking a bad UUID is going to get you anywhere anywho + //This also makes it way easier to re-use parts of this function + if(!useRememberMeToken){ + //Look for previous failed attempts + var attempt = failedAttempts.get(identifier); - //If this is the first attempt - if(attempt == null){ - //Create new attempt object - attempt = { - count: 1, - lastAttempt: new Date() - } - }else{ - //Create updated attempt object - attempt = { - count: attempt.count + 1, - lastAttempt: new Date() + //If this is the first attempt + if(attempt == null){ + //Create new attempt object + attempt = { + count: 1, + lastAttempt: new Date() + } + }else{ + //Create updated attempt object + attempt = { + count: attempt.count + 1, + lastAttempt: new Date() + } } + + //Commit the failed attempt to the failed sign-in cache + failedAttempts.set(identifier, attempt); } - //Commit the failed attempt to the failed sign-in cache - failedAttempts.set(user, attempt); - //y33t throw err; } From e00e5a608be09f4955340ad6e6b67b9d31d3a5fa Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 20 Oct 2025 07:49:41 -0400 Subject: [PATCH 152/209] Continued work on remember me tokens. --- package.json | 1 + .../api/account/loginController.js | 48 ++++++++++++++---- src/schemas/user/rememberMeSchema.js | 16 +++--- src/server.js | 17 ++++++- src/utils/sessionUtils.js | 1 + src/validators/accountValidator.js | 50 +++++++++++++------ src/views/login.ejs | 1 + src/views/partial/navbar.ejs | 2 + www/js/login.js | 6 ++- www/js/navbar.js | 3 +- www/js/utils.js | 4 +- 11 files changed, 113 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 86045a3..0076162 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bcrypt": "^5.1.1", "bootstrap-icons": "^1.11.3", "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.7", "csrf-sync": "^4.0.3", "ejs": "^3.1.10", "express": "^4.18.2", diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index f5f2130..e7c5345 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -22,6 +22,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const migrationModel = require('../../../schemas/user/migrationSchema.js'); +const rememberMeModel = require('../../../schemas/user/rememberMeSchema.js'); const sessionUtils = require('../../../utils/sessionUtils'); const hashUtils = require('../../../utils/hashUtils.js'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); @@ -35,10 +36,39 @@ module.exports.post = async function(req, res){ //if we don't have errors if(validResult.isEmpty()){ //Pull sanatzied/validated data - const {user, pass} = matchedData(req); - - //try to authenticate the session, and return a successful code if it works - await sessionUtils.authenticateSession(user, pass, req); + const data = matchedData(req); + + //try to authenticate the session, throwing an error and breaking the current code block if user is un-authorized + await sessionUtils.authenticateSession(data.user, data.pass, req); + + //If the user already has a remember me token + if(data.rememberme != null && data.rememberme.id != null){ + //Fucking nuke the bitch + await rememberMeModel.deleteOne({id: data.rememberme.id}) + + //Tell the client to drop the token + res.clearCookie("rememberme.id"); + res.clearCookie("rememberme.token"); + } + + //If the user requested a rememberMe token (I'm not validation checking a fucking boolean) + if(req.body.rememberMe){ + //Gen user token + //requires second DB call, but this enforces password requirement for toke generation while ensuring we only + //need one function in the userModel for authentication, even if the second woulda just been a wrapper. + //Less attack surface is less attack surface, and this isn't something thats going to be getting constantly called + const authToken = await rememberMeModel.genToken(data.user, data.pass); + + //Check config for protocol + const secure = config.protocol.toLowerCase() == "https"; + + //Set remember me ID and token as browser-side cookies for safe-keeping + res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure}); + //This should be the servers last interaction with the plaintext token before saving the hashed copy, and dropping it out of RAM + res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure}); + } + + //Tell the browser everything is dandy return res.sendStatus(200); }else{ res.status(400); @@ -64,22 +94,22 @@ module.exports.post = async function(req, res){ return res.sendStatus(301); } } - + //Get login attempts const attempts = sessionUtils.getLoginAttempts(user) //if we've gone over max attempts - if(attempts.count > sessionUtils.throttleAttempts){ + if(attempts != null && attempts.count > sessionUtils.throttleAttempts){ //tell client it needs a captcha return res.sendStatus(429); + }else{ + //Scream about any un-caught errors + return exceptionHandler(res, err); } }else{ res.status(400); return res.send({errors: validResult.array()}) } - - //Scream about any un-caught errors - return exceptionHandler(res, err); } } \ No newline at end of file diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 6b63942..3ac78ef 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -27,7 +27,7 @@ const crypto = require("node:crypto"); const {mongoose} = require('mongoose'); //Local Imports -const userSchema = require('./userSchema'); +const {userModel} = require('./userSchema'); const hashUtil = require('../../utils/hashUtils'); const loggerUtils = require('../../utils/loggerUtils'); @@ -67,10 +67,14 @@ const rememberMeToken = new mongoose.Schema({ * Pre-Save function for rememberMeSchema */ rememberMeToken.pre('save', async function (next){ + //Ensure tokens ALWAYS get a new UUID and creation date + this.id = crypto.randomUUID(); + this.date = new Date(); + //If the token was changed if(this.isModified("token")){ //Hash that sunnovabitch, no questions asked. - this.token = hashUtil.hashRememberMeToken(this.token); + this.token = await hashUtil.hashRememberMeToken(this.token); } //All is good, continue on saving. @@ -79,10 +83,10 @@ rememberMeToken.pre('save', async function (next){ //statics rememberMeToken.statics.genToken = async function(user, pass){ - try{ - //Authenticate user and pull document - const userDB = await userSchema.authenticate(user, pass); + //Authenticate user and pull document + const userDB = await userModel.authenticate(user, pass); + try{ //Generate a cryptographically secure string of 32 bytes in hexidecimal const token = crypto.randomBytes(32).toString('hex'); @@ -94,7 +98,7 @@ rememberMeToken.statics.genToken = async function(user, pass){ id: tokenDB.id, token }; - //If we failed (most likely for bad login) + //If we failed for a non-login reason }catch(err){ return loggerUtils.localExceptionHandler(err); } diff --git a/src/server.js b/src/server.js index 8aedf91..87472a2 100644 --- a/src/server.js +++ b/src/server.js @@ -25,6 +25,7 @@ const fs = require('fs'); const express = require('express'); const session = require('express-session'); const {createServer } = require('http'); +const cookieParser = require('cookie-parser'); const { Server } = require('socket.io'); const path = require('path'); const mongoStore = require('connect-mongo'); @@ -38,6 +39,8 @@ const pmHandler = require('./app/pm/pmHandler'); const configCheck = require('./utils/configCheck'); const scheduler = require('./utils/scheduler'); const {errorMiddleware} = require('./utils/loggerUtils'); +//Validator +const accountValidator = require('./validators/accountValidator'); //DB Model const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); @@ -87,7 +90,11 @@ const sessionMiddleware = session({ secret: config.secrets.sessionSecret, resave: false, saveUninitialized: false, - store: module.exports.store + store: module.exports.store, + cookie: { + sameSite: "strict", + secure: config.protocol.toLowerCase() == "https" + } }); //Declare web server @@ -143,7 +150,9 @@ app.set('views', __dirname + '/views'); //Middlware //Enable Express app.use(express.json()); -//app.use(express.urlencoded()); + +//Enable Express Ccokie-Parser +app.use(cookieParser()); //Enable Express-Sessions app.use(sessionMiddleware); @@ -151,6 +160,10 @@ app.use(sessionMiddleware); //Enable Express-Session w/ Socket.IO io.engine.use(sessionMiddleware); +//Use rememberMe validators accross all requests. +app.use(accountValidator.rememberMeID()); +app.use(accountValidator.rememberMeToken()); + //Routes //Humie-Friendly app.use('/', indexRouter); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 584411d..970718b 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -18,6 +18,7 @@ along with this program. If not, see .*/ const config = require('../../config.json'); const {userModel} = require('../schemas/user/userSchema.js'); const userBanModel = require('../schemas/user/userBanSchema.js'); +const rememberMeModel = require('../schemas/user/rememberMeSchema.js'); const altchaUtils = require('../utils/altchaUtils.js'); const loggerUtils = require('../utils/loggerUtils.js'); diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 4e031d3..806aa43 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -177,19 +177,41 @@ module.exports.rank = function(field = 'rank'){ }); } -module.exports.securityToken = function(field = 'token'){ - return checkSchema({ - [field]: { - escape: true, - trim: true, - isHexadecimal: true, - isLength: { - options: { - min: 64, - max: 64 - } - }, - errorMessage: "Invalid security token." +const securityTokenSchema = { + escape: true, + trim: true, + isHexadecimal: true, + isLength: { + options: { + min: 64, + max: 64 } - }); + }, + errorMessage: "Invalid security token." +} + +module.exports.securityToken = function(field = 'token'){ + return checkSchema({[field]:securityTokenSchema}); +} + +module.exports.rememberMeID = function(field = 'rememberme.id'){ + return checkSchema({ + [field]:{ + in: ['cookies'], + optional: true, + isUUID: true + } + }) +} + +module.exports.rememberMeToken = function(field = 'rememberme.token'){ + //Create our own schema with blackjack and hookers + const tokenSchema = structuredClone(securityTokenSchema); + + //Modify as needed + tokenSchema.in = ['cookies']; + tokenSchema.optional = true; + + //Return the validator + return checkSchema({[field]:tokenSchema}); } \ No newline at end of file diff --git a/src/views/login.ejs b/src/views/login.ejs index 1a5e63f..12e8e78 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -38,6 +38,7 @@ along with this program. If not, see . %> <% if(challenge != null){ %> <% } %> + Create New Account Forgot Password diff --git a/src/views/partial/navbar.ejs b/src/views/partial/navbar.ejs index 924efba..3dc3065 100644 --- a/src/views/partial/navbar.ejs +++ b/src/views/partial/navbar.ejs @@ -19,6 +19,8 @@ along with this program. If not, see . %> <% if(user){ %> <% }else{ %> + + diff --git a/www/js/login.js b/www/js/login.js index c5a3408..cb93172 100644 --- a/www/js/login.js +++ b/www/js/login.js @@ -21,6 +21,8 @@ class registerPrompt{ this.user.value = window.location.search.replace("?user=",''); //Grab pass prompts this.pass = document.querySelector("#login-page-password"); + //Remember me checkbox + this.rememberMe = document.querySelector("#login-page-remember-me"); //Grab register button this.button = document.querySelector("#login-page-button"); //Grab altcha widget @@ -58,10 +60,10 @@ class registerPrompt{ } //login with verification - utils.ajax.login(this.user.value , this.pass.value, this.verification); + utils.ajax.login(this.user.value , this.pass.value, this.rememberMe.checked, this.verification); }else{ //login - utils.ajax.login(this.user.value, this.pass.value); + utils.ajax.login(this.user.value, this.pass.value, this.rememberMe.checked); } } } diff --git a/www/js/navbar.js b/www/js/navbar.js index 69697fd..ce9bac3 100644 --- a/www/js/navbar.js +++ b/www/js/navbar.js @@ -19,6 +19,7 @@ async function navbarLogin(event){ if(!event || !event.key || event.key == "Enter"){ var user = document.querySelector("#username-prompt").value; var pass = document.querySelector("#password-prompt").value; + var rememberMe = document.querySelector("#remember-me").checked; //If no user or pass is presented if(user == "" || pass == ""){ @@ -26,7 +27,7 @@ async function navbarLogin(event){ window.location = '/login' } - utils.ajax.login(user, pass); + utils.ajax.login(user, pass, rememberMe); } } diff --git a/www/js/utils.js b/www/js/utils.js index 9e023f1..2db077a 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -755,14 +755,14 @@ class canopyAjaxUtils{ } } - async login(user, pass, verification){ + async login(user, pass, rememberMe, verification){ const response = await fetch(`/api/account/login`,{ method: "POST", headers: { "Content-Type": "application/json", "x-csrf-token": utils.ajax.getCSRFToken() }, - body: JSON.stringify(verification ? {user, pass, verification} : {user, pass}) + body: JSON.stringify(verification ? {user, pass, rememberMe, verification} : {user, rememberMe, pass}) }); if(response.ok){ From 61ec3ffc5286a595bf3fa86bea482da189e60433 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 21 Oct 2025 00:10:17 -0400 Subject: [PATCH 153/209] Finished up with remember me middleware. --- .../api/account/loginController.js | 7 ++- src/schemas/user/rememberMeSchema.js | 47 +++++++++++++++++++ src/server.js | 20 ++++---- src/utils/sessionUtils.js | 44 ++++++++++++++++- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index e7c5345..910ff5e 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -62,10 +62,13 @@ module.exports.post = async function(req, res){ //Check config for protocol const secure = config.protocol.toLowerCase() == "https"; + //Create expiration date for cookies (180 days) + const expires = new Date(Date.now() + (1000 * 60 * 60 * 24 * 180)) + //Set remember me ID and token as browser-side cookies for safe-keeping - res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure}); + res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure, expires}); //This should be the servers last interaction with the plaintext token before saving the hashed copy, and dropping it out of RAM - res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure}); + res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure, expires}); } //Tell the browser everything is dandy diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 3ac78ef..74fc11d 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -81,6 +81,12 @@ rememberMeToken.pre('save', async function (next){ next(); }); +//Methods +rememberMeToken.methods.checkToken = async function(token){ + //Compare ingested token to saved hash + return await hashUtil.compareRememberMeToken(token, this.token); +} + //statics rememberMeToken.statics.genToken = async function(user, pass){ //Authenticate user and pull document @@ -104,4 +110,45 @@ rememberMeToken.statics.genToken = async function(user, pass){ } } +/** + * Authenticates an id and token pair + * @param {String} id - id of token auth against + * @param {String} token - token string to auth against + * @param {String} failLine - Line to paste into custom error upon login failure + * @returns {Mongoose.Document} - User DB Document upon success + */ +rememberMeToken.statics.authenticate = async function(id, token, failLine = "Bad Username or Password."){ + //check for missing pass + if(!id || !token){ + throw loggerUtils.exceptionSmith("Missing id/token.", "validation"); + } + + //get the token if it exists + const tokenDB = await this.findOne({id}); + + //if not scream and shout + if(!tokenDB){ + badLogin(); + } + + //Check our password is correct + if(await tokenDB.checkToken(token)){ + //Populate the user field + await tokenDB.populate('user'); + + //Return the user doc + return tokenDB.user; + }else{ + //Nuke the token for security + await tokenDB.deleteOne(); + //if not scream and shout + badLogin(); + } + + //standardize bad login response so it's unknown which is bad for security reasons. + function badLogin(){ + throw loggerUtils.exceptionSmith(failLine, "unauthorized"); + } +} + module.exports = mongoose.model("rememberMe", rememberMeToken); \ No newline at end of file diff --git a/src/server.js b/src/server.js index 87472a2..3f47c43 100644 --- a/src/server.js +++ b/src/server.js @@ -39,6 +39,7 @@ const pmHandler = require('./app/pm/pmHandler'); const configCheck = require('./utils/configCheck'); const scheduler = require('./utils/scheduler'); const {errorMiddleware} = require('./utils/loggerUtils'); +const sessionUtils = require('./utils/sessionUtils'); //Validator const accountValidator = require('./validators/accountValidator'); //DB Model @@ -143,6 +144,14 @@ mongoose.set("sanitizeFilter", true).connect(dbUrl).then(() => { process.exit(); }); +//Static File Server, set this up first to avoid middleware running on top of it +//Serve client-side libraries +app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); //Icon set +app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); //Self-Hosted PoW-based Captcha +app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler +//Server public 'www' folder +app.use(express.static(path.join(__dirname, '../www'))); + //Set View Engine app.set('view engine', 'ejs'); app.set('views', __dirname + '/views'); @@ -164,6 +173,9 @@ io.engine.use(sessionMiddleware); app.use(accountValidator.rememberMeID()); app.use(accountValidator.rememberMeToken()); +//Use remember me middleware +app.use(sessionUtils.rememberMeMiddleware); + //Routes //Humie-Friendly app.use('/', indexRouter); @@ -183,14 +195,6 @@ app.use('/tooltip', tooltipRouter); //Bot-Ready app.use('/api', apiRouter); -//Static File Server -//Serve client-side libraries -app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); //Icon set -app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); //Self-Hosted PoW-based Captcha -app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler -//Server public 'www' folder -app.use(express.static(path.join(__dirname, '../www'))); - //Handle error checking app.use(errorMiddleware); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 970718b..fd166a9 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -14,6 +14,9 @@ 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 config = require('../../config.json'); const {userModel} = require('../schemas/user/userSchema.js'); @@ -101,7 +104,7 @@ module.exports.authenticateSession = async function(identifier, secret, req, use //If we're using remember me tokens if(useRememberMeToken){ - + userDB = await rememberMeModel.authenticate(identifier, secret); //Otherwise }else{ //Fallback on to username/password authentication @@ -211,5 +214,44 @@ module.exports.processExpiredAttempts = function(){ } } +module.exports.rememberMeMiddleware = function(req, res, next){ + //if we have an un-authenticated user + if(req.session.user == null || req.session.user == ""){ + //Check validation result + const validResult = validationResult(req); + + //if we don't have errors + if(validResult.isEmpty()){ + //Pull verified data from request + const data = matchedData(req); + + //If we have a valid remember me id and token + if(data.rememberme != null && data.rememberme.id != null && data.rememberme.token != null){ + //Authenticate against standard auth function in remember me mode + module.exports.authenticateSession(data.rememberme.id, data.rememberme.token, req, true).then((userDB)=>{ + //Jump to next middleware + next(); + }).catch((err)=>{ + //Clear out remember me fields + res.clearCookie('rememberme.id'); + res.clearCookie('rememberme.token'); + + //Bitch, Moan, and guess what? That's fuckin' right! COMPLAIN!!!! + return loggerUtils.exceptionHandler(res, err); + }); + }else{ + //Jump to next middleware, this looks gross but it's only because they made me use .then like a bunch of fucking dicks + next(); + } + }else{ + //Jump to next middleware + next(); + } + }else{ + //Jump to next middleware + next(); + } +} + module.exports.throttleAttempts = throttleAttempts; module.exports.maxAttempts = maxAttempts; \ No newline at end of file From 1d5a087d79a302167b8a9bac24062c66a0521798 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 21 Oct 2025 00:21:44 -0400 Subject: [PATCH 154/209] Server now deletes associated remember-me token on user requested log-outs. --- .../api/account/loginController.js | 2 +- .../api/account/logoutController.js | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index 910ff5e..293842c 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -63,7 +63,7 @@ module.exports.post = async function(req, res){ const secure = config.protocol.toLowerCase() == "https"; //Create expiration date for cookies (180 days) - const expires = new Date(Date.now() + (1000 * 60 * 60 * 24 * 180)) + const expires = new Date(Date.now() + (1000 * 60 * 60 * 24 * 180)); //Set remember me ID and token as browser-side cookies for safe-keeping res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure, expires}); diff --git a/src/controllers/api/account/logoutController.js b/src/controllers/api/account/logoutController.js index 0499469..1964edc 100644 --- a/src/controllers/api/account/logoutController.js +++ b/src/controllers/api/account/logoutController.js @@ -15,13 +15,36 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //local imports -const accountUtils = require('../../../utils/sessionUtils'); -const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); +const rememberMeModel = require('../../../schemas/user/rememberMeSchema'); +const sessionUtils = require('../../../utils/sessionUtils'); +const {exceptionHandler} = require('../../../utils/loggerUtils'); +const {validationResult, matchedData} = require('express-validator'); module.exports.post = async function(req, res){ if(req.session.user){ try{ - accountUtils.killSession(req.session); + sessionUtils.killSession(req.session); + + //Check validation results + const validResult = validationResult(req); + + //if we don't have errors + if(validResult.isEmpty()){ + //Pull sanatzied/validated data + const data = matchedData(req); + + //If the user has a remember me token id they've submitted with the request + if(data.rememberme.id){ + //Find the associated token and nuke it + await rememberMeModel.deleteOne({id: data.rememberme.id}) + } + } + + //Clear out remember me tokens + res.clearCookie("rememberme.id"); + res.clearCookie("rememberme.token"); + + //Return status return res.sendStatus(200); }catch(err){ return exceptionHandler(res, err); From 3fb71ffb7890f7e9d9268687fd7579a4ffc30193 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 21 Oct 2025 07:42:20 -0400 Subject: [PATCH 155/209] Improved Documentation. --- src/utils/sessionUtils.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index fd166a9..7d4a94b 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -214,6 +214,12 @@ module.exports.processExpiredAttempts = function(){ } } +/** + * Express Middleware for handling remember-me authentication tokens + * @param {express.Request} req - Express Request Object + * @param {express.Response} res - Express Response Object + * @param {function} next - Function to call upon next middleware + */ module.exports.rememberMeMiddleware = function(req, res, next){ //if we have an un-authenticated user if(req.session.user == null || req.session.user == ""){ From bc0657a70243646cc538b009f9a352057e49a32a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 21 Oct 2025 07:59:15 -0400 Subject: [PATCH 156/209] Remember me tokens now nuked upon full account logout. --- .../api/account/loginController.js | 23 +++++++++++-------- .../api/account/logoutController.js | 2 +- src/schemas/user/rememberMeSchema.js | 11 +++++---- src/schemas/user/userSchema.js | 4 ++++ src/utils/sessionUtils.js | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index 293842c..d509342 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -39,7 +39,7 @@ module.exports.post = async function(req, res){ const data = matchedData(req); //try to authenticate the session, throwing an error and breaking the current code block if user is un-authorized - await sessionUtils.authenticateSession(data.user, data.pass, req); + const userDB = await sessionUtils.authenticateSession(data.user, data.pass, req); //If the user already has a remember me token if(data.rememberme != null && data.rememberme.id != null){ @@ -57,18 +57,21 @@ module.exports.post = async function(req, res){ //requires second DB call, but this enforces password requirement for toke generation while ensuring we only //need one function in the userModel for authentication, even if the second woulda just been a wrapper. //Less attack surface is less attack surface, and this isn't something thats going to be getting constantly called - const authToken = await rememberMeModel.genToken(data.user, data.pass); + const authToken = await rememberMeModel.genToken(userDB, data.pass); - //Check config for protocol - const secure = config.protocol.toLowerCase() == "https"; + //If we properly authed + if(authToken != null){ + //Check config for protocol + const secure = config.protocol.toLowerCase() == "https"; - //Create expiration date for cookies (180 days) - const expires = new Date(Date.now() + (1000 * 60 * 60 * 24 * 180)); + //Create expiration date for cookies (180 days) + const expires = new Date(Date.now() + (1000 * 60 * 60 * 24 * 180)); - //Set remember me ID and token as browser-side cookies for safe-keeping - res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure, expires}); - //This should be the servers last interaction with the plaintext token before saving the hashed copy, and dropping it out of RAM - res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure, expires}); + //Set remember me ID and token as browser-side cookies for safe-keeping + res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure, expires}); + //This should be the servers last interaction with the plaintext token before saving the hashed copy, and dropping it out of RAM + res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure, expires}); + } } //Tell the browser everything is dandy diff --git a/src/controllers/api/account/logoutController.js b/src/controllers/api/account/logoutController.js index 1964edc..0688f42 100644 --- a/src/controllers/api/account/logoutController.js +++ b/src/controllers/api/account/logoutController.js @@ -34,7 +34,7 @@ module.exports.post = async function(req, res){ const data = matchedData(req); //If the user has a remember me token id they've submitted with the request - if(data.rememberme.id){ + if(data.rememberme != null && data.rememberme.id != null){ //Find the associated token and nuke it await rememberMeModel.deleteOne({id: data.rememberme.id}) } diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 74fc11d..dfb925b 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -27,7 +27,6 @@ const crypto = require("node:crypto"); const {mongoose} = require('mongoose'); //Local Imports -const {userModel} = require('./userSchema'); const hashUtil = require('../../utils/hashUtils'); const loggerUtils = require('../../utils/loggerUtils'); @@ -88,9 +87,13 @@ rememberMeToken.methods.checkToken = async function(token){ } //statics -rememberMeToken.statics.genToken = async function(user, pass){ - //Authenticate user and pull document - const userDB = await userModel.authenticate(user, pass); +rememberMeToken.statics.genToken = async function(userDB, pass){ + //Normally I'd use userModel auth, but this saves on DB calls and keeps us from having to refrence the userModel directly + //Saving us from circular depedency hell + //Plus this is only really getting called along-side other auth, theres already going to be an error message if this is wrong XP + if(!await userDB.checkPass(pass)){ + return; + } try{ //Generate a cryptographically secure string of 32 bytes in hexidecimal diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index e9368a3..cc4d364 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -28,6 +28,7 @@ const permissionModel = require('../permissionSchema'); const emoteModel = require('../emoteSchema'); const emailChangeModel = require('./emailChangeSchema'); const playlistSchema = require('../channel/media/playlistSchema'); +const rememberMeModel = require('./rememberMeSchema'); //Utils const hashUtil = require('../../utils/hashUtils'); const mailUtil = require('../../utils/mailUtils'); @@ -807,6 +808,9 @@ userSchema.methods.tattooIPRecord = async function(ip){ * @param {String} reason - Reason to kill user sessions */ userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){ + //Nuke all related remember me tokens + await rememberMeModel.deleteMany({user: this._id}); + //get authenticated sessions var sessions = await this.getAuthenticatedSessions(); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 7d4a94b..7c5ad33 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -145,7 +145,7 @@ module.exports.authenticateSession = async function(identifier, secret, req, use } //return user - return userDB.user; + return userDB; }catch(err){ //Failed attempts at good tokens are handled by the token schema by dropping the users effected tokens and screaming bloody murder //Failed attempts with bad tokens don't need to be handled as it's not like attacking a bad UUID is going to get you anywhere anywho From d874f5e2daa7eb028e9c54150e7559bb3924dce4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 21 Oct 2025 08:09:55 -0400 Subject: [PATCH 157/209] Cleaned up remember-me error handling. --- src/utils/loggerUtils.js | 4 ++-- src/utils/sessionUtils.js | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 3e9c0aa..075f9ed 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -64,8 +64,8 @@ module.exports.errorHandler = function(res, msg, type = "Generic", status = 400) * @param {Error} err - Exception to handle */ module.exports.localExceptionHandler = function(err){ - //If we're being verbose - if(config.verbose){ + //If we're being verbose and this isn't just a basic bitch + if(!err.custom && config.verbose){ //Log the error module.exports.dumpError(err); } diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 7c5ad33..655dd46 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -242,8 +242,11 @@ module.exports.rememberMeMiddleware = function(req, res, next){ res.clearCookie('rememberme.id'); res.clearCookie('rememberme.token'); - //Bitch, Moan, and guess what? That's fuckin' right! COMPLAIN!!!! - return loggerUtils.exceptionHandler(res, err); + //Quietly handle exceptions without pestering the user + loggerUtils.localExceptionHandler(err); + + //Go on with life + next(); }); }else{ //Jump to next middleware, this looks gross but it's only because they made me use .then like a bunch of fucking dicks From 1e48d3af2c6dad405a00094d26bb70021b3b899d Mon Sep 17 00:00:00 2001 From: rainbownapkin Date: Tue, 21 Oct 2025 19:07:51 -0400 Subject: [PATCH 158/209] Added repo badges to readme. Added repo badges to readme. --- README.md | 72 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 2f346eb..ac1f39c 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,41 @@ -Canopy - 0.4-INDEV -====== - -Canopy - /ˈkæ.nə.pi/: - - The upper layer of foliage and branches of a forest, containing the majority of animal life. - - An honest attempt at a freedom/privacy respecting, libre, and open-source refrence implementation of what a stoner streaming service can be. - -Canopy is a community chat & synced video embedding web application, intended to replace fore.st as the server software for ourfore.st. -This new codebase intends to solve the following issues with the current CyTube based software: - - - Unmaintained upstream codebase. - - Different goals. - - Different coding styles. - - Obsolete Technology and Dependencies. - - General Clunk - - Less Unique Community Identity - -Canopy is a simple node/express.js app, leveraging yt-dlp and the internet archive REST api for metadata gathering. Persistant storage is handled by mongodb, as it's document based nature inherintly works well for cleanly storing large config documents for user/channel settings, and the low use of inter-collection references within the canopy software. All hardcore security functions like server-side input sanatization, session handling, CSRF mitigation, and password hashing are handled by industry-standard open source libraries such as validator/express-validator, express-sessions, csrf-sync, and bcrypt, however it IS hobbiest software, and it should be treated as such. - -The Canopy codebase does not, nor will it ever contain: - - Advertisements (targetted or otherwise) - - Proprietary Code - - Cryptocurrency/Blockchain integration - - 'Analytics/Telemtry' spyware - - The use of video sources which require proprietary 'Digital ~~Rights Management~~ Ristricitons Malware' such as Widevine. - - The use of large language models, stable diffusion, or generative AI in either development or function. - - Thirdparty media providers may or may not contain all of the above atrocities :P (though browser-side DRM extensions will never be required), always use an ad-blocker! - - Our current goal is to create a cleaner, more modern, purpose-built back-end that has feature-parity with the current version of fore.st, writing improvements where possible. Paired with this functionality, are a mix of engineering and artistic choices which attempt to re-create the power-user friendly UX of desktop sites from the early 2010's, with the 'aged like wine' looks that late oughts/early web 2.0 designs graced us with. Making sure that pageloads are low, and GPU use is non-existant along the way, to ensure everything is usable, even on low-end machines. - - ## License +Canopy +====== + + + + + + +0.4-INDEV +========= + +Canopy - /ˈkæ.nə.pi/: + - The upper layer of foliage and branches of a forest, containing the majority of animal life. + - An honest attempt at a freedom/privacy respecting, libre, and open-source refrence implementation of what a stoner streaming service can be. + +Canopy is a community chat & synced video embedding web application, intended to replace fore.st as the server software for ourfore.st. +This new codebase intends to solve the following issues with the current CyTube based software: + + - Unmaintained upstream codebase. + - Different goals. + - Different coding styles. + - Obsolete Technology and Dependencies. + - General Clunk + - Less Unique Community Identity + +Canopy is a simple node/express.js app, leveraging yt-dlp and the internet archive REST api for metadata gathering. Persistant storage is handled by mongodb, as it's document based nature inherintly works well for cleanly storing large config documents for user/channel settings, and the low use of inter-collection references within the canopy software. All hardcore security functions like server-side input sanatization, session handling, CSRF mitigation, and password hashing are handled by industry-standard open source libraries such as validator/express-validator, express-sessions, csrf-sync, and bcrypt, however it IS hobbiest software, and it should be treated as such. + +The Canopy codebase does not, nor will it ever contain: + - Advertisements (targetted or otherwise) + - Proprietary Code + - Cryptocurrency/Blockchain integration + - 'Analytics/Telemtry' spyware + - The use of video sources which require proprietary 'Digital ~~Rights Management~~ Ristricitons Malware' such as Widevine. + - The use of large language models, stable diffusion, or generative AI in either development or function. + + Thirdparty media providers may or may not contain all of the above atrocities :P (though browser-side DRM extensions will never be required), always use an ad-blocker! + + Our current goal is to create a cleaner, more modern, purpose-built back-end that has feature-parity with the current version of fore.st, writing improvements where possible. Paired with this functionality, are a mix of engineering and artistic choices which attempt to re-create the power-user friendly UX of desktop sites from the early 2010's, with the 'aged like wine' looks that late oughts/early web 2.0 designs graced us with. Making sure that pageloads are low, and GPU use is non-existant along the way, to ensure everything is usable, even on low-end machines. + + ## License Canopy is written by the community, and provided under the GNU Affero General Public License v3 in order to prevent Canopy from being used in proprietary software or shitcoin scams. \ No newline at end of file From 597a984e46390007e8712083b9ecd9058559e3b8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 03:04:16 -0400 Subject: [PATCH 159/209] Split pmHandler to create auxServer class for easy creation of server classes auxiliary to the channel. --- src/app/auxServer.js | 79 ++++++++++++++++++++++++++++++++ src/app/pm/pmHandler.js | 51 +++++---------------- www/js/channel/panels/pmPanel.js | 1 - 3 files changed, 90 insertions(+), 41 deletions(-) create mode 100644 src/app/auxServer.js diff --git a/src/app/auxServer.js b/src/app/auxServer.js new file mode 100644 index 0000000..3a66337 --- /dev/null +++ b/src/app/auxServer.js @@ -0,0 +1,79 @@ +/*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 .*/ + +//local includes +const loggerUtils = require("../utils/loggerUtils"); +const socketUtils = require("../utils/socketUtils"); + +/** + * Class containg global server-side logic for handling namespaces outside of the main one + */ +class auxServer{ + /** + * Instantiates object containing global server-side private message relay logic + * @param {Socket.io} io - Socket.io server instanced passed down from server.js + * @param {channelManager} chanServer - Sister channel management server object + */ + constructor(io, chanServer, namespace){ + /** + * Socket.io server instance passed down from server.js + */ + this.io = io; + + /** + * Sister channel management server object + */ + this.chanServer = chanServer; + + /** + * Socket.io server namespace for handling messaging + */ + this.namespace = io.of(namespace); + + //Handle connections from private messaging namespace + this.namespace.on("connection", this.handleConnection.bind(this) ); + } + + /** + * Handles global server-side initialization for new connections to aux server + * @param {Socket} socket - Requesting Socket + */ + async handleConnection(socket){ + try{ + //ensure unbanned ip and valid CSRF token + if(!(await socketUtils.validateSocket(socket))){ + socket.disconnect(); + return false; + } + + //If the socket wasn't authorized + if(await socketUtils.authSocketLite(socket) == null){ + socket.disconnect(); + return false; + } + + return true; + }catch(err){ + //Flip a table if something fucks up + return loggerUtils.socketCriticalExceptionHandler(socket, err); + } + } + + defineListeners(socket){ + } +} + +module.exports = auxServer; \ No newline at end of file diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index b783be8..5e16ffc 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -14,44 +14,25 @@ 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 validator = require('validator');//No express here, so regular validator it is! - //local includes +const auxServer = require('../auxServer'); const chatPreprocessor = require('../chatPreprocessor'); const loggerUtils = require("../../utils/loggerUtils"); -const socketUtils = require("../../utils/socketUtils"); const message = require("./message"); /** * Class containg global server-side private message relay logic */ -class pmHandler{ +class pmHandler extends auxServer{ /** * Instantiates object containing global server-side private message relay logic * @param {Socket.io} io - Socket.io server instanced passed down from server.js * @param {channelManager} chanServer - Sister channel management server object */ constructor(io, chanServer){ - /** - * Socket.io server instance passed down from server.js - */ - this.io = io; - - /** - * Sister channel management server object - */ - this.chanServer = chanServer; - - /** - * Socket.io server namespace for handling messaging - */ - this.namespace = io.of('/pm'); + super(io, chanServer, "/pm"); this.chatPreprocessor = new chatPreprocessor(null, null); - - //Handle connections from private messaging namespace - this.namespace.on("connection", this.handleConnection.bind(this) ); } /** @@ -59,31 +40,21 @@ class pmHandler{ * @param {Socket} socket - Requesting Socket */ async handleConnection(socket){ - try{ - //ensure unbanned ip and valid CSRF token - if(!(await socketUtils.validateSocket(socket))){ - socket.disconnect(); - return; - } + //Check if we're properly authorized + const authorized = await super.handleConnection(socket, "${user}"); - //If the socket wasn't authorized - if(await socketUtils.authSocketLite(socket) == null){ - socket.disconnect(); - return; - } - - //Throw socket into room named after it's user + //If we're authorized + if(authorized){ + //Throw the user into their own unique channel socket.join(socket.user.user); - //Define network related event listeners against socket + //Define listeners this.defineListeners(socket); - }catch(err){ - //Flip a table if something fucks up - return loggerUtils.socketCriticalExceptionHandler(socket, err); - } + } } defineListeners(socket){ + super.defineListeners(socket); socket.on("pm", (data)=>{this.handlePM(data, socket)}); } diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js index 7751d59..aaab30b 100644 --- a/www/js/channel/panels/pmPanel.js +++ b/www/js/channel/panels/pmPanel.js @@ -369,7 +369,6 @@ class pmPanel extends panelObj{ handleAutoScroll(){ //If autoscroll is enabled if(this.autoScroll){ - console.log("SCROLLME"); //Set seshBuffer scrollTop to the difference between scrollHeight and buffer height (scroll to the bottom) this.seshBuffer.scrollTop = this.seshBuffer.scrollHeight - Math.round(this.seshBuffer.getBoundingClientRect().height); } From 5eb307bb9ef4d4445b37ef6984d9132098bc48f6 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 04:34:05 -0400 Subject: [PATCH 160/209] Created dedicated queue broadcast namespace, to make authorized queue broadcasts as painless as possible. --- README.md | 2 +- src/app/auxServer.js | 19 +++-- src/app/channel/channelManager.js | 13 ++- .../channel/media/queueBroadcastManager.js | 82 +++++++++++++++++++ src/app/pm/pmHandler.js | 5 +- .../channel/channelPermissionSchema.js | 6 ++ src/utils/socketUtils.js | 4 + www/js/channel/channel.js | 5 ++ www/js/utils.js | 4 + 9 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/app/channel/media/queueBroadcastManager.js diff --git a/README.md b/README.md index ac1f39c..75720ec 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Canopy ====== - + diff --git a/src/app/auxServer.js b/src/app/auxServer.js index 3a66337..2779dbf 100644 --- a/src/app/auxServer.js +++ b/src/app/auxServer.js @@ -51,21 +51,30 @@ class auxServer{ * Handles global server-side initialization for new connections to aux server * @param {Socket} socket - Requesting Socket */ - async handleConnection(socket){ + async handleConnection(socket, lite = true){ try{ + //Define empty value to hold result + let result = null; + //ensure unbanned ip and valid CSRF token if(!(await socketUtils.validateSocket(socket))){ socket.disconnect(); - return false; + return null; + } + + if(lite){ + result = await socketUtils.authSocketLite(socket); + }else{ + result = await socketUtils.authSocket(socket); } //If the socket wasn't authorized - if(await socketUtils.authSocketLite(socket) == null){ + if(result == null){ socket.disconnect(); - return false; + return null; } - return true; + return result; }catch(err){ //Flip a table if something fucks up return loggerUtils.socketCriticalExceptionHandler(socket, err); diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 289c227..e9223f1 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -25,6 +25,7 @@ const loggerUtils = require('../../utils/loggerUtils'); const presenceUtils = require('../../utils/presenceUtils'); const activeChannel = require('./activeChannel'); const chatHandler = require('./chatHandler'); +const queueBroadcastManager = require('./media/queueBroadcastManager'); /** * Class containing global server-side channel connection management logic @@ -55,6 +56,16 @@ class channelManager{ */ this.chatHandler = new chatHandler(this); + /** + * Global Chat Handler Object + */ + this.chatHandler = new chatHandler(this); + + /** + * Global Auxiliary Server for Authenticated Queue Metadata Brodcasting + */ + this.queueBroadcastManager = new queueBroadcastManager(this.io, this) + //Handle connections from socket.io io.on("connection", this.handleConnection.bind(this) ); } @@ -152,7 +163,7 @@ class channelManager{ * @returns {Object} Object containing users active channel name and channel document object */ async getActiveChan(socket){ - socket.chan = socket.handshake.headers.referer.split('/c/')[1].split('/')[0]; + socket.chan = socketUtils.getChannelName(socket); const chanDB = (await channelModel.findOne({name: socket.chan})); //Check if channel exists diff --git a/src/app/channel/media/queueBroadcastManager.js b/src/app/channel/media/queueBroadcastManager.js new file mode 100644 index 0000000..c72a1c3 --- /dev/null +++ b/src/app/channel/media/queueBroadcastManager.js @@ -0,0 +1,82 @@ +/*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 .*/ + +//local includes +const auxServer = require("../../auxServer"); +const socketUtils = require("../../../utils/socketUtils") +const loggerUtils = require("../../../utils/loggerUtils"); +const channelModel = require("../../../schemas/channel/channelSchema"); + +/** + * Class containg global server-side private message relay logic + * + * Exists to make broadcasting channel queues to groups of authenticated users with the 'read-queue' perm as painless as possible, + * reducing DB call/perm checks to just connection time, and not requireing any out-of-library user iteration at broadcast time. + * + * Calls to modify and write to the schedule are still handled by the main namespace + * This is both for it's ease of access to the rest of the channel logic, but also to keep this class as small as possible. + */ +class queueBroadcastManager extends auxServer{ + /** + * Instantiates object containing global server-side channel schedule broadcasting subsystem + * @param {Socket.io} io - Socket.io server instanced passed down from server.js + * @param {channelManager} chanServer - Sister channel management server object + */ + constructor(io, chanServer){ + super(io, chanServer, "/queue-broadcast"); + } + + /** + * Handles global server-side initialization for new connections to the queue broadcast subsystem + * @param {Socket} socket - Requesting Socket + */ + async handleConnection(socket){ + //Check if we're properly authorized + const userObj = await super.handleConnection(socket); + + //If we're un-authorized + if(userObj == null){ + //Drop the connection + return; + } + + //Set socket channel value + socket.chan = socketUtils.getChannelName(socket); + //Pull channel DB + const chanDB = (await channelModel.findOne({name: socket.chan})); + + //If the user is connecting from an invalid channel + if(chanDB == null){ + //Drop the connection + return; + } + + //If the user is allowed to read the schedule + if(await chanDB.permCheck(socket.user, 'readSchedule')){ + //Throw the user into the channels room within the queue-broadcast instance + socket.join(socket.chan); + + //Define listeners + this.defineListeners(socket); + } + } + + defineListeners(socket){ + super.defineListeners(socket); + } +} + +module.exports = queueBroadcastManager; \ No newline at end of file diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js index 5e16ffc..10c505e 100644 --- a/src/app/pm/pmHandler.js +++ b/src/app/pm/pmHandler.js @@ -19,6 +19,7 @@ const auxServer = require('../auxServer'); const chatPreprocessor = require('../chatPreprocessor'); const loggerUtils = require("../../utils/loggerUtils"); const message = require("./message"); +const socketUtils = require("../../utils/socketUtils"); /** * Class containg global server-side private message relay logic @@ -41,10 +42,10 @@ class pmHandler extends auxServer{ */ async handleConnection(socket){ //Check if we're properly authorized - const authorized = await super.handleConnection(socket, "${user}"); + const authorized = await super.handleConnection(socket); //If we're authorized - if(authorized){ + if(authorized != null){ //Throw the user into their own unique channel socket.join(socket.user.user); diff --git a/src/schemas/channel/channelPermissionSchema.js b/src/schemas/channel/channelPermissionSchema.js index bdd709e..1916efd 100644 --- a/src/schemas/channel/channelPermissionSchema.js +++ b/src/schemas/channel/channelPermissionSchema.js @@ -89,6 +89,12 @@ const channelPermissionSchema = new mongoose.Schema({ default: "admin", required: true }, + readSchedule: { + type: mongoose.SchemaTypes.String, + enum: rankEnum, + default: "admin", + required: true + }, scheduleMedia: { type: mongoose.SchemaTypes.String, enum: rankEnum, diff --git a/src/utils/socketUtils.js b/src/utils/socketUtils.js index 765ea02..8b538df 100644 --- a/src/utils/socketUtils.js +++ b/src/utils/socketUtils.js @@ -98,4 +98,8 @@ module.exports.authSocket = async function(socket){ }; return userDB; +} + +module.exports.getChannelName = function(socket){ + return socket.handshake.headers.referer.split('/c/')[1].split('/')[0]; } \ No newline at end of file diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 9198ee1..e63a8a0 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -67,6 +67,10 @@ class channel{ //Freak out any weirdos who take a peek in the dev console for shits n gigs console.log("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇."); + //Preach the good word about software which is Free as in Freedom + console.log(`Did you know Canopy, the software that runs '${utils.ux.getInstanceName()}' is software that is free as in freedom AND free weed?`); + console.log("This means you can read/modify/redistribute the code, run your own server, and even contribute back!"); + console.log("https://git.ourfore.st/rainbownapkin/canopy"); } /** @@ -81,6 +85,7 @@ class channel{ }; this.socket = io(clientOptions); + this.queueBroadcastSocket = io("/queue-broadcast", clientOptions); this.pmSocket = io("/pm", clientOptions); } diff --git a/www/js/utils.js b/www/js/utils.js index 2db077a..1c25aeb 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -50,6 +50,10 @@ class canopyUXUtils{ constructor(){ } + getInstanceName(){ + return document.querySelector("#instance-title a").innerText; + } + async awaitNextFrame(){ //return a new promise return new Promise((resolve)=>{ From 6d16ac2353b248e13143b0232ae74a82308e94f2 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 05:00:59 -0400 Subject: [PATCH 161/209] Moved server-side queue transmission to it's own protected namespace. --- src/app/channel/connectedUser.js | 5 +---- src/app/channel/media/queue.js | 13 ++++++++++++- src/app/channel/media/queueBroadcastManager.js | 12 ++++++++++++ www/js/channel/channel.js | 6 ++---- www/js/channel/panels/queuePanel/queuePanel.js | 3 +-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 81b3220..03bea6e 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -226,9 +226,6 @@ class connectedUser{ } }); - //Get schedule as a temporary array - const queue = await this.channel.queue.prepQueue(chanDB); - //Get schedule lock status const queueLock = this.channel.queue.locked; @@ -236,7 +233,7 @@ class connectedUser{ const chatBuffer = this.channel.chatBuffer.buffer; //Send off the metadata to our user's clients - this.emit("clientMetadata", {user: userObj, flairList, queue, queueLock, chatBuffer}); + this.emit("clientMetadata", {user: userObj, flairList, queueLock, chatBuffer}); } /** diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 9f68021..8284420 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -1607,7 +1607,18 @@ class queue{ * @param {Mongoose.Document} chanDB - Pass through Channel Document to save on DB Transactions */ async broadcastQueue(chanDB){ - this.server.io.in(this.channel.name).emit('queue',{queue: await this.prepQueue(chanDB)}); + //Broadcast queue to authenticated sockets within the channels room inside the protected 'queue-broadcast' namespace + this.server.queueBroadcastManager.namespace.in(this.channel.name).emit('queue',{queue: await this.prepQueue(chanDB)}); + } + + /** + * Broadcasts channel queue to a single socket + * @param {Mongoose.Document} chanDB - Pass through Channel Document to save on DB Transactions + * @param {Socket} socket - Socket to send queue to + */ + async emitQueue(chanDB, socket){ + //Broadcast queue to authenticated sockets within the channels room inside the protected 'queue-broadcast' namespace + socket.emit('queue',{queue: await this.prepQueue(chanDB)}); } /** diff --git a/src/app/channel/media/queueBroadcastManager.js b/src/app/channel/media/queueBroadcastManager.js index c72a1c3..61e0497 100644 --- a/src/app/channel/media/queueBroadcastManager.js +++ b/src/app/channel/media/queueBroadcastManager.js @@ -55,6 +55,15 @@ class queueBroadcastManager extends auxServer{ //Set socket channel value socket.chan = socketUtils.getChannelName(socket); + //Pull active channel + const activeChannel = this.chanServer.activeChannels.get(socket.chan); + + //If there isn't an active channel + if(activeChannel == null){ + //Drop the connection + return; + } + //Pull channel DB const chanDB = (await channelModel.findOne({name: socket.chan})); @@ -69,6 +78,9 @@ class queueBroadcastManager extends auxServer{ //Throw the user into the channels room within the queue-broadcast instance socket.join(socket.chan); + //Send the queue down to our newly connected user + activeChannel.queue.emitQueue(chanDB, socket); + //Define listeners this.defineListeners(socket); } diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index e63a8a0..6220ed0 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -113,7 +113,8 @@ class channel{ this.socket.on("error", utils.ux.displayResponseError); - this.socket.on("queue", (data) => { + this.queueBroadcastSocket.on("queue", (data) => { + console.log(data); this.queue = new Map(data.queue); }); @@ -138,9 +139,6 @@ class channel{ //should it have its own event listener instead? Guess it's a stylistic choice :P this.chatBox.handleClientInfo(data); - //Store queue for use by the queue panel - this.queue = new Map(data.queue); - //Store queue lock status this.queueLock = data.queueLock; } diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index 2b05766..a872c92 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -137,8 +137,7 @@ class queuePanel extends panelObj{ defineListeners(){ //Render queue when we receive a new copy of the queue data from the server //Render queue should be called within an arrow function so that it's called with default parameters, and not handed an event as a date - this.client.socket.on("clientMetadata", () => {this.renderQueue();}); - this.client.socket.on("queue", () => {this.renderQueue();}); + this.client.queueBroadcastSocket.on("queue", () => {this.renderQueue();}); this.client.socket.on("start", this.handleStart.bind(this)); this.client.socket.on("end", this.handleEnd.bind(this)); this.client.socket.on("lock", this.handleScheduleLock.bind(this)); From 57787f81e7560c18eca84b0146610d61f5cfa6aa Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 05:36:55 -0400 Subject: [PATCH 162/209] Queue icon now only shows when readSchedule is allowed. --- src/views/partial/channel/chatPanel.ejs | 2 +- www/js/channel/channel.js | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/views/partial/channel/chatPanel.ejs b/src/views/partial/channel/chatPanel.ejs index e56c28a..0939f30 100644 --- a/src/views/partial/channel/chatPanel.ejs +++ b/src/views/partial/channel/chatPanel.ejs @@ -75,7 +75,7 @@ along with this program. If not, see . %>
- +

diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 6220ed0..8bc9a24 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -113,14 +113,13 @@ class channel{ this.socket.on("error", utils.ux.displayResponseError); - this.queueBroadcastSocket.on("queue", (data) => { - console.log(data); - this.queue = new Map(data.queue); - }); - this.socket.on("lock", (data) => { this.queueLock = data.locked; }); + + this.queueBroadcastSocket.on("queue", (data) => { + this.queue = new Map(data.queue); + }); } /** @@ -135,6 +134,12 @@ class channel{ this.user.permMap.site = new Map(data.user.permMap.site); this.user.permMap.chan = new Map(data.user.permMap.chan); + //If we can read the schedule + if(client.user.permMap.chan.get('readSchedule')){ + //Display the queue icon + this.chatBox.adminIcon.style.display = ""; + } + //Tell the chatbox to handle client info //should it have its own event listener instead? Guess it's a stylistic choice :P this.chatBox.handleClientInfo(data); From aa325872598f6f37b1c921224a934d0928716e56 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 20:17:53 -0400 Subject: [PATCH 163/209] Server now auto-magically nukes expired remember me tokens on startup and at midnight UTC. --- src/schemas/user/rememberMeSchema.js | 52 ++++++++++++++++++++++++---- src/utils/scheduler.js | 6 +++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index dfb925b..32df58e 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -80,12 +80,6 @@ rememberMeToken.pre('save', async function (next){ next(); }); -//Methods -rememberMeToken.methods.checkToken = async function(token){ - //Compare ingested token to saved hash - return await hashUtil.compareRememberMeToken(token, this.token); -} - //statics rememberMeToken.statics.genToken = async function(userDB, pass){ //Normally I'd use userModel auth, but this saves on DB calls and keeps us from having to refrence the userModel directly @@ -154,4 +148,50 @@ rememberMeToken.statics.authenticate = async function(id, token, failLine = "Bad } } +/** + * Schedulable function for processing expired remember me tokens + */ +rememberMeToken.statics.processExpiredTokens = async function(){ + //Pull all tokens from the DB + //Tested finding request by date, but mongoose kept throwing casting errors. + //This seems to be an intermittent issue online. Maybe it will work in a future version? + const tokenDB = await this.find({}); + + //Fire em all off at once without waiting for the last one to complete since we don't fuckin' need to + for(let tokenIndex in tokenDB){ + //pull token from tokenDB by index + const token = tokenDB[tokenIndex]; + + //If the token hasn't been processed and it's been expired + if(token.getDaysUntilExpiration() <= 0){ + //Delete the token + await token.deleteOne(); + } + } +} + +//Methods +/** + * Intakes a plaintext token string and compares it to the hashed remember me token from the database + * @param {String} token - Plaintext token retrieved from browser cookie + * @returns {Boolean} Comparison result + */ +rememberMeToken.methods.checkToken = async function(token){ + //Compare ingested token to saved hash + return await hashUtil.compareRememberMeToken(token, this.token); +} + +/** + * Returns number of days until token expiration + * @returns {Number} Number of days until token expiration + */ +rememberMeToken.methods.getDaysUntilExpiration = function(){ + //Get request date + const expirationDate = new Date(this.date); + //Get expiration days and calculate expiration date + expirationDate.setDate(expirationDate.getDate() + daysToExpire); + //Calculate and return days until request expiration + return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); +} + module.exports = mongoose.model("rememberMe", rememberMeToken); \ No newline at end of file diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js index f1a09bc..a0851ee 100644 --- a/src/utils/scheduler.js +++ b/src/utils/scheduler.js @@ -22,9 +22,9 @@ const {userModel} = require('../schemas/user/userSchema'); const userBanModel = require('../schemas/user/userBanSchema'); const passwordResetModel = require('../schemas/user/passwordResetSchema'); const emailChangeModel = require('../schemas/user/emailChangeSchema'); +const rememberMeModel = require('../schemas/user/rememberMeSchema'); const channelModel = require('../schemas/channel/channelSchema'); const sessionUtils = require('./sessionUtils'); -const { email } = require('../validators/accountValidator'); /** * Schedules all timed jobs accross the server @@ -42,6 +42,8 @@ module.exports.schedule = function(){ cron.schedule('0 0 * * *', ()=>{passwordResetModel.processExpiredRequests()},{scheduled: true, timezone: "UTC"}); //Process expired email change requests every night at midnight cron.schedule('0 0 * * *', ()=>{emailChangeModel.processExpiredRequests()},{scheduled: true, timezone: "UTC"}); + //Process expired remember me tokens every night at midnight + cron.schedule('0 0 * * *', ()=>{rememberMeModel.processExpiredTokens()},{scheduled: true, timezone: "UTC"}); } /** @@ -58,6 +60,8 @@ module.exports.kickoff = function(){ passwordResetModel.processExpiredRequests(); //Process expired email change requests that may have expired since last restart emailChangeModel.processExpiredRequests(); + //Process expired remember me tokens that may have expired since last restart + rememberMeModel.processExpiredTokens() //Schedule jobs module.exports.schedule(); From a68bc6a7dac8c0cb96fc74d32cda18a403f77586 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 20:29:32 -0400 Subject: [PATCH 164/209] Update README badges (super important) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75720ec..c485bb0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ Canopy ====== - + + + From b620b423f640bedbf604d2051ce85f022a032e47 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 20:34:21 -0400 Subject: [PATCH 165/209] Updated badges again... --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c485bb0..65f4680 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Canopy + From 875baa833f5ea50a6e8b27961abc4437f636be4c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 20:46:00 -0400 Subject: [PATCH 166/209] Moar readme updates. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 65f4680..cd53f8c 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ Canopy ====== + - - - - + + + 0.4-INDEV ========= From f9ac076e6f065e9edc93a8393b183bef60cccecc Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 20:49:30 -0400 Subject: [PATCH 167/209] readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cd53f8c..1bb3e5e 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Canopy - - - + + + 0.4-INDEV ========= From 79df27b72cbd5127d069247bacfb24a3adba4178 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 21:04:08 -0400 Subject: [PATCH 168/209] Autocomplete placeholder now replaces spaces in input value with unicode non-breaking space character. --- www/js/channel/chat.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index e733bdf..5311fb5 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -278,11 +278,11 @@ class chatBox{ //Find current match const match = this.checkAutocomplete(); + //Set placeholder to space out the autocomplete display - //Use text content because it's unescaped, and while this only effects local users, it'll keep someone from noticing and whinging about it - this.autocompletePlaceholder.textContent = this.chatPrompt.value; + this.autocompletePlaceholder.innerText = this.chatPrompt.value.replaceAll(" ","\u00A0"); //Set the autocomplete display - this.autocompleteDisplay.textContent = match.match.replace(match.word, ''); + this.autocompleteDisplay.innerText = match.match.replace(match.word, ''); } /** From a34ece43744e9289bc186157ea623652fcd05ff5 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 21:20:07 -0400 Subject: [PATCH 169/209] Video title now renders escaped entities properly. --- www/js/channel/mediaHandler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 4c4f491..f15ef89 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -187,7 +187,7 @@ class mediaHandler{ * @param {String} title - Title to set */ setVideoTitle(title){ - this.player.title.textContent = `Currently Playing: ${title}`; + this.player.title.innerText = `Currently Playing: ${utils.unescapeEntities(title)}`; } /** @@ -370,7 +370,7 @@ class nullHandler extends rawFileBase{ } setVideoTitle(title){ - this.player.title.textContent = `Channel Off Air`; + this.player.title.innerText = `Channel Off Air`; } } @@ -601,7 +601,7 @@ class youtubeEmbedHandler extends mediaHandler{ setVideoTitle(){ //Clear out the player title so that youtube's baked in title can do it's thing. //This will be replaced once we complete the full player control and remove the defualt youtube UI - this.player.title.textContent = ""; + this.player.title.innerText = ""; } /** @@ -741,12 +741,12 @@ class hlsLiveStreamHandler extends hlsBase{ setVideoTitle(title){ //Add title as text content for security :P - this.player.title.textContent = `: ${title}`; + this.player.title.innerText = `: ${utils.unescapeEntities(title)}`; //Create glow span const glowSpan = document.createElement('span'); //Fill glow span content - glowSpan.textContent = "🔴LIVE"; + glowSpan.innerText = "🔴LIVE"; //Set glowspan class glowSpan.classList.add('critical-danger-text'); From 1bd9fcdc8025ce3e1fef01fc91b11c8f47a440cd Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Oct 2025 21:53:41 -0400 Subject: [PATCH 170/209] High-level rank changes and bad attempts and good Remember-Me tokens now logged. --- .gitignore | 5 +-- src/schemas/user/rememberMeSchema.js | 7 ++-- src/schemas/user/userSchema.js | 5 +++ src/utils/loggerUtils.js | 57 ++++++++++++++++++++-------- src/utils/mailUtils.js | 2 +- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 9868f22..76cfb45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ node_modules/ -log/crash/* -!log/crash +log/* www/doc/*/* -!www/doc/client -!www/doc/server package-lock.json config.json config.json.old diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 32df58e..be162fd 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -128,14 +128,15 @@ rememberMeToken.statics.authenticate = async function(id, token, failLine = "Bad badLogin(); } + //Populate the user field + await tokenDB.populate('user'); + //Check our password is correct if(await tokenDB.checkToken(token)){ - //Populate the user field - await tokenDB.populate('user'); - //Return the user doc return tokenDB.user; }else{ + loggerUtils.dumpSecurityLog(`Failed attempt at ${tokenDB.user.user}'s Remember-Me token {${tokenDB.id}}... Nuking token!`); //Nuke the token for security await tokenDB.deleteOne(); //if not scream and shout diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index cc4d364..2d8517c 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -186,6 +186,11 @@ userSchema.pre('save', async function (next){ //If rank was changed if(this.isModified("rank")){ + //If this rank change is above 2 (Mod or above) + if(permissionModel.rankToNum(this.rank) > 2){ + loggerUtils.dumpSecurityLog(`${this.user}'s rank was set to ${this.rank}.`); + } + //force a full log-out await this.killAllSessions("Your site-wide rank has changed. Sign-in required."); } diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 075f9ed..6017a9b 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -16,6 +16,7 @@ along with this program. If not, see .*/ //Node const fs = require('node:fs/promises'); +const crypto = require('node:crypto'); //Config const config = require('../../config.json'); @@ -172,42 +173,68 @@ module.exports.errorMiddleware = function(err, req, res, next){ * Dumps unexpected server crashes to dedicated log files * @param {Error} err - error to dump to file * @param {Date} date - Date of error, defaults to now + * @param {String} subDir - subdirectory inside the log folder we want to dump to + * @param {Boolean} muzzle - Tells the function to STFU */ -module.exports.dumpError = async function(err, date = new Date(), subDir = ''){ +module.exports.dumpError = async function(err, date = new Date(), subDir = 'crash/', muzzle = false){ + //Generate content from error + const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`; + + //Dump text to file + module.exports.dumpLog(content, date.getTime(), subDir, muzzle); +} + + +module.exports.dumpSecurityLog = async function(content, date = new Date()){ + module.exports.dumpLog(content, `Incident-{${crypto.randomUUID()}}-${date.getTime()}`, 'security/', true); +} + +/** + * Dumps log file to log folder + * @param {String} content - Text to dump to file + * @param {String} name - file name to save to + * @param {String} subDir - subdirectory inside the log folder we want to dump to + * @param {Boolean} muzzle - Tells the function to STFU + */ +module.exports.dumpLog = async function(content, name, subDir = '/', muzzle = false){ try{ //Crash directory - const dir = `./log/crash/${subDir}` + const dir = `./log/${subDir}` //Double check crash folder exists try{ await fs.stat(dir); //If we caught an error (most likely it's missing) }catch(err){ - //Shout about it - module.exports.consoleWarn("Log folder missing, mking dir!") + if(!muzzle){ + //Shout about it + module.exports.consoleWarn("Log folder missing, mking dir!") + } //Make it if doesn't await fs.mkdir(dir, {recursive: true}); } //Assemble log file path - const path = `${dir}${date.getTime()}.log`; - //Generate error file content - const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`; + const path = `${dir}${name}.log`; //Write content to file fs.writeFile(path, content); - //Whine about the error - module.exports.consoleWarn(`Warning: Unexpected Server Crash gracefully dumped to '${path}'... SOMETHING MAY BE VERY BROKEN!!!!`); + if(!muzzle){ + //Whine about the error + module.exports.consoleWarn(`Warning: Unexpected Server Crash gracefully dumped to '${path}'... SOMETHING MAY BE VERY BROKEN!!!!`); + } //If somethine went really really wrong }catch(doubleErr){ - //Use humor to cope with the pain - module.exports.consoleWarn("Yo Dawg, I herd you like errors, so I put an error in your error dump, so you can dump while you dump:"); - //Dump the original error to console - module.exports.consoleWarn(err); - //Dump the error we had saving that error to file to console - module.exports.consoleWarn(doubleErr); + if(!muzzle){ + //Use humor to cope with the pain + module.exports.consoleWarn("Yo Dawg, I herd you like errors, so I put an error in your error dump, so you can dump while you dump:"); + //Dump the original error to console + module.exports.consoleWarn(err); + //Dump the error we had saving that error to file to console + module.exports.consoleWarn(doubleErr); + } } } diff --git a/src/utils/mailUtils.js b/src/utils/mailUtils.js index fce92ca..4287160 100644 --- a/src/utils/mailUtils.js +++ b/src/utils/mailUtils.js @@ -78,7 +78,7 @@ module.exports.mailem = async function(to, subject, body, htmlBody = false){ //return the mail info return sentMail; }catch(err){ - loggerUtils.dumpError(err, new Date(), 'mail/'); + loggerUtils.dumpError(err, new Date(), 'crash/mail/'); } } From 7cda9517d4aa61bd75bd07f8ca5d11ba09afe073 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 23 Oct 2025 07:50:49 -0400 Subject: [PATCH 171/209] Added basic about page. --- config.example.json | 3 +- config.example.jsonc | 4 ++- src/controllers/aboutController.js | 27 ++++++++++++++++ src/routers/aboutRouter.js | 34 +++++++++++++++++++++ src/server.js | 2 ++ src/views/about.ejs | 49 ++++++++++++++++++++++++++++++ src/views/partial/navbar.ejs | 9 ++++-- www/css/about.css | 39 ++++++++++++++++++++++++ 8 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/controllers/aboutController.js create mode 100644 src/routers/aboutRouter.js create mode 100644 src/views/about.ejs create mode 100644 www/css/about.css diff --git a/config.example.json b/config.example.json index cb43df6..58445d8 100644 --- a/config.example.json +++ b/config.example.json @@ -32,5 +32,6 @@ "secure": true, "address": "toke@42069.weed", "pass": "CHANGE_ME" - } + }, + "aboutText":"ourfore.st is the one and only original canopy instance. Setup, ran, and administered by rainbownapkin herself. This site exists to provide a featureful, preformant, and comfy replacement for the TTN community." } \ No newline at end of file diff --git a/config.example.jsonc b/config.example.jsonc index 02fb88e..3f1bd0a 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -59,5 +59,7 @@ "secure": true, "address": "toke@42069.weed", "pass": "CHANGE_ME" - } + }, + //Fills the 'about ${instanceName}' section on the /about page, lets users know about your specific instance + "aboutText":"ourfore.st is the one and only original canopy instance. Setup, ran, and administered by rainbownapkin herself. This site exists to provide a featureful, preformant, and comfy replacement for the TTN community." } \ No newline at end of file diff --git a/src/controllers/aboutController.js b/src/controllers/aboutController.js new file mode 100644 index 0000000..2b32b84 --- /dev/null +++ b/src/controllers/aboutController.js @@ -0,0 +1,27 @@ +/*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 .*/ + +//Config +const config = require('../../config.json'); + +//Local Imports +const csrfUtils = require('../utils/csrfUtils'); + +//register page functions +module.exports.get = async function(req, res){ + //Render page + return res.render('about', {aboutText: config.aboutText, instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)}); +} \ No newline at end of file diff --git a/src/routers/aboutRouter.js b/src/routers/aboutRouter.js new file mode 100644 index 0000000..8739880 --- /dev/null +++ b/src/routers/aboutRouter.js @@ -0,0 +1,34 @@ +/*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 { Router } = require('express'); + + +//local imports +const aboutController = require("../controllers/aboutController"); +const presenceUtils = require("../utils/presenceUtils"); + +//globals +const router = Router(); + +//Use presence middleware +router.use(presenceUtils.presenceMiddleware); + +//routing functions +router.get('/', aboutController.get); + +module.exports = router; diff --git a/src/server.js b/src/server.js index 3f47c43..da30147 100644 --- a/src/server.js +++ b/src/server.js @@ -54,6 +54,7 @@ const fileNotFoundController = require('./controllers/404Controller'); //Router //Humie-Friendly const indexRouter = require('./routers/indexRouter'); +const aboutRouter = require('./routers/aboutRouter'); const registerRouter = require('./routers/registerRouter'); const loginRouter = require('./routers/loginRouter'); const profileRouter = require('./routers/profileRouter'); @@ -179,6 +180,7 @@ app.use(sessionUtils.rememberMeMiddleware); //Routes //Humie-Friendly app.use('/', indexRouter); +app.use('/about', aboutRouter); app.use('/register', registerRouter); app.use('/login', loginRouter); app.use('/profile', profileRouter); diff --git a/src/views/about.ejs b/src/views/about.ejs new file mode 100644 index 0000000..8842ff0 --- /dev/null +++ b/src/views/about.ejs @@ -0,0 +1,49 @@ +<%# 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 . %> + + + + <%- include('partial/styles', {instance, user}); %> + <%- include('partial/csrfToken', {csrfToken}); %> + + <%= instance %> - about + + + <%- include('partial/navbar', {user}); %> +
+

About <%= instance %>

+
+

About <%= instance %>

+ <%# It's not XSS if the text came from the config made by the server admin. If you can't trust that, you're already fucked.%> +

<%- aboutText %>

+

About Canopy

+

Canopy is the software behind <%= instance %>. Originally written by rainbownapkin for the founding instance, + ourfore.st. Ourfore.st was originally a cytube instance, set up after the 2021 + shutdown of TTN, a movie watching/weed smoking community related to the r/trees + subreddit. +
+
+ After a years of service, thousands of lines worth of stapled on modifications, the shutdown of sister sites, + it was decided that the original cytube fork, fore.st, had been run past it's prime. In summer/fall 2024, work began on a + replacement. The resulting software became Canopy, which was + first used to run the ourfore.st instance in late 2025.

+
+
+ +
+ <%- include('partial/scripts', {user}); %> +
+ diff --git a/src/views/partial/navbar.ejs b/src/views/partial/navbar.ejs index 3dc3065..aa9a56a 100644 --- a/src/views/partial/navbar.ejs +++ b/src/views/partial/navbar.ejs @@ -14,16 +14,19 @@ 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 . %> \ No newline at end of file diff --git a/www/css/about.css b/www/css/about.css new file mode 100644 index 0000000..52ce658 --- /dev/null +++ b/www/css/about.css @@ -0,0 +1,39 @@ +/*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 .*/ +#about-div{ + display: flex; + flex-direction: column; +} + +@media (orientation: landscape){ + #about-div{ + margin: 0 25%; + } +} + +@media (orientation: portrait){ + #about-div{ + margin: 0 10%; + } +} + +h1{ + text-align: center; +} + +#about-text{ + padding: 0 0.5em; +} \ No newline at end of file From b8de76b448c25deb88468535ef975996f5c2e9e1 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 23 Oct 2025 08:14:25 -0400 Subject: [PATCH 172/209] Fixed canopyUXUtils.getInstanceName() --- www/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/utils.js b/www/js/utils.js index 1c25aeb..4532934 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -51,7 +51,7 @@ class canopyUXUtils{ } getInstanceName(){ - return document.querySelector("#instance-title a").innerText; + return document.querySelector("#instance-title").innerText; } async awaitNextFrame(){ From db3ec58ad9fa3134ae414919bc4839fe3056912e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 24 Oct 2025 00:26:29 -0400 Subject: [PATCH 173/209] Simplified chatMetadata based classes. --- src/app/channel/chat.js | 7 +------ src/app/chatMetadata.js | 8 +++++++- src/app/pm/message.js | 7 +------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/app/channel/chat.js b/src/app/channel/chat.js index c9c5853..eb09071 100644 --- a/src/app/channel/chat.js +++ b/src/app/channel/chat.js @@ -27,12 +27,7 @@ class chat extends chatMetadata{ */ constructor(user, flair, highLevel, msg, type, links){ //Call derived constructor - super(flair, highLevel, msg, type, links); - - /** - * User who sent the message - */ - this.user = user; + super(user, flair, highLevel, msg, type, links); } } diff --git a/src/app/chatMetadata.js b/src/app/chatMetadata.js index df5fbd6..23320ac 100644 --- a/src/app/chatMetadata.js +++ b/src/app/chatMetadata.js @@ -20,13 +20,19 @@ along with this program. If not, see .*/ class chatMetadata{ /** * Instantiates a chat metadata object + * @param {String} user - Name of user who sent the message * @param {String} flair - Flair ID String for the flair used to send the message * @param {Number} highLevel - Number representing current high level * @param {String} msg - Contents of the message, with links replaced with numbered file-seperator markers * @param {String} type - Message Type Identifier, used for client-side processing. * @param {Array} links - Array of URLs/Links included in the message. */ - constructor(flair, highLevel, msg, type, links){ + constructor(user, flair, highLevel, msg, type, links){ + /** + * Name of user who sent the message + */ + this.user = user; + /** * Flair ID String for the flair used to send the message */ diff --git a/src/app/pm/message.js b/src/app/pm/message.js index 28dc46f..a6604ff 100644 --- a/src/app/pm/message.js +++ b/src/app/pm/message.js @@ -27,12 +27,7 @@ class message extends chatMetadata{ */ constructor(user, recipients, flair, highLevel, msg, type, links){ //Call derived constructor - super(flair, highLevel, msg, type, links); - - /** - * Name of user who sent the message - */ - this.user = user; + super(user, flair, highLevel, msg, type, links); /** * Array of usernames who are supposed to receive the message From f95a0ae48c4dcd7c1e03b8a61de01d2ef3b1ddfd Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 25 Oct 2025 09:46:28 -0400 Subject: [PATCH 174/209] Added queue debugging. --- config.example.json | 1 + config.example.jsonc | 5 ++++ src/app/channel/media/queue.js | 49 ++++++++++++++++++++++++++++++++- src/schemas/permissionSchema.js | 6 ++++ src/server.js | 6 ++-- src/utils/configCheck.js | 5 ++++ 6 files changed, 68 insertions(+), 4 deletions(-) diff --git a/config.example.json b/config.example.json index 58445d8..6e7519f 100644 --- a/config.example.json +++ b/config.example.json @@ -8,6 +8,7 @@ "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", "migrate": false, "dropLegacyTokes": false, + "debug": false, "secrets":{ "passwordSecret": "CHANGE_ME", "rememberMeSecret": "CHANGE_ME", diff --git a/config.example.jsonc b/config.example.jsonc index 3f1bd0a..75d4591 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -24,6 +24,11 @@ //Requires migration to be disabled before it takes effect. //WARNING: this does NOT affect user toke counts, migrated or otherwise. Use carefully! "dropLegacyTokes": false, + //Enters the server into debug mode, allows specific commands to be emitted from the client-side dev console + //Usually to get the server to dump some sort of internal data for debugging purposes. + //Obviously, this means enabling this can have some gnar implications. + //Probably don't enable this on your production server unless you REALLY REALLY have to, and you probably don't. + "debug": false, //Server Secrets //Be careful with what you keep in secrets, you should use special chars, but test your deployment, as some chars may break account registration //An update to either kill the server and bitch about the issue in console is planned so it's not so confusing for new admins diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 8284420..8fa933f 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -18,10 +18,12 @@ along with this program. If not, see .*/ const validator = require('validator'); //Local imports +const config = require('../../../../config.json'); const queuedMedia = require('./queuedMedia'); const yanker = require('../../../utils/media/yanker'); const loggerUtils = require('../../../utils/loggerUtils'); const channelModel = require('../../../schemas/channel/channelSchema'); +const permissionModel = require('../../../schemas/permissionSchema'); /** * Object represneting a single channel's media queue @@ -114,6 +116,11 @@ class queue{ socket.on("move", (data) => {this.moveMedia(socket, data)}); socket.on("lock", () => {this.toggleLock(socket)}); socket.on("goLive", (data) => {this.goLive(socket, data)}); + + //If debug mode is enabled + if(config.debug){ + socket.on("dumpQueue", (data) => {this.dumpQueue(socket, data)}); + } } //--- USER FACING QUEUEING FUNCTIONS --- @@ -404,6 +411,44 @@ class queue{ } } + async dumpQueue(socket, data){ + //If we somehow got here while config.debug is disabled, or the user isn't allowed to preform server-side debugging + if(!(config.debug && await permissionModel.permCheck(socket.user, "debug"))){ + //FUCKIN' CHEESE IT! + return; + } + + //If a full data dump was requested + if(data != null && data.full){ + //Pull the channel DB doc + const chanDB = await channelModel.findOne({name:this.channel.name}); + + //Cook and emit a new object from all of the data + socket.emit("dumpQueue", { + cache: { + schedule: Array.from(this.schedule), + nowPlaying: this.nowPlaying + }, + DB: { + schedule: chanDB.media.scheduled, + nowPlaying: chanDB.media.nowPlaying, + archived: chanDB.media.archived, + } + }); + + //DONE. + return; + } + + //Otherwise, just dump whats in RAM + socket.emit("dumpQueue", { + cache: { + schedule: Array.from(this.schedule), + nowPlaying: this.nowPlaying + } + }); + } + //--- INTERNAL USE ONLY QUEUEING FUNCTIONS --- /** * Clears and scheduling timers @@ -1788,7 +1833,9 @@ class queue{ chanDB.media.scheduled = newSched; //Save the DB - await chanDB.save(); + await chanDB.save() + + //End the media; //if something fucked up }catch(err){ diff --git a/src/schemas/permissionSchema.js b/src/schemas/permissionSchema.js index 108c888..322e8e2 100644 --- a/src/schemas/permissionSchema.js +++ b/src/schemas/permissionSchema.js @@ -100,6 +100,12 @@ const permissionSchema = new mongoose.Schema({ type: channelPermissionSchema, default: () => ({}) }, + debug: { + type: mongoose.SchemaTypes.String, + enum: rankEnum, + default: "admin", + required: true + }, }); //Statics diff --git a/src/server.js b/src/server.js index da30147..50dcda0 100644 --- a/src/server.js +++ b/src/server.js @@ -78,9 +78,6 @@ const config = require('../config.json'); const port = config.port; const dbUrl = `mongodb://${config.db.user}:${config.db.pass}@${config.db.address}:${config.db.port}/${config.db.database}`; -//Check for insecure config -configCheck.securityCheck(); - //Define express const app = express(); @@ -234,6 +231,9 @@ async function asyncKickStart(){ //Kick off scheduled-jobs scheduler.kickoff(); + //Check for insecure config + configCheck.securityCheck(); + //Increment launch counter await statModel.incrementLaunchCount(); diff --git a/src/utils/configCheck.js b/src/utils/configCheck.js index d979b32..411c8b8 100644 --- a/src/utils/configCheck.js +++ b/src/utils/configCheck.js @@ -74,4 +74,9 @@ module.exports.securityCheck = function(){ if(!validator.isStrongPassword(config.mail.pass) || config.mail.pass == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure Email Password! Change Email password!"); } + + //check debug mode + if(config.debug){ + loggerUtil.consoleWarn("Debug mode enabled! Understand the risks and security implications before enabling on production servers!"); + } } \ No newline at end of file From 37990ff8c31862db834a7555f706302188013da8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 25 Oct 2025 09:55:40 -0400 Subject: [PATCH 175/209] Traded bug with queue.end() being called as volatile from functions which handle their own DB save, in which stale item was left in cache, for a simple queue rending bug. --- src/app/channel/media/queue.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 8fa933f..ec44c81 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -1209,6 +1209,12 @@ class queue{ return this.endLivestream(wasPlaying, chanDB) } + + //Moved this from the block below to prevent accidental over-caching + //We may need to throw this into it's own conditional if it causes issues + //Take it out of the active schedule + this.schedule.delete(wasPlaying.startTime); + //If we're not in volatile mode and we're not ending a livestream if(!volatile){ //If we wheren't handed a channel @@ -1229,9 +1235,6 @@ class queue{ await chanDB.media.nowPlaying.deleteOne(); } - //Take it out of the active schedule - this.schedule.delete(wasPlaying.startTime); - //If archiving is enabled if(!noArchive){ //Add the item to the channel archive From 166e174397424f1c770d4feef6d579d1451142d9 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 25 Oct 2025 09:56:24 -0400 Subject: [PATCH 176/209] comment --- src/app/channel/media/queue.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index ec44c81..9bbaa01 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -1213,6 +1213,7 @@ class queue{ //Moved this from the block below to prevent accidental over-caching //We may need to throw this into it's own conditional if it causes issues //Take it out of the active schedule + //Ultimitaly though, if something is voltaile it should handle saving chanDB on its own, so this should be a non-issue this.schedule.delete(wasPlaying.startTime); //If we're not in volatile mode and we're not ending a livestream From 787846c7d6ec88efa478f3b9d30bba61dd61f60c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 27 Oct 2025 19:29:07 -0400 Subject: [PATCH 177/209] Updated config example. --- config.example.json | 6 +++--- config.example.jsonc | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config.example.json b/config.example.json index 6e7519f..5d8f957 100644 --- a/config.example.json +++ b/config.example.json @@ -1,9 +1,9 @@ { "instanceName": "Canopy", "verbose": false, - "port": 8080, - "proxied": false, - "protocol": "http", + "port": 8443, + "proxied": true, + "protocol": "https", "domain": "localhost", "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", "migrate": false, diff --git a/config.example.jsonc b/config.example.jsonc index 75d4591..f315d9a 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -6,9 +6,9 @@ //Scream about exceptions in the console "verbose": false, //Port to bind to (most linux/unix systems req root for ports below 1000, you should probably use nginx if you want port 80 or 443) - "port": 8080, + "port": 8443, //Lets the server know it's sitting behind a reverse-proxy - "proxied": false, + "proxied": true, //Protocol (either HTTP or HTTPS) "protocol": "http", //Domain the server is available at, used for server-side link generation From dd66601f0dbd477e58bc53f0234e9f37c4a17753 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 27 Oct 2025 20:31:14 -0400 Subject: [PATCH 178/209] Fixed out of order queues being sent off to clients. --- src/app/channel/media/queue.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 9bbaa01..671fac7 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -1708,8 +1708,8 @@ class queue{ media.earlyEnd = null; } - //Add it to the schedule array as if it where part of the actual schedule map - schedule.push([media.startTime, media]); + //Add it to the temporary schedule array as if it where part of the actual schedule map + schedule.unshift([media.startTime, media]); //Otherwise if it's older }else{ //Then we should be done as archived items are added as they are played/end. From a1f08243308d453b898f77c22b9914e1a52d928c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 28 Oct 2025 06:33:52 -0400 Subject: [PATCH 179/209] Fixed queue rendering issue by changing server-side behavior around queue broadcasting. --- src/app/channel/media/queue.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 671fac7..f52d75c 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -1078,6 +1078,7 @@ class queue{ if(this.nowPlaying != null){ //Silently end the media in RAM so the database isn't stepping on itself up ahead //Alternatively we could've used await, but then we'd be doubling up on DB transactions :P + //VOLATILE END this.end(true, true, true); } @@ -1116,6 +1117,9 @@ class queue{ //Save the channel await chanDB.save(); + + //Broadcast queue since we ended our media item early + this.broadcastQueue(chanDB); }catch(err){ loggerUtils.localExceptionHandler(err); } @@ -1171,7 +1175,7 @@ class queue{ * End currently playing media * @param {Boolean} quiet - Enable to prevent ending the media client-side * @param {Boolean} noArchive - Enable to prevent ended media from being written to channel archive. Deletes media if Volatile is false - * @param {Boolean} volatile - Enable to prevent DB Transactions + * @param {Boolean} volatile - Enable to prevent DB Transactions, also prevents queue broadcasting so the calling code can broadcast once it's done. * @param {Mongoose.Document} chanDB - Pass through Channel Document to save on DB Transactions */ async end(quiet = false, noArchive = false, volatile = false, chanDB){ @@ -1247,9 +1251,6 @@ class queue{ //Save our changes to the DB await chanDB.save(); - }else{ - //broadcast queue using unsaved archive - this.broadcastQueue(chanDB); } }catch(err){ this.broadcastQueue(); From 349a6b82aab76a0948e20f681663a491d7df7f58 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 28 Oct 2025 07:03:16 -0400 Subject: [PATCH 180/209] Moving currently playing items to an invalid spot in the schedule no longer creates ghost items. --- src/app/channel/media/queue.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index f52d75c..3905702 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -642,6 +642,8 @@ class queue{ //Find our media, don't remove it yet since we want to do some more testing first let media = this.getItemByUUID(uuid); + //Create value to hold old media in-case somethin' fucks up + let oldMedia = null; //If we got a bad request if(media == null){ @@ -658,6 +660,8 @@ class queue{ if(media.startTime < new Date().getTime()){ //If the item is currently playing if(media.getEndTime() > new Date().getTime()){ + //Set old media + oldMedia = media; //Dupe media for the rest of the function media = media.clone(); @@ -699,6 +703,13 @@ class queue{ //Reset the start time stamp for re-calculation media.startTimeStamp = 0; + //If there's a cut-off from moving a now-playing item + if(oldMedia != null){ + //Remove it from the queue to prevent ghost items + this.removeMedia(oldMedia.uuid, socket, chanDB, true); + } + + //Schedule in old slot with noSave enabled await this.scheduleMedia([media], socket, chanDB, true); } @@ -721,7 +732,7 @@ class queue{ * @param {String} uuid - UUID of item to reschedule * @param {Socket} socket - Requesting Socket * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access - * @param {Boolean} noScheduling - Disables schedule timer refresh if true + * @param {Boolean} noScheduling - Disables schedule timer refresh and this.save() calls if true, good for internal function calls * @returns {Media} Deleted Media Item */ async removeMedia(uuid, socket, chanDB, noScheduling = false){ @@ -773,8 +784,12 @@ class queue{ }else{ //Broadcast changes this.broadcastQueue(chanDB); - //Save changes to the DB - await chanDB.save(); + + //If saving is disabled + if(!noScheduling){ + //Save changes to the DB + await chanDB.save(); + } } }catch(err){ //If this was originated by someone From e0832c2c1f94ac089cd5e48dacffe347da5f44ed Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 29 Oct 2025 08:35:37 -0400 Subject: [PATCH 181/209] Started working on pushing multiple raw links to user. --- src/app/channel/media/media.js | 20 +++++++++++++------ src/utils/media/yanker.js | 3 +++ src/utils/media/ytdlpUtils.js | 36 ++++++++++++++++++++++++++++------ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/app/channel/media/media.js b/src/app/channel/media/media.js index a072e6e..609f1bf 100644 --- a/src/app/channel/media/media.js +++ b/src/app/channel/media/media.js @@ -26,9 +26,9 @@ class media{ * @param {String} id - Video ID from source (IE: youtube watch code/archive.org file path) * @param {String} type - Original video source * @param {Number} duration - Length of media in seconds - * @param {String} rawLink - URL to raw file copy of media, not applicable to all sources + * @param {String} rawLink - URLs to raw file copies of media, not applicable to all sources, not saved to the DB */ - constructor(title, fileName, url, id, type, duration, rawLink = url){ + constructor(title, fileName, url, id, type, duration, rawLink){ /** * Chosen title of media */ @@ -59,10 +59,18 @@ class media{ */ this.duration = duration; - /** - * URL to raw file copy of media, not applicable to all sources - */ - this.rawLink = rawLink; + if(rawLink == null){ + /** + * URL to raw file copy of media, not applicable to all sources + */ + this.rawLink = { + audio: [], + video: [], + combo: [['default',url]] + }; + }else{ + this.rawLink = rawLink; + } } } diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 8072712..c5109ab 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -67,6 +67,8 @@ module.exports.yankMedia = async function(url, title){ module.exports.refreshRawLink = async function(mediaObj){ switch(mediaObj.type){ case 'yt': + console.log("lolnope"); + /* We're skipping this one for now... //Scrape expiration from query strings const expires = mediaObj.rawLink.match(/expire=([0-9]+)/); //Went with regex for speed, but I figure I'd keep this around in case we want the accuracy of a battle-tested implementation @@ -85,6 +87,7 @@ module.exports.refreshRawLink = async function(mediaObj){ //return media object return mediaObj; + */ } //Return null to tell the calling function there is no refresh required for this media type diff --git a/src/utils/media/ytdlpUtils.js b/src/utils/media/ytdlpUtils.js index 075189a..e6bfcd6 100644 --- a/src/utils/media/ytdlpUtils.js +++ b/src/utils/media/ytdlpUtils.js @@ -100,7 +100,7 @@ module.exports.fetchDailymotionMetadata = async function(id, title){ * @param {String} type - Link type to attach to the resulting media object * @returns {Array} Array of Media objects containing relevant metadata */ -async function fetchVideoMetadata(link, title, type, format = 'b'){ +async function fetchVideoMetadata(link, title, type, format = 'ba,bv'){ //Create media list const mediaList = []; @@ -109,16 +109,40 @@ async function fetchVideoMetadata(link, title, type, format = 'b'){ //Pull data from rawMetadata, sanatizing title to prevent XSS const name = validator.escape(validator.trim(rawMetadata.title)); - const rawLink = rawMetadata.requested_downloads[0].url; + + //Create new raw link object (should we make a class? Probably over kill for a fucking method-less hashtable) + const rawLinks = { + audio: [], + video: [], + combo: [] + } + + //for each item + for(const link of rawMetadata.requested_downloads){ + //if there isn't video included + if(link.vcodec == 'none'){ + //Add the link under the format within the audio map + rawLinks.audio.push([link.format_note, link.url]); + //if there isn't audio included + }else if(link.acodec == 'none'){ + //Add the link under the format within the video map + rawLinks.video.push([link.format_note, link.url]); + //otherwise, it includes audio and video + }else{ + //Add the link under the format within the combo map + rawLinks.combo.push([link.format_note, link.url]); + } + } + const id = rawMetadata.id; //if we where handed a null title if(title == null || title == ''){ //Create new media object from file info substituting filename for title - mediaList.push(new media(name, name, link, id, type, Number(rawMetadata.duration), rawLink)); + mediaList.push(new media(name, name, link, id, type, Number(rawMetadata.duration), rawLinks)); }else{ //Create new media object from file info - mediaList.push(new media(title, name, link, id, type, Number(rawMetadata.duration), rawLink)); + mediaList.push(new media(title, name, link, id, type, Number(rawMetadata.duration), rawLinks)); } //Return list of media @@ -136,10 +160,10 @@ async function fetchVideoMetadata(link, title, type, format = 'b'){ * @param {String} format - Format string to hand YT-DLP, defaults to 'b' * @returns {Object} Metadata dump from YT-DLP */ -async function ytdlpFetch(link, format = 'b'){ +async function ytdlpFetch(link, format = 'ba,ogg'){ //return promise from ytdlp return ytdlp(link, { + format, dumpSingleJson: true, - format }); } \ No newline at end of file From a59b6d0e192d21de69f66446947a1e3b129cf809 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 31 Oct 2025 20:22:17 -0400 Subject: [PATCH 182/209] Started work on syncronizing seperated audio and video tracks into back into one player. --- www/js/channel/mediaHandler.js | 124 ++++++++++++++++++++++++++++++++- www/js/channel/player.js | 4 +- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index f15ef89..0eb0d4e 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -328,6 +328,18 @@ class rawFileBase extends mediaHandler{ //Pull volume from video this.player.volume = this.video.volume; } + + onSeek(event){ + super.onSeek(event); + } + + onBuffer(event){ + super.onBuffer(event); + } + + onPause(event){ + super.onPause(event); + } } /** @@ -397,17 +409,48 @@ class rawFileHandler extends rawFileBase{ //Run derived method super.defineListeners(); + this.video.addEventListener('playing', this.onPlay.bind(this)); this.video.addEventListener('pause', this.onPause.bind(this)); this.video.addEventListener('seeked', this.onSeek.bind(this)); this.video.addEventListener('waiting', this.onBuffer.bind(this)); } + buildPlayer(){ + super.buildPlayer(); + + this.audio = new Audio(); + } + + destroyPlayer(){ + //Call derived method + super.destroyPlayer(); + + //Destroy the audio player + this.audio.pause(); + this.audio.remove(); + } + start(){ //Call derived start super.start(); - //Set video - this.video.src = this.nowPlaying.rawLink; + //Just pull the combo source by default + const combo = this.nowPlaying.rawLink.combo[0] + + //Check if the combo source is null + if(combo != null){ + //Set video + this.video.src = combo[1]; + }else{ + + //Pull video only link + const video = this.nowPlaying.rawLink.video[0] + const audio = this.nowPlaying.rawLink.audio[0]; + + //Set video source + this.video.src = video[1]; + this.audio.src = audio[1]; + } //Set video volume this.video.volume = this.player.volume; @@ -417,14 +460,38 @@ class rawFileHandler extends rawFileBase{ //play video this.video.play(); + + //if we have an audio src + if(this.audio.src != ""){ + //Set audio volume + this.audio.volume = this.player.volume; + + //Play it too + this.audio.play(); + } } play(){ + //play video this.video.play(); + + + //if we have a seperate audio track + if(this.audio.src != ""){ + //Play it too + this.audio.play(); + } } pause(){ + //pause video this.video.pause(); + + //if we have a seperate audio track + if(this.audio.src != ""){ + //Pause it too + this.audio.pause(); + } } sync(timestamp = this.lastTimestamp){ @@ -436,12 +503,65 @@ class rawFileHandler extends rawFileBase{ //Set current video time based on timestamp received from server this.video.currentTime = timestamp; } + + //if we have a seperate audio track + if(this.audio != ""){ + //Re-sync it to the video, regardless if we synced video + this.audio.currentTime = this.video.currentTime; + } } getTimestamp(){ //Return current timestamp return this.video.currentTime; } + + onSeek(event){ + //Call derived event + super.onSeek(event); + + //if we have a seperate audio track + if(this.audio != "" && this.video != null){ + //Set it's timestamp too + this.audio.currentTime = this.video.currentTime; + } + } + + onBuffer(event){ + //Call derived event + super.onBuffer(event); + + //if we have a seperate audio track + if(this.audio != "" && this.video != null){ + //Set it's timestamp + this.audio.currentTime = this.video.currentTime; + //pause it + this.audio.pause(); + } + } + + onPause(event){ + //Call derived event + super.onPause(event); + + //if we have a seperate audio track + if(this.audio != "" && this.video != null){ + //Set it's timestamp + this.audio.currentTime = this.video.currentTime; + //pause it + this.audio.pause(); + } + } + + onPlay(event){ + //if we have a seperate audio track + if(this.audio != "" && this.video != null){ + //Set it's timestamp + this.audio.currentTime = this.video.currentTime; + //pause it + this.audio.play(); + } + } } /** diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 568886f..612d946 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -195,12 +195,12 @@ class player{ //If we're running a source from IA if(data.media.type == 'ia'){ //Replace specified CDN with generic URL, in-case of hard reload - data.media.rawLink = data.media.rawLink.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download") + //data.media.rawLink = data.media.rawLink.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download") //If we have an IA source and a custom IA CDN Server set if(data.media.type == 'ia' && localStorage.getItem("IACDN") != ""){ //Generate and set new link - data.media.rawLink = data.media.rawLink.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`); + //data.media.rawLink = data.media.rawLink.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`); } } From ccb1d91a5bed870f04931e5779d5fc70facbff88 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 07:10:40 -0400 Subject: [PATCH 183/209] Seperate audio tracks are now *reasonably* synchronized to their videos. This may need more testing to perfect. --- www/js/channel/mediaHandler.js | 46 +++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 0eb0d4e..b4792af 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -401,6 +401,15 @@ class rawFileHandler extends rawFileBase{ //Call derived constructor super(client, player, media, 'raw'); + //Re-sync audio every .1 seconds + this.audioSyncDelta = 1000; + + //Set audio sync tolerance to .3 + this.audioSyncTolerance = .3; + + //Create value to hold last calculated difference between audio and video timestamp + this.lastAudioDelta = 0; + //Define listeners this.defineListeners(); } @@ -428,6 +437,7 @@ class rawFileHandler extends rawFileBase{ //Destroy the audio player this.audio.pause(); this.audio.remove(); + clearInterval(this.audioInterval); } start(){ @@ -461,14 +471,11 @@ class rawFileHandler extends rawFileBase{ //play video this.video.play(); - //if we have an audio src + /*/if we have an audio src if(this.audio.src != ""){ - //Set audio volume - this.audio.volume = this.player.volume; - //Play it too this.audio.play(); - } + }*/ } play(){ @@ -476,22 +483,22 @@ class rawFileHandler extends rawFileBase{ this.video.play(); - //if we have a seperate audio track + /*/if we have a seperate audio track if(this.audio.src != ""){ //Play it too this.audio.play(); - } + }*/ } pause(){ //pause video this.video.pause(); - //if we have a seperate audio track + /*/if we have a seperate audio track if(this.audio.src != ""){ //Pause it too this.audio.pause(); - } + }*/ } sync(timestamp = this.lastTimestamp){ @@ -537,6 +544,7 @@ class rawFileHandler extends rawFileBase{ this.audio.currentTime = this.video.currentTime; //pause it this.audio.pause(); + clearInterval(this.audioInterval); } } @@ -550,18 +558,38 @@ class rawFileHandler extends rawFileBase{ this.audio.currentTime = this.video.currentTime; //pause it this.audio.pause(); + clearInterval(this.audioInterval); } } onPlay(event){ //if we have a seperate audio track if(this.audio != "" && this.video != null){ + //Set audio volume + this.audio.volume = this.player.volume; //Set it's timestamp this.audio.currentTime = this.video.currentTime; //pause it this.audio.play(); + this.audioInterval = setInterval(this.syncAudio.bind(this), this.audioSyncDelta); } } + + syncAudio(){ + //get current audi odelta + const audioDelta = this.video.currentTime - this.audio.currentTime; + + //If the audio is out of sync enough that someone would notice + if(Math.abs(audioDelta) > this.audioSyncTolerance){ + //Set audio volume + this.audio.volume = this.player.volume; + //Re-sync the audio + this.audio.currentTime = this.video.currentTime + audioDelta; + } + + //Set last audio delta + this.lastAudioDelta = audioDelta; + } } /** From b57d723d62fbbb5e287c14769ff3a034daa1af90 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 07:45:23 -0400 Subject: [PATCH 184/209] Fixed client-side IACDN setting. --- www/js/channel/channel.js | 1 + www/js/channel/panels/settingsPanel.js | 2 +- www/js/channel/player.js | 16 +++++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js index 8bc9a24..5d50499 100644 --- a/www/js/channel/channel.js +++ b/www/js/channel/channel.js @@ -242,6 +242,7 @@ class channel{ //If we're playing a video from Internet Archive if(nowPlaying != null && nowPlaying.type == 'ia'){ + console.log("RELOAD"); //Hard reload the media, forcing media handler re-creation this.player.hardReload(); } diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js index d3949ef..e9f0519 100644 --- a/www/js/channel/panels/settingsPanel.js +++ b/www/js/channel/panels/settingsPanel.js @@ -137,7 +137,7 @@ class settingsPanel extends panelObj{ //If we hit enter if(event.key == "Enter"){ //If we have an invalid server string - if(!(this.iaCDN.value.match(/^ia[0-9]{6}\...$/g) || this.iaCDN.value == "")){ + if(!(this.iaCDN.value.match(/^(ia|dn)[0-9]{6}\...$/g) || this.iaCDN.value == "")){ //reset back to what was set before this.iaCDN.value = localStorage.getItem('IACDN'); diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 612d946..3303b14 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -192,15 +192,21 @@ class player{ this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media); //Otherwise, if we have a raw-file compatible source }else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){ + //If we're running a source from IA if(data.media.type == 'ia'){ - //Replace specified CDN with generic URL, in-case of hard reload - //data.media.rawLink = data.media.rawLink.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download") - //If we have an IA source and a custom IA CDN Server set if(data.media.type == 'ia' && localStorage.getItem("IACDN") != ""){ - //Generate and set new link - //data.media.rawLink = data.media.rawLink.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`); + for(const linkIndex in data.media.rawLink.combo){ + //Pull link to sprinkle on dat syntatic sugar (tasty) + let link = data.media.rawLink.combo[linkIndex][1] + + //Replace specified CDN with generic URL, in-case of hard reload + link = link.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download"); + + //Generate and set new link + data.media.rawLink.combo[linkIndex][1] = link.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`); + } } } From a70879c76cc42c6ddb94b1709a63864d4132fee8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 07:55:34 -0400 Subject: [PATCH 185/209] Quick cleanup. --- src/views/partial/panels/settings.ejs | 2 +- www/js/channel/player.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/views/partial/panels/settings.ejs b/src/views/partial/panels/settings.ejs index 5fd29d7..6fe53d2 100644 --- a/src/views/partial/panels/settings.ejs +++ b/src/views/partial/panels/settings.ejs @@ -20,7 +20,7 @@ along with this program. If not, see . %>

Youtube Player Type:

diff --git a/www/js/channel/player.js b/www/js/channel/player.js index 3303b14..ddbdd0e 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -276,8 +276,6 @@ class player{ //End current media handler this.end(); - console.log(data); - //Restart from last media handlers this.start(data); } From 02c4d214fa6ea8505e2a6df868802ef35aa9303d Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 08:09:54 -0400 Subject: [PATCH 186/209] Started work on re-implementation of youtube raw-link reloading. --- src/utils/media/yanker.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index c5109ab..76df22e 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -67,7 +67,6 @@ module.exports.yankMedia = async function(url, title){ module.exports.refreshRawLink = async function(mediaObj){ switch(mediaObj.type){ case 'yt': - console.log("lolnope"); /* We're skipping this one for now... //Scrape expiration from query strings const expires = mediaObj.rawLink.match(/expire=([0-9]+)/); @@ -80,14 +79,16 @@ module.exports.refreshRawLink = async function(mediaObj){ return null; } + */ + //Re-fetch media metadata metadata = await ytdlpUtil.fetchYoutubeMetadata(mediaObj.id); + //Refresh media rawlink from metadata mediaObj.rawLink = metadata[0].rawLink; //return media object return mediaObj; - */ } //Return null to tell the calling function there is no refresh required for this media type From 366766d0a369b015d45a534b0021d26a82b33b41 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 08:51:30 -0400 Subject: [PATCH 187/209] Finished up work with youtube raw link refreshing. --- src/utils/media/yanker.js | 49 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 76df22e..2dff869 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -67,28 +67,41 @@ module.exports.yankMedia = async function(url, title){ module.exports.refreshRawLink = async function(mediaObj){ switch(mediaObj.type){ case 'yt': - /* We're skipping this one for now... - //Scrape expiration from query strings - const expires = mediaObj.rawLink.match(/expire=([0-9]+)/); - //Went with regex for speed, but I figure I'd keep this around in case we want the accuracy of a battle-tested implementation - //const expires = new URL(mediaObj.rawLink).searchParams.get("expire"); + //Create boolean to hold expired state + let expired = false; + //Create boolean to hold whether or not rawLink object is empty + let empty = true; - //If we have a valid raw file link that will be good by the end of the video - if(expires != null && (expires * 1000) > mediaObj.getEndTime()){ - //Return null to tell the calling function there is no refresh required for this video at this time - return null; + //For each link map in the rawLink object + for(const key of Object.keys(mediaObj.rawLink)){ + //Ignore da wombo-combo since it's probably just the fuckin regular URL + if(key != "combo"){ + for(const link of mediaObj.rawLink[key]){ + //Let it be known, this bitch got links + empty = false; + //Get expiration parameter from the link + const expires = new URL(link[1]).searchParams.get("expire") * 1000; + + //If this shit's already expired + if(expires < Date.now()){ + //Set expired to true, don't directly set the bool because we don't ever want to unset this flag + expired = true; + } + } + } } - */ + //If the raw link object is empty or expired + if(empty || expired){ + //Re-fetch media metadata + metadata = await ytdlpUtil.fetchYoutubeMetadata(mediaObj.id); + + //Refresh media rawlink from metadata + mediaObj.rawLink = metadata[0].rawLink; - //Re-fetch media metadata - metadata = await ytdlpUtil.fetchYoutubeMetadata(mediaObj.id); - - //Refresh media rawlink from metadata - mediaObj.rawLink = metadata[0].rawLink; - - //return media object - return mediaObj; + //return media object + return mediaObj; + } } //Return null to tell the calling function there is no refresh required for this media type From c3712bfd779e9a1243b86454fcd02bb21114b93f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 09:10:47 -0400 Subject: [PATCH 188/209] Added some nullchecks to mediaHandler.js to quiet down stale handlers right after death. --- www/js/channel/mediaHandler.js | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index b4792af..3dd11ae 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -470,35 +470,18 @@ class rawFileHandler extends rawFileBase{ //play video this.video.play(); - - /*/if we have an audio src - if(this.audio.src != ""){ - //Play it too - this.audio.play(); - }*/ } play(){ + super.play(); //play video this.video.play(); - - - /*/if we have a seperate audio track - if(this.audio.src != ""){ - //Play it too - this.audio.play(); - }*/ } pause(){ + super.pause(); //pause video this.video.pause(); - - /*/if we have a seperate audio track - if(this.audio.src != ""){ - //Pause it too - this.audio.pause(); - }*/ } sync(timestamp = this.lastTimestamp){ @@ -512,7 +495,7 @@ class rawFileHandler extends rawFileBase{ } //if we have a seperate audio track - if(this.audio != ""){ + if(this.audio != null && this.audio != ""){ //Re-sync it to the video, regardless if we synced video this.audio.currentTime = this.video.currentTime; } @@ -528,7 +511,7 @@ class rawFileHandler extends rawFileBase{ super.onSeek(event); //if we have a seperate audio track - if(this.audio != "" && this.video != null){ + if(this.audio != null && this.audio != "" && this.video != null){ //Set it's timestamp too this.audio.currentTime = this.video.currentTime; } @@ -539,7 +522,7 @@ class rawFileHandler extends rawFileBase{ super.onBuffer(event); //if we have a seperate audio track - if(this.audio != "" && this.video != null){ + if(this.audio != null && this.audio != "" && this.video != null){ //Set it's timestamp this.audio.currentTime = this.video.currentTime; //pause it @@ -553,7 +536,7 @@ class rawFileHandler extends rawFileBase{ super.onPause(event); //if we have a seperate audio track - if(this.audio != "" && this.video != null){ + if(this.audio != null && this.audio != "" && this.video != null){ //Set it's timestamp this.audio.currentTime = this.video.currentTime; //pause it @@ -564,7 +547,7 @@ class rawFileHandler extends rawFileBase{ onPlay(event){ //if we have a seperate audio track - if(this.audio != "" && this.video != null){ + if(this.audio != null && this.audio != "" && this.video != null){ //Set audio volume this.audio.volume = this.player.volume; //Set it's timestamp From dd36b1d923ee34d7c9343c861531de3caa0ab393 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 1 Nov 2025 11:34:50 -0400 Subject: [PATCH 189/209] Optimized queue.removeRange() --- src/app/channel/media/queue.js | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 3905702..d76a92a 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -360,6 +360,7 @@ class queue{ chanDB.media.liveRemainder = this.nowPlaying.uuid; //Save the chanDB + console.log("Saving db from goLive() :363"); await chanDB.save(); } @@ -592,9 +593,24 @@ class queue{ //For each item for(let item of foundItems){ //Remove media, passing down chanDB so we're not looking again and again - await this.removeMedia(item.uuid, socket, chanDB); + await this.removeMedia(item.uuid, socket, chanDB, true); + + //If this is the current item + if(this.nowPlaying != null && item.uuid == this.nowPlaying.uuid){ + //End it + await this.end(false, true, false, chanDB); + } } + + //Save data to channe database + await chanDB.save(); + + //Refresh next timer + await this.refreshNextTimer(); + + console.log("Saved by removeRange() :599"); + }catch(err){ //If this was originated by someone if(socket != null){ @@ -788,6 +804,7 @@ class queue{ //If saving is disabled if(!noScheduling){ //Save changes to the DB + console.log("Saving db from removeMedia() :792"); await chanDB.save(); } } @@ -844,6 +861,7 @@ class queue{ //If saving is enabled (seperate from all DB transactions since caller function may want modifications but handle saving on its own) if(!noScheduling){ + console.log("Saving db from removeMedia() :849"); await chanDB.save(); } @@ -1034,6 +1052,7 @@ class queue{ try{ //If saving is enabled (seperate from all DB transactions since caller function may want modifications but handle saving on its own) if(!noSave){ + console.log("Saving db from scheduleMedia() :1040"); //Save the database await chanDB.save(); } @@ -1130,6 +1149,9 @@ class queue{ return record.uuid != mediaObj.uuid; }); + + console.log("Saving db from start() :1138"); + //Save the channel await chanDB.save(); @@ -1264,6 +1286,9 @@ class queue{ //broadcast queue using unsaved archive, run this before chanDB.save() for better responsiveness this.broadcastQueue(chanDB); + + console.log("Saving db from end() :1275"); + //Save our changes to the DB await chanDB.save(); } @@ -1293,6 +1318,7 @@ class queue{ } //Disable stream lock this.streamLock = false; + console.log("testem"); //We don't have to save here since someone else will do it for us :) //Reminder for those of us reading this in the future since I'm a dipshit: this only clears the DB liveRemainder, NOT the RAM backed variable @@ -1359,6 +1385,9 @@ class queue{ //Throw the livestream into the archive chanDB.media.archived.push(wasPlaying); + + console.log("Saving db from livestreamOverwriteSchedule() :1373"); + //Save the DB await chanDB.save(); @@ -1852,6 +1881,9 @@ class queue{ //Update schedule to only contain what hasn't been played yet chanDB.media.scheduled = newSched; + + console.log("Saving db from rehydrateQueue() :1869"); + //Save the DB await chanDB.save() From 75301ec7d9e1efd0416f81115ccd487ae53c13c5 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 3 Nov 2025 00:13:17 -0500 Subject: [PATCH 190/209] Finished optimizing automated queue transactions. --- src/app/channel/media/queue.js | 53 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index d76a92a..0036019 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -360,7 +360,6 @@ class queue{ chanDB.media.liveRemainder = this.nowPlaying.uuid; //Save the chanDB - console.log("Saving db from goLive() :363"); await chanDB.save(); } @@ -593,12 +592,12 @@ class queue{ //For each item for(let item of foundItems){ //Remove media, passing down chanDB so we're not looking again and again - await this.removeMedia(item.uuid, socket, chanDB, true); + await this.removeMedia(item.uuid, socket, chanDB, true, true); //If this is the current item if(this.nowPlaying != null && item.uuid == this.nowPlaying.uuid){ //End it - await this.end(false, true, false, chanDB); + await this.end(false, true, false, chanDB, true); } } @@ -609,7 +608,8 @@ class queue{ //Refresh next timer await this.refreshNextTimer(); - console.log("Saved by removeRange() :599"); + //Broadcast Queue + await this.broadcastQueue(chanDB); }catch(err){ //If this was originated by someone @@ -749,9 +749,10 @@ class queue{ * @param {Socket} socket - Requesting Socket * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access * @param {Boolean} noScheduling - Disables schedule timer refresh and this.save() calls if true, good for internal function calls + * @param {Boolean} noBroadcast - Disables schedule broadcasting to clients, good for internal function calls * @returns {Media} Deleted Media Item */ - async removeMedia(uuid, socket, chanDB, noScheduling = false){ + async removeMedia(uuid, socket, chanDB, noScheduling = false, noBroadcast = false){ //If we're streamlocked if(this.streamLock){ //If an originating socket was provided for this request @@ -798,13 +799,15 @@ class queue{ } //Otherwise }else{ - //Broadcast changes - this.broadcastQueue(chanDB); + //If broadcasting is enabled + if(!noBroadcast){ + //Broadcast changes + this.broadcastQueue(chanDB); + } //If saving is disabled if(!noScheduling){ //Save changes to the DB - console.log("Saving db from removeMedia() :792"); await chanDB.save(); } } @@ -861,16 +864,21 @@ class queue{ //If saving is enabled (seperate from all DB transactions since caller function may want modifications but handle saving on its own) if(!noScheduling){ - console.log("Saving db from removeMedia() :849"); await chanDB.save(); } - //Broadcast the channel - this.broadcastQueue(chanDB); + //If broadcasting is enabled + if(!noBroadcast){ + //Broadcast the channel + this.broadcastQueue(chanDB); + } }catch(err){ - //Broadcast the channel - this.broadcastQueue(); + //If broadcasting is enabled + if(!noBroadcast){ + //Broadcast the channel + this.broadcastQueue(); + } //If this was originated by someone if(socket != null){ @@ -1052,7 +1060,6 @@ class queue{ try{ //If saving is enabled (seperate from all DB transactions since caller function may want modifications but handle saving on its own) if(!noSave){ - console.log("Saving db from scheduleMedia() :1040"); //Save the database await chanDB.save(); } @@ -1149,9 +1156,6 @@ class queue{ return record.uuid != mediaObj.uuid; }); - - console.log("Saving db from start() :1138"); - //Save the channel await chanDB.save(); @@ -1286,9 +1290,6 @@ class queue{ //broadcast queue using unsaved archive, run this before chanDB.save() for better responsiveness this.broadcastQueue(chanDB); - - console.log("Saving db from end() :1275"); - //Save our changes to the DB await chanDB.save(); } @@ -1318,7 +1319,6 @@ class queue{ } //Disable stream lock this.streamLock = false; - console.log("testem"); //We don't have to save here since someone else will do it for us :) //Reminder for those of us reading this in the future since I'm a dipshit: this only clears the DB liveRemainder, NOT the RAM backed variable @@ -1385,9 +1385,6 @@ class queue{ //Throw the livestream into the archive chanDB.media.archived.push(wasPlaying); - - console.log("Saving db from livestreamOverwriteSchedule() :1373"); - //Save the DB await chanDB.save(); @@ -1509,7 +1506,7 @@ class queue{ const mediaObj = entry[1]; //Remove media from queue without calling chanDB.save() to make room before we move everything - await this.removeMedia(mediaObj.uuid, null, chanDB, true); + await this.removeMedia(mediaObj.uuid, null, chanDB, true, true); mediaObj.genUUID(); @@ -1525,6 +1522,9 @@ class queue{ //Schedule the moved schedule, letting scheduleMedia save our changes for us, starting w/o saves to prevent over-saving await this.scheduleMedia(newSched, null, chanDB); + + //Broadcast the queue now that everything is hunky dory + await this.broadcastQueue(chanDB); }catch(err){ //Null out live remainder for the next stream this.liveRemainder = null; @@ -1881,9 +1881,6 @@ class queue{ //Update schedule to only contain what hasn't been played yet chanDB.media.scheduled = newSched; - - console.log("Saving db from rehydrateQueue() :1869"); - //Save the DB await chanDB.save() From ade2a4210dc0170b2be2e28d42e16bea30d926da Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 3 Nov 2025 19:07:38 -0500 Subject: [PATCH 191/209] User IP Hashes are now salted with 24 bits from a cryptographically secure random generation function formatted into base 64 for extra privacy/security. --- src/app/chatPreprocessor.js | 1 - src/schemas/user/userBanSchema.js | 4 +--- src/schemas/user/userSchema.js | 6 ++---- src/utils/hashUtils.js | 35 +++++++++++++++++++++++++------ 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/app/chatPreprocessor.js b/src/app/chatPreprocessor.js index be7cf26..a4331ab 100644 --- a/src/app/chatPreprocessor.js +++ b/src/app/chatPreprocessor.js @@ -132,7 +132,6 @@ class chatPreprocessor{ commandObj.links = []; //For each link sent from the client - //this.rawData.links.forEach((link) => { for (const link of commandObj.rawData.links){ //Add a marked link object to our links array commandObj.links.push(await linkUtils.markLink(link)); diff --git a/src/schemas/user/userBanSchema.js b/src/schemas/user/userBanSchema.js index 0c1db1c..0fd352c 100644 --- a/src/schemas/user/userBanSchema.js +++ b/src/schemas/user/userBanSchema.js @@ -73,8 +73,6 @@ const userBanSchema = new mongoose.Schema({ * @returns {Mongoose.Document} Found ban Document if one exists. */ userBanSchema.statics.checkBanByIP = async function(ip){ - //Get hash of ip - const ipHash = hashUtil.hashIP(ip); //Get all bans const banDB = await this.find({}); //Create null variable to hold any found ban @@ -106,7 +104,7 @@ userBanSchema.statics.checkBanByIP = async function(ip){ const curHash = ban.ips.hashed[ipIndex]; //Check the current hash against the given hash - if(ipHash == curHash){ + if(hashUtil.compareIPHash(ip, curHash)){ //If it matches we found the ban foundBan = ban; diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 2d8517c..41dfcda 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -757,8 +757,6 @@ userSchema.methods.tattooIPRecord = async function(ip){ lastLog: new Date() }; - //We should really start using for loops and stop acting like its 2008 - //Though to be quite honest this bit would be particularly brutal without them //For every user in the userlist for(let curUser of users){ //Ensure we're not checking the user against itself @@ -766,7 +764,7 @@ userSchema.methods.tattooIPRecord = async function(ip){ //For every IP record in the current user for(let curRecord of curUser.recentIPs){ //If it matches the current ipHash - if(curRecord.ipHash == ipHash){ + if(hashUtil.compareIPHash(ip, curRecord.ipHash)){ //Check if we've already marked the user as an alt const foundAlt = this.alts.indexOf(curUser._id); @@ -803,7 +801,7 @@ userSchema.methods.tattooIPRecord = async function(ip){ //Look for matching ip record function checkHash(ipRecord){ //return matching records - return ipRecord.ipHash == ipHash; + return hashUtil.compareIPHash(ip, ipRecord.ipHash); } } diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index e60d81e..a428066 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -60,17 +60,40 @@ module.exports.compareLegacyPassword = function(pass, hash){ * * Provides a basic level of privacy by only logging salted hashes of IP's * @param {String} ip - IP to hash - * @returns {String} Hashed/Peppered IP Adress + * @param {String} salt - (optional) string to salt IP with, leave empty to default to a securely generated string encoded in base64 + * @returns {String} Hashed/Peppered/Salted IP Address */ -module.exports.hashIP = function(ip){ +module.exports.hashIP= function(ip, salt){ //Create hash object const hashObj = crypto.createHash('sha512'); - //add IP and pepper to the hash - hashObj.update(`${ip}${config.secrets.ipSecret}`); + //If we wheren't provided salt + if(salt == null){ + //Generate salt with cryptographically secure rng function + const rawSalt = crypto.randomBytes(24); + //Convert generated salt to base64 + salt = rawSalt.toString('base64'); + } - //return the IP hash as a string - return hashObj.digest('hex'); + //Generate new salted hash + hashObj.update(`${ip}${config.secrets.ipSecret}${salt}`); + + //Convert hash data into a base64 string + const hash = hashObj.digest('base64'); + + //Return salty hash + return `${salt}$${hash}`; +} + +module.exports.compareIPHash = function(ip, hash){ + //Split hash by salt delimiter + const splitHash = hash.split("$"); + + //Re-generate hash from received plaintext IP and salt scraped from existing hash + const tempHash = module.exports.hashIP(ip, splitHash[0]); + + //If the hash we calculates matches the original + return tempHash == hash; } /** From 35fd81e1b2fb8c9fafc32b78aa4a9de75e18fd20 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 3 Nov 2025 19:11:31 -0500 Subject: [PATCH 192/209] Improved JSDoc for IP Hash Comparison function. --- src/utils/hashUtils.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index a428066..47352fb 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -85,6 +85,14 @@ module.exports.hashIP= function(ip, salt){ return `${salt}$${hash}`; } +/** + * Site-wide IP hash comparison function + * + * Allows us to identify new plaintext IP's against saved IP hashes + * @param {String} ip - IP to hash + * @param {String} salt - (optional) string to salt IP with, leave empty to default to a securely generated string encoded in base64 + * @returns {Boolean} Whether or not the IP matched the hash + */ module.exports.compareIPHash = function(ip, hash){ //Split hash by salt delimiter const splitHash = hash.split("$"); From 08fe051269785530f08adac00709aefa89f8fb8c Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 4 Nov 2025 06:09:26 -0500 Subject: [PATCH 193/209] Improved sanatization for server-side templating. --- src/controllers/adminPanelController.js | 6 +++- src/controllers/channelSettingsController.js | 5 +++- src/controllers/indexController.js | 5 +++- src/controllers/panel/profileController.js | 5 +++- src/controllers/profileController.js | 9 ++++-- src/controllers/tooltip/altListController.js | 3 +- src/controllers/tooltip/profileController.js | 7 +++-- src/schemas/tokebot/tokeSchema.js | 2 +- src/views/adminPanel.ejs | 4 +-- src/views/channelSettings.ejs | 8 +++--- src/views/index.ejs | 10 +++---- src/views/partial/adminPanel/channelList.ejs | 18 ++++++------ src/views/partial/adminPanel/permList.ejs | 12 ++++---- src/views/partial/adminPanel/userList.ejs | 26 ++++++++--------- src/views/partial/channelSettings/info.ejs | 6 ++-- .../partial/channelSettings/permList.ejs | 7 +++-- .../partial/channelSettings/settings.ejs | 6 ++-- src/views/partial/panels/profile.ejs | 28 +++++++++++-------- src/views/partial/profile/bio.ejs | 16 +++++++++-- src/views/partial/profile/date.ejs | 2 +- src/views/partial/profile/image.ejs | 2 +- src/views/partial/profile/pronouns.ejs | 4 +-- src/views/partial/profile/settings.ejs | 2 +- src/views/partial/profile/signature.ejs | 4 +-- src/views/partial/profile/status.ejs | 6 ++-- src/views/partial/profile/tokeCount.ejs | 6 ++-- src/views/partial/tooltip/altList.ejs | 12 ++++---- src/views/partial/tooltip/profile.ejs | 10 +++---- src/views/profile.ejs | 16 +++++------ www/css/panel/profile.css | 8 +++++- 30 files changed, 151 insertions(+), 104 deletions(-) diff --git a/src/controllers/adminPanelController.js b/src/controllers/adminPanelController.js index 2d5e72f..ca1a74c 100644 --- a/src/controllers/adminPanelController.js +++ b/src/controllers/adminPanelController.js @@ -17,6 +17,9 @@ along with this program. If not, see .*/ //Config const config = require('../../config.json'); +//NPM Imports +const validator = require('validator');//No express here, so regular validator it is! + //Local Imports const {userModel} = require('../schemas/user/userSchema'); const permissionModel = require('../schemas/permissionSchema'); @@ -45,7 +48,8 @@ module.exports.get = async function(req, res){ chanGuide: chanGuide, userList: userList, permList: permList, - csrfToken: csrfUtils.generateToken(req) + csrfToken: csrfUtils.generateToken(req), + unescape: validator.unescape }); }catch(err){ diff --git a/src/controllers/channelSettingsController.js b/src/controllers/channelSettingsController.js index 5b20aba..e310773 100644 --- a/src/controllers/channelSettingsController.js +++ b/src/controllers/channelSettingsController.js @@ -17,6 +17,9 @@ along with this program. If not, see .*/ //Config const config = require('../../config.json'); +//NPM Imports +const validator = require('validator');//No express here, so regular validator it is! + //local imports const channelModel = require('../schemas/channel/channelSchema'); const permissionModel = require('../schemas/permissionSchema'); @@ -39,7 +42,7 @@ module.exports.get = async function(req, res){ throw loggerUtils.exceptionSmith("Channel not found.", "queue"); } - return res.render('channelSettings', {instance: config.instanceName, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req)}); + return res.render('channelSettings', {instance: config.instanceName, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape}); }catch(err){ return exceptionHandler(res, err); } diff --git a/src/controllers/indexController.js b/src/controllers/indexController.js index bbf1915..30e9682 100644 --- a/src/controllers/indexController.js +++ b/src/controllers/indexController.js @@ -17,6 +17,9 @@ along with this program. If not, see .*/ //Config const config = require('../../config.json'); +//NPM Imports +const validator = require('validator');//No express here, so regular validator it is! + //local imports const channelModel = require('../schemas/channel/channelSchema'); const csrfUtils = require('../utils/csrfUtils'); @@ -26,7 +29,7 @@ const {exceptionHandler, errorHandler} = require('../utils/loggerUtils'); module.exports.get = async function(req, res){ try{ const chanGuide = await channelModel.getChannelList(); - return res.render('index', {instance: config.instanceName, user: req.session.user, chanGuide: chanGuide, csrfToken: csrfUtils.generateToken(req)}); + return res.render('index', {instance: config.instanceName, user: req.session.user, chanGuide: chanGuide, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape}); }catch(err){ return exceptionHandler(res, err); } diff --git a/src/controllers/panel/profileController.js b/src/controllers/panel/profileController.js index 4a58c7d..f24d1dc 100644 --- a/src/controllers/panel/profileController.js +++ b/src/controllers/panel/profileController.js @@ -17,6 +17,9 @@ along with this program. If not, see .*/ //NPM Imports const {validationResult, matchedData} = require('express-validator'); +//NPM Imports +const validator = require('validator');//No express here, so regular validator it is! + //local imports const presenceUtils = require('../../utils/presenceUtils'); const {userModel} = require('../../schemas/user/userSchema'); @@ -34,7 +37,7 @@ module.exports.get = async function(req, res){ //Pull presence (should be quick since everyone whos been on since last startup will be backed in RAM) const presence = await presenceUtils.getPresence(profile.user); - return res.render('partial/panels/profile', {profile, presence}); + return res.render('partial/panels/profile', {profile, presence, unescape: validator.unescape}); }else{ res.status(400); return res.send({errors: validResult.array()}) diff --git a/src/controllers/profileController.js b/src/controllers/profileController.js index fd13cac..67d1895 100644 --- a/src/controllers/profileController.js +++ b/src/controllers/profileController.js @@ -20,6 +20,9 @@ const csrfUtils = require('../utils/csrfUtils'); const presenceUtils = require('../utils/presenceUtils'); const {exceptionHandler, errorHandler} = require('../utils/loggerUtils'); +//NPM Imports +const validator = require('validator');//No express here, so regular validator it is! + //Config const config = require('../../config.json'); @@ -44,7 +47,8 @@ module.exports.get = async function(req, res){ profile, selfProfile, presence, - csrfToken: csrfUtils.generateToken(req) + csrfToken: csrfUtils.generateToken(req), + unescape: validator.unescape }); }else{ res.render('profile', { @@ -53,7 +57,8 @@ module.exports.get = async function(req, res){ profile: null, selfProfile: false, presence: null, - csrfToken: csrfUtils.generateToken(req) + csrfToken: csrfUtils.generateToken(req), + unescape: validator.unescape }); } }catch(err){ diff --git a/src/controllers/tooltip/altListController.js b/src/controllers/tooltip/altListController.js index a037cd0..d470114 100644 --- a/src/controllers/tooltip/altListController.js +++ b/src/controllers/tooltip/altListController.js @@ -16,6 +16,7 @@ along with this program. If not, see .*/ //NPM Imports const {validationResult, matchedData} = require('express-validator'); +const validator = require('validator');//Because sometimes one isn't enough... //local imports const {userModel} = require('../../schemas/user/userSchema'); @@ -34,7 +35,7 @@ module.exports.get = async function(req, res){ return errorHandler(res, 'Cannot get alts for non-existant user!'); } - return res.render('partial/tooltip/altList', {alts: await userDB.getAltProfiles()}); + return res.render('partial/tooltip/altList', {alts: await userDB.getAltProfiles(), unescape: validator.unescape}); }else{ res.status(400); return res.send({errors: validResult.array()}) diff --git a/src/controllers/tooltip/profileController.js b/src/controllers/tooltip/profileController.js index 18f9cff..e4b4a0c 100644 --- a/src/controllers/tooltip/profileController.js +++ b/src/controllers/tooltip/profileController.js @@ -17,6 +17,9 @@ along with this program. If not, see .*/ //NPM Imports const {validationResult, matchedData} = require('express-validator'); +//NPM Imports +const validator = require('validator');//No express here, so regular validator it is! + //local imports const {userModel} = require('../../schemas/user/userSchema'); const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils'); @@ -30,10 +33,10 @@ module.exports.get = async function(req, res){ const data = matchedData(req); const profile = await userModel.findProfile({user: data.user}); - return res.render('partial/tooltip/profile', {profile}); + return res.render('partial/tooltip/profile', {profile, unescape: validator.unescape}); }else{ res.status(400); - return res.send({errors: validResult.array()}) + return res.send({errors: validResult.array()}); } }catch(err){ diff --git a/src/schemas/tokebot/tokeSchema.js b/src/schemas/tokebot/tokeSchema.js index ca186af..7b86913 100644 --- a/src/schemas/tokebot/tokeSchema.js +++ b/src/schemas/tokebot/tokeSchema.js @@ -91,7 +91,7 @@ tokeSchema.statics.calculateTokeMap = async function(){ //Display calculated toke sats for funsies if(config.verbose){ - console.log(`Processed ${this.commandCount} toke command callouts accross ${await this.estimatedDocumentCount()} tokes.`); + console.log(`Processed ${this.commandCount} toke command callouts accross ${this.count} tokes, averaging ${(this.commandCount/this.count).toFixed(3)} tokers per toke.`); } } diff --git a/src/views/adminPanel.ejs b/src/views/adminPanel.ejs index d34c773..b49d7ea 100644 --- a/src/views/adminPanel.ejs +++ b/src/views/adminPanel.ejs @@ -25,8 +25,8 @@ along with this program. If not, see . %> <%- include('partial/navbar', {user}); %>

<%= instance %> Admin Panel

- <%- include('partial/adminPanel/channelList', {chanGuide}) %> - <%- include('partial/adminPanel/userList', {user, userList, rankEnum}) %> + <%- include('partial/adminPanel/channelList', {chanGuide, unescape}) %> + <%- include('partial/adminPanel/userList', {user, userList, rankEnum, unescape}) %> <%- include('partial/adminPanel/permList', {permList, rankEnum}) %> <%- include('partial/adminPanel/userBanList') %> <%- include('partial/adminPanel/tokeCommandList') %> diff --git a/src/views/channelSettings.ejs b/src/views/channelSettings.ejs index d94e44f..81b4e59 100644 --- a/src/views/channelSettings.ejs +++ b/src/views/channelSettings.ejs @@ -24,13 +24,13 @@ along with this program. If not, see . %> <%- include('partial/navbar', {user}); %> -

<%- channel.name %> - Channel Settings

+

<%= unescape(channel.name) %> - Channel Settings

- <%- include('partial/channelSettings/info.ejs', {channel}); %> + <%- include('partial/channelSettings/info.ejs', {unescape, channel}); %> <%- include('partial/channelSettings/userList.ejs'); %> <%- include('partial/channelSettings/banList.ejs'); %> - <%- include('partial/channelSettings/settings.ejs'); %> - <%- include('partial/channelSettings/permList.ejs'); %> + <%- include('partial/channelSettings/settings.ejs', {unescape, channel}); %> + <%- include('partial/channelSettings/permList.ejs', {channel}); %> <%- include('partial/channelSettings/tokeCommandList.ejs'); %> <%- include('partial/channelSettings/emoteList.ejs'); %>
diff --git a/src/views/index.ejs b/src/views/index.ejs index dc47f6d..4ee326c 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -26,11 +26,11 @@ along with this program. If not, see . %>

Start a new channel...

<% chanGuide.forEach((channel) => { %> -
- -

<%- channel.name %>

- -

<%- channel.description %> +

+ +

<%= unescape(channel.name) %>

+ +

<%= unescape(channel.description) %>

<% }); %> diff --git a/src/views/partial/adminPanel/channelList.ejs b/src/views/partial/adminPanel/channelList.ejs index 53311f7..e6e53ec 100644 --- a/src/views/partial/adminPanel/channelList.ejs +++ b/src/views/partial/adminPanel/channelList.ejs @@ -29,19 +29,19 @@ along with this program. If not, see . %> <% chanGuide.forEach((channel) => { %> - - - - + + + + - - - <%- channel.name %> + + + <%= unescape(channel.name) %> - - <%- channel.description %> + + <%= unescape(channel.description) %> <% }); %> diff --git a/src/views/partial/adminPanel/permList.ejs b/src/views/partial/adminPanel/permList.ejs index d0e9ce1..40bce34 100644 --- a/src/views/partial/adminPanel/permList.ejs +++ b/src/views/partial/adminPanel/permList.ejs @@ -20,10 +20,10 @@ along with this program. If not, see . %> <% Object.keys(permList).forEach((key)=>{ %> <% if(key != "channelOverrides"){ %> - - <%rankEnum.slice().reverse().forEach((rank)=>{ %> - + <% }); %> @@ -33,10 +33,10 @@ along with this program. If not, see . %> <% Object.keys(permList.channelOverrides).forEach((key)=>{ %> <% if(key != "channelOverrides"){ %> - - <%rankEnum.slice().reverse().forEach((rank)=>{ %> - + <% }); %> diff --git a/src/views/partial/adminPanel/userList.ejs b/src/views/partial/adminPanel/userList.ejs index d94142a..df54d21 100644 --- a/src/views/partial/adminPanel/userList.ejs +++ b/src/views/partial/adminPanel/userList.ejs @@ -41,42 +41,42 @@ along with this program. If not, see . %> <% userList.forEach((curUser) => { %> - + - - + + - - <%- curUser.id %> + + <%= curUser.id %> - - <%- curUser.user %> + + <%= unescape(curUser.user) %> <% if(rankEnum.indexOf(curUser.rank) < rankEnum.indexOf(user.rank)){%> - <%rankEnum.slice().reverse().forEach((rank)=>{ %> - + <% }); %> <% }else{ %> - <%- curUser.rank %> + <%= curUser.rank %> <% } %> - <%- curUser.email ? curUser.email : "N/A" %> + <%= unescape(curUser.email) ? curUser.email : "N/A" %> - <%- curUser.date.toUTCString() %> + <%= unescape(curUser.date.toUTCString()) %> <%# It's either this or add whitespce >:( %> - + <% }); %> diff --git a/src/views/partial/channelSettings/info.ejs b/src/views/partial/channelSettings/info.ejs index a2f073f..11aeafb 100644 --- a/src/views/partial/channelSettings/info.ejs +++ b/src/views/partial/channelSettings/info.ejs @@ -19,13 +19,13 @@ along with this program. If not, see . %>

Thumbnail:

- - + +

Description:

-

<%= channel.description %>

+

<%= unescape(channel.description) %>

\ No newline at end of file diff --git a/src/views/partial/channelSettings/permList.ejs b/src/views/partial/channelSettings/permList.ejs index 80ca3e4..0c9cb20 100644 --- a/src/views/partial/channelSettings/permList.ejs +++ b/src/views/partial/channelSettings/permList.ejs @@ -20,10 +20,11 @@ along with this program. If not, see . %> <% Object.keys(channel.permissions.toObject()).forEach((key)=>{ %> <% if(key != "channelOverrides"){ %> - - <%rankEnum.slice().reverse().forEach((rank)=>{ %> - + <% }); %> diff --git a/src/views/partial/channelSettings/settings.ejs b/src/views/partial/channelSettings/settings.ejs index 9ddc294..1ec464c 100644 --- a/src/views/partial/channelSettings/settings.ejs +++ b/src/views/partial/channelSettings/settings.ejs @@ -19,13 +19,13 @@ along with this program. If not, see . %> <% Object.keys(channel.settings).forEach((key) => { %> - + <% switch(typeof channel.settings[key]){ case "string": %> - + <% break; default: %> - checked <% } %>> + checked <% } %>> <% break; } %> diff --git a/src/views/partial/panels/profile.ejs b/src/views/partial/panels/profile.ejs index b5728c6..63fc487 100644 --- a/src/views/partial/panels/profile.ejs +++ b/src/views/partial/panels/profile.ejs @@ -18,16 +18,22 @@ along with this program. If not, see . %> <% if(profile == null){ %>

Profile not found!

<% }else{ %> - View Full Profile -

<%- profile.user %>

- <%- include('../profile/status', {profile, presence, auxClass:"panel"}); %> - -

Toke Count: <%- profile.tokeCount %>

- <% if(profile.pronouns != '' && profile.pronouns != null){ %> -

Pronouns: <%- profile.pronouns %>

- <% } %> -

Signature: <%- profile.signature %>

-

Bio:

-

<%- profile.bio %>

+ <% const splitBio = profile.bio.split('\n'); %> + View Full Profile +

<%= unescape(profile.user) %>

+ <%- include('../profile/status', {profile, presence, auxClass:"panel", unescape}); %> + +
+

Toke Count: <%= profile.tokeCount %>

+ <% if(profile.pronouns != '' && profile.pronouns != null){ %> +

Pronouns: <%= unescape(profile.pronouns) %>

+ <% } %> +

Signature: <%= unescape(profile.signature) %>

+

+ <% for(const line of splitBio){ %> + <%= unescape(line) %>
+ <% } %> +

+
<% } %>
\ No newline at end of file diff --git a/src/views/partial/profile/bio.ejs b/src/views/partial/profile/bio.ejs index 0cc620b..3d28720 100644 --- a/src/views/partial/profile/bio.ejs +++ b/src/views/partial/profile/bio.ejs @@ -15,11 +15,23 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . %>

Bio:

+ <% + //Split bio by newline + const splitBio = profile.bio.split('\n'); + %> <% if(selfProfile){ %> <%# Make sure to convert newlines to br so they display proepr %> -

<%- profile.bio.replaceAll('\n','
') %>

+

+ <% for(const line of splitBio){ %> + <%= unescape(line) %>
+ <% } %> +

<% }else{ %> -

<%- profile.bio.replaceAll('\n','
') %>

+

+ <% for(const line of splitBio){ %> + <%= unescape(line) %>
+ <% } %> +

<% } %>
\ No newline at end of file diff --git a/src/views/partial/profile/date.ejs b/src/views/partial/profile/date.ejs index 27b21aa..55c175b 100644 --- a/src/views/partial/profile/date.ejs +++ b/src/views/partial/profile/date.ejs @@ -14,5 +14,5 @@ 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 . %> -

Joined: <%- profile.date.toLocaleDateString(); %>

+

Joined: <%= profile.date.toLocaleDateString(); %>

\ No newline at end of file diff --git a/src/views/partial/profile/image.ejs b/src/views/partial/profile/image.ejs index 7665509..30f4afe 100644 --- a/src/views/partial/profile/image.ejs +++ b/src/views/partial/profile/image.ejs @@ -14,7 +14,7 @@ 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 . %>
- + <% if(selfProfile){ %> <% } %> diff --git a/src/views/partial/profile/pronouns.ejs b/src/views/partial/profile/pronouns.ejs index 3d427f2..cd35da2 100644 --- a/src/views/partial/profile/pronouns.ejs +++ b/src/views/partial/profile/pronouns.ejs @@ -24,10 +24,10 @@ along with this program. If not, see . %> <% }else if(profile.pronouns != null && profile.pronouns != ""){ %> <% if(selfProfile){ %> -

Pronouns: <%- profile.pronouns %>

+

Pronouns: <%= unescape(profile.pronouns) %>

<% }else{ %> -

Pronouns: <%- profile.pronouns %>

+

Pronouns: <%= unescape(profile.pronouns) %>

<% } %>
<% } %> \ No newline at end of file diff --git a/src/views/partial/profile/settings.ejs b/src/views/partial/profile/settings.ejs index 84a0ac8..3135653 100644 --- a/src/views/partial/profile/settings.ejs +++ b/src/views/partial/profile/settings.ejs @@ -17,7 +17,7 @@ along with this program. If not, see . %> <% if(profile.email){ %> - + <% } %>
From 27ab1c2c71b12a9d0b1cfad754e73105385be38e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 12 Nov 2025 19:24:59 -0500 Subject: [PATCH 207/209] Upgraded version from 0.4-Indev Hotfix 3 to 0.1-Alpha --- README.md | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c6f2762..04459b0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Canopy -0.4-INDEV Hotfix 3 +0.1-Alpha ========= Canopy - /ˈkæ.nə.pi/: diff --git a/package.json b/package.json index e0c23a8..e5e493e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "canopy-of-indev", - "version": "0.4.3", - "canopyDisplayVersion": "0.4-Indev Hotfix 3", + "name": "canopy-of-alpha", + "version": "0.1", + "canopyDisplayVersion": "0.1-Alpha", "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "^7.1.1", From f0555169fe431ede29fdba43b9ce717da3bc8bc4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 23 Nov 2025 07:50:49 -0500 Subject: [PATCH 208/209] Bugfix for DB lookup by username for certain internal methods/functions. --- src/controllers/api/account/emailChangeRequestController.js | 2 +- src/controllers/api/account/loginController.js | 2 +- src/schemas/user/migrationSchema.js | 2 +- src/schemas/user/userSchema.js | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/account/emailChangeRequestController.js b/src/controllers/api/account/emailChangeRequestController.js index 4791ba2..3108ab6 100644 --- a/src/controllers/api/account/emailChangeRequestController.js +++ b/src/controllers/api/account/emailChangeRequestController.js @@ -60,7 +60,7 @@ module.exports.post = async function(req, res){ //Look through DB and migration cache for existing email - const existingDB = await userModel.findOne({email: new RegExp(email, 'i')}); + const existingDB = await userModel.findOne({email: new RegExp(`^${email}$`, 'i')}); const needsMigration = userModel.migrationCache.emails.includes(email.toLowerCase()); //If the email is in use diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index d509342..be8685b 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -90,7 +90,7 @@ module.exports.post = async function(req, res){ const {user, pass} = matchedData(req); //Look for the username in the migration DB - const migrationDB = await migrationModel.findOne({user}); + const migrationDB = await migrationModel.findOne({user: new RegExp(`^${user}$`, 'i')}); //If we found a migration profile if(migrationDB != null){ diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 9aff595..4e61ce0 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -323,7 +323,7 @@ migrationSchema.statics.buildMigrationCache = async function(){ migrationSchema.statics.consumeByUsername = async function(ip, migration){ //Pull migration doc by case-insensitive username - const migrationDB = await this.findOne({user: new RegExp(migration.user, 'i')}); + const migrationDB = await this.findOne({user: new RegExp(`^${migration.user}$`, 'i')}); //If we have no migration document if(migrationDB == null){ diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 41dfcda..783b241 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -256,13 +256,13 @@ userSchema.statics.register = async function(userObj, ip){ //Check password confirmation matches if(pass == passConfirm){ //Setup user query - let userQuery = {user: new RegExp(user, 'i')}; + let userQuery = {user: new RegExp(`^${user}$`, 'i')}; //If we have an email if(email != null && email != ""){ userQuery = {$or: [ userQuery, - {email: new RegExp(email, 'i')} + {email: new RegExp(`^${email}$`, 'i')} ]}; } @@ -319,7 +319,7 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use } //get the user if it exists - const userDB = await this.findOne({ user: new RegExp(user, 'i')}); + const userDB = await this.findOne({ user: new RegExp(`^${user}$`, 'i')}); //if not scream and shout if(!userDB){ From 02f57fafd5f95b9ad96fee604cbd994f18ae09e4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 23 Nov 2025 07:50:59 -0500 Subject: [PATCH 209/209] Rolled over to v0.1-Alpha Hotfix 1 --- README.md | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 04459b0..145d7e6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Canopy -0.1-Alpha +0.1-Alpha (Panama Red) - Hotfix 1 ========= Canopy - /ˈkæ.nə.pi/: diff --git a/package.json b/package.json index e5e493e..8554c13 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canopy-of-alpha", - "version": "0.1", - "canopyDisplayVersion": "0.1-Alpha", + "version": "0.1.1", + "canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 1", "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "^7.1.1",