the home of serif.blue
1// ==UserScript== 2// @name Bluesky Community Verifications 3// @namespace https://tangled.sh/@dunkirk.sh/serif/verifications 4// @version 0.2 5// @description Shows verification badges from trusted community members on Bluesky 6// @author Kieran Klukas 7// @match https://bsky.app/* 8// @grant none 9// @run-at document-end 10// ==/UserScript== 11 12(() => { 13 // Script has already been initialized check 14 if (window.bskyTrustedUsersInitialized) { 15 console.log("Trusted Users script already initialized"); 16 return; 17 } 18 19 // Mark script as initialized 20 window.bskyTrustedUsersInitialized = true; 21 22 // Define storage keys 23 const TRUSTED_USERS_STORAGE_KEY = "bsky_trusted_users"; 24 const VERIFICATION_CACHE_STORAGE_KEY = "bsky_verification_cache"; 25 const CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000; // 24 hours 26 const BADGE_TYPE_STORAGE_KEY = "bsky_verification_badge_type"; 27 const BADGE_COLOR_STORAGE_KEY = "bsky_verification_badge_color"; 28 29 // Default badge configuration 30 const DEFAULT_BADGE_TYPE = "checkmark"; 31 const DEFAULT_BADGE_COLOR = "#0070ff"; 32 33 // Functions to get/set badge configuration 34 const getBadgeType = () => { 35 return localStorage.getItem(BADGE_TYPE_STORAGE_KEY) || DEFAULT_BADGE_TYPE; 36 }; 37 38 const getBadgeColor = () => { 39 return localStorage.getItem(BADGE_COLOR_STORAGE_KEY) || DEFAULT_BADGE_COLOR; 40 }; 41 42 const saveBadgeType = (type) => { 43 localStorage.setItem(BADGE_TYPE_STORAGE_KEY, type); 44 }; 45 46 const saveBadgeColor = (color) => { 47 localStorage.setItem(BADGE_COLOR_STORAGE_KEY, color); 48 }; 49 50 const getBadgeContent = (type) => { 51 switch (type) { 52 case "checkmark": 53 return "✓"; 54 case "star": 55 return "★"; 56 case "heart": 57 return "♥"; 58 case "shield": 59 return "🛡️"; 60 case "lock": 61 return "🔒"; 62 case "verified": 63 return `<svg viewBox="0 0 24 24" width="16" height="16"> 64 <path fill="white" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path> 65 </svg>`; 66 default: 67 return "✓"; 68 } 69 }; 70 71 // Function to get trusted users from local storage 72 const getTrustedUsers = () => { 73 const storedUsers = localStorage.getItem(TRUSTED_USERS_STORAGE_KEY); 74 return storedUsers ? JSON.parse(storedUsers) : []; 75 }; 76 77 // Function to save trusted users to local storage 78 const saveTrustedUsers = (users) => { 79 localStorage.setItem(TRUSTED_USERS_STORAGE_KEY, JSON.stringify(users)); 80 }; 81 82 // Populate default trusted users if not already set 83 if (!localStorage.getItem(TRUSTED_USERS_STORAGE_KEY)) { 84 const defaultTrustedUsers = [ 85 "bsky.app", 86 "nytimes.com", 87 "wired.com", 88 "theathletic.bsky.social", 89 ]; 90 saveTrustedUsers(defaultTrustedUsers); 91 console.log("Added default trusted users:", defaultTrustedUsers); 92 } 93 94 // Function to add a trusted user 95 const addTrustedUser = (handle) => { 96 const users = getTrustedUsers(); 97 if (!users.includes(handle)) { 98 users.push(handle); 99 saveTrustedUsers(users); 100 } 101 }; 102 103 // Function to remove a trusted user 104 const removeTrustedUser = (handle) => { 105 const users = getTrustedUsers(); 106 const updatedUsers = users.filter((user) => user !== handle); 107 saveTrustedUsers(updatedUsers); 108 }; 109 110 // Cache functions 111 const getVerificationCache = () => { 112 const cache = localStorage.getItem(VERIFICATION_CACHE_STORAGE_KEY); 113 return cache ? JSON.parse(cache) : {}; 114 }; 115 116 const saveVerificationCache = (cache) => { 117 localStorage.setItem(VERIFICATION_CACHE_STORAGE_KEY, JSON.stringify(cache)); 118 }; 119 120 const getCachedVerifications = (user) => { 121 const cache = getVerificationCache(); 122 return cache[user] || null; 123 }; 124 125 const cacheVerifications = (user, records) => { 126 const cache = getVerificationCache(); 127 cache[user] = { 128 records, 129 timestamp: Date.now(), 130 }; 131 saveVerificationCache(cache); 132 }; 133 134 const isCacheValid = (cacheEntry) => { 135 return cacheEntry && Date.now() - cacheEntry.timestamp < CACHE_EXPIRY_TIME; 136 }; 137 138 // Function to remove a specific user from the verification cache 139 const removeUserFromCache = (handle) => { 140 const cache = getVerificationCache(); 141 if (cache[handle]) { 142 delete cache[handle]; 143 saveVerificationCache(cache); 144 console.log(`Removed ${handle} from verification cache`); 145 } 146 }; 147 148 const clearCache = () => { 149 localStorage.removeItem(VERIFICATION_CACHE_STORAGE_KEY); 150 console.log("Verification cache cleared"); 151 }; 152 153 // Store all verifiers for a profile 154 let profileVerifiers = []; 155 156 // Store current profile DID 157 let currentProfileDid = null; 158 159 // Function to check if a trusted user has verified the current profile 160 const checkTrustedUserVerifications = async (profileDid) => { 161 currentProfileDid = profileDid; // Store for recheck functionality 162 const trustedUsers = getTrustedUsers(); 163 profileVerifiers = []; // Reset the verifiers list 164 165 if (trustedUsers.length === 0) { 166 console.log("No trusted users to check for verifications"); 167 return false; 168 } 169 170 console.log(`Checking if any trusted users have verified ${profileDid}`); 171 172 // Use Promise.all to fetch all verification data in parallel 173 const verificationPromises = trustedUsers.map(async (trustedUser) => { 174 try { 175 // Helper function to fetch all verification records with pagination 176 const fetchAllVerifications = async (user) => { 177 // Check cache first 178 const cachedData = getCachedVerifications(user); 179 if (cachedData && isCacheValid(cachedData)) { 180 console.log(`Using cached verification data for ${user}`); 181 return cachedData.records; 182 } 183 184 console.log(`Fetching fresh verification data for ${user}`); 185 let allRecords = []; 186 let cursor = null; 187 let hasMore = true; 188 189 while (hasMore) { 190 const url = cursor 191 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification&cursor=${cursor}` 192 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification`; 193 194 const response = await fetch(url); 195 const data = await response.json(); 196 197 if (data.records && data.records.length > 0) { 198 allRecords = [...allRecords, ...data.records]; 199 } 200 201 if (data.cursor) { 202 cursor = data.cursor; 203 } else { 204 hasMore = false; 205 } 206 } 207 208 // Save to cache 209 cacheVerifications(user, allRecords); 210 return allRecords; 211 }; 212 213 // Fetch all verification records for this trusted user 214 const records = await fetchAllVerifications(trustedUser); 215 216 console.log(`Received verification data from ${trustedUser}`, { 217 records, 218 }); 219 220 // Check if this trusted user has verified the current profile 221 if (records.length > 0) { 222 for (const record of records) { 223 if (record.value && record.value.subject === profileDid) { 224 console.log( 225 `${profileDid} is verified by trusted user ${trustedUser}`, 226 ); 227 228 // Add to verifiers list 229 profileVerifiers.push(trustedUser); 230 break; // Once we find a verification, we can stop checking 231 } 232 } 233 } 234 return { trustedUser, success: true }; 235 } catch (error) { 236 console.error( 237 `Error checking verifications from ${trustedUser}:`, 238 error, 239 ); 240 return { trustedUser, success: false, error }; 241 } 242 }); 243 244 // Wait for all verification checks to complete 245 const results = await Promise.all(verificationPromises); 246 247 // Log summary of API calls 248 console.log(`API calls completed: ${results.length}`); 249 console.log(`Successful calls: ${results.filter((r) => r.success).length}`); 250 console.log(`Failed calls: ${results.filter((r) => !r.success).length}`); 251 252 // If we have verifiers, display the badge 253 if (profileVerifiers.length > 0) { 254 displayVerificationBadge(profileVerifiers); 255 return true; 256 } 257 258 console.log(`${profileDid} is not verified by any trusted users`); 259 260 return false; 261 }; 262 263 // Function to display verification badge on the profile 264 const displayVerificationBadge = (verifierHandles) => { 265 // Find the profile header or name element to add the badge to 266 const nameElements = document.querySelectorAll( 267 '[data-testid="profileHeaderDisplayName"]', 268 ); 269 const nameElement = nameElements[nameElements.length - 1]; 270 271 console.log("nameElement", nameElement); 272 273 if (nameElement) { 274 // Remove existing badge if present 275 const existingBadge = document.getElementById( 276 "user-trusted-verification-badge", 277 ); 278 if (existingBadge) { 279 existingBadge.remove(); 280 } 281 282 const badge = document.createElement("span"); 283 badge.id = "user-trusted-verification-badge"; 284 285 // Get user badge preferences 286 const badgeType = getBadgeType(); 287 const badgeColor = getBadgeColor(); 288 289 // Set badge content based on type 290 badge.innerHTML = getBadgeContent(badgeType); 291 292 // check if there is a div with button underneath 293 // Check if this user is verified by Bluesky 294 const isBlueskyVerified = nameElement.querySelector("div button"); 295 if (isBlueskyVerified) isBlueskyVerified.remove(); 296 297 // Create tooltip text with all verifiers 298 const verifiersText = 299 verifierHandles.length > 1 300 ? `Verified by: ${verifierHandles.join(", ")}` 301 : `Verified by ${verifierHandles[0]}`; 302 303 badge.title = verifiersText; 304 badge.style.cssText = ` 305 background-color: ${badgeColor}; 306 color: white; 307 border-radius: 50%; 308 width: 22px; 309 height: 22px; 310 margin-left: 8px; 311 font-size: 14px; 312 font-weight: bold; 313 cursor: help; 314 display: inline-flex; 315 align-items: center; 316 justify-content: center; 317 vertical-align: 5px; 318 `; 319 320 // Add a click event to show all verifiers 321 badge.addEventListener("click", (e) => { 322 e.stopPropagation(); 323 showVerifiersPopup(verifierHandles); 324 }); 325 326 nameElement.appendChild(badge); 327 } 328 }; 329 330 // Function to show a popup with all verifiers 331 const showVerifiersPopup = (verifierHandles) => { 332 // Remove existing popup if any 333 const existingPopup = document.getElementById("verifiers-popup"); 334 if (existingPopup) { 335 existingPopup.remove(); 336 } 337 338 // Create popup 339 const popup = document.createElement("div"); 340 popup.id = "verifiers-popup"; 341 popup.style.cssText = ` 342 position: fixed; 343 top: 50%; 344 left: 50%; 345 transform: translate(-50%, -50%); 346 background-color: #24273A; 347 padding: 20px; 348 border-radius: 10px; 349 z-index: 10002; 350 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 351 max-width: 400px; 352 width: 90%; 353 `; 354 355 // Create popup content 356 popup.innerHTML = ` 357 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3> 358 <div style="max-height: 300px; overflow-y: auto;"> 359 ${verifierHandles 360 .map( 361 (handle) => ` 362 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;"> 363 ${handle} 364 </div> 365 `, 366 ) 367 .join("")} 368 </div> 369 <button id="close-verifiers-popup" style=" 370 margin-top: 15px; 371 padding: 8px 15px; 372 background-color: #473A3A; 373 color: white; 374 border: none; 375 border-radius: 4px; 376 cursor: pointer; 377 ">Close</button> 378 `; 379 380 // Add to body 381 document.body.appendChild(popup); 382 383 // Add close handler 384 document 385 .getElementById("close-verifiers-popup") 386 .addEventListener("click", () => { 387 popup.remove(); 388 }); 389 390 // Close when clicking outside 391 document.addEventListener("click", function closePopup(e) { 392 if (!popup.contains(e.target)) { 393 popup.remove(); 394 document.removeEventListener("click", closePopup); 395 } 396 }); 397 }; 398 399 // Create settings modal 400 let settingsModal = null; 401 402 // Function to update the list of trusted users in the UI 403 const updateTrustedUsersList = () => { 404 const trustedUsersList = document.getElementById("trustedUsersList"); 405 if (!trustedUsersList) return; 406 407 const users = getTrustedUsers(); 408 trustedUsersList.innerHTML = ""; 409 410 if (users.length === 0) { 411 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>"; 412 return; 413 } 414 415 for (const user of users) { 416 const userItem = document.createElement("div"); 417 userItem.style.cssText = ` 418 display: flex; 419 justify-content: space-between; 420 align-items: center; 421 padding: 8px 0; 422 border-bottom: 1px solid #eee; 423 `; 424 425 userItem.innerHTML = ` 426 <span>${user}</span> 427 <button class="remove-user" data-handle="${user}" style="background-color: #CE3838; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer;">Remove</button> 428 `; 429 430 trustedUsersList.appendChild(userItem); 431 } 432 433 // Add event listeners to remove buttons 434 const removeButtons = document.querySelectorAll(".remove-user"); 435 for (const btn of removeButtons) { 436 btn.addEventListener("click", (e) => { 437 const handle = e.target.getAttribute("data-handle"); 438 removeTrustedUser(handle); 439 removeUserFromCache(handle); 440 updateTrustedUsersList(); 441 }); 442 } 443 }; 444 445 const searchUsers = async (searchQuery) => { 446 if (!searchQuery || searchQuery.length < 2) return []; 447 448 try { 449 const response = await fetch( 450 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?term=${encodeURIComponent(searchQuery)}&limit=5`, 451 ); 452 const data = await response.json(); 453 return data.actors || []; 454 } catch (error) { 455 console.error("Error searching for users:", error); 456 return []; 457 } 458 }; 459 460 // Function to create and show the autocomplete dropdown 461 const showAutocompleteResults = (results, inputElement) => { 462 // Remove existing dropdown if any 463 const existingDropdown = document.getElementById("autocomplete-dropdown"); 464 if (existingDropdown) existingDropdown.remove(); 465 466 if (results.length === 0) return; 467 468 // Create dropdown 469 const dropdown = document.createElement("div"); 470 dropdown.id = "autocomplete-dropdown"; 471 dropdown.style.cssText = ` 472 position: absolute; 473 background-color: #2A2E3D; 474 border: 1px solid #444; 475 border-radius: 4px; 476 box-shadow: 0 4px 8px rgba(0,0,0,0.2); 477 max-height: 300px; 478 overflow-y: auto; 479 width: ${inputElement.offsetWidth}px; 480 z-index: 10002; 481 margin-top: 2px; 482 `; 483 484 // Position dropdown below input 485 const inputRect = inputElement.getBoundingClientRect(); 486 dropdown.style.left = `${inputRect.left}px`; 487 dropdown.style.top = `${inputRect.bottom}px`; 488 489 // Add results to dropdown 490 for (const user of results) { 491 const userItem = document.createElement("div"); 492 userItem.className = "autocomplete-item"; 493 userItem.style.cssText = ` 494 display: flex; 495 align-items: center; 496 padding: 8px 12px; 497 cursor: pointer; 498 color: white; 499 border-bottom: 1px solid #444; 500 `; 501 userItem.onmouseover = () => { 502 userItem.style.backgroundColor = "#3A3F55"; 503 }; 504 userItem.onmouseout = () => { 505 userItem.style.backgroundColor = ""; 506 }; 507 508 // Add profile picture 509 const avatar = document.createElement("img"); 510 avatar.src = user.avatar || "https://bsky.app/static/default-avatar.png"; 511 avatar.style.cssText = ` 512 width: 32px; 513 height: 32px; 514 border-radius: 50%; 515 margin-right: 10px; 516 object-fit: cover; 517 `; 518 519 // Add user info 520 const userInfo = document.createElement("div"); 521 userInfo.style.cssText = ` 522 display: flex; 523 flex-direction: column; 524 `; 525 526 const displayName = document.createElement("div"); 527 displayName.textContent = user.displayName || user.handle; 528 displayName.style.fontWeight = "bold"; 529 530 const handle = document.createElement("div"); 531 handle.textContent = user.handle; 532 handle.style.fontSize = "0.8em"; 533 handle.style.opacity = "0.8"; 534 535 userInfo.appendChild(displayName); 536 userInfo.appendChild(handle); 537 538 userItem.appendChild(avatar); 539 userItem.appendChild(userInfo); 540 541 // Handle click on user item 542 userItem.addEventListener("click", () => { 543 inputElement.value = user.handle; 544 dropdown.remove(); 545 }); 546 547 dropdown.appendChild(userItem); 548 } 549 550 document.body.appendChild(dropdown); 551 552 // Close dropdown when clicking outside 553 document.addEventListener("click", function closeDropdown(e) { 554 if (e.target !== inputElement && !dropdown.contains(e.target)) { 555 dropdown.remove(); 556 document.removeEventListener("click", closeDropdown); 557 } 558 }); 559 }; 560 561 // Function to import verifications from the current user 562 const importVerificationsFromSelf = async () => { 563 try { 564 // Check if we can determine the current user 565 const bskyStorageData = localStorage.getItem("BSKY_STORAGE"); 566 let userData = null; 567 568 if (bskyStorageData) { 569 try { 570 const bskyStorage = JSON.parse(bskyStorageData); 571 if (bskyStorage.session.currentAccount) { 572 userData = bskyStorage.session.currentAccount; 573 } 574 } catch (error) { 575 console.error("Error parsing BSKY_STORAGE data:", error); 576 } 577 } 578 579 if (!userData || !userData.handle) { 580 alert( 581 "Could not determine your Bluesky handle. Please ensure you're logged in.", 582 ); 583 return; 584 } 585 586 if (!userData || !userData.handle) { 587 alert( 588 "Unable to determine your Bluesky handle. Make sure you're logged in.", 589 ); 590 return; 591 } 592 593 const userHandle = userData.handle; 594 595 // Show loading state 596 const importButton = document.getElementById("importVerificationsBtn"); 597 const originalText = importButton.textContent; 598 importButton.textContent = "Importing..."; 599 importButton.disabled = true; 600 601 // Fetch verification records from the user's account with pagination 602 let allRecords = []; 603 let cursor = null; 604 let hasMore = true; 605 606 while (hasMore) { 607 const url = cursor 608 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification&cursor=${cursor}` 609 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification`; 610 611 const verificationResponse = await fetch(url); 612 const data = await verificationResponse.json(); 613 614 if (data.records && data.records.length > 0) { 615 allRecords = [...allRecords, ...data.records]; 616 } 617 618 if (data.cursor) { 619 cursor = data.cursor; 620 } else { 621 hasMore = false; 622 } 623 } 624 625 const verificationData = { records: allRecords }; 626 627 if (!verificationData.records || verificationData.records.length === 0) { 628 alert("No verification records found in your account."); 629 importButton.textContent = originalText; 630 importButton.disabled = false; 631 return; 632 } 633 634 // Extract the handles of verified users 635 const verifiedUsers = []; 636 for (const record of verificationData.records) { 637 console.log(record.value.handle); 638 verifiedUsers.push(record.value.handle); 639 } 640 641 // Add all found users to trusted users 642 let addedCount = 0; 643 for (const handle of verifiedUsers) { 644 const existingUsers = getTrustedUsers(); 645 if (!existingUsers.includes(handle)) { 646 addTrustedUser(handle); 647 addedCount++; 648 } 649 } 650 651 // Update the UI 652 updateTrustedUsersList(); 653 654 // Reset button state 655 importButton.textContent = originalText; 656 importButton.disabled = false; 657 658 // Show result 659 alert( 660 `Successfully imported ${addedCount} verified users from your account.`, 661 ); 662 } catch (error) { 663 console.error("Error importing verifications:", error); 664 alert("Error importing verifications. Check console for details."); 665 const importButton = document.getElementById("importVerificationsBtn"); 666 if (importButton) { 667 importButton.textContent = "Import Verifications"; 668 importButton.disabled = false; 669 } 670 } 671 }; 672 673 const addSettingsButton = () => { 674 // Check if we're on the settings page 675 if (!window.location.href.includes("bsky.app/settings")) { 676 return; 677 } 678 679 // Check if our button already exists to avoid duplicates 680 if (document.getElementById("community-verifications-settings-button")) { 681 return; 682 } 683 684 // Find the right place to insert our button (after content-and-media link) 685 const contentMediaLink = document.querySelector( 686 'a[href="/settings/content-and-media"]', 687 ); 688 if (!contentMediaLink) { 689 console.log("Could not find content-and-media link to insert after"); 690 return; 691 } 692 693 // Clone the existing link and modify it 694 const verificationButton = contentMediaLink.cloneNode(true); 695 verificationButton.id = "community-verifications-settings-button"; 696 verificationButton.href = "#"; // No actual link, we'll handle click with JS 697 verificationButton.setAttribute("aria-label", "Community Verifications"); 698 699 const highlightColor = 700 verificationButton.firstChild.style.backgroundColor || "rgb(30,41,54)"; 701 702 // Add hover effect to highlight the button 703 verificationButton.addEventListener("mouseover", () => { 704 verificationButton.firstChild.style.backgroundColor = highlightColor; 705 }); 706 707 verificationButton.addEventListener("mouseout", () => { 708 verificationButton.firstChild.style.backgroundColor = null; 709 }); 710 711 // Update the text content 712 const textDiv = verificationButton.querySelector(".css-146c3p1"); 713 if (textDiv) { 714 textDiv.textContent = "Community Verifications"; 715 } 716 717 // Update the icon 718 const iconDiv = verificationButton.querySelector( 719 ".css-175oi2r[style*='width: 28px']", 720 ); 721 if (iconDiv) { 722 iconDiv.innerHTML = ` 723 <svg fill="none" width="28" viewBox="0 0 24 24" height="28" style="color: rgb(241, 243, 245);"> 724 <path fill="hsl(211, 20%, 95.3%)" d="M21.2,9.3c-0.5-0.5-1.1-0.7-1.8-0.7h-2.3V6.3c0-2.1-1.7-3.7-3.7-3.7h-3c-2.1,0-3.7,1.7-3.7,3.7v2.3H4.6 725 c-0.7,0-1.3,0.3-1.8,0.7c-0.5,0.5-0.7,1.1-0.7,1.8v9.3c0,0.7,0.3,1.3,0.7,1.8c0.5,0.5,1.1,0.7,1.8,0.7h14.9c0.7,0,1.3-0.3,1.8-0.7 726 c0.5-0.5,0.7-1.1,0.7-1.8v-9.3C22,10.4,21.7,9.8,21.2,9.3z M14.1,15.6l-1.3,1.3c-0.1,0.1-0.3,0.2-0.5,0.2c-0.2,0-0.3-0.1-0.5-0.2l-3.3-3.3 727 c-0.1-0.1-0.2-0.3-0.2-0.5c0-0.2,0.1-0.3,0.2-0.5l1.3-1.3c0.1-0.1,0.3-0.2,0.5-0.2c0.2,0,0.3,0.1,0.5,0.2l1.5,1.5l4.2-4.2 728 c0.1-0.1,0.3-0.2,0.5-0.2c0.2,0,0.3,0.1,0.5,0.2l1.3,1.3c0.1,0.1,0.2,0.3,0.2,0.5c0,0.2-0.1,0.3-0.2,0.5L14.1,15.6z M9.7,6.3 729 c0-0.9,0.7-1.7,1.7-1.7h3c0.9,0,1.7,0.7,1.7,1.7v2.3H9.7V6.3z"/> 730 </svg> 731 `; 732 } 733 734 // Insert our button after the content-and-media link 735 const parentElement = contentMediaLink.parentElement; 736 parentElement.insertBefore( 737 verificationButton, 738 contentMediaLink.nextSibling, 739 ); 740 741 // Add click event to open our settings modal 742 verificationButton.addEventListener("click", (e) => { 743 e.preventDefault(); 744 if (settingsModal) { 745 settingsModal.style.display = "flex"; 746 updateTrustedUsersList(); 747 } else { 748 createSettingsModal(); 749 } 750 }); 751 752 console.log("Added Community Verifications button to settings page"); 753 }; 754 755 // Function to create the settings modal 756 const createSettingsModal = () => { 757 // Create modal container 758 settingsModal = document.createElement("div"); 759 settingsModal.id = "bsky-trusted-settings-modal"; 760 settingsModal.style.cssText = ` 761 display: flex; 762 position: fixed; 763 top: 0; 764 left: 0; 765 width: 100%; 766 height: 100%; 767 background-color: rgba(0, 0, 0, 0.5); 768 z-index: 10001; 769 justify-content: center; 770 align-items: center; 771 `; 772 773 // Create modal content 774 const modalContent = document.createElement("div"); 775 modalContent.style.cssText = ` 776 background-color: #24273A; 777 padding: 20px; 778 border-radius: 10px; 779 width: 400px; 780 max-height: 80vh; 781 overflow-y: auto; 782 `; 783 784 // Create modal header 785 const modalHeader = document.createElement("div"); 786 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`; 787 788 const badgeCustomization = document.createElement("div"); 789 badgeCustomization.style.cssText = ` 790 margin-top: 15px; 791 padding-top: 15px; 792 border-top: 1px solid #eee; 793 `; 794 795 badgeCustomization.innerHTML = ` 796 <h2 style="margin-top: 0; color: white;">Badge Customization</h3> 797 798 <div style="margin-bottom: 1rem;"> 799 <p style="margin-bottom: 8px; color: white;">Badge Type:</p> 800 <div style="display: flex; flex-wrap: wrap; gap: 10px;"> 801 <label style="display: flex; align-items: center; cursor: pointer; color: white;"> 802 <input type="radio" name="badgeType" value="checkmark" ${getBadgeType() === "checkmark" ? "checked" : ""}> 803 <span style="margin-left: 5px;">Checkmark (✓)</span> 804 </label> 805 <label style="display: flex; align-items: center; cursor: pointer; color: white;"> 806 <input type="radio" name="badgeType" value="star" ${getBadgeType() === "star" ? "checked" : ""}> 807 <span style="margin-left: 5px;">Star (★)</span> 808 </label> 809 <label style="display: flex; align-items: center; cursor: pointer; color: white;"> 810 <input type="radio" name="badgeType" value="heart" ${getBadgeType() === "heart" ? "checked" : ""}> 811 <span style="margin-left: 5px;">Heart (♥)</span> 812 </label> 813 <label style="display: flex; align-items: center; cursor: pointer; color: white;"> 814 <input type="radio" name="badgeType" value="shield" ${getBadgeType() === "shield" ? "checked" : ""}> 815 <span style="margin-left: 5px;">Shield (🛡️)</span> 816 </label> 817 <label style="display: flex; align-items: center; cursor: pointer; color: white;"> 818 <input type="radio" name="badgeType" value="lock" ${getBadgeType() === "lock" ? "checked" : ""}> 819 <span style="margin-left: 5px;">Lock (🔒)</span> 820 </label> 821 <label style="display: flex; align-items: center; cursor: pointer; color: white;"> 822 <input type="radio" name="badgeType" value="verified" ${getBadgeType() === "verified" ? "checked" : ""}> 823 <span style="margin-left: 5px;">Verified</span> 824 </label> 825 </div> 826 </div> 827 828 <div> 829 <p style="margin-bottom: 8px; color: white;">Badge Color:</p> 830 <div style="display: flex; align-items: center;"> 831 <input type="color" id="badgeColorPicker" value="${getBadgeColor()}" style="margin-right: 10px;"> 832 <span id="badgeColorPreview" style="display: inline-block; width: 24px; height: 24px; background-color: ${getBadgeColor()}; border-radius: 50%; margin-right: 10px;"></span> 833 <button id="resetBadgeColor" style="padding: 5px 10px; background: #473A3A; color: white; border: none; border-radius: 4px; cursor: pointer;">Reset to Default</button> 834 </div> 835 </div> 836 837 <div style="margin-top: 20px; margin-bottom: 1rem;"> 838 <p style="color: white;">Preview:</p> 839 <div style="display: flex; align-items: center; margin-top: 8px;"> 840 <span style="color: white; font-weight: bold;">User Name</span> 841 <span id="badgePreview" style=" 842 background-color: ${getBadgeColor()}; 843 color: white; 844 border-radius: 50%; 845 width: 22px; 846 height: 22px; 847 margin-left: 8px; 848 font-size: 14px; 849 font-weight: bold; 850 display: inline-flex; 851 align-items: center; 852 justify-content: center; 853 ">${getBadgeContent(getBadgeType())}</span> 854 </div> 855 </div> 856 `; 857 858 // Add the badge customization section to the modal content 859 modalContent.appendChild(badgeCustomization); 860 861 // Add event listeners for the badge customization controls 862 setTimeout(() => { 863 // Badge type selection 864 const badgeTypeRadios = document.querySelectorAll( 865 'input[name="badgeType"]', 866 ); 867 for (const radio of badgeTypeRadios) { 868 radio.addEventListener("change", (e) => { 869 const selectedType = e.target.value; 870 saveBadgeType(selectedType); 871 updateBadgePreview(); 872 }); 873 } 874 875 // Badge color picker 876 const colorPicker = document.getElementById("badgeColorPicker"); 877 const colorPreview = document.getElementById("badgeColorPreview"); 878 879 colorPicker.addEventListener("input", (e) => { 880 const selectedColor = e.target.value; 881 colorPreview.style.backgroundColor = selectedColor; 882 saveBadgeColor(selectedColor); 883 updateBadgePreview(); 884 }); 885 886 // Reset color button 887 const resetColorBtn = document.getElementById("resetBadgeColor"); 888 resetColorBtn.addEventListener("click", () => { 889 colorPicker.value = DEFAULT_BADGE_COLOR; 890 colorPreview.style.backgroundColor = DEFAULT_BADGE_COLOR; 891 saveBadgeColor(DEFAULT_BADGE_COLOR); 892 updateBadgePreview(); 893 }); 894 895 // Function to update the badge preview 896 function updateBadgePreview() { 897 const badgePreview = document.getElementById("badgePreview"); 898 const selectedType = getBadgeType(); 899 const selectedColor = getBadgeColor(); 900 901 badgePreview.innerHTML = getBadgeContent(selectedType); 902 badgePreview.style.backgroundColor = selectedColor; 903 } 904 905 // Initialize preview 906 updateBadgePreview(); 907 }, 100); 908 909 // Create input form 910 const form = document.createElement("div"); 911 form.innerHTML = ` 912 <p>Add Bluesky handles you trust:</p> 913 <div style="display: flex; margin-bottom: 15px; position: relative;"> 914 <input id="trustedUserInput" type="text" placeholder="Search for a user..." style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;"> 915 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button> 916 </div> 917 `; 918 919 // Create import button 920 const importContainer = document.createElement("div"); 921 importContainer.style.cssText = ` 922 margin-top: 10px; 923 margin-bottom: 15px; 924 `; 925 926 const importButton = document.createElement("button"); 927 importButton.id = "importVerificationsBtn"; 928 importButton.textContent = "Import Your Verifications"; 929 importButton.style.cssText = ` 930 background-color: #2D578D; 931 color: white; 932 border: none; 933 border-radius: 4px; 934 padding: 8px 15px; 935 cursor: pointer; 936 width: 100%; 937 `; 938 939 importButton.addEventListener("click", importVerificationsFromSelf); 940 importContainer.appendChild(importButton); 941 942 // Create trusted users list 943 const trustedUsersList = document.createElement("div"); 944 trustedUsersList.id = "trustedUsersList"; 945 trustedUsersList.style.cssText = ` 946 margin-top: 15px; 947 border-top: 1px solid #eee; 948 padding-top: 15px; 949 `; 950 951 // Create cache control buttons 952 const cacheControls = document.createElement("div"); 953 cacheControls.style.cssText = ` 954 margin-top: 15px; 955 padding-top: 15px; 956 border-top: 1px solid #eee; 957 `; 958 959 const clearCacheButton = document.createElement("button"); 960 clearCacheButton.textContent = "Clear Verification Cache"; 961 clearCacheButton.style.cssText = ` 962 padding: 8px 15px; 963 background-color: #735A5A; 964 color: white; 965 border: none; 966 border-radius: 4px; 967 cursor: pointer; 968 margin-right: 10px; 969 `; 970 clearCacheButton.addEventListener("click", () => { 971 clearCache(); 972 alert( 973 "Verification cache cleared. Fresh data will be fetched on next check.", 974 ); 975 }); 976 977 cacheControls.appendChild(clearCacheButton); 978 979 // Create close button 980 const closeButton = document.createElement("button"); 981 closeButton.textContent = "Close"; 982 closeButton.style.cssText = ` 983 margin-top: 20px; 984 padding: 8px 15px; 985 background-color: #473A3A; 986 border: none; 987 border-radius: 4px; 988 cursor: pointer; 989 `; 990 991 // Assemble modal 992 modalContent.appendChild(modalHeader); 993 modalContent.appendChild(form); 994 modalContent.appendChild(importContainer); 995 modalContent.appendChild(trustedUsersList); 996 modalContent.appendChild(cacheControls); 997 modalContent.appendChild(closeButton); 998 settingsModal.appendChild(modalContent); 999 1000 // Add to document 1001 document.body.appendChild(settingsModal); 1002 1003 const userInput = document.getElementById("trustedUserInput"); 1004 1005 // Add input event for autocomplete 1006 let debounceTimeout; 1007 userInput.addEventListener("input", (e) => { 1008 clearTimeout(debounceTimeout); 1009 debounceTimeout = setTimeout(async () => { 1010 const searchQuery = e.target.value.trim(); 1011 if (searchQuery.length >= 2) { 1012 const results = await searchUsers(searchQuery); 1013 showAutocompleteResults(results, userInput); 1014 } else { 1015 const dropdown = document.getElementById("autocomplete-dropdown"); 1016 if (dropdown) dropdown.remove(); 1017 } 1018 }, 300); // Debounce for 300ms 1019 }); 1020 1021 // Event listeners 1022 closeButton.addEventListener("click", () => { 1023 settingsModal.style.display = "none"; 1024 }); 1025 1026 // Function to add a user from the input field 1027 const addUserFromInput = () => { 1028 const input = document.getElementById("trustedUserInput"); 1029 const handle = input.value.trim(); 1030 if (handle) { 1031 addTrustedUser(handle); 1032 input.value = ""; 1033 updateTrustedUsersList(); 1034 1035 // Remove dropdown if present 1036 const dropdown = document.getElementById("autocomplete-dropdown"); 1037 if (dropdown) dropdown.remove(); 1038 } 1039 }; 1040 1041 // Add trusted user button event 1042 document 1043 .getElementById("addTrustedUserBtn") 1044 .addEventListener("click", addUserFromInput); 1045 1046 // Add keydown event to input for Enter key 1047 userInput.addEventListener("keydown", (e) => { 1048 if (e.key === "Enter") { 1049 e.preventDefault(); 1050 addUserFromInput(); 1051 } 1052 }); 1053 1054 // Close modal when clicking outside 1055 settingsModal.addEventListener("click", (e) => { 1056 if (e.target === settingsModal) { 1057 settingsModal.style.display = "none"; 1058 } 1059 }); 1060 1061 // Initialize the list 1062 updateTrustedUsersList(); 1063 }; 1064 1065 // Function to check the current profile 1066 const checkCurrentProfile = () => { 1067 const currentUrl = window.location.href; 1068 // Only trigger on profile pages 1069 if ( 1070 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) || 1071 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) || 1072 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/) 1073 ) { 1074 const handle = currentUrl.split("/profile/")[1].split("/")[0]; 1075 console.log("Detected profile page for:", handle); 1076 1077 // Fetch user profile data 1078 fetch( 1079 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 1080 ) 1081 .then((response) => response.json()) 1082 .then((data) => { 1083 console.log("User profile data:", data); 1084 1085 // Extract the DID from the profile data 1086 const did = data.uri.split("/")[2]; 1087 console.log("User DID:", did); 1088 1089 // Check if any trusted users have verified this profile using the DID 1090 checkTrustedUserVerifications(did); 1091 }) 1092 .catch((error) => { 1093 console.error("Error checking profile:", error); 1094 }); 1095 1096 console.log("Bluesky profile detected"); 1097 } else { 1098 // Not on a profile page, reset state 1099 currentProfileDid = null; 1100 profileVerifiers = []; 1101 1102 // Remove UI elements if present 1103 const existingBadge = document.getElementById( 1104 "user-trusted-verification-badge", 1105 ); 1106 if (existingBadge) { 1107 existingBadge.remove(); 1108 } 1109 } 1110 }; 1111 1112 const checkUserLinksOnPage = async () => { 1113 // Look for profile links with handles 1114 // Find all profile links and filter to get only one link per parent 1115 const allProfileLinks = Array.from( 1116 document.querySelectorAll('a[href^="/profile/"]:not(:has(*))'), 1117 ); 1118 1119 // Use a Map to keep track of parent elements and their first child link 1120 const parentMap = new Map(); 1121 1122 // For each link, store only the first one found for each parent 1123 for (const link of allProfileLinks) { 1124 const parent = link.parentElement; 1125 if (parent && !parentMap.has(parent)) { 1126 parentMap.set(parent, link); 1127 } 1128 } 1129 1130 // Get only the first link for each parent 1131 const profileLinks = Array.from(parentMap.values()); 1132 1133 if (profileLinks.length === 0) return; 1134 1135 console.log(`Found ${profileLinks.length} possible user links on page`); 1136 1137 // Process profile links to identify user containers 1138 for (const link of profileLinks) { 1139 try { 1140 // Check if we already processed this link 1141 if (link.getAttribute("data-verification-checked") === "true") continue; 1142 1143 // Mark as checked 1144 link.setAttribute("data-verification-checked", "true"); 1145 1146 // Extract handle from href 1147 const handle = link.getAttribute("href").split("/profile/")[1]; 1148 if (!handle) continue; 1149 1150 // check if there is anything after the handle 1151 const handleTrailing = handle.split("/").length > 1; 1152 if (handleTrailing) continue; 1153 1154 // Find parent container that might contain the handle and verification icon 1155 // Look for containers where this link is followed by another link with the same handle 1156 const parent = link.parentElement; 1157 1158 // If we found a container with the verification icon 1159 if (parent) { 1160 // Check if this user already has our verification badge 1161 if (parent.querySelector(".trusted-user-inline-badge")) continue; 1162 1163 try { 1164 // Fetch user profile data to get DID 1165 const response = await fetch( 1166 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 1167 ); 1168 const data = await response.json(); 1169 1170 // Extract the DID from the profile data 1171 const did = data.uri.split("/")[2]; 1172 1173 // Check if this user is verified by our trusted users 1174 const trustedUsers = getTrustedUsers(); 1175 let isVerified = false; 1176 const verifiers = []; 1177 1178 // Check cache first for each trusted user 1179 for (const trustedUser of trustedUsers) { 1180 const cachedData = getCachedVerifications(trustedUser); 1181 1182 if (cachedData && isCacheValid(cachedData)) { 1183 // Use cached verification data 1184 const records = cachedData.records; 1185 1186 for (const record of records) { 1187 if (record.value && record.value.subject === did) { 1188 isVerified = true; 1189 verifiers.push(trustedUser); 1190 break; 1191 } 1192 } 1193 } 1194 } 1195 1196 // If verified, add a small badge 1197 if (isVerified && verifiers.length > 0) { 1198 // Create a badge element 1199 const smallBadge = document.createElement("span"); 1200 smallBadge.className = "trusted-user-inline-badge"; 1201 1202 // Get user badge preferences 1203 const badgeType = getBadgeType(); 1204 const badgeColor = getBadgeColor(); 1205 1206 smallBadge.innerHTML = getBadgeContent(badgeType); 1207 1208 // Create tooltip text with all verifiers 1209 const verifiersText = 1210 verifiers.length > 1 1211 ? `Verified by: ${verifiers.join(", ")}` 1212 : `Verified by ${verifiers[0]}`; 1213 1214 smallBadge.title = verifiersText; 1215 smallBadge.style.cssText = ` 1216 background-color: ${badgeColor}; 1217 color: white; 1218 border-radius: 50%; 1219 width: 16px; 1220 height: 16px; 1221 font-size: 11px; 1222 font-weight: bold; 1223 cursor: help; 1224 display: inline-flex; 1225 align-items: center; 1226 justify-content: center; 1227 margin-left: 4px; 1228 `; 1229 1230 // Add click event to show verifiers 1231 smallBadge.addEventListener("click", (e) => { 1232 e.stopPropagation(); 1233 showVerifiersPopup(verifiers); 1234 }); 1235 1236 // Insert badge after the SVG element 1237 parent.firstChild.after(smallBadge); 1238 parent.style.flexDirection = "row"; 1239 parent.style.alignItems = "center"; 1240 1241 // Look for verification SVG icon in the parent and remove it if it exists 1242 const badgeSvgIcon = Array.from(parent.childNodes).find( 1243 (node) => 1244 node.nodeType === Node.ELEMENT_NODE && 1245 node.tagName === "DIV" && 1246 node.querySelector("svg"), 1247 ); 1248 if (badgeSvgIcon) { 1249 badgeSvgIcon.remove(); 1250 } 1251 } 1252 } catch (error) { 1253 console.error(`Error checking verification for ${handle}:`, error); 1254 } 1255 } 1256 } catch (error) { 1257 console.error("Error processing profile link:", error); 1258 } 1259 } 1260 }; 1261 1262 const observeContentChanges = () => { 1263 // Use a debounced function to check for new user links 1264 const debouncedCheck = () => { 1265 clearTimeout(window.userLinksCheckTimeout); 1266 window.userLinksCheckTimeout = setTimeout(() => { 1267 checkUserLinksOnPage(); 1268 }, 300); 1269 }; 1270 1271 // Create a mutation observer that watches for DOM changes 1272 const observer = new MutationObserver((mutations) => { 1273 let hasRelevantChanges = false; 1274 1275 // Check if any mutations involve adding new nodes 1276 for (const mutation of mutations) { 1277 if (mutation.addedNodes.length > 0) { 1278 for (const node of mutation.addedNodes) { 1279 if (node.nodeType === Node.ELEMENT_NODE) { 1280 // Check if this element or its children might contain profile links 1281 if ( 1282 node.querySelector('a[href^="/profile/"]') || 1283 (node.tagName === "A" && 1284 node.getAttribute("href")?.startsWith("/profile/")) 1285 ) { 1286 hasRelevantChanges = true; 1287 break; 1288 } 1289 } 1290 } 1291 } 1292 if (hasRelevantChanges) break; 1293 } 1294 1295 if (hasRelevantChanges) { 1296 debouncedCheck(); 1297 } 1298 }); 1299 1300 // Observe the entire document for content changes that might include profile links 1301 observer.observe(document.body, { childList: true, subtree: true }); 1302 1303 // Also check periodically for posts that might have been loaded but not caught by the observer 1304 setInterval(debouncedCheck, 5000); 1305 }; 1306 1307 // Wait for DOM to be fully loaded before initializing 1308 document.addEventListener("DOMContentLoaded", () => { 1309 // Initial check for user links 1310 checkUserLinksOnPage(); 1311 1312 // Initial check 1313 setTimeout(checkCurrentProfile, 2000); 1314 1315 // Add settings button if we're on the settings page 1316 if (window.location.href.includes("bsky.app/settings")) { 1317 // Wait for the content-and-media link to appear before adding our button 1318 const waitForSettingsLink = setInterval(() => { 1319 const contentMediaLink = document.querySelector( 1320 'a[href="/settings/content-and-media"]', 1321 ); 1322 if (contentMediaLink) { 1323 clearInterval(waitForSettingsLink); 1324 addSettingsButton(); 1325 } 1326 }, 200); 1327 } 1328 }); 1329 1330 // Start observing for content changes to detect newly loaded posts 1331 observeContentChanges(); 1332 1333 // Set up a MutationObserver to watch for URL changes 1334 const observeUrlChanges = () => { 1335 let lastUrl = location.href; 1336 1337 const observer = new MutationObserver(() => { 1338 if (location.href !== lastUrl) { 1339 const oldUrl = lastUrl; 1340 lastUrl = location.href; 1341 console.log("URL changed from:", oldUrl, "to:", location.href); 1342 1343 // Reset current profile DID 1344 currentProfileDid = null; 1345 profileVerifiers = []; 1346 1347 // Clean up UI elements 1348 const existingBadge = document.getElementById( 1349 "user-trusted-verification-badge", 1350 ); 1351 if (existingBadge) { 1352 existingBadge.remove(); 1353 } 1354 1355 // Check if we're on a profile page now 1356 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated 1357 1358 if (window.location.href.includes("bsky.app/settings")) { 1359 // Give the page a moment to fully load 1360 setTimeout(addSettingsButton, 200); 1361 } 1362 } 1363 }); 1364 1365 observer.observe(document, { subtree: true, childList: true }); 1366 }; 1367 1368 // Start observing for URL changes 1369 observeUrlChanges(); 1370})();