Compare commits

..

No commits in common. "main" and "0.4-indev-hotfix-2" have entirely different histories.

46 changed files with 101 additions and 446 deletions

1
.gitignore vendored
View file

@ -10,4 +10,3 @@ server.cert
server.key
www/nonfree/*
migration/*
www/hrt.zip

View file

@ -9,7 +9,7 @@ Canopy
<a href="https://git.ourfore.st/rainbownapkin/canopy/issues" target="_blank"><img src="https://git.ourfore.st/rainbownapkin/canopy/badges/issues/closed.svg"></a>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank"><img src="https://img.shields.io/badge/License-AGPL_v3-663366.svg"></a>
0.1-Alpha (Panama Red) - Hotfix 3
0.4-INDEV Hotfix 2
=========
Canopy - /ˈkæ.nə.pi/:

View file

@ -34,10 +34,5 @@
"address": "toke@42069.weed",
"pass": "CHANGE_ME"
},
"links":{
"About": "/about",
"Code": "https://git.ourfore.st/rainbownapkin/canopy",
"HRT": "/hrt"
},
"aboutText":"<a href=\"https://ourfore.st/\">ourfore.st</a> is the one and only original canopy instance. Setup, ran, and administered by rainbownapkin herself. This site exists to provide a featureful, preformant, and comfy replacement for the TTN community."
}

View file

@ -65,12 +65,6 @@
"address": "toke@42069.weed",
"pass": "CHANGE_ME"
},
//Provides customizable links for navbar
"links":{
"About": "/about",
"Code": "https://git.ourfore.st/rainbownapkin/canopy",
"HRT": "/hrt"
},
//Fills the 'about ${instanceName}' section on the /about page, lets users know about your specific instance
"aboutText":"<a href=\"https://ourfore.st/\">ourfore.st</a> is the one and only original canopy instance. Setup, ran, and administered by rainbownapkin herself. This site exists to provide a featureful, preformant, and comfy replacement for the TTN community."
}

View file

@ -1,14 +1,13 @@
{
"name": "canopy-of-alpha",
"version": "0.1.3",
"canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 3",
"name": "canopy-of-indev",
"version": "0.4.2",
"license": "AGPL-3.0-only",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
"altcha": "^2.3.0",
"altcha": "^1.0.7",
"altcha-lib": "^1.2.0",
"argon2": "^0.44.0",
"bcrypt": "^6.0.0",
"bcrypt": "^5.1.1",
"bootstrap-icons": "^1.11.3",
"connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
@ -20,8 +19,8 @@
"hls.js": "^1.6.2",
"mongoose": "^8.4.3",
"node-cron": "^3.0.3",
"nodemailer": "^8.0.7",
"socket.io": "^4.2.0",
"nodemailer": "^7.0.9",
"socket.io": "^4.8.1",
"youtube-dl-exec": "^3.0.20"
},
"scripts": {

View file

@ -212,12 +212,6 @@ class chatHandler{
* @param {chat} chat - Chat Object representing the message to broadcast to the given channel
*/
relayChatObject(chan, chat){
//If we have an empty chat
if(chat.msg.length <= 0){
//Drop it
return;
}
//Send out chat
this.server.io.in(chan).emit("chatMessage", chat);

View file

@ -148,15 +148,6 @@ class tokebot{
//Add the toking user to the tokers map
this.tokers.set(commandObj.socket.user.user, commandObj.argumentArray[0].toLowerCase());
if(this.tokeCounter <= 3){
//Drop the toke timer
clearTimeout(this.tokeTimer);
//Roll the toke counter back to 3
this.tokeCounter = 3;
//Re-start the toke timer
this.tokeTimer = setTimeout(this.countdown.bind(this), 1000);
}
//If the user is already in the toke
}else{
//Tell them to fuck off
@ -219,7 +210,7 @@ class tokebot{
//Decrement toke time
this.tokeCounter--;
//try again in another second
this.tokeTimer = setTimeout(this.countdown.bind(this), 1000);
this.tokeTimer = setTimeout(this.countdown.bind(this), 1000)
}
/**

View file

@ -57,7 +57,7 @@ class chatPreprocessor{
//If we don't pass sanatization/validation turn this car around
if(!this.sanatizeCommand(commandObj)){
return false;
return;
}
//split the command

View file

@ -26,5 +26,5 @@ module.exports = async function(req, res){
res.status(404);
//Render page
return res.render('404', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
return res.render('404', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -16,7 +16,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Config
const config = require('../../config.json');
const package = require('../../package.json');
//Local Imports
const csrfUtils = require('../utils/csrfUtils');
@ -24,5 +23,5 @@ const csrfUtils = require('../utils/csrfUtils');
//register page functions
module.exports.get = async function(req, res){
//Render page
return res.render('about', {aboutText: config.aboutText, instance: config.instanceName, links: config.links, user: req.session.user, version: package.canopyDisplayVersion, csrfToken: csrfUtils.generateToken(req)});
return res.render('about', {aboutText: config.aboutText, instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -42,7 +42,7 @@ module.exports.get = async function(req, res){
//Render out the page
return res.render('adminPanel', {
instance: config.instanceName, links: config.links,
instance: config.instanceName,
user: req.session.user,
rankEnum: permissionModel.rankEnum,
chanGuide: chanGuide,

View file

@ -60,7 +60,7 @@ module.exports.post = async function(req, res){
//Look through DB and migration cache for existing email
const existingDB = await userModel.findOne({email: new RegExp(`^${email}$`, 'i')});
const existingDB = await userModel.findOne({email: new RegExp(email, 'i')});
const needsMigration = userModel.migrationCache.emails.includes(email.toLowerCase());
//If the email is in use

View file

@ -90,7 +90,7 @@ module.exports.post = async function(req, res){
const {user, pass} = matchedData(req);
//Look for the username in the migration DB
const migrationDB = await migrationModel.findOne({user: new RegExp(`^${user}$`, 'i')});
const migrationDB = await migrationModel.findOne({user});
//If we found a migration profile
if(migrationDB != null){

View file

@ -22,5 +22,5 @@ const csrfUtils = require('../utils/csrfUtils');
//channel functions
module.exports.get = function(req, res){
res.render('channel', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
res.render('channel', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -42,7 +42,7 @@ module.exports.get = async function(req, res){
throw loggerUtils.exceptionSmith("Channel not found.", "queue");
}
return res.render('channelSettings', {instance: config.instanceName, links: config.links, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
return res.render('channelSettings', {instance: config.instanceName, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
}catch(err){
return exceptionHandler(res, err);
}

View file

@ -40,18 +40,18 @@ module.exports.get = async function(req, res){
//If we have an invalid request
if(requestDB == null){
return res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
return res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
}
//Speak of our success (don't wait for the emails to be sent)
res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: true});
res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: true});
//Consume the request
await requestDB.consume();
}else{
return res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
return res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
}
}catch(err){
return res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
return res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
}
}

View file

@ -1,28 +0,0 @@
/*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/>.*/
//Config
const config = require('../../config.json');
const package = require('../../package.json');
//Local Imports
const csrfUtils = require('../utils/csrfUtils');
//register page functions
module.exports.get = async function(req, res){
//Render page
return res.render('hrt', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -29,7 +29,7 @@ const {exceptionHandler, errorHandler} = require('../utils/loggerUtils');
module.exports.get = async function(req, res){
try{
const chanGuide = await channelModel.getChannelList();
return res.render('index', {instance: config.instanceName, links: config.links, user: req.session.user, chanGuide: chanGuide, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
return res.render('index', {instance: config.instanceName, user: req.session.user, chanGuide: chanGuide, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
}catch(err){
return exceptionHandler(res, err);
}

View file

@ -45,7 +45,7 @@ module.exports.get = async function(req, res){
//if we have previous attempts for this user
if(attempts != null){
if(attempts.count > sessionUtils.maxAttempts){
return res.render('lockedAccount', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
return res.render('lockedAccount', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
}
//If the users login's are being throttled
@ -56,16 +56,16 @@ module.exports.get = async function(req, res){
const challenge = await altchaUtils.genCaptcha(difficulty, user);
//Render page
return res.render('login', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
}
//otherwise
}else{
//Render generic page
return res.render('login', {instance: config.instanceName, links: config.links, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
}
//if we received invalid input
}else{
//Render pretend nothing happened, send out a generic page
return res.render('login', {instance: config.instanceName, links: config.links, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
}
}

View file

@ -27,5 +27,5 @@ module.exports.get = async function(req, res){
const challenge = await altchaUtils.genCaptcha();
//Render page
return res.render('migrate', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
return res.render('migrate', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -27,5 +27,5 @@ module.exports.get = async function(req, res){
const challenge = await altchaUtils.genCaptcha();
//render the page
return res.render('newChannel', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
return res.render('newChannel', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -19,5 +19,5 @@ const config = require('../../../config.json');
//popout panel container functions
module.exports.get = async function(req, res){
res.render('popoutContainer', {instance: config.instanceName, links: config.links});
res.render('popoutContainer', {instance: config.instanceName});
}

View file

@ -47,11 +47,11 @@ module.exports.get = async function(req, res){
*/
//Render page
return res.render('passwordReset', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, token, csrfToken: csrfUtils.generateToken(req)});
return res.render('passwordReset', {instance: config.instanceName, user: req.session.user, challenge, token, csrfToken: csrfUtils.generateToken(req)});
//If we didn't get a valid token
}else{
//otherwise render generic page
return res.render('passwordReset', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, token: null, csrfToken: csrfUtils.generateToken(req)});
return res.render('passwordReset', {instance: config.instanceName, user: req.session.user, challenge, token: null, csrfToken: csrfUtils.generateToken(req)});
}
}catch(err){
return exceptionHandler(res, err);

View file

@ -42,7 +42,7 @@ module.exports.get = async function(req, res){
const presence = await presenceUtils.getPresence(profile.user);
res.render('profile', {
instance: config.instanceName, links: config.links,
instance: config.instanceName,
user: req.session.user,
profile,
selfProfile,
@ -52,7 +52,7 @@ module.exports.get = async function(req, res){
});
}else{
res.render('profile', {
instance: config.instanceName, links: config.links,
instance: config.instanceName,
user: req.session.user,
profile: null,
selfProfile: false,

View file

@ -27,5 +27,5 @@ module.exports.get = async function(req, res){
const challenge = await altchaUtils.genCaptcha();
//Render page
return res.render('register', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
return res.render('register', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -1,34 +0,0 @@
/*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/>.*/
//npm imports
const { Router } = require('express');
//local imports
const hrtController = require("../controllers/hrtController");
const presenceUtils = require("../utils/presenceUtils");
//globals
const router = Router();
//Use presence middleware
router.use(presenceUtils.presenceMiddleware);
//routing functions
router.get('/', hrtController.get);
module.exports = router;

View file

@ -217,22 +217,13 @@ migrationSchema.statics.ingestLegacyUser = async function(rawProfile){
return;
}
//Pull rank, dropping over-ranked users down to current enum length
let rank = Math.min(Math.max(0, profileArray[3]), permissionModel.rankEnum.length - 1);
//If this user was a mod on the old site
if(rank == 2){
//Set them up as a mod here
rank = permissionModel.rankEnum.length - 2;
}
//Create migration profile object from scraped info
const migrationProfile = new this({
user: profileArray[1],
pass: profileArray[2],
//Clamp rank to 0 and the max setting allowed by the rank enum
rank,
rank: Math.min(Math.max(0, profileArray[3]), permissionModel.rankEnum.length - 1),
email: validator.normalizeEmail(profileArray[4]),
date: profileArray[7],
})
@ -323,7 +314,7 @@ migrationSchema.statics.buildMigrationCache = async function(){
migrationSchema.statics.consumeByUsername = async function(ip, migration){
//Pull migration doc by case-insensitive username
const migrationDB = await this.findOne({user: new RegExp(`^${migration.user}$`, 'i')});
const migrationDB = await this.findOne({user: new RegExp(migration.user, 'i')});
//If we have no migration document
if(migrationDB == null){

View file

@ -256,13 +256,13 @@ userSchema.statics.register = async function(userObj, ip){
//Check password confirmation matches
if(pass == passConfirm){
//Setup user query
let userQuery = {user: new RegExp(`^${user}$`, 'i')};
let userQuery = {user: new RegExp(user, 'i')};
//If we have an email
if(email != null && email != ""){
userQuery = {$or: [
userQuery,
{email: new RegExp(`^${email}$`, 'i')}
{email: new RegExp(email, 'i')}
]};
}
@ -319,7 +319,7 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use
}
//get the user if it exists
const userDB = await this.findOne({ user: new RegExp(`^${user}$`, 'i')});
const userDB = await this.findOne({ user: new RegExp(user, 'i')});
//if not scream and shout
if(!userDB){

View file

@ -55,7 +55,6 @@ const fileNotFoundController = require('./controllers/404Controller');
//Humie-Friendly
const indexRouter = require('./routers/indexRouter');
const aboutRouter = require('./routers/aboutRouter');
const hrtRouter = require('./routers/hrtRouter');
const registerRouter = require('./routers/registerRouter');
const loginRouter = require('./routers/loginRouter');
const profileRouter = require('./routers/profileRouter');
@ -76,7 +75,6 @@ const apiRouter = require('./routers/apiRouter');
//Define Config variables
const config = require('../config.json');
const package = require('../package.json');
const port = config.port;
const dbUrl = `mongodb://${config.db.user}:${config.db.pass}@${config.db.address}:${config.db.port}/${config.db.database}`;
@ -180,7 +178,6 @@ app.use(sessionUtils.rememberMeMiddleware);
//Humie-Friendly
app.use('/', indexRouter);
app.use('/about', aboutRouter);
app.use('/hrt', hrtRouter);
app.use('/register', registerRouter);
app.use('/login', loginRouter);
app.use('/profile', profileRouter);
@ -211,7 +208,7 @@ Might be better if she kicked off everything at once, and ran a while loop to ch
This runs once at server startup, and most startups will run fairly quickly so... Not worth it?*/
async function asyncKickStart(){
//Lettum fuckin' know wassup
console.log(`${config.instanceName}(Powered by Canopy ${package.canopyDisplayVersion}) is booting up!`);
console.log(`${config.instanceName}(Powered by Canopy) is booting up!`);
//Run legacy migration
await migrationModel.ingestLegacyDump();

View file

@ -14,9 +14,6 @@ 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/>.*/
//Config
const config = require('../../config.json');
//NPM Imports
const validator = require('validator');//No express here, so regular validator it is!
const {sanitizeUrl} = require("@braintree/sanitize-url");
@ -35,15 +32,6 @@ module.exports.cache = new Map();
module.exports.markLink = async function(dirtyLink){
const link = sanitizeUrl(dirtyLink);
//If this link is referencing this web server
if(link.match(new RegExp(`^${config.protocol}://${config.domain}`)) != null){
//Lazily return it as a good link, since we know it'll at least return a good 404 page XP
return {
link,
type: "link"
}
}
//Check link cache for the requested link
const cachedLink = module.exports.cache.get(link);

View file

@ -131,7 +131,7 @@ module.exports.getMediaType = async function(dirtyURL){
}
//If we have link to a resource from archive.org
if(match = url.match(/archive\.org\/(?:details|download)\/(.+)/)){
if(match = url.match(/archive\.org\/(?:details|download)\/([a-zA-Z0-9\/._-\s\%]+)/)){
//return internet archive upload id and filepath
return {
type: "ia",

View file

@ -40,8 +40,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
it was decided that the original cytube fork, fore.st, had been run past it's prime. In summer/fall 2024, work began on a
replacement. The resulting software became <a href="https://git.ourfore.st/rainbownapkin/canopy">Canopy</a>, which was
first used to run the ourfore.st instance in late 2025.</p>
<br>
<h2>Canopy Ver: <%= version %></h2>
</div>
</div>
</body>

View file

@ -1,56 +0,0 @@
<%# 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/>. %>
<!DOCTYPE html>
<html>
<head>
<%- include('partial/styles', {instance, user}); %>
<%- include('partial/csrfToken', {csrfToken}); %>
<link rel="stylesheet" type="text/css" href="css/about.css">
<title><%= instance %> - DIY HRT Archive</title>
</head>
<body>
<%- include('partial/navbar', {user}); %>
<div id="about-div">
<h1>Bowie's DIY HRT Archive</h1>
<div class="dynamic-container" id="about-text">
<br>
This page is an attempt at putting together everything I know about DIY HRT.
<br><br>
So far I have used Homebrew Sublingual Oil from Open Gate Labs with great results, and have received a small batch of raw estradoil from Dragon Ordnance.
<br><br>
I am currently in the process of figuring out brewing my own sublingual oil.
<br><br>
<a href="/hrt.zip"><h3>This zip file contains everything I know.</h3></a>
<br>
<span>You should probably use <a href="https://www.torproject.org/">TOR</a> or a <a href="https://mullvad.net">decent VPN</a> in either an <a href="https://tails.net">amnesiac OS</a> or <a href="https://qubes-os.org">dispoable VM.</a> Everything paid w/ either <a href="https://www.getmonero.org/">XMR</a> or <a href="https://mullvad.net/en/blog/sending-cash-use-our-new-address">cash by mail</a>.</span>
<br><br>
<span class="critical-danger-text">
This page is not intended to be a replacement for professional medical advice, merely an attempt at harm reduction for my friends.
It should be used at most as a starting point for research. Everyone's HRT experience, and really transition, are unique and individual journeys.
Take the time to do the best research you can, to make sure you're starting and continuing yours correctly.
</span>
<br><br>
Much love, and remember to take your meds!
<br><br>
&nbsp;&nbsp;&nbsp;-rainbownapkin &lt;3
<br>&nbsp;
</div>
</div>
</body>
<footer>
<%- include('partial/scripts', {user}); %>
</footer>
</html>

View file

@ -20,24 +20,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
</span>
<span class="navbar-item" id="right-controls">
<% if(user){ %>
<p class="navbar-item">Welcome, <a class="navbar-item" id="username" href="/profile"><%= user.user %></a> - <% if(user.rank == "admin"){ %>
<a href="/adminPanel" title="Admin Panel" class="bi bi-server navbar-item"></a> -
<% } %>
<% for(link of Object.keys(links)){ %>
<a target="_blank" class="navbar-item" href="<%- links[link] %>"><%= link %></a> -
<% } %>
<a class="navbar-item" href="javascript:" id="logout-button">Logout</a></p>
<p class="navbar-item">Welcome, <a class="navbar-item" id="username" href="/profile"><%= user.user %></a> - <% if(user.rank == "admin"){ %><a href="/adminPanel" title="Admin Panel" class="bi bi-server navbar-item"></a> - <% } %> <a class="navbar-item" href="/about">About</a> - <a class="navbar-item" href="javascript:" id="logout-button">Logout</a></p>
<% }else{ %>
<p class="navbar-item">Remember Me:</p>
<input class="navbar-item login-prompt" id="remember-me" type="checkbox">
<input class="navbar-item login-prompt" id="username-prompt" placeholder="username">
<input class="navbar-item login-prompt" id="password-prompt" placeholder="password" type="password">
<p class="navbar-item"><a class="navbar-item" href="javascript:" id="login-button">Login</a> - <a class="navbar-item" href="/passwordReset">Forgot Password</a> - <a class="navbar-item" href="/register">Register</a>
<% for(link of Object.keys(links)){ %>
- <a target="_blank" class="navbar-item" href="<%- links[link] %>"><%= link %></a>
<% } %></p>
<p class="navbar-item"><a class="navbar-item" href="javascript:" id="login-button">Login</a> - <a class="navbar-item" href="/passwordReset">Forgot Password</a> - <a class="navbar-item" href="/register">Register</a> - <a class="navbar-item" href="/about">About</a></p>
<% } %>
</span>
</div>

View file

@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<link rel="stylesheet" type="text/css" href="/css/panel/settings.css">
<div id="settings-panel">
<h2>Client Settings</h2>
<h4>Playeback Settings</h4>
<h4>Player Settings</h4>
<span id="settings-panel-youtube-source" class="settings-panel-setting">
<p>Youtube Player Type: </p>
<select>
@ -40,15 +40,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<p>Syncronization Delta: </p>
<input type="number">
</span>
<h4>Display Settings</h4>
<h4>Chat Settings</h4>
<span id="settings-panel-min-chat-width" class="settings-panel-setting">
<p>Chat Width Minimum While Locked to Aspect Ratio: </p>
<p>Aspect-Ratio Lock Chat Width Minimum: </p>
<input type="number">
</span>
<span id="settings-panel-disable-portrait" class="settings-panel-setting">
<p>Disable Portrait/Mobile Layout: </p>
<input type="checkbox">
</span>
<h4>Notification Settings</h4>
<span id="settings-panel-ping-on-pm-rx" class="settings-panel-setting">
<p>Play Sound for received PMs: </p>

View file

@ -1,11 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="strikethroughFilter" primitiveUnits="objectBoundingBox">
<filter id="strikethroughFilter">
<feFlood
result="floodFill"
height="2%"
width="100%"
x="0"
y="49%"
y="50%"
width="100%"
height="1"
flood-color="black"
flood-opacity="1"
/>

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 376 B

Before After
Before After

View file

@ -288,16 +288,6 @@ class channel{
//Set Chat Box Width minimum while Locked to Aspect-Ratio
this.chatBox.chatWidthMinimum = value / 100;
return;
case 'disablePortrait':
//If the chat isn't loaded
if(this.chatBox == null){
//We're fuckin' done here
return;
}
//Toggle portrait mode
this.chatBox.togglePortrait();
return;
case 'userlistHidden':
//If the userlist class isn't loaded in yet
if(this.userList == null){
@ -336,8 +326,7 @@ class channel{
["rxPMSound", 'unread'],
["txPMSound", false],
["newSeshSound", true],
["endSeshSound", true],
["disablePortrait", false]
["endSeshSound", true]
]);
}
@ -348,8 +337,6 @@ function onYouTubeIframeAPIReady(){
//Set embed api to true
client.ytEmbedAPILoaded = true;
//If the player is ready and has a mediaHandler loaded
if(client.player != null && client.player.mediaHandler != null){
//Get currently playing item
const nowPlaying = client.player.mediaHandler.nowPlaying;
@ -358,7 +345,6 @@ function onYouTubeIframeAPIReady(){
//Restart the video now that the embed api has loaded
client.player.start({media: nowPlaying});
}
}
}
const client = new channel();

View file

@ -38,11 +38,6 @@ class chatBox{
*/
this.autoScroll = true;
/**
* Whether or not the screen is currently in portrait mode
*/
this.portrait = false;
/**
* Chat-Width Minimum while sized to media Aspect-Ratio
*/
@ -79,11 +74,6 @@ class chatBox{
this.chatPostprocessor = new chatPostprocessor(client);
//Element Nodes
/**
* Channel Div
*/
this.channelDiv = document.querySelector("#channel-flexbox");
/**
* Chat Panel Container Div
*/
@ -483,14 +473,8 @@ class chatBox{
resizeAspect(event){
const playerHidden = this.client.player.playerDiv.style.display == "none";
//If window is taller than wide and not in portrait mode, or vice-versa
if(this.portrait != (window.innerWidth <= window.innerHeight)){
//Toggle portrait mode
this.togglePortrait();
}
//If the aspect is locked/the window is portrait and the player isn't hidden
if((this.aspectLock || this.portrait) && !playerHidden){
//If the aspect is locked and the player is hidden
if(this.aspectLock && !playerHidden){
this.sizeToAspect();
//Otherwise
}else{
@ -506,13 +490,8 @@ class chatBox{
* Re-sizes chat box relative to media aspect ratio
*/
sizeToAspect(){
//If the chat panel is visible
if(this.chatPanel.style.display != "none"){
//If our window width is more than or equal to window height (not portrait mode)
if(!this.portrait){
//Get target video width by multiplying media ratio by window height
var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height;
//Get target chat width my subtracting target media width from total window width
const targetChatWidth = window.innerWidth - targetVidWidth;
//This should be changeable in settings later on, for now it defaults to 20%
const limit = window.innerWidth * this.chatWidthMinimum;
@ -523,17 +502,6 @@ class chatBox{
//Fix busted layout
var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
this.chatPanel.style.flexBasis = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
}else{
//Calculate target video height from media aspect ratio and window width
var targetVidHeight = window.innerWidth / this.client.player.getRatio();
//Calculate target chat height from the difference between the channel div height and the target video height
var targetChatHeight = this.channelDiv.getBoundingClientRect().height - targetVidHeight;
//Set div heights accordingly
this.client.player.playerDiv.style.height = `${targetVidHeight}px`;
this.chatPanel.style.height = `${targetChatHeight}px`;
}
//This sometimes gets called before userList ahs been initiated :p
if(this.client.userList != null){
this.client.userList.clickDragger.fixCutoff();
@ -541,38 +509,6 @@ class chatBox{
}
}
togglePortrait(){
//If our window width is more than or equal to window height (not portrait mode), or portrait mode is on while its supposed to be disabled
if(window.innerWidth >= window.innerHeight || (localStorage.getItem("disablePortrait") == 'true' && this.portrait)){
//Disable portrait CSS modifiers
this.channelDiv.style.flexDirection = "row";
this.clickDragger.enabled = true;
this.chatPanel.style.width = "";
this.client.player.playerDiv.style.height = "";
this.chatPanel.style.height = "";
//Disable portrait behavior modifiers
this.portrait = false;
//resize player in-case of empty flex basis
this.resizeAspect();
//Otherwise, if portrait mode is enabled
}else if(localStorage.getItem("disablePortrait") != 'true'){
//Modify CSS for portrait displays
this.channelDiv.style.flexDirection = "column";
this.clickDragger.enabled = false;
this.chatPanel.style.width = "100%";
this.chatPanel.style.flexBasis = "";
//Enable portrait behavior modifiers
this.portrait = true;
//resize player to correct height
this.resizeAspect();
}
}
/**
* Toggles Chat Box UX
* @param {Boolean} show - Whether or not to show Chat Box UX

View file

@ -140,11 +140,10 @@ class chatPostprocessor{
this.messageArray = [];
//Unescape any sanatized char codes as we use .textContent for double-safety, and to prevent splitting of char codes
//Split string by word-boundries on words and non-word boundries around whitespace,
//with negative lookaheads to exclude file seperators so we don't split link placeholders, dashes so we dont split usernames and other things, and accented characters to keep those from splitting boundries too
//Split string by word-boundries on words and non-word boundries around whitespace, with negative lookaheads to exclude file seperators so we don't split link placeholders, and dashes so we dont split usernames and other things
//Also split by any invisble whitespace as a crutch to handle mushed links/emotes
//If we can one day figure out how to split non-repeating special chars instead of special chars with whitespace, that would be perf, unfortunately my brain hasn't rotted enough to understand regex like that just yet.
const splitString = utils.unescapeEntities(this.rawData.msg).split(/(?<!-)(?<!␜)(?=\w)\b|(?!-|[\u00C0-\u017F])(?<=\w)\b|(?=\s)\B|(?<=\s)\B|/g);
const splitString = utils.unescapeEntities(this.rawData.msg).split(/(?<!-)(?<!␜)(?=\w)\b|(?!-)(?<=\w)\b|(?=\s)\B|(?<=\s)\B|/g);
//for each word in the splitstring
splitString.forEach((string) => {
@ -260,7 +259,7 @@ class chatPostprocessor{
link.textContent = wordObj.command;
//Add chatbox functionality
link.addEventListener('click', () => {this.client.chatBox.transmit(wordObj.command)});
link.addEventListener('click', () => {this.client.chatBox.commandPreprocessor.preprocess(wordObj.command)});
//We don't have to worry about injecting this into whitespace since there shouldn't be any here.
injectionArray.push(link);
@ -474,7 +473,7 @@ class chatPostprocessor{
//After eight characters
if(charIndex > 8){
//Push an invisible line-break character between every character
wordArray.push("");
wordArray.push("");
}
});

View file

@ -116,14 +116,14 @@ class commandPreprocessor{
*/
processEmotes(){
//inject invisible whitespace in-between emotes to prevent from mushing links together
this.message = this.message.replaceAll('][','][');
this.message = this.message.replaceAll('][','][');
//For each list of emotes
Object.keys(this.emotes).forEach((key) => {
//For each emote in the current list
this.emotes[key].forEach((emote) => {
//Inject emote links into the message, pad with invisible whitespace to keep link from getting mushed
this.message = this.message.replaceAll(`[${emote.name}]`, `${emote.link}`);
this.message = this.message.replaceAll(`[${emote.name}]`, `${emote.link}`);
});
});
}
@ -135,13 +135,13 @@ class commandPreprocessor{
//Strip out file seperators in-case the user is being a smart-ass
this.message = this.message.replaceAll('␜','');
//Split message by links
var splitMessage = this.message.split(/(https?:\/\/[^\s]+)/g);
var splitMessage = this.message.split(/(https?:\/\/[^\s]+)/g);
//Create an empty array to hold links
this.links = [];
splitMessage.forEach((chunk, chunkIndex) => {
//For each chunk that is a link
if(chunk.match(/(https?:\/\/[^\s]+)/g)){
if(chunk.match(/(https?:\/\/[^\s]+)/g)){
//I looked online for obscure characters that no one would use to prevent people from chatting embed placeholders
//Then I found this fucker, turns out it's literally made for the job lmao (even if it was originally intended for paper/magnetic tape)
//Replace link with indexed placeholder
@ -269,8 +269,7 @@ class commandPreprocessor{
usernames:{
prefix: '',
postfix: '',
//cmds: injectPerms(Array.from(client.userList.colorMap.keys()))
cmds: injectPerms(client.userList.getOnlineUserNames())
cmds: injectPerms(Array.from(client.userList.colorMap.keys()))
},
emotes:{
prefix:'[',

View file

@ -907,10 +907,6 @@ class hlsLiveStreamHandler extends hlsBase{
return;
}
//Resize chat box to video aspect, since this is the only event thats reliably called on ratio change
//Re-enforcing UX rules a little more often shouldnt cause too many issues anywho.
this.client.chatBox.resizeAspect();
//Calculate distance to end of stream
const difference = this.video.duration - this.video.currentTime;

View file

@ -24,7 +24,7 @@ class pmPanel extends panelObj{
* @param {channel} client - Parent client Management Object
* @param {Document} panelDocument - Panel Document
*/
constructor(client, panelDocument, startSesh){
constructor(client, panelDocument){
super(client, "Private Messaging", "/panel/pm", panelDocument);
/**
@ -71,17 +71,7 @@ class pmPanel extends panelObj{
//Tell PMHandler to start tracking this panel
this.client.pmHandler.panelList.set(this.uuid, null);
//Define network related listeners
this.defineListeners();
//If a start sesh was provided
if(startSesh != null && startSesh != ""){
//Send message out to server
this.client.pmSocket.emit("pm", {
recipients: startSesh.split(" "),
msg: ""
});
}
}
closer(){
@ -136,6 +126,7 @@ class pmPanel extends panelObj{
this.seshSendButton.addEventListener("click", this.send.bind(this));
this.seshBuffer.addEventListener('scroll', this.scrollHandler.bind(this));
this.ownerDoc.defaultView.addEventListener('resize', this.handleAutoScroll.bind(this));
}
startSesh(event){
@ -189,12 +180,6 @@ class pmPanel extends panelObj{
* Render out current sesh array to sesh list UI
*/
renderSeshList(){
//If we don't have a sesh list
if(this.seshList == null){
//Fuck off, you're not even done building the object yet.
return;
}
//Clear out the sesh list
this.seshList.innerHTML = "";

View file

@ -1233,11 +1233,6 @@ class queuePanel extends panelObj{
//Convert start epoch to JS date object
const started = new Date(nowPlaying.startTime);
//If the date the scheduler is set to isn't within the livestream
if(!utils.isSameDate(started, this.day) && !utils.dateWithinRange(started, new Date(), this.day)){
return;
}
//If this started today
if(utils.isSameDate(this.day, started)){
//Set entryDiv top-border location based on start time
@ -1251,29 +1246,15 @@ class queuePanel extends panelObj{
entryDiv.style.top = `${this.offsetByDate(dawn)}px`;
//Apply rest of the styling rules for items that started yestarday
entryDiv.classList.add('started-yesterday');
entryDiv.classList.add('started-yesterday')
}
//Create entry title
const entryTitle = document.createElement('p');
entryTitle.textContent = utils.unescapeEntities(nowPlaying.title);
//If we're looking at today
if(utils.isSameDate(this.day, new Date())){
//Set entry div bottom-border location based on current time, round to match time marker
entryDiv.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`;
}else{
//Get midnight
const dusk = new Date();
dusk.setHours(23,59,59,999);
//Set stream to continue to run into the next morning
entryDiv.style.bottom = `${Math.round(this.offsetByDate(dusk, true))}px`;
//Apply rest of the styling rules for items that end after today
entryDiv.classList.add('ends-tomorrow');
}
entryDiv.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
//Assembly entryDiv
entryDiv.appendChild(entryTitle);
@ -1304,12 +1285,9 @@ class queuePanel extends panelObj{
//Append entry div to queue container
this.queueContainer.appendChild(entryDiv);
}else{
//If we're looking at today
if(utils.isSameDate(this.day, new Date())){
//Update existing entry, round offset to match time marker
staleEntry.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
}
}
//Keep tooltip date seperate so it re-calculates live duration properly
function buildTooltip(date = new Date()){
@ -1563,7 +1541,7 @@ class reschedulePopup extends schedulePopup{
this.media = media;
}
schedule(event){
startSesh(event){
//If we clicked or hit enter
if(event.key == null || event.key == "Enter"){
//Get localized input date

View file

@ -62,11 +62,6 @@ class settingsPanel extends panelObj{
*/
this.chatWidthMinimum = this.panelDocument.querySelector("#settings-panel-min-chat-width input");
/**
* Disable Portrait/Mobile Layout
*/
this.disablePortrait = this.panelDocument.querySelector("#settings-panel-disable-portrait input");
/**
* Audible Ping on PM Recieved
*/
@ -104,7 +99,6 @@ class settingsPanel extends panelObj{
this.liveSyncTolerance.addEventListener('change', this.updateLiveSyncTolerance.bind(this));
this.syncDelta.addEventListener('change', this.updateSyncDelta.bind(this));
this.chatWidthMinimum.addEventListener('change', this.updateChatWidthMinimum.bind(this));
this.disablePortrait.addEventListener('change', this.updateDisablePortrait.bind(this));
this.rxPMSound.addEventListener('change', this.updateRXPMSound.bind(this));
this.txPMSound.addEventListener('change', this.updateTXPMSound.bind(this));
this.newSeshSound.addEventListener('change', this.updateNewPMSeshSound.bind(this));
@ -121,7 +115,6 @@ class settingsPanel extends panelObj{
this.liveSyncTolerance.value = localStorage.getItem("liveSyncTolerance");
this.syncDelta.value = localStorage.getItem("syncDelta");
this.chatWidthMinimum.value = localStorage.getItem("chatWidthMin");
this.disablePortrait.checked = localStorage.getItem("disablePortrait") == 'true';
this.rxPMSound.value = localStorage.getItem('rxPMSound');
this.txPMSound.checked = localStorage.getItem('txPMSound') == 'true';
this.newSeshSound.checked = localStorage.getItem('newSeshSound') == 'true';
@ -225,20 +218,11 @@ class settingsPanel extends panelObj{
client.processConfig("chatWidthMin", this.chatWidthMinimum.value);
}
/**
* Handles change toggle of disable/enable portrait
*/
updateDisablePortrait(){
localStorage.setItem('disablePortrait', this.disablePortrait.checked);
client.processConfig("disablePortrait", this.disablePortrait.checked);
}
/**
* Handles changes to RX PM Sound setting
*/
updateRXPMSound(){
localStorage.setItem('rxPMSound', this.rxPMSound.value);
client.processConfig("rxPMSound", this.rxPMSound.value);
}
/**
@ -246,7 +230,6 @@ class settingsPanel extends panelObj{
*/
updateTXPMSound(){
localStorage.setItem('txPMSound', this.txPMSound.checked);
client.processConfig("txPMSound", this.txPMSound.checked);
}
/**
@ -254,7 +237,6 @@ class settingsPanel extends panelObj{
*/
updateNewPMSeshSound(){
localStorage.setItem('newSeshSound', this.newSeshSound.checked);
client.processConfig("newSeshSound", this.newSeshSound.checked);
}
/**
@ -262,6 +244,5 @@ class settingsPanel extends panelObj{
*/
updateEndPMSeshSound(){
localStorage.setItem('endSeshSound', this.endSeshSound.checked);
client.processConfig("endSeshSound", this.endSeshSound.checked);
}
}

View file

@ -33,12 +33,6 @@ class userList{
*/
this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-users-drag-handle", "#chat-panel-users-div", true, this.client.chatBox.clickDragger);
/**
* List of currently connected users
*/
this.userList = [];
/**
* Userlist color array (Maps to css classes)
*/
@ -52,7 +46,7 @@ class userList{
"userlist-color6"];
/**
* Map of connected and buffered usernames to assigned username color/flair
* Map of usernames to assigned username color
*/
this.colorMap = new Map();
@ -64,7 +58,7 @@ class userList{
/**
* userlist div
*/
this.userListDiv = document.querySelector("#chat-panel-users-list-div");
this.userList = document.querySelector("#chat-panel-users-list-div");
/**
* user count label
@ -109,28 +103,28 @@ class userList{
updateList(list){
//Clear list and set user count
this.userCount.textContent = list.length == 1 ? '1 User' : `${list.length} Users`;
this.userListDiv.innerHTML = null;
this.userList.innerHTML = null;
//Set userlist array to received list
this.userList = list;
//create a new map
var newMap = new Map();
//for each user
list.forEach((user) => {
//randomly pick a color
var color = this.userColors[Math.floor(Math.random()*this.userColors.length)]
//if this user wasn't in the previous colormap
if(this.colorMap.get(user.user) == null){
//Store new randomly selected color
this.colorMap.set(user.user, color);
}else{
//Use color from previous entry
//if this user was in the previous colormap
if(this.colorMap.get(user.user) != null){
//Override with previous color
color = this.colorMap.get(user.user);
}
newMap.set(user.user, color);
this.renderUser(user, color);
});
this.colorMap = newMap;
//Make sure we're not cutting the ux off
this.clickDragger.fixCutoff();
}
@ -174,13 +168,12 @@ class userList{
userSpan.addEventListener('click', renderContextMenu.bind(this));
userSpan.addEventListener('contextmenu', renderContextMenu.bind(this));
this.userListDiv.appendChild(userSpan);
this.userList.appendChild(userSpan);
function renderContextMenu(event){
//Setup menu map
let menuMap = new Map([
["Profile", ()=>{this.client.cPanel.setActivePanel(new panelObj(this.client, user.user, `/panel/profile?user=${user.user}`))}],
["PM", ()=>{this.client.cPanel.setActivePanel(new pmPanel(client, undefined, user.user))}],
["Profile", ()=>{this.client.cPanel.setActivePanel(new panelObj(this.client, `${user.user}`, `/panel/profile?user=${user.user}`))}],
["Mention", ()=>{this.client.chatBox.catChat(`${user.user} `)}],
["Toke With", ()=>{this.client.chatBox.tokeWith(user.user)}],
]);
@ -217,14 +210,4 @@ class userList{
}
}
//returns list of strings containing all currently online users
getOnlineUserNames(){
var names = [];
for(let user of this.userList){
names.push(user.user);
}
return names;
}
}

View file

@ -198,7 +198,7 @@ class rankList{
imgNode.src = user.img;
//If the listed user rank is equal or higher than the signed-in user
if(curUser != null && rankEnum.indexOf(user.rank) >= rankEnum.indexOf(curUser.rank)){
if(rankEnum.indexOf(user.rank) >= rankEnum.indexOf(curUser.rank)){
var rankContent = user.rank;
}else{
//Create rank select