Source: utils/linkUtils.js

/*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 validator = require('validator');//No express here, so regular validator it is!

//Create link cache
/**
 * Basic RAM-Based cache of links, so we don't have to re-pull things after we get them
 */
module.exports.cache = new Map();

/**
 * Validates links and returns a marked link object that can be returned to the client to format/embed accordingly
 * @param {String} link - URL to Validate
 * @returns {Object} Marked link object
 */
module.exports.markLink = async function(link){
    //Check link cache for the requested link
    const cachedLink = module.exports.cache.get(link);

    //If we have a cached result
    if(cachedLink){
        //return the cached link
        return cachedLink;
    }

    //Set max file size to 4MB
    const maxSize = 4000000;
    //Assume links are guilty until proven innocent
    var type = "malformedLink"    

    //Make sure we have an actual, factual URL
    if(validator.isURL(link)){
        //The URL is valid, so this is at least a dead link
        type = 'deadLink';

        //Don't try this at home, we're what you call "Experts"
        //TODO: Handle this shit simultaneously and send the chat before its done, then send updated types for each link as they're pulled individually
        try{
            //Pull content type
            var response = await fetch(link,{
                method: "HEAD",
            });

            //If we made it this far then the link is, at the very least, not dead.
            type = 'link'

            //Get file type from header
            const fileType = response.headers.get('content-type');
            const fileSize = response.headers.get('content-length');

            //If they're reporting file types
            if(fileType != null){
                //If we have an image
                if(fileType.match('image/')){
                    //If the file size is unreported OR it's smaller than 4MB (not all servers report this and images that big are pretty rare)
                    if(fileSize == null || fileSize <= maxSize){
                        //Mark link as an image
                        type = 'image';
                    }
                //If it's a video
                }else if(fileType.match('video/mp4' || 'video/webm')){
                    //If the server is reporting file-size and it's reporting under 4MB (Reject unreported sizes to be on the safe side is video is huge)
                    if(fileSize != null && fileSize <= maxSize){
                        //mark link as a video
                        type = 'video';
                    }
                }
            }
        //Probably bad form but if something happens in here I'm blaming whoever hosted the link
        //maybe don't host a fucked up server and I wouldn't handle with an empty catch
        }catch{};
    }

    //Create the link object from processed information
    const linkObj = {
        link,
        type
    }

    //Cache the result
    module.exports.cache.set(link, linkObj);

    //Set timer to remove cache entry in five minutes
    setTimeout(()=>{
        module.exports.cache.delete(link);
    }, 300000)

    //return the link
    return linkObj;
}