Redirects instance of Redlib that having an error or has a Anubis/Cerberus/Cloudflare/GoAway check to another instance. The CSP for websites must be removed/modified using an addon for this script to work. To have a better effect, make sure to reorder this script so it runs as soon as possible.
// ==UserScript==
// @name [Redlib] Error & PoW Redirector
// @include /^https?:\/\/(?:lib|safe)?red(?:lib|dit)(-[0-9]+)?\./
// @include /^https?:\/\/[il]\.opnxng\.com/
// @include /^https?:\/\/(?:lr|oratrice)\.ptr\.moe/
// @match https://eddrit.com/*
// @match https://kddit.kalli.st/*
// @match https://lr.vern.cc/*
// @match https://r.darklab.sh/*
// @match https://red.artemislena.eu/*
// @match https://snoo.habedieeh.re/*
// @noframes
// @run-at document-start
// @inject-into page
// @grant GM_cookie.delete
// @grant GM_deleteValue
// @grant GM_deleteValues
// @grant GM_getValues
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_setValues
// @grant GM_unregisterMenuCommand
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect raw.githubusercontent.com
// @namespace Violentmonkey Scripts
// @author SedapnyaTidur
// @version 1.0.37
// @license MIT
// @revision 5/14/2026, 3:03:08 PM
// @description Redirects instance of Redlib that having an error or has a Anubis/Cerberus/Cloudflare/GoAway check to another instance. The CSP for websites must be removed/modified using an addon for this script to work. To have a better effect, make sure to reorder this script so it runs as soon as possible.
// ==/UserScript==
(async function() {
'use strict';
const window = unsafeWindow;
let currentURL = window.location.href, blocked = false, hasChecked = false, redirectIntervalId = 0, redirectTimeoutId = 0, redirectWaitId = 0, unload = false;
// To block Anubis/Cerberus/Cloudflare/GoAway.
const fetch_ = window.fetch;
window.fetch = function(resource, options) {
if (blocked) throw new Error();
try { // if resource is null, undefined or who-knows-what.
const url = (resource instanceof window.Request) ? resource.url : resource.toString();
if (/\/\.(?:within\.website|cerberus|well-known)/.test(url)) {
blocked = true;
throw new Error();
}
return fetch_.apply(this, arguments);
} catch(e) { throw e; }
};
const parse_ = window.JSON.parse;
window.JSON.parse = function(text, reviver) {
if (blocked) throw new Error();
if (/(?:^\{"(?:audioBoolean|challenge|rules|userAgent)":|\/\.(?:within\.website|cerberus|well-known)\/)/.test(text)) {
blocked = true;
throw new Error();
}
return parse_.apply(this, arguments);
};
const replaceState_ = window.History.prototype.replaceState;
window.History.prototype.replaceState = function(state, unused, url) {
if (blocked) throw new Error();
if (/(?:[?&]__(?:cf_chl|goaway)|\/\.(?:within\.website|cerberus|well-known)\/)/.test(url)) {
blocked = true;
throw new Error();
}
return replaceState_.apply(this, arguments);
};
const pushState_ = window.History.prototype.pushState;
window.History.prototype.pushState = function(state, unused, url) {
if (blocked) throw new Error();
if (/(?:[?&]__(?:cf_chl|goaway)|\/\.(?:within\.website|cerberus|well-known)\/)/.test(url)) {
blocked = true;
throw new Error();
}
return pushState_.apply(this, arguments);
};
// blacklist and failedHosts must be arrays even though they are empty.
let { blacklist, failedHosts, currentHost, delay, deleteCookies, externalOrigins, hideSettings, lastUpdate, localOriginsFailedDate, preferOrigins, preferRedlib, redirectHost, updateFrequency, workingSite } = GM_getValues({
blacklist: [], // Contains hostnames that will never be redirected to.
failedHosts: [], // Having an error or websites that always have a Anubis/Cerberus/GoAway check.
currentHost: undefined, // Current website's hostname.
delay: 'No delay', // Wait N seconds before redirect.
deleteCookies: true, // Delete sessionStorage, localStorage & cookies before redirect?.
externalOrigins: undefined, // An array of origins from external source.
hideSettings: true, // Hide settings in GM menu commands?
lastUpdate: undefined, // When the external origins was updated.
localOriginsFailedDate: undefined, // Date of when all local origins have failed.
preferOrigins: 'external > local', // local, external, local > external, external > local.
preferRedlib: true, // Do not redirect to one of non-Reblib instances after 30 minutes?
redirectHost: undefined, // Hostname that will be redirected to.
updateFrequency: '30 minutes', // Update external origins when at least this much time has passed.
workingSite: undefined, // Origin of working website.
});
const downloader = {
abort: false,
instance: undefined,
resolve: undefined,
timeoutId: 0
};
// Unload the page: navigate, reload, back_forward.
window.addEventListener('beforeunload', function(event) {
unload = true;
window.clearTimeout(redirectWaitId);
window.clearTimeout(downloader.timeoutId);
if (downloader.resolve) downloader.resolve();
if (downloader.instance) downloader.instance.abort();
}, true);
window.addEventListener('pagehide', function(event) {
if (!unload) return;
window.clearTimeout(redirectTimeoutId);
window.clearInterval(redirectIntervalId);
}, true);
const configs = [{
query: ':scope > :is(pre:first-child,h1:first-child)',
texts: ['', 'Moved Permanently', 'Service has been shutdown', 'maintenance', 'no available server', 'Error 503 Service Unavailable']
}, {
query: ':scope > :is(main,div:first-child,article:first-child,center:first-child) > h1:first-child',
texts: ["Making sure you're not a bot!", 'The Oratrice is rendering its judgment!', 'Oh noes!', 'agh, the service', "We’ll be back soon!", 'Performance Tracking', '504 Gateway Time-out', '503 Service Temporarily Unavailable', '502 Bad Gateway', 'ERROR']
}, {
query: ':scope > :first-child > :first-child > h1:first-child',
texts: ["Making sure you're not a bot!", 'Checking you are not a bot', 'An Error Occurred', 'Oh no! Internal Server Error']
}, {
query: ':scope > :first-child > :first-child > noscript',
texts: ['challenge-error-text']
}, {
query: ':scope > main > div#error:first-child > :first-child',
texts: ['Failed to parse page JSON data:', 'Reddit rate limit exceeded.', "Couldn't send request to Reddit:"] // 'Nothing here'
}, {
query: ':scope > #cf-wrapper:first-child > #cf-error-details > :first-child > :first-child',
texts: ['Sorry, you have been blocked']
}, {
query: ':scope > #cf-wrapper:first-child > #cf-error-details:first-child > :first-child > :first-child > :first-child',
// https://cloudflare-error-page-3th.pages.dev/
texts: ['SSL handshake failed'] // Cloudflare error: server side.
}];
// Must be an array. Unfortunately, they are not up-to-date.
const localOrigins = [
//'https://l.opnxng.com', // For info: https://about.opnxng.com/blog/#redlib
//'https://oratrice.ptr.moe', // GONE 12/25/2025
'https://red.artemislena.eu',
//'https://reddit.adminforge.de',
'https://reddit.utsav2.dev',
'https://redlib.4o1x5.dev',
'https://redlib.catsarch.com',
'https://redlib.ducks.party', // GONE, WORKS FINE ON 12/25/2025
//'https://redlib.frontendfriendly.xyz',
'https://redlib.nadeko.net',
//'https://redlib.orangenet.cc',
'https://redlib.perennialte.ch',
'https://redlib.privacyredirect.com',
'https://redlib.privadency.com',
//'https://redlib.private.coffee',
'https://redlib.pussthecat.org',
'https://redlib.reallyaweso.me',
//'https://redlib.thebunny.zone', // NOT RESPONDING
'https://redlib.tiekoetter.com',
'https://snoo.habedieeh.re',
//'https://safereddit.com', // REDIRECTION LOOP TEST.
// THESE TWO ARE NOT A REDLIB BUT WHATEVER STILL ALTERNATIVES.
'https://eddrit.com',
'https://kddit.kalli.st', // Can't search. No search button.
];
// Day/Month/Year, Hours:Minutes:Seconds AM/PM
// 7/12/2025, 4:43:36 PM
const getDate = function() {
const date = new Date();
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}, ${date.getHours() % 12 || 12}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice(-2)} ${date.getHours() > 11 ? 'PM' : 'AM'}`;
};
const hasElapsed = function(updateFrequency, lastCheckedDate) {
if (!updateFrequency || !lastCheckedDate) return false;
const elapsed = updateFrequency.toLowerCase().replace(/[ s]/g, '').replace(/(mi|h|d|mo|y).*$/, field => {
return { minute:' 1',hour:' 60',day:' 1440',month:' 43829',year:' 525949' }[field]
}).split(' ').reduce((sum, value) => sum * Number(value), 1);
const values = lastCheckedDate.replace(/:[0-9]+\s+[APap][Mm]$/, '').split(/(?:\/|,\s+|:)/).map(Number);
const hour24 = (/[Pp][Mm]$/.test(lastCheckedDate) && values[3] !== 12) ? 12 : (/[Aa][Mm]$/.test(lastCheckedDate) && values[3] === 12) ? -12 : 0;
const past = (values[0] * 1440) + (values[1] * 43829) + (values[2] * 525949) + ((values[3] + hour24) * 60) + values[4];
const date = new Date();
const now = (date.getFullYear() * 525949) + ((date.getMonth() + 1) * 43829) + (date.getDate() * 1440) + (date.getHours() * 60) + date.getMinutes();
return (now - past >= elapsed);
};
const getRemainTime = function(updateFrequency, lastCheckedDate) {
if (!lastCheckedDate) return 'tomorrow or at later time.';
const elapsed = updateFrequency.toLowerCase().replace(/[ s]/g, '').replace(/(mi|h|d|mo|y).*$/, field => {
return { minute:' 1',hour:' 60',day:' 1440',month:' 43829',year:' 525949' }[field]
}).split(' ').reduce((sum, value) => sum * Number(value), 1);
const values = lastCheckedDate.replace(/:[0-9]+\s+[APap][Mm]$/, '').split(/(?:\/|,\s+|:)/).map(Number);
const hour24 = (/[Pp][Mm]$/.test(lastCheckedDate) && values[3] !== 12) ? 12 : (/[Aa][Mm]$/.test(lastCheckedDate) && values[3] === 12) ? -12 : 0;
const future = elapsed + ((values[0] * 1440) + (values[1] * 43829) + (values[2] * 525949) + ((values[3] + hour24) * 60) + values[4]);
const date = new Date();
const now = (date.getFullYear() * 525949) + ((date.getMonth() + 1) * 43829) + (date.getDate() * 1440) + (date.getHours() * 60) + date.getMinutes();
let remain = future - now;
if (remain <= 0) return 'by reloading the page.';
let result = 'after ';
if (remain >= 525949) { result += `${Math.floor(remain / 525949)} year(s), `; remain %= 525949; }
if (remain >= 43829) { result += `${Math.floor(remain / 43829)} month(s), `; remain %= 43829; }
if (remain >= 1440) { result += `${Math.floor(remain / 1440)} day(s), `; remain %= 1440; }
if (remain >= 60) { result += `${Math.floor(remain / 60)} hour(s), `; remain %= 60; }
if (remain > 0) result += `${remain} minute(s)`;
return result.replace(/, $/, '') + '.';
};
const shouldUpdate = function(checkFailedOrigins) {
if (!preferOrigins || !updateFrequency || preferOrigins === 'local') return false;
if (checkFailedOrigins && preferOrigins === 'local > external') { // Have we tried all origins in localOrigins?
for (const origin of localOrigins) {
const host = origin.replace(/^https?:\/\/([^/]+).*$/, '$1');
if (!failedHosts.includes(host)) return false;
}
}
if (checkFailedOrigins && (!externalOrigins || !externalOrigins.length)) return true;
if (checkFailedOrigins && preferOrigins === 'external > local') {
for (const origin of externalOrigins) {
const host = origin.replace(/^https?:\/\/([^/]+).*$/, '$1');
if (!failedHosts.includes(host)) return false;
}
}
if (!lastUpdate) return true;
return hasElapsed(updateFrequency, lastUpdate);
};
// Download a json file.
const download = async function(url, retries = 1, timeout = 5000, waitInterval = 1000) {
if (!window.navigator.onLine) return null;
const sleep = function(duration) {
return new Promise(resolve => {
downloader.resolve = resolve;
downloader.timeoutId = window.setTimeout(() => {
downloader.timeoutId = 0;
downloader.resolve = undefined;
resolve();
}, duration);
});
};
const config = {
anonymous: true, // Do not send cookies in request header. Privacy.
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Cache-Control': 'max-age=0, no-cache, no-store, must-revalidate, proxy-revalidate',
},
method: 'GET',
responseType: 'json',
timeout: timeout,
url: url,
// Unfortunately, response argument does not have abort(), so it can't be canceled in HEADERS_RECEIVED without a GM_xmlhttpRequest instance.
// Secondly, each response is a new object that does not shared across listeners. Adding new properties is useless.
onabort: function(response) {
downloader.abort = true;
downloader.instance = undefined;
this.resolve(null);
},
onload: function(response) {
downloader.instance = undefined;
this.resolve(response.response); // Can be null;
},
init: function() {
this.onerror = this.ontimeout = function(response) {
downloader.instance = undefined;
this.resolve(undefined);
};
this.onabort = this.onabort.bind(this);
this.onerror = this.onerror.bind(this);
this.onload = this.onload.bind(this);
this.ontimeout = this.ontimeout.bind(this);
return this;
},
}.init();
for (let i = 0; i <= retries && !downloader.abort; ++i) {
if (i > 0) await sleep(waitInterval);
const promise = new Promise(resolve => config.resolve = resolve);
downloader.instance = GM_xmlhttpRequest(config);
const response = await promise;
if (response) return response;
}
return null;
};
// Specifically for Reblib's json structure.
const getReblib = async function() {
const url = 'https://raw.githubusercontent.com/redlib-org/redlib-instances/refs/heads/main/instances.json';
//const url = 'https://raw.githubusercontent.com/redlib-org/redlib-instances/main/instances.json';
const setExternalOrigins = function(object) {
if (!object || !object.instances) return false;
externalOrigins = []; // Reset the array. Delete all the old origins.
for (const instance of object.instances) {
if (!instance.url) continue;
externalOrigins.push(instance.url);
}
if (externalOrigins.length) {
GM_setValues({ externalOrigins: externalOrigins, lastUpdate: getDate() });
return true;
} else {
GM_deleteValues(['externalOrigins', 'lastUpdate']);
externalOrigins = lastUpdate = undefined;
return false;
}
};
// Cancel pending download.
if (downloader.instance) {
window.clearTimeout(downloader.timeoutId);
if (downloader.resolve) downloader.resolve();
downloader.instance.abort();
downloader.instance = undefined;
downloader.timeoutId = 0;
}
downloader.abort = false;
return setExternalOrigins(await download(url));
};
const expireCookies = async function() {
if (!deleteCookies) return;
window.sessionStorage.clear();
window.localStorage.clear();
const host = window.location.hostname;
const domain = host.replace(/^(?:[^.]+\.)*([^.]+\.[^.]+)$/, '$1');
const skip = (domain === host);
document.cookie = 'techaro.lol-anubis-auth=;Path=/;Expires=Mon, 01 Jan 1970 08:00:00 GMT;Secure;HostOnly';
document.cookie = 'techaro.lol-anubis-cookie-verification=;Path=/;Expires=Mon, 01 Jan 1970 08:00:00 GMT;Secure;HostOnly';
if (document.cookie) {
// Can't expire/delete HttpOnly cookies this way.
document.cookie.split('; ').forEach(cookie => {
document.cookie = `${cookie};Path=/;Expires=Thu, 01 Jan 1970 00:00:01 GMT;Secure;HostOnly;`;
document.cookie = `${cookie};Domain=${domain};Path=/;Expires=Thu, 01 Jan 1970 00:00:01 GMT;Secure;HostOnly;`;
document.cookie = `${cookie};Domain=.${domain};Path=/;Expires=Thu, 01 Jan 1970 00:00:01 GMT;Secure;`;
if (skip) return;
document.cookie = `${cookie};Domain=${host};Path=/;Expires=Thu, 01 Jan 1970 00:00:01 GMT;Secure;HostOnly;`;
document.cookie = `${cookie};Domain=.${host};Path=/;Expires=Thu, 01 Jan 1970 00:00:01 GMT;Secure;`;
});
}
// For Tampermonkey. Not tested. Does it delete cookies for subdomains? Or how to delete them?
// If it doesn't work, probably because it doesn't recognise "@grant GM_cookie.delete" but "@grant GM_cookie" only.
// If so, then it is a security bug. Don't expect people to review the whole code thoroughly - A through Z.
// Set <Config Mode> to <Advanced> and in <Security> category, change <Allow scripts to access cookies> to <All> to work.
if (typeof(GM_cookie) !== 'undefined' && GM_cookie.delete) {
GM_cookie.delete({ url: window.location.origin + '/' });
if (!skip) GM_cookie.delete({ url: window.location.protocol + '//' + domain + '/' });
}
};
const getNewUrl = async function() {
const location = new URL(currentURL);
const hostname = location.hostname;
// Save the failed hostname first before trying to redirect.
if (!failedHosts.includes(hostname)) {
failedHosts.push(hostname);
GM_setValue('failedHosts', failedHosts);
}
// This is a must. The "redir=" in query can cause an infinite redirectiom loop.
// https://oratrice.ptr.moe/.within.website/?redir=https%3A%2F%2Flr.ptr.moe%2Fr%2Fworldnews%2Fnew%3F
// https://snoo.habedieeh.re/.within.website/x/cmd/anubis/api/pass-challenge?response=00dfda4398a2cf0fe692db546fff4ff3922b44ac545845f9dd11938d82f0a38c&nonce=21&redir=https%3A%2F%2Fsnoo.habedieeh.re%2F&elapsedTime=193
// https://l.opnxng.com/r/worldnews/new?__cf_chl_rt_tk=eDk9eqEOCfOkswcBNxcKwYOXNe69zQ7463JZvkyL_sw-1765384843-1.0.1.1-Q48lQqRml97LuskqNrjHx6yuZGMrM.GaKDWOBDtsC20
// https://redlib.nadeko.net/r/worldnews/new?__goaway_challenge=meta-refresh&__goaway_id=0cfc79fd2542edd56dc276cf1d0f65c1&__goaway_referer=https%3A%2F%2Fredlib.frontendfriendly.xyz%2F
if (/^\/\.(?:within\.website|cerberus|well-known)\//.test(location.pathname)) { // Anubis/Cerberus/GoAway
currentURL = location.origin + '/';
location.search.split('&').some(query => {
const uri = window.decodeURIComponent(query);
if (/^\??redir=(?:https?:\/\/[^/]+)?\/./.test(uri)) {
currentURL += uri.match(/^\??redir=(?:https?:\/\/[^/]+)?\/(.+)/)[1];
return true;
}
});
} else if (/[?&]__(?:cf_chl|goaway)/.test(location.search)) { // Cloudflare/GoAway
currentURL = location.origin + location.pathname;
}
// Redirect to the last working website.
if (workingSite && location.origin !== workingSite) {
GM_setValues({
currentHost: hostname,
redirectHost: workingSite.replace(/^https?:\/\/([^/]+).*$/, '$1')
});
return currentURL.replace(/^https?:\/\/[^/]+/, workingSite);
}
if (workingSite) GM_deleteValue('workingSite');
// local, external, local > external, external > local.
const listsOfOrigins = preferOrigins.split(' > ').map(prefer => { return (prefer === 'local') ? localOrigins : externalOrigins });
let uptodate = null;
for (let i = 0; i < listsOfOrigins.length; ++i) {
if (!listsOfOrigins[i]) { // First time or force update.
if (shouldUpdate(false)) uptodate = await getReblib();
if (!externalOrigins) continue;
listsOfOrigins[i] = externalOrigins;
// Give the failed external origins another try after an update?
const len = failedHosts.length;
for (const origin of externalOrigins) {
const index = failedHosts.indexOf(origin.replace(/^https?:\/\/([^/]+).*$/, '$1'));
if (index >= 0) failedHosts.splice(index, 1);
}
for (const host of blacklist) {
if (!failedHosts.includes(host)) failedHosts.push(host);
}
if (len !== failedHosts.length) GM_setValue('failedHosts', failedHosts);
}
for (const origin of listsOfOrigins[i]) {
const host = origin.replace(/^https?:\/\/([^/]+).*$/, '$1');
if (hostname !== host && !failedHosts.includes(host)) {
GM_setValues({ currentHost: hostname, redirectHost: host });
return currentURL.replace(/^https?:\/\/[^/]+/, origin);
}
}
if (uptodate) continue; // If true, externalOrigins is up-to-date.
if (listsOfOrigins[i] === externalOrigins) { // Force update.
listsOfOrigins[i] = externalOrigins = undefined;
--i;
} else if (localOriginsFailedDate && hasElapsed('30 minutes', localOriginsFailedDate)) {
localOriginsFailedDate = undefined;
GM_deleteValue('localOriginsFailedDate');
const len = failedHosts.length;
for (const origin of localOrigins) {
const index = failedHosts.indexOf(origin.replace(/^https?:\/\/([^/]+).*$/, '$1'));
if (index >= 0) failedHosts.splice(index, 1);
}
for (const host of blacklist) {
if (!failedHosts.includes(host)) failedHosts.push(host);
}
if (len !== failedHosts.length) GM_setValue('failedHosts', failedHosts);
--i;
} else if (!localOriginsFailedDate) {
localOriginsFailedDate = getDate();
GM_setValue('localOriginsFailedDate', localOriginsFailedDate);
}
}
// JSON structure changed, file not found (URL changed?) or download error (no/slow internet?).
if (uptodate === false) return undefined;
return null;
};
const check = async function() {
if (hasChecked) return;
hasChecked = true;
for (const config of configs) {
const target = document.body.querySelector(config.query);
if (!target) continue;
for (const text of config.texts) {
if (!target.innerText.includes(text)) continue;
window.stop();
try { // Wakelock the screen. Don't let screen goes off. Can fail when being low on battery.
await window.navigator.wakeLock.request('screen');
} catch(e) {}
const url = await getNewUrl();
const container = document.createElement('redirector-dialog');
const wrapper = document.createElement('div');
const h1_1 = document.createElement('h1');
const h1_2 = document.createElement('h1');
const h1_3 = document.createElement('h1');
const h1_4 = document.createElement('h1');
const h1_5 = document.createElement('h1');
const style = document.createElement('style');
style.textContent = `redirector-dialog#redirector {
align-items: center;
background: rgba(0,0,0,0);
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
left: 0px;
pointer-events: none;
position: fixed;
top: 0px;
width: 100%;
z-index: 2147483647;
} redirector-dialog#redirector > div {
align-items: center;
background: rgb(30,30,30);
border: 3px solid rgb(80,80,80);
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: auto;
padding: 20px;
} redirector-dialog#redirector > div > * {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
font-family: sans-serif;
font-size: 32px;
font-weight: 700px;
margin: 0px;
white-space: wrap;
} redirector-dialog#redirector > div > :first-child {
color: rgb(255,99,71);
} redirector-dialog#redirector > div > :nth-child(2) {
color: rgb(172,157,83);
} redirector-dialog#redirector > div > :nth-child(3) {
color: rgb(65,105,225);
} redirector-dialog#redirector > div > :nth-child(4) {
color: rgb(210,210,210);
text-shadow: -2px -2px 0 #000000, 2px -2px 0 #000000, -2px 2px 0 #000000, 2px 2px 0 #000000;
} redirector-dialog#redirector > div > :nth-child(5) {
color: rgb(213,68,85);
}`;
document.head.appendChild(style);
container.id = 'redirector';
wrapper.appendChild(h1_1);
wrapper.appendChild(h1_2);
wrapper.appendChild(h1_3);
wrapper.appendChild(h1_4);
wrapper.appendChild(h1_5);
container.appendChild(wrapper);
document.body.appendChild(container);
if (url === undefined) { // Download error for whatever reason.
h1_2.innerText = "⚠️ There was a problem getting alternative URLs of Reblib.\nCheck your network connection & VPN or proxy if you're using one and try reloading the page.\nIf you see this message again, that means this script is broken and should be uninstalled or disabled until it gets an update.";
}
if (!url) {
h1_5.innerText = '💢 Failed to redirect!. All instances are broken.';
h1_5.innerText += '\nTry again ' + ((preferOrigins === 'local') ? getRemainTime('30 minutes', localOriginsFailedDate) : getRemainTime(updateFrequency, lastUpdate));
return;
}
h1_3.innerText = 'Redirecting to another instance...';
h1_4.innerText = `${url}`;
expireCookies();
const retries = 1; // Not responding.
const waitRedirect = Number(('0' + delay).replace(/^([0-9]+).*$/, '$1' + '000'));
redirectTimeoutId = window.setTimeout(() => { // Only once.
redirectTimeoutId = 0;
let retry = retries;
redirectIntervalId = window.setInterval(past => { // For websites that are not responding.
if (retry > 0) {
h1_1.innerText = `⚠️ To-be-redirected server did not respond after ${Math.floor((Date.now() - past) / 1000)} seconds.`;
h1_1.innerText += `\n[${retry--}/${retries}] Retrying...`;
window.location.href = 'about:blank';
window.location.replace(url);
} else {
window.clearInterval(redirectIntervalId);
redirectIntervalId = 0; // For unload.
h1_1.innerText = '⚠️ To-be-redirected server did not respond. Reloading the page...';
const host = url.replace(/^https?:\/\/([^/]+).*$/, '$1');
if (!failedHosts.includes(host)) {
failedHosts.push(host);
GM_setValue('failedHosts', failedHosts);
}
GM_deleteValues(['currentHost', 'redirectHost', 'workingSite']);
window.location.reload(); // Re-run the script.
}
}, 10000, Date.now()); // 10 seconds. Is it too long?
}, waitRedirect);
if (waitRedirect > 0) {
redirectWaitId = window.setTimeout(() => {
redirectWaitId = 0;
window.location.replace(url);
}, waitRedirect);
} else {
window.location.replace(url);
}
return; // Breaks the loops & exits the function.
}
}
// No redirection.
window.fetch = fetch_;
window.JSON.parse = parse_;
window.History.prototype.replaceState = replaceState_;
window.History.prototype.pushState = pushState_;
// Give Redlib instances higher priority?
if (preferRedlib && localOrigins.indexOf(window.location.origin, -2) !== -1) return;
if (blacklist.includes(window.location.hostname)) return;
// Remember the current working website.
workingSite = window.location.origin;
GM_setValue('workingSite', workingSite);
// Remove the hostname from failedHosts.
const index = failedHosts.indexOf(window.location.hostname);
if (index >= 0) {
failedHosts.splice(index, 1);
GM_setValue('failedHosts', failedHosts);
}
};
const checkRedirectionLoop = function() {
if (!currentHost || window.performance.getEntriesByType('navigation')[0].type !== 'navigate') return;
if (window.location.hostname === currentHost && !failedHosts.includes(redirectHost)) {
failedHosts.push(redirectHost);
GM_setValue('failedHosts', failedHosts);
}
GM_deleteValues(['currentHost', 'redirectHost']);
currentHost = redirectHost = undefined;
};
// === Execute this statement as soon as possible. ===
if (document.readyState === 'loading') {
checkRedirectionLoop();
document.addEventListener('DOMContentLoaded', check, true);
// Nah. I've decided not to download anything unnecessarily and should run check() as soon as possible.
//if (shouldUpdate(true)) getReblib(); // Non-blocking.
if (document.readyState !== 'loading' && !hasChecked) check(); // In case we missed the DOMContentLoaded call during checkRedirectionLoop().
} else {
checkRedirectionLoop();
check();
}
// Script's menu commands here.
const menu = [{
title: 'Alternative URLs: 《{}》',
choices: [ 'Locally', 'Externally', 'Prefer Locally, Fallback to Externally', 'Prefer Externally, Fallback to Locally' ],
options: { id: '0', autoClose: false, title: 'Using alternative URLs in this script or get up-to-date alternative URLs from Redlib?' },
init: function() {
this.choices_ = this.choices.map(choice => choice.toLowerCase().split(' ').filter(field => /(?:local|external)/.test(field)).map(choice => choice.replace(/(local|external).*/, '$1')).join(' > '));
this.index = this.choices_.indexOf(preferOrigins);
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (!hideSettings) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[0];
object.index = ++object.index & 3;
object.title_ = object.title.replace('{}', object.choices[object.index]);
preferOrigins = object.choices_[object.index];
GM_setValue('preferOrigins', preferOrigins);
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
if (preferOrigins === 'local' && menu[i].title.startsWith('Update Frequency')) {
menu[i].hide = true;
continue;
}
menu[i].hide = false;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: 'Update Frequency: 《{}》',
// Adding or removing intervals is possible. "30 minutes" must be in choices.
// Only minute(s)/hour(s)/day(s)/month(s) and year(s) are supported. Anything else will crash the script.
// Big or small values are fine e.g. 5 minutes, 9999 Minutes, 99999MINUTES, 999 hour or 999Day. (Yes, grammar guru).
choices: [ '30 minutes', '1 hour', '6 hours', '12 hours', '1 day', '1 month', '1 year' ],
options: { id: '1', autoClose: false, title: 'How often to get new alternative URLs from Redlib after all the old URLs have failed?' },
init: function() {
this.index = this.choices.map(time => time.toLowerCase().replace(/[ s]/g, '')).indexOf(updateFrequency.toLowerCase().replace(/[ s]/g, ''));
if (this.index < 0) { this.hide = true; return this; }
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (preferOrigins === 'local') this.hide = true;
if (!hideSettings && !this.hide) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[1];
object.index = ++object.index % object.choices.length;
object.title_ = object.title.replace('{}', object.choices[object.index]);
updateFrequency = object.choices[object.index];
GM_setValue('updateFrequency', updateFrequency);
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: 'Wait Before Redirect: 《{}》',
choices: [ 'No delay', '2 seconds', '4 seconds', '6 seconds', '8 seconds', '10 seconds' ],
options: { id: '2', autoClose: false, title: 'Wait N seconds before redirect.' },
init: function() {
this.index = this.choices.indexOf(delay);
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (!hideSettings) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[2];
object.index = ++object.index % object.choices.length;
object.title_ = object.title.replace('{}', object.choices[object.index]);
delay = object.choices[object.index];
GM_setValue('delay', delay);
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
if (menu[i].hide) continue;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: 'Delete SessionStorage, LocalStorage & Cookies: 《{}》',
choices: [ 'Yes', 'No' ],
options: { id: '3', autoClose: false, title: 'Delete sessionStorage, localStorage & cookies before redirecting?' },
init: function() {
this.index = Number(!deleteCookies);
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (!hideSettings) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[3];
object.index = ++object.index & 1;
object.title_ = object.title.replace('{}', object.choices[object.index]);
deleteCookies = !object.index;
GM_setValue('deleteCookies', deleteCookies);
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
if (menu[i].hide) continue;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: 'Redirect to This Website: 《{}》',
choices: [ 'Yes', 'No' ],
options: { id: '4', autoClose: false, title: "Yes: Redirect to this website. No: Never ever redirect to this website." },
init: function() {
this.index = Number(blacklist.includes(window.location.hostname));
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (!hideSettings) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[4];
object.index = ++object.index & 1;
object.title_ = object.title.replace('{}', object.choices[object.index]);
const host = window.location.hostname;
let index = blacklist.indexOf(host);
if (!object.index) { // Yes.
if (index >= 0) {
blacklist.splice(index, 1);
GM_setValue('blacklist', blacklist);
index = failedHosts.indexOf(host);
if (index >= 0) {
failedHosts.splice(index, 1);
GM_setValue('failedHosts', failedHosts);
}
}
} else { // No.
if (index < 0) {
blacklist.push(host);
GM_setValue('blacklist', blacklist);
if (!failedHosts.includes(host)) {
failedHosts.push(host);
GM_setValue('failedHosts', failedHosts);
}
}
if (workingSite === window.location.origin) {
workingSite = undefined;
GM_deleteValue('workingSite');
}
}
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
if (menu[i].hide) continue;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: 'Prefer Redlib Over Non-Redlib: 《{}》',
choices: [ 'Yes', 'No' ],
options: { id: '5', autoClose: false, title: 'Yes: Try redirecting to Redlib first. No: Instantly redirect to non-Redlib.' },
init: function() {
this.index = Number(!preferRedlib);
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (!hideSettings) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[5];
object.index = ++object.index & 1;
object.title_ = object.title.replace('{}', object.choices[object.index]);
preferRedlib = !object.index;
GM_setValue('preferRedlib', preferRedlib);
if (preferRedlib && localOrigins.indexOf(workingSite, -2) !== -1) {
workingSite = undefined;
GM_deleteValue('workingSite');
}
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
if (menu[i].hide) continue;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: 'Factory Reset: 《{}》',
choices: [ '💣💣💣', '💣💣', '💣', '💥💥💥' ],
options: { id: '6', autoClose: false, title: 'Reset everything as if the script was a fresh install?' },
init: function() {
this.index = 0;
this.title_ = this.title.replace('{}', this.choices[this.index]);
if (!hideSettings) this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function(event) {
const object = menu[6];
object.index = ++object.index % object.choices.length;
object.title_ = object.title.replace('{}', object.choices[object.index]);
if (!object.index) {
object.options.autoClose = false;
GM_deleteValues(['blacklist','currentHost','delay','deleteCookies','externalOrigins','failedHosts','hideSettings','lastUpdate','localOriginsFailedDate','preferOrigins','preferRedlib','redirectHost','updateFrequency','workingSite']);
blacklist = [];
delay = 'No delay';
deleteCookies = true;
failedHosts = [];
hideSettings = true;
preferOrigins = 'external > local';
preferRedlib = true;
updateFrequency = '30 minutes';
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) menu[i].init();
return;
}
if (object.index === object.choices.length - 1) object.options.autoClose = true;
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = 0; i < menu.length; ++i) {
if (menu[i].hide) continue;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init(), {
title: '{} Settings',
choices: [ 'Show', 'Hide' ],
options: { id: '7', autoClose: false, title: "Show or hide this script's settings." },
init: function() {
this.index = Number(!hideSettings);
this.title_ = this.title.replace('{}', this.choices[this.index]);
this.id = GM_registerMenuCommand(this.title_, this.click, this.options);
return this;
},
click: function() {
const object = menu[7];
object.index = ++object.index & 1;
object.title_ = object.title.replace('{}', object.choices[object.index]);
hideSettings = !object.index;
GM_setValue('hideSettings', hideSettings);
menu.forEach(object => GM_unregisterMenuCommand(object.id));
for (let i = (hideSettings) ? menu.length - 1 : 0; i < menu.length; ++i) {
if (menu[i].hide) continue;
menu[i].id = GM_registerMenuCommand(menu[i].title_, menu[i].click, menu[i].options);
}
},
}.init()];
})();