Created
June 17, 2025 02:36
-
-
Save blueset/43d08a2998e691da7453e14b98fc96e5 to your computer and use it in GitHub Desktop.
Add indications to accounts on Threads for if it is federated with the ActivityPub fediverse.
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 Threads Federation Detector | |
// @namespace http://tampermonkey.net/ | |
// @version 1.0 | |
// @description Detect federated accounts on Threads. Add indications to accounts on Threads for if it is federated with the ActivityPub fediverse. | |
// @author Eana Hufwe | |
// @match https://www.threads.com/* | |
// @match https://www.threads.net/* | |
// @match https://threads.com/* | |
// @match https://threads.net/* | |
// @grant GM_xmlhttpRequest | |
// @run-at document-start | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// Cache for webfinger results | |
const federationCache = new Map(); | |
const visitedLinks = new WeakSet(); | |
// Add CSS for the federation indicator | |
const style = document.createElement('style'); | |
style.textContent = ` | |
a.federation-indicator.federated { | |
filter: drop-shadow(0 0 3px #00d084) drop-shadow(0 0 6px #00d084); | |
} | |
a.federation-indicator:not(.federated) { | |
filter: drop-shadow(0 0 3px #ff3b30) drop-shadow(0 0 6px #ff3b30); | |
text-decoration: line-through; | |
} | |
`; | |
document.head.appendChild(style); | |
function checkWebfinger(account) { | |
return new Promise((resolve) => { | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: `https://threads.net/.well-known/webfinger?resource=acct:${account}@threads.net`, | |
onload: function(response) { | |
resolve(response.status === 200); | |
}, | |
onerror: function() { | |
resolve(false); | |
} | |
}); | |
}); | |
} | |
async function processAccountLink(link) { | |
if (link.classList.contains('federation-indicator')) { | |
return; | |
} | |
const href = link.getAttribute('href'); | |
const match = href.match(/^\/@([a-zA-Z0-9_\-\.]+)$/); | |
if (!match) { | |
return; | |
} | |
const account = match[1]; | |
// Check cache first | |
if (federationCache.has(account)) { | |
if (federationCache.get(account)) { | |
addFederationIndicator(link); | |
} else { | |
addNonFederationIndicator(link); | |
} | |
return; | |
} | |
// Make webfinger request | |
const isFederated = await checkWebfinger(account); | |
console.log("checkWebfinger", account, isFederated); | |
federationCache.set(account, isFederated); | |
if (isFederated) { | |
addFederationIndicator(link); | |
} else { | |
addNonFederationIndicator(link); | |
} | |
} | |
function addFederationIndicator(link) { | |
// Check if indicator already exists | |
if (link.classList.contains('federation-indicator')) { | |
return; | |
} | |
link.classList.add('federation-indicator', 'federated'); | |
link.title = 'Federated account'; | |
} | |
function addNonFederationIndicator(link) { | |
// Check if indicator already exists | |
if (link.classList.contains('federation-indicator')) { | |
return; | |
} | |
link.classList.add('federation-indicator'); | |
link.title = 'Non-federated account'; | |
} | |
function scanForAccountLinks() { | |
const links = document.querySelectorAll('a[href^="/@"]'); | |
links.forEach(processAccountLink); | |
} | |
// Initial scan when DOM is ready | |
if (document.readyState === 'loading') { | |
document.addEventListener('DOMContentLoaded', scanForAccountLinks); | |
} else { | |
scanForAccountLinks(); | |
} | |
// Watch for dynamic content changes | |
const observer = new MutationObserver((mutations) => { | |
let hasNewLinks = false; | |
mutations.forEach((mutation) => { | |
mutation.addedNodes.forEach((node) => { | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
const shouldLog = node?.innerHtml?.match(/href="@\/[a-zA-Z0-9_\-\.]+"/g); | |
if (node.matches('a[href^="/@"]')) { | |
if (shouldLog) console.log("added node match", node); | |
processAccountLink(node); | |
hasNewLinks = true; | |
} else if (node.querySelector) { | |
const newLinks = node.querySelectorAll('a[href^="/@"]'); | |
if (shouldLog) console.log("added node qs", node, newLinks); | |
if (newLinks.length > 0) { | |
newLinks.forEach(processAccountLink); | |
hasNewLinks = true; | |
} | |
} else { | |
if (shouldLog) console.log("added node else", node); | |
} | |
} | |
}); | |
}); | |
}); | |
observer.observe(document.body || document.documentElement, { | |
childList: true, | |
subtree: true | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment