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