diff options
| -rw-r--r-- | .prettierrc.json | 1 | ||||
| -rw-r--r-- | .woodpecker.yml | 2 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | src/assets/javascripts/services.js | 1702 | ||||
| -rw-r--r-- | src/config/config.json | 1220 | ||||
| -rw-r--r-- | src/instances/get_instances.py | 161 | ||||
| -rw-r--r-- | web-ext-config.js | 4 |
7 files changed, 1556 insertions, 1537 deletions
diff --git a/.prettierrc.json b/.prettierrc.json index 3ac20a06..ce2a27b2 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -5,6 +5,7 @@ "arrowParens": "avoid", "printWidth": 200, "bracketSameLine": true, + "endOfLine": "lf", "overrides": [ { "files": ["*.js", "*.json"], diff --git a/.woodpecker.yml b/.woodpecker.yml index 177368e5..3c7d58b2 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,6 +1,6 @@ pipeline: instancefetch: - image: python:alpine + image: python:slim secrets: [token, mail] when: - event: cron @@ -55,7 +55,6 @@ A web extension that redirects YouTube, Twitter, Instagram... requests to altern [](https://codeberg.org/LibRedirect/libredirect) [](https://github.com/libredirect/libredirect/) - ## Translate [](https://hosted.weblate.org/projects/libredirect/extension) @@ -72,9 +71,11 @@ npm install ``` To generate html that uses `config.json` (needed to develop/build the extention), run: + ``` npm run ejs ``` + Afterwards, you will need to run it if you modify `config.json` or any files ending with .ejs. ### Build the extention zip archive: diff --git a/src/assets/javascripts/services.js b/src/assets/javascripts/services.js index d02e03f6..e67ed842 100644 --- a/src/assets/javascripts/services.js +++ b/src/assets/javascripts/services.js @@ -1,851 +1,851 @@ -window.browser = window.browser || window.chrome
-
-import utils from "./utils.js"
-
-let config, options, redirects, targets
-
-function init() {
- return new Promise(async resolve => {
- browser.storage.local.get(["options", "redirects", "targets"], r => {
- options = r.options
- redirects = r.redirects
- targets = r.targets
- fetch("/config/config.json")
- .then(response => response.text())
- .then(configData => {
- config = JSON.parse(configData)
- resolve()
- })
- })
- })
-}
-
-init()
-browser.storage.onChanged.addListener(init)
-
-function fetchFrontendInstanceList(service, frontend, redirects, options, config) {
- let tmp = []
- if (config.services[service].frontends[frontend].instanceList) {
- for (const network in config.networks) {
- tmp.push(...redirects[network], ...options[frontend][network].custom)
- }
- } else if (config.services[service].frontends[frontend].singleInstance) tmp = config.services[service].frontends[frontend].singleInstance
- return tmp
-}
-
-function all(service, frontend, options, config, redirects) {
- let instances = []
- if (!frontend) {
- for (const frontend in config.services[service].frontends) {
- instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config))
- }
- } else {
- instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config))
- }
- return instances
-}
-
-function regexArray(service, url, config) {
- if (config.services[service].targets == "datajson") {
- if (targets[service].includes(utils.protocolHost(url))) return true
- } else {
- const targetList = config.services[service].targets
- for (const targetString in targetList) {
- const target = new RegExp(targetList[targetString])
- if (target.test(url.href)) return true
- }
- }
- return false
-}
-
-function redirect(url, type, initiator, forceRedirection) {
- if (type != "main_frame" && type != "sub_frame") return
- let randomInstance
- let frontend
- for (const service in config.services) {
- if (!forceRedirection && !options[service].enabled) continue
- if (config.services[service].embeddable && type != options[service].redirectType && options[service].redirectType != "both") continue
- if (!config.services[service].embeddable && type != "main_frame") continue
- // let targets = new RegExp(config.services[service].targets.join("|"), "i")
-
- if (!regexArray(service, url, config)) continue
- // if (initiator) {
- // console.log(initiator.host)
- // if (targets.test(initiator.host)) continue
- // //if (all(service, null, options, config, redirects).includes(initiator.origin) && reverse(initiator) == url) return "BYPASSTAB"
- // }
-
- if (Object.keys(config.services[service].frontends).length > 1) {
- if (type == "sub_frame" && config.services[service].embeddable && !config.services[service].frontends[options[service].frontend].embeddable) frontend = options[service].embedFrontend
- else frontend = options[service].frontend
- } else frontend = Object.keys(config.services[service].frontends)[0]
-
- if (config.services[service].frontends[frontend].instanceList) {
- let instanceList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom]
- if (instanceList.length === 0 && options.networkFallback) instanceList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom]
- if (instanceList.length === 0) return
- randomInstance = utils.getRandomInstance(instanceList)
- } else if (config.services[service].frontends[frontend].singleInstance) randomInstance = config.services[service].frontends[frontend].singleInstance
- break
- }
- if (!frontend) return
-
- // Here is a (temperory) space for defining constants required in 2 or more switch cases.
- // When possible, try have the two switch cases share all their code as done with searx and searxng.
- // Do not do that when they do not share 100% of their code.
-
- const mapCentreRegex = /@(-?\d[0-9.]*),(-?\d[0-9.]*),(\d{1,2})[.z]/
- const dataLatLngRegex = /!3d(-?[0-9]{1,}.[0-9]{1,})!4d(-?[0-9]{1,}.[0-9]{1,})/
- const placeRegex = /\/place\/(.*)\//
- function convertMapCentre() {
- let [lat, lon, zoom] = [null, null, null]
- if (url.pathname.match(mapCentreRegex)) {
- // Set map centre if present
- ;[lat, lon, zoom] = url.pathname.match(mapCentreRegex)
- } else if (url.searchParams.has("center")) {
- ;[lat, lon] = url.searchParams.get("center").split(",")
- zoom = url.searchParams.get("zoom") ?? "17"
- }
- return [zoom, lon, lat]
- }
-
- switch (frontend) {
- // This is where all instance-specific code must be ran to convert the service url to one that can be understood by the frontend.
- case "beatbump":
- return `${randomInstance}${url.pathname}${url.search}`
- .replace("/watch?v=", "/listen?id=")
- .replace("/channel/", "/artist/")
- .replace("/playlist?list=", "/playlist/VL")
- .replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/") + "?filter=all")
- case "hyperpipe":
- return `${randomInstance}${url.pathname}${url.search}`.replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/"))
- case "bibliogram":
- const reservedPaths = ["u", "p", "privacy"]
- if (url.pathname === "/" || reservedPaths.includes(url.pathname.split("/")[1])) return `${randomInstance}${url.pathname}${url.search}`
- if (url.pathname.startsWith("/reel") || url.pathname.startsWith("/tv")) return `${randomInstance}/p${url.pathname.replace(/\/reel|\/tv/i, "")}${url.search}`
- else return `${randomInstance}/u${url.pathname}${url.search}` // Likely a user profile, redirect to '/u/...'
- case "lbryDesktop":
- return url.href.replace(/^https?:\/{2}odysee\.com\//, "lbry://").replace(/:(?=[a-zA-Z0-9])/g, "#")
- case "neuters":
- if (url.pathname.startsWith("/article/") || url.pathname.startsWith("/pf/") || url.pathname.startsWith("/arc/") || url.pathname.startsWith("/resizer/")) return null
- else if (url.pathname.endsWith("/")) return `${randomInstance}${url.pathname}`
- else return `${randomInstance}${url.pathname}/`
- case "searx":
- case "searxng":
- return `${randomInstance}/?q=${encodeURIComponent(url.searchParams.get("q"))}`
- case "whoogle":
- return `${randomInstance}/search?q=${encodeURIComponent(url.searchParams.get("q"))}`
- case "librex":
- return `${randomInstance}/search.php?q=${encodeURIComponent(url.searchParams.get("q"))}`
- case "send":
- return randomInstance
- case "nitter":
- let search = new URLSearchParams(url.search)
-
- search.delete("ref_src")
- search.delete("ref_url")
-
- search = search.toString()
- if (search !== "") search = `?${search}`
-
- if (url.host.split(".")[0] === "pbs" || url.host.split(".")[0] === "video") {
- try {
- const [, id, format, extra] = search.match(/(.*)\?format=(.*)&(.*)/)
- const query = encodeURIComponent(`${id}.${format}?${extra}`)
- return `${randomInstance}/pic${url.pathname}${query}`
- } catch {
- return `${randomInstance}/pic${url.pathname}${search}`
- }
- }
-
- if (url.pathname.split("/").includes("tweets")) return `${randomInstance}${url.pathname.replace("/tweets", "")}${search}`
- if (url.host == "t.co") return `${randomInstance}/t.co${url.pathname}`
- return `${randomInstance}${url.pathname}${search}`
- case "yattee":
- return url.href.replace(/^https?:\/{2}/, "yattee://")
- case "freetube":
- return `freetube://https://youtu.be${url.pathname}${url.search}`.replace(/watch\?v=/, "")
- case "simplyTranslate":
- return `${randomInstance}/${url.search}`
- case "libreTranslate":
- return `${randomInstance}/${url.search}`
- .replace(/(?<=\/?)sl/, "source")
- .replace(/(?<=&)tl/, "target")
- .replace(/(?<=&)text/, "q")
- case "osm": {
- if (initiator && initiator.host === "earth.google.com") return
- const travelModes = {
- driving: "fossgis_osrm_car",
- walking: "fossgis_osrm_foot",
- bicycling: "fossgis_osrm_bike",
- transit: "fossgis_osrm_car", // not implemented on OSM, default to car.
- }
-
- function addressToLatLng(address) {
- const xmlhttp = new XMLHttpRequest()
- xmlhttp.open("GET", `https://nominatim.openstreetmap.org/search/${address}?format=json&limit=1`, false)
- xmlhttp.send()
- if (xmlhttp.status === 200) {
- const json = JSON.parse(xmlhttp.responseText)[0]
- if (json) {
- console.log("json", json)
- return [`${json.lat},${json.lon}`, `${json.boundingbox[2]},${json.boundingbox[1]},${json.boundingbox[3]},${json.boundingbox[0]}`]
- }
- }
- console.info("Error: Status is " + xmlhttp.status)
- }
-
- let mapCentre = "#"
- let prefs = {}
-
- const mapCentreData = convertMapCentre()
- if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#map=${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}`
- if (url.searchParams.get("layer")) prefs.layers = osmLayers[url.searchParams.get("layer")]
-
- if (url.pathname.includes("/embed")) {
- // Handle Google Maps Embed API
- // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France
- //console.log("embed life")
-
- let query = ""
- if (url.searchParams.has("q")) query = url.searchParams.get("q")
- else if (url.searchParams.has("query")) query = url.searchParams.has("query")
- else if (url.searchParams.has("pb"))
- try {
- query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1]
- } catch (error) {
- console.error(error)
- } // Unable to find map marker in URL.
-
- let [coords, boundingbox] = addressToLatLng(query)
- prefs.bbox = boundingbox
- prefs.marker = coords
- prefs.layer = "mapnik"
- let prefsEncoded = new URLSearchParams(prefs).toString()
- return `${randomInstance}/export/embed.html?${prefsEncoded}`
- } else if (url.pathname.includes("/dir")) {
- // Handle Google Maps Directions
- // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling
-
- let travMod = url.searchParams.get("travelmode")
- if (url.searchParams.has("travelmode")) prefs.engine = travelModes[travMod]
-
- let orgVal = url.searchParams.get("origin")
- let destVal = url.searchParams.get("destination")
-
- let org = addressToLatLng(orgVal)
- let dest = addressToLatLng(destVal)
- prefs.route = `${org};${dest}`
-
- let prefsEncoded = new URLSearchParams(prefs).toString()
- return `${randomInstance}/directions?${prefsEncoded}${mapCentre}`
- } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) {
- // Get marker from data attribute
- // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948
- //console.log("data life")
-
- let [, mlat, mlon] = url.pathname.match(dataLatLngRegex)
-
- return `${randomInstance}/search?query=${mlat}%2C${mlon}`
- } else if (url.searchParams.has("ll")) {
- // Get marker from ll param
- // https://maps.google.com/?ll=38.882147,-76.99017
- //console.log("ll life")
-
- const [mlat, mlon] = url.searchParams.get("ll").split(",")
-
- return `${randomInstance}/search?query=${mlat}%2C${mlon}`
- } else if (url.searchParams.has("viewpoint")) {
- // Get marker from viewpoint param.
- // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80
- //console.log("viewpoint life")
-
- const [mlat, mlon] = url.searchParams.get("viewpoint").split(",")
-
- return `${randomInstance}/search?query=${mlat}%2C${mlon}`
- } else {
- // Use query as search if present.
- //console.log("normal life")
-
- let query
- if (url.searchParams.has("q")) query = url.searchParams.get("q")
- else if (url.searchParams.has("query")) query = url.searchParams.get("query")
- else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1]
-
- let prefsEncoded = new URLSearchParams(prefs).toString()
- if (query) return `${randomInstance}/search?query="${query}${mapCentre}&${prefsEncoded}`
- }
-
- let prefsEncoded = new URLSearchParams(prefs).toString()
- console.log("mapCentre", mapCentre)
- console.log("prefs", prefs)
- console.log("prefsEncoded", prefsEncoded)
- return `${randomInstance}/${mapCentre}&${prefsEncoded}`
- }
- case "facil": {
- if (initiator && initiator.host === "earth.google.com") return
- const travelModes = {
- driving: "car",
- walking: "pedestrian",
- bicycling: "bicycle",
- transit: "car", // not implemented on Facil, default to car.
- }
- const mapCentreData = convertMapCentre()
- let mapCentre = "#"
- if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}`
-
- if (url.pathname.includes("/embed")) {
- // Handle Google Maps Embed API
- // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France
- //console.log("embed life")
-
- let query = ""
- if (url.searchParams.has("q")) query = url.searchParams.get("q")
- else if (url.searchParams.has("query")) query = url.searchParams.has("query")
- else if (url.searchParams.has("pb"))
- try {
- query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1]
- } catch (error) {
- console.error(error)
- } // Unable to find map marker in URL.
-
- return `${randomInstance}/#q=${query}`
- } else if (url.pathname.includes("/dir")) {
- // Handle Google Maps Directions
- // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling
-
- let travMod = url.searchParams.get("travelmode")
-
- let orgVal = url.searchParams.get("origin")
- let destVal = url.searchParams.get("destination")
-
- return `${randomInstance}/#q=${orgVal}%20to%20${destVal}%20by%20${travelModes[travMod]}`
- } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) {
- // Get marker from data attribute
- // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948
- //console.log("data life")
-
- let [, mlat, mlon] = url.pathname.match(dataLatLngRegex)
-
- return `${randomInstance}/#q=${mlat}%2C${mlon}`
- } else if (url.searchParams.has("ll")) {
- // Get marker from ll param
- // https://maps.google.com/?ll=38.882147,-76.99017
- //console.log("ll life")
-
- const [mlat, mlon] = url.searchParams.get("ll").split(",")
-
- return `${randomInstance}/#q=${mlat}%2C${mlon}`
- } else if (url.searchParams.has("viewpoint")) {
- // Get marker from viewpoint param.
- // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80
- //console.log("viewpoint life")
-
- const [mlat, mlon] = url.searchParams.get("viewpoint").split(",")
-
- return `${randomInstance}/#q=${mlat}%2C${mlon}`
- } else {
- // Use query as search if present.
- //console.log("normal life")
-
- let query
- if (url.searchParams.has("q")) query = url.searchParams.get("q")
- else if (url.searchParams.has("query")) query = url.searchParams.get("query")
- else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1]
-
- if (query) return `${randomInstance}/${mapCentre}/Mpnk/${query}`
- }
- }
- case "wikiless":
- let GETArguments = []
- if (url.search.length > 0) {
- let search = url.search.substring(1) //get rid of '?'
- let argstrings = search.split("&")
- for (let i = 0; i < argstrings.length; i++) {
- let args = argstrings[i].split("=")
- GETArguments.push([args[0], args[1]])
- }
- }
-
- let link = `${randomInstance}${url.pathname}`
- let urlSplit = url.host.split(".")
- if (urlSplit[0] != "wikipedia" && urlSplit[0] != "www") {
- if (urlSplit[0] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"])
- else GETArguments.push(["lang", urlSplit[0]])
- if (urlSplit[1] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"])
- // wikiless doesn't have mobile view support yet
- }
- for (let i = 0; i < GETArguments.length; i++) link += (i == 0 ? "?" : "&") + GETArguments[i][0] + "=" + GETArguments[i][1]
- return link
-
- case "lingva":
- let params_arr = url.search.split("&")
- params_arr[0] = params_arr[0].substring(1)
- let params = {}
- for (let i = 0; i < params_arr.length; i++) {
- let pair = params_arr[i].split("=")
- params[pair[0]] = pair[1]
- }
- if (params.sl && params.tl && params.text) {
- return `${randomInstance}/${params.sl}/${params.tl}/${params.text}`
- }
- return randomInstance
- case "breezeWiki":
- let wiki = url.hostname.match(/^[a-zA-Z0-9-]+(?=\.fandom\.com)/)
- if (wiki == "www" || !wiki) wiki = ""
- else wiki = "/" + wiki
- if (url.href.search(/Special:Search\?query/) > -1) return `${randomInstance}${wiki}${url.pathname}${url.search}`.replace(/Special:Search\?query/, "search?q").replace(/\/wiki/, "")
- else return `${randomInstance}${wiki}${url.pathname}${url.search}`
- case "rimgo":
- if (url.href.search(/^https?:\/{2}(?:[im]\.)?stack\./) > -1) return `${randomInstance}/stack${url.pathname}${url.search}`
- else return `${randomInstance}${url.pathname}${url.search}`
- case "libreddit":
- const subdomain = url.hostname.match(/^(?:(?:external-)?preview|i)(?=\.redd\.it)/)
- if (!subdomain) return `${randomInstance}${url.pathname}${url.search}`
- switch (subdomain[0]) {
- case "preview":
- return `${randomInstance}/preview/pre${url.pathname}${url.search}`
- case "external-preview":
- return `${randomInstance}/preview/external-pre${url.pathname}${url.search}`
- case "i":
- return `${randomInstance}/img${url.pathname}`
- }
- case "teddit":
- if (/^(?:(?:external-)?preview|i)\.redd\.it/.test(url.hostname)) {
- if (url.search == "") return `${randomInstance}${url.pathname}?teddit_proxy=${url.hostname}`
- else return `${randomInstance}${url.pathname}${url.search}&teddit_proxy=${url.hostname}`
- }
- return `${randomInstance}${url.pathname}${url.search}`
- default:
- return `${randomInstance}${url.pathname}${url.search}`
- }
-}
-
-function computeService(url, returnFrontend) {
- return new Promise(resolve => {
- fetch("/config/config.json")
- .then(response => response.text())
- .then(configData => {
- const config = JSON.parse(configData)
- browser.storage.local.get(["redirects", "options"], r => {
- const redirects = r.redirects
- const options = r.options
- for (const service in config.services) {
- if (regexArray(service, url, config)) {
- resolve(service)
- return
- } else {
- for (const frontend in config.services[service].frontends) {
- if (all(service, frontend, options, config, redirects).includes(utils.protocolHost(url))) {
- if (returnFrontend) resolve([service, frontend, utils.protocolHost(url)])
- else resolve(service)
- return
- }
- }
- }
- }
- resolve()
- })
- })
- })
-}
-
-function switchInstance(url) {
- return new Promise(async resolve => {
- await init()
- const protocolHost = utils.protocolHost(url)
- for (const service in config.services) {
- if (!all(service, null, options, config, redirects).includes(protocolHost)) continue
-
- let instancesList
- if (Object.keys(config.services[service].frontends).length == 1) {
- const frontend = Object.keys(config.services[service].frontends)[0]
- instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom]
- if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom]
- } else {
- const frontend = options[service].frontend
- instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom]
- if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom]
- }
-
- let oldInstance
- const i = instancesList.indexOf(protocolHost)
- if (i > -1) {
- oldInstance = instancesList[i]
- instancesList.splice(i, 1)
- }
- if (instancesList.length === 0) {
- resolve()
- return
- }
- const randomInstance = utils.getRandomInstance(instancesList)
- const oldUrl = `${oldInstance}${url.pathname}${url.search}`
- // This is to make instance switching work when the instance depends on the pathname, eg https://darmarit.org/searx
- // Doesn't work because of .includes array method, not a top priotiry atm
- resolve(oldUrl.replace(oldInstance, randomInstance))
- return
- }
- resolve()
- })
-}
-
-function reverse(url, urlString) {
- return new Promise(async resolve => {
- await init()
- let protocolHost
- if (!urlString) protocolHost = utils.protocolHost(url)
- else protocolHost = url.match(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/)[0]
- for (const service in config.services) {
- if (!all(service, null, options, config, redirects).includes(protocolHost)) continue
-
- switch (service) {
- case "instagram":
- case "youtube":
- case "imdb":
- case "imgur":
- case "tiktok":
- case "twitter":
- case "reddit":
- case "imdb":
- case "reuters":
- case "quora":
- case "medium":
- if (!urlString) resolve(config.services[service].url + url.pathname + url.search)
- else resolve(url.replace(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/, config.services[service].url))
- return
- default:
- resolve()
- return
- }
- }
- resolve()
- })
-}
-
-function unifyPreferences(url, tabId) {
- return new Promise(async resolve => {
- await init()
- const protocolHost = utils.protocolHost(url)
- for (const service in config.services) {
- for (const frontend in config.services[service].frontends) {
- if (all(service, frontend, options, config, redirects).includes(protocolHost)) {
- let instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom]
- if (options.networkFallback && options.network != "clearnet") instancesList.push(...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom)
-
- const frontendObject = config.services[service].frontends[frontend]
- if ("cookies" in frontendObject.preferences) {
- for (const cookie of frontendObject.preferences.cookies) {
- await utils.copyCookie(url, instancesList, cookie)
- }
- }
- if ("localstorage" in frontendObject.preferences) {
- browser.storage.local.set({ tmp: [frontend, frontendObject.preferences.localstorage] })
- browser.tabs.executeScript(tabId, {
- file: "/assets/javascripts/get-localstorage.js",
- runAt: "document_start",
- })
- for (const instance of instancesList)
- browser.tabs.create({ url: instance }, tab =>
- browser.tabs.executeScript(tab.id, {
- file: "/assets/javascripts/set-localstorage.js",
- runAt: "document_start",
- })
- )
- }
- /*
- if ("indexeddb" in frontendObject.preferences) {
- }
- if ("token" in frontendObject.preferences) {
- }
- */
- resolve(true)
- return
- }
- }
- }
- })
-}
-
-function setRedirects(passedRedirects) {
- return new Promise(resolve => {
- fetch("/config/config.json")
- .then(response => response.text())
- .then(configData => {
- browser.storage.local.get(/* [ */ "options" /* , "blacklists"] */, async r => {
- let redirects = passedRedirects
- let options = r.options
- const config = JSON.parse(configData)
- let targets = {}
- for (const service in config.services) {
- if (config.services[service].targets == "datajson") {
- targets[service] = redirects[service]
- delete redirects[service]
- }
- for (const frontend in config.services[service].frontends) {
- if (config.services[service].frontends[frontend].instanceList) {
- for (const network in config.networks) {
- for (const instance of options[frontend][network].enabled) {
- let i = redirects[frontend][network].indexOf(instance)
- if (i < 0) options[frontend][network].enabled.splice(i, 1)
- }
- }
- }
- }
- /*
- for (const frontend in config.services[service].frontends) {
- if (config.services[service].frontends[frontend].instanceList) {
- for (const network in config.networks) {
- options[frontend][network].enabled = redirects[frontend][network]
- }
- for (const blacklist in r.blacklists) {
- for (const instance of blacklist) {
- let i = options[frontend].clearnet.enabled.indexOf(instance)
- if (i > -1) options[frontend].clearnet.enabled.splice(i, 1)
- }
- }
- }
- }
- */
- // The above will be implemented with https://github.com/libredirect/libredirect/issues/334
- }
- for (const frontend in redirects) {
- let exists = false
- for (const service in config.services) if (config.services[service].frontends[frontend]) exists = true
- if (!exists) delete redirects[frontend]
- else for (const network in redirects[frontend]) if (!config.networks[network]) delete redirects[frontend][network]
- }
- browser.storage.local.set({ redirects, targets, options }, () => resolve())
- })
- })
- })
-}
-
-function initDefaults() {
- return new Promise(resolve => {
- fetch("/instances/data.json")
- .then(response => response.text())
- .then(data => {
- fetch("/config/config.json")
- .then(response => response.text())
- .then(configData => {
- browser.storage.local.get(["options", "blacklists"], r => {
- let redirects = JSON.parse(data)
- let options = r.options
- let targets = {}
- let config = JSON.parse(configData)
- const localstorage = {}
- const latency = {}
- for (const service in config.services) {
- options[service] = {}
- if (config.services[service].targets == "datajson") {
- targets[service] = redirects[service]
- delete redirects[service]
- }
- for (const defaultOption in config.services[service].options) options[service][defaultOption] = config.services[service].options[defaultOption]
- for (const frontend in config.services[service].frontends) {
- if (config.services[service].frontends[frontend].instanceList) {
- options[frontend] = {}
- for (const network in config.networks) {
- options[frontend][network] = {}
- options[frontend][network].enabled = JSON.parse(data)[frontend][network]
- options[frontend][network].custom = []
- }
- for (const blacklist in r.blacklists) {
- for (const instance of r.blacklists[blacklist]) {
- let i = options[frontend].clearnet.enabled.indexOf(instance)
- if (i > -1) options[frontend].clearnet.enabled.splice(i, 1)
- }
- }
- }
- }
- }
- browser.storage.local.set({ redirects, options, targets, latency, localstorage })
- resolve()
- })
- })
- })
- })
-}
-
-function upgradeOptions() {
- return new Promise(resolve => {
- fetch("/config/config.json")
- .then(response => response.text())
- .then(configData => {
- browser.storage.local.get(null, r => {
- let options = r.options
- let latency = {}
- const config = JSON.parse(configData)
- options.exceptions = r.exceptions
- if (r.theme != "DEFAULT") options.theme = r.theme
- options.popupServices = r.popupFrontends
- let tmp = options.popupServices.indexOf("tikTok")
- if (tmp > -1) {
- options.popupServices.splice(tmp, 1)
- options.popupServices.push("tiktok")
- }
- tmp = options.popupServices.indexOf("sendTarget")
- if (tmp > -1) {
- options.popupServices.splice(tmp, 1)
- options.popupServices.push("sendFiles")
- }
- options.autoRedirect = r.autoRedirect
- switch (r.onlyEmbeddedVideo) {
- case "onlyNotEmbedded":
- options.youtube.redirectType = "main_frame"
- case "onlyEmbedded":
- options.youtube.redirectType = "sub_frame"
- case "both":
- options.youtube.redirectType = "both"
- }
- for (const service in config.services) {
- let oldService
- switch (service) {
- case "tiktok":
- oldService = "tikTok"
- break
- case "sendFiles":
- oldService = "sendTarget"
- break
- default:
- oldService = service
- }
- options[service].enabled = !r["disable" + utils.camelCase(oldService)]
- if (r[oldService + "Frontend"]) {
- if (r[oldService + "Frontend"] == "yatte") options[service].frontend = "yattee"
- else options[service].frontend = r[oldService + "Frontend"]
- }
- if (r[oldService + "RedirectType"]) options[service].redirectType = r[oldService + "RedirectType"]
- if (r[oldService + "EmbedFrontend"] && (service != "youtube" || r[oldService + "EmbedFrontend"] == "invidious" || r[oldService + "EmbedFrontend"] == "piped"))
- options[service].embedFrontend = r[oldService + "EmbedFrontend"]
- for (const frontend in config.services[service].frontends) {
- if (r[frontend + "Latency"]) latency[frontend] = r[frontend + "Latency"]
- for (const network in config.networks) {
- let protocol
- if (network == "clearnet") protocol = "normal"
- else protocol = network
- if (r[frontend + utils.camelCase(protocol) + "RedirectsChecks"]) {
- options[frontend][network].enabled = r[frontend + utils.camelCase(protocol) + "RedirectsChecks"]
- options[frontend][network].custom = r[frontend + utils.camelCase(protocol) + "CustomRedirects"]
- for (const instance of options[frontend][network].enabled) {
- let i = r.redirects[frontend][network].indexOf(instance)
- if (i < 0) options[frontend][network].enabled.splice(i, 1)
- }
- }
- }
- }
- }
- browser.storage.local.set({ options, latency }, () => resolve())
- })
- })
- })
-}
-
-function processUpdate() {
- return new Promise(resolve => {
- fetch("/instances/data.json")
- .then(response => response.text())
- .then(data => {
- fetch("/config/config.json")
- .then(response => response.text())
- .then(configData => {
- browser.storage.local.get(["options", "blacklists", "targets"], r => {
- let redirects = JSON.parse(data)
- let options = r.options
- let targets = r.targets
- let config = JSON.parse(configData)
- for (const service in config.services) {
- if (!options[service]) options[service] = {}
- if (config.services[service].targets == "datajson") {
- targets[service] = redirects[service]
- delete redirects[service]
- }
- for (const defaultOption in config.services[service].options) {
- if (options[service][defaultOption] === undefined) {
- options[service][defaultOption] = config.services[service].options[defaultOption]
- }
- }
- for (const frontend in config.services[service].frontends) {
- if (config.services[service].frontends[frontend].instanceList) {
- if (!options[frontend]) options[frontend] = {}
- for (const network in config.networks) {
- if (!options[frontend][network]) {
- options[frontend][network] = {}
- options[frontend][network].enabled = JSON.parse(data)[frontend][network]
- options[frontend][network].custom = []
- if (network == "clearnet") {
- for (const blacklist in r.blacklists) {
- for (const instance of r.blacklists[blacklist]) {
- let i = options[frontend].clearnet.enabled.indexOf(instance)
- if (i > -1) options[frontend].clearnet.enabled.splice(i, 1)
- }
- }
- }
- } else {
- for (const instance of options[frontend][network].enabled) {
- let i = redirects[frontend][network].indexOf(instance)
- if (i < 0) options[frontend][network].enabled.splice(i, 1)
- }
- }
- }
- }
- }
- }
- browser.storage.local.set({ redirects, options, targets })
- resolve()
- })
- })
- })
- })
-}
-
-// For websites that have a strict policy that would not normally allow these frontends to be embedded within the website.
-function modifyContentSecurityPolicy(details) {
- let isChanged = false
- if (details.type == "main_frame") {
- for (const header in details.responseHeaders) {
- if (details.responseHeaders[header].name == "content-security-policy") {
- let instancesList = []
- for (const service in config.services) {
- if (config.services[service].embeddable) {
- for (const frontend in config.services[service].frontends) {
- if (config.services[service].frontends[frontend].embeddable) {
- for (const network in config.networks) {
- instancesList.push(...options[frontend][network].enabled, ...options[frontend][network].custom)
- }
- }
- }
- }
- }
- let securityPolicyList = details.responseHeaders[header].value.split(";")
- for (const i in securityPolicyList) securityPolicyList[i] = securityPolicyList[i].trim()
- let newSecurity = ""
- for (const item of securityPolicyList) {
- if (item.trim() == "") continue
- let regex = item.match(/([a-z-]{0,}) (.*)/)
- if (regex == null) continue
- let [, key, vals] = regex
- if (key == "frame-src") vals = vals + " " + instancesList.join(" ")
- newSecurity += key + " " + vals + "; "
- }
-
- details.responseHeaders[header].value = newSecurity
- isChanged = true
- }
- }
- if (isChanged) return { responseHeaders: details.responseHeaders }
- }
-}
-
-export default {
- redirect,
- computeService,
- switchInstance,
- reverse,
- unifyPreferences,
- setRedirects,
- initDefaults,
- upgradeOptions,
- processUpdate,
- modifyContentSecurityPolicy,
-}
+window.browser = window.browser || window.chrome + +import utils from "./utils.js" + +let config, options, redirects, targets + +function init() { + return new Promise(async resolve => { + browser.storage.local.get(["options", "redirects", "targets"], r => { + options = r.options + redirects = r.redirects + targets = r.targets + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + config = JSON.parse(configData) + resolve() + }) + }) + }) +} + +init() +browser.storage.onChanged.addListener(init) + +function fetchFrontendInstanceList(service, frontend, redirects, options, config) { + let tmp = [] + if (config.services[service].frontends[frontend].instanceList) { + for (const network in config.networks) { + tmp.push(...redirects[network], ...options[frontend][network].custom) + } + } else if (config.services[service].frontends[frontend].singleInstance) tmp = config.services[service].frontends[frontend].singleInstance + return tmp +} + +function all(service, frontend, options, config, redirects) { + let instances = [] + if (!frontend) { + for (const frontend in config.services[service].frontends) { + instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config)) + } + } else { + instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config)) + } + return instances +} + +function regexArray(service, url, config) { + if (config.services[service].targets == "datajson") { + if (targets[service].includes(utils.protocolHost(url))) return true + } else { + const targetList = config.services[service].targets + for (const targetString in targetList) { + const target = new RegExp(targetList[targetString]) + if (target.test(url.href)) return true + } + } + return false +} + +function redirect(url, type, initiator, forceRedirection) { + if (type != "main_frame" && type != "sub_frame") return + let randomInstance + let frontend + for (const service in config.services) { + if (!forceRedirection && !options[service].enabled) continue + if (config.services[service].embeddable && type != options[service].redirectType && options[service].redirectType != "both") continue + if (!config.services[service].embeddable && type != "main_frame") continue + // let targets = new RegExp(config.services[service].targets.join("|"), "i") + + if (!regexArray(service, url, config)) continue + // if (initiator) { + // console.log(initiator.host) + // if (targets.test(initiator.host)) continue + // //if (all(service, null, options, config, redirects).includes(initiator.origin) && reverse(initiator) == url) return "BYPASSTAB" + // } + + if (Object.keys(config.services[service].frontends).length > 1) { + if (type == "sub_frame" && config.services[service].embeddable && !config.services[service].frontends[options[service].frontend].embeddable) frontend = options[service].embedFrontend + else frontend = options[service].frontend + } else frontend = Object.keys(config.services[service].frontends)[0] + + if (config.services[service].frontends[frontend].instanceList) { + let instanceList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (instanceList.length === 0 && options.networkFallback) instanceList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] + if (instanceList.length === 0) return + randomInstance = utils.getRandomInstance(instanceList) + } else if (config.services[service].frontends[frontend].singleInstance) randomInstance = config.services[service].frontends[frontend].singleInstance + break + } + if (!frontend) return + + // Here is a (temperory) space for defining constants required in 2 or more switch cases. + // When possible, try have the two switch cases share all their code as done with searx and searxng. + // Do not do that when they do not share 100% of their code. + + const mapCentreRegex = /@(-?\d[0-9.]*),(-?\d[0-9.]*),(\d{1,2})[.z]/ + const dataLatLngRegex = /!3d(-?[0-9]{1,}.[0-9]{1,})!4d(-?[0-9]{1,}.[0-9]{1,})/ + const placeRegex = /\/place\/(.*)\// + function convertMapCentre() { + let [lat, lon, zoom] = [null, null, null] + if (url.pathname.match(mapCentreRegex)) { + // Set map centre if present + ;[lat, lon, zoom] = url.pathname.match(mapCentreRegex) + } else if (url.searchParams.has("center")) { + ;[lat, lon] = url.searchParams.get("center").split(",") + zoom = url.searchParams.get("zoom") ?? "17" + } + return [zoom, lon, lat] + } + + switch (frontend) { + // This is where all instance-specific code must be ran to convert the service url to one that can be understood by the frontend. + case "beatbump": + return `${randomInstance}${url.pathname}${url.search}` + .replace("/watch?v=", "/listen?id=") + .replace("/channel/", "/artist/") + .replace("/playlist?list=", "/playlist/VL") + .replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/") + "?filter=all") + case "hyperpipe": + return `${randomInstance}${url.pathname}${url.search}`.replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/")) + case "bibliogram": + const reservedPaths = ["u", "p", "privacy"] + if (url.pathname === "/" || reservedPaths.includes(url.pathname.split("/")[1])) return `${randomInstance}${url.pathname}${url.search}` + if (url.pathname.startsWith("/reel") || url.pathname.startsWith("/tv")) return `${randomInstance}/p${url.pathname.replace(/\/reel|\/tv/i, "")}${url.search}` + else return `${randomInstance}/u${url.pathname}${url.search}` // Likely a user profile, redirect to '/u/...' + case "lbryDesktop": + return url.href.replace(/^https?:\/{2}odysee\.com\//, "lbry://").replace(/:(?=[a-zA-Z0-9])/g, "#") + case "neuters": + if (url.pathname.startsWith("/article/") || url.pathname.startsWith("/pf/") || url.pathname.startsWith("/arc/") || url.pathname.startsWith("/resizer/")) return null + else if (url.pathname.endsWith("/")) return `${randomInstance}${url.pathname}` + else return `${randomInstance}${url.pathname}/` + case "searx": + case "searxng": + return `${randomInstance}/?q=${encodeURIComponent(url.searchParams.get("q"))}` + case "whoogle": + return `${randomInstance}/search?q=${encodeURIComponent(url.searchParams.get("q"))}` + case "librex": + return `${randomInstance}/search.php?q=${encodeURIComponent(url.searchParams.get("q"))}` + case "send": + return randomInstance + case "nitter": + let search = new URLSearchParams(url.search) + + search.delete("ref_src") + search.delete("ref_url") + + search = search.toString() + if (search !== "") search = `?${search}` + + if (url.host.split(".")[0] === "pbs" || url.host.split(".")[0] === "video") { + try { + const [, id, format, extra] = search.match(/(.*)\?format=(.*)&(.*)/) + const query = encodeURIComponent(`${id}.${format}?${extra}`) + return `${randomInstance}/pic${url.pathname}${query}` + } catch { + return `${randomInstance}/pic${url.pathname}${search}` + } + } + + if (url.pathname.split("/").includes("tweets")) return `${randomInstance}${url.pathname.replace("/tweets", "")}${search}` + if (url.host == "t.co") return `${randomInstance}/t.co${url.pathname}` + return `${randomInstance}${url.pathname}${search}` + case "yattee": + return url.href.replace(/^https?:\/{2}/, "yattee://") + case "freetube": + return `freetube://https://youtu.be${url.pathname}${url.search}`.replace(/watch\?v=/, "") + case "simplyTranslate": + return `${randomInstance}/${url.search}` + case "libreTranslate": + return `${randomInstance}/${url.search}` + .replace(/(?<=\/?)sl/, "source") + .replace(/(?<=&)tl/, "target") + .replace(/(?<=&)text/, "q") + case "osm": { + if (initiator && initiator.host === "earth.google.com") return + const travelModes = { + driving: "fossgis_osrm_car", + walking: "fossgis_osrm_foot", + bicycling: "fossgis_osrm_bike", + transit: "fossgis_osrm_car", // not implemented on OSM, default to car. + } + + function addressToLatLng(address) { + const xmlhttp = new XMLHttpRequest() + xmlhttp.open("GET", `https://nominatim.openstreetmap.org/search/${address}?format=json&limit=1`, false) + xmlhttp.send() + if (xmlhttp.status === 200) { + const json = JSON.parse(xmlhttp.responseText)[0] + if (json) { + console.log("json", json) + return [`${json.lat},${json.lon}`, `${json.boundingbox[2]},${json.boundingbox[1]},${json.boundingbox[3]},${json.boundingbox[0]}`] + } + } + console.info("Error: Status is " + xmlhttp.status) + } + + let mapCentre = "#" + let prefs = {} + + const mapCentreData = convertMapCentre() + if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#map=${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}` + if (url.searchParams.get("layer")) prefs.layers = osmLayers[url.searchParams.get("layer")] + + if (url.pathname.includes("/embed")) { + // Handle Google Maps Embed API + // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France + //console.log("embed life") + + let query = "" + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.has("query") + else if (url.searchParams.has("pb")) + try { + query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1] + } catch (error) { + console.error(error) + } // Unable to find map marker in URL. + + let [coords, boundingbox] = addressToLatLng(query) + prefs.bbox = boundingbox + prefs.marker = coords + prefs.layer = "mapnik" + let prefsEncoded = new URLSearchParams(prefs).toString() + return `${randomInstance}/export/embed.html?${prefsEncoded}` + } else if (url.pathname.includes("/dir")) { + // Handle Google Maps Directions + // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling + + let travMod = url.searchParams.get("travelmode") + if (url.searchParams.has("travelmode")) prefs.engine = travelModes[travMod] + + let orgVal = url.searchParams.get("origin") + let destVal = url.searchParams.get("destination") + + let org = addressToLatLng(orgVal) + let dest = addressToLatLng(destVal) + prefs.route = `${org};${dest}` + + let prefsEncoded = new URLSearchParams(prefs).toString() + return `${randomInstance}/directions?${prefsEncoded}${mapCentre}` + } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) { + // Get marker from data attribute + // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948 + //console.log("data life") + + let [, mlat, mlon] = url.pathname.match(dataLatLngRegex) + + return `${randomInstance}/search?query=${mlat}%2C${mlon}` + } else if (url.searchParams.has("ll")) { + // Get marker from ll param + // https://maps.google.com/?ll=38.882147,-76.99017 + //console.log("ll life") + + const [mlat, mlon] = url.searchParams.get("ll").split(",") + + return `${randomInstance}/search?query=${mlat}%2C${mlon}` + } else if (url.searchParams.has("viewpoint")) { + // Get marker from viewpoint param. + // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80 + //console.log("viewpoint life") + + const [mlat, mlon] = url.searchParams.get("viewpoint").split(",") + + return `${randomInstance}/search?query=${mlat}%2C${mlon}` + } else { + // Use query as search if present. + //console.log("normal life") + + let query + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.get("query") + else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1] + + let prefsEncoded = new URLSearchParams(prefs).toString() + if (query) return `${randomInstance}/search?query="${query}${mapCentre}&${prefsEncoded}` + } + + let prefsEncoded = new URLSearchParams(prefs).toString() + console.log("mapCentre", mapCentre) + console.log("prefs", prefs) + console.log("prefsEncoded", prefsEncoded) + return `${randomInstance}/${mapCentre}&${prefsEncoded}` + } + case "facil": { + if (initiator && initiator.host === "earth.google.com") return + const travelModes = { + driving: "car", + walking: "pedestrian", + bicycling: "bicycle", + transit: "car", // not implemented on Facil, default to car. + } + const mapCentreData = convertMapCentre() + let mapCentre = "#" + if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}` + + if (url.pathname.includes("/embed")) { + // Handle Google Maps Embed API + // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France + //console.log("embed life") + + let query = "" + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.has("query") + else if (url.searchParams.has("pb")) + try { + query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1] + } catch (error) { + console.error(error) + } // Unable to find map marker in URL. + + return `${randomInstance}/#q=${query}` + } else if (url.pathname.includes("/dir")) { + // Handle Google Maps Directions + // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling + + let travMod = url.searchParams.get("travelmode") + + let orgVal = url.searchParams.get("origin") + let destVal = url.searchParams.get("destination") + + return `${randomInstance}/#q=${orgVal}%20to%20${destVal}%20by%20${travelModes[travMod]}` + } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) { + // Get marker from data attribute + // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948 + //console.log("data life") + + let [, mlat, mlon] = url.pathname.match(dataLatLngRegex) + + return `${randomInstance}/#q=${mlat}%2C${mlon}` + } else if (url.searchParams.has("ll")) { + // Get marker from ll param + // https://maps.google.com/?ll=38.882147,-76.99017 + //console.log("ll life") + + const [mlat, mlon] = url.searchParams.get("ll").split(",") + + return `${randomInstance}/#q=${mlat}%2C${mlon}` + } else if (url.searchParams.has("viewpoint")) { + // Get marker from viewpoint param. + // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80 + //console.log("viewpoint life") + + const [mlat, mlon] = url.searchParams.get("viewpoint").split(",") + + return `${randomInstance}/#q=${mlat}%2C${mlon}` + } else { + // Use query as search if present. + //console.log("normal life") + + let query + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.get("query") + else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1] + + if (query) return `${randomInstance}/${mapCentre}/Mpnk/${query}` + } + } + case "wikiless": + let GETArguments = [] + if (url.search.length > 0) { + let search = url.search.substring(1) //get rid of '?' + let argstrings = search.split("&") + for (let i = 0; i < argstrings.length; i++) { + let args = argstrings[i].split("=") + GETArguments.push([args[0], args[1]]) + } + } + + let link = `${randomInstance}${url.pathname}` + let urlSplit = url.host.split(".") + if (urlSplit[0] != "wikipedia" && urlSplit[0] != "www") { + if (urlSplit[0] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"]) + else GETArguments.push(["lang", urlSplit[0]]) + if (urlSplit[1] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"]) + // wikiless doesn't have mobile view support yet + } + for (let i = 0; i < GETArguments.length; i++) link += (i == 0 ? "?" : "&") + GETArguments[i][0] + "=" + GETArguments[i][1] + return link + + case "lingva": + let params_arr = url.search.split("&") + params_arr[0] = params_arr[0].substring(1) + let params = {} + for (let i = 0; i < params_arr.length; i++) { + let pair = params_arr[i].split("=") + params[pair[0]] = pair[1] + } + if (params.sl && params.tl && params.text) { + return `${randomInstance}/${params.sl}/${params.tl}/${params.text}` + } + return randomInstance + case "breezeWiki": + let wiki = url.hostname.match(/^[a-zA-Z0-9-]+(?=\.fandom\.com)/) + if (wiki == "www" || !wiki) wiki = "" + else wiki = "/" + wiki + if (url.href.search(/Special:Search\?query/) > -1) return `${randomInstance}${wiki}${url.pathname}${url.search}`.replace(/Special:Search\?query/, "search?q").replace(/\/wiki/, "") + else return `${randomInstance}${wiki}${url.pathname}${url.search}` + case "rimgo": + if (url.href.search(/^https?:\/{2}(?:[im]\.)?stack\./) > -1) return `${randomInstance}/stack${url.pathname}${url.search}` + else return `${randomInstance}${url.pathname}${url.search}` + case "libreddit": + const subdomain = url.hostname.match(/^(?:(?:external-)?preview|i)(?=\.redd\.it)/) + if (!subdomain) return `${randomInstance}${url.pathname}${url.search}` + switch (subdomain[0]) { + case "preview": + return `${randomInstance}/preview/pre${url.pathname}${url.search}` + case "external-preview": + return `${randomInstance}/preview/external-pre${url.pathname}${url.search}` + case "i": + return `${randomInstance}/img${url.pathname}` + } + case "teddit": + if (/^(?:(?:external-)?preview|i)\.redd\.it/.test(url.hostname)) { + if (url.search == "") return `${randomInstance}${url.pathname}?teddit_proxy=${url.hostname}` + else return `${randomInstance}${url.pathname}${url.search}&teddit_proxy=${url.hostname}` + } + return `${randomInstance}${url.pathname}${url.search}` + default: + return `${randomInstance}${url.pathname}${url.search}` + } +} + +function computeService(url, returnFrontend) { + return new Promise(resolve => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + const config = JSON.parse(configData) + browser.storage.local.get(["redirects", "options"], r => { + const redirects = r.redirects + const options = r.options + for (const service in config.services) { + if (regexArray(service, url, config)) { + resolve(service) + return + } else { + for (const frontend in config.services[service].frontends) { + if (all(service, frontend, options, config, redirects).includes(utils.protocolHost(url))) { + if (returnFrontend) resolve([service, frontend, utils.protocolHost(url)]) + else resolve(service) + return + } + } + } + } + resolve() + }) + }) + }) +} + +function switchInstance(url) { + return new Promise(async resolve => { + await init() + const protocolHost = utils.protocolHost(url) + for (const service in config.services) { + if (!all(service, null, options, config, redirects).includes(protocolHost)) continue + + let instancesList + if (Object.keys(config.services[service].frontends).length == 1) { + const frontend = Object.keys(config.services[service].frontends)[0] + instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] + } else { + const frontend = options[service].frontend + instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] + } + + let oldInstance + const i = instancesList.indexOf(protocolHost) + if (i > -1) { + oldInstance = instancesList[i] + instancesList.splice(i, 1) + } + if (instancesList.length === 0) { + resolve() + return + } + const randomInstance = utils.getRandomInstance(instancesList) + const oldUrl = `${oldInstance}${url.pathname}${url.search}` + // This is to make instance switching work when the instance depends on the pathname, eg https://darmarit.org/searx + // Doesn't work because of .includes array method, not a top priotiry atm + resolve(oldUrl.replace(oldInstance, randomInstance)) + return + } + resolve() + }) +} + +function reverse(url, urlString) { + return new Promise(async resolve => { + await init() + let protocolHost + if (!urlString) protocolHost = utils.protocolHost(url) + else protocolHost = url.match(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/)[0] + for (const service in config.services) { + if (!all(service, null, options, config, redirects).includes(protocolHost)) continue + + switch (service) { + case "instagram": + case "youtube": + case "imdb": + case "imgur": + case "tiktok": + case "twitter": + case "reddit": + case "imdb": + case "reuters": + case "quora": + case "medium": + if (!urlString) resolve(config.services[service].url + url.pathname + url.search) + else resolve(url.replace(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/, config.services[service].url)) + return + default: + resolve() + return + } + } + resolve() + }) +} + +function unifyPreferences(url, tabId) { + return new Promise(async resolve => { + await init() + const protocolHost = utils.protocolHost(url) + for (const service in config.services) { + for (const frontend in config.services[service].frontends) { + if (all(service, frontend, options, config, redirects).includes(protocolHost)) { + let instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (options.networkFallback && options.network != "clearnet") instancesList.push(...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom) + + const frontendObject = config.services[service].frontends[frontend] + if ("cookies" in frontendObject.preferences) { + for (const cookie of frontendObject.preferences.cookies) { + await utils.copyCookie(url, instancesList, cookie) + } + } + if ("localstorage" in frontendObject.preferences) { + browser.storage.local.set({ tmp: [frontend, frontendObject.preferences.localstorage] }) + browser.tabs.executeScript(tabId, { + file: "/assets/javascripts/get-localstorage.js", + runAt: "document_start", + }) + for (const instance of instancesList) + browser.tabs.create({ url: instance }, tab => + browser.tabs.executeScript(tab.id, { + file: "/assets/javascripts/set-localstorage.js", + runAt: "document_start", + }) + ) + } + /* + if ("indexeddb" in frontendObject.preferences) { + } + if ("token" in frontendObject.preferences) { + } + */ + resolve(true) + return + } + } + } + }) +} + +function setRedirects(passedRedirects) { + return new Promise(resolve => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(/* [ */ "options" /* , "blacklists"] */, async r => { + let redirects = passedRedirects + let options = r.options + const config = JSON.parse(configData) + let targets = {} + for (const service in config.services) { + if (config.services[service].targets == "datajson") { + targets[service] = redirects[service] + delete redirects[service] + } + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + for (const network in config.networks) { + for (const instance of options[frontend][network].enabled) { + let i = redirects[frontend][network].indexOf(instance) + if (i < 0) options[frontend][network].enabled.splice(i, 1) + } + } + } + } + /* + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + for (const network in config.networks) { + options[frontend][network].enabled = redirects[frontend][network] + } + for (const blacklist in r.blacklists) { + for (const instance of blacklist) { + let i = options[frontend].clearnet.enabled.indexOf(instance) + if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) + } + } + } + } + */ + // The above will be implemented with https://github.com/libredirect/libredirect/issues/334 + } + for (const frontend in redirects) { + let exists = false + for (const service in config.services) if (config.services[service].frontends[frontend]) exists = true + if (!exists) delete redirects[frontend] + else for (const network in redirects[frontend]) if (!config.networks[network]) delete redirects[frontend][network] + } + browser.storage.local.set({ redirects, targets, options }, () => resolve()) + }) + }) + }) +} + +function initDefaults() { + return new Promise(resolve => { + fetch("/instances/data.json") + .then(response => response.text()) + .then(data => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(["options", "blacklists"], r => { + let redirects = JSON.parse(data) + let options = r.options + let targets = {} + let config = JSON.parse(configData) + const localstorage = {} + const latency = {} + for (const service in config.services) { + options[service] = {} + if (config.services[service].targets == "datajson") { + targets[service] = redirects[service] + delete redirects[service] + } + for (const defaultOption in config.services[service].options) options[service][defaultOption] = config.services[service].options[defaultOption] + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + options[frontend] = {} + for (const network in config.networks) { + options[frontend][network] = {} + options[frontend][network].enabled = JSON.parse(data)[frontend][network] + options[frontend][network].custom = [] + } + for (const blacklist in r.blacklists) { + for (const instance of r.blacklists[blacklist]) { + let i = options[frontend].clearnet.enabled.indexOf(instance) + if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) + } + } + } + } + } + browser.storage.local.set({ redirects, options, targets, latency, localstorage }) + resolve() + }) + }) + }) + }) +} + +function upgradeOptions() { + return new Promise(resolve => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(null, r => { + let options = r.options + let latency = {} + const config = JSON.parse(configData) + options.exceptions = r.exceptions + if (r.theme != "DEFAULT") options.theme = r.theme + options.popupServices = r.popupFrontends + let tmp = options.popupServices.indexOf("tikTok") + if (tmp > -1) { + options.popupServices.splice(tmp, 1) + options.popupServices.push("tiktok") + } + tmp = options.popupServices.indexOf("sendTarget") + if (tmp > -1) { + options.popupServices.splice(tmp, 1) + options.popupServices.push("sendFiles") + } + options.autoRedirect = r.autoRedirect + switch (r.onlyEmbeddedVideo) { + case "onlyNotEmbedded": + options.youtube.redirectType = "main_frame" + case "onlyEmbedded": + options.youtube.redirectType = "sub_frame" + case "both": + options.youtube.redirectType = "both" + } + for (const service in config.services) { + let oldService + switch (service) { + case "tiktok": + oldService = "tikTok" + break + case "sendFiles": + oldService = "sendTarget" + break + default: + oldService = service + } + options[service].enabled = !r["disable" + utils.camelCase(oldService)] + if (r[oldService + "Frontend"]) { + if (r[oldService + "Frontend"] == "yatte") options[service].frontend = "yattee" + else options[service].frontend = r[oldService + "Frontend"] + } + if (r[oldService + "RedirectType"]) options[service].redirectType = r[oldService + "RedirectType"] + if (r[oldService + "EmbedFrontend"] && (service != "youtube" || r[oldService + "EmbedFrontend"] == "invidious" || r[oldService + "EmbedFrontend"] == "piped")) + options[service].embedFrontend = r[oldService + "EmbedFrontend"] + for (const frontend in config.services[service].frontends) { + if (r[frontend + "Latency"]) latency[frontend] = r[frontend + "Latency"] + for (const network in config.networks) { + let protocol + if (network == "clearnet") protocol = "normal" + else protocol = network + if (r[frontend + utils.camelCase(protocol) + "RedirectsChecks"]) { + options[frontend][network].enabled = r[frontend + utils.camelCase(protocol) + "RedirectsChecks"] + options[frontend][network].custom = r[frontend + utils.camelCase(protocol) + "CustomRedirects"] + for (const instance of options[frontend][network].enabled) { + let i = r.redirects[frontend][network].indexOf(instance) + if (i < 0) options[frontend][network].enabled.splice(i, 1) + } + } + } + } + } + browser.storage.local.set({ options, latency }, () => resolve()) + }) + }) + }) +} + +function processUpdate() { + return new Promise(resolve => { + fetch("/instances/data.json") + .then(response => response.text()) + .then(data => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(["options", "blacklists", "targets"], r => { + let redirects = JSON.parse(data) + let options = r.options + let targets = r.targets + let config = JSON.parse(configData) + for (const service in config.services) { + if (!options[service]) options[service] = {} + if (config.services[service].targets == "datajson") { + targets[service] = redirects[service] + delete redirects[service] + } + for (const defaultOption in config.services[service].options) { + if (options[service][defaultOption] === undefined) { + options[service][defaultOption] = config.services[service].options[defaultOption] + } + } + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + if (!options[frontend]) options[frontend] = {} + for (const network in config.networks) { + if (!options[frontend][network]) { + options[frontend][network] = {} + options[frontend][network].enabled = JSON.parse(data)[frontend][network] + options[frontend][network].custom = [] + if (network == "clearnet") { + for (const blacklist in r.blacklists) { + for (const instance of r.blacklists[blacklist]) { + let i = options[frontend].clearnet.enabled.indexOf(instance) + if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) + } + } + } + } else { + for (const instance of options[frontend][network].enabled) { + let i = redirects[frontend][network].indexOf(instance) + if (i < 0) options[frontend][network].enabled.splice(i, 1) + } + } + } + } + } + } + browser.storage.local.set({ redirects, options, targets }) + resolve() + }) + }) + }) + }) +} + +// For websites that have a strict policy that would not normally allow these frontends to be embedded within the website. +function modifyContentSecurityPolicy(details) { + let isChanged = false + if (details.type == "main_frame") { + for (const header in details.responseHeaders) { + if (details.responseHeaders[header].name == "content-security-policy") { + let instancesList = [] + for (const service in config.services) { + if (config.services[service].embeddable) { + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].embeddable) { + for (const network in config.networks) { + instancesList.push(...options[frontend][network].enabled, ...options[frontend][network].custom) + } + } + } + } + } + let securityPolicyList = details.responseHeaders[header].value.split(";") + for (const i in securityPolicyList) securityPolicyList[i] = securityPolicyList[i].trim() + let newSecurity = "" + for (const item of securityPolicyList) { + if (item.trim() == "") continue + let regex = item.match(/([a-z-]{0,}) (.*)/) + if (regex == null) continue + let [, key, vals] = regex + if (key == "frame-src") vals = vals + " " + instancesList.join(" ") + newSecurity += key + " " + vals + "; " + } + + details.responseHeaders[header].value = newSecurity + isChanged = true + } + } + if (isChanged) return { responseHeaders: details.responseHeaders } + } +} + +export default { + redirect, + computeService, + switchInstance, + reverse, + unifyPreferences, + setRedirects, + initDefaults, + upgradeOptions, + processUpdate, + modifyContentSecurityPolicy, +} diff --git a/src/config/config.json b/src/config/config.json index ef598ea8..b1cbd036 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1,610 +1,610 @@ -{
- "networks": {
- "clearnet": {
- "tld": "org",
- "name": "Clearnet"
- },
- "tor": {
- "tld": "onion",
- "name": "Tor"
- },
- "i2p": {
- "tld": "i2p",
- "name": "I2P"
- },
- "loki": {
- "tld": "loki",
- "name": "Lokinet"
- }
- },
- "services": {
- "youtube": {
- "frontends": {
- "invidious": {
- "preferences": {
- "cookies": ["PREFS"],
- "localstorage": ["dark_mode"]
- },
- "name": "Invidious",
- "embeddable": true,
- "instanceList": true
- },
- "piped": {
- "preferences": {
- "localstorage": [
- "bufferGoal",
- "comments",
- "disableLBRY",
- "enabledCodecs",
- "hl",
- "homepage",
- "instance",
- "listen",
- "minimizeDescription",
- "playerAutoPlay",
- "proxyLBRY",
- "quality",
- "region",
- "selectedSkip",
- "sponsorblock",
- "theme",
- "volume",
- "watchHistory",
- "localSubscriptions"
- ]
- },
- "name": "Piped",
- "embeddable": true,
- "instanceList": true
- },
- "pipedMaterial": {
- "preferences": {
- "localstorage": ["PREFERENCES"]
- },
- "name": "Piped-Material",
- "embeddable": false,
- "instanceList": true
- },
- "cloudtube": {
- "preferences": {
- "token": "token",
- "fetchEndpoint": "/api/settings",
- "setEndpoint": "/settings"
- },
- "name": "CloudTube",
- "embeddable": false,
- "instanceList": true
- },
- "freetube": {
- "name": "FreeTube",
- "embeddable": false,
- "instanceList": false
- },
- "yattee": {
- "name": "Yattee",
- "embeddable": false,
- "instanceList": false
- }
- },
- "targets": [
- "^https?:\\/{2}(?:www\\.|m\\.|)youtube.com(\\/|$)(?!iframe_api\\/|redirect\\/)",
- "^https?:\\/{2}img\\.youtube.com\\/vi\\/.*\\/..*",
- "^https?:\\/{2}(?:i|s)\\.ytimg.com\\/vi\\/.*\\/..*",
- "^https?:\\/{2}(?:www\\.|)youtube.com\\/watch?v=..*",
- "^https?:\\/{2}youtu\\.be\\/..*",
- "^https?:\\/{2}(?:www\\.|)(youtube|youtube-nocookie)\\.com\\/embed\\/..*"
- ],
- "name": "Youtube",
- "options": {
- "enabled": true,
- "redirectType": "both",
- "frontend": "invidious",
- "embedFrontend": "invidious"
- },
- "imageType": "png",
- "embeddable": true,
- "url": "https://youtube.com"
- },
- "youtubeMusic": {
- "frontends": {
- "beatbump": {
- "preferences": {
- "localstorage": ["settings"],
- "indexeddb": "beatbump"
- },
- "name": "Beatbump",
- "instanceList": true
- },
- "hyperpipe": {
- "preferences": {
- "localstorage": ["api", "authapi", "codec", "locale", "next", "pipedapi", "quality", "theme", "vol"],
- "indexeddb": "hyperpipedb"
- },
- "name": "Hyperpipe",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}music\\.youtube\\.com(\\/|$)"],
- "name": "YT Music",
- "options": {
- "enabled": true,
- "frontend": "beatbump"
- },
- "imageType": "png",
- "embeddable": false,
- "url": "https://music.youtube.com"
- },
- "twitter": {
- "frontends": {
- "nitter": {
- "preferences": {
- "cookies": [
- "autoplayGifs",
- "bidiSupport",
- "hideBanner",
- "hidePins",
- "hideReplies",
- "hideTweetStats",
- "hlsPlayback",
- "infiniteScroll",
- "mp4Playback",
- "muteVideos",
- "proxyVideos",
- "replaceInstagram",
- "replaceReddit",
- "replaceTwitter",
- "replaceYouTube",
- "squareAvatars",
- "theme"
- ]
- },
- "name": "Nitter",
- "embeddable": true,
- "instanceList": true
- }
- },
- "targets": [
- "^https?:\\/{2}(www\\.|mobile\\.|)twitter\\.com(\\/|$)",
- "^https?:\\/{2}(pbs\\.|video\\.|)twimg\\.com(\\/|$)",
- "^https?:\\/{2}platform\\.twitter\\.com/embed(\\/|$)",
- "^https?:\\/{2}t\\.co(\\/|$)"
- ],
- "name": "Twitter",
- "options": {
- "enabled": true,
- "redirectType": "both"
- },
- "imageType": "png",
- "embeddable": true,
- "url": "https://twitter.com"
- },
- "instagram": {
- "frontends": {
- "bibliogram": {
- "preferences": {
- "token": "token",
- "fetchEndpoint": "/settings.json",
- "setEndpoint": "/applysettings"
- },
- "name": "Bibliogram",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(www\\.)?instagram\\.com\\/?(p\\/|$)"],
- "name": "Instagram",
- "options": {
- "enabled": true
- },
- "imageType": "png",
- "embeddable": false,
- "url": "https://instagram.com"
- },
- "tiktok": {
- "frontends": {
- "proxiTok": {
- "preferences": {
- "cookies": ["api-test_endpoints", "theme"]
- },
- "name": "ProxiTok",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(www\\.|)tiktok\\.com(\\/|$)"],
- "name": "TikTok",
- "options": {
- "enabled": true
- },
- "imageType": "png",
- "embeddable": false,
- "url": "https://tiktok.com"
- },
- "reddit": {
- "frontends": {
- "libreddit": {
- "preferences": {
- "cookies": ["theme", "front_page", "layout", "wide", "post_sort", "comment_sort", "show_nsfw", "autoplay_videos", "use_hls", "hide_hls_notification", "subscriptions", "filters"]
- },
- "name": "Libreddit",
- "instanceList": true
- },
- "teddit": {
- "preferences": {
- "cookies": [
- "collapse_child_comments",
- "default_comment_sort",
- "domain_instagram",
- "domain_twitter",
- "domain_youtube",
- "flairs",
- "highlight_controversial",
- "nsfw_enabled",
- "post_media_max_height",
- "prefer_frontpage",
- "show_large_gallery_images",
- "show_upvoted_percentage",
- "show_upvotes",
- "subbed_subreddits",
- "theme",
- "videos_muted"
- ]
- },
- "name": "Teddit",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(www\\.|old\\.|np\\.|new\\.|amp\\.|)reddit\\.com(?=\\/u(ser)?\\/|\\/r\\/|\\/?$)", "^https?:\\/{2}(i|(external-)?preview)\\.redd\\.it"],
- "name": "Reddit",
- "options": {
- "enabled": true,
- "frontend": "libreddit"
- },
- "imageType": "png",
- "embeddable": false,
- "url": "https://reddit.com"
- },
- "imgur": {
- "frontends": {
- "rimgo": {
- "name": "rimgo",
- "embeddable": true,
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}([im]\\.)?(stack\\.)?imgur\\.(com|io)(\\/|$)"],
- "name": "Imgur",
- "options": {
- "enabled": true,
- "redirectType": "both"
- },
- "imageType": "png",
- "embeddable": true,
- "url": "https://imgur.com"
- },
- "wikipedia": {
- "frontends": {
- "wikiless": {
- "preferences": {
- "cookies": ["theme", "default_lang"]
- },
- "name": "Wikiless",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(?:[a-z]+\\.)*wikipedia\\.org(\\/|$)"],
- "name": "Wikipedia",
- "options": {
- "enabled": false
- },
- "imageType": "svg",
- "embeddable": false,
- "url": "https://wikipedia.org"
- },
- "medium": {
- "frontends": {
- "scribe": {
- "name": "Scribe",
- "instanceList": true
- }
- },
- "targets": [
- "(?:.*\\.)*(?<!(link\\.|cdn\\-images\\-\\d+\\.))medium\\.com(\\/.*)?$",
- "^https?:\\/{2}towardsdatascience\\.com(\\/|$)",
- "^https?:\\/{2}uxdesign\\.cc(\\/|$)",
- "^https?:\\/{2}uxplanet\\.org(\\/|$)",
- "^https?:\\/{2}betterprogramming\\.pub(\\/|$)",
- "^https?:\\/{2}aninjusticemag\\.com(\\/|$)",
- "^https?:\\/{2}betterhumans\\.pub(\\/|$)",
- "^https?:\\/{2}psiloveyou\\.xyz(\\/|$)",
- "^https?:\\/{2}entrepreneurshandbook\\.co(\\/|$)",
- "^https?:\\/{2}blog\\.coinbase\\.com(\\/|$)",
- "^https?:\\/{2}levelup\\.gitconnected\\.com(\\/|$)",
- "^https?:\\/{2}javascript\\.plainenglish\\.io(\\/|$)",
- "^https?:\\/{2}blog\\.bitsrc\\.io(\\/|$)",
- "^https?:\\/{2}itnext\\.io(\\/|$)",
- "^https?:\\/{2}codeburst\\.io(\\/|$)",
- "^https?:\\/{2}infosecwriteups\\.com(\\/|$)",
- "^https?:\\/{2}blog\\.devgenius\\.io(\\/|$)",
- "^https?:\\/{2}writingcooperative\\.com(\\/|$)"
- ],
- "name": "Medium",
- "options": {
- "enabled": true
- },
- "imageType": "svgMono",
- "embeddable": false,
- "url": "https://medium.com"
- },
- "quora": {
- "frontends": {
- "quetre": {
- "preferences": {
- "localstorage": ["theme"]
- },
- "name": "Quetre",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}([a-zA-Z0-9-]+\\.)*quora\\.com(\\/|$)"],
- "name": "Quora",
- "options": {
- "enabled": true
- },
- "imageType": "png",
- "embeddable": false,
- "url": "https://quora.com"
- },
- "imdb": {
- "frontends": {
- "libremdb": {
- "preferences": {
- "localstorage": ["theme"]
- },
- "name": "libremdb",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(?:www\\.|)imdb\\.com\\/title"],
- "name": "IMDb",
- "options": {
- "enabled": true
- },
- "imageType": "svg",
- "embeddable": false,
- "url": "https://imdb.com"
- },
- "reuters": {
- "frontends": {
- "neuters": {
- "name": "Neuters",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(www\\.|)reuters\\.com(\\/|$)"],
- "name": "Reuters",
- "options": {
- "enabled": false
- },
- "imageType": "svg",
- "embeddable": false,
- "url": "https://reuters.com"
- },
- "fandom": {
- "frontends": {
- "breezeWiki": {
- "name": "BreezeWiki",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}(?:[a-zA-Z0-9-]+\\.)?fandom\\.com(?=\\/wiki|\\/?$)"],
- "name": "Fandom",
- "options": {
- "enabled": true
- },
- "imageType": "svg",
- "embeddable": false,
- "url": "https://fandom.com"
- },
- "peertube": {
- "frontends": {
- "simpleertube": {
- "name": "SimpleerTube",
- "instanceList": true
- }
- },
- "targets": "datajson",
- "name": "PeerTube",
- "options": {
- "enabled": false
- },
- "imageType": "svg",
- "embeddable": false,
- "url": "https://joinpeertube.org"
- },
- "lbry": {
- "frontends": {
- "librarian": {
- "preferences": {
- "cookies": ["nsfw", "theme"],
- "localstorage": ["autoplay", "autoplayNextVid", "collapseComments", "plyr", "sb_categories", "showRelated"]
- },
- "name": "Librarian",
- "embeddable": true,
- "instanceList": true
- },
- "lbryDesktop": {
- "name": "LBRY Desktop",
- "embeddable": false,
- "instanceList": false
- }
- },
- "targets": ["^https?:\\/{2}odysee\\.com(\\/|$)", "^https?:\\/{2}lbry\\.tv(\\/|$)"],
- "name": "LBRY",
- "options": {
- "enabled": true,
- "frontend": "librarian",
- "redirectType": "both",
- "embedFrontend": "librarian"
- },
- "imageType": "png",
- "embeddable": true,
- "url": "https://odysee.com"
- },
- "search": {
- "frontends": {
- "searx": {
- "preferences": {
- "cookies": [
- "advanced_search",
- "autocomplete",
- "categories",
- "disabled_engines",
- "disabled_plugins",
- "doi_resolver",
- "enabled_engines",
- "enabled_plugins",
- "image_proxy",
- "language",
- "locale",
- "method",
- "oscar-style",
- "results_on_new_tab",
- "safesearch",
- "theme",
- "tokens"
- ]
- },
- "name": "SearX",
- "instanceList": true
- },
- "searxng": {
- "preferences": {
- "cookies": [
- "autocomplete",
- "categories",
- "center_alignment",
- "disabled_engines",
- "disabled_plugins",
- "doi_resolver",
- "enabled_plugins",
- "enabled_engines",
- "image_proxy",
- "infinite_scroll",
- "language",
- "locale",
- "maintab",
- "method",
- "query_in_title",
- "results_on_new_tab",
- "safesearch",
- "simple_style",
- "theme",
- "tokens"
- ]
- },
- "name": "SearXNG",
- "instanceList": true
- },
- "whoogle": {
- "name": "Whoogle",
- "instanceList": true
- },
- "librex": {
- "preferences": {
- "cookies": ["bibliogram", "disable_frontends", " disable_special", "invidious", "libreddit", "nitter", "proxitok", "save", "theme", "wikiless"]
- },
- "name": "LibreX",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}search\\.libredirect\\.invalid"],
- "name": "Search",
- "options": {
- "enabled": true,
- "frontend": "searxng"
- },
- "imageType": "svgMono",
- "embeddable": false,
- "url": "https://search.libredirect.invalid"
- },
- "translate": {
- "frontends": {
- "simplyTranslate": {
- "preferences": {
- "cookies": ["from_lang", "to_lang", "tts_enabled", "use_text_fields"]
- },
- "name": "SimplyTranslate",
- "instanceList": true
- },
- "lingva": {
- "preferences": {
- "localstorage": ["isauto", "source", "target", "chakra-ui-color-mode"]
- },
- "name": "Lingva Translate",
- "instanceList": true
- },
- "libreTranslate": {
- "name": "LibreTranslate",
- "instanceList": true
- }
- },
- "targets": ["^https?:\\/{2}translate\\.google(\\.[a-z]{2,3}){1,2}\\/", "^https?:\\/{2}translate\\.libredirect\\.invalid"],
- "name": "Translate",
- "options": {
- "enabled": true,
- "frontend": "simplyTranslate"
- },
- "imageType": "svgMono",
- "embeddable": false,
- "url": "https://translate.libredirect.invalid"
- },
- "maps": {
- "frontends": {
- "facil": {
- "name": "FacilMap",
- "instanceList": true
- },
- "osm": {
- "name": "OpenStreetMap",
- "instanceList": false,
- "singleInstance": "https://www.openstreetmap.org"
- }
- },
- "targets": ["^https?:\\/{2}maps\\.libredirect\\.invalid", "^https?:\\/{2}(((www|maps)\\.)?(google\\.).*(\\/maps)|maps\\.(google\\.).*)"],
- "name": "Maps",
- "options": {
- "enabled": true,
- "frontend": "osm"
- },
- "imageType": "svgMono",
- "embeddable": false,
- "url": "https://maps.libredirect.invalid"
- },
- "sendFiles": {
- "frontends": {
- "send": {
- "name": "Send",
- "instanceList": "true"
- }
- },
- "targets": ["^https?:\\/{2}send\\.libredirect\\.invalid", "^https?:\\/{2}send\\.firefox\\.com\\/?$", "^https?:\\/{2}sendfiles\\.online\\/?$"],
- "name": "Send Files",
- "options": {
- "enabled": true
- },
- "imageType": "svgMono",
- "embeddable": false,
- "url": "https://send.libredirect.invalid"
- }
- },
- "blacklist": {
- "cloudflare": {
- "color": "red"
- },
- "authenticate": {
- "color": "orange"
- },
- "offline": {
- "color": "grey"
- }
- }
-}
+{ + "networks": { + "clearnet": { + "tld": "org", + "name": "Clearnet" + }, + "tor": { + "tld": "onion", + "name": "Tor" + }, + "i2p": { + "tld": "i2p", + "name": "I2P" + }, + "loki": { + "tld": "loki", + "name": "Lokinet" + } + }, + "services": { + "youtube": { + "frontends": { + "invidious": { + "preferences": { + "cookies": ["PREFS"], + "localstorage": ["dark_mode"] + }, + "name": "Invidious", + "embeddable": true, + "instanceList": true + }, + "piped": { + "preferences": { + "localstorage": [ + "bufferGoal", + "comments", + "disableLBRY", + "enabledCodecs", + "hl", + "homepage", + "instance", + "listen", + "minimizeDescription", + "playerAutoPlay", + "proxyLBRY", + "quality", + "region", + "selectedSkip", + "sponsorblock", + "theme", + "volume", + "watchHistory", + "localSubscriptions" + ] + }, + "name": "Piped", + "embeddable": true, + "instanceList": true + }, + "pipedMaterial": { + "preferences": { + "localstorage": ["PREFERENCES"] + }, + "name": "Piped-Material", + "embeddable": false, + "instanceList": true + }, + "cloudtube": { + "preferences": { + "token": "token", + "fetchEndpoint": "/api/settings", + "setEndpoint": "/settings" + }, + "name": "CloudTube", + "embeddable": false, + "instanceList": true + }, + "freetube": { + "name": "FreeTube", + "embeddable": false, + "instanceList": false + }, + "yattee": { + "name": "Yattee", + "embeddable": false, + "instanceList": false + } + }, + "targets": [ + "^https?:\\/{2}(?:www\\.|m\\.|)youtube.com(\\/|$)(?!iframe_api\\/|redirect\\/)", + "^https?:\\/{2}img\\.youtube.com\\/vi\\/.*\\/..*", + "^https?:\\/{2}(?:i|s)\\.ytimg.com\\/vi\\/.*\\/..*", + "^https?:\\/{2}(?:www\\.|)youtube.com\\/watch?v=..*", + "^https?:\\/{2}youtu\\.be\\/..*", + "^https?:\\/{2}(?:www\\.|)(youtube|youtube-nocookie)\\.com\\/embed\\/..*" + ], + "name": "Youtube", + "options": { + "enabled": true, + "redirectType": "both", + "frontend": "invidious", + "embedFrontend": "invidious" + }, + "imageType": "png", + "embeddable": true, + "url": "https://youtube.com" + }, + "youtubeMusic": { + "frontends": { + "beatbump": { + "preferences": { + "localstorage": ["settings"], + "indexeddb": "beatbump" + }, + "name": "Beatbump", + "instanceList": true + }, + "hyperpipe": { + "preferences": { + "localstorage": ["api", "authapi", "codec", "locale", "next", "pipedapi", "quality", "theme", "vol"], + "indexeddb": "hyperpipedb" + }, + "name": "Hyperpipe", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}music\\.youtube\\.com(\\/|$)"], + "name": "YT Music", + "options": { + "enabled": true, + "frontend": "beatbump" + }, + "imageType": "png", + "embeddable": false, + "url": "https://music.youtube.com" + }, + "twitter": { + "frontends": { + "nitter": { + "preferences": { + "cookies": [ + "autoplayGifs", + "bidiSupport", + "hideBanner", + "hidePins", + "hideReplies", + "hideTweetStats", + "hlsPlayback", + "infiniteScroll", + "mp4Playback", + "muteVideos", + "proxyVideos", + "replaceInstagram", + "replaceReddit", + "replaceTwitter", + "replaceYouTube", + "squareAvatars", + "theme" + ] + }, + "name": "Nitter", + "embeddable": true, + "instanceList": true + } + }, + "targets": [ + "^https?:\\/{2}(www\\.|mobile\\.|)twitter\\.com(\\/|$)", + "^https?:\\/{2}(pbs\\.|video\\.|)twimg\\.com(\\/|$)", + "^https?:\\/{2}platform\\.twitter\\.com/embed(\\/|$)", + "^https?:\\/{2}t\\.co(\\/|$)" + ], + "name": "Twitter", + "options": { + "enabled": true, + "redirectType": "both" + }, + "imageType": "png", + "embeddable": true, + "url": "https://twitter.com" + }, + "instagram": { + "frontends": { + "bibliogram": { + "preferences": { + "token": "token", + "fetchEndpoint": "/settings.json", + "setEndpoint": "/applysettings" + }, + "name": "Bibliogram", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(www\\.)?instagram\\.com\\/?(p\\/|$)"], + "name": "Instagram", + "options": { + "enabled": true + }, + "imageType": "png", + "embeddable": false, + "url": "https://instagram.com" + }, + "tiktok": { + "frontends": { + "proxiTok": { + "preferences": { + "cookies": ["api-test_endpoints", "theme"] + }, + "name": "ProxiTok", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(www\\.|)tiktok\\.com(\\/|$)"], + "name": "TikTok", + "options": { + "enabled": true + }, + "imageType": "png", + "embeddable": false, + "url": "https://tiktok.com" + }, + "reddit": { + "frontends": { + "libreddit": { + "preferences": { + "cookies": ["theme", "front_page", "layout", "wide", "post_sort", "comment_sort", "show_nsfw", "autoplay_videos", "use_hls", "hide_hls_notification", "subscriptions", "filters"] + }, + "name": "Libreddit", + "instanceList": true + }, + "teddit": { + "preferences": { + "cookies": [ + "collapse_child_comments", + "default_comment_sort", + "domain_instagram", + "domain_twitter", + "domain_youtube", + "flairs", + "highlight_controversial", + "nsfw_enabled", + "post_media_max_height", + "prefer_frontpage", + "show_large_gallery_images", + "show_upvoted_percentage", + "show_upvotes", + "subbed_subreddits", + "theme", + "videos_muted" + ] + }, + "name": "Teddit", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(www\\.|old\\.|np\\.|new\\.|amp\\.|)reddit\\.com(?=\\/u(ser)?\\/|\\/r\\/|\\/?$)", "^https?:\\/{2}(i|(external-)?preview)\\.redd\\.it"], + "name": "Reddit", + "options": { + "enabled": true, + "frontend": "libreddit" + }, + "imageType": "png", + "embeddable": false, + "url": "https://reddit.com" + }, + "imgur": { + "frontends": { + "rimgo": { + "name": "rimgo", + "embeddable": true, + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}([im]\\.)?(stack\\.)?imgur\\.(com|io)(\\/|$)"], + "name": "Imgur", + "options": { + "enabled": true, + "redirectType": "both" + }, + "imageType": "png", + "embeddable": true, + "url": "https://imgur.com" + }, + "wikipedia": { + "frontends": { + "wikiless": { + "preferences": { + "cookies": ["theme", "default_lang"] + }, + "name": "Wikiless", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(?:[a-z]+\\.)*wikipedia\\.org(\\/|$)"], + "name": "Wikipedia", + "options": { + "enabled": false + }, + "imageType": "svg", + "embeddable": false, + "url": "https://wikipedia.org" + }, + "medium": { + "frontends": { + "scribe": { + "name": "Scribe", + "instanceList": true + } + }, + "targets": [ + "(?:.*\\.)*(?<!(link\\.|cdn\\-images\\-\\d+\\.))medium\\.com(\\/.*)?$", + "^https?:\\/{2}towardsdatascience\\.com(\\/|$)", + "^https?:\\/{2}uxdesign\\.cc(\\/|$)", + "^https?:\\/{2}uxplanet\\.org(\\/|$)", + "^https?:\\/{2}betterprogramming\\.pub(\\/|$)", + "^https?:\\/{2}aninjusticemag\\.com(\\/|$)", + "^https?:\\/{2}betterhumans\\.pub(\\/|$)", + "^https?:\\/{2}psiloveyou\\.xyz(\\/|$)", + "^https?:\\/{2}entrepreneurshandbook\\.co(\\/|$)", + "^https?:\\/{2}blog\\.coinbase\\.com(\\/|$)", + "^https?:\\/{2}levelup\\.gitconnected\\.com(\\/|$)", + "^https?:\\/{2}javascript\\.plainenglish\\.io(\\/|$)", + "^https?:\\/{2}blog\\.bitsrc\\.io(\\/|$)", + "^https?:\\/{2}itnext\\.io(\\/|$)", + "^https?:\\/{2}codeburst\\.io(\\/|$)", + "^https?:\\/{2}infosecwriteups\\.com(\\/|$)", + "^https?:\\/{2}blog\\.devgenius\\.io(\\/|$)", + "^https?:\\/{2}writingcooperative\\.com(\\/|$)" + ], + "name": "Medium", + "options": { + "enabled": true + }, + "imageType": "svgMono", + "embeddable": false, + "url": "https://medium.com" + }, + "quora": { + "frontends": { + "quetre": { + "preferences": { + "localstorage": ["theme"] + }, + "name": "Quetre", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}([a-zA-Z0-9-]+\\.)*quora\\.com(\\/|$)"], + "name": "Quora", + "options": { + "enabled": true + }, + "imageType": "png", + "embeddable": false, + "url": "https://quora.com" + }, + "imdb": { + "frontends": { + "libremdb": { + "preferences": { + "localstorage": ["theme"] + }, + "name": "libremdb", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(?:www\\.|)imdb\\.com\\/title"], + "name": "IMDb", + "options": { + "enabled": true + }, + "imageType": "svg", + "embeddable": false, + "url": "https://imdb.com" + }, + "reuters": { + "frontends": { + "neuters": { + "name": "Neuters", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(www\\.|)reuters\\.com(\\/|$)"], + "name": "Reuters", + "options": { + "enabled": false + }, + "imageType": "svg", + "embeddable": false, + "url": "https://reuters.com" + }, + "fandom": { + "frontends": { + "breezeWiki": { + "name": "BreezeWiki", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}(?:[a-zA-Z0-9-]+\\.)?fandom\\.com(?=\\/wiki|\\/?$)"], + "name": "Fandom", + "options": { + "enabled": true + }, + "imageType": "svg", + "embeddable": false, + "url": "https://fandom.com" + }, + "peertube": { + "frontends": { + "simpleertube": { + "name": "SimpleerTube", + "instanceList": true + } + }, + "targets": "datajson", + "name": "PeerTube", + "options": { + "enabled": false + }, + "imageType": "svg", + "embeddable": false, + "url": "https://joinpeertube.org" + }, + "lbry": { + "frontends": { + "librarian": { + "preferences": { + "cookies": ["nsfw", "theme"], + "localstorage": ["autoplay", "autoplayNextVid", "collapseComments", "plyr", "sb_categories", "showRelated"] + }, + "name": "Librarian", + "embeddable": true, + "instanceList": true + }, + "lbryDesktop": { + "name": "LBRY Desktop", + "embeddable": false, + "instanceList": false + } + }, + "targets": ["^https?:\\/{2}odysee\\.com(\\/|$)", "^https?:\\/{2}lbry\\.tv(\\/|$)"], + "name": "LBRY", + "options": { + "enabled": true, + "frontend": "librarian", + "redirectType": "both", + "embedFrontend": "librarian" + }, + "imageType": "png", + "embeddable": true, + "url": "https://odysee.com" + }, + "search": { + "frontends": { + "searx": { + "preferences": { + "cookies": [ + "advanced_search", + "autocomplete", + "categories", + "disabled_engines", + "disabled_plugins", + "doi_resolver", + "enabled_engines", + "enabled_plugins", + "image_proxy", + "language", + "locale", + "method", + "oscar-style", + "results_on_new_tab", + "safesearch", + "theme", + "tokens" + ] + }, + "name": "SearX", + "instanceList": true + }, + "searxng": { + "preferences": { + "cookies": [ + "autocomplete", + "categories", + "center_alignment", + "disabled_engines", + "disabled_plugins", + "doi_resolver", + "enabled_plugins", + "enabled_engines", + "image_proxy", + "infinite_scroll", + "language", + "locale", + "maintab", + "method", + "query_in_title", + "results_on_new_tab", + "safesearch", + "simple_style", + "theme", + "tokens" + ] + }, + "name": "SearXNG", + "instanceList": true + }, + "whoogle": { + "name": "Whoogle", + "instanceList": true + }, + "librex": { + "preferences": { + "cookies": ["bibliogram", "disable_frontends", " disable_special", "invidious", "libreddit", "nitter", "proxitok", "save", "theme", "wikiless"] + }, + "name": "LibreX", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}search\\.libredirect\\.invalid"], + "name": "Search", + "options": { + "enabled": true, + "frontend": "searxng" + }, + "imageType": "svgMono", + "embeddable": false, + "url": "https://search.libredirect.invalid" + }, + "translate": { + "frontends": { + "simplyTranslate": { + "preferences": { + "cookies": ["from_lang", "to_lang", "tts_enabled", "use_text_fields"] + }, + "name": "SimplyTranslate", + "instanceList": true + }, + "lingva": { + "preferences": { + "localstorage": ["isauto", "source", "target", "chakra-ui-color-mode"] + }, + "name": "Lingva Translate", + "instanceList": true + }, + "libreTranslate": { + "name": "LibreTranslate", + "instanceList": true + } + }, + "targets": ["^https?:\\/{2}translate\\.google(\\.[a-z]{2,3}){1,2}\\/", "^https?:\\/{2}translate\\.libredirect\\.invalid"], + "name": "Translate", + "options": { + "enabled": true, + "frontend": "simplyTranslate" + }, + "imageType": "svgMono", + "embeddable": false, + "url": "https://translate.libredirect.invalid" + }, + "maps": { + "frontends": { + "facil": { + "name": "FacilMap", + "instanceList": true + }, + "osm": { + "name": "OpenStreetMap", + "instanceList": false, + "singleInstance": "https://www.openstreetmap.org" + } + }, + "targets": ["^https?:\\/{2}maps\\.libredirect\\.invalid", "^https?:\\/{2}(((www|maps)\\.)?(google\\.).*(\\/maps)|maps\\.(google\\.).*)"], + "name": "Maps", + "options": { + "enabled": true, + "frontend": "osm" + }, + "imageType": "svgMono", + "embeddable": false, + "url": "https://maps.libredirect.invalid" + }, + "sendFiles": { + "frontends": { + "send": { + "name": "Send", + "instanceList": "true" + } + }, + "targets": ["^https?:\\/{2}send\\.libredirect\\.invalid", "^https?:\\/{2}send\\.firefox\\.com\\/?$", "^https?:\\/{2}sendfiles\\.online\\/?$"], + "name": "Send Files", + "options": { + "enabled": true + }, + "imageType": "svgMono", + "embeddable": false, + "url": "https://send.libredirect.invalid" + } + }, + "blacklist": { + "cloudflare": { + "color": "red" + }, + "authenticate": { + "color": "orange" + }, + "offline": { + "color": "grey" + } + } +} diff --git a/src/instances/get_instances.py b/src/instances/get_instances.py index 9c0543ca..8a0258c0 100644 --- a/src/instances/get_instances.py +++ b/src/instances/get_instances.py @@ -12,12 +12,15 @@ import socket mightyList = {} config = {} -startRegex = r"https?:\/{2}(?:[^\s\/]+\.)+" +startRegex = r"https?:\/{2}(?:[^\s\/]+\.)*" endRegex = "(?:\/[^\s\/]+)*\/?" torRegex = startRegex + "onion" + endRegex i2pRegex = startRegex + "i2p" + endRegex lokiRegex = startRegex + "loki" + endRegex -authRegex = r"https?:\/{2}\S+:\S+@(?:[^\s\/]+\.)+[a-zA-Z0-9]+" + endRegex +authRegex = r"https?:\/{2}\S+:\S+@(?:[^\s\/]+\.)*[a-zA-Z0-9]+" + endRegex + +# 2.0 because Libredirect is currently on version 2.x.x +headers = {'User-Agent': 'Libredirect-instance-fetcher/2.0'} with open('./src/config/config.json', 'rt') as tmp: config['networks'] = json.load(tmp)['networks'] @@ -92,7 +95,8 @@ def is_cloudflare(url): instance_bin_masked = instance_bin[:mask] if cloudflare_bin_masked == instance_bin_masked: - print(url + ' is behind ' + Fore.RED + 'cloudflare' + Style.RESET_ALL) + print(url + ' is behind ' + Fore.RED + + 'cloudflare' + Style.RESET_ALL) return True return False @@ -100,11 +104,13 @@ def is_cloudflare(url): def is_authenticate(url): try: if re.match(authRegex, url): - print(url + ' requires ' + Fore.RED + 'authentication' + Style.RESET_ALL) + print(url + ' requires ' + Fore.RED + + 'authentication' + Style.RESET_ALL) return True - r = requests.get(url, timeout=5) + r = requests.get(url, timeout=5, headers=headers) if 'www-authenticate' in r.headers: - print(url + ' requires ' + Fore.RED + 'authentication' + Style.RESET_ALL) + print(url + ' requires ' + Fore.RED + + 'authentication' + Style.RESET_ALL) return True except Exception: return False @@ -113,7 +119,7 @@ def is_authenticate(url): def is_offline(url): try: - r = requests.get(url, timeout=5) + r = requests.get(url, timeout=5, headers=headers) if r.status_code >= 400: print(url + ' is ' + Fore.RED + 'offline' + Style.RESET_ALL) print("Status code") @@ -126,9 +132,12 @@ def is_offline(url): def fetchCache(frontend, name): - with open('./src/instances/data.json') as file: - mightyList[frontend] = json.load(file)[frontend] - print(Fore.YELLOW + 'Failed' + Style.RESET_ALL + ' to fetch ' + name) + try: + with open('./src/instances/data.json') as file: + mightyList[frontend] = json.load(file)[frontend] + print(Fore.YELLOW + 'Failed' + Style.RESET_ALL + ' to fetch ' + name) + except Exception: + print(Fore.RED + 'Failed' + Style.RESET_ALL + ' to get cached ' + name) def fetchFromFile(frontend, name): @@ -139,7 +148,7 @@ def fetchFromFile(frontend, name): def fetchJsonList(frontend, name, url, urlItem, jsonObject): try: - r = requests.get(url) + r = requests.get(url, headers=headers) rJson = json.loads(r.text) if jsonObject: rJson = rJson['instances'] @@ -178,7 +187,7 @@ def fetchJsonList(frontend, name, url, urlItem, jsonObject): def fetchRegexList(frontend, name, url, regex): try: - r = requests.get(url) + r = requests.get(url, headers=headers) _list = {} for network in config['networks']: _list[network] = [] @@ -205,23 +214,32 @@ def fetchRegexList(frontend, name, url, regex): def fetchTextList(frontend, name, url, prepend): try: - r = requests.get(url) - tmp = r.text.strip().split('\n') - _list = {} for network in config['networks']: _list[network] = [] - for item in tmp: - item = prepend + item - if re.search(torRegex, item): - _list['tor'].append(item) - elif re.search(i2pRegex, item): - _list['i2p'].append(item) - elif re.search(lokiRegex, item): - _list['loki'].append(item) - else: - _list['clearnet'].append(item) + if type(url) == dict: + for network in config['networks']: + if url[network] is not None: + r = requests.get(url[network], headers=headers) + tmp = r.text.strip().split('\n') + for item in tmp: + item = prepend[network] + item + _list[network].append(item) + else: + r = requests.get(url, headers=headers) + tmp = r.text.strip().split('\n') + + for item in tmp: + item = prepend + item + if re.search(torRegex, item): + _list['tor'].append(item) + elif re.search(i2pRegex, item): + _list['i2p'].append(item) + elif re.search(lokiRegex, item): + _list['loki'].append(item) + else: + _list['clearnet'].append(item) mightyList[frontend] = _list print(Fore.GREEN + 'Fetched ' + Style.RESET_ALL + name) except Exception: @@ -239,7 +257,7 @@ def invidious(): _list['tor'] = [] _list['i2p'] = [] _list['loki'] = [] - r = requests.get(url) + r = requests.get(url, headers=headers) rJson = json.loads(r.text) for instance in rJson: if instance[1]['type'] == 'https': @@ -265,13 +283,13 @@ def piped(): _list['i2p'] = [] _list['loki'] = [] r = requests.get( - 'https://raw.githubusercontent.com/wiki/TeamPiped/Piped/Instances.md') + 'https://raw.githubusercontent.com/wiki/TeamPiped/Piped/Instances.md', headers=headers) tmp = re.findall( r'(?:[^\s\/]+\.)+[a-zA-Z]+ (?:\(Official\) )?\| (https:\/{2}(?:[^\s\/]+\.)+[a-zA-Z]+) \| ', r.text) for item in tmp: try: - url = requests.get(item, timeout=5).url + url = requests.get(item, timeout=5, headers=headers).url if url.strip("/") == item: continue else: @@ -287,7 +305,8 @@ def piped(): def pipedMaterial(): - fetchRegexList('pipedMaterial', 'Piped-Material', 'https://raw.githubusercontent.com/mmjee/Piped-Material/master/README.md', r"\| (https?:\/{2}(?:\S+\.)+[a-zA-Z0-9]*) +\| Production") + fetchRegexList('pipedMaterial', 'Piped-Material', 'https://raw.githubusercontent.com/mmjee/Piped-Material/master/README.md', + r"\| (https?:\/{2}(?:\S+\.)+[a-zA-Z0-9]*) +\| Production") def cloudtube(): @@ -295,15 +314,18 @@ def cloudtube(): def proxitok(): - fetchRegexList('proxiTok', 'ProxiTok', 'https://raw.githubusercontent.com/wiki/pablouser1/ProxiTok/Public-instances.md', r"\| \[.*\]\(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)(?: \(Official\))? +\|(?:(?: [A-Z]*.*\|.*\|)|(?:$))") + fetchRegexList('proxiTok', 'ProxiTok', 'https://raw.githubusercontent.com/wiki/pablouser1/ProxiTok/Public-instances.md', + r"\| \[.*\]\(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)(?: \(Official\))? +\|(?:(?: [A-Z]*.*\|.*\|)|(?:$))") def send(): - fetchRegexList('send', 'Send', 'https://gitlab.com/timvisee/send-instances/-/raw/master/README.md', r"- ([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z0-9]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)*\|*[A-Z]{0,}") + fetchRegexList('send', 'Send', 'https://gitlab.com/timvisee/send-instances/-/raw/master/README.md', + r"- ([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z0-9]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)*\|*[A-Z]{0,}") def nitter(): - fetchRegexList('nitter', 'Nitter', 'https://raw.githubusercontent.com/wiki/zedeus/nitter/Instances.md', r"(?:(?:\| )|(?:- ))\[(?:(?:\S+\.)+[a-zA-Z0-9]+)\/?\]\((https?:\/{2}(?:\S+\.)+[a-zA-Z0-9]+)\/?\)(?:(?: (?:\((?:\S+ ?\S*)\) )? *\| [^❌]{1,4} +\|(?:(?:\n)|(?: ❌)|(?: ✅)|(?: ❓)|(?: \[)))|(?:\n))") + fetchRegexList('nitter', 'Nitter', 'https://raw.githubusercontent.com/wiki/zedeus/nitter/Instances.md', + r"(?:(?:\| )|(?:- ))\[(?:(?:\S+\.)+[a-zA-Z0-9]+)\/?\]\((https?:\/{2}(?:\S+\.)+[a-zA-Z0-9]+)\/?\)(?:(?: (?:\((?:\S+ ?\S*)\) )? *\| [^❌]{1,4} +\|(?:(?:\n)|(?: ❌)|(?: ✅)|(?: ❓)|(?: \[)))|(?:\n))") def bibliogram(): @@ -311,65 +333,53 @@ def bibliogram(): def libreddit(): - fetchJsonList('libreddit', 'Libreddit', 'https://github.com/ferritreader/libreddit-instances/raw/master/instances.json', {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, True) + fetchJsonList('libreddit', 'Libreddit', 'https://github.com/ferritreader/libreddit-instances/raw/master/instances.json', + {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, True) def teddit(): - fetchJsonList('teddit', 'Teddit', 'https://codeberg.org/teddit/teddit/raw/branch/main/instances.json', {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, False) + fetchJsonList('teddit', 'Teddit', 'https://codeberg.org/teddit/teddit/raw/branch/main/instances.json', + {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, False) def wikiless(): - fetchJsonList('wikiless', 'Wikiless', 'https://wikiless.org/instances.json', {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, False) + fetchJsonList('wikiless', 'Wikiless', 'https://wikiless.org/instances.json', + {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, False) def scribe(): - fetchJsonList('scribe', 'Scribe', 'https://git.sr.ht/~edwardloveall/scribe/blob/main/docs/instances.json', None, False) + fetchJsonList('scribe', 'Scribe', + 'https://git.sr.ht/~edwardloveall/scribe/blob/main/docs/instances.json', None, False) def quetre(): - fetchRegexList('quetre', 'Quetre', 'https://raw.githubusercontent.com/zyachel/quetre/main/README.md', r"\| \[.*\]\(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z0-9]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)*\|*[A-Z]{0,}.*\|.*\|") + fetchRegexList('quetre', 'Quetre', 'https://raw.githubusercontent.com/zyachel/quetre/main/README.md', + r"\| \[.*\]\(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z0-9]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)*\|*[A-Z]{0,}.*\|.*\|") def libremdb(): - fetchRegexList('libremdb', 'libremdb', 'https://raw.githubusercontent.com/zyachel/libremdb/main/README.md', r"\| \[.*\]\(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z0-9]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)*\|*[A-Z]{0,}.*\|.*\|") + fetchRegexList('libremdb', 'libremdb', 'https://raw.githubusercontent.com/zyachel/libremdb/main/README.md', + r"\| \[.*\]\(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,}\.[a-z0-9]{2,}\b(?:\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?)\)*\|*[A-Z]{0,}.*\|.*\|") def simpleertube(): - fetchTextList('simpleertube', 'SimpleerTube', 'https://simple-web.org/instances/simpleertube', 'https://') + fetchTextList('simpleertube', 'SimpleerTube', {'clearnet': 'https://simple-web.org/instances/simpleertube', 'tor': 'https://simple-web.org/instances/simpleertube_onion', + 'i2p': 'https://simple-web.org/instances/simpleertube_i2p', 'loki': None}, {'clearnet': 'https://', 'tor': 'http://', 'i2p': 'http://', 'loki': 'http://'}) def simplytranslate(): - r = requests.get('https://simple-web.org/instances/simplytranslate') - simplyTranslateList = {} - simplyTranslateList['clearnet'] = [] - for item in r.text.strip().split('\n'): - simplyTranslateList['clearnet'].append('https://' + item) - - r = requests.get('https://simple-web.org/instances/simplytranslate_onion') - simplyTranslateList['tor'] = [] - for item in r.text.strip().split('\n'): - simplyTranslateList['tor'].append('http://' + item) - - r = requests.get('https://simple-web.org/instances/simplytranslate_i2p') - simplyTranslateList['i2p'] = [] - for item in r.text.strip().split('\n'): - simplyTranslateList['i2p'].append('http://' + item) - - r = requests.get('https://simple-web.org/instances/simplytranslate_loki') - simplyTranslateList['loki'] = [] - for item in r.text.strip().split('\n'): - simplyTranslateList['loki'].append('http://' + item) - - mightyList['simplyTranslate'] = simplyTranslateList - print(Fore.GREEN + 'Fetched ' + Style.RESET_ALL + 'SimplyTranslate') + fetchTextList('simplyTranslate', 'SimplyTranslate', {'clearnet': 'https://simple-web.org/instances/simplytranslate', 'tor': 'https://simple-web.org/instances/simplytranslate_onion', + 'i2p': 'https://simple-web.org/instances/simplytranslate_i2p', 'loki': 'https://simple-web.org/instances/simplytranslate_loki'}, {'clearnet': 'https://', 'tor': 'http://', 'i2p': 'http://', 'loki': 'http://'}) def linvgatranslate(): - fetchJsonList('lingva', 'LingvaTranslate', 'https://raw.githubusercontent.com/TheDavidDelta/lingva-translate/main/instances.json', None, False) + fetchJsonList('lingva', 'LingvaTranslate', + 'https://raw.githubusercontent.com/TheDavidDelta/lingva-translate/main/instances.json', None, False) def searx_searxng(): - r = requests.get('https://searx.space/data/instances.json') + r = requests.get( + 'https://searx.space/data/instances.json', headers=headers) rJson = json.loads(r.text) searxList = {} searxList['clearnet'] = [] @@ -404,19 +414,23 @@ def searx_searxng(): def whoogle(): - fetchTextList('whoogle', 'Whoogle', 'https://raw.githubusercontent.com/benbusby/whoogle-search/main/misc/instances.txt', '') + fetchRegexList('whoogle', 'Whoogle', 'https://raw.githubusercontent.com/benbusby/whoogle-search/main/README.md', + r"\| \[https?:\/{2}(?:[^\s\/]+\.)*(?:[^\s\/]+\.)+[a-zA-Z0-9]+\]\((https?:\/{2}(?:[^\s\/]+\.)*(?:[^\s\/]+\.)+[a-zA-Z0-9]+)\/?\) \| ") def librex(): - fetchJsonList('librex', 'LibreX', 'https://raw.githubusercontent.com/hnhx/librex/main/instances.json', {'clearnet': 'clearnet', 'tor': 'tor', 'i2p': 'i2p', 'loki': None}, True) + fetchJsonList('librex', 'LibreX', 'https://raw.githubusercontent.com/hnhx/librex/main/instances.json', + {'clearnet': 'clearnet', 'tor': 'tor', 'i2p': 'i2p', 'loki': None}, True) def rimgo(): - fetchJsonList('rimgo', 'rimgo', 'https://codeberg.org/video-prize-ranch/rimgo/raw/branch/main/instances.json', {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, False) + fetchJsonList('rimgo', 'rimgo', 'https://codeberg.org/video-prize-ranch/rimgo/raw/branch/main/instances.json', + {'clearnet': 'url', 'tor': 'onion', 'i2p': 'i2p', 'loki': None}, False) def librarian(): - fetchJsonList('librarian', 'Librarian', 'https://codeberg.org/librarian/librarian/raw/branch/main/instances.json', 'url', True) + fetchJsonList('librarian', 'Librarian', + 'https://codeberg.org/librarian/librarian/raw/branch/main/instances.json', 'url', True) def neuters(): @@ -428,7 +442,8 @@ def beatbump(): def hyperpipe(): - fetchJsonList('hyperpipe', 'Hyperpipe', 'https://codeberg.org/Hyperpipe/pages/raw/branch/main/api/frontend.json', 'url', False) + fetchJsonList('hyperpipe', 'Hyperpipe', + 'https://codeberg.org/Hyperpipe/pages/raw/branch/main/api/frontend.json', 'url', False) def facil(): @@ -436,17 +451,19 @@ def facil(): def libreTranslate(): - fetchRegexList('libreTranslate', 'LibreTranslate', 'https://raw.githubusercontent.com/LibreTranslate/LibreTranslate/main/README.md', r"\[(?:[^\s\/]+\.)+[a-zA-Z0-9]+\]\((https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+)\/?\)\|") + fetchRegexList('libreTranslate', 'LibreTranslate', 'https://raw.githubusercontent.com/LibreTranslate/LibreTranslate/main/README.md', + r"\[(?:[^\s\/]+\.)+[a-zA-Z0-9]+\]\((https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+)\/?\)\|") def breezeWiki(): - fetchRegexList('breezeWiki', 'BreezeWiki', 'https://gitdab.com/cadence/breezewiki-docs/raw/branch/main/docs.scrbl', r"\(\"[^\n\s\r\t\f\v\"]+\" \"https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+(?:\/[^\s\/]+)*\" \"(https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+(?:\/[^\s\/]+)*)\"\)") + fetchRegexList('breezeWiki', 'BreezeWiki', 'https://gitdab.com/cadence/breezewiki-docs/raw/branch/main/docs.scrbl', + r"\(\"[^\n\s\r\t\f\v\"]+\" \"https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+(?:\/[^\s\/]+)*\" \"(https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+(?:\/[^\s\/]+)*)\"\)") def peertube(): try: r = requests.get( - 'https://instances.joinpeertube.org/api/v1/instances?start=0&count=1045&sort=-createdAt') + 'https://instances.joinpeertube.org/api/v1/instances?start=0&count=1045&sort=-createdAt', headers=headers) rJson = json.loads(r.text) myList = ['https://search.joinpeertube.org'] diff --git a/web-ext-config.js b/web-ext-config.js index dd90deca..8c691b04 100644 --- a/web-ext-config.js +++ b/web-ext-config.js @@ -5,6 +5,6 @@ module.exports = { browserConsole: true, }, build: { - overwriteDest: true - } + overwriteDest: true, + }, } |
