// ==UserScript== // @name Shortside // @version 1.2.1 // @description Redirects the configured websites to third-party privacy-respecting front ends. // @author examosa // @license AGPLv3-or-later // @updateUrl https://gist.github.com/examosa/50eb28bc16a006b62b3f43893ab457e7/raw/shortside.user.js // @downloadUrl https://gist.github.com/examosa/50eb28bc16a006b62b3f43893ab457e7/raw/shortside.user.js // @match https://*.fandom.com/* // @match https://*.medium.com/* // @match https://stackoverflow.com/* // @match https://www.reddit.com/* // @match https://www.youtube.com/* // @match https://www.quora.com/* // @match https://www.geeksforgeeks.org/* // @run-at document_start // @allFrames false // @noframes // ==/UserScript== const cdn = (path) => new URL(path, 'https://nobsdelivr.private.coffee/'); const withCatch = (action) => (...args) => Promise.try(action, ...args).catch((error) => { console.error('Caught error:', error); }); const getJson = withCatch((url) => fetch(url, { headers: { Accept: 'application/json' } }).then((response) => response.json(), ), ); function createCache() { const cacheKey = 'SHORTSIDE_INSTANCES'; const drivers = { vanillaPudding: { async isSupported() { const getMessage = withCatch( () => import(cdn('npm/@vanilla-pudding/message@1.4.0/+esm')), ); const message = await getMessage(); const ext = message?.useExt(); if (!ext?.getVersion()) { return false; } const store = ext.bgt.extLocalStore; Object.assign(this, { isSupported: () => true, get: () => store.getByStrict(cacheKey), set: (value) => store.set(cacheKey, value, 10), }); return true; }, }, greaseMonkey: { isSupported: () => typeof GM === 'object' && typeof GM.getValue === 'function' && typeof GM.setValue === 'function', get: () => GM.getValue(cacheKey).then((value) => JSON.parse(value)), set: (value) => GM.setValue(cacheKey, JSON.stringify(value)), }, localStorage: { isSupported: () => true, get() { const item = localStorage.getItem(cacheKey); return JSON.parse(item); }, async set(value) { localStorage.setItem(cacheKey, JSON.stringify(value)); }, }, backup: { async isSupported() { try { await fetch('https://httpbin.private.coffee/status/204'); const backups = [ 'https://jsonblob.com/api/jsonBlob/1392945386074857472', 'https://api.npoint.io/6ee2cb1b5a1aa6f10be5', 'https://api.pastes.dev/VSjbFP63yq', 'https://api.codetabs.com/v1/proxy/?quest=https%3A%2F%2Fstarb.in%2Fraw%2FMgDjym', 'https://bytebin.lucko.me/bDAxqrUNqD', ]; Object.assign(this, { isSupported: () => true, async get() { const result = await Promise.any( backups.map((backup) => getJson(backup)), ); this.get = () => result; return result; }, }); return true; } catch { return false; } }, set(value) { const original = this.isSupported; this.isSupported = function () { this.isSupported = original; return false; }; return cache.set(value); }, }, }; async function eachDriver(call) { const safeCall = withCatch(call); for (const driver of Object.values(drivers)) { if (!(await driver.isSupported())) continue; const result = await safeCall(driver); if (result) return result; } } const cache = { get: () => eachDriver((driver) => driver.get()), set: (value) => eachDriver((driver) => driver.set(value).then(() => true)), }; return cache; } async function fetchInstances() { const cache = createCache(); const cached = await cache.get(); if (cached) { return cached; } const instances = Object.assign(Object.create(null), { ducksforducks: ['https://ducksforducks.private.coffee'], nerdsfornerds: ['https://nn.vern.cc'], }); const providers = { libredirect: { source: 'libredirect/instances/data.json', normalize: (result) => Object.entries(result).map(([name, instances]) => ({ type: name.toLowerCase(), instances: instances.clearnet, })), }, farside: { source: 'benbusby/farside/services.json', normalize: (result) => result.map((service) => ({ type: service.type, instances: service.instances.concat(service.fallback), })), }, fastside: { source: 'cofob/fastside/services.json', normalize: (result) => result.services.map((service) => ({ type: service.type, instances: service.instances .filter((instance) => instance.tags.includes('clearnet')) .map((instance) => instance.url), })), }, }; try { const promises = Object.values(providers).map(async (provider) => { const result = await getJson(cdn(`gh/${provider.source}`)); if (!result) { return []; } return provider.normalize(result); }); const sources = await Promise.all(promises).flat(); for (const source of sources) { const list = (instances[source.type] ??= []); for (const url of source.instances) { if (!list.includes(url)) { list.push(url); } } } await cache.set(instances); } catch {} return instances; } const replacements = Object.entries({ fandom: ['breezewiki'], geeksforgeeks: ['ducksforducks', 'nerdsfornerds'], medium: ['libmedium', 'scribe'], quora: ['quetre'], reddit: ['eddrit', 'photon-reddit', 'redlib'], stackoverflow: ['anonymousoverflow'], }); async function run() { const instances = await fetchInstances(); const replacement = replacements.find(([host]) => location.hostname.includes(host), ); if (replacement) { const chooseRandom = (list) => list.at(Math.floor(Math.random() * list.length)); const [, options] = replacement; const type = chooseRandom(options); const instancesForType = instances[type]; if (instancesForType?.length > 0) { const redirect = new URL( location.pathname, chooseRandom(instancesForType), ); return location.replace(redirect); } } location.replace(`https://farside.link/${location.href}`); } run();