Itch.io Web Integration

Shows if an Itch.io link has been claimed or not

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name        Itch.io Web Integration
// @namespace   Lex@GreasyFork
// @match       *://*.itch.io/*
// @match       *://*.steamgifts.com/discussion/*
// @match       *://*.keylol.com/*
// @match       *://*.reddit.com/r/*
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @version     0.1.8.8
// @author      Lex
// @description Shows if an Itch.io link has been claimed or not
// @connect     itch.io
// ==/UserScript==

(function(){
    'use strict';

    const CACHE_VERSION_KEY = "CacheVersion";
    const INVALIDATION_TIME = 5*60*60*1000; // 5 hour cache time
    const ITCH_GAME_CACHE_KEY = 'ItchGameCache';
    var ItchGameCache;
    
    // Promise wrapper for GM_xmlhttpRequest
    const Request = details => new Promise((resolve, reject) => {
        details.onerror = details.ontimeout = reject;
        details.onload = resolve;
        GM_xmlhttpRequest(details);
    });
    
    function versionCacheInvalidator() {
        const sVersion = v => {
            if (typeof v !== 'string' || !v.match(/\d+\.\d+/)) return 0;
            return parseFloat(v.match(/\d+\.\d+/)[0]);
        }
        const prev = sVersion(GM_getValue(CACHE_VERSION_KEY, '0.0'));
        if (prev < 0.1) {
            console.log(`${GM_info.script.version} > ${prev}`);
            console.log(`New minor version of ${GM_info.script.name} detected. Invalidating cache.`)
            _clearItchCache();
        }
        GM_setValue(CACHE_VERSION_KEY, GM_info.script.version);
    }
    
    function _clearItchCache() {
        ItchGameCache = {};
        _saveItchCache();
    }
    
    function loadItchCache() {
        ItchGameCache = JSON.parse(GM_getValue(ITCH_GAME_CACHE_KEY, '{}'));
    }
    
    function _saveItchCache() {
        if (ItchGameCache === undefined) return;
        GM_setValue(ITCH_GAME_CACHE_KEY, JSON.stringify(ItchGameCache));
    }
    
    function setItchGameCache(key, game) {
        loadItchCache(); // refresh our cache in case another tab has edited it
        ItchGameCache[key] = game;
        _saveItchCache();
    }
    
    function deleteItchGameCache(key) {
        if (key === undefined) return;
        loadItchCache();
        delete ItchGameCache[key];
        _saveItchCache();
    }
    
    function getItchGameCache(link) {
        if (!ItchGameCache) loadItchCache();
        if (Object.prototype.hasOwnProperty.call(ItchGameCache, link)) {
            return ItchGameCache[link];
        }
        return null;
    }
    
    async function claimGame(url) {
        const parser = new DOMParser();
        
        const purchase_url = url + "/purchase";
        console.log("Getting purchase page: " + purchase_url);
        const purchase_resp = await Request({method: "GET", url: purchase_url});
        const purchase_dom = parser.parseFromString(purchase_resp.responseText, 'text/html');
        const download_csrf_token = purchase_dom.querySelector("form.form").csrf_token.value;
        
        const download_url_resp = await Request({
            method: "POST",
            url: url + "/download_url",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            },
            data: 'csrf_token='+encodeURIComponent(download_csrf_token)
        });
        const downloadUrl = JSON.parse(download_url_resp.responseText).url;
        console.log("Received download url: " + downloadUrl);

        const download_resp = await Request({method: "GET", url: downloadUrl});
        const dom = parser.parseFromString(download_resp.responseText, 'text/html');
        const claimForm = dom.querySelector(".claim_to_download_box form");
        const claim_csrf_token = claimForm.csrf_token.value;
        const claim_key_url = claimForm.action;

        console.log("Claiming game using " + claim_key_url);
        const claim_key_resp = await Request({
            method: "POST",
            url: claim_key_url,
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            },
            data: 'csrf_token='+encodeURIComponent(claim_csrf_token)
        });
        return /You claimed this/.test(claim_key_resp.responseText);
    }
    
    // Parses a DOM into a game object
    function parsePage(url, dom) {
        // Gets the inner text of an element if it can be found otherwise returns undefined
        const txt = query => { const e = dom.querySelector(query); return e && e.innerText.trim(); };
      
        // JSON.parse(document.querySelectorAll(`script[type="application/ld+json"]`)[1].innerText)
        
        const game = {};
        
        game.cachetime = (new Date()).getTime();
        game.url = url;
        game.title = txt('h1.game_title');
        
        game.isOwned = dom.querySelector(".purchase_banner_inner .key_row .ownership_reason") !== null;        
        game.isClaimable = [...dom.querySelectorAll(".buy_btn")].find(e => e.innerText == "Download or claim") !== undefined;
        game.isFree = [...dom.querySelectorAll("span[itemprop=price]")].find(e => e.innerText === "$0.00 USD") !== undefined;
        game.hasPurchase = [...dom.querySelectorAll("span[itemprop=price]")].find(e => e.innerText !== "$0.00 USD") !== undefined;
        game.hasFreeDownload = [...dom.querySelectorAll("a.download_btn,a.buy_btn")].find(e => e.innerText == "Download" || e.innerText == "Download Now") !== undefined;
        game.hasCommunityCopies = document.querySelector(".reward_footer") !== null;
        const copiesBlock = document.querySelector(".remaining_count");
        game.communityCopies = copiesBlock && copiesBlock.innerText.match(/\d+/) && copiesBlock.innerText.match(/\d+/)[0];
        game.communityCopies = game.communityCopies || 0;
        game.original_price = txt("span.original_price");
        game.price = txt("span[itemprop=price]");
        game.saleRate = txt(".sale_rate");
        game.breadcrumbs = txt(".breadcrumbs");
      
        const categoryHeader = [...document.querySelectorAll(".game_info_panel_widget td:first-child")].find(e=>e.innerText === "Category");
        if (categoryHeader)
            game.category = categoryHeader.nextSibling.innerText;
        return game;
    }
    
    // Sends an XHR request and parses the results into a game object
    async function fetchItchGame(url) {
        const response = await Request({method: "GET",
                                 url: url});
        if (response.status != 200) {
            console.log(`Error ${response.status} fetching page ${url}`);
            return null;
        }
        const parser = new DOMParser();
        const dom = parser.parseFromString(response.responseText, 'text/html');
        return parsePage(url, dom);
    }
    
    // Loads an itch game from cache or fetches the page if needed
    async function getItchGame(url) {
        let game = getItchGameCache(url);
        if (game !== null) {
            const isExpired = (new Date()).getTime() - game.cachetime > INVALIDATION_TIME;
            // Expiration checking currently disabled
            /*if (isExpired) {
                game = null;
            }*/
        }
        if (game === null) {
            game = await fetchItchGame(url);
            if (game !== null)
                setItchGameCache(url, game);
        }
        return game;
    }
    
    async function claimClicked(a, game) {
        const iwic = a.closest(".iwi-container");
        const claimBtn = iwic.querySelector(".ClaimButton");
        console.log("Attempting to claim " + game.url);
        claimBtn.innerText += ' ⌛';
        claimBtn.onclick = null;
        const success = await claimGame(game.url);
        if (success === true) {
            claimBtn.style.display = "none";
            const ownMark = iwic.querySelector(".iwi-ownmark");
            ownMark.innerHTML = `<span title="Successfully claimed">✔️</span>`;
            deleteItchGameCache(game.url);
        } else {
            claimBtn.innerHTML = `❗ Error`;
        }
    }
    
    // Appends the isOwned tag to an anchor link
    function appendTags(a, game) {
        const div = document.createElement("div");
        div.className = "iwi-container";
        div.style.display = "inline-block";
        const span = document.createElement("span");
        div.append(span);
        span.style = "margin-left: 5px; background:rgb(230,230,230); padding: 2px; border-radius: 2px";
        
        if (game === null) {
            span.innerHTML = `<span title="Status unknown. Try refreshing.">❓</span>`;
            a.after(div);
            return;
        }
        
        if (game.isOwned) {
            span.innerHTML = `<span class="iwi-ownmark" title="Game is already claimed on itch.io">✔️</span>`;
        } else {
            if (!game.isClaimable) {
                if (game.hasFreeDownload && !game.hasPurchase) {
                    span.innerHTML = `<span title="Game is a free download but not claimable">🆓</span>`;
                } else if (game.price) {
                    span.innerHTML = `<span title="🛒 Game costs ${game.price}">🛒</span>`;
                } else {
                    span.innerHTML = `<span title="Status unknown">👽</span>`;
                }
            } else {
                let tooltip = [`Game is claimable but you haven't claimed it.`];
                if (game.original_price) tooltip.push(`🛒 Original price: ${game.original_price}`);
                if (game.price) tooltip.push(`💸 Current Price: ${game.price}`);
                span.innerHTML = `<span class="iwi-ownmark" title="${tooltip.join(" ")}">❌</span>`;
                
                const claimBtn = document.createElement("span");
                claimBtn.style = `margin-left: 2px; padding: 2px; cursor:pointer; background:rgb(220,220,220); border-radius: 5px`;
                claimBtn.className = "ClaimButton";
                claimBtn.innerText = "🛄 Claim Game";
                claimBtn.onclick = function(event) { claimClicked(event.target, game); };
                span.after(claimBtn);
            }
        }
        
        if (game.hasCommunityCopies) {
            const communityTag = document.createElement("span");
            communityTag.title = `This game has ${game.communityCopies} Community Copies availible.`;
            communityTag.innerText = '👪';
            span.append(communityTag);
        }
        if (game.breadcrumbs) {
            span.firstChild.title += ' ℹ️ ' + game.breadcrumbs;
            if (!a.title)
                a.title = game.breadcrumbs;
            const tags = {
                //"Games": { icon: '🎮', title: "Video game" },
                "Tools": { icon: '🛠️', title: "Tool" },
                "Game assets": { icon: '🗃️', title: "Game asset" },
                "Comics": { icon: '🗨️', title: "Comic" },
                "Books": { icon: '📘', title: "Book" },
                "Physical games": { icon: '📖', title: "Physical game" },
                "Soundtracks": { icon: '🎵', title: "Soundtrack" },
                "Game mods": { icon: '⚙️', title: "Game mod" },
            }
            const category = game.breadcrumbs.split("›")[0].trim();
            if (Object.prototype.hasOwnProperty.call(tags, category)) {
                const tag = document.createElement("span");
                tag.title = tags[category].title;
                tag.innerText = tags[category].icon;
                span.append(tag);
            }
        }
        
        a.after(div);
    }
    
    function addClickHandler(a) {
        // If you open a link to an Itch page, it will delete that page from the cache
        // this forces an update the next time you load the page
        a.addEventListener('mouseup', event => {
            deleteItchGameCache(event.target.href);
        });
    }

    // Handles an itch.io link on a page
    async function handleLink(a) {
        // Checks if the link has already been tagged
        if (!a.nextSibling || a.nextSibling.className !== "iwi-container") {
            addClickHandler(a);
            const game = await getItchGame(a.href);
            appendTags(a, game);
        }
    }
    
    function isGameUrl(url) {
        return /^https:\/\/[^.]+\.itch\.io\/[^/]+$/.test(url);
    }
    
    // Finds all the itch.io links on the current page
    function getItchLinks() {
        let links = [...document.querySelectorAll("a[href*='itch.io/']")];
        links = links.filter(a => isGameUrl(a.href));
        links = links.filter(a => !a.classList.contains("return_link"));
        links = links.filter(a => { const t = a.textContent.trim(); return t !== "" && t !== "GIF"; });
        return links;
    }
    
    function handlePage() {
        if (isGameUrl(window.location.href)) {
            // If we're on an Itch game page, update the cached details
            const game = parsePage(window.location.href, document);
            setItchGameCache(window.location.href, game);
        }
        // Try to find any itch links on the page and tag them
        const as = getItchLinks();
        as.forEach(handleLink);
        // Monitor new links loaded on the page
        setInterval(function(){
            const as = getItchLinks();
            as.forEach(handleLink);
        }, 1000);
    }
    
    versionCacheInvalidator();
    handlePage();
})();