SimpCity Re-re-direct

Bypass the new external link warning on SimpCity

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        SimpCity Re-re-direct
// @namespace   https://github.com/chuckmingus
// @version     1.2.0
// @description Bypass the new external link warning on SimpCity
// @author      chuckmingus
// @icon        https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/cobalt.png
// @license     MIT
// @match       https://simpcity.cr/*
// @match       https://*.simpcity.cr/*
// @grant       none
// @run-at      document-start
// ==/UserScript==

(function () {
  "use strict";

  function isValidUrl(str) {
    try {
      const parsed = new URL(str);
      return parsed.protocol === "http:" || parsed.protocol === "https:";
    } catch {
      return false;
    }
  }

  function decodeTarget(str) {
    try {
      let normalized = str.replace(/-/g, "+").replace(/_/g, "/");
      while (normalized.length % 4 !== 0) {
        normalized += "=";
      }
      const binary = atob(normalized);
      const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
      return new TextDecoder().decode(bytes);
    } catch (e) {
      console.error("[SimpCity Re-re-direct] Failed to decode base64:", str, e);
      return null;
    }
  }

  function extractTargetFromUrl(href) {
    try {
      const url = new URL(href, window.location.origin);
      let targetUrl = url.searchParams.get("to");
      const mode = url.searchParams.get("m");

      if (targetUrl && mode === "b64") {
        targetUrl = decodeTarget(targetUrl);
      }

      if (targetUrl && isValidUrl(targetUrl)) {
        return targetUrl;
      }
    } catch (e) {
      // Silently fail for invalid URLs
    }
    return null;
  }

  // fallback for when we are already on a redirect page
  if (window.location.pathname.includes("/redirect/")) {
    const immediateTarget = extractTargetFromUrl(window.location.href);
    if (immediateTarget) {
      window.location.replace(immediateTarget);
      return;
    }

    // wait for DOM if parameters weren't available in search string
    document.addEventListener("DOMContentLoaded", () => {
      const link = document.querySelector(".simpLinkProxy-targetLink");
      if (link && link.href && isValidUrl(link.href)) {
        window.location.replace(link.href);
      }
    });
  }

  function processSingleLink(link) {
    link.dataset.bypassed = "true";
    const targetUrl = extractTargetFromUrl(link.href);

    if (targetUrl) {
      link.href = targetUrl;
      link.rel = "noopener noreferrer";
      link.removeAttribute("data-proxy-handler");
      link.removeAttribute("data-blank-handler");
      link.removeAttribute("data-proxy-href");
    }
  }

  function processLinks() {
    const links = document.querySelectorAll(
      'a[href*="/redirect/?to="]:not([data-bypassed])'
    );
    links.forEach(processSingleLink);
  }

  // intercept click events as a fallback
  document.addEventListener(
    "click",
    (e) => {
      const link = e.target.closest('a[href*="/redirect/?to="]');
      if (link && !link.dataset.bypassed) {
        processSingleLink(link);
      }
    },
    true
  );

  let isScheduled = false;
  function scheduleProcessLinks() {
    if (isScheduled) return;
    isScheduled = true;
    requestAnimationFrame(() => {
      processLinks();
      isScheduled = false;
    });
  }

  const observer = new MutationObserver((mutations) => {
    const hasAddedNodes = mutations.some((m) => m.addedNodes.length > 0);
    if (hasAddedNodes) {
      scheduleProcessLinks();
    }
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
  });

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", processLinks);
  } else {
    processLinks();
  }
})();