Profile pages now display user status.

This commit is contained in:
rainbow napkin 2025-09-18 02:43:43 -04:00
parent 6445950f90
commit 1384b02f4d
11 changed files with 119 additions and 5 deletions

View file

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

View file

@ -18,6 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
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()})

View file

@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//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)
});
}

View file

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

View file

@ -15,6 +15,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/>.*/
//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

View file

@ -20,6 +20,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<% }else{ %>
<a class="panel profile-link" target="_blank" href="/profile/<%- profile.user %>">View Full Profile<i class="bi-box-arrow-in-up-right"></i></a>
<h2 class="panel profile-name"><%- profile.user %></h2>
<%- include('../profile/status', {profile, presence, auxClass:"panel"}); %>
<img class="panel profile-img" src="<%- profile.img %>">
<p class="panel profile-info">Toke Count: <%- profile.tokeCount %></p>
<% if(profile.pronouns != '' && profile.pronouns != null){ %>

View file

@ -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 <https://www.gnu.org/licenses/>. %>
<% if(profile.user == "Tokebot"){ %>
<p id="profile-status" class="<%- auxClass %> positive profile-status"><span class="bi-record-fill"></span>Perma-Couched</p>
<% }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"); %>
<p id="profile-status" class="<%- auxClass %> <%- statusClass %> profile-status"><span class="bi-record-fill"></span><%- presence.status %><%-curChan%></p>
<% } %>

View file

@ -33,6 +33,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<div class="profile dynamic-container" id="profile-div">
<span id="profile-info" class="profile">
<h1 class="profile-item" id="profile-username"><%- profile.user %></h1>
<%- 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}); %>

View file

@ -24,6 +24,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
.panel.profile-name{
text-align: center;
margin-bottom: 0;
}
.panel.profile-status{
text-align: center;
margin-bottom: 1em;
}
.panel.profile-img{

View file

@ -56,6 +56,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
margin: 0;
}
#profile-status{
margin: 0;
text-wrap: nowrap;
}
#profile-img{
position: relative;
display: flex;

View file

@ -31,6 +31,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
--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);