the home of serif.blue
1(() => { 2 // Script has already been initialized check 3 if (window.bskyTrustedUsersInitialized) { 4 console.log("Trusted Users script already initialized"); 5 return; 6 } 7 8 // Mark script as initialized 9 window.bskyTrustedUsersInitialized = true; 10 11 // Define storage keys 12 const TRUSTED_USERS_STORAGE_KEY = "bsky_trusted_users"; 13 const VERIFICATION_CACHE_STORAGE_KEY = "bsky_verification_cache"; 14 const CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000; // 24 hours 15 16 // Function to get trusted users from local storage 17 const getTrustedUsers = () => { 18 const storedUsers = localStorage.getItem(TRUSTED_USERS_STORAGE_KEY); 19 return storedUsers ? JSON.parse(storedUsers) : []; 20 }; 21 22 // Function to save trusted users to local storage 23 const saveTrustedUsers = (users) => { 24 localStorage.setItem(TRUSTED_USERS_STORAGE_KEY, JSON.stringify(users)); 25 }; 26 27 // Function to add a trusted user 28 const addTrustedUser = (handle) => { 29 const users = getTrustedUsers(); 30 if (!users.includes(handle)) { 31 users.push(handle); 32 saveTrustedUsers(users); 33 } 34 }; 35 36 // Function to remove a trusted user 37 const removeTrustedUser = (handle) => { 38 const users = getTrustedUsers(); 39 const updatedUsers = users.filter((user) => user !== handle); 40 saveTrustedUsers(updatedUsers); 41 }; 42 43 // Cache functions 44 const getVerificationCache = () => { 45 const cache = localStorage.getItem(VERIFICATION_CACHE_STORAGE_KEY); 46 return cache ? JSON.parse(cache) : {}; 47 }; 48 49 const saveVerificationCache = (cache) => { 50 localStorage.setItem(VERIFICATION_CACHE_STORAGE_KEY, JSON.stringify(cache)); 51 }; 52 53 const getCachedVerifications = (user) => { 54 const cache = getVerificationCache(); 55 return cache[user] || null; 56 }; 57 58 const cacheVerifications = (user, records) => { 59 const cache = getVerificationCache(); 60 cache[user] = { 61 records, 62 timestamp: Date.now(), 63 }; 64 saveVerificationCache(cache); 65 }; 66 67 const isCacheValid = (cacheEntry) => { 68 return cacheEntry && Date.now() - cacheEntry.timestamp < CACHE_EXPIRY_TIME; 69 }; 70 71 const clearCache = () => { 72 localStorage.removeItem(VERIFICATION_CACHE_STORAGE_KEY); 73 console.log("Verification cache cleared"); 74 }; 75 76 // Store all verifiers for a profile 77 let profileVerifiers = []; 78 79 // Store current profile DID 80 let currentProfileDid = null; 81 82 // Function to check if a trusted user has verified the current profile 83 const checkTrustedUserVerifications = async (profileDid) => { 84 currentProfileDid = profileDid; // Store for recheck functionality 85 const trustedUsers = getTrustedUsers(); 86 profileVerifiers = []; // Reset the verifiers list 87 88 if (trustedUsers.length === 0) { 89 console.log("No trusted users to check for verifications"); 90 return false; 91 } 92 93 console.log(`Checking if any trusted users have verified ${profileDid}`); 94 95 // Use Promise.all to fetch all verification data in parallel 96 const verificationPromises = trustedUsers.map(async (trustedUser) => { 97 try { 98 // Helper function to fetch all verification records with pagination 99 const fetchAllVerifications = async (user) => { 100 // Check cache first 101 const cachedData = getCachedVerifications(user); 102 if (cachedData && isCacheValid(cachedData)) { 103 console.log(`Using cached verification data for ${user}`); 104 return cachedData.records; 105 } 106 107 console.log(`Fetching fresh verification data for ${user}`); 108 let allRecords = []; 109 let cursor = null; 110 let hasMore = true; 111 112 while (hasMore) { 113 const url = cursor 114 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification&cursor=${cursor}` 115 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification`; 116 117 const response = await fetch(url); 118 const data = await response.json(); 119 120 if (data.records && data.records.length > 0) { 121 allRecords = [...allRecords, ...data.records]; 122 } 123 124 if (data.cursor) { 125 cursor = data.cursor; 126 } else { 127 hasMore = false; 128 } 129 } 130 131 // Save to cache 132 cacheVerifications(user, allRecords); 133 return allRecords; 134 }; 135 136 // Fetch all verification records for this trusted user 137 const records = await fetchAllVerifications(trustedUser); 138 139 console.log(`Received verification data from ${trustedUser}`, { 140 records, 141 }); 142 143 // Check if this trusted user has verified the current profile 144 if (records.length > 0) { 145 for (const record of records) { 146 if (record.value && record.value.subject === profileDid) { 147 console.log( 148 `${profileDid} is verified by trusted user ${trustedUser}`, 149 ); 150 151 // Add to verifiers list 152 profileVerifiers.push(trustedUser); 153 break; // Once we find a verification, we can stop checking 154 } 155 } 156 } 157 return { trustedUser, success: true }; 158 } catch (error) { 159 console.error( 160 `Error checking verifications from ${trustedUser}:`, 161 error, 162 ); 163 return { trustedUser, success: false, error }; 164 } 165 }); 166 167 // Wait for all verification checks to complete 168 const results = await Promise.all(verificationPromises); 169 170 // Log summary of API calls 171 console.log(`API calls completed: ${results.length}`); 172 console.log(`Successful calls: ${results.filter((r) => r.success).length}`); 173 console.log(`Failed calls: ${results.filter((r) => !r.success).length}`); 174 175 // If we have verifiers, display the badge 176 if (profileVerifiers.length > 0) { 177 await displayVerificationBadge(profileVerifiers); 178 return true; 179 } 180 181 console.log(`${profileDid} is not verified by any trusted users`); 182 183 // Add recheck button even when no verifications are found 184 createPillButtons(); 185 186 return false; 187 }; 188 189 // Function to create a pill with recheck and settings buttons 190 const createPillButtons = () => { 191 // Remove existing buttons if any 192 const existingPill = document.getElementById( 193 "trusted-users-pill-container", 194 ); 195 if (existingPill) { 196 existingPill.remove(); 197 } 198 199 // Create pill container 200 const pillContainer = document.createElement("div"); 201 pillContainer.id = "trusted-users-pill-container"; 202 pillContainer.style.cssText = ` 203 position: fixed; 204 bottom: 20px; 205 right: 20px; 206 z-index: 10000; 207 display: flex; 208 border-radius: 20px; 209 overflow: hidden; 210 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 211 `; 212 213 // Create recheck button (left half of pill) 214 const recheckButton = document.createElement("button"); 215 recheckButton.id = "trusted-users-recheck-button"; 216 recheckButton.innerHTML = "↻ Recheck"; 217 recheckButton.style.cssText = ` 218 padding: 10px 15px; 219 background-color: #2D578D; 220 color: white; 221 border: none; 222 cursor: pointer; 223 font-weight: bold; 224 border-top-left-radius: 20px; 225 border-bottom-left-radius: 20px; 226 `; 227 228 // Add click event to recheck 229 recheckButton.addEventListener("click", async () => { 230 if (currentProfileDid) { 231 // Remove any existing badges when rechecking 232 const existingBadge = document.getElementById( 233 "user-trusted-verification-badge", 234 ); 235 if (existingBadge) { 236 existingBadge.remove(); 237 } 238 239 // Show loading state 240 recheckButton.innerHTML = "⟳ Checking..."; 241 recheckButton.disabled = true; 242 243 // Recheck verifications 244 await checkTrustedUserVerifications(currentProfileDid); 245 246 // Reset button 247 recheckButton.innerHTML = "↻ Recheck"; 248 recheckButton.disabled = false; 249 } 250 }); 251 252 // Create vertical divider 253 const divider = document.createElement("div"); 254 divider.style.cssText = ` 255 width: 1px; 256 background-color: rgba(255, 255, 255, 0.3); 257 `; 258 259 // Create settings button (right half of pill) 260 const settingsButton = document.createElement("button"); 261 settingsButton.id = "bsky-trusted-settings-button"; 262 settingsButton.textContent = "Settings"; 263 settingsButton.style.cssText = ` 264 padding: 10px 15px; 265 background-color: #2D578D; 266 color: white; 267 border: none; 268 cursor: pointer; 269 font-weight: bold; 270 border-top-right-radius: 20px; 271 border-bottom-right-radius: 20px; 272 `; 273 274 // Add elements to pill 275 pillContainer.appendChild(recheckButton); 276 pillContainer.appendChild(divider); 277 pillContainer.appendChild(settingsButton); 278 279 // Add pill to page 280 document.body.appendChild(pillContainer); 281 282 // Add event listener to settings button 283 settingsButton.addEventListener("click", () => { 284 if (settingsModal) { 285 settingsModal.style.display = "flex"; 286 updateTrustedUsersList(); 287 } else { 288 createSettingsModal(); 289 } 290 }); 291 }; 292 293 const findProfileHeaderWithRetry = (retryCount = 0, maxRetries = 10) => { 294 const nameElements = document.querySelectorAll( 295 '[data-testid="profileHeaderDisplayName"]', 296 ); 297 const nameElement = nameElements[nameElements.length - 1]; 298 299 if (nameElement) { 300 console.log("Profile header found"); 301 return nameElement; 302 } 303 if (retryCount < maxRetries) { 304 // Retry with exponential backoff 305 const delay = Math.min(100 * 1.5 ** retryCount, 2000); 306 console.log( 307 `Profile header not found, retrying in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`, 308 ); 309 310 return new Promise((resolve) => { 311 setTimeout(() => { 312 resolve(findProfileHeaderWithRetry(retryCount + 1, maxRetries)); 313 }, delay); 314 }); 315 } 316 console.log("Failed to find profile header after maximum retries"); 317 return null; 318 }; 319 320 // Function to display verification badge on the profile 321 const displayVerificationBadge = async (verifierHandles) => { 322 // Find the profile header or name element to add the badge to 323 const nameElement = await findProfileHeaderWithRetry(); 324 325 console.log(nameElement); 326 327 if (nameElement) { 328 // Remove existing badge if present 329 const existingBadge = document.getElementById( 330 "user-trusted-verification-badge", 331 ); 332 if (existingBadge) { 333 existingBadge.remove(); 334 } 335 336 const badge = document.createElement("span"); 337 badge.id = "user-trusted-verification-badge"; 338 badge.innerHTML = "✓"; 339 340 // Create tooltip text with all verifiers 341 const verifiersText = 342 verifierHandles.length > 1 343 ? `Verified by: ${verifierHandles.join(", ")}` 344 : `Verified by ${verifierHandles[0]}`; 345 346 badge.title = verifiersText; 347 badge.style.cssText = ` 348 background-color: #0070ff; 349 color: white; 350 border-radius: 50%; 351 width: 18px; 352 height: 18px; 353 margin-left: 8px; 354 font-size: 12px; 355 font-weight: bold; 356 cursor: help; 357 display: inline-flex; 358 align-items: center; 359 justify-content: center; 360 `; 361 362 // Add a click event to show all verifiers 363 badge.addEventListener("click", (e) => { 364 e.stopPropagation(); 365 showVerifiersPopup(verifierHandles); 366 }); 367 368 nameElement.appendChild(badge); 369 } 370 371 // Also add pill buttons when verification is found 372 createPillButtons(); 373 }; 374 375 // Function to show a popup with all verifiers 376 const showVerifiersPopup = (verifierHandles) => { 377 // Remove existing popup if any 378 const existingPopup = document.getElementById("verifiers-popup"); 379 if (existingPopup) { 380 existingPopup.remove(); 381 } 382 383 // Create popup 384 const popup = document.createElement("div"); 385 popup.id = "verifiers-popup"; 386 popup.style.cssText = ` 387 position: fixed; 388 top: 50%; 389 left: 50%; 390 transform: translate(-50%, -50%); 391 background-color: #24273A; 392 padding: 20px; 393 border-radius: 10px; 394 z-index: 10002; 395 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 396 max-width: 400px; 397 width: 90%; 398 `; 399 400 // Create popup content 401 popup.innerHTML = ` 402 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3> 403 <div style="max-height: 300px; overflow-y: auto;"> 404 ${verifierHandles 405 .map( 406 (handle) => ` 407 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;"> 408 ${handle} 409 </div> 410 `, 411 ) 412 .join("")} 413 </div> 414 <button id="close-verifiers-popup" style=" 415 margin-top: 15px; 416 padding: 8px 15px; 417 background-color: #473A3A; 418 color: white; 419 border: none; 420 border-radius: 4px; 421 cursor: pointer; 422 ">Close</button> 423 `; 424 425 // Add to body 426 document.body.appendChild(popup); 427 428 // Add close handler 429 document 430 .getElementById("close-verifiers-popup") 431 .addEventListener("click", () => { 432 popup.remove(); 433 }); 434 435 // Close when clicking outside 436 document.addEventListener("click", function closePopup(e) { 437 if (!popup.contains(e.target)) { 438 popup.remove(); 439 document.removeEventListener("click", closePopup); 440 } 441 }); 442 }; 443 444 // Create settings modal 445 let settingsModal = null; 446 447 // Function to update the list of trusted users in the UI 448 const updateTrustedUsersList = () => { 449 const trustedUsersList = document.getElementById("trustedUsersList"); 450 if (!trustedUsersList) return; 451 452 const users = getTrustedUsers(); 453 trustedUsersList.innerHTML = ""; 454 455 if (users.length === 0) { 456 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>"; 457 return; 458 } 459 460 for (const user of users) { 461 const userItem = document.createElement("div"); 462 userItem.style.cssText = ` 463 display: flex; 464 justify-content: space-between; 465 align-items: center; 466 padding: 8px 0; 467 border-bottom: 1px solid #eee; 468 `; 469 470 userItem.innerHTML = ` 471 <span>${user}</span> 472 <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> 473 `; 474 475 trustedUsersList.appendChild(userItem); 476 } 477 478 // Add event listeners to remove buttons 479 const removeButtons = document.querySelectorAll(".remove-user"); 480 for (const btn of removeButtons) { 481 btn.addEventListener("click", (e) => { 482 const handle = e.target.getAttribute("data-handle"); 483 removeTrustedUser(handle); 484 updateTrustedUsersList(); 485 }); 486 } 487 }; 488 489 // Function to create the settings modal 490 const createSettingsModal = () => { 491 // Create modal container 492 settingsModal = document.createElement("div"); 493 settingsModal.id = "bsky-trusted-settings-modal"; 494 settingsModal.style.cssText = ` 495 display: none; 496 position: fixed; 497 top: 0; 498 left: 0; 499 width: 100%; 500 height: 100%; 501 background-color: rgba(0, 0, 0, 0.5); 502 z-index: 10001; 503 justify-content: center; 504 align-items: center; 505 `; 506 507 // Create modal content 508 const modalContent = document.createElement("div"); 509 modalContent.style.cssText = ` 510 background-color: #24273A; 511 padding: 20px; 512 border-radius: 10px; 513 width: 400px; 514 max-height: 80vh; 515 overflow-y: auto; 516 `; 517 518 // Create modal header 519 const modalHeader = document.createElement("div"); 520 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`; 521 522 // Create input form 523 const form = document.createElement("div"); 524 form.innerHTML = ` 525 <p>Add Bluesky handles you trust:</p> 526 <div style="display: flex; margin-bottom: 15px;"> 527 <input id="trustedUserInput" type="text" placeholder="username.bsky.social" style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;"> 528 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button> 529 </div> 530 `; 531 532 // Create trusted users list 533 const trustedUsersList = document.createElement("div"); 534 trustedUsersList.id = "trustedUsersList"; 535 trustedUsersList.style.cssText = ` 536 margin-top: 15px; 537 border-top: 1px solid #eee; 538 padding-top: 15px; 539 `; 540 541 // Create cache control buttons 542 const cacheControls = document.createElement("div"); 543 cacheControls.style.cssText = ` 544 margin-top: 15px; 545 padding-top: 15px; 546 border-top: 1px solid #eee; 547 `; 548 549 const clearCacheButton = document.createElement("button"); 550 clearCacheButton.textContent = "Clear Verification Cache"; 551 clearCacheButton.style.cssText = ` 552 padding: 8px 15px; 553 background-color: #735A5A; 554 color: white; 555 border: none; 556 border-radius: 4px; 557 cursor: pointer; 558 margin-right: 10px; 559 `; 560 clearCacheButton.addEventListener("click", () => { 561 clearCache(); 562 alert( 563 "Verification cache cleared. Fresh data will be fetched on next check.", 564 ); 565 }); 566 567 cacheControls.appendChild(clearCacheButton); 568 569 // Create close button 570 const closeButton = document.createElement("button"); 571 closeButton.textContent = "Close"; 572 closeButton.style.cssText = ` 573 margin-top: 20px; 574 padding: 8px 15px; 575 background-color: #473A3A; 576 border: none; 577 border-radius: 4px; 578 cursor: pointer; 579 `; 580 581 // Assemble modal 582 modalContent.appendChild(modalHeader); 583 modalContent.appendChild(form); 584 modalContent.appendChild(trustedUsersList); 585 modalContent.appendChild(cacheControls); 586 modalContent.appendChild(closeButton); 587 settingsModal.appendChild(modalContent); 588 589 // Add to document 590 document.body.appendChild(settingsModal); 591 592 // Event listeners 593 closeButton.addEventListener("click", () => { 594 settingsModal.style.display = "none"; 595 }); 596 597 // Add trusted user button event 598 document 599 .getElementById("addTrustedUserBtn") 600 .addEventListener("click", () => { 601 const input = document.getElementById("trustedUserInput"); 602 const handle = input.value.trim(); 603 if (handle) { 604 addTrustedUser(handle); 605 input.value = ""; 606 updateTrustedUsersList(); 607 } 608 }); 609 610 // Close modal when clicking outside 611 settingsModal.addEventListener("click", (e) => { 612 if (e.target === settingsModal) { 613 settingsModal.style.display = "none"; 614 } 615 }); 616 617 // Initialize the list 618 updateTrustedUsersList(); 619 }; 620 621 // Function to create the settings UI if it doesn't exist yet 622 const createSettingsUI = () => { 623 // Create pill with buttons 624 createPillButtons(); 625 626 // Create the settings modal if it doesn't exist yet 627 if (!settingsModal) { 628 createSettingsModal(); 629 } 630 }; 631 632 // Function to check the current profile 633 const checkCurrentProfile = () => { 634 const currentUrl = window.location.href; 635 // Only trigger on profile pages 636 if ( 637 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) || 638 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) || 639 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/) 640 ) { 641 const handle = currentUrl.split("/profile/")[1].split("/")[0]; 642 console.log("Detected profile page for:", handle); 643 644 // Create and add the settings UI (only once) 645 createSettingsUI(); 646 647 // Fetch user profile data 648 fetch( 649 `https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 650 ) 651 .then((response) => response.json()) 652 .then((data) => { 653 console.log("User profile data:", data); 654 655 // Extract the DID from the profile data 656 const did = data.uri.split("/")[2]; 657 console.log("User DID:", did); 658 659 // Check if any trusted users have verified this profile using the DID 660 checkTrustedUserVerifications(did); 661 }) 662 .catch((error) => { 663 console.error("Error checking profile:", error); 664 }); 665 666 console.log("Bluesky profile detected"); 667 } else { 668 // Not on a profile page, reset state 669 currentProfileDid = null; 670 profileVerifiers = []; 671 672 // Remove UI elements if present 673 const existingBadge = document.getElementById( 674 "user-trusted-verification-badge", 675 ); 676 if (existingBadge) { 677 existingBadge.remove(); 678 } 679 680 const existingPill = document.getElementById( 681 "trusted-users-pill-container", 682 ); 683 if (existingPill) { 684 existingPill.remove(); 685 } 686 } 687 }; 688 689 // Initial check 690 checkCurrentProfile(); 691 692 // Set up a MutationObserver to watch for URL changes 693 const observeUrlChanges = () => { 694 let lastUrl = location.href; 695 696 const observer = new MutationObserver(() => { 697 if (location.href !== lastUrl) { 698 const oldUrl = lastUrl; 699 lastUrl = location.href; 700 console.log("URL changed from:", oldUrl, "to:", location.href); 701 702 // Reset current profile DID 703 currentProfileDid = null; 704 profileVerifiers = []; 705 706 // Clean up UI elements 707 const existingBadge = document.getElementById( 708 "user-trusted-verification-badge", 709 ); 710 if (existingBadge) { 711 existingBadge.remove(); 712 } 713 714 const existingPill = document.getElementById( 715 "trusted-users-pill-container", 716 ); 717 if (existingPill) { 718 existingPill.remove(); 719 } 720 721 // Check if we're on a profile page now 722 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated 723 } 724 }); 725 726 observer.observe(document, { subtree: true, childList: true }); 727 }; 728 729 // Start observing for URL changes 730 observeUrlChanges(); 731})();