// ==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 }); })();