/*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 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 PM Handler */ this.pmHandler = new pmHandler(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(){ const clientOptions = { extraHeaders: { //Include CSRF token 'x-csrf-token': utils.ajax.getCSRFToken() } }; this.socket = io(clientOptions); this.pmSocket = io("/pm", clientOptions); } /** * 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!"){ //Warn the user new canopyUXUtils.popup('Invalid CSRF Token detected, reloading client...'); //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}`); } }); 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; } /** * 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){ //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': 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 nowPlaying = this.player.mediaHandler.nowPlaying; //If we're playing a youtube video if(nowPlaying != null && nowPlaying.type == 'yt'){ //Restart the video 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; 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; } } /** * Default channel config */ static defaultConfig = new Map([ ["ytPlayerType","raw"], ["IACDN",""], ["syncTolerance",0.4], ["liveSyncTolerance", 2], ["syncDelta", 6], ["chatWidthMin", 20], ["userlistHidden", false], ["cinemaMode", false], ["rxPMSound", 'unread'], ["txPMSound", false], ["newSeshSound", true], ["endSeshSound", true] ]); } /** * 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();