From 27ab1c2c71b12a9d0b1cfad754e73105385be38e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 12 Nov 2025 19:24:59 -0500 Subject: [PATCH 01/12] Upgraded version from 0.4-Indev Hotfix 3 to 0.1-Alpha --- README.md | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c6f2762..04459b0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Canopy -0.4-INDEV Hotfix 3 +0.1-Alpha ========= Canopy - /ˈkæ.nə.pi/: diff --git a/package.json b/package.json index e0c23a8..e5e493e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "canopy-of-indev", - "version": "0.4.3", - "canopyDisplayVersion": "0.4-Indev Hotfix 3", + "name": "canopy-of-alpha", + "version": "0.1", + "canopyDisplayVersion": "0.1-Alpha", "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "^7.1.1", From f0555169fe431ede29fdba43b9ce717da3bc8bc4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 23 Nov 2025 07:50:49 -0500 Subject: [PATCH 02/12] Bugfix for DB lookup by username for certain internal methods/functions. --- src/controllers/api/account/emailChangeRequestController.js | 2 +- src/controllers/api/account/loginController.js | 2 +- src/schemas/user/migrationSchema.js | 2 +- src/schemas/user/userSchema.js | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/account/emailChangeRequestController.js b/src/controllers/api/account/emailChangeRequestController.js index 4791ba2..3108ab6 100644 --- a/src/controllers/api/account/emailChangeRequestController.js +++ b/src/controllers/api/account/emailChangeRequestController.js @@ -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 diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index d509342..be8685b 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -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}); + const migrationDB = await migrationModel.findOne({user: new RegExp(`^${user}$`, 'i')}); //If we found a migration profile if(migrationDB != null){ diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 9aff595..4e61ce0 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -323,7 +323,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){ diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 41dfcda..783b241 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -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){ From 02f57fafd5f95b9ad96fee604cbd994f18ae09e4 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 23 Nov 2025 07:50:59 -0500 Subject: [PATCH 03/12] Rolled over to v0.1-Alpha Hotfix 1 --- README.md | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 04459b0..145d7e6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Canopy -0.1-Alpha +0.1-Alpha (Panama Red) - Hotfix 1 ========= Canopy - /ˈkæ.nə.pi/: diff --git a/package.json b/package.json index e5e493e..8554c13 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canopy-of-alpha", - "version": "0.1", - "canopyDisplayVersion": "0.1-Alpha", + "version": "0.1.1", + "canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 1", "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "^7.1.1", From 47bd012c8da05ad9a2006bcb3cf160513c67688e Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 25 Dec 2025 01:32:09 -0500 Subject: [PATCH 04/12] Fixed admin-facing display bug in channel settings page. --- www/js/channelSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/channelSettings.js b/www/js/channelSettings.js index 1af50fc..5ff1aaf 100644 --- a/www/js/channelSettings.js +++ b/www/js/channelSettings.js @@ -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(rankEnum.indexOf(user.rank) >= rankEnum.indexOf(curUser.rank)){ + if(curUser != null && rankEnum.indexOf(user.rank) >= rankEnum.indexOf(curUser.rank)){ var rankContent = user.rank; }else{ //Create rank select From 7436626df7b741c6ab8ed27c55c1f9edaa6e4a01 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 25 Dec 2025 03:07:39 -0500 Subject: [PATCH 05/12] Updated version number to hotfix 2 --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 145d7e6..b333dfb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Canopy -0.1-Alpha (Panama Red) - Hotfix 1 +0.1-Alpha (Panama Red) - Hotfix 2 ========= Canopy - /ˈkæ.nə.pi/: diff --git a/package.json b/package.json index 8554c13..115c861 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "canopy-of-alpha", - "version": "0.1.1", + "version": "0.1.2", "canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 1", "license": "AGPL-3.0-only", "dependencies": { From 3deb2e2d522ad16db614ff083b5a235b13422265 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 25 Dec 2025 03:07:41 -0500 Subject: [PATCH 06/12] Fixed toke saves. --- package.json | 2 +- src/app/channel/tokebot.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 115c861..ee1cb25 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canopy-of-alpha", "version": "0.1.2", - "canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 1", + "canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 2", "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "^7.1.1", diff --git a/src/app/channel/tokebot.js b/src/app/channel/tokebot.js index 6e41471..52743eb 100644 --- a/src/app/channel/tokebot.js +++ b/src/app/channel/tokebot.js @@ -148,6 +148,15 @@ 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 @@ -210,7 +219,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); } /** From 7aeb4e9d0ece6fb2f27a6cb6dcb4f05921295d0f Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 25 Dec 2025 03:20:57 -0500 Subject: [PATCH 07/12] Fixed clickable command examples. --- www/js/channel/chatPostprocessor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index e6daf7b..a9d1370 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -259,7 +259,7 @@ class chatPostprocessor{ link.textContent = wordObj.command; //Add chatbox functionality - link.addEventListener('click', () => {this.client.chatBox.commandPreprocessor.preprocess(wordObj.command)}); + link.addEventListener('click', () => {this.client.chatBox.transmit(wordObj.command)}); //We don't have to worry about injecting this into whitespace since there shouldn't be any here. injectionArray.push(link); From 7b054b235db54603d43a852efc366cc74f7b9972 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 26 Dec 2025 12:42:40 -0500 Subject: [PATCH 08/12] Server now prevents empty chats. --- src/app/channel/chatHandler.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index bc1e751..218f561 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -212,6 +212,12 @@ 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); From d669ed4783ad07a78483c89d51551f29cc1e0edc Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 26 Dec 2025 12:54:41 -0500 Subject: [PATCH 09/12] Prevented accented letters splitting words in client-side chatPostprocessor --- www/js/channel/chatPostprocessor.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index a9d1370..a1b7e43 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -140,10 +140,11 @@ 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, and dashes so we dont split usernames and other things + //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 //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(/(? { From de5268a41f13d6d25498e0615ae502febf84f4e8 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 22 Apr 2026 12:34:49 -0400 Subject: [PATCH 10/12] Userlist colors persist accross other users' reconnects. --- www/js/channel/commandPreprocessor.js | 3 +- www/js/channel/userlist.js | 40 +++++++++++++++++++-------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index af05cfe..1dbc218 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -269,7 +269,8 @@ class commandPreprocessor{ usernames:{ prefix: '', postfix: '', - cmds: injectPerms(Array.from(client.userList.colorMap.keys())) + //cmds: injectPerms(Array.from(client.userList.colorMap.keys())) + cmds: injectPerms(client.userList.getOnlineUserNames()) }, emotes:{ prefix:'[', diff --git a/www/js/channel/userlist.js b/www/js/channel/userlist.js index 5a4f24c..9edecb0 100644 --- a/www/js/channel/userlist.js +++ b/www/js/channel/userlist.js @@ -32,6 +32,12 @@ class userList{ * Click Dragger object for handling userlist resizes */ 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) @@ -46,7 +52,7 @@ class userList{ "userlist-color6"]; /** - * Map of usernames to assigned username color + * Map of connected and buffered usernames to assigned username color/flair */ this.colorMap = new Map(); @@ -58,7 +64,7 @@ class userList{ /** * userlist div */ - this.userList = document.querySelector("#chat-panel-users-list-div"); + this.userListDiv = document.querySelector("#chat-panel-users-list-div"); /** * user count label @@ -103,28 +109,28 @@ class userList{ updateList(list){ //Clear list and set user count this.userCount.textContent = list.length == 1 ? '1 User' : `${list.length} Users`; - this.userList.innerHTML = null; + this.userListDiv.innerHTML = null; - //create a new map - var newMap = new Map(); + //Set userlist array to received list + this.userList = list; //for each user list.forEach((user) => { //randomly pick a color var color = this.userColors[Math.floor(Math.random()*this.userColors.length)] - //if this user was in the previous colormap - if(this.colorMap.get(user.user) != null){ - //Override with previous color + //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 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(); } @@ -168,7 +174,7 @@ class userList{ userSpan.addEventListener('click', renderContextMenu.bind(this)); userSpan.addEventListener('contextmenu', renderContextMenu.bind(this)); - this.userList.appendChild(userSpan); + this.userListDiv.appendChild(userSpan); function renderContextMenu(event){ //Setup menu map @@ -211,4 +217,14 @@ 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; + } } \ No newline at end of file From 084acabae104346e64709adfc213b80bc9431109 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 13 May 2026 01:33:35 -0400 Subject: [PATCH 11/12] Started work on community DIY HRT Archive --- src/controllers/hrtController.js | 28 +++++++++++++++++++ src/routers/hrtRouter.js | 34 +++++++++++++++++++++++ src/server.js | 1 + src/views/hrt.ejs | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 src/controllers/hrtController.js create mode 100644 src/routers/hrtRouter.js create mode 100644 src/views/hrt.ejs diff --git a/src/controllers/hrtController.js b/src/controllers/hrtController.js new file mode 100644 index 0000000..e03d5ac --- /dev/null +++ b/src/controllers/hrtController.js @@ -0,0 +1,28 @@ +/*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 .*/ + +//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, user: req.session.user, csrfToken: csrfUtils.generateToken(req)}); +} \ No newline at end of file diff --git a/src/routers/hrtRouter.js b/src/routers/hrtRouter.js new file mode 100644 index 0000000..bcf314b --- /dev/null +++ b/src/routers/hrtRouter.js @@ -0,0 +1,34 @@ +/*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 .*/ + +//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; diff --git a/src/server.js b/src/server.js index f783a6d..935b07d 100644 --- a/src/server.js +++ b/src/server.js @@ -55,6 +55,7 @@ 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'); diff --git a/src/views/hrt.ejs b/src/views/hrt.ejs new file mode 100644 index 0000000..e435da9 --- /dev/null +++ b/src/views/hrt.ejs @@ -0,0 +1,46 @@ +<%# 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 . %> + + + + <%- include('partial/styles', {instance, user}); %> + <%- include('partial/csrfToken', {csrfToken}); %> + + <%= instance %> - DIY HRT Archive + + + <%- include('partial/navbar', {user}); %> +
+

