Profile pages now display user status.
This commit is contained in:
parent
6445950f90
commit
1384b02f4d
|
|
@ -1,9 +1,9 @@
|
||||||
Canopy - 0.3-INDEV - Hotfix 1
|
Canopy - 0.4-INDEV
|
||||||
======
|
======
|
||||||
|
|
||||||
Canopy - /ˈkæ.nə.pi/:
|
Canopy - /ˈkæ.nə.pi/:
|
||||||
- The upper layer of foliage and branches of a forest, containing the majority of animal life.
|
- 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.
|
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:
|
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
|
- General Clunk
|
||||||
- Less Unique Community Identity
|
- 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:
|
The Canopy codebase does not, nor will it ever contain:
|
||||||
- Advertisements (targetted or otherwise)
|
- Advertisements (targetted or otherwise)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
const {validationResult, matchedData} = require('express-validator');
|
const {validationResult, matchedData} = require('express-validator');
|
||||||
|
|
||||||
//local imports
|
//local imports
|
||||||
|
const presenceUtils = require('../../utils/presenceUtils');
|
||||||
const {userModel} = require('../../schemas/user/userSchema');
|
const {userModel} = require('../../schemas/user/userSchema');
|
||||||
const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils');
|
const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils');
|
||||||
|
|
||||||
|
|
@ -30,7 +31,10 @@ module.exports.get = async function(req, res){
|
||||||
const data = matchedData(req);
|
const data = matchedData(req);
|
||||||
const profile = await userModel.findProfile({user: data.user});
|
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{
|
}else{
|
||||||
res.status(400);
|
res.status(400);
|
||||||
return res.send({errors: validResult.array()})
|
return res.send({errors: validResult.array()})
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
//Local Imports
|
//Local Imports
|
||||||
const {userModel} = require('../schemas/user/userSchema');
|
const {userModel} = require('../schemas/user/userSchema');
|
||||||
const csrfUtils = require('../utils/csrfUtils');
|
const csrfUtils = require('../utils/csrfUtils');
|
||||||
|
const presenceUtils = require('../utils/presenceUtils');
|
||||||
const {exceptionHandler, errorHandler} = require('../utils/loggerUtils');
|
const {exceptionHandler, errorHandler} = require('../utils/loggerUtils');
|
||||||
|
|
||||||
//Config
|
//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
|
//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;
|
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', {
|
res.render('profile', {
|
||||||
instance: config.instanceName,
|
instance: config.instanceName,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
profile,
|
profile,
|
||||||
selfProfile,
|
selfProfile,
|
||||||
|
presence,
|
||||||
csrfToken: csrfUtils.generateToken(req)
|
csrfToken: csrfUtils.generateToken(req)
|
||||||
});
|
});
|
||||||
}else{
|
}else{
|
||||||
|
|
@ -47,6 +52,7 @@ module.exports.get = async function(req, res){
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
profile: null,
|
profile: null,
|
||||||
selfProfile: false,
|
selfProfile: false,
|
||||||
|
presence: null,
|
||||||
csrfToken: csrfUtils.generateToken(req)
|
csrfToken: csrfUtils.generateToken(req)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ const userSchema = new mongoose.Schema({
|
||||||
lastActive: {
|
lastActive: {
|
||||||
type: mongoose.SchemaTypes.Date,
|
type: mongoose.SchemaTypes.Date,
|
||||||
required: true,
|
required: true,
|
||||||
default: new Date()
|
default: new Date(0)
|
||||||
},
|
},
|
||||||
rank: {
|
rank: {
|
||||||
type: mongoose.SchemaTypes.String,
|
type: mongoose.SchemaTypes.String,
|
||||||
|
|
@ -505,6 +505,7 @@ userSchema.methods.getProfile = function(includeEmail = false){
|
||||||
id: this.id,
|
id: this.id,
|
||||||
user: this.user,
|
user: this.user,
|
||||||
date: this.date,
|
date: this.date,
|
||||||
|
lastActive: this.lastActive,
|
||||||
tokes: this.tokes,
|
tokes: this.tokes,
|
||||||
tokeCount: this.getTokeCount(),
|
tokeCount: this.getTokeCount(),
|
||||||
img: this.img,
|
img: this.img,
|
||||||
|
|
|
||||||
|
|
@ -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/>.*/
|
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
|
|
||||||
//local includes
|
//local includes
|
||||||
|
const server = require('../server');
|
||||||
const userSchema = require('../schemas/user/userSchema');
|
const userSchema = require('../schemas/user/userSchema');
|
||||||
|
|
||||||
//User activity map to keep us from constantly reading off of the DB
|
//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)
|
//How much difference between last write and now until we hit the DB again (in millis)
|
||||||
//Defaults to two minutes
|
//Defaults to two minutes
|
||||||
const tolerance = 2 * (60 * 1000);
|
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){
|
module.exports.presenceMiddleware = function(req, res, next){
|
||||||
//Pull user from session
|
//Pull user from session
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<% }else{ %>
|
<% }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>
|
<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>
|
<h2 class="panel profile-name"><%- profile.user %></h2>
|
||||||
|
<%- include('../profile/status', {profile, presence, auxClass:"panel"}); %>
|
||||||
<img class="panel profile-img" src="<%- profile.img %>">
|
<img class="panel profile-img" src="<%- profile.img %>">
|
||||||
<p class="panel profile-info">Toke Count: <%- profile.tokeCount %></p>
|
<p class="panel profile-info">Toke Count: <%- profile.tokeCount %></p>
|
||||||
<% if(profile.pronouns != '' && profile.pronouns != null){ %>
|
<% if(profile.pronouns != '' && profile.pronouns != null){ %>
|
||||||
|
|
|
||||||
22
src/views/partial/profile/status.ejs
Normal file
22
src/views/partial/profile/status.ejs
Normal 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>
|
||||||
|
<% } %>
|
||||||
|
|
@ -33,6 +33,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<div class="profile dynamic-container" id="profile-div">
|
<div class="profile dynamic-container" id="profile-div">
|
||||||
<span id="profile-info" class="profile">
|
<span id="profile-info" class="profile">
|
||||||
<h1 class="profile-item" id="profile-username"><%- profile.user %></h1>
|
<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/image', {profile, selfProfile}); %>
|
||||||
<%- include('partial/profile/pronouns', {profile, selfProfile}); %>
|
<%- include('partial/profile/pronouns', {profile, selfProfile}); %>
|
||||||
<%- include('partial/profile/signature', {profile, selfProfile}); %>
|
<%- include('partial/profile/signature', {profile, selfProfile}); %>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
|
|
||||||
.panel.profile-name{
|
.panel.profile-name{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel.profile-status{
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel.profile-img{
|
.panel.profile-img{
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#profile-status{
|
||||||
|
margin: 0;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
#profile-img{
|
#profile-img{
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
--accent0-alt1: rgb(70, 70, 70);
|
--accent0-alt1: rgb(70, 70, 70);
|
||||||
--accent1: rgb(245, 245, 245);
|
--accent1: rgb(245, 245, 245);
|
||||||
--accent1-alt0: rgb(185, 185, 185);
|
--accent1-alt0: rgb(185, 185, 185);
|
||||||
|
--accent1-alt1: rgb(124, 124, 124);
|
||||||
--accent2: var(--accent0-alt0);
|
--accent2: var(--accent0-alt0);
|
||||||
|
|
||||||
--focus0: rgb(51, 153, 51);
|
--focus0: rgb(51, 153, 51);
|
||||||
|
|
@ -157,6 +158,14 @@ textarea{
|
||||||
text-shadow: var(--focus-glow0);
|
text-shadow: var(--focus-glow0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.positive-low{
|
||||||
|
color: var(--focus0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive{
|
||||||
|
color: var(--accent1-alt1);
|
||||||
|
}
|
||||||
|
|
||||||
.danger-button{
|
.danger-button{
|
||||||
background-color: var(--danger0);
|
background-color: var(--danger0);
|
||||||
color: var(--accent1);
|
color: var(--accent1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue