import random import re from dataclasses import dataclass, field from typing import Callable from urllib import parse from qutebrowser.api import interceptor from qutebrowser.extensions.interceptors import QUrl, RedirectException from qutebrowser.utils import message @dataclass class Service: source: list[str] = field(default_factory=lambda: []) target: list[str] = field(default_factory=lambda: []) custom_targets: bool = False preprocess: Callable[[QUrl], QUrl] | None = None postprocess: Callable[[QUrl], QUrl] | None = None def __contains__(self, item: str): for source in self.source: if re.search(source, item): return True return False def scribe_global_identity(url: QUrl): """Fix external medium blog to scribe translation. Some paths from medium will go through a 'global identity' path which messes up the actual url path we want to go to and puts it in queries. This puts it back on the path. """ path = parse.unquote(f"{url.path()}{url.query()}", encoding="ascii") url.setQuery(None) new_path = re.sub(r"m/global-identity-2redirectUrl=", "", path) url.setPath( parse.quote(new_path), mode=QUrl.ParsingMode.StrictMode, ) return url def breezewiki_host_to_path(url: QUrl): host = url.host() if wiki := host[0 : host.find(".fandom.com")]: url.setPath(f"/{wiki}{url.path()}") return url default_services = [ Service(source=["youtube.com"], target=["invidious"]), Service(source=["stackoverflow.com"], target=["anonymousoverflow"]), Service(source=["odysee.com"], target=["librarian"]), Service(source=["reddit.com"], target=["redlib"]), Service(source=["instagram.com"], target=["proxigram"]), Service(source=["twitter.com"], target=["nitter"]), Service(source=["imdb.com"], target=["libremdb"]), Service(source=["tiktok.com"], target=["proxitok"]), Service(source=["imgur.com"], target=["rimgo"]), Service( source=["medium.com"], target=["scribe"], postprocess=scribe_global_identity ), Service( source=["fandom.com"], target=["breezewiki"], preprocess=breezewiki_host_to_path ), Service(source=["quora.com"], target=["quetre"]), Service(source=["google.com"], target=["whoogle"]), Service(source=["translate.google.com"], target=["lingva", "simplytranslate"]), Service(source=["deepl.com"], target=["simplytranslate"]), Service(source=["bandcamp.com"], target=["tent"]), Service( custom_targets=True, source=["genius.com"], target=[ "dumb.privacydev.net", "dumb.privacydev.net", "dm.vern.cc", "sing.whatever.social", "dumb.nunosempere.com", "dumb.lunar.icu", "dumb.esmailelbob.xyz", ], ), Service( custom_targets=True, source=["pinterest.com"], target=[ "pain.thirtysix.pw", "pt.bloat.cat", "painterest.gitro.xyz", ], ), Service( custom_targets=True, source=["tumblr.com"], target=[ "pb.bloat.cat", "tb.opnxng.com", "priviblur.pussthecat.org", "priviblur.thebunny.zone", "priviblur.gitro.xyz", "priviblur.canine.tools", ], ), Service( custom_targets=True, source=["twitch.com"], target=[ "safetwitch.drgns.space", "safetwitch.projectsegfau.lt", "stream.whateveritworks.org", "safetwitch.datura.network", "ttv.vern.cc", "safetwitch.frontendfriendly.xyz", "ttv.femboy.band", "twitch.seitan-ayoub.lol", "twitch.sudovanilla.org", "safetwitch.r4fo.com", "safetwitch.ducks.party", "safetwitch.privacyredirect.com", "st.ngn.tf", "safetwitch.darkness.services", "safetwitch.adminforge.de", ], ), Service( custom_targets=True, source=["goodreads.com"], target=[ "biblioreads.eu.org", "biblioreads.vercel.app", "biblioreads.mooo.com", "bl.vern.cc", "biblioreads.lunar.icu", "read.whateveritworks.org", "biblioreads.privacyfucking.rocks", "read.seitan-ayoub.lol", "read.freedit.eu", "biblioreads.ducks.party", "biblioreads.snine.nl", "biblioreads.privacyredirect.com", "reads.nezumi.party", "br.bloat.cat", "read.canine.tools", ], ), ] @dataclass class Redirects: services: list[Service] = field(default_factory=lambda: default_services) selector: Callable[[list[str]], str] = lambda c: c[ random.randint(0, len(c) - 1) ] # selection algorithm farside_service: str = ( "farside.link" # Contains url for farside-like redirector (e.g. 'fastsi.de') ) def __post_init__(self): interceptor.register(self.intercept) def intercept(self, request: interceptor.Request) -> None: # TODO: Implement config check (maybe 'privacy.redirect = False?') # if config.get(name="content.oss_redirects") is False: # return if ( request.resource_type != interceptor.ResourceType.main_frame or request.request_url.scheme() in {"data", "blob"} ): return url = request.request_url if url in self: try: request.redirect(self.get(url)) except RedirectException as e: message.error(str(e)) def get(self, url: QUrl) -> QUrl: service = self.get_service(url) if not service: return url foss_host = self.selector(service.target) if service.preprocess: url = service.preprocess(url) try: if service.custom_targets: url.setHost(foss_host) else: url.setHost(self.farside_service) url.setPath(f"/{foss_host}{url.path()}") except RedirectException as e: message.error(str(e)) if service.postprocess: url = service.postprocess(url) return url def get_service(self, url: QUrl) -> Service | None: host = url.host() for service in self.services: if host in service: return service def __contains__(self, item: QUrl): if self.get_service(item): return True return False _ = Redirects()