Bowie's DIY HRT Archive

+
+ This page is an attempt at putting together everything I know about DIY HRT. + + 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. + + I am currently in the process of figuring out brewing my own sublingual oil. + + This zip file contains everything I know. + + 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 reasearch. Everyone's HRT experience, and really transition, are unique and individual journies. + Take the time to do the best to make sure you're starting and continuing yours correctly. +
+
+ +
+ <%- include('partial/scripts', {user}); %> +
+ From 4a684295fdb8b446354d4654304cb1284d070674 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Wed, 13 May 2026 02:11:41 -0400 Subject: [PATCH 12/12] Finished up HRT Archive. --- .gitignore | 3 ++- src/server.js | 1 + src/views/hrt.ejs | 24 +++++++++++++++++------- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 76cfb45..88e898a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ chatexamples.txt server.cert server.key www/nonfree/* -migration/* \ No newline at end of file +migration/* +www/hrt.zip \ No newline at end of file diff --git a/src/server.js b/src/server.js index 935b07d..228d555 100644 --- a/src/server.js +++ b/src/server.js @@ -180,6 +180,7 @@ 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); diff --git a/src/views/hrt.ejs b/src/views/hrt.ejs index e435da9..8777403 100644 --- a/src/views/hrt.ejs +++ b/src/views/hrt.ejs @@ -26,17 +26,27 @@ along with this program. If not, see . %>

Bowie's DIY HRT Archive

+
This page is an attempt at putting together everything I know about DIY HRT. - +

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

I am currently in the process of figuring out brewing my own sublingual oil. - - This zip file contains everything I know. - +

+

This zip file contains everything I know.

+
+ You should probably use TOR or a decent VPN in either an amnesiac OS or dispoable VM. Everything paid w/ either XMR or cash by mail. +

+ 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 reasearch. Everyone's HRT experience, and really transition, are unique and individual journies. - Take the time to do the best to make sure you're starting and continuing yours correctly. + 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. + +

+ Much love, and remember to take your meds! +

+    -rainbownapkin <3 +