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