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 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 // Legacy function kept for compatibility but redirects to the new implementation 294 const addRecheckButton = () => { 295 createPillButtons(); 296 }; 297 298 // Function to display verification badge on the profile 299 const displayVerificationBadge = (verifierHandles) => { 300 // Find the profile header or name element to add the badge to 301 const nameElement = document.querySelector( 302 '[data-testid="profileHeaderDisplayName"]', 303 ); 304 305 if (nameElement) { 306 // Check if badge already exists 307 if (!document.getElementById("user-trusted-verification-badge")) { 308 const badge = document.createElement("span"); 309 badge.id = "user-trusted-verification-badge"; 310 badge.innerHTML = "✓"; 311 312 // Create tooltip text with all verifiers 313 const verifiersText = 314 verifierHandles.length > 1 315 ? `Verified by: ${verifierHandles.join(", ")}` 316 : `Verified by ${verifierHandles[0]}`; 317 318 badge.title = verifiersText; 319 badge.style.cssText = ` 320 background-color: #0070ff; 321 color: white; 322 border-radius: 50%; 323 width: 18px; 324 height: 18px; 325 margin-left: 8px; 326 font-size: 12px; 327 font-weight: bold; 328 cursor: help; 329 display: inline-flex; 330 align-items: center; 331 justify-content: center; 332 `; 333 334 // Add a click event to show all verifiers 335 badge.addEventListener("click", (e) => { 336 e.stopPropagation(); 337 showVerifiersPopup(verifierHandles); 338 }); 339 340 nameElement.appendChild(badge); 341 } 342 } 343 344 // Also add pill buttons when verification is found 345 createPillButtons(); 346 }; 347 348 // Function to show a popup with all verifiers 349 const showVerifiersPopup = (verifierHandles) => { 350 // Remove existing popup if any 351 const existingPopup = document.getElementById("verifiers-popup"); 352 if (existingPopup) { 353 existingPopup.remove(); 354 } 355 356 // Create popup 357 const popup = document.createElement("div"); 358 popup.id = "verifiers-popup"; 359 popup.style.cssText = ` 360 position: fixed; 361 top: 50%; 362 left: 50%; 363 transform: translate(-50%, -50%); 364 background-color: #24273A; 365 padding: 20px; 366 border-radius: 10px; 367 z-index: 10002; 368 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 369 max-width: 400px; 370 width: 90%; 371 `; 372 373 // Create popup content 374 popup.innerHTML = ` 375 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3> 376 <div style="max-height: 300px; overflow-y: auto;"> 377 ${verifierHandles 378 .map( 379 (handle) => ` 380 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;"> 381 ${handle} 382 </div> 383 `, 384 ) 385 .join("")} 386 </div> 387 <button id="close-verifiers-popup" style=" 388 margin-top: 15px; 389 padding: 8px 15px; 390 background-color: #473A3A; 391 color: white; 392 border: none; 393 border-radius: 4px; 394 cursor: pointer; 395 ">Close</button> 396 `; 397 398 // Add to body 399 document.body.appendChild(popup); 400 401 // Add close handler 402 document 403 .getElementById("close-verifiers-popup") 404 .addEventListener("click", () => { 405 popup.remove(); 406 }); 407 408 // Close when clicking outside 409 document.addEventListener("click", function closePopup(e) { 410 if (!popup.contains(e.target)) { 411 popup.remove(); 412 document.removeEventListener("click", closePopup); 413 } 414 }); 415 }; 416 417 // Create settings modal 418 let settingsModal = null; 419 420 // Function to update the list of trusted users in the UI 421 const updateTrustedUsersList = () => { 422 const trustedUsersList = document.getElementById("trustedUsersList"); 423 if (!trustedUsersList) return; 424 425 const users = getTrustedUsers(); 426 trustedUsersList.innerHTML = ""; 427 428 if (users.length === 0) { 429 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>"; 430 return; 431 } 432 433 for (const user of users) { 434 const userItem = document.createElement("div"); 435 userItem.style.cssText = ` 436 display: flex; 437 justify-content: space-between; 438 align-items: center; 439 padding: 8px 0; 440 border-bottom: 1px solid #eee; 441 `; 442 443 userItem.innerHTML = ` 444 <span>${user}</span> 445 <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> 446 `; 447 448 trustedUsersList.appendChild(userItem); 449 } 450 451 // Add event listeners to remove buttons 452 const removeButtons = document.querySelectorAll(".remove-user"); 453 for (const btn of removeButtons) { 454 btn.addEventListener("click", (e) => { 455 const handle = e.target.getAttribute("data-handle"); 456 removeTrustedUser(handle); 457 updateTrustedUsersList(); 458 }); 459 } 460 }; 461 462 // Function to create the settings modal 463 const createSettingsModal = () => { 464 // Create modal container 465 settingsModal = document.createElement("div"); 466 settingsModal.id = "bsky-trusted-settings-modal"; 467 settingsModal.style.cssText = ` 468 display: none; 469 position: fixed; 470 top: 0; 471 left: 0; 472 width: 100%; 473 height: 100%; 474 background-color: rgba(0, 0, 0, 0.5); 475 z-index: 10001; 476 justify-content: center; 477 align-items: center; 478 `; 479 480 // Create modal content 481 const modalContent = document.createElement("div"); 482 modalContent.style.cssText = ` 483 background-color: #24273A; 484 padding: 20px; 485 border-radius: 10px; 486 width: 400px; 487 max-height: 80vh; 488 overflow-y: auto; 489 `; 490 491 // Create modal header 492 const modalHeader = document.createElement("div"); 493 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`; 494 495 // Create input form 496 const form = document.createElement("div"); 497 form.innerHTML = ` 498 <p>Add Bluesky handles you trust:</p> 499 <div style="display: flex; margin-bottom: 15px;"> 500 <input id="trustedUserInput" type="text" placeholder="username.bsky.social" style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;"> 501 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button> 502 </div> 503 `; 504 505 // Create trusted users list 506 const trustedUsersList = document.createElement("div"); 507 trustedUsersList.id = "trustedUsersList"; 508 trustedUsersList.style.cssText = ` 509 margin-top: 15px; 510 border-top: 1px solid #eee; 511 padding-top: 15px; 512 `; 513 514 // Create cache control buttons 515 const cacheControls = document.createElement("div"); 516 cacheControls.style.cssText = ` 517 margin-top: 15px; 518 padding-top: 15px; 519 border-top: 1px solid #eee; 520 `; 521 522 const clearCacheButton = document.createElement("button"); 523 clearCacheButton.textContent = "Clear Verification Cache"; 524 clearCacheButton.style.cssText = ` 525 padding: 8px 15px; 526 background-color: #735A5A; 527 color: white; 528 border: none; 529 border-radius: 4px; 530 cursor: pointer; 531 margin-right: 10px; 532 `; 533 clearCacheButton.addEventListener("click", () => { 534 clearCache(); 535 alert( 536 "Verification cache cleared. Fresh data will be fetched on next check.", 537 ); 538 }); 539 540 cacheControls.appendChild(clearCacheButton); 541 542 // Create close button 543 const closeButton = document.createElement("button"); 544 closeButton.textContent = "Close"; 545 closeButton.style.cssText = ` 546 margin-top: 20px; 547 padding: 8px 15px; 548 background-color: #473A3A; 549 border: none; 550 border-radius: 4px; 551 cursor: pointer; 552 `; 553 554 // Assemble modal 555 modalContent.appendChild(modalHeader); 556 modalContent.appendChild(form); 557 modalContent.appendChild(trustedUsersList); 558 modalContent.appendChild(cacheControls); 559 modalContent.appendChild(closeButton); 560 settingsModal.appendChild(modalContent); 561 562 // Add to document 563 document.body.appendChild(settingsModal); 564 565 // Event listeners 566 closeButton.addEventListener("click", () => { 567 settingsModal.style.display = "none"; 568 }); 569 570 // Add trusted user button event 571 document 572 .getElementById("addTrustedUserBtn") 573 .addEventListener("click", () => { 574 const input = document.getElementById("trustedUserInput"); 575 const handle = input.value.trim(); 576 if (handle) { 577 addTrustedUser(handle); 578 input.value = ""; 579 updateTrustedUsersList(); 580 } 581 }); 582 583 // Close modal when clicking outside 584 settingsModal.addEventListener("click", (e) => { 585 if (e.target === settingsModal) { 586 settingsModal.style.display = "none"; 587 } 588 }); 589 590 // Initialize the list 591 updateTrustedUsersList(); 592 }; 593 594 // Function to create the settings UI if it doesn't exist yet 595 const createSettingsUI = () => { 596 // Create pill with buttons 597 createPillButtons(); 598 599 // Create the settings modal if it doesn't exist yet 600 if (!settingsModal) { 601 createSettingsModal(); 602 } 603 }; 604 605 // Function to check the current profile 606 const checkCurrentProfile = () => { 607 const currentUrl = window.location.href; 608 if (currentUrl.includes("bsky.app/profile/")) { 609 const handle = currentUrl.split("/profile/")[1].split("/")[0]; 610 console.log("Extracted handle:", handle); 611 612 // Create and add the settings UI (only once) 613 createSettingsUI(); 614 615 // Fetch user profile data 616 fetch( 617 `https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 618 ) 619 .then((response) => response.json()) 620 .then((data) => { 621 console.log("User profile data:", data); 622 623 // Extract the DID from the profile data 624 const did = data.uri.split("/")[2]; 625 console.log("User DID:", did); 626 627 // Now fetch the app.bsky.graph.verification data specifically 628 fetch( 629 `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=app.bsky.graph.verification`, 630 ) 631 .then((response) => response.json()) 632 .then((verificationData) => { 633 console.log("Verification data:", verificationData); 634 if ( 635 verificationData.records && 636 verificationData.records.length > 0 637 ) { 638 console.log( 639 "User has app.bsky.graph.verification:", 640 verificationData.records, 641 ); 642 } else { 643 console.log("User does not have app.bsky.graph.verification"); 644 } 645 646 // Check if any trusted users have verified this profile using the DID 647 checkTrustedUserVerifications(did); 648 }) 649 .catch((verificationError) => { 650 console.error( 651 "Error fetching verification data:", 652 verificationError, 653 ); 654 }); 655 }) 656 .catch((error) => { 657 console.error("Error checking profile:", error); 658 }); 659 660 console.log("Bluesky profile detected"); 661 } 662 }; 663 664 // Initial check 665 checkCurrentProfile(); 666 667 // Set up a MutationObserver to watch for URL changes 668 const observeUrlChanges = () => { 669 let lastUrl = location.href; 670 671 const observer = new MutationObserver(() => { 672 if (location.href !== lastUrl) { 673 lastUrl = location.href; 674 console.log("URL changed to:", location.href); 675 676 // Remove any existing badges when URL changes 677 const existingBadge = document.getElementById( 678 "user-trusted-verification-badge", 679 ); 680 if (existingBadge) { 681 existingBadge.remove(); 682 } 683 684 // Remove the pill container when URL changes 685 const existingPill = document.getElementById( 686 "trusted-users-pill-container", 687 ); 688 if (existingPill) { 689 existingPill.remove(); 690 } 691 692 // Check if we're on a profile page now 693 checkCurrentProfile(); 694 } 695 }); 696 697 observer.observe(document, { subtree: true, childList: true }); 698 }; 699 700 // Start observing for URL changes 701 observeUrlChanges(); 702})();