// ==UserScript== // @name Bluesky Community Verifications // @namespace https://tangled.sh/@dunkirk.sh/serif/verifications // @version 0.2.1 // @description Shows verification badges from trusted community members on Bluesky // @author Kieran Klukas // @match https://bsky.app/* // @grant none // @run-at document-end // @updateURL https://tangled.sh/@dunkirk.sh/serif/raw/main/bluesky-community-verifications.user.meta.js // @downloadURL https://tangled.sh/@dunkirk.sh/serif/raw/main/bluesky-community-verifications.user.js // ==/UserScript== (() => { // Script has already been initialized check if (window.bskyTrustedUsersInitialized) { console.log("Trusted Users script already initialized"); return; } // Mark script as initialized window.bskyTrustedUsersInitialized = true; // Define storage keys const TRUSTED_USERS_STORAGE_KEY = "bsky_trusted_users"; const VERIFICATION_CACHE_STORAGE_KEY = "bsky_verification_cache"; const CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000; // 24 hours const BADGE_TYPE_STORAGE_KEY = "bsky_verification_badge_type"; const BADGE_COLOR_STORAGE_KEY = "bsky_verification_badge_color"; // Default badge configuration const DEFAULT_BADGE_TYPE = "checkmark"; const DEFAULT_BADGE_COLOR = "#0070ff"; // Functions to get/set badge configuration const getBadgeType = () => { return localStorage.getItem(BADGE_TYPE_STORAGE_KEY) || DEFAULT_BADGE_TYPE; }; const getBadgeColor = () => { return localStorage.getItem(BADGE_COLOR_STORAGE_KEY) || DEFAULT_BADGE_COLOR; }; const saveBadgeType = (type) => { localStorage.setItem(BADGE_TYPE_STORAGE_KEY, type); }; const saveBadgeColor = (color) => { localStorage.setItem(BADGE_COLOR_STORAGE_KEY, color); }; const getBadgeContent = (type) => { switch (type) { case "checkmark": return "✓"; case "star": return "★"; case "heart": return "♥"; case "shield": return "🛡️"; case "lock": return "🔒"; case "verified": return ``; default: return "✓"; } }; // Function to get trusted users from local storage const getTrustedUsers = () => { const storedUsers = localStorage.getItem(TRUSTED_USERS_STORAGE_KEY); return storedUsers ? JSON.parse(storedUsers) : []; }; // Function to save trusted users to local storage const saveTrustedUsers = (users) => { localStorage.setItem(TRUSTED_USERS_STORAGE_KEY, JSON.stringify(users)); }; // Populate default trusted users if not already set if (!localStorage.getItem(TRUSTED_USERS_STORAGE_KEY)) { const defaultTrustedUsers = [ "bsky.app", "nytimes.com", "wired.com", "theathletic.bsky.social", ]; saveTrustedUsers(defaultTrustedUsers); console.log("Added default trusted users:", defaultTrustedUsers); } // Function to add a trusted user const addTrustedUser = (handle) => { const users = getTrustedUsers(); if (!users.includes(handle)) { users.push(handle); saveTrustedUsers(users); } }; // Function to remove a trusted user const removeTrustedUser = (handle) => { const users = getTrustedUsers(); const updatedUsers = users.filter((user) => user !== handle); saveTrustedUsers(updatedUsers); }; // Cache functions const getVerificationCache = () => { const cache = localStorage.getItem(VERIFICATION_CACHE_STORAGE_KEY); return cache ? JSON.parse(cache) : {}; }; const saveVerificationCache = (cache) => { localStorage.setItem(VERIFICATION_CACHE_STORAGE_KEY, JSON.stringify(cache)); }; const getCachedVerifications = (user) => { const cache = getVerificationCache(); return cache[user] || null; }; const cacheVerifications = (user, records) => { const cache = getVerificationCache(); cache[user] = { records, timestamp: Date.now(), }; saveVerificationCache(cache); }; const isCacheValid = (cacheEntry) => { return cacheEntry && Date.now() - cacheEntry.timestamp < CACHE_EXPIRY_TIME; }; // Function to remove a specific user from the verification cache const removeUserFromCache = (handle) => { const cache = getVerificationCache(); if (cache[handle]) { delete cache[handle]; saveVerificationCache(cache); console.log(`Removed ${handle} from verification cache`); } }; const clearCache = () => { localStorage.removeItem(VERIFICATION_CACHE_STORAGE_KEY); console.log("Verification cache cleared"); }; // Store all verifiers for a profile let profileVerifiers = []; // Store current profile DID let currentProfileDid = null; // Function to check if a trusted user has verified the current profile const checkTrustedUserVerifications = async (profileDid) => { currentProfileDid = profileDid; // Store for recheck functionality const trustedUsers = getTrustedUsers(); profileVerifiers = []; // Reset the verifiers list if (trustedUsers.length === 0) { console.log("No trusted users to check for verifications"); return false; } console.log(`Checking if any trusted users have verified ${profileDid}`); // Use Promise.all to fetch all verification data in parallel const verificationPromises = trustedUsers.map(async (trustedUser) => { try { // Helper function to fetch all verification records with pagination const fetchAllVerifications = async (user) => { // Check cache first const cachedData = getCachedVerifications(user); if (cachedData && isCacheValid(cachedData)) { console.log(`Using cached verification data for ${user}`); return cachedData.records; } console.log(`Fetching fresh verification data for ${user}`); let allRecords = []; let cursor = null; let hasMore = true; while (hasMore) { const url = cursor ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification&cursor=${cursor}` : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification`; const response = await fetch(url); const data = await response.json(); if (data.records && data.records.length > 0) { allRecords = [...allRecords, ...data.records]; } if (data.cursor) { cursor = data.cursor; } else { hasMore = false; } } // Save to cache cacheVerifications(user, allRecords); return allRecords; }; // Fetch all verification records for this trusted user const records = await fetchAllVerifications(trustedUser); console.log(`Received verification data from ${trustedUser}`, { records, }); // Check if this trusted user has verified the current profile if (records.length > 0) { for (const record of records) { if (record.value && record.value.subject === profileDid) { console.log( `${profileDid} is verified by trusted user ${trustedUser}`, ); // Add to verifiers list profileVerifiers.push(trustedUser); break; // Once we find a verification, we can stop checking } } } return { trustedUser, success: true }; } catch (error) { console.error( `Error checking verifications from ${trustedUser}:`, error, ); return { trustedUser, success: false, error }; } }); // Wait for all verification checks to complete const results = await Promise.all(verificationPromises); // Log summary of API calls console.log(`API calls completed: ${results.length}`); console.log(`Successful calls: ${results.filter((r) => r.success).length}`); console.log(`Failed calls: ${results.filter((r) => !r.success).length}`); // If we have verifiers, display the badge if (profileVerifiers.length > 0) { displayVerificationBadge(profileVerifiers); return true; } console.log(`${profileDid} is not verified by any trusted users`); return false; }; // Function to display verification badge on the profile const displayVerificationBadge = (verifierHandles) => { // Find the profile header or name element to add the badge to const nameElements = document.querySelectorAll( '[data-testid="profileHeaderDisplayName"]', ); const nameElement = nameElements[nameElements.length - 1]; console.log("nameElement", nameElement); if (nameElement) { // Remove existing badge if present const existingBadge = document.getElementById( "user-trusted-verification-badge", ); if (existingBadge) { existingBadge.remove(); } const badge = document.createElement("span"); badge.id = "user-trusted-verification-badge"; // Get user badge preferences const badgeType = getBadgeType(); const badgeColor = getBadgeColor(); // Set badge content based on type badge.innerHTML = getBadgeContent(badgeType); // check if there is a div with button underneath // Check if this user is verified by Bluesky const isBlueskyVerified = nameElement.querySelector("div button"); if (isBlueskyVerified) isBlueskyVerified.remove(); // Create tooltip text with all verifiers const verifiersText = verifierHandles.length > 1 ? `Verified by: ${verifierHandles.join(", ")}` : `Verified by ${verifierHandles[0]}`; badge.title = verifiersText; badge.style.cssText = ` background-color: ${badgeColor}; color: white; border-radius: 50%; width: 22px; height: 22px; margin-left: 8px; font-size: 14px; font-weight: bold; cursor: help; display: inline-flex; align-items: center; justify-content: center; vertical-align: 5px; `; // Add a click event to show all verifiers badge.addEventListener("click", (e) => { e.stopPropagation(); showVerifiersPopup(verifierHandles); }); nameElement.appendChild(badge); } }; // Function to show a popup with all verifiers const showVerifiersPopup = (verifierHandles) => { // Remove existing popup if any const existingPopup = document.getElementById("verifiers-popup"); if (existingPopup) { existingPopup.remove(); } // Create popup const popup = document.createElement("div"); popup.id = "verifiers-popup"; popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #24273A; padding: 20px; border-radius: 10px; z-index: 10002; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); max-width: 400px; width: 90%; `; // Create popup content popup.innerHTML = `
No trusted users added yet.
"; return; } for (const user of users) { const userItem = document.createElement("div"); userItem.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #eee; `; userItem.innerHTML = ` ${user} `; trustedUsersList.appendChild(userItem); } // Add event listeners to remove buttons const removeButtons = document.querySelectorAll(".remove-user"); for (const btn of removeButtons) { btn.addEventListener("click", (e) => { const handle = e.target.getAttribute("data-handle"); removeTrustedUser(handle); removeUserFromCache(handle); updateTrustedUsersList(); }); } }; const searchUsers = async (searchQuery) => { if (!searchQuery || searchQuery.length < 2) return []; try { const response = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?term=${encodeURIComponent(searchQuery)}&limit=5`, ); const data = await response.json(); return data.actors || []; } catch (error) { console.error("Error searching for users:", error); return []; } }; // Function to create and show the autocomplete dropdown const showAutocompleteResults = (results, inputElement) => { // Remove existing dropdown if any const existingDropdown = document.getElementById("autocomplete-dropdown"); if (existingDropdown) existingDropdown.remove(); if (results.length === 0) return; // Create dropdown const dropdown = document.createElement("div"); dropdown.id = "autocomplete-dropdown"; dropdown.style.cssText = ` position: absolute; background-color: #2A2E3D; border: 1px solid #444; border-radius: 4px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); max-height: 300px; overflow-y: auto; width: ${inputElement.offsetWidth}px; z-index: 10002; margin-top: 2px; `; // Position dropdown below input const inputRect = inputElement.getBoundingClientRect(); dropdown.style.left = `${inputRect.left}px`; dropdown.style.top = `${inputRect.bottom}px`; // Add results to dropdown for (const user of results) { const userItem = document.createElement("div"); userItem.className = "autocomplete-item"; userItem.style.cssText = ` display: flex; align-items: center; padding: 8px 12px; cursor: pointer; color: white; border-bottom: 1px solid #444; `; userItem.onmouseover = () => { userItem.style.backgroundColor = "#3A3F55"; }; userItem.onmouseout = () => { userItem.style.backgroundColor = ""; }; // Add profile picture const avatar = document.createElement("img"); avatar.src = user.avatar || "https://bsky.app/static/default-avatar.png"; avatar.style.cssText = ` width: 32px; height: 32px; border-radius: 50%; margin-right: 10px; object-fit: cover; `; // Add user info const userInfo = document.createElement("div"); userInfo.style.cssText = ` display: flex; flex-direction: column; `; const displayName = document.createElement("div"); displayName.textContent = user.displayName || user.handle; displayName.style.fontWeight = "bold"; const handle = document.createElement("div"); handle.textContent = user.handle; handle.style.fontSize = "0.8em"; handle.style.opacity = "0.8"; userInfo.appendChild(displayName); userInfo.appendChild(handle); userItem.appendChild(avatar); userItem.appendChild(userInfo); // Handle click on user item userItem.addEventListener("click", () => { inputElement.value = user.handle; dropdown.remove(); }); dropdown.appendChild(userItem); } document.body.appendChild(dropdown); // Close dropdown when clicking outside document.addEventListener("click", function closeDropdown(e) { if (e.target !== inputElement && !dropdown.contains(e.target)) { dropdown.remove(); document.removeEventListener("click", closeDropdown); } }); }; // Function to import verifications from the current user const importVerificationsFromSelf = async () => { try { // Check if we can determine the current user const bskyStorageData = localStorage.getItem("BSKY_STORAGE"); let userData = null; if (bskyStorageData) { try { const bskyStorage = JSON.parse(bskyStorageData); if (bskyStorage.session.currentAccount) { userData = bskyStorage.session.currentAccount; } } catch (error) { console.error("Error parsing BSKY_STORAGE data:", error); } } if (!userData || !userData.handle) { alert( "Could not determine your Bluesky handle. Please ensure you're logged in.", ); return; } if (!userData || !userData.handle) { alert( "Unable to determine your Bluesky handle. Make sure you're logged in.", ); return; } const userHandle = userData.handle; // Show loading state const importButton = document.getElementById("importVerificationsBtn"); const originalText = importButton.textContent; importButton.textContent = "Importing..."; importButton.disabled = true; // Fetch verification records from the user's account with pagination let allRecords = []; let cursor = null; let hasMore = true; while (hasMore) { const url = cursor ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification&cursor=${cursor}` : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification`; const verificationResponse = await fetch(url); const data = await verificationResponse.json(); if (data.records && data.records.length > 0) { allRecords = [...allRecords, ...data.records]; } if (data.cursor) { cursor = data.cursor; } else { hasMore = false; } } const verificationData = { records: allRecords }; if (!verificationData.records || verificationData.records.length === 0) { alert("No verification records found in your account."); importButton.textContent = originalText; importButton.disabled = false; return; } // Extract the handles of verified users const verifiedUsers = []; for (const record of verificationData.records) { console.log(record.value.handle); verifiedUsers.push(record.value.handle); } // Add all found users to trusted users let addedCount = 0; for (const handle of verifiedUsers) { const existingUsers = getTrustedUsers(); if (!existingUsers.includes(handle)) { addTrustedUser(handle); addedCount++; } } // Update the UI updateTrustedUsersList(); // Reset button state importButton.textContent = originalText; importButton.disabled = false; // Show result alert( `Successfully imported ${addedCount} verified users from your account.`, ); } catch (error) { console.error("Error importing verifications:", error); alert("Error importing verifications. Check console for details."); const importButton = document.getElementById("importVerificationsBtn"); if (importButton) { importButton.textContent = "Import Verifications"; importButton.disabled = false; } } }; const addSettingsButton = () => { // Check if we're on the settings page if (!window.location.href.includes("bsky.app/settings")) { return; } // Check if our button already exists to avoid duplicates if (document.getElementById("community-verifications-settings-button")) { return; } // Find the right place to insert our button (after content-and-media link) const contentMediaLink = document.querySelector( 'a[href="/settings/content-and-media"]', ); if (!contentMediaLink) { console.log("Could not find content-and-media link to insert after"); return; } // Clone the existing link and modify it const verificationButton = contentMediaLink.cloneNode(true); verificationButton.id = "community-verifications-settings-button"; verificationButton.href = "#"; // No actual link, we'll handle click with JS verificationButton.setAttribute("aria-label", "Community Verifications"); const highlightColor = verificationButton.firstChild.style.backgroundColor || "rgb(30,41,54)"; // Add hover effect to highlight the button verificationButton.addEventListener("mouseover", () => { verificationButton.firstChild.style.backgroundColor = highlightColor; }); verificationButton.addEventListener("mouseout", () => { verificationButton.firstChild.style.backgroundColor = null; }); // Update the text content const textDiv = verificationButton.querySelector(".css-146c3p1"); if (textDiv) { textDiv.textContent = "Community Verifications"; } // Update the icon const iconDiv = verificationButton.querySelector( ".css-175oi2r[style*='width: 28px']", ); if (iconDiv) { iconDiv.innerHTML = ` `; } // Insert our button after the content-and-media link const parentElement = contentMediaLink.parentElement; parentElement.insertBefore( verificationButton, contentMediaLink.nextSibling, ); // Add click event to open our settings modal verificationButton.addEventListener("click", (e) => { e.preventDefault(); if (settingsModal) { settingsModal.style.display = "flex"; updateTrustedUsersList(); } else { createSettingsModal(); } }); console.log("Added Community Verifications button to settings page"); }; // Function to create the settings modal const createSettingsModal = () => { // Create modal container settingsModal = document.createElement("div"); settingsModal.id = "bsky-trusted-settings-modal"; settingsModal.style.cssText = ` display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 10001; justify-content: center; align-items: center; `; // Create modal content const modalContent = document.createElement("div"); modalContent.style.cssText = ` background-color: #24273A; padding: 20px; border-radius: 10px; width: 400px; max-height: 80vh; overflow-y: auto; `; // Create modal header const modalHeader = document.createElement("div"); modalHeader.innerHTML = `Badge Type:
Badge Color:
Preview:
Add Bluesky handles you trust: