[Redlib] Error & PoW Redirector

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.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         [Redlib] Error & PoW Redirector
// @include      /^https?:\/\/(?:lib|safe)?red(?:lib|dit)\./
// @include      /^https?:\/\/[il]\.opnxng\.com/
// @include      /^https?:\/\/(?:lr|oratrice)\.ptr\.moe/
// @match        https://lr.vern.cc/*
// @match        https://r.darklab.sh/*
// @match        https://red.artemislena.eu/*
// @match        https://rl.bloat.cat/*
// @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.9
// @license      MIT
// @revision     12/27/2025, 5:41:00 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==

(function() {
  'use strict';

  const window = unsafeWindow;
  let currentURL = window.location.href, blocked = false, hasChecked = false, redirectIntervalId = 0;

  // To block redirections to Anubis/Cerberus/Cloudflare/GoAway.
  const fetch = window.fetch;
  window.fetch = function(resource, options) {
    if (blocked) throw new Error();
    const url = (resource instanceof window.Request) ? resource.url : resource.toString();
    if (/\/\.(?:within\.website|cerberus|well-known)/.test(url)) {
      blocked = true;
      throw new Error();
    }
    fetch.apply(this, arguments);
  };
  const parse = window.JSON.parse;
  window.JSON.parse = function(text, reviver) {
    if (blocked) throw new Error();
    if (/(?:^\{"(?:userAgent|audioBoolean|challenge)":|\/\.(?: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);
  };
  // failedHosts must be an array even though it is empty.
  let { failedHosts, currentHost, deleteCookies, externalOrigins, hideSettings, lastUpdate, preferOrigins, redirectHost, updateFrequency, workingSite } = GM_getValues({
    // Websites that always have a Anubis/Cerberus/GoAway check or redirect to Anubis/Cerberus/GoAway.
    failedHosts: [
      'redlib.thebunny.zone', // NOT RESPONDING IS SO PROBLEMATIC. NOT WORTH THE RISK.
      'lr.ptr.moe',
      'oratrice.ptr.moe',
    ],
    currentHost: undefined, // Current website's hostname.
    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.
    preferOrigins: 'external > local', // local, external, local > external, external > local.
    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,
    timeoutId: 0
  };

  // Unload the page: navigate, reload, back_forward.
  window.addEventListener('beforeunload', function(event) {
    //if (currentHost) GM_deleteValues(['currentHost', 'redirectHost']); // Reload by user only not by a script.
    window.clearInterval(redirectIntervalId);
    window.clearTimeout(downloader.timeoutId);
    if (downloader.instance) downloader.instance.abort();
  }, true);

  const configs = [{
    query: ':scope > pre:first-child',
    texts: ['', 'Moved Permanently', 'Service has been shutdown']
  }, {
    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!', "We’ll be back soon!", 'Performance Tracking', '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']
  }, {
    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 > :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',
    //'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://rl.bloat.cat',
    'https://snoo.habedieeh.re',
    //'https://safereddit.com', // REDIRECTION LOOP TEST.
  ];

  // Day/Month/Year, Hours:Minutes:Seconds AM/PM
  // 7/12/2025, 4:43:36 PM
  // Returns a date string like above or a decimal numbers for minutes.
  const getDate = function(inMinutes) {
    const date = new Date();
    if (inMinutes) return (date.getFullYear() * 525949) + ((date.getMonth() + 1) * 43829) + (date.getDate() * 1440) + (date.getHours() * 60) + date.getMinutes();
    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 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;
    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 = lastUpdate.replace(/:[0-9]+\s+[apAP][mM]$/, '').split(/(?:\/|,\s+|:)/).map(Number);
    const past = (values[0] * 1440) + (values[1] * 43829) + (values[2] * 525949) + (values[3] * 60) + values[4];
    const now = getDate(true); // Get current date in minutes as late as possible.
    if (now - past >= elapsed) return true;
    return false;
  };

  // Download a json file. The returned javascript object/array/null have to be gotten using Promise.then.
  const download = function(url, retries = 1, timeout = 5000, waitInterval = 1000) {
    return new Promise(resolve => {
      if (!window.navigator.onLine) {
        resolve(null);
        return;
      }

      const configs = {
        anonymous: true, // No cookies. 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,
        onabort: function(response) {
          downloader.abort = true;
          downloader.instance = undefined;
          resolve(null);
        },
        ontimeout: function(response) {
          if (downloader.abort || retries <= 0) {
            downloader.instance = undefined;
            resolve(null);
            return;
          }
          downloader.timeoutId = window.setTimeout(() => {
            downloader.timeoutId = 0;
            download(url, --retries, timeout, waitInterval).then(resolve);
          }, waitInterval);
        },
        // 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.
        init: function() {
          this.onerror = this.onload = function(response) {
            if (!response.response && response.status >= 500 && !downloader.abort && retries > 0) {
              downloader.timeoutId = window.setTimeout(() => {
                downloader.timeoutId = 0;
                download(url, --retries, timeout, waitInterval).then(resolve);
              }, waitInterval);
              return;
            }
            resolve(response.response); // Can be null.
            downloader.instance = undefined;
          };
          return this;
        },
      }.init();
      downloader.instance = GM_xmlhttpRequest(configs);
    });
  };

  // Specifically for Reblib's json structure.
  const getReblib = function() {
    return new Promise(resolve => {
      const url = 'https://raw.githubusercontent.com/redlib-org/redlib-instances/refs/heads/main/instances.json';

      const setExternalOrigins = function(object) {
        if (!object || !object.instances) {
          resolve(false);
          return;
        }
        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(false) });
          resolve(true);
        } else {
          externalOrigins = undefined;
          resolve(false);
        }
      };
      // Cancel pending download.
      if (downloader.instance) {
        window.clearTimeout(downloader.timeoutId);
        downloader.instance.abort();
        downloader.instance = undefined;
        downloader.timeoutId = 0;
      }
      downloader.abort = false;
      download(url).then(setExternalOrigins);
    });
  };

  const expireCookies = function() {
    if (!deleteCookies) return;
    window.sessionStorage.clear();
    window.localStorage.clear();
    if (document.cookie) {
      // Can't expire/delete HttpOnly cookies this way.
      const host = window.location.hostname;
      const domain = host.replace(/^(?:[^.]+\.)*([^.]+\.[^.]+)$/, '$1');
      const skip = (domain === host);
      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 + '/' });
      GM_cookie.delete({ url: 'https://anubis.techaro.lol/' });
    }
  };

  const getNewUrl = function() {
    return new Promise(async resolve => {
      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 + '/';
        const uri = window.decodeURIComponent(location.search);
        if (/[?&]redir=https?:\/\/[^/]+\/[^&]/.test(uri)) { // Absolute path
          currentURL += uri.match(/[?&]redir=https?:\/\/[^/]+\/([^&]+)/)[1];
        } else if (/[?&]redir=\/[^&]/.test(uri)) { // Relative path
          currentURL += uri.match(/[?&]redir=\/([^&]+)/)[1];
        }
      } 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,
          // Why not just /^https?:\/\/(.+)$/ you asked? In case you edited/added new origins and put a slash at the end e.g. https://a.b.c/
          redirectHost: workingSite.replace(/^https?:\/\/([^/]+).*$/, '$1')
        });
        currentHost = hostname; // For unload.
        resolve(currentURL.replace(/^https?:\/\/[^/]+/, workingSite));
        return;
      }
      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]) {
          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); // Exclude the first index.
          }
          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 });
            currentHost = hostname; // For unload.
            resolve(currentURL.replace(/^https?:\/\/[^/]+/, origin));
            return;
          }
        }
        if (!uptodate && listsOfOrigins[i] === externalOrigins) {
          listsOfOrigins[i] = undefined;
          externalOrigins = undefined;
          --i;
        }
      }
      if (uptodate === false) { // JSON structure changed, file not found or download error.
        resolve(undefined);
        return;
      }
      resolve(null);
    });
  };

  const check = async function() {
    if (hasChecked) return;
    hasChecked = true;
    let retries = 1;
    for (const config of configs) {
      let 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 path = config.query.replace(':scope', ':root > body').replace(/ [^ ]+$/, ' div#redirector');
        const div = document.createElement('div');
        const h1_1 = document.createElement('h1');
        const h1_2 = document.createElement('h1');
        const h1_3 = document.createElement('h1');
        const style = document.createElement('style');
        style.textContent = `${path + ' > *'} {
          display: block !important;
          font-family: sans-serif !important;
          font-size: 32px !important;
          font-weight: 700px !important;
          margin: 0px !important;
          text-align: center !important;
        }
        ${path + ' > :first-child'} {
          color: rgb(172,157,83) !important;
          overflow-wrap: break-word !important;
        }
        ${path + ' > :nth-child(2)'} {
          color: rgb(65,105,225) !important;
          overflow-wrap: break-word !important;
        }
        ${path + ' > :nth-child(3)'} {
          color: rgb(210,210,210) !important;
          text-shadow: -2px -2px 0 #000000, 2px -2px 0 #000000, -2px 2px 0 #000000, 2px 2px 0 #000000 !important;
          word-break: break-all !important;
        }`;
        document.head.appendChild(style);
        div.id = 'redirector';
        div.appendChild(h1_1);
        div.appendChild(h1_2);
        div.appendChild(h1_3);
        if (url === undefined) {
          h1_1.innerText = '⚠️ There was a problem getting alternative URLs of Reblib.';
        }
        if (!url) {
          h1_2.innerText = '💢 Failed to redirect!. All instances are broken.';
          h1_2.innerText += (preferOrigins === 'local') ? '\nTry again later.' : `\nTry again after ${updateFrequency}.`;
          style.textContent = style.textContent.replace(/rgb\(65,105,225\)/m, 'rgb(213,68,85)');
          target.replaceWith(div);
          return;
        }
        h1_2.innerText = 'Redirecting to another instance...';
        h1_3.innerText = `${url}`;
        target.replaceWith(div);
        expireCookies();
        redirectIntervalId = window.setInterval(past => { // For websites that are not responding.
          if (retries > 0) {
            h1_1.innerText = `⚠️ To-be-redirected server is not responding after ${Math.abs((Date.now() - past) / 1000)} seconds. Retrying: ${retries--}`;
            window.location.replace(url);
          } else {
            window.clearInterval(redirectIntervalId);
            redirectIntervalId = 0; // For unload.
            const host = url.replace(/^https?:\/\/([^/]+).*$/, '$1');
            if (!failedHosts.includes(host)) {
              failedHosts.push(host);
              GM_setValue('failedHosts', failedHosts);
            }
            GM_deleteValues(['currentHost', 'redirectHost']);
            window.location.reload(); // Re-run the script.
          }
        }, 10000, Date.now()); // 10 seconds. Is it too long?
        window.location.replace(url);
        return;
      }
    }

    // No redirection. 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 = undefined; // For unload.
    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: 'Delete SessionStorage, LocalStorage & Cookies: 《{}》',
    choices: [ 'Yes', 'No' ],
    options: { id: '2', 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[2];
      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: '3', autoClose: false, title: "Yes: Redirect to this website. No: Don't redirect to this website." },
    init: function() {
      this.index = Number(failedHosts.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[3];
      object.index = ++object.index & 1;
      object.title_ = object.title.replace('{}', object.choices[object.index]);
      const index = failedHosts.indexOf(window.location.hostname);
      if (!object.index) {
        if (index > 0) {
          failedHosts.splice(index, 1);
          GM_setValue('failedHosts', failedHosts);
        }
      } else {
        if (index < 0) {
          failedHosts.push(window.location.hostname);
          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: 'Factory Reset: 《{}》',
    choices: [ '💣💣💣', '💣💣', '💣', '💥💥💥' ],
    options: { id: '4', 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[4];
      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(['currentHost','deleteCookies','externalOrigins','failedHosts','hideSettings','lastUpdate','preferOrigins','redirectHost','updateFrequency','workingSite']);
        deleteCookies = true;
        failedHosts = [
          'redlib.thebunny.zone',
          'lr.ptr.moe',
          'oratrice.ptr.moe',
        ];
        hideSettings = true;
        preferOrigins = 'external > local';
        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: '5', 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[5];
      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()];

})();