您需要先安装一个扩展,例如 篡改猴、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.2.1 // @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 // @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://changeme_gnv6cavn4bbpwvrpmjs5", "http://changeme_qiivckbjhtzuinufzvr2", "http://changeme_qiivckbjhtzuinufzvr2", "http://kek.henlo.fi", "https://0v0.social", "https://0xdd.org.ru", "https://1337lemmy.com", "https://158436977.xyz", "https://acqrs.co.uk", "https://actuallyruben.nl", "https://aggregation.cafe", "https://agora.nop.chat", "https://aiparadise.moe", "https://algebro.xyz", "https://anarch.is", "https://androiddev.network", "https://anime-pub.moe", "https://animoe.xyz", "https://apollo.town", "https://areality.social", "https://ascy.mooo.com", "https://aulem.org", "https://aussie.zone", "https://badblocks.rocks", "https://baomi.tv", "https://baraza.africa", "https://bbs.9tail.net", "https://bbs.darkwitch.net", "https://beehaw.org", "https://beer.andma.la", "https://beevibes.net", "https://bethe.kingofdog.de", "https://bigfoot.ninja", "https://biglemmowski.win", "https://bluuit.org", "https://board.minimally.online", "https://bolha.social", "https://bookwormstory.social", "https://booty.world", "https://botnet.club", "https://bubblesthebunny.com", "https://bulletintree.com", "https://butts.international", "https://c.calciumlabs.com", "https://caint.org", "https://cavy.rocks", "https://centennialstate.social", "https://chat.maiion.com", "https://cigar.cx", "https://civilloquy.com", "https://clatter.eu", "https://cnvrs.net", "https://code4lib.net", "https://coeus.sbs", "https://communick.news", "https://community.adiquaints.moe", "https://community.nicfab.it", "https://compuverse.uk", "https://crystals.rest", "https://cubing.social", "https://culture0.cc", "https://darmok.xyz", "https://databend.run", "https://dataterm.digital", "https://dendarii.alaeron.com", "https://derp.foo", "https://derpzilla.net", "https://dgngrnder.com", "https://diggit.xyz", "https://digipres.cafe", "https://digitalgoblin.uk", "https://discuss.icewind.me", "https://discuss.jacen.moe", "https://discuss.ntfy.sh", "https://discuss.online", "https://discuss.tchncs.de", "https://distress.digital", "https://dmv.social", "https://donky.social", "https://dormi.zone", "https://dot.surf", "https://drachennetz.com", "https://drak.gg", "https://drlemmy.net", "https://dsilo061298.ddns.net", "https://dubvee.org", "https://dubvee.org", "https://einweckglas.com", "https://endlesstalk.org", "https://endofti.me", "https://eslemmy.es", "https://eventfrontier.com", "https://eviltoast.org", "https://exploding-heads.com", "https://f.jbradaric.me", "https://fadoverso.pt", "https://fanaticus.social", "https://fanexus.com", "https://fed.rosssi.co.uk", "https://feddi.no", "https://feddit.at", "https://feddit.ch", "https://feddit.cl", "https://feddit.de", "https://feddit.dk", "https://feddit.eu", "https://feddit.fun", "https://feddit.it", "https://feddit.jp", "https://feddit.nl", "https://feddit.nu", "https://feddit.nz", "https://feddit.pl", "https://feddit.ro", "https://feddit.site", "https://feddit.strike23.de", "https://feddit.tech", "https://feddit.win", "https://feddiverse.org", "https://federated.community", "https://federated.ninja", "https://fedibb.ml", "https://fedit.io", "https://feditown.com", "https://fediverse.love", "https://fediverse.ro", "https://feedly.j-cloud.uk", "https://femboys.bar", "https://fenbushi.site", "https://fig.systems", "https://fjdk.uk", "https://fl0w.cc", "https://forkk.me", "https://foros.fediverso.gal", "https://forum.basedcount.com", "https://forum.stellarcastle.net", "https://freewilltiger.page", "https://frig.social", "https://geddit.social", "https://geddit.social", "https://glasstower.nl", "https://goddess.name", "https://gonelemmy.xyz", "https://granitestate.social", "https://greg.city", "https://group.lt", "https://grumpy.schuppentier.club", "https://hakbox.social", "https://halubilo.social", "https://hammerdown.0fucks.nl", "https://hc.frorayz.tech", "https://heckoverflow.net", "https://hoodlem.me", "https://hoodratshit.org", "https://hqueue.dev", "https://hyperfair.link", "https://iceorchid.net", "https://info.prou.be", "https://infosec.pub", "https://insane.dev", "https://invariant-marxism.red", "https://jalim.xyz", "https://jamie.moe", "https://jemmy.jeena.net", "https://ka.tet42.org", "https://kale.social", "https://keeb.lol", "https://keylog.zip", "https://kleptonix.com", "https://kyu.de", "https://l.1in1.net", "https://l.biendeo.com", "https://l.bxy.sh", "https://l.jugregator.org", "https://l.mathers.fr", "https://l.mchome.net", "https://l.nulltext.org", "https://l.plabs.social", "https://l.roofo.cc", "https://l.twos.dev", "https://labdegato.com", "https://laguna.chat", "https://latte.isnot.coffee", "https://le-me.xyz", "https://le.fduck.net", "https://le.mnau.xyz", "https://leaf.dance", "https://leby.dev", "https://leddit.minnal.icu", "https://leddit.social", "https://lef.li", "https://lem.agoomem.xyz", "https://lem.amd.im", "https://lem.cochrun.xyz", "https://lem.elbullazul.com", "https://lem.free.as", "https://lem.lyk.pw", "https://lem.monster", "https://lem.ph3j.com", "https://lem.serkozh.me", "https://lem.simple-gear.com", "https://lem.southcape.social", "https://lemdit.com", "https://lemido.freakspot.net", "https://lemitar.meta1203.com", "https://lemiverse.xyz", "https://lemm.ee", "https://lemmerz.org", "https://lemmings.basic-domain.com", "https://lemmings.online", "https://lemmit.online", "https://lemmit.online", "https://lemmit.xyz", "https://lemmony.click", "https://lemmus.org", "https://lemmy", "https://lemmy", "https://lemmy", "https://lemmy", "https://lemmy-xqddz-u4892.vm.elestio.app", "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.aguiarvieira.pt", "https://lemmy.albertoluna.es", "https://lemmy.alexland.ca", "https://lemmy.amxl.com", "https://lemmy.amyjnobody.com", "https://lemmy.ananace.dev", "https://lemmy.andiru.de", "https://lemmy.anji.nl", "https://lemmy.anonion.social", "https://lemmy.antemeridiem.xyz", "https://lemmy.antisocial.ly", "https://lemmy.appeine.com", "https://lemmy.arclight.pro", "https://lemmy.astheriver.art", "https://lemmy.aucubin.de", "https://lemmy.austinite.online", "https://lemmy.austinvaness.com", "https://lemmy.austinwadeheller.com", "https://lemmy.avata.social", "https://lemmy.azamserver.com", "https://lemmy.barnacles.one", "https://lemmy.baswag.de", "https://lemmy.batlord.org", "https://lemmy.beebl.es", "https://lemmy.beru.co", "https://lemmy.best", "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.borlax.com", "https://lemmy.brad.ee", "https://lemmy.bradis.me", "https://lemmy.brief.guru", "https://lemmy.bringdaruck.us", "https://lemmy.browntown.dev", "https://lemmy.brynstuff.co.uk", "https://lemmy.bulwarkob.com", "https://lemmy.bunbi.net", "https://lemmy.burger.rodeo", "https://lemmy.burger.rodeo", "https://lemmy.burningboard.net", "https://lemmy.ca", "https://lemmy.cablepick.net", "https://lemmy.cafe", "https://lemmy.caffeinated.social", "https://lemmy.calebmharper.com", "https://lemmy.calvss.com", "https://lemmy.cat", "https://lemmy.chiisana.net", "https://lemmy.chromozone.dev", "https://lemmy.ciechom.eu", "https://lemmy.click", "https://lemmy.cloudhub.social", "https://lemmy.clueware.org", "https://lemmy.cmstactical.net", "https://lemmy.cnschn.com", "https://lemmy.cock.social", "https://lemmy.coeus.icu", "https://lemmy.comfysnug.space", "https://lemmy.commodore.social", "https://lemmy.cook.gg", "https://lemmy.coolmathgam.es", "https://lemmy.cornspace.space", "https://lemmy.corrigan.xyz", "https://lemmy.coupou.fr", "https://lemmy.croc.pw", "https://lemmy.cultimean.group", "https://lemmy.davidbuckley.ca", "https://lemmy.davidfreina.at", "https://lemmy.dbzer0.com", "https://lemmy.dcrich.net", "https://lemmy.deadca.de", "https://lemmy.death916.xyz", "https://lemmy.decronym.xyz", "https://lemmy.deev.io", "https://lemmy.dekay.se", "https://lemmy.demonoftheday.eu", "https://lemmy.devils.house", "https://lemmy.direktoratet.se", "https://lemmy.discothe.quest", "https://lemmy.dlgreen.com", "https://lemmy.dnet.social", "https://lemmy.donmcgin.com", "https://lemmy.doomeer.com", "https://lemmy.dork.lol", "https://lemmy.dormedas.com", "https://lemmy.douwes.co.uk", "https://lemmy.dudeami.win", "https://lemmy.dupper.net", "https://lemmy.dustybeer.com", "https://lemmy.dyslexicjedi.com", "https://lemmy.easfrq.live", "https://lemmy.eatsleepcode.ca", "https://lemmy.eco.br", "https://lemmy.edgarchirivella.com", "https://lemmy.efradaphi.de", "https://lemmy.eic.lu", "https://lemmy.einval.net", "https://lemmy.elest.io", "https://lemmy.elest.io", "https://lemmy.elmusfire.xyz", "https://lemmy.emopolarbear.com", "https://lemmy.enchanted.social", "https://lemmy.escapebigtech.info", "https://lemmy.eus", "https://lemmy.fakecake.org", "https://lemmy.fanboys.xyz", "https://lemmy.fauxreigner.net", "https://lemmy.fbxl.net", "https://lemmy.fdvrs.xyz", "https://lemmy.fedi.bub.org", "https://lemmy.fedihub.social", "https://lemmy.fediverse.jp", "https://lemmy.fediversum.de", "https://lemmy.film", "https://lemmy.finance", "https://lemmy.flauschbereich.de", "https://lemmy.flugratte.dev", "https://lemmy.fmhy.ml", "https://lemmy.foxden.party", "https://lemmy.foxden.social", "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.fucs.io", "https://lemmy.fun", "https://lemmy.funkyfish.cool", "https://lemmy.funkylab.xyz", "https://lemmy.fwgx.uk", "https://lemmy.fyi", "https://lemmy.g97.top", "https://lemmy.game-files.net", "https://lemmy.gareth.computer", "https://lemmy.ghostplanet.org", "https://lemmy.gjz010.com", "https://lemmy.glasgow.social", "https://lemmy.gmprojects.pro", "https://lemmy.graphics", "https://lemmy.graz.social", "https://lemmy.gross.hosting", "https://lemmy.grouchysysadmin.com", "https://lemmy.grygon.com", "https://lemmy.gsp8181.co.uk", "https://lemmy.gtfo.social", "https://lemmy.haigner.me", "https://lemmy.hamrick.xyz", "https://lemmy.helheim.net", "https://lemmy.helios42.de", "https://lemmy.hellwhore.com", "https://lemmy.help", "https://lemmy.helvetet.eu", "https://lemmy.holmosapien.com", "https://lemmy.hopskipjump.cloud", "https://lemmy.hosted.frl", "https://lemmy.hpost.no", "https://lemmy.hutch.chat", "https://lemmy.intai.tech", "https://lemmy.irrealis.net", "https://lemmy.isamp.com", "https://lemmy.iswhereits.at", "https://lemmy.itermori.de", "https://lemmy.iys.io", "https://lemmy.jacaranda.club", "https://lemmy.jacaranda.club", "https://lemmy.jamesj999.co.uk", "https://lemmy.jamestrey.com", "https://lemmy.jasondove.me", "https://lemmy.jasonsanta.xyz", "https://lemmy.javant.xyz", "https://lemmy.jigoku.us.to", "https://lemmy.jlh.name", "https://lemmy.jmtr.org", "https://lemmy.johnpanos.com", "https://lemmy.jpaulus.io", "https://lemmy.jpiolho.com", "https://lemmy.jstsmthrgk.eu", "https://lemmy.jtmn.dev", "https://lemmy.juggler.jp", "https://lemmy.k6qw.com", "https://lemmy.kagura.eu", "https://lemmy.karaolidis.com", "https://lemmy.katia.sh", "https://lemmy.kemomimi.fans", "https://lemmy.keychat.org", "https://lemmy.kghorvath.com", "https://lemmy.kizaing.ca", "https://lemmy.knocknet.net", "https://lemmy.ko4abp.com", "https://lemmy.kodeklang.social", "https://lemmy.korgen.xyz", "https://lemmy.koski.co", "https://lemmy.krobier.com", "https://lemmy.kutara.io", "https://lemmy.kya.moe", "https://lemmy.l00p.org", "https://lemmy.l0nax.org", "https://lemmy.legally-berlin.de", "https://lemmy.lemist.de", "https://lemmy.lif.ovh", "https://lemmy.link", "https://lemmy.linuxuserspace.show", "https://lemmy.littlejth.com", "https://lemmy.livesound.world", "https://lemmy.loomy.li", "https://lemmy.loungerat.io", "https://lemmy.loutsenhizer.com", "https://lemmy.lpcha.im", "https://lemmy.lucaslower.com", "https://lemmy.lukeog.com", "https://lemmy.lylapol.com", "https://lemmy.m1k.cloud", "https://lemmy.maatwo.com", "https://lemmy.macaddict89.me", "https://lemmy.mambastretch.com", "https://lemmy.maples.dev", "https://lemmy.mariusdavid.fr", "https://lemmy.marud.fr", "https://lemmy.mats.ooo", "https://lemmy.matthe815.dev", "https://lemmy.mazurka.xyz", "https://lemmy.mb-server.com", "https://lemmy.mbirth.uk", "https://lemmy.media", "https://lemmy.meissners.me", "https://lemmy.meli.dev", "https://lemmy.menos.gotdns.org", "https://lemmy.mentalarts.info", "https://lemmy.meowchat.xyz", "https://lemmy.meridiangrp.co.uk", "https://lemmy.mildgrim.com", "https://lemmy.mira.pm", "https://lemmy.ml", "https://lemmy.mlaga97.space", "https://lemmy.mlsn.fr", "https://lemmy.modshiftx.com", "https://lemmy.monster", "https://lemmy.moonling.nl", "https://lemmy.morrisherd.com", "https://lemmy.mpcjanssen.nl", "https://lemmy.mrm.one", "https://lemmy.munsell.io", "https://lemmy.mxh.nu", "https://lemmy.my.id", "https://lemmy.mypinghertz.com", "https://lemmy.mywire.xyz", "https://lemmy.n1ks.net", "https://lemmy.naga.sh", "https://lemmy.name", "https://lemmy.nanoklinika.tk", "https://lemmy.nathaneaston.com", "https://lemmy.nauk.io", "https://lemmy.neeley.cloud", "https://lemmy.nerdcave.us", "https://lemmy.nerdcore.social", "https://lemmy.nexus", "https://lemmy.nikore.net", "https://lemmy.nine-hells.net", "https://lemmy.ninja", "https://lemmy.nope.ly", "https://lemmy.nopro.be", "https://lemmy.norbz.org", "https://lemmy.notdead.net", "https://lemmy.nrd.li", "https://lemmy.nz", "https://lemmy.obrell.se", "https://lemmy.one", "https://lemmy.onitato.com", "https://lemmy.onlylans.io", "https://lemmy.otakufarms.com", "https://lemmy.parasrah.com", "https://lemmy.pastwind.top", "https://lemmy.pathoris.de", "https://lemmy.pcft.eu", "https://lemmy.pe1uca.dev", "https://lemmy.pec0ra.ch", "https://lemmy.perigrine.ca", "https://lemmy.philipcristiano.com", "https://lemmy.picote.ch", "https://lemmy.pineapplemachine.com", "https://lemmy.pineapplenest.com", "https://lemmy.pipe01.net", "https://lemmy.piperservers.net", "https://lemmy.pipipopo.pl", "https://lemmy.pit.ninja", "https://lemmy.place", "https://lemmy.plasmatrap.com", "https://lemmy.podycust.co.uk", "https://lemmy.podycust.co.uk", "https://lemmy.ppl.town", "https://lemmy.primboard.de", "https://lemmy.pro", "https://lemmy.productionservers.net", "https://lemmy.proxmox-lab.com", "https://lemmy.pryst.de", "https://lemmy.pt", "https://lemmy.pub", "https://lemmy.pubsub.fun", "https://lemmy.pussthecat.org", "https://lemmy.pwzle.com", "https://lemmy.pxm.nl", "https://lemmy.qpixel.me", "https://lemmy.quad442.com", "https://lemmy.radio", "https://lemmy.rat.academy", "https://lemmy.ravc.tech", "https://lemmy.recursed.net", "https://lemmy.remotelab.uk", "https://lemmy.rhymelikedi.me", "https://lemmy.riffel.family", "https://lemmy.rimkus.it", "https://lemmy.rollenspiel.monster", "https://lemmy.room409.xyz", "https://lemmy.roombob.cat", "https://lemmy.root6.de", "https://lemmy.rubenernst.dev", "https://lemmy.run", "https://lemmy.run", "https://lemmy.s9m.xyz", "https://lemmy.saik0.com", "https://lemmy.samad.one", "https://lemmy.sascamooch.com", "https://lemmy.sbs", "https://lemmy.scam-mail.me", "https://lemmy.scav1.com", "https://lemmy.schlunker.com", "https://lemmy.schmeisserweb.at", "https://lemmy.schuerz.at", "https://lemmy.scottlabs.io", "https://lemmy.sdf.org", "https://lemmy.sebbem.se", "https://lemmy.secnd.me", "https://lemmy.sedimentarymountains.com", "https://lemmy.seifert.id", "https://lemmy.selfhost.quest", "https://lemmy.selfip.org", "https://lemmy.server.fifthdread.com", "https://lemmy.services.coupou.fr", "https://lemmy.sh", "https://lemmy.shattervideoisland.com", "https://lemmy.sidh.bzh", "https://lemmy.sietch.online", "https://lemmy.skillissue.dk", "https://lemmy.smeargle.fans", "https://lemmy.snoot.tube", "https://lemmy.socdojo.com", "https://lemmy.soontm.de", "https://lemmy.spacestation14.com", "https://lemmy.sprawl.club", "https://lemmy.srv.eco", "https://lemmy.srv0.lol", "https://lemmy.staphup.nl", "https://lemmy.stark-enterprise.net", "https://lemmy.starlightkel.xyz", "https://lemmy.starmade.de", "https://lemmy.steken.xyz", "https://lemmy.stormlight.space", "https://lemmy.strandundmeer.net", "https://lemmy.stuart.fun", "https://lemmy.studio", "https://lemmy.suchmeme.nl", "https://lemmy.sumuun.net", "https://lemmy.sumuun.net", "https://lemmy.svc.vesey.tech", "https://lemmy.sweevo.net", "https://lemmy.syrasu.com", "https://lemmy.sysctl.io", "https://lemmy.tancomps.net", "https://lemmy.tanktrace.de", "https://lemmy.tario.org", "https://lemmy.tarsis.org", "https://lemmy.taubin.cc", "https://lemmy.teaisatfour.com", "https://lemmy.technosorcery.net", "https://lemmy.techstache.com", "https://lemmy.tedomum.net", "https://lemmy.telaax.com", "https://lemmy.tf", "https://lemmy.tgxn.net", "https://lemmy.thanatos.at", "https://lemmy.the-burrow.com", "https://lemmy.the-goblin.net", "https://lemmy.theia.cafe", "https://lemmy.themainframe.org", "https://lemmy.theonecurly.page", "https://lemmy.thepixelproject.com", "https://lemmy.therhys.co.uk", "https://lemmy.thesmokinglounge.club", "https://lemmy.thias.xyz", "https://lemmy.tillicumnet.com", "https://lemmy.timdn.com", "https://lemmy.timon.sh", "https://lemmy.timwaterhouse.com", "https://lemmy.tobyvin.dev", "https://lemmy.today", "https://lemmy.toot.pt", "https://lemmy.towards.vision", "https://lemmy.tr00st.co.uk", "https://lemmy.trippy.pizza", "https://lemmy.ubergeek77.chat", "https://lemmy.umainfo.live", "https://lemmy.uncomfortable.business", "https://lemmy.unfiltered.social", "https://lemmy.uninsane.org", "https://lemmy.utopify.org", "https://lemmy.utveckla.re", "https://lemmy.va-11-hall-a.cafe", "https://lemmy.vanoverloop.xyz", "https://lemmy.vepta.org", "https://lemmy.villa-straylight.social", "https://lemmy.vinodjam.com", "https://lemmy.vip", "https://lemmy.virtim.dev", "https://lemmy.vodkatonic.org", "https://lemmy.vrchat-dev.tech", "https://lemmy.vskr.net", "https://lemmy.vyizis.tech", "https://lemmy.w9r.de", "https://lemmy.webgirand.eu", "https://lemmy.website", "https://lemmy.weckhorst.no", "https://lemmy.weiser.social", "https://lemmy.whalesinspace.de", "https://lemmy.whynotdrs.org", "https://lemmy.wiuf.net", "https://lemmy.wizjenkins.com", "https://lemmy.world", "https://lemmy.wraithsquadrongaming.com", "https://lemmy.wtf", "https://lemmy.wxbu.de", "https://lemmy.wyattsmith.org", "https://lemmy.x01.ninja", "https://lemmy.xce.pt", "https://lemmy.xcoolgroup.com", "https://lemmy.xoynq.com", "https://lemmy.zelinsky.dev", "https://lemmy.zell-mbc.com", "https://lemmy.zip", "https://lemmy.zone", "https://lemmy.zroot.org", "https://lemmy2.addictmud.org", "https://lemmybedan.com", "https://lemmydeals.com", "https://lemmyfi.com", "https://lemmyfly.org", "https://lemmygrad.ml", "https://lemmygrid.com", "https://lemmyis.fun", "https://lemmyngs.social", "https://lemmynsfw.com", "https://lemmyonline.com", "https://lemmypets.xyz", "https://lemmyrs.org", "https://lemmyunchained.net", "https://lemmywinks.com", "https://lemmywinks.xyz", "https://lemnt.telaax.com", "https://lemthony.com", "https://lib.lgbt", "https://libreauto.app", "https://liminal.southfox.me", "https://link.fossdle.org", "https://linkage.ds8.zone", "https://linkopath.com", "https://links.decafbad.com", "https://links.hackliberty.org", "https://links.lowsec.club", "https://links.rocks", "https://links.roobre.es", "https://links.wageoffsite.com", "https://livy.one", "https://lm.bittervets.org", "https://lm.byteme.social", "https://lm.curlefry.net", "https://lm.electrospek.com", "https://lm.gsk.moe", "https://lm.halfassmart.net", "https://lm.inu.is", "https://lm.kalinowski.dev", "https://lm.korako.me", "https://lm.m0e.space", "https://lm.madiator.cloud", "https://lm.melonbread.dev", "https://lm.paradisus.day", "https://lm.put.tf", "https://lm.qtt.no", "https://lm.runnerd.net", "https://lm.sethp.cc", "https://lm.suitwaffle.com", "https://lm.williampuckering.com", "https://lmmy.io", "https://lmmy.net", "https://lmmy.tvdl.dev", "https://lmmy.ylwsgn.cc", "https://lmy.dotcomitsa.website", "https://lmy.drundo.com.au", "https://local106.com", "https://localghost.org", "https://localhost", "https://localhost", "https://localhost", "https://localhost", "https://localhost", "https://logophilia.net", "https://lolimbeer.com", "https://lostcheese.com", "https://lsmu.schmurian.xyz", "https://lucitt.social", "https://mander.xyz", "https://matejc.com", "https://matts.digital", "https://mayheminc.win", "https://mcr.town", "https://meganice.online", "https://melly.0x-ia.moe", "https://merv.news", "https://mesita.link", "https://midwest.social", "https://milksteak.org", "https://mindshare.space", "https://mlem.a-smol-cat.fr", "https://mobilemmohub.com", "https://monero.house", "https://monero.town", "https://monyet.cc", "https://moose.best", "https://moot.place", "https://moto.teamswollen.org", "https://mujico.org", "https://mydomain.ml", "https://mydomain.ml", "https://mydomain.ml", "https://mylem.my", "https://mylemmy.win", "https://narod.city", "https://negativenull.com", "https://neodrain.net", "https://netmonkey.tech", "https://news.cosocial.ca", "https://news.deghg.org", "https://news.idlestate.org", "https://nlemmy.nl", "https://no.lastname.nz", "https://nonewfriends.club", "https://normalcity.life", "https://notdigg.com", "https://notlemmy.notawebsite.fr", "https://notlemmy.site", "https://novoidspace.com", "https://nrsk.no", "https://nunu.dev", "https://nwdr.club", "https://occult.social", "https://oceanbreeze.earth", "https://odin.lanofthedead.xyz", "https://omg.qa", "https://opendmz.social", "https://orava.dev", "https://orzen.games", "https://outpost.zeuslink.net", "https://partizle.com", "https://pasta.faith", "https://pathfinder.social", "https://pathofexile-discuss.com", "https://pawb.social", "https://philly.page", "https://pootusmaximus.xyz", "https://popplesburger.hilciferous.nl", "https://poptalk.scrubbles.tech", "https://possumpat.io", "https://posta.no", "https://preserve.games", "https://programming.dev", "https://proit.org", "https://psychedelia.ink", "https://purrito.kamartaj.xyz", "https://pwzle.com", "https://quex.cc", "https://r-sauna.fi", "https://r.rosettast0ned.com", "https://r.stoi.cc", "https://r196.club", "https://rabbitea.rs", "https://radiation.party", "https://rammy.site", "https://rational-racoon.de", "https://rblind.com", "https://re.tei.li", "https://read.widerweb.org", "https://readit.space", "https://red.cyberhase.de", "https://reddit.moonbeam.town", "https://reddthat.com", "https://retarded.dev", "https://ripo.st", "https://rlyeh.cc", "https://rustyshackleford.cfd", "https://s.jape.work", "https://sambaspy.com", "https://scif6.nsalanparty.com", "https://seattlelunarsociety.org", "https://sedd.it", "https://seemel.ink", "https://selfhosted.forum", "https://sffa.community", "https://sh.itjust.works", "https://sha1.nl", "https://shinobu.cloud", "https://shitposting.monster", "https://shork.online", "https://sigmet.io", "https://silicon-dragon.com", "https://slangenettet.pyjam.as", "https://slrpnk.net", "https://sneakernet.social", "https://snkkis.me", "https://snuv.win", "https://soccer.forum", "https://social.coalition.space", "https://social.cyb3r.dog", "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.nerdswire.de", "https://social.nerdswire.de", "https://social.poisson.me", "https://social.sour.is", "https://social.vmdk.ca", "https://social2.blahajspin.lol", "https://solstice.etbr.top", "https://sopuli.xyz", "https://sowhois.gay", "https://spgrn.com", "https://stammtisch.hallertau.social", "https://stanx.page", "https://stars.leemoon.network", "https://startrek.website", "https://sub.rdls.dev", "https://sub.wetshaving.social", "https://sublight.one", "https://suppo.fi", "https://support.futbol", "https://support.futbol", "https://surlesworld.com", "https://szmer.info", "https://tabletop.place", "https://tagpro.lol", "https://talka.live", "https://techy.news", "https://tezzo.f0rk.pl", "https://thaumatur.ge", "https://thegarden-u4873.vm.elestio.app", "https://thegarden.land", "https://thegarden.land", "https://thelemmy.club", "https://theotter.social", "https://thepride.hexodine.com", "https://thesidewalkends.io", "https://thesimplecorner.org", "https://thevapor.space", "https://toast.ooo", "https://toons.zone", "https://tortoisewrath.com", "https://tslemmy.duckdns.org", "https://ttrpg.network", "https://tucson.social", "https://typemi.me", "https://upvote.au", "https://versalife.duckdns.org", "https://vlemmy.net", "https://voxpop.social", "https://wallstreets.bet", "https://waveform.social", "https://wayfarershaven.eu", "https://weiner.zone", "https://werm.social", "https://whata.clusterfsck.com", "https://whatyoulike.club", "https://whiskers.bim.boats", "https://wilbo.tech", "https://wirebase.org", "https://wired.bluemarch.art", "https://wizanons.dev", "https://wolfballs.com", "https://wumbo.buzz", "https://www.jrz.city", "https://www.korzekwa.io", "https://xffxe4.lol", "https://yall.theatl.social", "https://yiffit.net", "https://ymmel.nl", "https://yogibytes.page", "https://zemmy.cc", "https://zerobytes.monster", "https://zoo.splitlinux.org" ]); var INSTANCES_KBIN = /* @__PURE__ */ new Set([ "https://champserver.net", "https://community.yshi.org", "https://feddit.online", "https://fedi196.gay", "https://fedia.io", "https://fediverse.boo", "https://forum.fail", "https://frmsn.space", "https://gehirneimer.de", "https://jlailu.social", "https://k.fe.derate.me", "https://karab.in", "https://kayb.ee", "https://kbin.buzz", "https://kbin.chat", "https://kbin.cocopoops.com", "https://kbin.dentora.social", "https://kbin.dk", "https://kbin.donar.dev", "https://kbin.fedi.cr", "https://kbin.korgen.xyz", "https://kbin.lgbt", "https://kbin.lol", "https://kbin.mastodont.cat", "https://kbin.melroy.org", "https://kbin.place", "https://kbin.possum.city", "https://kbin.projectsegfau.lt", "https://kbin.rocks", "https://kbin.run", "https://kbin.sh", "https://kbin.social", "https://kbin.tech", "https://kilioa.org", "https://kopnij.in", "https://longley.ws", "https://nadajnik.org", "https://nerdbin.social", "https://nolani.academy", "https://readit.buzz", "https://remy.city", "https://social.tath.link", "https://streetbikes.club", "https://teacup.social", "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_ID: withNS(`show-at-home`), MAKE_HOME_BUTTON_ID: withNS(`make-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` }; 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 isoData?.site_res?.version.startsWith(`0.17`); } var stopEventHandler = (event) => { event.preventDefault(); event.stopPropagation(); }; // 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 GM.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 GM.getValue(`authNoticeShown`)) { alert(`Lemmy Universal Link Switcher: Post/comment rewriting has been set up successfully`); await GM.setValue(`authNoticeShown`, null); } } else if (HOME && !await GM.getValue(`authNoticeShown`)) { await GM.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 GM.getValue(`auth`); } // src/rewriting/url-mapping.js function splitPaths(url) { return url.pathname.split(`/`).slice(1); } function isRemoteLemmyUrl(url) { return url.origin !== HOME && isLemmyInstance(url); } function isRemoteKbinUrl(url) { return url.origin !== HOME && 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; } 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); } }); } 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.innerText = ` .${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); 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/rewriting/make-home.js function addMakeHomeButton() { if (document.querySelector(`#` + constants_default.MAKE_HOME_BUTTON_ID)) return false; if (!isRemoteLemmyUrl(location) || location.pathname !== `/settings`) return false; const insertAfter = document.querySelector(`#user-password`)?.closest(`.card`); if (!insertAfter) return; const button = document.createElement(`button`); button.id = constants_default.MAKE_HOME_BUTTON_ID; button.setAttribute(`class`, `btn btn-block btn-primary mr-4 w-100`); button.innerText = `Make this my home instance for URL rewriting`; button.addEventListener(`click`, () => { setHome(location.origin); button.remove(); }); registerAddedNode(constants_default.MAKE_HOME_BUTTON_ID, `#` + constants_default.MAKE_HOME_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.id = constants_default.SHOW_AT_HOME_BUTTON_ID; showAtHome.innerText = showAtHomeButtonText(); showAtHome.href = localUrl; registerAddedNode(constants_default.SHOW_AT_HOME_BUTTON_ID, `#` + constants_default.SHOW_AT_HOME_BUTTON_ID); 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.querySelector(`#` + constants_default.SHOW_AT_HOME_BUTTON_ID); if (oldButton && oldButton.dataset.creationHref !== location.href) { debug(`Removing old show at home button`); oldButton.remove(); } else if (oldButton) { 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 addedMakeHomeButton = addMakeHomeButton(); if (addedMakeHomeButton) debug(`Added Make Home Button`); 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/gm.js async function setValue(key, value) { await GM.setValue(key, value); } async function getValue(key) { return await GM.getValue(key); } // src/home.js var HOME; async function initHome() { HOME = await getValue(`home`); 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) { if (typeof newHome !== `string`) { newHome = null; } HOME = newHome; await setValue(`home`, newHome); } async function getHome() { return await getValue(`home`); } function updateHomePeriodically() { debug(`Current HOME`, HOME); setInterval(async () => { const prev = HOME; await setHome(await getHome()); if (prev !== HOME) { debug(`HOME changed from`, prev, `to`, HOME); triggerRewrite(); } }, 1337); } // src/main.js (async () => { await initHome(); updateHomePeriodically(); await initAuth(); updateAuthPeriodically(); startRewriting(); })(); })();