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