diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js
index 9c3e6ab..6113873 100644
--- a/src/app/channel/channelManager.js
+++ b/src/app/channel/channelManager.js
@@ -23,6 +23,7 @@ const loggerUtils = require('../../utils/loggerUtils');
const csrfUtils = require('../../utils/csrfUtils');
const activeChannel = require('./activeChannel');
const chatHandler = require('./chatHandler');
+const mediaYanker = require('./media/yanker');
module.exports = class{
constructor(io){
@@ -34,6 +35,7 @@ module.exports = class{
//Load server components
this.chatHandler = new chatHandler(this);
+ this.mediaYanker = new mediaYanker(this);
//Handle connections from socket.io
io.on("connection", this.handleConnection.bind(this) );
@@ -71,6 +73,7 @@ module.exports = class{
//Define listeners
this.defineListeners(socket);
this.chatHandler.defineListeners(socket);
+ this.mediaYanker.defineListeners(socket);
//Connect the socket to it's given channel
//Lil' hacky to pass chanDB like that, but why double up on DB calls?
@@ -127,7 +130,7 @@ module.exports = class{
}
async getActiveChan(socket){
- socket.chan = socket.handshake.headers.referer.split('/c/')[1];
+ socket.chan = socket.handshake.headers.referer.split('/c/')[1].split('/')[0];
const chanDB = (await channelModel.findOne({name: socket.chan}))
//Check if channel exists
diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js
index 232f158..03a4bc9 100644
--- a/src/app/channel/commandPreprocessor.js
+++ b/src/app/channel/commandPreprocessor.js
@@ -33,7 +33,7 @@ module.exports = class commandPreprocessor{
async preprocess(socket, data){
//Set command object
- let commandObj = {
+ const commandObj = {
socket,
sendFlag: true,
rawData: data,
diff --git a/src/app/channel/media/media.js b/src/app/channel/media/media.js
index 998d666..30e21e5 100644
--- a/src/app/channel/media/media.js
+++ b/src/app/channel/media/media.js
@@ -18,8 +18,9 @@ along with this program. If not, see .*/
const crypto = require('node:crypto');
module.exports = class{
- constructor(title, id, type, duration){
+ constructor(title, fileName, id, type, duration){
this.title = title;
+ this.fileName = fileName
this.id = id;
this.type = type;
this.duration = duration;
diff --git a/src/app/channel/media/yanker.js b/src/app/channel/media/yanker.js
new file mode 100644
index 0000000..c6ad7be
--- /dev/null
+++ b/src/app/channel/media/yanker.js
@@ -0,0 +1,87 @@
+/*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 validator = require('validator');//No express here, so regular validator it is!
+
+//local import
+const loggerUtils = require('../../../utils/loggerUtils');
+const iaUtil = require('../../../utils/media/internetArchiveUtils');
+const media = require('./media');
+
+module.exports = class{
+ constructor(server){
+ this.server = server;
+ }
+
+ defineListeners(socket){
+ socket.on("yank", (data) => {this.testYank(socket, data)});
+ }
+
+ async testYank(socket, data){
+ try{
+ console.log(await this.yankMedia(data.url));
+ }catch(err){
+ return loggerUtils.socketExceptionHandler(socket, err);
+ }
+ }
+
+ async yankMedia(url){
+ const pullType = await this.getMediaType(url)
+
+ if(pullType == 'ia'){
+ //Create empty list to hold media objects
+ const mediaList = []
+ //Pull metadata from IA
+ const mediaInfo = await iaUtil.fetchMetadata(url);
+
+ //for every compatible and relevant file returned from IA
+ for(let file of mediaInfo.files){
+ //Split file path by directories
+ const path = file.name.split('/');
+ //pull filename from path
+ const name = path[path.length - 1];
+ //Construct link from pulled info
+ const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}`
+
+ //Create new media object from file info
+ mediaList.push(new media(name, name, link, 'ia', file.length));
+ }
+
+ //return media object list
+ return mediaList
+ }else{
+ //return null to signify a bad url
+ return null
+ }
+ }
+
+ async getMediaType(url){
+ //Check if we have a valid url
+ if(!validator.isURL(url)){
+ //If not toss the fucker out
+ return null
+ }
+
+ //If we have link to a resource from archive.org
+ if(url.match(/^https\:\/\/archive.org\//g)){
+ //return internet archive code
+ return "ia";
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/media/internetArchiveUtils.js b/src/utils/media/internetArchiveUtils.js
new file mode 100644
index 0000000..3bad9f8
--- /dev/null
+++ b/src/utils/media/internetArchiveUtils.js
@@ -0,0 +1,84 @@
+/*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 .*/
+
+//Node Imports
+const url = require("node:url");
+
+//Local Imports
+const regexUtils = require('../regexUtils');
+
+module.exports.fetchMetadata = async function(link){
+ //Parse link
+ const parsedLink = new url.URL(link);
+ //Split link path
+ const splitPath = parsedLink.pathname.split('/');
+ //Get ItemID from link path
+ const itemID = splitPath[2]
+ //Splice the empty string, request type, and item ID out from link path
+ splitPath.splice(0,3)
+ //Join remaining link path back together to get requested file path within the given archive.org upload
+ const requestedPath = decodeURIComponent(splitPath.join('/'));
+
+ //Create metadata link from itemID
+ const metadataLink = `https://archive.org/metadata/${itemID}`;
+
+ //Fetch item metadata from the internet archive
+ const response = await fetch(metadataLink,
+ {
+ method: "GET"
+ }
+ );
+
+ //If we hit a snag
+ if(!response.ok){
+ //Scream and shout
+ const errorBody = await response.text();
+ throw new Error(`Internet Archive Error '${response.status}': ${errorBody}`);
+ }
+
+ //Collect our metadata
+ const rawMetadata = await response.json();
+
+ //Filter out any in-compatible files
+ const compatibleFiles = rawMetadata.files.filter(compatibilityFilter);
+
+ //If we're requesting an empty path
+ if(requestedPath == ''){
+ //Return item metadata and compatible files
+ return {
+ files: compatibleFiles,
+ metadata: rawMetadata.metadata
+ }
+ //Other wise
+ }else{
+ //Return item metadata and matching compatible files
+ return {
+ //Filter files out that don't match requested path and return remaining list
+ files: compatibleFiles.filter(pathFilter),
+ metadata: rawMetadata.metadata
+ }
+ }
+
+ function compatibilityFilter(file){
+ //return true for all files that match for web-safe formats
+ return file.format == "h.264"
+ }
+
+ function pathFilter(file){
+ //return true for all file names which match the given requested file path
+ return file.name.match(`^${regexUtils.escapeRegex(requestedPath)}`);
+ }
+}
\ No newline at end of file
diff --git a/src/utils/regexUtils.js b/src/utils/regexUtils.js
new file mode 100644
index 0000000..09a50c4
--- /dev/null
+++ b/src/utils/regexUtils.js
@@ -0,0 +1,23 @@
+ /*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 .*/
+
+module.exports.escapeRegex = function(string){
+ /* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid
+ In my defense I only did it because js-runtime-devs are taking fucking eons to implement RegExp.escape()
+ This should be replaced once that function becomes available in mainline versions of node.js:
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape */
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
\ No newline at end of file
diff --git a/www/js/adminUtils.js b/www/js/adminUtils.js
index fbe83de..cb5c3a6 100644
--- a/www/js/adminUtils.js
+++ b/www/js/adminUtils.js
@@ -31,7 +31,7 @@ class canopyAdminUtils{
body: JSON.stringify({permissionsMap: Object.fromEntries(permMap)})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -49,7 +49,7 @@ class canopyAdminUtils{
body: JSON.stringify({channelPermissionsMap: Object.fromEntries(permMap)})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -67,7 +67,7 @@ class canopyAdminUtils{
body: JSON.stringify({user, rank})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -79,7 +79,7 @@ class canopyAdminUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -97,7 +97,7 @@ class canopyAdminUtils{
body: JSON.stringify({user, permanent, ipBan, expirationDays})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -115,7 +115,7 @@ class canopyAdminUtils{
body: JSON.stringify({user})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -127,7 +127,7 @@ class canopyAdminUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -145,7 +145,7 @@ class canopyAdminUtils{
body: JSON.stringify({command})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -163,7 +163,7 @@ class canopyAdminUtils{
body: JSON.stringify({command})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -175,7 +175,7 @@ class canopyAdminUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -193,7 +193,7 @@ class canopyAdminUtils{
body: JSON.stringify({name, link})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -211,7 +211,7 @@ class canopyAdminUtils{
body: JSON.stringify({name})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -229,7 +229,7 @@ class canopyAdminUtils{
body: JSON.stringify({user})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js
index c2d951c..cf23311 100644
--- a/www/js/channel/channel.js
+++ b/www/js/channel/channel.js
@@ -22,7 +22,7 @@ class channel{
this.defineListeners();
//Scrape channel name off URL
- this.channelName = window.location.pathname.split('/c/')[`1`];
+ this.channelName = window.location.pathname.split('/c/')[1].split('/')[0];
//Create the Video Player Object
this.player = new player(this);
diff --git a/www/js/utils.js b/www/js/utils.js
index 64250f5..12a3750 100644
--- a/www/js/utils.js
+++ b/www/js/utils.js
@@ -501,7 +501,7 @@ class canopyAjaxUtils{
body: JSON.stringify(email ? {user, pass, passConfirm, email, verification} : {user, pass, passConfirm, verification})
});
- if(response.status == 200){
+ if(response.ok){
location = "/";
}else{
utils.ux.displayResponseError(await response.json());
@@ -518,7 +518,7 @@ class canopyAjaxUtils{
body: JSON.stringify(verification ? {user, pass, verification} : {user, pass})
});
- if(response.status == 200){
+ if(response.ok){
location.reload();
}else if(response.status == 429){
location = `/login?user=${user}`;
@@ -535,7 +535,7 @@ class canopyAjaxUtils{
}
});
- if(response.status == 200){
+ if(response.ok){
location.reload();
}else{
utils.ux.displayResponseError(await response.json());
@@ -552,7 +552,7 @@ class canopyAjaxUtils{
body: JSON.stringify(update)
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -564,7 +564,7 @@ class canopyAjaxUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return (await response.json())
}else{
utils.ux.displayResponseError(await response.json());
@@ -581,7 +581,7 @@ class canopyAjaxUtils{
body: JSON.stringify({pass})
});
- if(response.status == 200){
+ if(response.ok){
window.location.pathname = '/';
}else{
utils.ux.displayResponseError(await response.json());
@@ -599,7 +599,7 @@ class canopyAjaxUtils{
});
//If we received a successful response
- if(response.status == 200){
+ if(response.ok){
//Create pop-up
const popup = new canopyUXUtils.popup("A password reset link has been sent to the email associated with the account requested assuming it has one!");
//Go to home-page on pop-up closure
@@ -621,7 +621,7 @@ class canopyAjaxUtils{
});
//If we received a successful response
- if(response.status == 200){
+ if(response.ok){
//Create pop-up
const popup = new canopyUXUtils.popup("Your password has been reset, and all devices have been logged out of your account!");
//Go to home-page on pop-up closure
@@ -643,7 +643,7 @@ class canopyAjaxUtils{
});
//If we received a successful response
- if(response.status == 200){
+ if(response.ok){
const popup = new canopyUXUtils.popup("A confirmation link has been sent to the new email address.");
//Otherwise
}else{
@@ -662,7 +662,7 @@ class canopyAjaxUtils{
body: JSON.stringify(thumbnail ? {name, description, thumbnail, verification} : {name, description, verification})
});
- if(response.status == 200){
+ if(response.ok){
location = "/";
}else{
utils.ux.displayResponseError(await response.json());
@@ -680,7 +680,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, settingsMap: Object.fromEntries(settingsMap)})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -698,7 +698,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, channelPermissionsMap: Object.fromEntries(permissionsMap)})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -710,7 +710,7 @@ class canopyAjaxUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return (await response.json())
}else{
utils.ux.displayResponseError(await response.json());
@@ -727,7 +727,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, user, rank})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -742,7 +742,7 @@ class canopyAjaxUtils{
}
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -759,7 +759,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, user, expirationDays, banAlts})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -776,7 +776,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, user})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -791,7 +791,7 @@ class canopyAjaxUtils{
}
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -809,7 +809,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, command})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -826,7 +826,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, command})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -841,7 +841,7 @@ class canopyAjaxUtils{
}
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -858,7 +858,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, emoteName, link})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -875,7 +875,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, emoteName})
});
- if(response.status == 200){
+ if(response.ok){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
@@ -892,7 +892,7 @@ class canopyAjaxUtils{
body: JSON.stringify({chanName, confirm})
});
- if(response.status == 200){
+ if(response.ok){
location = "/";
}else{
utils.ux.displayResponseError(await response.json());
@@ -906,7 +906,7 @@ class canopyAjaxUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return (await response.text())
}else{
utils.ux.displayResponseError(await response.json());
@@ -919,7 +919,7 @@ class canopyAjaxUtils{
method: "GET"
});
- if(response.status == 200){
+ if(response.ok){
return (await response.text())
}else{
utils.ux.displayResponseError(await response.json());