random bun scripts that dont fit anywhere else
1// ==UserScript== 2// @name Bluesky Community Verifications 3// @namespace https://tangled.sh/@dunkirk.sh/bunplayground 4// @version 0.1 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 create the settings modal 604 const createSettingsModal = () => { 605 // Create modal container 606 settingsModal = document.createElement("div"); 607 settingsModal.id = "bsky-trusted-settings-modal"; 608 settingsModal.style.cssText = ` 609 display: none; 610 position: fixed; 611 top: 0; 612 left: 0; 613 width: 100%; 614 height: 100%; 615 background-color: rgba(0, 0, 0, 0.5); 616 z-index: 10001; 617 justify-content: center; 618 align-items: center; 619 `; 620 621 // Create modal content 622 const modalContent = document.createElement("div"); 623 modalContent.style.cssText = ` 624 background-color: #24273A; 625 padding: 20px; 626 border-radius: 10px; 627 width: 400px; 628 max-height: 80vh; 629 overflow-y: auto; 630 `; 631 632 // Create modal header 633 const modalHeader = document.createElement("div"); 634 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`; 635 636 // Create input form 637 const form = document.createElement("div"); 638 form.innerHTML = ` 639 <p>Add Bluesky handles you trust:</p> 640 <div style="display: flex; margin-bottom: 15px; position: relative;"> 641 <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;"> 642 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button> 643 </div> 644 `; 645 646 // Create trusted users list 647 const trustedUsersList = document.createElement("div"); 648 trustedUsersList.id = "trustedUsersList"; 649 trustedUsersList.style.cssText = ` 650 margin-top: 15px; 651 border-top: 1px solid #eee; 652 padding-top: 15px; 653 `; 654 655 // Create cache control buttons 656 const cacheControls = document.createElement("div"); 657 cacheControls.style.cssText = ` 658 margin-top: 15px; 659 padding-top: 15px; 660 border-top: 1px solid #eee; 661 `; 662 663 const clearCacheButton = document.createElement("button"); 664 clearCacheButton.textContent = "Clear Verification Cache"; 665 clearCacheButton.style.cssText = ` 666 padding: 8px 15px; 667 background-color: #735A5A; 668 color: white; 669 border: none; 670 border-radius: 4px; 671 cursor: pointer; 672 margin-right: 10px; 673 `; 674 clearCacheButton.addEventListener("click", () => { 675 clearCache(); 676 alert( 677 "Verification cache cleared. Fresh data will be fetched on next check.", 678 ); 679 }); 680 681 cacheControls.appendChild(clearCacheButton); 682 683 // Create close button 684 const closeButton = document.createElement("button"); 685 closeButton.textContent = "Close"; 686 closeButton.style.cssText = ` 687 margin-top: 20px; 688 padding: 8px 15px; 689 background-color: #473A3A; 690 border: none; 691 border-radius: 4px; 692 cursor: pointer; 693 `; 694 695 // Assemble modal 696 modalContent.appendChild(modalHeader); 697 modalContent.appendChild(form); 698 modalContent.appendChild(trustedUsersList); 699 modalContent.appendChild(cacheControls); 700 modalContent.appendChild(closeButton); 701 settingsModal.appendChild(modalContent); 702 703 // Add to document 704 document.body.appendChild(settingsModal); 705 706 const userInput = document.getElementById("trustedUserInput"); 707 708 // Add input event for autocomplete 709 let debounceTimeout; 710 userInput.addEventListener("input", (e) => { 711 clearTimeout(debounceTimeout); 712 debounceTimeout = setTimeout(async () => { 713 const searchQuery = e.target.value.trim(); 714 if (searchQuery.length >= 2) { 715 const results = await searchUsers(searchQuery); 716 showAutocompleteResults(results, userInput); 717 } else { 718 const dropdown = document.getElementById("autocomplete-dropdown"); 719 if (dropdown) dropdown.remove(); 720 } 721 }, 300); // Debounce for 300ms 722 }); 723 724 // Event listeners 725 closeButton.addEventListener("click", () => { 726 settingsModal.style.display = "none"; 727 }); 728 729 // Function to add a user from the input field 730 const addUserFromInput = () => { 731 const input = document.getElementById("trustedUserInput"); 732 const handle = input.value.trim(); 733 if (handle) { 734 addTrustedUser(handle); 735 input.value = ""; 736 updateTrustedUsersList(); 737 738 // Remove dropdown if present 739 const dropdown = document.getElementById("autocomplete-dropdown"); 740 if (dropdown) dropdown.remove(); 741 } 742 }; 743 744 // Add trusted user button event 745 document 746 .getElementById("addTrustedUserBtn") 747 .addEventListener("click", addUserFromInput); 748 749 // Add keydown event to input for Enter key 750 userInput.addEventListener("keydown", (e) => { 751 if (e.key === "Enter") { 752 e.preventDefault(); 753 addUserFromInput(); 754 } 755 }); 756 757 // Close modal when clicking outside 758 settingsModal.addEventListener("click", (e) => { 759 if (e.target === settingsModal) { 760 settingsModal.style.display = "none"; 761 } 762 }); 763 764 // Initialize the list 765 updateTrustedUsersList(); 766 }; 767 768 // Function to create the settings UI if it doesn't exist yet 769 const createSettingsUI = () => { 770 // Create pill with buttons 771 createPillButtons(); 772 773 // Create the settings modal if it doesn't exist yet 774 if (!settingsModal) { 775 createSettingsModal(); 776 } 777 }; 778 779 // Function to check the current profile 780 const checkCurrentProfile = () => { 781 const currentUrl = window.location.href; 782 // Only trigger on profile pages 783 if ( 784 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) || 785 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) || 786 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/) 787 ) { 788 const handle = currentUrl.split("/profile/")[1].split("/")[0]; 789 console.log("Detected profile page for:", handle); 790 791 // Create and add the settings UI (only once) 792 createSettingsUI(); 793 794 // Fetch user profile data 795 fetch( 796 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 797 ) 798 .then((response) => response.json()) 799 .then((data) => { 800 console.log("User profile data:", data); 801 802 // Extract the DID from the profile data 803 const did = data.uri.split("/")[2]; 804 console.log("User DID:", did); 805 806 // Check if any trusted users have verified this profile using the DID 807 checkTrustedUserVerifications(did); 808 }) 809 .catch((error) => { 810 console.error("Error checking profile:", error); 811 }); 812 813 console.log("Bluesky profile detected"); 814 } else { 815 // Not on a profile page, reset state 816 currentProfileDid = null; 817 profileVerifiers = []; 818 819 // Remove UI elements if present 820 const existingBadge = document.getElementById( 821 "user-trusted-verification-badge", 822 ); 823 if (existingBadge) { 824 existingBadge.remove(); 825 } 826 827 const existingPill = document.getElementById( 828 "trusted-users-pill-container", 829 ); 830 if (existingPill) { 831 existingPill.remove(); 832 } 833 } 834 }; 835 836 // Initial check 837 checkCurrentProfile(); 838 839 const checkUserLinksOnPage = async () => { 840 // Look for profile links with handles 841 // Find all profile links and filter to get only one link per parent 842 const allProfileLinks = Array.from( 843 document.querySelectorAll('a[href^="/profile/"]:not(:has(*))'), 844 ); 845 846 // Use a Map to keep track of parent elements and their first child link 847 const parentMap = new Map(); 848 849 // For each link, store only the first one found for each parent 850 for (const link of allProfileLinks) { 851 const parent = link.parentElement; 852 if (parent && !parentMap.has(parent)) { 853 parentMap.set(parent, link); 854 } 855 } 856 857 // Get only the first link for each parent 858 const profileLinks = Array.from(parentMap.values()); 859 860 if (profileLinks.length === 0) return; 861 862 console.log(`Found ${profileLinks.length} possible user links on page`); 863 864 // Process profile links to identify user containers 865 for (const link of profileLinks) { 866 try { 867 // Check if we already processed this link 868 if (link.getAttribute("data-verification-checked") === "true") continue; 869 870 // Mark as checked 871 link.setAttribute("data-verification-checked", "true"); 872 873 // Extract handle from href 874 const handle = link.getAttribute("href").split("/profile/")[1]; 875 if (!handle) continue; 876 877 // check if there is anything after the handle 878 const handleTrailing = handle.split("/").length > 1; 879 if (handleTrailing) continue; 880 881 // Find parent container that might contain the handle and verification icon 882 // Look for containers where this link is followed by another link with the same handle 883 const parent = link.parentElement; 884 885 // If we found a container with the verification icon 886 if (parent) { 887 // Check if this user already has our verification badge 888 if (parent.querySelector(".trusted-user-inline-badge")) continue; 889 890 try { 891 // Fetch user profile data to get DID 892 const response = await fetch( 893 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 894 ); 895 const data = await response.json(); 896 897 // Extract the DID from the profile data 898 const did = data.uri.split("/")[2]; 899 900 // Check if this user is verified by our trusted users 901 const trustedUsers = getTrustedUsers(); 902 let isVerified = false; 903 const verifiers = []; 904 905 // Check cache first for each trusted user 906 for (const trustedUser of trustedUsers) { 907 const cachedData = getCachedVerifications(trustedUser); 908 909 if (cachedData && isCacheValid(cachedData)) { 910 // Use cached verification data 911 const records = cachedData.records; 912 913 for (const record of records) { 914 if (record.value && record.value.subject === did) { 915 isVerified = true; 916 verifiers.push(trustedUser); 917 break; 918 } 919 } 920 } 921 } 922 923 // If verified, add a small badge 924 if (isVerified && verifiers.length > 0) { 925 // Create a badge element 926 const smallBadge = document.createElement("span"); 927 smallBadge.className = "trusted-user-inline-badge"; 928 smallBadge.innerHTML = "✓"; 929 930 // Create tooltip text with all verifiers 931 const verifiersText = 932 verifiers.length > 1 933 ? `Verified by: ${verifiers.join(", ")}` 934 : `Verified by ${verifiers[0]}`; 935 936 smallBadge.title = verifiersText; 937 smallBadge.style.cssText = ` 938 background-color: #0070ff; 939 color: white; 940 border-radius: 50%; 941 width: 14px; 942 height: 14px; 943 font-size: 10px; 944 font-weight: bold; 945 cursor: help; 946 display: inline-flex; 947 align-items: center; 948 justify-content: center; 949 margin-left: 4px; 950 `; 951 952 // Add click event to show verifiers 953 smallBadge.addEventListener("click", (e) => { 954 e.stopPropagation(); 955 showVerifiersPopup(verifiers); 956 }); 957 958 // Insert badge after the SVG element 959 parent.firstChild.after(smallBadge); 960 parent.style.flexDirection = "row"; 961 parent.style.alignItems = "center"; 962 } 963 } catch (error) { 964 console.error(`Error checking verification for ${handle}:`, error); 965 } 966 } 967 } catch (error) { 968 console.error("Error processing profile link:", error); 969 } 970 } 971 }; 972 973 const observeContentChanges = () => { 974 // Use a debounced function to check for new user links 975 const debouncedCheck = () => { 976 clearTimeout(window.userLinksCheckTimeout); 977 window.userLinksCheckTimeout = setTimeout(() => { 978 checkUserLinksOnPage(); 979 }, 300); 980 }; 981 982 // Create a mutation observer that watches for DOM changes 983 const observer = new MutationObserver((mutations) => { 984 let hasRelevantChanges = false; 985 986 // Check if any mutations involve adding new nodes 987 for (const mutation of mutations) { 988 if (mutation.addedNodes.length > 0) { 989 for (const node of mutation.addedNodes) { 990 if (node.nodeType === Node.ELEMENT_NODE) { 991 // Check if this element or its children might contain profile links 992 if ( 993 node.querySelector('a[href^="/profile/"]') || 994 (node.tagName === "A" && 995 node.getAttribute("href")?.startsWith("/profile/")) 996 ) { 997 hasRelevantChanges = true; 998 break; 999 } 1000 } 1001 } 1002 } 1003 if (hasRelevantChanges) break; 1004 } 1005 1006 if (hasRelevantChanges) { 1007 debouncedCheck(); 1008 } 1009 }); 1010 1011 // Observe the entire document for content changes that might include profile links 1012 observer.observe(document.body, { childList: true, subtree: true }); 1013 1014 // Also check periodically for posts that might have been loaded but not caught by the observer 1015 setInterval(debouncedCheck, 5000); 1016 }; 1017 1018 // Add these calls to the initialization section 1019 // Initial check for user links 1020 setTimeout(checkUserLinksOnPage, 1000); // Slight delay to ensure page has loaded 1021 1022 // Start observing for content changes to detect newly loaded posts 1023 observeContentChanges(); 1024 1025 // Set up a MutationObserver to watch for URL changes 1026 const observeUrlChanges = () => { 1027 let lastUrl = location.href; 1028 1029 const observer = new MutationObserver(() => { 1030 if (location.href !== lastUrl) { 1031 const oldUrl = lastUrl; 1032 lastUrl = location.href; 1033 console.log("URL changed from:", oldUrl, "to:", location.href); 1034 1035 // Reset current profile DID 1036 currentProfileDid = null; 1037 profileVerifiers = []; 1038 1039 // Clean up UI elements 1040 const existingBadge = document.getElementById( 1041 "user-trusted-verification-badge", 1042 ); 1043 if (existingBadge) { 1044 existingBadge.remove(); 1045 } 1046 1047 const existingPill = document.getElementById( 1048 "trusted-users-pill-container", 1049 ); 1050 if (existingPill) { 1051 existingPill.remove(); 1052 } 1053 1054 // Check if we're on a profile page now 1055 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated 1056 } 1057 }); 1058 1059 observer.observe(document, { subtree: true, childList: true }); 1060 }; 1061 1062 // Start observing for URL changes 1063 observeUrlChanges(); 1064})();