From b443840c299f38fc13043a395f1bcff68bbf5473 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 12 Jan 2025 16:39:03 -0500 Subject: [PATCH] Finished up with internet archive api utility. --- src/app/channel/channelManager.js | 5 +- src/app/channel/commandPreprocessor.js | 2 +- src/app/channel/media/media.js | 3 +- src/app/channel/media/yanker.js | 87 +++++++++++++++++++++++++ src/utils/media/internetArchiveUtils.js | 84 ++++++++++++++++++++++++ src/utils/regexUtils.js | 23 +++++++ www/js/adminUtils.js | 26 ++++---- www/js/channel/channel.js | 2 +- www/js/utils.js | 52 +++++++-------- 9 files changed, 241 insertions(+), 43 deletions(-) create mode 100644 src/app/channel/media/yanker.js create mode 100644 src/utils/media/internetArchiveUtils.js create mode 100644 src/utils/regexUtils.js 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());