From 969e0a628871889ca3e45b8e1b22cc88ff4cf55f Mon Sep 17 00:00:00 2001 From: Marty Oehme <marty.oehme@gmail.com> Date: Fri, 14 Feb 2025 19:43:41 +0100 Subject: [PATCH] qutebrowser: Refactor redirects as importable class Make more extensive using of dataclasses for typing and simpler future refactors. The redirects 'plugin' is now a simple class which can be imported into the configuration and is automatically active when instantiated. For that to work it needs access to the qutebrowser python library but that should hopefully be a given if wanting to have a qutebrowser plugin. To make the default redirects active, simply import the class and instantiate it: ```python from fossredirect import Redirects _ = Redirects() ``` This loads the defaults and activates them in qutebrowser. Try to go to e.g. 'reddit.com' and it will automatically open in a libreddit frontend. To customize the redirects, provide a custom list of Services: ```python from fossredirect import Redirects _ = Redirects(services=[Service(source=["fromhere.com"], target=["tohere"])]) ``` It works a little more flexibly now: Redirects contains a list of Services. Each service is a simple data container with the following: ```python Service( source=["list.of", "hosts.to", "redirect.com"], target=["farside-redirect"] ) ``` The above redirects any of the source hosts to the far-side provided target (in this case it would be `farside.link/farside-redirect/<original-path>`). However we can also specify 'custom' targets if farside does not have a service that we want to redirect. ```python Service( source=["list.of", "hosts.to", "redirect.com"], target=["my-redirected-host.org"], custom_targets=True ) ``` This directly rewrites the host to `https://my-redirected-host.org/<original-path>`. Lastly, we can have custom preprocess/postprocess functions which fix some more involved redirect: ```python Service( source=["list.of", "hosts.to", "redirect.com"], target=["my-redirected-host.org"], postprocess=lambda item: item ) ``` Be aware that the functions take 'QUrl' objects so you have to access e.g. the actual host with `item.host()` before rewriting. Look at the breezewiki rewrite function for an easy example. --- qutebrowser/config/__init__.py | 0 qutebrowser/config/config.py | 10 +- qutebrowser/config/freedirect/__init__.py | 0 qutebrowser/config/freedirect/freedirect.py | 203 +++++++++++++++++++ qutebrowser/config/redirects.py | 207 -------------------- 5 files changed, 210 insertions(+), 210 deletions(-) create mode 100644 qutebrowser/config/__init__.py create mode 100644 qutebrowser/config/freedirect/__init__.py create mode 100644 qutebrowser/config/freedirect/freedirect.py delete mode 100644 qutebrowser/config/redirects.py diff --git a/qutebrowser/config/__init__.py b/qutebrowser/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 914ed43..a2d97ae 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -3,16 +3,18 @@ from typing import cast # pylint: disable=C0111 from qutebrowser.config.config import ConfigContainer # noqa: F401 -from qutebrowser.config.configfiles import ConfigAPI # noqa: F401 +from qutebrowser.config.configfiles import ConfigAPI + +from freedirect.freedirect import Redirects config: ConfigAPI = cast(ConfigAPI, config) # noqa: F821 pylint: disable=E0602,C0103 c: ConfigContainer = cast(ConfigContainer, c) # noqa: F821 pylint: disable=E0602,C0103 + # Autogenerated config.py # Documentation: # qute://help/configuring.html # qute://help/settings.html - # load additional settings configured via autoconfig.yml _ = config.load_autoconfig() @@ -59,7 +61,9 @@ config.source("alias.py") config.source("maps.py") config.source("content.py") config.source("searchengines.py") -config.source("redirects.py") + +_ = Redirects() + # Tab-Bar # have tab bar on the right, not on the top diff --git a/qutebrowser/config/freedirect/__init__.py b/qutebrowser/config/freedirect/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qutebrowser/config/freedirect/freedirect.py b/qutebrowser/config/freedirect/freedirect.py new file mode 100644 index 0000000..b18e3f0 --- /dev/null +++ b/qutebrowser/config/freedirect/freedirect.py @@ -0,0 +1,203 @@ +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=["genius.com"], target=["dumb"]), + 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=["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() diff --git a/qutebrowser/config/redirects.py b/qutebrowser/config/redirects.py deleted file mode 100644 index d6db906..0000000 --- a/qutebrowser/config/redirects.py +++ /dev/null @@ -1,207 +0,0 @@ -import random -import re -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 - - -def fixScribePath(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. - """ - # double unquoting necessary! - # I suppose we double-wrap it earlier somewhere? - # unquoted = parse.unquote( - # url.path(options=QUrl.ComponentFormattingOption.FullyEncoded) - # ) - 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 - - -type Service = dict[str, list[str]] -type Redirects = dict[str, Service] - - -redirects: Redirects = { - "youtube": { - "source": ["youtube.com"], - "farside": ["invidious"], - }, - "stackoverflow": { - "source": ["stackoverflow.com", "askubuntu.com"], - "farside": ["anonymousoverflow"], - }, - "lbry": { - "source": ["odysee.com"], - "farside": ["librarian"], - }, - "reddit": { - "source": ["reddit.com"], - "farside": ["redlib"], - }, - "instagram": { - "source": ["instagram.com"], - "farside": ["proxigram"], - }, - "twitter": { - "source": ["twitter.com"], - "farside": ["nitter"], - }, - "imdb": { - "source": ["imdb.com"], - "farside": ["libremdb"], - }, - "translate": { - "source": ["translate.google.com"], - "farside": ["lingva"], - }, - "tiktok": { - "source": ["tiktok.com"], - "farside": ["proxitok"], - }, - "imgur": { - "source": ["imgur.com"], - "farside": ["rimgo"], - }, - "medium": { - "source": ["medium.com"], - "farside": ["scribe"], - # "postprocess": fixScribePath - }, - "fandom": { - "source": ["fandom.com"], - "farside": ["breezewiki"], - }, - "quora": { - "source": ["quora.com"], - "farside": ["quetre"], - # "postprocess": lambda url: message.info(f"CALLING QUORA WITH {url}") - }, - "google": { - "source": ["google.com"], - "target": [ - "search.albony.xyz", - "search.garudalinux.org", - "search.dr460nf1r3.org", - "s.tokhmi.xyz", - "search.sethforprivacy.com", - "whoogle.dcs0.hu", - "gowogle.voring.me", - "whoogle.privacydev.net", - "wg.vern.cc", - "whoogle.hxvy0.gq", - "whoogle.hostux.net", - "whoogle.lunar.icu", - "wgl.frail.duckdns.org", - "whoogle.no-logs.com", - "whoogle.ftw.lol", - "whoogle-search--replitcomreside.repl.co", - "search.notrustverify.ch", - "whoogle.datura.network", - "whoogle.yepserver.xyz", - "search.nezumi.party", - ], - }, - "biblioreads": { - "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", - ], - }, - "safetwitch": { - "source": ["twitch.tv"], - "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", - "st.ggtyler.dev", - "safetwitch.lunar.icu", - "twitch.sudovanilla.com", - "safetwitch.r4fo.com", - "safetwitch.ducks.party", - "safetwitch.nogafam.fr", - "safetwitch.privacyredirect.com", - "st.ngn.tf", - ], - }, -} - - -def rewrite(request: interceptor.Request) -> None: - # 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 service := _should_be_redirected(url.host()): - url = _farside_redirect( - url, _pick_random(service["farside" if "farside" in service else "target"]) - ) - try: - request.redirect(url) - except RedirectException as e: - message.error(str(e)) - - if "postprocess" in service and isinstance(service["postprocess"], Callable): - url = service["postprocess"](url) - - -def _farside_redirect(url: QUrl, service: str, use_fastside: bool = True) -> QUrl: - try: - url.setHost("fastside.link" if use_fastside else "farside.link") - url.setPath(f"/{service}{url.path()}") - except RedirectException as e: - message.error(str(e)) - return url - - -def _pick_random[T](choices: list[T]) -> T: - return choices[random.randint(0, len(choices) - 1)] - - -def _should_be_redirected( - # TODO: Update to use typedefs/classes instead of this jumble - host: str, - redirects: Redirects = redirects, -) -> Service | None: - for service in redirects.values(): - for source in service["source"]: - if re.search(source, host): - return service - return None - - -interceptor.register(rewrite)