From 6222535c4790d37b263407a43e950e011179e394 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 17 Sep 2025 05:11:45 -0400 Subject: [PATCH] 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