Last active
September 29, 2025 19:37
-
-
Save afontenot/63722433f34b799c7e9c9aa4f07b34e2 to your computer and use it in GitHub Desktop.
User script to show Fediverse links for bridged users on their Bluesky profiles
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Bluesky Bridged User Labeler | |
| // @match https://bsky.app/* | |
| // @version 1.6.2 | |
| // ==/UserScript== | |
| // Link to your Fediverse instance (use "{handle}" as a template) | |
| const FEDI_URL = "https://mastodon.social/@{handle}@bsky.brid.gy"; | |
| // Globals | |
| const BRIDGE_ACTOR = "did:plc:xbifsywyv5pka5jlknhv5yv3"; | |
| const APPVIEW_API = "https://public.api.bsky.app/xrpc"; | |
| const CACHE = new Set(); | |
| let ACTIVE_TIMEOUT = null; | |
| const addLink = async function(handle) { | |
| // Bsky hides soft-naved profiles, so we need to find the link from the visible one | |
| const followLink = Array.from( | |
| document.querySelectorAll('a[data-testid="profileHeaderFollowsButton"]') | |
| ).find((link) => link.checkVisibility()); | |
| // sometimes script can get run before React has generated the profile page | |
| if (!followLink) { | |
| ACTIVE_TIMEOUT = setTimeout(addLink, 100, handle); | |
| return; | |
| } | |
| // avoid adding another Fediverse link if one already exists | |
| const profileMeta = followLink.parentElement; | |
| if (profileMeta.getElementsByClassName("bbul-link").length !== 0) { | |
| return; | |
| } | |
| const fedilink = document.createElement("a"); | |
| fedilink.setAttribute("style", followLink.lastElementChild.getAttribute("style")); | |
| fedilink.style.textDecoration = "unset"; | |
| fedilink.href = FEDI_URL.replace("{handle}", handle); | |
| fedilink.textContent = "Fediverse"; | |
| fedilink.classList.add("bbul-link"); | |
| profileMeta.append(fedilink); | |
| ACTIVE_TIMEOUT = null; | |
| }; | |
| const checkHandle = async function() { | |
| // If timer is currently waiting for element, cancel it in case navigation has occurred | |
| if (ACTIVE_TIMEOUT !== null) { | |
| clearTimeout(ACTIVE_TIMEOUT); | |
| ACTIVE_TIMEOUT = null; | |
| } | |
| // get handle from title element (less race-y than document.location) | |
| const title = document.getElementsByTagName("title")[0].textContent; | |
| const parts = title.match(/\(@(.*)\) — Bluesky/); | |
| if (!parts) { | |
| return; | |
| } | |
| const handle = parts[parts.length - 1]; | |
| // avoid hitting endpoint multiple times on soft navigation | |
| if (CACHE.has(handle)) { | |
| addLink(handle); | |
| return; | |
| } | |
| // get actor | |
| let resolveURL = `${APPVIEW_API}/com.atproto.identity.resolveHandle?handle=${handle}`; | |
| const resolveResponse = await fetch(resolveURL); | |
| if (!resolveResponse.ok) { | |
| console.log(`BBUL ERROR: ${resolveResponse.status}`); | |
| return; | |
| } | |
| const actorJSON = await resolveResponse.json(); | |
| const actor = actorJSON.did; | |
| let relURL = `${APPVIEW_API}/app.bsky.graph.getRelationships?actor=${actor}&others=${BRIDGE_ACTOR}`; | |
| const relResponse = await fetch(relURL); | |
| if (!relResponse.ok) { | |
| console.log(`BBUL ERROR: ${relResponse.status}`); | |
| return; | |
| } | |
| const relationshipJSON = await relResponse.json(); | |
| if ( | |
| relationshipJSON.hasOwnProperty("relationships") && ( | |
| relationshipJSON.relationships[0].hasOwnProperty("following") || | |
| relationshipJSON.relationships[0].hasOwnProperty("followedBy") | |
| )) { | |
| CACHE.add(handle); | |
| addLink(handle); | |
| } | |
| }; | |
| checkHandle(); | |
| // catch same-document navigations | |
| const observer = new MutationObserver(function() { | |
| // can't filter on navigations by checking href, since same profile -> profile links can be clicked | |
| checkHandle(); | |
| }); | |
| observer.observe(document.getElementsByTagName("title")[0], {childList: true}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment