channelManager now tracks all active connectedUser objects for a given user.

This commit is contained in:
rainbow napkin 2025-09-17 05:11:45 -04:00
parent 261dce7b29
commit 6222535c47
7 changed files with 160 additions and 24 deletions

3
.gitignore vendored
View file

@ -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

View file

@ -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.

View file

@ -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",

View file

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

View file

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

View file

@ -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;
}
/**

View file

@ -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 <https://www.gnu.org/licenses/>.*/
/**
* 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;