random bun scripts that dont fit anywhere else
1// ==UserScript==
2// @name Bluesky Community Verifications
3// @namespace https://tangled.sh/@dunkirk.sh/bunplayground
4// @version 0.1
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 create the settings modal
604 const createSettingsModal = () => {
605 // Create modal container
606 settingsModal = document.createElement("div");
607 settingsModal.id = "bsky-trusted-settings-modal";
608 settingsModal.style.cssText = `
609 display: none;
610 position: fixed;
611 top: 0;
612 left: 0;
613 width: 100%;
614 height: 100%;
615 background-color: rgba(0, 0, 0, 0.5);
616 z-index: 10001;
617 justify-content: center;
618 align-items: center;
619 `;
620
621 // Create modal content
622 const modalContent = document.createElement("div");
623 modalContent.style.cssText = `
624 background-color: #24273A;
625 padding: 20px;
626 border-radius: 10px;
627 width: 400px;
628 max-height: 80vh;
629 overflow-y: auto;
630 `;
631
632 // Create modal header
633 const modalHeader = document.createElement("div");
634 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`;
635
636 // Create input form
637 const form = document.createElement("div");
638 form.innerHTML = `
639 <p>Add Bluesky handles you trust:</p>
640 <div style="display: flex; margin-bottom: 15px; position: relative;">
641 <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;">
642 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button>
643 </div>
644 `;
645
646 // Create trusted users list
647 const trustedUsersList = document.createElement("div");
648 trustedUsersList.id = "trustedUsersList";
649 trustedUsersList.style.cssText = `
650 margin-top: 15px;
651 border-top: 1px solid #eee;
652 padding-top: 15px;
653 `;
654
655 // Create cache control buttons
656 const cacheControls = document.createElement("div");
657 cacheControls.style.cssText = `
658 margin-top: 15px;
659 padding-top: 15px;
660 border-top: 1px solid #eee;
661 `;
662
663 const clearCacheButton = document.createElement("button");
664 clearCacheButton.textContent = "Clear Verification Cache";
665 clearCacheButton.style.cssText = `
666 padding: 8px 15px;
667 background-color: #735A5A;
668 color: white;
669 border: none;
670 border-radius: 4px;
671 cursor: pointer;
672 margin-right: 10px;
673 `;
674 clearCacheButton.addEventListener("click", () => {
675 clearCache();
676 alert(
677 "Verification cache cleared. Fresh data will be fetched on next check.",
678 );
679 });
680
681 cacheControls.appendChild(clearCacheButton);
682
683 // Create close button
684 const closeButton = document.createElement("button");
685 closeButton.textContent = "Close";
686 closeButton.style.cssText = `
687 margin-top: 20px;
688 padding: 8px 15px;
689 background-color: #473A3A;
690 border: none;
691 border-radius: 4px;
692 cursor: pointer;
693 `;
694
695 // Assemble modal
696 modalContent.appendChild(modalHeader);
697 modalContent.appendChild(form);
698 modalContent.appendChild(trustedUsersList);
699 modalContent.appendChild(cacheControls);
700 modalContent.appendChild(closeButton);
701 settingsModal.appendChild(modalContent);
702
703 // Add to document
704 document.body.appendChild(settingsModal);
705
706 const userInput = document.getElementById("trustedUserInput");
707
708 // Add input event for autocomplete
709 let debounceTimeout;
710 userInput.addEventListener("input", (e) => {
711 clearTimeout(debounceTimeout);
712 debounceTimeout = setTimeout(async () => {
713 const searchQuery = e.target.value.trim();
714 if (searchQuery.length >= 2) {
715 const results = await searchUsers(searchQuery);
716 showAutocompleteResults(results, userInput);
717 } else {
718 const dropdown = document.getElementById("autocomplete-dropdown");
719 if (dropdown) dropdown.remove();
720 }
721 }, 300); // Debounce for 300ms
722 });
723
724 // Event listeners
725 closeButton.addEventListener("click", () => {
726 settingsModal.style.display = "none";
727 });
728
729 // Function to add a user from the input field
730 const addUserFromInput = () => {
731 const input = document.getElementById("trustedUserInput");
732 const handle = input.value.trim();
733 if (handle) {
734 addTrustedUser(handle);
735 input.value = "";
736 updateTrustedUsersList();
737
738 // Remove dropdown if present
739 const dropdown = document.getElementById("autocomplete-dropdown");
740 if (dropdown) dropdown.remove();
741 }
742 };
743
744 // Add trusted user button event
745 document
746 .getElementById("addTrustedUserBtn")
747 .addEventListener("click", addUserFromInput);
748
749 // Add keydown event to input for Enter key
750 userInput.addEventListener("keydown", (e) => {
751 if (e.key === "Enter") {
752 e.preventDefault();
753 addUserFromInput();
754 }
755 });
756
757 // Close modal when clicking outside
758 settingsModal.addEventListener("click", (e) => {
759 if (e.target === settingsModal) {
760 settingsModal.style.display = "none";
761 }
762 });
763
764 // Initialize the list
765 updateTrustedUsersList();
766 };
767
768 // Function to create the settings UI if it doesn't exist yet
769 const createSettingsUI = () => {
770 // Create pill with buttons
771 createPillButtons();
772
773 // Create the settings modal if it doesn't exist yet
774 if (!settingsModal) {
775 createSettingsModal();
776 }
777 };
778
779 // Function to check the current profile
780 const checkCurrentProfile = () => {
781 const currentUrl = window.location.href;
782 // Only trigger on profile pages
783 if (
784 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) ||
785 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) ||
786 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/)
787 ) {
788 const handle = currentUrl.split("/profile/")[1].split("/")[0];
789 console.log("Detected profile page for:", handle);
790
791 // Create and add the settings UI (only once)
792 createSettingsUI();
793
794 // Fetch user profile data
795 fetch(
796 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
797 )
798 .then((response) => response.json())
799 .then((data) => {
800 console.log("User profile data:", data);
801
802 // Extract the DID from the profile data
803 const did = data.uri.split("/")[2];
804 console.log("User DID:", did);
805
806 // Check if any trusted users have verified this profile using the DID
807 checkTrustedUserVerifications(did);
808 })
809 .catch((error) => {
810 console.error("Error checking profile:", error);
811 });
812
813 console.log("Bluesky profile detected");
814 } else {
815 // Not on a profile page, reset state
816 currentProfileDid = null;
817 profileVerifiers = [];
818
819 // Remove UI elements if present
820 const existingBadge = document.getElementById(
821 "user-trusted-verification-badge",
822 );
823 if (existingBadge) {
824 existingBadge.remove();
825 }
826
827 const existingPill = document.getElementById(
828 "trusted-users-pill-container",
829 );
830 if (existingPill) {
831 existingPill.remove();
832 }
833 }
834 };
835
836 // Initial check
837 checkCurrentProfile();
838
839 const checkUserLinksOnPage = async () => {
840 // Look for profile links with handles
841 // Find all profile links and filter to get only one link per parent
842 const allProfileLinks = Array.from(
843 document.querySelectorAll('a[href^="/profile/"]:not(:has(*))'),
844 );
845
846 // Use a Map to keep track of parent elements and their first child link
847 const parentMap = new Map();
848
849 // For each link, store only the first one found for each parent
850 for (const link of allProfileLinks) {
851 const parent = link.parentElement;
852 if (parent && !parentMap.has(parent)) {
853 parentMap.set(parent, link);
854 }
855 }
856
857 // Get only the first link for each parent
858 const profileLinks = Array.from(parentMap.values());
859
860 if (profileLinks.length === 0) return;
861
862 console.log(`Found ${profileLinks.length} possible user links on page`);
863
864 // Process profile links to identify user containers
865 for (const link of profileLinks) {
866 try {
867 // Check if we already processed this link
868 if (link.getAttribute("data-verification-checked") === "true") continue;
869
870 // Mark as checked
871 link.setAttribute("data-verification-checked", "true");
872
873 // Extract handle from href
874 const handle = link.getAttribute("href").split("/profile/")[1];
875 if (!handle) continue;
876
877 // check if there is anything after the handle
878 const handleTrailing = handle.split("/").length > 1;
879 if (handleTrailing) continue;
880
881 // Find parent container that might contain the handle and verification icon
882 // Look for containers where this link is followed by another link with the same handle
883 const parent = link.parentElement;
884
885 // If we found a container with the verification icon
886 if (parent) {
887 // Check if this user already has our verification badge
888 if (parent.querySelector(".trusted-user-inline-badge")) continue;
889
890 try {
891 // Fetch user profile data to get DID
892 const response = await fetch(
893 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
894 );
895 const data = await response.json();
896
897 // Extract the DID from the profile data
898 const did = data.uri.split("/")[2];
899
900 // Check if this user is verified by our trusted users
901 const trustedUsers = getTrustedUsers();
902 let isVerified = false;
903 const verifiers = [];
904
905 // Check cache first for each trusted user
906 for (const trustedUser of trustedUsers) {
907 const cachedData = getCachedVerifications(trustedUser);
908
909 if (cachedData && isCacheValid(cachedData)) {
910 // Use cached verification data
911 const records = cachedData.records;
912
913 for (const record of records) {
914 if (record.value && record.value.subject === did) {
915 isVerified = true;
916 verifiers.push(trustedUser);
917 break;
918 }
919 }
920 }
921 }
922
923 // If verified, add a small badge
924 if (isVerified && verifiers.length > 0) {
925 // Create a badge element
926 const smallBadge = document.createElement("span");
927 smallBadge.className = "trusted-user-inline-badge";
928 smallBadge.innerHTML = "✓";
929
930 // Create tooltip text with all verifiers
931 const verifiersText =
932 verifiers.length > 1
933 ? `Verified by: ${verifiers.join(", ")}`
934 : `Verified by ${verifiers[0]}`;
935
936 smallBadge.title = verifiersText;
937 smallBadge.style.cssText = `
938 background-color: #0070ff;
939 color: white;
940 border-radius: 50%;
941 width: 14px;
942 height: 14px;
943 font-size: 10px;
944 font-weight: bold;
945 cursor: help;
946 display: inline-flex;
947 align-items: center;
948 justify-content: center;
949 margin-left: 4px;
950 `;
951
952 // Add click event to show verifiers
953 smallBadge.addEventListener("click", (e) => {
954 e.stopPropagation();
955 showVerifiersPopup(verifiers);
956 });
957
958 // Insert badge after the SVG element
959 parent.firstChild.after(smallBadge);
960 parent.style.flexDirection = "row";
961 parent.style.alignItems = "center";
962 }
963 } catch (error) {
964 console.error(`Error checking verification for ${handle}:`, error);
965 }
966 }
967 } catch (error) {
968 console.error("Error processing profile link:", error);
969 }
970 }
971 };
972
973 const observeContentChanges = () => {
974 // Use a debounced function to check for new user links
975 const debouncedCheck = () => {
976 clearTimeout(window.userLinksCheckTimeout);
977 window.userLinksCheckTimeout = setTimeout(() => {
978 checkUserLinksOnPage();
979 }, 300);
980 };
981
982 // Create a mutation observer that watches for DOM changes
983 const observer = new MutationObserver((mutations) => {
984 let hasRelevantChanges = false;
985
986 // Check if any mutations involve adding new nodes
987 for (const mutation of mutations) {
988 if (mutation.addedNodes.length > 0) {
989 for (const node of mutation.addedNodes) {
990 if (node.nodeType === Node.ELEMENT_NODE) {
991 // Check if this element or its children might contain profile links
992 if (
993 node.querySelector('a[href^="/profile/"]') ||
994 (node.tagName === "A" &&
995 node.getAttribute("href")?.startsWith("/profile/"))
996 ) {
997 hasRelevantChanges = true;
998 break;
999 }
1000 }
1001 }
1002 }
1003 if (hasRelevantChanges) break;
1004 }
1005
1006 if (hasRelevantChanges) {
1007 debouncedCheck();
1008 }
1009 });
1010
1011 // Observe the entire document for content changes that might include profile links
1012 observer.observe(document.body, { childList: true, subtree: true });
1013
1014 // Also check periodically for posts that might have been loaded but not caught by the observer
1015 setInterval(debouncedCheck, 5000);
1016 };
1017
1018 // Add these calls to the initialization section
1019 // Initial check for user links
1020 setTimeout(checkUserLinksOnPage, 1000); // Slight delay to ensure page has loaded
1021
1022 // Start observing for content changes to detect newly loaded posts
1023 observeContentChanges();
1024
1025 // Set up a MutationObserver to watch for URL changes
1026 const observeUrlChanges = () => {
1027 let lastUrl = location.href;
1028
1029 const observer = new MutationObserver(() => {
1030 if (location.href !== lastUrl) {
1031 const oldUrl = lastUrl;
1032 lastUrl = location.href;
1033 console.log("URL changed from:", oldUrl, "to:", location.href);
1034
1035 // Reset current profile DID
1036 currentProfileDid = null;
1037 profileVerifiers = [];
1038
1039 // Clean up UI elements
1040 const existingBadge = document.getElementById(
1041 "user-trusted-verification-badge",
1042 );
1043 if (existingBadge) {
1044 existingBadge.remove();
1045 }
1046
1047 const existingPill = document.getElementById(
1048 "trusted-users-pill-container",
1049 );
1050 if (existingPill) {
1051 existingPill.remove();
1052 }
1053
1054 // Check if we're on a profile page now
1055 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated
1056 }
1057 });
1058
1059 observer.observe(document, { subtree: true, childList: true });
1060 };
1061
1062 // Start observing for URL changes
1063 observeUrlChanges();
1064})();