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