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 // Add recheck button even when no verifications are found 205 createPillButtons(); 206 207 return false; 208 }; 209 210 // Function to create a pill with recheck and settings buttons 211 const createPillButtons = () => { 212 // Remove existing buttons if any 213 const existingPill = document.getElementById( 214 "trusted-users-pill-container", 215 ); 216 if (existingPill) { 217 existingPill.remove(); 218 } 219 220 // Create pill container 221 const pillContainer = document.createElement("div"); 222 pillContainer.id = "trusted-users-pill-container"; 223 pillContainer.style.cssText = ` 224 position: fixed; 225 bottom: 20px; 226 right: 20px; 227 z-index: 10000; 228 display: flex; 229 border-radius: 20px; 230 overflow: hidden; 231 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 232 `; 233 234 // Create recheck button (left half of pill) 235 const recheckButton = document.createElement("button"); 236 recheckButton.id = "trusted-users-recheck-button"; 237 recheckButton.innerHTML = "↻ Recheck"; 238 recheckButton.style.cssText = ` 239 padding: 10px 15px; 240 background-color: #2D578D; 241 color: white; 242 border: none; 243 cursor: pointer; 244 font-weight: bold; 245 border-top-left-radius: 20px; 246 border-bottom-left-radius: 20px; 247 `; 248 249 // Add click event to recheck 250 recheckButton.addEventListener("click", async () => { 251 if (currentProfileDid) { 252 // Remove any existing badges when rechecking 253 const existingBadge = document.getElementById( 254 "user-trusted-verification-badge", 255 ); 256 if (existingBadge) { 257 existingBadge.remove(); 258 } 259 260 // Show loading state 261 recheckButton.innerHTML = "⟳ Checking..."; 262 recheckButton.disabled = true; 263 264 // Recheck verifications 265 await checkTrustedUserVerifications(currentProfileDid); 266 267 // Reset button 268 recheckButton.innerHTML = "↻ Recheck"; 269 recheckButton.disabled = false; 270 } 271 }); 272 273 // Create vertical divider 274 const divider = document.createElement("div"); 275 divider.style.cssText = ` 276 width: 1px; 277 background-color: rgba(255, 255, 255, 0.3); 278 `; 279 280 // Create settings button (right half of pill) 281 const settingsButton = document.createElement("button"); 282 settingsButton.id = "bsky-trusted-settings-button"; 283 settingsButton.textContent = "Settings"; 284 settingsButton.style.cssText = ` 285 padding: 10px 15px; 286 background-color: #2D578D; 287 color: white; 288 border: none; 289 cursor: pointer; 290 font-weight: bold; 291 border-top-right-radius: 20px; 292 border-bottom-right-radius: 20px; 293 `; 294 295 // Add elements to pill 296 pillContainer.appendChild(recheckButton); 297 pillContainer.appendChild(divider); 298 pillContainer.appendChild(settingsButton); 299 300 // Add pill to page 301 document.body.appendChild(pillContainer); 302 303 // Add event listener to settings button 304 settingsButton.addEventListener("click", () => { 305 if (settingsModal) { 306 settingsModal.style.display = "flex"; 307 updateTrustedUsersList(); 308 } else { 309 createSettingsModal(); 310 } 311 }); 312 }; 313 314 // Function to display verification badge on the profile 315 const displayVerificationBadge = (verifierHandles) => { 316 // Find the profile header or name element to add the badge to 317 const nameElements = document.querySelectorAll( 318 '[data-testid="profileHeaderDisplayName"]', 319 ); 320 const nameElement = nameElements[nameElements.length - 1]; 321 322 console.log(nameElement); 323 324 if (nameElement) { 325 // Remove existing badge if present 326 const existingBadge = document.getElementById( 327 "user-trusted-verification-badge", 328 ); 329 if (existingBadge) { 330 existingBadge.remove(); 331 } 332 333 const badge = document.createElement("span"); 334 badge.id = "user-trusted-verification-badge"; 335 badge.innerHTML = "✓"; 336 337 // Create tooltip text with all verifiers 338 const verifiersText = 339 verifierHandles.length > 1 340 ? `Verified by: ${verifierHandles.join(", ")}` 341 : `Verified by ${verifierHandles[0]}`; 342 343 badge.title = verifiersText; 344 badge.style.cssText = ` 345 background-color: #0070ff; 346 color: white; 347 border-radius: 50%; 348 width: 18px; 349 height: 18px; 350 margin-left: 8px; 351 font-size: 12px; 352 font-weight: bold; 353 cursor: help; 354 display: inline-flex; 355 align-items: center; 356 justify-content: center; 357 `; 358 359 // Add a click event to show all verifiers 360 badge.addEventListener("click", (e) => { 361 e.stopPropagation(); 362 showVerifiersPopup(verifierHandles); 363 }); 364 365 nameElement.appendChild(badge); 366 } 367 368 // Also add pill buttons when verification is found 369 createPillButtons(); 370 }; 371 372 // Function to show a popup with all verifiers 373 const showVerifiersPopup = (verifierHandles) => { 374 // Remove existing popup if any 375 const existingPopup = document.getElementById("verifiers-popup"); 376 if (existingPopup) { 377 existingPopup.remove(); 378 } 379 380 // Create popup 381 const popup = document.createElement("div"); 382 popup.id = "verifiers-popup"; 383 popup.style.cssText = ` 384 position: fixed; 385 top: 50%; 386 left: 50%; 387 transform: translate(-50%, -50%); 388 background-color: #24273A; 389 padding: 20px; 390 border-radius: 10px; 391 z-index: 10002; 392 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 393 max-width: 400px; 394 width: 90%; 395 `; 396 397 // Create popup content 398 popup.innerHTML = ` 399 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3> 400 <div style="max-height: 300px; overflow-y: auto;"> 401 ${verifierHandles 402 .map( 403 (handle) => ` 404 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;"> 405 ${handle} 406 </div> 407 `, 408 ) 409 .join("")} 410 </div> 411 <button id="close-verifiers-popup" style=" 412 margin-top: 15px; 413 padding: 8px 15px; 414 background-color: #473A3A; 415 color: white; 416 border: none; 417 border-radius: 4px; 418 cursor: pointer; 419 ">Close</button> 420 `; 421 422 // Add to body 423 document.body.appendChild(popup); 424 425 // Add close handler 426 document 427 .getElementById("close-verifiers-popup") 428 .addEventListener("click", () => { 429 popup.remove(); 430 }); 431 432 // Close when clicking outside 433 document.addEventListener("click", function closePopup(e) { 434 if (!popup.contains(e.target)) { 435 popup.remove(); 436 document.removeEventListener("click", closePopup); 437 } 438 }); 439 }; 440 441 // Create settings modal 442 let settingsModal = null; 443 444 // Function to update the list of trusted users in the UI 445 const updateTrustedUsersList = () => { 446 const trustedUsersList = document.getElementById("trustedUsersList"); 447 if (!trustedUsersList) return; 448 449 const users = getTrustedUsers(); 450 trustedUsersList.innerHTML = ""; 451 452 if (users.length === 0) { 453 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>"; 454 return; 455 } 456 457 for (const user of users) { 458 const userItem = document.createElement("div"); 459 userItem.style.cssText = ` 460 display: flex; 461 justify-content: space-between; 462 align-items: center; 463 padding: 8px 0; 464 border-bottom: 1px solid #eee; 465 `; 466 467 userItem.innerHTML = ` 468 <span>${user}</span> 469 <button class="remove-user" data-handle="${user}" style="background-color: #CE3838; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer;">Remove</button> 470 `; 471 472 trustedUsersList.appendChild(userItem); 473 } 474 475 // Add event listeners to remove buttons 476 const removeButtons = document.querySelectorAll(".remove-user"); 477 for (const btn of removeButtons) { 478 btn.addEventListener("click", (e) => { 479 const handle = e.target.getAttribute("data-handle"); 480 removeTrustedUser(handle); 481 removeUserFromCache(handle); 482 updateTrustedUsersList(); 483 }); 484 } 485 }; 486 487 const searchUsers = async (searchQuery) => { 488 if (!searchQuery || searchQuery.length < 2) return []; 489 490 try { 491 const response = await fetch( 492 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?term=${encodeURIComponent(searchQuery)}&limit=5`, 493 ); 494 const data = await response.json(); 495 return data.actors || []; 496 } catch (error) { 497 console.error("Error searching for users:", error); 498 return []; 499 } 500 }; 501 502 // Function to create and show the autocomplete dropdown 503 const showAutocompleteResults = (results, inputElement) => { 504 // Remove existing dropdown if any 505 const existingDropdown = document.getElementById("autocomplete-dropdown"); 506 if (existingDropdown) existingDropdown.remove(); 507 508 if (results.length === 0) return; 509 510 // Create dropdown 511 const dropdown = document.createElement("div"); 512 dropdown.id = "autocomplete-dropdown"; 513 dropdown.style.cssText = ` 514 position: absolute; 515 background-color: #2A2E3D; 516 border: 1px solid #444; 517 border-radius: 4px; 518 box-shadow: 0 4px 8px rgba(0,0,0,0.2); 519 max-height: 300px; 520 overflow-y: auto; 521 width: ${inputElement.offsetWidth}px; 522 z-index: 10002; 523 margin-top: 2px; 524 `; 525 526 // Position dropdown below input 527 const inputRect = inputElement.getBoundingClientRect(); 528 dropdown.style.left = `${inputRect.left}px`; 529 dropdown.style.top = `${inputRect.bottom}px`; 530 531 // Add results to dropdown 532 for (const user of results) { 533 const userItem = document.createElement("div"); 534 userItem.className = "autocomplete-item"; 535 userItem.style.cssText = ` 536 display: flex; 537 align-items: center; 538 padding: 8px 12px; 539 cursor: pointer; 540 color: white; 541 border-bottom: 1px solid #444; 542 `; 543 userItem.onmouseover = () => { 544 userItem.style.backgroundColor = "#3A3F55"; 545 }; 546 userItem.onmouseout = () => { 547 userItem.style.backgroundColor = ""; 548 }; 549 550 // Add profile picture 551 const avatar = document.createElement("img"); 552 avatar.src = user.avatar || "https://bsky.app/static/default-avatar.png"; 553 avatar.style.cssText = ` 554 width: 32px; 555 height: 32px; 556 border-radius: 50%; 557 margin-right: 10px; 558 object-fit: cover; 559 `; 560 561 // Add user info 562 const userInfo = document.createElement("div"); 563 userInfo.style.cssText = ` 564 display: flex; 565 flex-direction: column; 566 `; 567 568 const displayName = document.createElement("div"); 569 displayName.textContent = user.displayName || user.handle; 570 displayName.style.fontWeight = "bold"; 571 572 const handle = document.createElement("div"); 573 handle.textContent = user.handle; 574 handle.style.fontSize = "0.8em"; 575 handle.style.opacity = "0.8"; 576 577 userInfo.appendChild(displayName); 578 userInfo.appendChild(handle); 579 580 userItem.appendChild(avatar); 581 userItem.appendChild(userInfo); 582 583 // Handle click on user item 584 userItem.addEventListener("click", () => { 585 inputElement.value = user.handle; 586 dropdown.remove(); 587 }); 588 589 dropdown.appendChild(userItem); 590 } 591 592 document.body.appendChild(dropdown); 593 594 // Close dropdown when clicking outside 595 document.addEventListener("click", function closeDropdown(e) { 596 if (e.target !== inputElement && !dropdown.contains(e.target)) { 597 dropdown.remove(); 598 document.removeEventListener("click", closeDropdown); 599 } 600 }); 601 }; 602 603 // Function to import verifications from the current user 604 const importVerificationsFromSelf = async () => { 605 try { 606 // Check if we can determine the current user 607 const bskyStorageData = localStorage.getItem("BSKY_STORAGE"); 608 let userData = null; 609 610 if (bskyStorageData) { 611 try { 612 const bskyStorage = JSON.parse(bskyStorageData); 613 if (bskyStorage.session.currentAccount) { 614 userData = bskyStorage.session.currentAccount; 615 } 616 } catch (error) { 617 console.error("Error parsing BSKY_STORAGE data:", error); 618 } 619 } 620 621 if (!userData || !userData.handle) { 622 alert( 623 "Could not determine your Bluesky handle. Please ensure you're logged in.", 624 ); 625 return; 626 } 627 628 if (!userData || !userData.handle) { 629 alert( 630 "Unable to determine your Bluesky handle. Make sure you're logged in.", 631 ); 632 return; 633 } 634 635 const userHandle = userData.handle; 636 637 // Show loading state 638 const importButton = document.getElementById("importVerificationsBtn"); 639 const originalText = importButton.textContent; 640 importButton.textContent = "Importing..."; 641 importButton.disabled = true; 642 643 // Fetch verification records from the user's account with pagination 644 let allRecords = []; 645 let cursor = null; 646 let hasMore = true; 647 648 while (hasMore) { 649 const url = cursor 650 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification&cursor=${cursor}` 651 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification`; 652 653 const verificationResponse = await fetch(url); 654 const data = await verificationResponse.json(); 655 656 if (data.records && data.records.length > 0) { 657 allRecords = [...allRecords, ...data.records]; 658 } 659 660 if (data.cursor) { 661 cursor = data.cursor; 662 } else { 663 hasMore = false; 664 } 665 } 666 667 const verificationData = { records: allRecords }; 668 669 if (!verificationData.records || verificationData.records.length === 0) { 670 alert("No verification records found in your account."); 671 importButton.textContent = originalText; 672 importButton.disabled = false; 673 return; 674 } 675 676 // Extract the handles of verified users 677 const verifiedUsers = []; 678 for (const record of verificationData.records) { 679 console.log(record.value.handle); 680 verifiedUsers.push(record.value.handle); 681 } 682 683 // Add all found users to trusted users 684 let addedCount = 0; 685 for (const handle of verifiedUsers) { 686 const existingUsers = getTrustedUsers(); 687 if (!existingUsers.includes(handle)) { 688 addTrustedUser(handle); 689 addedCount++; 690 } 691 } 692 693 // Update the UI 694 updateTrustedUsersList(); 695 696 // Reset button state 697 importButton.textContent = originalText; 698 importButton.disabled = false; 699 700 // Show result 701 alert( 702 `Successfully imported ${addedCount} verified users from your account.`, 703 ); 704 } catch (error) { 705 console.error("Error importing verifications:", error); 706 alert("Error importing verifications. Check console for details."); 707 const importButton = document.getElementById("importVerificationsBtn"); 708 if (importButton) { 709 importButton.textContent = "Import Verifications"; 710 importButton.disabled = false; 711 } 712 } 713 }; 714 715 const addSettingsButton = () => { 716 // Check if we're on the settings page 717 if (!window.location.href.includes("bsky.app/settings")) { 718 return; 719 } 720 721 // Check if our button already exists to avoid duplicates 722 if (document.getElementById("community-verifications-settings-button")) { 723 return; 724 } 725 726 // Find the right place to insert our button (after content-and-media link) 727 const contentMediaLink = document.querySelector( 728 'a[href="/settings/content-and-media"]', 729 ); 730 if (!contentMediaLink) { 731 console.log("Could not find content-and-media link to insert after"); 732 return; 733 } 734 735 // Clone the existing link and modify it 736 const verificationButton = contentMediaLink.cloneNode(true); 737 verificationButton.id = "community-verifications-settings-button"; 738 verificationButton.href = "#"; // No actual link, we'll handle click with JS 739 verificationButton.setAttribute("aria-label", "Community Verifications"); 740 741 const highlightColor = 742 verificationButton.firstChild.style.backgroundColor || "rgb(30,41,54)"; 743 744 // Add hover effect to highlight the button 745 verificationButton.addEventListener("mouseover", () => { 746 verificationButton.firstChild.style.backgroundColor = highlightColor; 747 }); 748 749 verificationButton.addEventListener("mouseout", () => { 750 verificationButton.firstChild.style.backgroundColor = null; 751 }); 752 753 // Update the text content 754 const textDiv = verificationButton.querySelector(".css-146c3p1"); 755 if (textDiv) { 756 textDiv.textContent = "Community Verifications"; 757 } 758 759 // Update the icon 760 const iconDiv = verificationButton.querySelector( 761 ".css-175oi2r[style*='width: 28px']", 762 ); 763 if (iconDiv) { 764 iconDiv.innerHTML = ` 765 <svg fill="none" width="28" viewBox="0 0 24 24" height="28" style="color: rgb(241, 243, 245);"> 766 <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 767 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 768 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 769 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 770 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 771 c0-0.9,0.7-1.7,1.7-1.7h3c0.9,0,1.7,0.7,1.7,1.7v2.3H9.7V6.3z"/> 772 </svg> 773 `; 774 } 775 776 // Insert our button after the content-and-media link 777 const parentElement = contentMediaLink.parentElement; 778 parentElement.insertBefore( 779 verificationButton, 780 contentMediaLink.nextSibling, 781 ); 782 783 // Add click event to open our settings modal 784 verificationButton.addEventListener("click", (e) => { 785 e.preventDefault(); 786 if (settingsModal) { 787 settingsModal.style.display = "flex"; 788 updateTrustedUsersList(); 789 } else { 790 createSettingsModal(); 791 } 792 }); 793 794 console.log("Added Community Verifications button to settings page"); 795 }; 796 797 // Function to create the settings modal 798 const createSettingsModal = () => { 799 // Create modal container 800 settingsModal = document.createElement("div"); 801 settingsModal.id = "bsky-trusted-settings-modal"; 802 settingsModal.style.cssText = ` 803 display: none; 804 position: fixed; 805 top: 0; 806 left: 0; 807 width: 100%; 808 height: 100%; 809 background-color: rgba(0, 0, 0, 0.5); 810 z-index: 10001; 811 justify-content: center; 812 align-items: center; 813 `; 814 815 // Create modal content 816 const modalContent = document.createElement("div"); 817 modalContent.style.cssText = ` 818 background-color: #24273A; 819 padding: 20px; 820 border-radius: 10px; 821 width: 400px; 822 max-height: 80vh; 823 overflow-y: auto; 824 `; 825 826 // Create modal header 827 const modalHeader = document.createElement("div"); 828 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`; 829 830 // Create input form 831 const form = document.createElement("div"); 832 form.innerHTML = ` 833 <p>Add Bluesky handles you trust:</p> 834 <div style="display: flex; margin-bottom: 15px; position: relative;"> 835 <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;"> 836 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button> 837 </div> 838 `; 839 840 // Create import button 841 const importContainer = document.createElement("div"); 842 importContainer.style.cssText = ` 843 margin-top: 10px; 844 margin-bottom: 15px; 845 `; 846 847 const importButton = document.createElement("button"); 848 importButton.id = "importVerificationsBtn"; 849 importButton.textContent = "Import Your Verifications"; 850 importButton.style.cssText = ` 851 background-color: #2D578D; 852 color: white; 853 border: none; 854 border-radius: 4px; 855 padding: 8px 15px; 856 cursor: pointer; 857 width: 100%; 858 `; 859 860 importButton.addEventListener("click", importVerificationsFromSelf); 861 importContainer.appendChild(importButton); 862 863 // Create trusted users list 864 const trustedUsersList = document.createElement("div"); 865 trustedUsersList.id = "trustedUsersList"; 866 trustedUsersList.style.cssText = ` 867 margin-top: 15px; 868 border-top: 1px solid #eee; 869 padding-top: 15px; 870 `; 871 872 // Create cache control buttons 873 const cacheControls = document.createElement("div"); 874 cacheControls.style.cssText = ` 875 margin-top: 15px; 876 padding-top: 15px; 877 border-top: 1px solid #eee; 878 `; 879 880 const clearCacheButton = document.createElement("button"); 881 clearCacheButton.textContent = "Clear Verification Cache"; 882 clearCacheButton.style.cssText = ` 883 padding: 8px 15px; 884 background-color: #735A5A; 885 color: white; 886 border: none; 887 border-radius: 4px; 888 cursor: pointer; 889 margin-right: 10px; 890 `; 891 clearCacheButton.addEventListener("click", () => { 892 clearCache(); 893 alert( 894 "Verification cache cleared. Fresh data will be fetched on next check.", 895 ); 896 }); 897 898 cacheControls.appendChild(clearCacheButton); 899 900 // Create close button 901 const closeButton = document.createElement("button"); 902 closeButton.textContent = "Close"; 903 closeButton.style.cssText = ` 904 margin-top: 20px; 905 padding: 8px 15px; 906 background-color: #473A3A; 907 border: none; 908 border-radius: 4px; 909 cursor: pointer; 910 `; 911 912 // Assemble modal 913 modalContent.appendChild(modalHeader); 914 modalContent.appendChild(form); 915 modalContent.appendChild(importContainer); 916 modalContent.appendChild(trustedUsersList); 917 modalContent.appendChild(cacheControls); 918 modalContent.appendChild(closeButton); 919 settingsModal.appendChild(modalContent); 920 921 // Add to document 922 document.body.appendChild(settingsModal); 923 924 const userInput = document.getElementById("trustedUserInput"); 925 926 // Add input event for autocomplete 927 let debounceTimeout; 928 userInput.addEventListener("input", (e) => { 929 clearTimeout(debounceTimeout); 930 debounceTimeout = setTimeout(async () => { 931 const searchQuery = e.target.value.trim(); 932 if (searchQuery.length >= 2) { 933 const results = await searchUsers(searchQuery); 934 showAutocompleteResults(results, userInput); 935 } else { 936 const dropdown = document.getElementById("autocomplete-dropdown"); 937 if (dropdown) dropdown.remove(); 938 } 939 }, 300); // Debounce for 300ms 940 }); 941 942 // Event listeners 943 closeButton.addEventListener("click", () => { 944 settingsModal.style.display = "none"; 945 }); 946 947 // Function to add a user from the input field 948 const addUserFromInput = () => { 949 const input = document.getElementById("trustedUserInput"); 950 const handle = input.value.trim(); 951 if (handle) { 952 addTrustedUser(handle); 953 input.value = ""; 954 updateTrustedUsersList(); 955 956 // Remove dropdown if present 957 const dropdown = document.getElementById("autocomplete-dropdown"); 958 if (dropdown) dropdown.remove(); 959 } 960 }; 961 962 // Add trusted user button event 963 document 964 .getElementById("addTrustedUserBtn") 965 .addEventListener("click", addUserFromInput); 966 967 // Add keydown event to input for Enter key 968 userInput.addEventListener("keydown", (e) => { 969 if (e.key === "Enter") { 970 e.preventDefault(); 971 addUserFromInput(); 972 } 973 }); 974 975 // Close modal when clicking outside 976 settingsModal.addEventListener("click", (e) => { 977 if (e.target === settingsModal) { 978 settingsModal.style.display = "none"; 979 } 980 }); 981 982 // Initialize the list 983 updateTrustedUsersList(); 984 }; 985 986 // Function to create the settings UI if it doesn't exist yet 987 const createSettingsUI = () => { 988 // Create pill with buttons 989 createPillButtons(); 990 991 // Create the settings modal if it doesn't exist yet 992 if (!settingsModal) { 993 createSettingsModal(); 994 } 995 }; 996 997 // Function to check the current profile 998 const checkCurrentProfile = () => { 999 const currentUrl = window.location.href; 1000 // Only trigger on profile pages 1001 if ( 1002 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) || 1003 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) || 1004 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/) 1005 ) { 1006 const handle = currentUrl.split("/profile/")[1].split("/")[0]; 1007 console.log("Detected profile page for:", handle); 1008 1009 // Create and add the settings UI (only once) 1010 createSettingsUI(); 1011 1012 // Fetch user profile data 1013 fetch( 1014 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 1015 ) 1016 .then((response) => response.json()) 1017 .then((data) => { 1018 console.log("User profile data:", data); 1019 1020 // Extract the DID from the profile data 1021 const did = data.uri.split("/")[2]; 1022 console.log("User DID:", did); 1023 1024 // Check if any trusted users have verified this profile using the DID 1025 checkTrustedUserVerifications(did); 1026 }) 1027 .catch((error) => { 1028 console.error("Error checking profile:", error); 1029 }); 1030 1031 console.log("Bluesky profile detected"); 1032 } else { 1033 // Not on a profile page, reset state 1034 currentProfileDid = null; 1035 profileVerifiers = []; 1036 1037 // Remove UI elements if present 1038 const existingBadge = document.getElementById( 1039 "user-trusted-verification-badge", 1040 ); 1041 if (existingBadge) { 1042 existingBadge.remove(); 1043 } 1044 1045 const existingPill = document.getElementById( 1046 "trusted-users-pill-container", 1047 ); 1048 if (existingPill) { 1049 existingPill.remove(); 1050 } 1051 } 1052 }; 1053 1054 // Initial check 1055 checkCurrentProfile(); 1056 1057 const checkUserLinksOnPage = async () => { 1058 // Look for profile links with handles 1059 // Find all profile links and filter to get only one link per parent 1060 const allProfileLinks = Array.from( 1061 document.querySelectorAll('a[href^="/profile/"]:not(:has(*))'), 1062 ); 1063 1064 // Use a Map to keep track of parent elements and their first child link 1065 const parentMap = new Map(); 1066 1067 // For each link, store only the first one found for each parent 1068 for (const link of allProfileLinks) { 1069 const parent = link.parentElement; 1070 if (parent && !parentMap.has(parent)) { 1071 parentMap.set(parent, link); 1072 } 1073 } 1074 1075 // Get only the first link for each parent 1076 const profileLinks = Array.from(parentMap.values()); 1077 1078 if (profileLinks.length === 0) return; 1079 1080 console.log(`Found ${profileLinks.length} possible user links on page`); 1081 1082 // Process profile links to identify user containers 1083 for (const link of profileLinks) { 1084 try { 1085 // Check if we already processed this link 1086 if (link.getAttribute("data-verification-checked") === "true") continue; 1087 1088 // Mark as checked 1089 link.setAttribute("data-verification-checked", "true"); 1090 1091 // Extract handle from href 1092 const handle = link.getAttribute("href").split("/profile/")[1]; 1093 if (!handle) continue; 1094 1095 // check if there is anything after the handle 1096 const handleTrailing = handle.split("/").length > 1; 1097 if (handleTrailing) continue; 1098 1099 // Find parent container that might contain the handle and verification icon 1100 // Look for containers where this link is followed by another link with the same handle 1101 const parent = link.parentElement; 1102 1103 // If we found a container with the verification icon 1104 if (parent) { 1105 // Check if this user already has our verification badge 1106 if (parent.querySelector(".trusted-user-inline-badge")) continue; 1107 1108 try { 1109 // Fetch user profile data to get DID 1110 const response = await fetch( 1111 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`, 1112 ); 1113 const data = await response.json(); 1114 1115 // Extract the DID from the profile data 1116 const did = data.uri.split("/")[2]; 1117 1118 // Check if this user is verified by our trusted users 1119 const trustedUsers = getTrustedUsers(); 1120 let isVerified = false; 1121 const verifiers = []; 1122 1123 // Check cache first for each trusted user 1124 for (const trustedUser of trustedUsers) { 1125 const cachedData = getCachedVerifications(trustedUser); 1126 1127 if (cachedData && isCacheValid(cachedData)) { 1128 // Use cached verification data 1129 const records = cachedData.records; 1130 1131 for (const record of records) { 1132 if (record.value && record.value.subject === did) { 1133 isVerified = true; 1134 verifiers.push(trustedUser); 1135 break; 1136 } 1137 } 1138 } 1139 } 1140 1141 // If verified, add a small badge 1142 if (isVerified && verifiers.length > 0) { 1143 // Create a badge element 1144 const smallBadge = document.createElement("span"); 1145 smallBadge.className = "trusted-user-inline-badge"; 1146 smallBadge.innerHTML = "✓"; 1147 1148 // Create tooltip text with all verifiers 1149 const verifiersText = 1150 verifiers.length > 1 1151 ? `Verified by: ${verifiers.join(", ")}` 1152 : `Verified by ${verifiers[0]}`; 1153 1154 smallBadge.title = verifiersText; 1155 smallBadge.style.cssText = ` 1156 background-color: #0070ff; 1157 color: white; 1158 border-radius: 50%; 1159 width: 14px; 1160 height: 14px; 1161 font-size: 10px; 1162 font-weight: bold; 1163 cursor: help; 1164 display: inline-flex; 1165 align-items: center; 1166 justify-content: center; 1167 margin-left: 4px; 1168 `; 1169 1170 // Add click event to show verifiers 1171 smallBadge.addEventListener("click", (e) => { 1172 e.stopPropagation(); 1173 showVerifiersPopup(verifiers); 1174 }); 1175 1176 // Insert badge after the SVG element 1177 parent.firstChild.after(smallBadge); 1178 parent.style.flexDirection = "row"; 1179 parent.style.alignItems = "center"; 1180 } 1181 } catch (error) { 1182 console.error(`Error checking verification for ${handle}:`, error); 1183 } 1184 } 1185 } catch (error) { 1186 console.error("Error processing profile link:", error); 1187 } 1188 } 1189 }; 1190 1191 const observeContentChanges = () => { 1192 // Use a debounced function to check for new user links 1193 const debouncedCheck = () => { 1194 clearTimeout(window.userLinksCheckTimeout); 1195 window.userLinksCheckTimeout = setTimeout(() => { 1196 checkUserLinksOnPage(); 1197 }, 300); 1198 }; 1199 1200 // Create a mutation observer that watches for DOM changes 1201 const observer = new MutationObserver((mutations) => { 1202 let hasRelevantChanges = false; 1203 1204 // Check if any mutations involve adding new nodes 1205 for (const mutation of mutations) { 1206 if (mutation.addedNodes.length > 0) { 1207 for (const node of mutation.addedNodes) { 1208 if (node.nodeType === Node.ELEMENT_NODE) { 1209 // Check if this element or its children might contain profile links 1210 if ( 1211 node.querySelector('a[href^="/profile/"]') || 1212 (node.tagName === "A" && 1213 node.getAttribute("href")?.startsWith("/profile/")) 1214 ) { 1215 hasRelevantChanges = true; 1216 break; 1217 } 1218 } 1219 } 1220 } 1221 if (hasRelevantChanges) break; 1222 } 1223 1224 if (hasRelevantChanges) { 1225 debouncedCheck(); 1226 } 1227 }); 1228 1229 // Observe the entire document for content changes that might include profile links 1230 observer.observe(document.body, { childList: true, subtree: true }); 1231 1232 // Also check periodically for posts that might have been loaded but not caught by the observer 1233 setInterval(debouncedCheck, 5000); 1234 }; 1235 1236 // Wait for DOM to be fully loaded before initializing 1237 document.addEventListener("DOMContentLoaded", () => { 1238 // Initial check for user links 1239 checkUserLinksOnPage(); 1240 1241 // Add settings button if we're on the settings page 1242 if (window.location.href.includes("bsky.app/settings")) { 1243 // Wait for the content-and-media link to appear before adding our button 1244 const waitForSettingsLink = setInterval(() => { 1245 const contentMediaLink = document.querySelector( 1246 'a[href="/settings/content-and-media"]', 1247 ); 1248 if (contentMediaLink) { 1249 clearInterval(waitForSettingsLink); 1250 addSettingsButton(); 1251 } 1252 }, 200); 1253 } 1254 }); 1255 1256 // Start observing for content changes to detect newly loaded posts 1257 observeContentChanges(); 1258 1259 // Set up a MutationObserver to watch for URL changes 1260 const observeUrlChanges = () => { 1261 let lastUrl = location.href; 1262 1263 const observer = new MutationObserver(() => { 1264 if (location.href !== lastUrl) { 1265 const oldUrl = lastUrl; 1266 lastUrl = location.href; 1267 console.log("URL changed from:", oldUrl, "to:", location.href); 1268 1269 // Reset current profile DID 1270 currentProfileDid = null; 1271 profileVerifiers = []; 1272 1273 // Clean up UI elements 1274 const existingBadge = document.getElementById( 1275 "user-trusted-verification-badge", 1276 ); 1277 if (existingBadge) { 1278 existingBadge.remove(); 1279 } 1280 1281 const existingPill = document.getElementById( 1282 "trusted-users-pill-container", 1283 ); 1284 if (existingPill) { 1285 existingPill.remove(); 1286 } 1287 1288 // Check if we're on a profile page now 1289 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated 1290 1291 if (window.location.href.includes("bsky.app/settings")) { 1292 // Give the page a moment to fully load 1293 setTimeout(addSettingsButton, 500); 1294 } 1295 } 1296 }); 1297 1298 observer.observe(document, { subtree: true, childList: true }); 1299 }; 1300 1301 // Start observing for URL changes 1302 observeUrlChanges(); 1303})();