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