Shows if an Itch.io link has been claimed or not
// ==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 r###lts 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(); })();