您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Ensures that all URLs to Lemmy instances always point to your main/home instance.
当前为
// ==UserScript== // @name Lemmy Universal Link Switcher // @namespace http://azzurite.tv/ // @license GPLv3 // @version 1.3.0 // @description Ensures that all URLs to Lemmy instances always point to your main/home instance. // @homepageURL https://gitlab.com/azzurite/lemmy-universal-link-switcher // @supportURL https://gitlab.com/azzurite/lemmy-universal-link-switcher/-/issues // @author Azzurite // @match *://*/* // @icon https://gitlab.com/azzurite/lemmy-universal-link-switcher/-/raw/main/resources/favicon.png?inline=true // @grant GM.setValue // @grant GM.getValue // @grant GM.xmlHttpRequest // @grant GM.registerMenuCommand // @connect * // @require https://unpkg.com/@popperjs/core@2 // @require https://unpkg.com/tippy.js@6 // ==/UserScript== (() => { // src/debug.js var DEBUG = false; function debug() { if (DEBUG) console.debug(`Rewriter | `, ...arguments); } function trace() { if (DEBUG === `trace`) console.debug(`Rewriter Trace | `, ...arguments); } // src/instances.js function isLemmyInstance(url) { return isInstance(INSTANCES_LEMMY, url); } function isKbinInstance(url) { return isInstance(INSTANCES_KBIN, url); } function isInstance(instances, url) { if (url.origin) { return instances.has(url.origin); } else { return false; } } trace(`Define instances sets start`); var INSTANCES_LEMMY = /* @__PURE__ */ new Set([ "http://lemmy.brdsnest.net", "http://lemmy.nijboer.chat", "https://0v0.social", "https://0xdd.org.ru", "https://1337lemmy.com", "https://158436977.xyz", "https://2dl.eu", "https://3t.au", "https://818.gallery", "https://acqrs.co.uk", "https://actuallyruben.nl", "https://adding.space", "https://ag.batlord.org", "https://agora.nop.chat", "https://aiparadise.moe", "https://akparti.xyz", "https://algebro.xyz", "https://anarch.is", "https://androiddev.network", "https://ani.social", "https://animoe.xyz", "https://apollo.town", "https://areality.social", "https://arpa.dev", "https://ascy.mooo.com", "https://aussie.zone", "https://awful.systems", "https://badatbeing.social", "https://badblocks.rocks", "https://baomi.tv", "https://baraza.africa", "https://bbs.9tail.net", "https://bbs.darkwitch.net", "https://beer.andma.la", "https://beevibes.net", "https://belfry.rip", "https://beta.programming.dev", "https://bethe.kingofdog.de", "https://bigfoot.ninja", "https://biglemmowski.win", "https://bin.pztrn.online", "https://blip.cf", "https://bluuit.org", "https://board.minimally.online", "https://bolha.forum", "https://bookwormstory.social", "https://boomer.casino", "https://borg.chat", "https://botnet.club", "https://boulder.ly", "https://bubblesthebunny.com", "https://bucketology.xyz", "https://bulletintree.com", "https://butts.international", "https://c.calciumlabs.com", "https://cake.army", "https://campfyre.nickwebster.dev", "https://casavaga.com", "https://catgirl.pub", "https://cdda.social", "https://centennialstate.social", "https://chat.maiion.com", "https://chatsubo.hiteklolife.net", "https://cigar.cx", "https://citizensgaming.com", "https://civilloquy.com", "https://clatter.eu", "https://cnvrs.net", "https://cocte.au", "https://code4lib.net", "https://codesink.io", "https://coeus.sbs", "https://coldst.one", "https://communick.news", "https://community.adiquaints.moe", "https://community.destinovate.com", "https://community.nicfab.it", "https://compuverse.uk", "https://corndog.uk", "https://corrigan.space", "https://cotix.gg", "https://crystals.rest", "https://culture0.cc", "https://cybercrime.zip", "https://czech-lemmy.eu", "https://dandroid.app", "https://darmok.xyz", "https://delete.gg", "https://demmy.co.uk", "https://demotheque.com", "https://dendarii.alaeron.com", "https://derp.foo", "https://derpzilla.net", "https://diggit.xyz", "https://digipres.cafe", "https://digitalgoblin.uk", "https://dirtbag.social", "https://discuss.as200950.com", "https://discuss.divergentparenting.space", "https://discuss.icewind.me", "https://discuss.jacen.moe", "https://discuss.ntfy.sh", "https://discuss.online", "https://discuss.petersanchez.com", "https://discuss.tchncs.de", "https://discuss.yussuv.com", "https://disflux.org", "https://distress.digital", "https://dit.reformed.social", "https://dmv.social", "https://dormi.zone", "https://drachennetz.com", "https://drak.gg", "https://drlemmy.net", "https://dubvee.org", "https://ducks.dev", "https://dumbdomain.xyz", "https://dzle125.stream", "https://einweckglas.com", "https://emmyl.com", "https://endlesstalk.org", "https://endofti.me", "https://eslemmy.es", "https://etaorion.org", "https://eventfrontier.com", "https://eviltoast.org", "https://exploding-heads.com", "https://ezekielrage.com", "https://f.jbradaric.me", "https://f.soopy.moe", "https://falconry.party", "https://fanexus.com", "https://fas.zer0hero.tk", "https://feddi.no", "https://feddit.afop.tech", "https://feddit.at", "https://feddit.cc", "https://feddit.ch", "https://feddit.cl", "https://feddit.de", "https://feddit.dk", "https://feddit.dnico.io", "https://feddit.eu", "https://feddit.fun", "https://feddit.ie", "https://feddit.it", "https://feddit.jcm.re", "https://feddit.jp", "https://feddit.nl", "https://feddit.nu", "https://feddit.nz", "https://feddit.pl", "https://feddit.ro", "https://feddit.rocks", "https://feddit.site", "https://feddit.strike23.de", "https://feddit.uk", "https://federated.community", "https://federated.pro", "https://federation.red", "https://fedii.me", "https://fedilemmy.net", "https://fedit.io", "https://feditown.com", "https://fediverse.boo", "https://fediverse.zeekzack.com", "https://feed.newt.wtf", "https://feed.timeloop.tv", "https://feedly.j-cloud.uk", "https://femboys.bar", "https://fenbushi.site", "https://fernchat.esotericmonkey.com", "https://final-assignment.zip", "https://fjdk.uk", "https://fl0w.cc", "https://forkk.me", "https://foro.markoop.org", "https://foros.fediverso.gal", "https://forum.bruvland.com", "https://forum.chanakancloud.net", "https://forum.liberatedsystems.co.uk", "https://forum.stellarcastle.net", "https://fost.hu", "https://frig.social", "https://frmsn.space", "https://gadgetro.id", "https://gathering.pikafan.de", "https://geddit.social", "https://gekinzuku.com", "https://gioia.news", "https://glasstower.nl", "https://goddess.name", "https://gonelemmy.xyz", "https://goon.vip", "https://granitestate.social", "https://greg.city", "https://group.lt", "https://grumpf.horcrux.dev", "https://hackertalks.com", "https://hakbox.social", "https://halubilo.social", "https://hammerdown.0fucks.nl", "https://hamro.world", "https://happysl.app", "https://harfang.social", "https://heckoverflow.net", "https://hive.atlanten.se", "https://hizeh.com", "https://hobbit.world", "https://hoboninjachicken.com", "https://hoihoi.superboi.eu.org", "https://hoodratshit.org", "https://hq.unit520.wtf", "https://hungryhippo.net", "https://hyperfair.link", "https://i.d0ntknow.me", "https://iceorchid.net", "https://ihax0r.com", "https://info.prou.be", "https://infosec.pub", "https://insane.dev", "https://invariant-marxism.red", "https://iusearchlinux.fyi", "https://jamie.moe", "https://jemmy.jeena.net", "https://jlai.lu", "https://just.vibin.wtf", "https://jzbr.uk", "https://ka.tet42.org", "https://keeb.lol", "https://kerala.party", "https://keylog.zip", "https://kleptonix.com", "https://klingon.nl", "https://krabb.org", "https://kulupu.duckdns.org", "https://kutsuya.dev", "https://kuu.kohana.fi", "https://kwarr.com", "https://kyu.de", "https://l.1in1.net", "https://l.60228.dev", "https://l.7rg1nt.moe", "https://l.alexdevs.me", "https://l.antiope.link", "https://l.biendeo.com", "https://l.buckodr.ink", "https://l.bxy.sh", "https://l.cackl.io", "https://l.clearbackblast.com", "https://l.dusty-radio.com", "https://l.henlo.fi", "https://l.jugregator.org", "https://l.lakes.com.au", "https://l.matestmc.ru", "https://l.mathers.fr", "https://l.mchome.net", "https://l.nulltext.org", "https://l.plabs.social", "https://l.rickebo.com", "https://l.roofo.cc", "https://l.sw0.com", "https://l.tta.wtf", "https://l.tunahan.social", "https://l.twos.dev", "https://l.vidja.social", "https://l3mmy.com", "https://laguna.chat", "https://latte.isnot.coffee", "https://le-me.xyz", "https://le.fduck.net", "https://le.mnau.xyz", "https://le.weme.wtf", "https://leaf.dance", "https://leby.dev", "https://leddit.danmark.party", "https://leddit.social", "https://lef.li", "https://lem.agoomem.xyz", "https://lem.amd.im", "https://lem.clinicians-exchange.org", "https://lem.cochrun.xyz", "https://lem.elbullazul.com", "https://lem.free.as", "https://lem.lyk.pw", "https://lem.monster", "https://lem.nimmog.uk", "https://lem.ph3j.com", "https://lem.qinvoy.com", "https://lem.serkozh.me", "https://lem.simple-gear.com", "https://lem.trashbrain.org", "https://lem.ur-mom.gay", "https://lemdit.com", "https://lemdro.id", "https://lemellem.dasonic.xyz", "https://leminal.space", "https://lemitar.meta1203.com", "https://lemiverse.xyz", "https://lemm.ee", "https://lemm.live", "https://lemmerz.org", "https://lemmi.social", "https://lemmie.be", "https://lemming.quest", "https://lemmings.sopelj.ca", "https://lemmings.world", "https://lemmit.online", "https://lemmit.xyz", "https://lemmity.com", "https://lemmonade.marbledfennec.net", "https://lemmony.click", "https://lemmus.org", "https://lemmy.01110100.xyz", "https://lemmy.0x3d.uk", "https://lemmy.1204.org", "https://lemmy.2kd.de", "https://lemmy.4d2.org", "https://lemmy.6px.eu", "https://lemmy.86thumbs.net", "https://lemmy.absolutesix.com", "https://lemmy.adambowl.es", "https://lemmy.aerir.xyz", "https://lemmy.aguiarvieira.pt", "https://lemmy.ahall.se", "https://lemmy.al", "https://lemmy.albertoluna.es", "https://lemmy.alexware.systems", "https://lemmy.amxl.com", "https://lemmy.ananace.dev", "https://lemmy.animal-machine.com", "https://lemmy.anonion.social", "https://lemmy.antemeridiem.xyz", "https://lemmy.antisocial.ly", "https://lemmy.anvor.li", "https://lemmy.anymore.nl", "https://lemmy.arpatubes.net", "https://lemmy.arun.cloud", "https://lemmy.asc6.org", "https://lemmy.atay.dev", "https://lemmy.atheos.org", "https://lemmy.aucubin.de", "https://lemmy.austinite.online", "https://lemmy.austinvaness.com", "https://lemmy.avata.social", "https://lemmy.azamserver.com", "https://lemmy.bapp.run", "https://lemmy.basedcount.com", "https://lemmy.baswag.de", "https://lemmy.bayern", "https://lemmy.beckmeyer.us", "https://lemmy.beebl.es", "https://lemmy.beep.computer", "https://lemmy.belegost.net", "https://lemmy.beru.co", "https://lemmy.beyondcombustion.net", "https://lemmy.bezzie.world", "https://lemmy.bg", "https://lemmy.bigsecretwebsite.net", "https://lemmy.billiam.net", "https://lemmy.bismith.net", "https://lemmy.bit-refined.eu", "https://lemmy.bitgoblin.tech", "https://lemmy.biz", "https://lemmy.blackeco.com", "https://lemmy.blad.is", "https://lemmy.blahaj.zone", "https://lemmy.bleh.au", "https://lemmy.bloodmoon-network.de", "https://lemmy.blue", "https://lemmy.blugatch.tube", "https://lemmy.bmck.au", "https://lemmy.bond", "https://lemmy.borlax.com", "https://lemmy.bothhands.ca", "https://lemmy.bp99.eu", "https://lemmy.brad.ee", "https://lemmy.brainiac.world", "https://lemmy.brief.guru", "https://lemmy.bringdaruck.us", "https://lemmy.browntown.dev", "https://lemmy.brynstuff.co.uk", "https://lemmy.buller.cc", "https://lemmy.bulwarkob.com", "https://lemmy.buzz", "https://lemmy.byteunion.com", "https://lemmy.ca", "https://lemmy.cablepick.net", "https://lemmy.cafe", "https://lemmy.calebmharper.com", "https://lemmy.calvss.com", "https://lemmy.capebreton.social", "https://lemmy.captainapathetic.cfd", "https://lemmy.catasaur.xyz", "https://lemmy.catattack.win", "https://lemmy.catgirl.biz", "https://lemmy.cb12.net", "https://lemmy.cfras.net", "https://lemmy.ch3n2k.com", "https://lemmy.chatterchannel.net", "https://lemmy.chatterverse.social", "https://lemmy.chiisana.net", "https://lemmy.chromozone.dev", "https://lemmy.ciechom.eu", "https://lemmy.cloud.aboutcher.co.uk", "https://lemmy.cloudaf.site", "https://lemmy.cloudhub.social", "https://lemmy.cloudsecurityofficehours.com", "https://lemmy.clueware.org", "https://lemmy.cnschn.com", "https://lemmy.coeus.icu", "https://lemmy.cogindo.net", "https://lemmy.comfysnug.space", "https://lemmy.commodore.social", "https://lemmy.computing.zone", "https://lemmy.conorab.com", "https://lemmy.cook.gg", "https://lemmy.cornspace.space", "https://lemmy.crafty-codes.ch", "https://lemmy.criticalbasics.xyz", "https://lemmy.croc.pw", "https://lemmy.cronocide.net", "https://lemmy.cultimean.group", "https://lemmy.cwagner.me", "https://lemmy.darmstadt.social", "https://lemmy.darvit.nl", "https://lemmy.datatekniker.dev", "https://lemmy.davidbuckley.ca", "https://lemmy.davidfreina.at", "https://lemmy.davidzeiger.net", "https://lemmy.dayl.in", "https://lemmy.dbzer0.com", "https://lemmy.dcrich.net", "https://lemmy.death916.xyz", "https://lemmy.decronym.xyz", "https://lemmy.deepspace.gay", "https://lemmy.deev.io", "https://lemmy.deltaa.xyz", "https://lemmy.demonoftheday.eu", "https://lemmy.design", "https://lemmy.devils.house", "https://lemmy.digital-alchemy.app", "https://lemmy.digitalcharon.in", "https://lemmy.digitalfall.net", "https://lemmy.dlgreen.com", "https://lemmy.dnet.social", "https://lemmy.doesnotexist.club", "https://lemmy.dogboy.xyz", "https://lemmy.dominikoso.me", "https://lemmy.donmcgin.com", "https://lemmy.doomeer.com", "https://lemmy.dormedas.com", "https://lemmy.dosh.dk", "https://lemmy.douwes.co.uk", "https://lemmy.drewk.org", "https://lemmy.dudeami.win", "https://lemmy.dupper.net", "https://lemmy.dws.rip", "https://lemmy.dynatron.me", "https://lemmy.easfrq.live", "https://lemmy.eatsleepcode.ca", "https://lemmy.ecliptik.com", "https://lemmy.eco.br", "https://lemmy.edgarchirivella.com", "https://lemmy.edgeburnmedia.com", "https://lemmy.einval.net", "https://lemmy.eldarerathis.com", "https://lemmy.elest.io", "https://lemmy.emopolarbear.com", "https://lemmy.enchanted.social", "https://lemmy.engineermantra.dedyn.io", "https://lemmy.escapebigtech.info", "https://lemmy.estrogen.plus", "https://lemmy.eus", "https://lemmy.everla.st", "https://lemmy.evzg.se", "https://lemmy.fail", "https://lemmy.fait.ch", "https://lemmy.fanboys.xyz", "https://lemmy.fancywhale.ca", "https://lemmy.fap.ie", "https://lemmy.fbsweb.de", "https://lemmy.fdr8.us", "https://lemmy.federa.net", "https://lemmy.federate.cc", "https://lemmy.fedi.bub.org", "https://lemmy.fedihub.social", "https://lemmy.fediverse.jp", "https://lemmy.fgilcc.eu", "https://lemmy.film", "https://lemmy.finance", "https://lemmy.flauschbereich.de", "https://lemmy.flight-crew.org", "https://lemmy.flock.house", "https://lemmy.fluffyb.net", "https://lemmy.flugratte.dev", "https://lemmy.foldling.org", "https://lemmy.fosshost.com", "https://lemmy.fourbees.net", "https://lemmy.foxden.party", "https://lemmy.freddeliv.me", "https://lemmy.fredhs.net", "https://lemmy.fribyte.no", "https://lemmy.friheter.com", "https://lemmy.fromshado.ws", "https://lemmy.frozeninferno.xyz", "https://lemmy.froztbyte.dev", "https://lemmy.fucs.io", "https://lemmy.funami.tech", "https://lemmy.funkyfish.cool", "https://lemmy.funkylab.xyz", "https://lemmy.fwgx.uk", "https://lemmy.fyi", "https://lemmy.g191919.com", "https://lemmy.g97.top", "https://lemmy.game-files.net", "https://lemmy.gamingnight.uy", "https://lemmy.gar.dev", "https://lemmy.gareth.computer", "https://lemmy.geekforbes.com", "https://lemmy.genkmc.de", "https://lemmy.giggly.de", "https://lemmy.gimmin.com", "https://lemmy.gjz010.com", "https://lemmy.glasgow.social", "https://lemmy.globe.pub", "https://lemmy.gmprojects.pro", "https://lemmy.goblackcat.com", "https://lemmy.graphics", "https://lemmy.graz.social", "https://lemmy.grenicmars.xyz", "https://lemmy.grimace.life", "https://lemmy.gross.hosting", "https://lemmy.grygon.com", "https://lemmy.grys.it", "https://lemmy.gsp8181.co.uk", "https://lemmy.gtfo.social", "https://lemmy.gwa.app", "https://lemmy.hackerbots.net", "https://lemmy.hacktheplanet.be", "https://lemmy.haigner.me", "https://lemmy.hakutaku.org", "https://lemmy.halfbro.xyz", "https://lemmy.hamrick.xyz", "https://lemmy.hansooloo.com", "https://lemmy.havocperil.uk", "https://lemmy.helios42.de", "https://lemmy.hellwhore.com", "https://lemmy.help", "https://lemmy.helvetet.eu", "https://lemmy.heorot.uk", "https://lemmy.hobbyhorse.dev", "https://lemmy.hogru.ch", "https://lemmy.holmosapien.com", "https://lemmy.home.titey.net", "https://lemmy.horwood.cloud", "https://lemmy.hoyle.me.uk", "https://lemmy.hqueue.dev", "https://lemmy.hugovr.dev", "https://lemmy.icyserver.eu", "https://lemmy.id", "https://lemmy.ilwwbs.com", "https://lemmy.imihov.com", "https://lemmy.inbutts.lol", "https://lemmy.insley.cloud", "https://lemmy.intai.tech", "https://lemmy.iqecke.de", "https://lemmy.irrealis.net", "https://lemmy.iswhereits.at", "https://lemmy.itsallbadsyntax.com", "https://lemmy.iwentto.science", "https://lemmy.iys.io", "https://lemmy.jacaranda.club", "https://lemmy.jamesj999.co.uk", "https://lemmy.jasondove.me", "https://lemmy.javant.xyz", "https://lemmy.jaytaggart.com", "https://lemmy.jcaks.net", "https://lemmy.jengo.ca", "https://lemmy.jerick.xyz", "https://lemmy.jgholistic.com", "https://lemmy.jigoku.us.to", "https://lemmy.jlh.name", "https://lemmy.jmatyka.com", "https://lemmy.jmtr.org", "https://lemmy.jnks.xyz", "https://lemmy.johnnei.org", "https://lemmy.johnscloud.net", "https://lemmy.jonaharagon.net", "https://lemmy.jpaulus.io", "https://lemmy.jpiolho.com", "https://lemmy.jsnordmann.de", "https://lemmy.jstsmthrgk.eu", "https://lemmy.jtmn.dev", "https://lemmy.juggler.jp", "https://lemmy.justin.rs", "https://lemmy.k6qw.com", "https://lemmy.kabus.ovh", "https://lemmy.kaelig.ovh", "https://lemmy.kagura.eu", "https://lemmy.karaolidis.com", "https://lemmy.kazsi.hu", "https://lemmy.kde.social", "https://lemmy.kensand.net", "https://lemmy.keychat.org", "https://lemmy.khorne.me", "https://lemmy.kiberness.xyz", "https://lemmy.kilo666.com", "https://lemmy.kitsuna.net", "https://lemmy.kizaing.ca", "https://lemmy.klaybox.xyz", "https://lemmy.kmoneyserver.com", "https://lemmy.kodemystic.dev", "https://lemmy.korgen.xyz", "https://lemmy.kosapps.com", "https://lemmy.koski.co", "https://lemmy.kozow.com", "https://lemmy.krantz.social", "https://lemmy.krobbel.xyz", "https://lemmy.kryo.ooo", "https://lemmy.kussi.me", "https://lemmy.kutara.io", "https://lemmy.kuub.io", "https://lemmy.kwain.net", "https://lemmy.kweb.ovh", "https://lemmy.kya.moe", "https://lemmy.l00p.org", "https://lemmy.l0nax.org", "https://lemmy.lacaveatonton.ovh", "https://lemmy.lantian.pub", "https://lemmy.legally-berlin.de", "https://lemmy.lemist.de", "https://lemmy.lesage.lol", "https://lemmy.letthewookiee.win", "https://lemmy.lettucegarden.net", "https://lemmy.libreprime.io", "https://lemmy.lif.ovh", "https://lemmy.linden.social", "https://lemmy.link", "https://lemmy.linuxuserspace.show", "https://lemmy.littleberrylodge.co.uk", "https://lemmy.lmkra.eu", "https://lemmy.loomy.li", "https://lemmy.loon.cc", "https://lemmy.loungerat.io", "https://lemmy.loutsenhizer.com", "https://lemmy.lt", "https://lemmy.lucitt.social", "https://lemmy.luke-duncan.com", "https://lemmy.lundgrensjostrom.com", "https://lemmy.lylapol.com", "https://lemmy.m1k.cloud", "https://lemmy.macaddict89.me", "https://lemmy.mackners.com", "https://lemmy.magnor.ovh", "https://lemmy.mags.ai", "https://lemmy.maisputain.ovh", "https://lemmy.majorshouse.com", "https://lemmy.mambastretch.com", "https://lemmy.management", "https://lemmy.maples.dev", "https://lemmy.mararead.com", "https://lemmy.mariusdavid.fr", "https://lemmy.marud.fr", "https://lemmy.masip.cat", "https://lemmy.masto.community", "https://lemmy.matoking.com", "https://lemmy.mats.ooo", "https://lemmy.matthe815.dev", "https://lemmy.max-p.me", "https://lemmy.mayes.io", "https://lemmy.mbirth.uk", "https://lemmy.mbl.social", "https://lemmy.megumin.org", "https://lemmy.meissners.me", "https://lemmy.memmex.de", "https://lemmy.menf.in", "https://lemmy.mengsk.org", "https://lemmy.menos.gotdns.org", "https://lemmy.meowchat.xyz", "https://lemmy.meridiangrp.co.uk", "https://lemmy.miichelle.moe", "https://lemmy.mildgrim.com", "https://lemmy.millennials.social", "https://lemmy.minecloud.ro", "https://lemmy.minie4.de", "https://lemmy.minigubben.se", "https://lemmy.minionflo.net", "https://lemmy.minji.xyz", "https://lemmy.mitchday.com", "https://lemmy.ml", "https://lemmy.mlaga97.space", "https://lemmy.mlsn.fr", "https://lemmy.mods4ever.com", "https://lemmy.modshiftx.com", "https://lemmy.moocloud.party", "https://lemmy.moonling.nl", "https://lemmy.mooo.com", "https://lemmy.moorefam.net", "https://lemmy.morrisherd.com", "https://lemmy.mrm.one", "https://lemmy.mrmonkey.xyz", "https://lemmy.mrrl.me", "https://lemmy.msinfo32.uk", "https://lemmy.multivers.cc", "https://lemmy.mumbled.xyz", "https://lemmy.munsell.io", "https://lemmy.mws.rocks", "https://lemmy.mxh.nu", "https://lemmy.my.id", "https://lemmy.myserv.one", "https://lemmy.myspamtrap.com", "https://lemmy.mywire.xyz", "https://lemmy.naga.sh", "https://lemmy.name", "https://lemmy.nathaneaston.com", "https://lemmy.nauk.io", "https://lemmy.nbsp.one", "https://lemmy.neeley.cloud", "https://lemmy.neoluxcommunications.com", "https://lemmy.nerdcore.social", "https://lemmy.nexus", "https://lemmy.nikore.net", "https://lemmy.nine-hells.net", "https://lemmy.ninja", "https://lemmy.nix-community.org", "https://lemmy.nofacade.xyz", "https://lemmy.noogs.me", "https://lemmy.nope.foo", "https://lemmy.nope.ly", "https://lemmy.nopro.be", "https://lemmy.nowsci.com", "https://lemmy.nrd.li", "https://lemmy.nrgup.net", "https://lemmy.nz", "https://lemmy.obrell.se", "https://lemmy.oldtr.uk", "https://lemmy.omat.nl", "https://lemmy.one", "https://lemmy.onlylans.io", "https://lemmy.orefice.win", "https://lemmy.org.il", "https://lemmy.otakufarms.com", "https://lemmy.ourdoma.in", "https://lemmy.oursphere.space", "https://lemmy.over-world.org", "https://lemmy.p3nguin.org", "https://lemmy.paesserver.de", "https://lemmy.parastor.net", "https://lemmy.pastwind.top", "https://lemmy.pathoris.de", "https://lemmy.paulstevens.org", "https://lemmy.pcft.eu", "https://lemmy.pec0ra.ch", "https://lemmy.pen15.club", "https://lemmy.perthchat.org", "https://lemmy.phiresky.xyz", "https://lemmy.phoenix591.com", "https://lemmy.physfluids.fr", "https://lemmy.pierre-couy.fr", "https://lemmy.pineapplenest.com", "https://lemmy.pipe01.net", "https://lemmy.piperservers.net", "https://lemmy.pipipopo.pl", "https://lemmy.piracy.guide", "https://lemmy.pit.ninja", "https://lemmy.plasmatrap.com", "https://lemmy.podycust.co.uk", "https://lemmy.potatoe.ca", "https://lemmy.poundncashdown.com", "https://lemmy.ppl.town", "https://lemmy.pro", "https://lemmy.probabilitydegeneration.xyz", "https://lemmy.procrastinati.org", "https://lemmy.pryst.de", "https://lemmy.pt", "https://lemmy.pub", "https://lemmy.public.ismaelrh.com", "https://lemmy.pussthecat.org", "https://lemmy.pwarde.nl", "https://lemmy.pwzle.com", "https://lemmy.quad442.com", "https://lemmy.r4r3.me", "https://lemmy.radio", "https://lemmy.ramble.moe", "https://lemmy.ramram.ink", "https://lemmy.rat.academy", "https://lemmy.ravc.tech", "https://lemmy.reckless.dev", "https://lemmy.recursed.net", "https://lemmy.reddeth.com", "https://lemmy.redkrieg.com", "https://lemmy.rekcon.io", "https://lemmy.remotelab.uk", "https://lemmy.remoteplay.im", "https://lemmy.rentadrunk.org", "https://lemmy.rhetro.de", "https://lemmy.rhymelikedi.me", "https://lemmy.richardkramer.de", "https://lemmy.riichi.me", "https://lemmy.robotra.sh", "https://lemmy.rollenspiel.monster", "https://lemmy.room409.xyz", "https://lemmy.rubenernst.dev", "https://lemmy.run", "https://lemmy.ruud.je", "https://lemmy.s9m.xyz", "https://lemmy.safe-internet.org", "https://lemmy.saik0.com", "https://lemmy.sakshamar.in", "https://lemmy.samad.one", "https://lemmy.sarcasticdeveloper.com", "https://lemmy.sascamooch.com", "https://lemmy.sbs", "https://lemmy.scam-mail.me", "https://lemmy.schlunker.com", "https://lemmy.schoenwolf-schroeder.com", "https://lemmy.schuerz.at", "https://lemmy.scootaloo.pl", "https://lemmy.scottlabs.io", "https://lemmy.scurry.io", "https://lemmy.sdf.org", "https://lemmy.sdfcn.org", "https://lemmy.sdfeu.org", "https://lemmy.sdfjp.org", "https://lemmy.sebbem.se", "https://lemmy.secondpartysoftware.com", "https://lemmy.sedimentarymountains.com", "https://lemmy.seifert.id", "https://lemmy.self-host.site", "https://lemmy.self-hosted.site", "https://lemmy.selfip.org", "https://lemmy.servarr.com", "https://lemmy.server.fifthdread.com", "https://lemmy.serverfail.party", "https://lemmy.setzman.synology.me", "https://lemmy.sherpahat.com", "https://lemmy.sherry.moe", "https://lemmy.shihaam.me", "https://lemmy.shinomoroll.net", "https://lemmy.shiny-task.com", "https://lemmy.shoshal.com", "https://lemmy.shtuf.eu", "https://lemmy.shwizard.chat", "https://lemmy.sidh.bzh", "https://lemmy.sietch.online", "https://lemmy.sikorski.cloud", "https://lemmy.silkky.dev", "https://lemmy.simpl.website", "https://lemmy.skyjake.fi", "https://lemmy.slyfabi.de", "https://lemmy.smeargle.fans", "https://lemmy.smoothbrain.org", "https://lemmy.snobvio.us", "https://lemmy.snoot.tube", "https://lemmy.socdojo.com", "https://lemmy.something.link", "https://lemmy.soontm.de", "https://lemmy.spacestation14.com", "https://lemmy.srcfiles.zip", "https://lemmy.srv.eco", "https://lemmy.srv0.lol", "https://lemmy.ssba.com", "https://lemmy.st", "https://lemmy.stardust.wtf", "https://lemmy.stark-enterprise.net", "https://lemmy.starless.one", "https://lemmy.starmade.de", "https://lemmy.stellarvortex.com", "https://lemmy.sthorne.dev", "https://lemmy.stonansh.org", "https://lemmy.stormlight.space", "https://lemmy.storo.social", "https://lemmy.stuart.fun", "https://lemmy.studio", "https://lemmy.suchmeme.nl", "https://lemmy.sumuun.net", "https://lemmy.svc.vesey.tech", "https://lemmy.sveri.de", "https://lemmy.sweevo.net", "https://lemmy.syntrofos.xyz", "https://lemmy.syrasu.com", "https://lemmy.sysctl.io", "https://lemmy.tabbynet.com", "https://lemmy.talung.org", "https://lemmy.tancomps.net", "https://lemmy.tangresh.ch", "https://lemmy.tanktrace.de", "https://lemmy.tario.org", "https://lemmy.tarsis.org", "https://lemmy.teaisatfour.com", "https://lemmy.tealshark.net", "https://lemmy.tebro.fi", "https://lemmy.techhaven.io", "https://lemmy.technosorcery.net", "https://lemmy.techstache.com", "https://lemmy.tedomum.net", "https://lemmy.telaax.com", "https://lemmy.temporus.me", "https://lemmy.tf", "https://lemmy.tgxn.net", "https://lemmy.thanatos.at", "https://lemmy.the-burrow.com", "https://lemmy.the-goblin.net", "https://lemmy.thebias.nl", "https://lemmy.thecpe.xyz", "https://lemmy.thegoodoldinternet.com", "https://lemmy.theia.cafe", "https://lemmy.themainframe.org", "https://lemmy.thenullcore.com", "https://lemmy.theonecurly.page", "https://lemmy.thepixelproject.com", "https://lemmy.thesanewriter.com", "https://lemmy.thias.xyz", "https://lemmy.tillicumnet.com", "https://lemmy.timon.sh", "https://lemmy.timwaterhouse.com", "https://lemmy.tobyvin.dev", "https://lemmy.today", "https://lemmy.todayyoutomorrow.me", "https://lemmy.toldi.eu", "https://lemmy.toot.pt", "https://lemmy.tostiman.com", "https://lemmy.towards.vision", "https://lemmy.tr00st.co.uk", "https://lemmy.trevor.coffee", "https://lemmy.trippy.pizza", "https://lemmy.trizz.io", "https://lemmy.trojaner.dev", "https://lemmy.trond.dev", "https://lemmy.tsyesika.se", "https://lemmy.tuxprint.com", "https://lemmy.twilightparadox.com", "https://lemmy.ubergeek77.chat", "https://lemmy.uhhoh.com", "https://lemmy.umainfo.live", "https://lemmy.umbrexus.com", "https://lemmy.unboiled.info", "https://lemmy.unfiltered.social", "https://lemmy.uninsane.org", "https://lemmy.unknownsys.com", "https://lemmy.urbanhost.top", "https://lemmy.utopify.org", "https://lemmy.utveckla.re", "https://lemmy.va-11-hall-a.cafe", "https://lemmy.vegan.dev", "https://lemmy.vepta.org", "https://lemmy.villa-straylight.social", "https://lemmy.vinodjam.com", "https://lemmy.virtim.dev", "https://lemmy.vollol.de", "https://lemmy.vonbergcompany.de", "https://lemmy.vrchat-dev.tech", "https://lemmy.vyizis.tech", "https://lemmy.w9r.de", "https://lemmy.website", "https://lemmy.weiser.social", "https://lemmy.weomucat.com", "https://lemmy.whynotdrs.org", "https://lemmy.wizjenkins.com", "https://lemmy.works", "https://lemmy.world", "https://lemmy.wraithsquadrongaming.com", "https://lemmy.wtf", "https://lemmy.wty.dk", "https://lemmy.wutbee.com", "https://lemmy.wxbu.de", "https://lemmy.wyattsmith.org", "https://lemmy.x3i.tech", "https://lemmy.xabis.xyz", "https://lemmy.xcoolgroup.com", "https://lemmy.xe.net.nz", "https://lemmy.xeviousx.eu", "https://lemmy.xoynq.com", "https://lemmy.xylight.dev", "https://lemmy.yakshed.org", "https://lemmy.z0r.co", "https://lemmy.zelinsky.dev", "https://lemmy.zephyrix.com", "https://lemmy.zerconian.com", "https://lemmy.zhukov.al", "https://lemmy.zip", "https://lemmy.zone", "https://lemmy.zroot.org", "https://lemmy.zwanenburg.info", "https://lemmy2.addictmud.org", "https://lemmy4lemmy.com", "https://lemmyadmin.site", "https://lemmybe.com", "https://lemmybedan.com", "https://lemmydeals.com", "https://lemmyf.uk", "https://lemmyfi.com", "https://lemmyfly.org", "https://lemmygrad.ml", "https://lemmyhub.com", "https://lemmyis.fun", "https://lemmyjapan.com", "https://lemmyland.com", "https://lemmymemes.com", "https://lemmymirage.net", "https://lemmyngton.au", "https://lemmynsfw.com", "https://lemmyonline.com", "https://lemmyrs.org", "https://lemmys.hivemind.at", "https://lemmyunchained.net", "https://lemmyverse.nl", "https://lemmyverse.org", "https://lemmywinks.com", "https://lemmywinks.net", "https://lemmywinks.xyz", "https://lemnt.telaax.com", "https://lemonine.hominine.xyz", "https://lems.app", "https://lemthony.com", "https://lemuria.es", "https://lemux.minnix.dev", "https://lemy.lol", "https://lemy.nl", "https://lib.lgbt", "https://libreauto.app", "https://liminal.southfox.me", "https://link.fossdle.org", "https://linkage.ds8.zone", "https://linkopath.com", "https://links.dartboard.social", "https://links.expressional.social", "https://links.hackliberty.org", "https://links.lowsec.club", "https://links.mrraph.photo", "https://links.rebel.ar", "https://links.rocks", "https://links.roobre.es", "https://links.wageoffsite.com", "https://linux.community", "https://livesound.world", "https://livy.one", "https://lm.ainyataovi.net", "https://lm.bittervets.org", "https://lm.blythhub.com", "https://lm.boing.icu", "https://lm.byteme.social", "https://lm.chaucago.com", "https://lm.curlefry.net", "https://lm.electrospek.com", "https://lm.gsk.moe", "https://lm.helilot.com", "https://lm.ilyamikcoder.com", "https://lm.inu.is", "https://lm.korako.me", "https://lm.madiator.cloud", "https://lm.melonbread.dev", "https://lm.mrrebot.com", "https://lm.mythoranium.com", "https://lm.paradisus.day", "https://lm.put.tf", "https://lm.qtt.no", "https://lm.rdbt.no", "https://lm.sethp.cc", "https://lm.suitwaffle.com", "https://lm.thesqooid.com", "https://lm.williampuckering.com", "https://lmmy.co.za", "https://lmmy.io", "https://lmmy.net", "https://lmmy.tvdl.dev", "https://lmmy.ylwsgn.cc", "https://lmy.axi.cx", "https://lmy.dotcomitsa.website", "https://lmy.mymte.de", "https://lmy.singinwhale.com", "https://local106.com", "https://lolimbeer.com", "https://lonestarlemmy.mooo.com", "https://lostcheese.com", "https://lsmu.schmurian.xyz", "https://lu.skbo.net", "https://lululemmy.com", "https://lurk.fun", "https://lurkinlemmings.com", "https://lzrprt.sbs", "https://mander.xyz", "https://matejc.com", "https://matts.digital", "https://mayheminc.win", "https://meganice.online", "https://mementomori.school", "https://merv.news", "https://mesita.link", "https://metapowers.org", "https://mfn.pub", "https://mh.znark.us", "https://michelsup.org", "https://midwest.social", "https://milksteak.org", "https://mimiclem.me", "https://mlem.lea.moe", "https://mmm.moooo.ooo", "https://mobilemmohub.com", "https://monero.town", "https://monyet.cc", "https://moose.best", "https://moto.teamswollen.org", "https://mtgzone.com", "https://mujico.org", "https://murray.social", "https://mylem.eu", "https://mylemmy.site", "https://mylemmy.win", "https://narod.city", "https://nc.gnzl.cl", "https://neatia.xyz", "https://nerdly.dev", "https://netmonkey.tech", "https://news.cosocial.ca", "https://news.idlestate.org", "https://nichenerdery.duckdns.org", "https://nickbuilds.net", "https://nlemmy.nl", "https://no.lastname.nz", "https://nom.mom", "https://normalcity.life", "https://not.alazy.dev", "https://notdigg.com", "https://notlemmy.notawebsite.fr", "https://notyour.rodeo", "https://novoidspace.com", "https://nunu.dev", "https://nwdr.club", "https://occult.social", "https://oceanbreeze.earth", "https://odin.lanofthedead.xyz", "https://omg.qa", "https://on.syrma.cc", "https://opendmz.social", "https://opidea.xyz", "https://orava.dev", "https://orcas.enjoying.yachts", "https://orzen.games", "https://ou0.de", "https://outpost.zeuslink.net", "https://overctrl.dbzer0.com", "https://pappdi.in", "https://parapheum.com", "https://partizle.com", "https://pathfinder.social", "https://pathofexile-discuss.com", "https://pawb.social", "https://pcglinks.com", "https://philly.page", "https://popplesburger.hilciferous.nl", "https://poptalk.scrubbles.tech", "https://pornlemmy.com", "https://positroni.ddns.net", "https://possumpat.io", "https://posta.no", "https://postit.quantentoast.de", "https://precious.net", "https://preserve.games", "https://pricefield.org", "https://prime8s.xyz", "https://programming.dev", "https://proit.org", "https://prxs.site", "https://psychedelia.ink", "https://purpleit.net", "https://purrito.kamartaj.xyz", "https://qlemmy.com", "https://quex.cc", "https://quo.ink", "https://quokk.au", "https://r-sauna.fi", "https://r.dcotta.eu", "https://r.gir.st", "https://r.irithyll.cc", "https://r.rosettast0ned.com", "https://rabbitea.rs", "https://radiation.party", "https://rammy.site", "https://random-hero.com", "https://rantoul.social", "https://rational-racoon.de", "https://rblind.com", "https://rc.liftoff-app.org", "https://rdr.lol", "https://re.tei.li", "https://read.widerweb.org", "https://reason.rocks", "https://red.cyberhase.de", "https://reddit.moonbeam.town", "https://reddrefuge.com", "https://reddthat.com", "https://rekabu.ru", "https://reseed.it", "https://retarded.dev", "https://ripo.st", "https://rlyeh.icu", "https://roanoke.social", "https://roznotech.xyz", "https://rustyshackleford.cfd", "https://s.itskhanow.com", "https://s.jape.work", "https://s0ss.net", "https://saldemi.casa", "https://schitthaus.com", "https://scif6.nsalanparty.com", "https://sedd.it", "https://seemel.ink", "https://selfhosted.forum", "https://septic.win", "https://seriously.iamincredibly.gay", "https://server1.duluth.lol", "https://sffa.community", "https://sh.itjust.works", "https://sha1.nl", "https://shitposting.monster", "https://shork.online", "https://showeq.com", "https://sigmet.io", "https://sjatar.net", "https://slangenettet.pyjam.as", "https://slightlyawesome.ninja", "https://slrpnk.net", "https://sneakernet.social", "https://snuv.win", "https://soc.ebmn.io", "https://soccer.forum", "https://social.belowland.com", "https://social.coalition.space", "https://social.dn42.us", "https://social.fossware.space", "https://social.fr4me.io", "https://social.ggbox.fr", "https://social.hamington.net", "https://social.jears.at", "https://social.nerdhouse.io", "https://social.poisson.me", "https://social.rocketsfall.net", "https://social.sour.is", "https://social.styxem.xyz", "https://social.vmdk.ca", "https://sockermunk.se", "https://solstice.etbr.top", "https://some.institute", "https://spgrn.com", "https://stable.liftoff-app.org", "https://stammtisch.hallertau.social", "https://stanx.page", "https://startrek.website", "https://sub.wetshaving.social", "https://sudood.us", "https://sullen.social", "https://superdark.social", "https://supernova.place", "https://suppo.fi", "https://support.futbol", "https://surlesworld.com", "https://surom.de", "https://swg-empire.de", "https://switter.su", "https://szmer.info", "https://t.bobamilktea.xyz", "https://tabletop.place", "https://tagpro.lol", "https://talk.kururin.tech", "https://talka.live", "https://techy.news", "https://terefere.eu", "https://textandmetal.com", "https://thaumatur.ge", "https://the.unknowing.dance", "https://thechurchofmemes.com", "https://theculture.social", "https://thegarden.land", "https://thelemmy.club", "https://theotter.social", "https://thesidewalkends.io", "https://thevapor.space", "https://threads.ruin.io", "https://threadup.space", "https://tkohhh.social", "https://toad.work", "https://toast.ooo", "https://toons.zone", "https://tron.atomicpile.info", "https://ttrpg.network", "https://tucson.social", "https://twisti.ca", "https://udclemmy.xyz", "https://unilem.org", "https://upvote.au", "https://va11halla.bar", "https://vermontconnect.com", "https://victoriagaming.ca", "https://vidya.dev", "https://voltage.vn", "https://voxpop.social", "https://wallstreets.bet", "https://waste-of.space", "https://waveform.social", "https://wayfarershaven.eu", "https://weddit.eu", "https://werm.social", "https://whemic.xyz", "https://whiskers.bim.boats", "https://wilbo.tech", "https://wired.bluemarch.art", "https://withachanceof.com", "https://wizanons.dev", "https://wny.lol", "https://www.jrz.city", "https://x69.org", "https://xbdv.com", "https://xcore.social", "https://xffxe4.lol", "https://xrs.cx", "https://xyz.higurashi.xyz", "https://yah.lol", "https://yakiak.com", "https://yall.theatl.social", "https://yamasaur.com", "https://yiffit.net", "https://ymmel.nl", "https://yukistorm.com", "https://zaros.club", "https://zen.lema.cl", "https://zerobytes.monster", "https://zoo.splitlinux.org" ]); var INSTANCES_KBIN = /* @__PURE__ */ new Set([ "https://1001.coolest.zone", "https://artemis.camp", "https://baguette.pub", "https://bin.pol.social", "https://community.yshi.org", "https://dmv.pub", "https://feddit.online", "https://fedi.chainbits.co.uk", "https://fedi196.gay", "https://fedia.io", "https://forum.fail", "https://fr3diver.se", "https://gehirneimer.de", "https://jlailu.social", "https://k.fe.derate.me", "https://karab.in", "https://kayb.ee", "https://kbin-u3.vm.elestio.app", "https://kbin.allthingstech.social", "https://kbin.buzz", "https://kbin.cafe", "https://kbin.chat", "https://kbin.dk", "https://kbin.donar.dev", "https://kbin.ectolab.net", "https://kbin.fedi.cr", "https://kbin.holmosapien.com", "https://kbin.lgbt", "https://kbin.life", "https://kbin.lol", "https://kbin.maciej.cloud", "https://kbin.melroy.org", "https://kbin.nz", "https://kbin.pithyphrase.net", "https://kbin.place", "https://kbin.primitivebits.social", "https://kbin.pro", "https://kbin.projectsegfau.lt", "https://kbin.run", "https://kbin.sh", "https://kbin.social", "https://kbin.tech", "https://kbin.thicknahalf.com", "https://kglitch.social", "https://kilioa.org", "https://kopnij.in", "https://lemmy.rjbasitali.com", "https://longley.ws", "https://moist.catsweat.com", "https://nadajnik.org", "https://nerdbin.social", "https://no.faux.moe", "https://nolani.academy", "https://open-source.social", "https://readit.buzz", "https://remy.city", "https://rimworld.gallery", "https://rimworldporn.cloud", "https://social.tath.link", "https://streetbikes.club", "https://the.coolest.zone", "https://thebrainbin.org", "https://tuna.cat", "https://wiku.hu" ]); trace(`Define instances sets end`); // src/our-changes.js var OUR_CHANGES = { addedNodes: {} }; function getAddedNodesSelectors() { return Object.values(OUR_CHANGES.addedNodes); } function registerAddedNode(id, selector) { OUR_CHANGES.addedNodes[id] = selector; } // src/constants.js var constants_default = { ICON_CLASS: withNS(`icon`), ICON_LOADING_CLASS: withNS(`loading`), ICON_STYLES_ID: withNS(`icon-styles`), ICON_LINK_CLASS: withNS(`icon-link`), ICON_LINK_SYMBOL_ID: withNS(`icon-link-symbol`), ICON_SPINNER_CLASS: withNS(`icon-spinner`), ICON_SPINNER_SYMBOL_ID: withNS(`icon-spinner-symbol`), ORIGINAL_LINK_CLASS: withNS(`original-link`), SHOW_AT_HOME_BUTTON_CLASS: withNS(`show-at-home`), ICON_SVG_TEMPLATE_ID: withNS(`icon-template`), AUTH_WRONG: `AUTH_WRONG`, AUTH_MISSING: `AUTH_MISSING`, REWRITE_STATUS: withNSCamelCase(`localUrlStatus`), REWRITE_STATUS_PENDING: `pending`, REWRITE_STATUS_SUCCESS: `success`, REWRITE_STATUS_ERROR: `error`, REWRITE_STATUS_UNRESOLVED: `unresolved`, SETUP_AUTH_MESSAGE: `Lemmy Universal Link Switcher: Visit your home instance once to set up post/comment rewriting`, SETTINGS_BUTTON_ID: withNS(`open-settings-button`), SETTINGS_MENU_ID: withNS(`settings`), SETTINGS_STYLES_ID: withNS(`settings-styles`) }; function withNS(identifier) { return `lemmy-rewrite-urls-` + identifier; } function withNSCamelCase(identifier) { return `lemmyRewriteUrls` + identifier.charAt(0).toUpperCase() + identifier.slice(1); } // src/rewriting/helpers.js function isHashLink(link) { return link.hash && link.origin + link.pathname + link.search === location.origin + location.pathname + location.search; } function isSamePage(url1, url2) { return url1.host === url2.host && url1.pathname === url2.pathname; } function isV17() { return window.isoData?.site_res?.version.startsWith(`0.17`); } var stopEventHandler = (event) => { event.preventDefault(); event.stopPropagation(); }; // src/gm.js async function setValue(key, value) { trace(`GM.setValue key ${key}, value ${value}`); await GM.setValue(key, value); } async function getValue(key) { return await GM.getValue(key); } function parseResponse(response) { try { return JSON.parse(response.response); } catch (e) { debug(`Error parsing response JSON`, e); return response.response; } } function logRequest(response, data) { trace( `FinalUrl`, response.finalUrl, `status`, response.status, `text`, response.statusText, `response`, data || response.response ); trace(`responseHeaders`, response.responseHeaders); } function performXmlHttpRequest(url, doneCallback) { GM.xmlHttpRequest({ url, onloadend: (response) => { const data = parseResponse(response); logRequest(response, data); doneCallback(response, data); } }); } function registerMenuCommand(name, onClick) { GM.registerMenuCommand(name, onClick); } // src/rewriting/auth.js var AUTH; function getAuthFromCookie() { return document.cookie.split("; ").find((row) => row.startsWith("jwt="))?.split("=")[1]; } async function setAuth(auth) { AUTH = auth; await setValue(`auth`, auth); } async function initAuth() { const curAuth = await getAuth(); if (curAuth) { AUTH = curAuth; return; } if (location.origin === HOME) { const newAuth = getAuthFromCookie(); await setAuth(newAuth); if (newAuth && await getValue(`authNoticeShown`)) { alert(`Lemmy Universal Link Switcher: Post/comment rewriting has been set up successfully`); await setValue(`authNoticeShown`, null); } } else if (HOME && !await getValue(`authNoticeShown`)) { await setValue(`authNoticeShown`, `true`); alert(constants_default.SETUP_AUTH_MESSAGE); } } function updateAuthPeriodically() { setInterval(async () => { const prev = AUTH; const newAuth = location.origin === HOME ? getAuthFromCookie() : await getAuth(); if (prev !== newAuth) { debug(`Auth changed`); await setAuth(newAuth); clearMissingUrlsInCache(); triggerRewrite(); } }, 1234); } async function getAuth() { return await getValue(`auth`); } // src/rewriting/url-mapping.js function splitPaths(url) { return url.pathname.split(`/`).slice(1); } function isRemoteLemmyUrl(url) { return !isHomeInstance(url) && isLemmyInstance(url); } function isRemoteKbinUrl(url) { return !isHomeInstance(url) && isKbinInstance(url); } function isRemoteUrl(url) { return isRemoteLemmyUrl(url) || isRemoteKbinUrl(url); } function findLocalUrlForStandardAtFormat(url, rootPath) { const paths = splitPaths(url); const name = paths[1].includes(`@`) ? paths[1] : paths[1] + `@` + url.host; return `${HOME}/${rootPath || paths[0]}/${name}` + url.search + url.hash; } function findLocalUrlForLemmyUrl(url) { if (isLemmyUserOrCommunityUrl(url)) { return findLocalUrlForStandardAtFormat(url); } else { return null; } } function findLocalUrlForKbinUserUrl(url) { const paths = splitPaths(url); const user = paths[1].startsWith(`@`) ? paths[1].substring(1) : paths[1]; const name = user.includes(`@`) ? user : user + `@` + url.host; return `${HOME}/u/${name}` + url.search + url.hash; } function findLocalUrlForKbinUrl(url) { if (isKbinMagazineUrl(url)) { return findLocalUrlForStandardAtFormat(url, mappedKbinRootPath(url)); } else if (isKbinUserUrl(url)) { return findLocalUrlForKbinUserUrl(url); } else { return null; } } function findLocalUrl(url) { if (isRemoteLemmyUrl(url)) return findLocalUrlForLemmyUrl(url); if (isRemoteKbinUrl(url)) return findLocalUrlForKbinUrl(url); return null; } async function fetchApIdFromRemote(url) { const endpoint = isLemmyPostUrl(url) ? `post` : `comment`; const paths = splitPaths(url); const id = paths[1]; return new Promise((resolve, reject) => { performXmlHttpRequest(`${url.origin}/api/v3/${endpoint}?id=${id}`, (response, data) => { const apId = data[`${endpoint}_view`]?.[endpoint]?.ap_id; if (response.status === 200 && apId) { resolve(apId); } else { handleFailedRequest(`fetching AP ID`, response, reject); } }); }); } function handleFailedRequest(requestName, response, reject) { if (response.status >= 200 && response.status <= 299) { reject(`${requestName}: Unhandled successful response, status: ${response.status}`); } else if (response.status >= 400 && response.status <= 599) { reject(`${requestName}: Error, status: ${response.status}`); } else { reject(`${requestName}: Something weird happened, status: ${response.status}`); } } async function resolveObjectFromHome(url) { return new Promise(async (resolve, reject) => { const auth = await getAuth(); if (!auth) { debug(`No auth token found`); reject(constants_default.AUTH_MISSING); } performXmlHttpRequest( `${HOME}/api/v3/resolve_object?auth=${auth}&q=${encodeURIComponent(url.href)}`, (response, data) => { if (response.status === 200 && data.post?.post?.id) { resolve(`${HOME}/post/${data.post.post.id}${url.search}${url.hash}`); } else if (response.status === 200 && data.comment?.comment?.id) { resolve(`${HOME}/comment/${data.comment.comment.id}${url.search}${url.hash}`); } else if (response.status === 400 && data?.error === `couldnt_find_object`) { resolve(null); } else if (response.status === 400 && data?.error === `not_logged_in`) { reject(constants_default.AUTH_WRONG); } else { handleFailedRequest(`resolving object`, response, reject); } } ); }); } var urlCache = {}; function clearMissingUrlsInCache() { for (const value of Object.values(urlCache)) { if (value.error) delete value.error; if (value.localUrl === null) delete value.localUrl; } } function getCacheKey(url) { return url.host + url.pathname + url.search; } function cacheResult(url, localUrl) { const key = getCacheKey(url); if (!urlCache[key]) { urlCache[key] = {}; } if (urlCache[key].error) delete urlCache[key].error; urlCache[key].localUrl = localUrl; return localUrl; } function cacheErrorResult(url, error) { const key = getCacheKey(url); if (!urlCache[key]) { urlCache[key] = {}; } urlCache[key].error = error; } function getLocalUrlfromCache(url) { const key = getCacheKey(url); if (urlCache[key]?.error) { throw urlCache[key]?.error; } else { return urlCache[key]?.localUrl; } } async function fetchLocalUrl(url, loadFromCache = true) { if (loadFromCache) { const cached = getLocalUrlfromCache(url); if (cached !== void 0) { trace(`Found URL ${url} in cache: ${cached}`); return cached; } } try { return cacheResult(url, await fetchLocalUrlNoCache(url)); } catch (e) { debug(`fetchLocalUrl error`, e); cacheErrorResult(url, e); throw e; } } async function fetchLocalUrlNoCache(url) { trace(`Trying to resolve URL ${url} directly`); const localUrl = await resolveObjectFromHome(url); if (localUrl !== null) { return localUrl; } else { trace(`Did not find URL ${url} directly`); } const apId = new URL(await fetchApIdFromRemote(url)); trace(`Found AP ID for URL ${url}: ${apId}`); if (!apId.search) { apId.search = url.search; } if (!apId.hash) { apId.hash = url.hash; } if (isSamePage(url, apId)) { trace(`Previous URL was AP ID already, URL not federated for some reason`); return null; } else { return await resolveObjectFromHome(apId); } } function isInstantlyRewritable(url) { return isRemoteLemmyUrl(url) && isLemmyUserOrCommunityUrl(url) || isRemoteKbinUrl(url) && (isKbinMagazineUrl(url) || isKbinUserUrl(url)); } function isRewritableAfterResolving(url) { return isRemoteLemmyUrl(url) && (isLemmyPostUrl(url) || isLemmyCommentUrl(url)); } function isLemmyPostUrl(url) { const paths = splitPaths(url); return paths[0] === `post`; } function isLemmyCommentUrl(url) { const paths = splitPaths(url); return paths[0] === `comment`; } function isLemmyUserOrCommunityUrl(url) { const paths = splitPaths(url); return paths[0] === `c` || paths[0] === `u`; } function isKbinPostUrl(url) { const paths = splitPaths(url); return paths[0] === `m` && paths.length > 2 && paths[2] === `t`; } function isKbinMicroblogUrl(url) { const paths = splitPaths(url); return paths[0] === `m` && paths.length > 2 && paths[2] === `p`; } function isKbinMicroblogOverviewUrl(url) { const paths = splitPaths(url); return paths[0] === `m` && paths.length > 2 && paths[2] === `microblog`; } function isKbinMagazinePeopleUrl(url) { const paths = splitPaths(url); return paths[0] === `m` && paths.length > 2 && paths[2] === `people`; } function isKbinMagazineUrl(url) { const paths = splitPaths(url); return paths[0] === `m` && !isKbinPostUrl(url) && !isKbinMagazinePeopleUrl(url) && !isKbinMicroblogUrl(url) && !isKbinMicroblogOverviewUrl(url); } function mappedKbinRootPath(url) { const paths = splitPaths(url); if (paths[0] === `m`) { return `c`; } else { return null; } } function isKbinUserUrl(url) { const paths = splitPaths(url); return paths.length === 2 && paths[0] === `u`; } // src/rewriting/links/icon.js function getIcon(link) { return link.querySelector(`.` + constants_default.ICON_CLASS); } function createIcon(link) { ensureTemplateAvailable(); ensureIconStylesAdded(); const wrapper = document.createElement(`span`); registerAddedNode(constants_default.ICON_CLASS, `.` + constants_default.ICON_CLASS); wrapper.classList.add(constants_default.ICON_CLASS); if (link.children.length === 0 || getComputedStyle(link.children[link.children.length - 1]).marginRight === `0px`) { wrapper.style.marginLeft = `0.5em`; } const linkIcon = createSVG(); linkIcon.classList.add(constants_default.ICON_LINK_CLASS); linkIcon.innerHTML = `<use href=#${constants_default.ICON_LINK_SYMBOL_ID} />`; wrapper.append(linkIcon); const spinnerIcon = createSVG(); spinnerIcon.classList.add(constants_default.ICON_SPINNER_CLASS); spinnerIcon.innerHTML = `<use href=#${constants_default.ICON_SPINNER_SYMBOL_ID} />`; wrapper.append(spinnerIcon); return wrapper; } function createSVG() { return document.createElementNS(`http://www.w3.org/2000/svg`, `svg`); } function ensureTemplateAvailable() { if (document.querySelector(`#` + constants_default.ICON_SVG_TEMPLATE_ID)) return; const template = createSVG(); template.id = constants_default.ICON_SVG_TEMPLATE_ID; template.innerHTML = `<defs> <symbol id=${constants_default.ICON_LINK_SYMBOL_ID} viewBox="0 0 100 100"><path d="M52.8 34.6c.8.8 1.8 1.2 2.8 1.2s2-.4 2.8-1.2c1.5-1.5 1.5-4 0-5.6l-5.2-5.2h26v30.6c0 2.2 1.8 3.9 3.9 3.9 2.2 0 3.9-1.8 3.9-3.9V19.8c0-2.2-1.8-3.9-3.9-3.9h-30l5.2-5.2c1.5-1.5 1.5-4 0-5.6s-4-1.5-5.6 0l-11.8 12c-1.5 1.5-1.5 4 0 5.6l11.9 11.9zM31.1 28.7V11c0-3-2.5-5.5-5.5-5.5H8C5 5.5 2.5 8 2.5 11v17.7c0 3 2.5 5.5 5.5 5.5h17.7c3 0 5.4-2.5 5.4-5.5zM47.2 65.4c-1.5-1.5-4-1.5-5.6 0s-1.5 4 0 5.6l5.2 5.2h-26V45.6c0-2.2-1.8-3.9-3.9-3.9S13 43.5 13 45.6v34.5c0 2.2 1.8 3.9 3.9 3.9h30l-5.2 5.2c-1.5 1.5-1.5 4 0 5.6.8.8 1.8 1.2 2.8 1.2s2-.4 2.8-1.2l11.9-11.9c1.5-1.5 1.5-4 0-5.6l-12-11.9zM92 65.8H74.4c-3 0-5.5 2.5-5.5 5.5V89c0 3 2.5 5.5 5.5 5.5H92c3 0 5.5-2.5 5.5-5.5V71.3c0-3-2.5-5.5-5.5-5.5z"/></symbol> <symbol id=${constants_default.ICON_SPINNER_SYMBOL_ID} viewBox="0 0 32 32"><path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"/></symbol> </defs>`; registerAddedNode(constants_default.ICON_SVG_TEMPLATE_ID, `#` + constants_default.ICON_SVG_TEMPLATE_ID); document.head.append(template); } function ensureIconStylesAdded() { if (document.querySelector(`#` + constants_default.ICON_STYLES_ID)) return; const style = document.createElement(`style`); style.id = constants_default.ICON_STYLES_ID; style.innerHTML = ` .${constants_default.ICON_SPINNER_CLASS} { display: none; animation: spins 2s linear infinite; } .${constants_default.ICON_LINK_CLASS} { display: inline-block; } .${constants_default.ICON_LOADING_CLASS} > .${constants_default.ICON_LINK_CLASS} { display: none; } .${constants_default.ICON_LOADING_CLASS} > .${constants_default.ICON_SPINNER_CLASS} { display: inline-block; } .${constants_default.ICON_CLASS} > svg { vertical-align: sub; height: 1em; width: 1em; stroke: currentColor; fill: currentColor; }`; registerAddedNode(constants_default.ICON_STYLES_ID, `#` + constants_default.ICON_STYLES_ID); document.head.append(style); } // src/tippy.js var tippy_default = window.tippy; // src/rewriting/links/tooltip.js function getOriginalLinkHtml(originalHref) { registerAddedNode(constants_default.ORIGINAL_LINK_CLASS, `.` + constants_default.ORIGINAL_LINK_CLASS); return `Original link: <a class="${constants_default.ORIGINAL_LINK_CLASS}" href="${originalHref}">${originalHref}</a>`; } function defaultOptions(link) { return { appendTo: () => link.parentNode, allowHTML: true, interactive: true, animation: false, placement: "bottom", hideOnClick: false }; } function createOriginalLinkTooltip(link, originalHref) { trace(`Create original link tooltip`, link, originalHref); getIcon(link).addEventListener(`click`, stopEventHandler); return createLinkTooltip(link, getOriginalLinkHtml(originalHref)); } function createLinkTooltip(link, content) { return tippy_default(getIcon(link), { ...defaultOptions(link), content }); } function createLinkLoadTooltip(link) { trace(`Create link load tooltip`, link); getIcon(link).classList.add(constants_default.ICON_LOADING_CLASS); return createLinkTooltip(link, `Loading home URL...<br />Don't want to wait? ${getOriginalLinkHtml(link.href)}`); } function linkLoadTooltipSuccess(tooltip, originalHref) { linkLoadResult(tooltip, `\u2714\uFE0F Changed link to home instance`, getOriginalLinkHtml(originalHref)); } function linkLoadTooltipError(tooltip, error) { linkLoadResult(tooltip, `\u274C ` + error); } function linkLoadResult(tooltip, result, finalContent = result) { const icon = tooltip.reference; icon.classList.remove(constants_default.ICON_LOADING_CLASS); icon.addEventListener(`click`, stopEventHandler); if (tooltip.state.isVisible) { tooltip.setContent(result); setTimeout(() => { tooltip.hide(); tooltip.setContent(finalContent); }, 2e3); } else { tooltip.setContent(finalContent); } } // src/rewriting/links/links.js function changeLinkHref(link, localUrl) { const treeWalker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT, (node) => { if (node.textContent.toLowerCase().trim() === link.href.toLowerCase().trim()) { return NodeFilter.FILTER_ACCEPT; } else { return NodeFilter.FILTER_SKIP; } }); let textNode; while ((textNode = treeWalker.nextNode()) !== null) { textNode.textContent = localUrl; } link.href = localUrl; link.addEventListener(`click`, (event) => { if (event.button === 0 && !event.ctrlKey && link.target !== `_blank`) { location.href = localUrl; } }); } function appendIconTo(elem, icon) { if (elem.children.length === 0 || getComputedStyle(elem.children[elem.children.length - 1]).display !== `inline-block`) { elem.append(icon); } else { appendIconTo(elem.children[elem.children.length - 1], icon); } } function addFetchLocalUrlHandler(link) { let tooltip; const handler = async (event) => { if (event.type === `click`) { stopEventHandler(event); if (tooltip) tooltip.show(); return; } link.removeEventListener(`focus`, handler); link.removeEventListener(`mouseenter`, handler); if (link.dataset[constants_default.REWRITE_STATUS] === constants_default.REWRITE_STATUS_PENDING) return; link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_PENDING; tooltip = createLinkLoadTooltip(link); try { const localUrl = await fetchLocalUrl(link); if (!localUrl) { debug(`Local URL for ${link.href} could not be found`); linkLoadTooltipError(tooltip, `Home URL could not be found`); return; } trace(`Local URL for ${link.href} found: ${localUrl}`); const oldHref = link.href; changeLinkHref(link, localUrl); linkLoadTooltipSuccess(tooltip, oldHref); link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_SUCCESS; } catch (e) { debug(`Error while trying to resolve local URL`, e); let msg; if (e === constants_default.AUTH_WRONG) { msg = `Saved login expired. Return to your home instance and log in again.`; } else if (e === constants_default.AUTH_MISSING) { msg = constants_default.SETUP_AUTH_MESSAGE; } else { msg = `Error while trying to find home URL`; } linkLoadTooltipError(tooltip, msg); link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_ERROR; } finally { link.removeEventListener(`click`, handler); } }; link.addEventListener(`click`, handler); link.addEventListener(`focus`, handler); link.addEventListener(`mouseenter`, handler); } function isFediverseLink(link) { const svg = link.querySelector(`svg`); if (!svg) return false; if (svg.children.length === 0) return false; return svg.children[0].getAttribute(`xlink:href`)?.includes(`#icon-fedilink`); } function rewriteToLocal(link) { if (!link.parentNode) return false; if (link.classList.contains(constants_default.ORIGINAL_LINK_CLASS)) return false; if (link.dataset[constants_default.REWRITE_STATUS] === constants_default.REWRITE_STATUS_SUCCESS) return false; if (isHashLink(link)) return false; if (!isRemoteUrl(link)) return false; if (isFediverseLink(link)) return false; if (isInstantlyRewritable(link)) { const localUrl = findLocalUrl(link); if (!localUrl) return false; if (isSamePage(new URL(localUrl), location)) return false; const oldHref = link.href; changeLinkHref(link, localUrl); const icon = createIcon(link); appendIconTo(link, icon); createOriginalLinkTooltip(link, oldHref); link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_SUCCESS; trace(`Rewrite link`, link, ` from`, oldHref, `to`, localUrl); return true; } else if (isRewritableAfterResolving(link)) { if (!getIcon(link)) { appendIconTo(link, createIcon(link)); } if (!link.dataset[constants_default.REWRITE_STATUS]) { link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_UNRESOLVED; addFetchLocalUrlHandler(link); } } } function findLinksInChange(change) { if (change.type === `childList`) { const links = Array.from(change.addedNodes).flatMap((addedNode) => { if (addedNode.tagName?.toLowerCase() === `a`) { return addedNode; } else if (addedNode.querySelectorAll) { return Array.from(addedNode.querySelectorAll(`a`)); } else { return []; } }); if (links.length > 0) trace(`Change`, change, `contained the links`, links); return links; } else if (change.type === `attributes`) { return change.target.matches?.(`a`) ? change.target : []; } else { return []; } } function findLinksToRewrite(changes) { if (!changes) { return document.querySelectorAll(`a`); } return changes.flatMap(findLinksInChange); } async function rewriteLinksToLocal(changes) { const links = findLinksToRewrite(changes); const chunkSize = 50; return await async function processChunk(currentChunk) { const startIdx = currentChunk * chunkSize; const endChunkIdx = (currentChunk + 1) * chunkSize; const endIdx = Math.min(links.length, endChunkIdx); debug( `Processing ${links.length} links, current chunk `, currentChunk, `processing links ${startIdx} to ${endIdx}` ); let anyRewritten = false; for (let i = startIdx; i < endIdx; ++i) { const rewritten = rewriteToLocal(links[i]); anyRewritten = anyRewritten || rewritten; } debug(`Processed links ${startIdx} to ${endIdx}`); if (endChunkIdx >= links.length) { return anyRewritten; } const chunkResult = await new Promise((resolve) => setTimeout(async () => { resolve(await processChunk(currentChunk + 1)); }, 0)); return anyRewritten || chunkResult; }(0); } // src/settings.js var refreshMethods = []; async function refresh() { for (const refreshMethod of refreshMethods) { await refreshMethod(); } } function closeSettings() { document.querySelector(`#` + constants_default.SETTINGS_MENU_ID)?.remove(); refreshMethods = []; } function initSettings() { registerMenuCommand(`Open Settings`, showSettings); } function styles(elem, style) { for (const [prop, val] of Object.entries(style)) { elem.style[prop] = val; } } var defaultBorder = `1px solid #AAA`; function createButton(content, options = {}) { const button = document.createElement(`div`); button.innerHTML = content; styles(button, { display: options.inline ? `inline-block` : `block`, backgroundColor: `#666`, textAlign: `center`, border: defaultBorder, cursor: `pointer`, borderRadius: `8px 8px`, padding: `5px` }); return button; } function ensureMenuStylesAdded() { if (document.querySelector(`#` + constants_default.SETTINGS_STYLES_ID)) return; const style = document.createElement(`style`); style.id = constants_default.SETTINGS_STYLES_ID; style.innerHTML = ` #${constants_default.SETTINGS_MENU_ID} * { box-sizing: border-box; } `; document.head.append(style); } function showSettings() { if (document.querySelector(`#` + constants_default.SETTINGS_MENU_ID)) return; if (window !== window.top) return; ensureMenuStylesAdded(); const background = document.createElement(`div`); background.id = constants_default.SETTINGS_MENU_ID; registerAddedNode(constants_default.SETTINGS_MENU_ID, `#` + constants_default.SETTINGS_MENU_ID); styles(background, { position: `fixed`, top: `0`, left: `0`, zIndex: 9999, width: `100vw`, height: `100vh`, backgroundColor: `#00000099`, backdropFilter: `blur(6px)`, display: `grid`, grid: `grid`, alignItems: `center`, justifyContent: `space-around` }); background.addEventListener(`click`, (event) => { if (event.target === background) { closeSettings(); } }); const menu = document.createElement(`div`); styles(menu, { position: `relative`, color: `#ddd`, maxWidth: `90vw`, maxHeight: `90vh`, backgroundColor: `#555`, padding: `20px`, border: defaultBorder, borderRadius: `8px`, overflow: `hidden auto` }); background.append(menu); const closeButton = createButton(`\u{1F5D9}`); styles(closeButton, { position: `absolute`, top: `-1px`, right: `-1px`, width: `30px`, height: `30px`, lineHeight: `25px`, fontSize: `25px`, borderRadius: `0 8px`, padding: 0 }); closeButton.addEventListener(`click`, () => closeSettings()); menu.append(closeButton); const menuHeader = createSettingHeader(`Lemmy Universal Link Switcher Settings`, 1.5); styles(menuHeader, { textDecoration: `underline`, marginTop: 0 }); menu.append(menuHeader); addMainHomeInstanceSetting(menu); addMoreHomeInstancesSettings(menu); document.body.append(background); } function createSettingHeader(text, sizeMultiplicator = 1) { const header = document.createElement(`div`); header.innerHTML = text; styles(header, { marginRight: `10px`, fontSize: `${sizeMultiplicator * 1.4}em`, lineHeight: `${sizeMultiplicator * 1.4}em`, fontWeight: `bold`, marginTop: `${sizeMultiplicator * 25}px`, marginBottom: `${sizeMultiplicator * 10}px` }); return header; } function addInfo(elem, content) { const info = document.createElement(`span`); info.innerHTML = ` \u{1F6C8}`; tippy_default(info, { content, placement: "bottom", triggerTarget: elem }); elem.append(info); } function createInput(options) { const inputElement = document.createElement(`input`); styles(inputElement, { width: `100%`, margin: 0 }); if (options.placeholder) { inputElement.placeholder = options.placeholder; } if (options.getter) { Promise.resolve(options.getter()).then((result) => inputElement.value = result); } if (!inputElement.value) { inputElement.setCustomValidity(`empty`); } styles(inputElement, { border: defaultBorder }); if (!options.validator && !options.setter) { return; } let validator = options.validator || (() => true); let setter = options.setter || (() => { }); inputElement.addEventListener(`input`, async () => { const validated = await validator(inputElement.value); if (!inputElement.value || validated) { inputElement.setCustomValidity(`empty`); styles(inputElement, { border: defaultBorder }); } if (inputElement.value) { if (validated) { inputElement.setCustomValidity(``); await setter(inputElement.value); } else if (!validated) { inputElement.setCustomValidity(`fail`); styles(inputElement, { border: `1px solid red` }); } } }); return inputElement; } var urlValidator = (value) => { try { new URL(value); return true; } catch (e) { return false; } }; var instancePlaceholder = `The full link (including http(s)://) to the instance`; function addMainHomeInstanceSetting(addTo) { const header = createSettingHeader(`Main Home Instance`); addInfo(header, `All links will be rewritten to this instance, except for links to your secondary home instances.`); addTo.append(header); const mainInstance = createInput({ getter: () => HOME, setter: (value) => { const url = new URL(value); if (HOME !== url.origin) { setHome(url.origin); refresh(); } }, validator: urlValidator, placeholder: instancePlaceholder }); addTo.append(mainInstance); const homeButtonWrapper = document.createElement(`div`); const refreshMakeHomeButton = () => { homeButtonWrapper.replaceChildren(); if (location.origin === HOME) return; const makeHomeButton = createButton(`Use current page as home instance`); styles(makeHomeButton, { borderRadius: `0 0 8px 8px` }); makeHomeButton.addEventListener(`click`, () => { mainInstance.value = location.origin; mainInstance.dispatchEvent(new Event(`input`)); }); homeButtonWrapper.append(makeHomeButton); }; refreshMakeHomeButton(); refreshMethods.push(refreshMakeHomeButton); addTo.append(homeButtonWrapper); } async function addMoreHomeInstancesSettings(addTo) { const header = createSettingHeader(`Secondary Home Instances`); addInfo(header, `All links pointing to these instances will not be changed.`); addTo.append(header); addTo.append(createListInput( async () => await getSecondaryHomeInstances(), async (value) => { const homeInstances = await getSecondaryHomeInstances(); homeInstances.push(new URL(value).origin); await setSecondaryHomeInstances(homeInstances); }, async (value) => { const homeInstances = await getSecondaryHomeInstances(); var index = homeInstances.indexOf(new URL(value).origin); if (index > -1) { homeInstances.splice(index, 1); } await setSecondaryHomeInstances(homeInstances); } )); } function createListItem(item, onClick) { const listItem = createButton(item + ` \u{1F5D9}`, { inline: true }); styles(listItem, { margin: `0px 5px 5px 0` }); listItem.addEventListener(`click`, onClick); return listItem; } function createListInput(getter, add, remove) { const wrapper = document.createElement(`div`); const list = document.createElement(`div`); styles(list, { marginBottom: `8px` }); const refreshList = async () => { const items = (await getter()).sort().map((item) => createListItem(item, async () => { await remove(item); refresh(); })); if (items.length) { list.replaceChildren(...items); } else { list.replaceChildren(`<None>`); } }; refreshList(); refreshMethods.push(refreshList); wrapper.append(list); const addInput = createInput({ validator: async (value) => { return urlValidator(value) && !(await getter()).includes(value); }, placeholder: instancePlaceholder }); wrapper.append(addInput); const buttonWrapper = document.createElement(`div`); const refreshButtons = async () => { buttonWrapper.replaceChildren(); const isCurrentPageHome = HOME === location.origin || (await getter()).includes(location.origin); const addButton2 = createButton(`Add`); styles(addButton2, { borderRadius: isCurrentPageHome ? `0 0 8px 8px` : `0` }); addButton2.addEventListener(`click`, async () => { if (addInput.validity.valid) { await add(addInput.value); addInput.value = ``; refresh(); } }); buttonWrapper.append(addButton2); if (!isCurrentPageHome) { const addCurrentButton = createButton(`Add current page`); styles(addCurrentButton, { borderRadius: `0 0 8px 8px` }); addCurrentButton.addEventListener(`click`, async () => { await add(location.origin); addInput.value = ``; refresh(); }); buttonWrapper.append(addCurrentButton); } }; refreshButtons(); refreshMethods.push(refreshButtons); wrapper.append(buttonWrapper); return wrapper; } // src/rewriting/settings-buttons.js function addSettingsButton() { if (location.pathname !== `/settings`) return false; if (!document.querySelector(`[name="Description"][content="Lemmy"]`)) return false; if (document.querySelector(`#` + constants_default.SETTINGS_BUTTON_ID)) return false; const insertAfter = document.querySelector(`#user-password`)?.closest(`.card`); if (!insertAfter) return; const button = document.createElement(`button`); button.id = constants_default.SETTINGS_BUTTON_ID; button.setAttribute(`class`, `btn btn-block btn-primary mr-4 w-100`); button.innerHTML = `Lemmy Universal Link Switcher Settings`; button.addEventListener(`click`, showSettings); registerAddedNode(constants_default.SETTINGS_BUTTON_ID, `#` + constants_default.SETTINGS_BUTTON_ID); insertAfter.insertAdjacentElement("afterend", button); return true; } // src/rewriting/show-at-home.js function showAtHomeButtonText() { const host = new URL(HOME).hostname; return `Show on ${host}`; } function createShowAtHomeAnchor(localUrl) { const showAtHome = document.createElement(`a`); showAtHome.dataset.creationHref = location.href; showAtHome.classList.add(constants_default.SHOW_AT_HOME_BUTTON_CLASS); showAtHome.innerHTML = showAtHomeButtonText(); showAtHome.href = localUrl; registerAddedNode(constants_default.SHOW_AT_HOME_BUTTON_CLASS, `.` + constants_default.SHOW_AT_HOME_BUTTON_CLASS); return showAtHome; } function addLemmyShowAtHomeButton(localUrl) { const logo = document.querySelector(`a.navbar-brand`); const navbarIcons = isV17() ? document.querySelector(`[title="Search"]`)?.closest(`.navbar-nav`) : document.querySelector(`#navbarIcons`); if (!logo || !navbarIcons) { debug(`Could not find position to insert ShowAtHomeButton at`); return false; } const mobile = createShowAtHomeAnchor(localUrl); mobile.classList.add(`d-md-none`); mobile.style[`margin-right`] = `8px`; mobile.style[`margin-left`] = `auto`; if (isV17()) { document.querySelector(`.navbar-nav.ml-auto`)?.classList.remove(`ml-auto`); } logo.insertAdjacentElement("afterend", mobile); const desktop = createShowAtHomeAnchor(localUrl); desktop.classList.add(`d-md-inline`); desktop.style[`margin-right`] = `8px`; navbarIcons.insertAdjacentElement("beforebegin", desktop); return true; } function addKbinShowAtHomeButton(localUrl) { const prependTo = document.querySelector(`#header menu:not(.head-nav__menu)`); if (!prependTo) { debug(`Could not find position to insert ShowAtHomeButton at`); return false; } const item = document.createElement(`li`); item.append(createShowAtHomeAnchor(localUrl)); prependTo.prepend(item); return true; } function addButton(localUrl) { const oldButton = document.querySelectorAll(`.` + constants_default.SHOW_AT_HOME_BUTTON_CLASS); if (oldButton.length > 0 && oldButton[0].dataset.creationHref !== location.href) { debug(`Removing old show at home button`); oldButton.forEach((btn) => btn.remove()); } else if (oldButton.length > 0) { debug(`Old show at home button still exists`); return false; } if (!localUrl) { debug(`No local URL for show at home button found`); return false; } else if (isRemoteLemmyUrl(location)) { return addLemmyShowAtHomeButton(localUrl); } else if (isRemoteKbinUrl(location)) { return addKbinShowAtHomeButton(localUrl); } else { return false; } } async function addShowAtHomeButton() { if (isInstantlyRewritable(location)) { return addButton(findLocalUrl(location)); } else if (isRewritableAfterResolving(location)) { try { return addButton(await fetchLocalUrl(location)); } catch (e) { debug(`Error while trying to add "show at home" button`, e); } } } // src/rewriting/rewrite.js function triggerRewrite() { doAllDomChanges(); } function isOrHasOurAddedNode(node) { return getAddedNodesSelectors().some((selector) => node.matches?.(selector) || node.querySelector?.(selector)); } function isIrrelevantChange(change) { if (change.type === `childList`) { if (Array.from(change.removedNodes).some(isOrHasOurAddedNode)) { trace(`Change`, change, `is relevant because a removed node is/has ours`); return false; } if (!Array.from(change.addedNodes).every(isOrHasOurAddedNode)) { trace(`Change`, change, `is relevant because not every added node is/has ours`); return false; } } else if (change.type === `attributes` && isRemoteUrl(new URL(change.target.href))) { trace(`Change`, change, `is relevant because href changed to a remote URL`); return false; } trace(`Change`, change, `is irrelevant`); return true; } async function startRewriting() { new MutationObserver((changes, observer) => { debug(`MutationObserver triggered`, changes); if (changes.every(isIrrelevantChange)) { debug(`All observed changes are irrelevant`); return; } doAllDomChanges(changes); }).observe(document.body, { subtree: true, childList: true, attributeFilter: [`href`] }); await doAllDomChanges(); } async function doAllDomChanges(changes) { debug(`doAllDomChanges start`); const addedSettingsButtons = addSettingsButton(); if (addedSettingsButtons) debug(`Added Settings Buttons`); const addedShowAtHomeButton = HOME ? addShowAtHomeButton() : false; if (addedShowAtHomeButton) debug(`Added Show At Home Button`); const rewrittenLinks = HOME ? await rewriteLinksToLocal(changes) : false; if (rewrittenLinks) debug(`Rewritten some links`); debug(`doAllDomChanges end`); } // src/home.js var HOME; var secondaryHomes = []; async function initHome() { HOME = await getValue(`home`); secondaryHomes = await getSecondaryHomeInstances(); if (!HOME && isLemmyInstance(location) && confirm(`Lemmy Universal Link Switcher: Set this instance to be your home instance to which all URLs get rewritten to?`)) { setHome(location.origin); } } async function setHome(newHome) { trace(`Set HOME from ${HOME} to ${newHome}`); if (typeof newHome !== `string`) { newHome = null; } HOME = newHome; await setValue(`home`, newHome); } async function getHome() { return await getValue(`home`); } function updateHomePeriodically() { trace(`Current HOME`, HOME); setInterval(async () => { const prevHome = HOME; const prevSecondaries = secondaryHomes; HOME = await getHome(); secondaryHomes = await getSecondaryHomeInstances(); if (prevHome !== HOME) { debug(`HOME changed externally from`, prevHome, `to`, HOME); triggerRewrite(); } else if (prevSecondaries.length !== secondaryHomes.length || !prevSecondaries.every((v) => secondaryHomes.includes(v))) { debug(`secondaryHomes changed externally from`, prevSecondaries, `to`, secondaryHomes); triggerRewrite(); } }, 1337); } function isHomeInstance(url) { return secondaryHomes.concat(HOME).includes(url.origin); } async function getSecondaryHomeInstances() { const homeInstancesStr = await getValue(`secondaryHomes`); return homeInstancesStr ? JSON.parse(homeInstancesStr) : []; } async function setSecondaryHomeInstances(homeInstances) { await setValue(`secondaryHomes`, JSON.stringify(homeInstances)); } // src/main.js (async () => { await initHome(); updateHomePeriodically(); await initAuth(); updateAuthPeriodically(); initSettings(); startRewriting(); })(); })();