Skip to content

Instantly share code, notes, and snippets.

@afontenot
Last active September 29, 2025 19:37
Show Gist options
  • Save afontenot/63722433f34b799c7e9c9aa4f07b34e2 to your computer and use it in GitHub Desktop.
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
// ==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