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
476 // Function to create the settings modal
477 const createSettingsModal = () => {
478 // Create modal container
479 settingsModal = document.createElement("div");
480 settingsModal.id = "bsky-trusted-settings-modal";
481 settingsModal.style.cssText = `
482 display: none;
483 position: fixed;
484 top: 0;
485 left: 0;
486 width: 100%;
487 height: 100%;
488 background-color: rgba(0, 0, 0, 0.5);
489 z-index: 10001;
490 justify-content: center;
491 align-items: center;
492 `;
493
494 // Create modal content
495 const modalContent = document.createElement("div");
496 modalContent.style.cssText = `
497 background-color: #24273A;
498 padding: 20px;
499 border-radius: 10px;
500 width: 400px;
501 max-height: 80vh;
502 overflow-y: auto;
503 `;
504
505 // Create modal header
506 const modalHeader = document.createElement("div");
507 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`;
508
509 // Create input form
510 const form = document.createElement("div");
511 form.innerHTML = `
512 <p>Add Bluesky handles you trust:</p>
513 <div style="display: flex; margin-bottom: 15px;">
514 <input id="trustedUserInput" type="text" placeholder="username.bsky.social" style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;">
515 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button>
516 </div>
517 `;
518
519 // Create trusted users list
520 const trustedUsersList = document.createElement("div");
521 trustedUsersList.id = "trustedUsersList";
522 trustedUsersList.style.cssText = `
523 margin-top: 15px;
524 border-top: 1px solid #eee;
525 padding-top: 15px;
526 `;
527
528 // Create cache control buttons
529 const cacheControls = document.createElement("div");
530 cacheControls.style.cssText = `
531 margin-top: 15px;
532 padding-top: 15px;
533 border-top: 1px solid #eee;
534 `;
535
536 const clearCacheButton = document.createElement("button");
537 clearCacheButton.textContent = "Clear Verification Cache";
538 clearCacheButton.style.cssText = `
539 padding: 8px 15px;
540 background-color: #735A5A;
541 color: white;
542 border: none;
543 border-radius: 4px;
544 cursor: pointer;
545 margin-right: 10px;
546 `;
547 clearCacheButton.addEventListener("click", () => {
548 clearCache();
549 alert(
550 "Verification cache cleared. Fresh data will be fetched on next check.",
551 );
552 });
553
554 cacheControls.appendChild(clearCacheButton);
555
556 // Create close button
557 const closeButton = document.createElement("button");
558 closeButton.textContent = "Close";
559 closeButton.style.cssText = `
560 margin-top: 20px;
561 padding: 8px 15px;
562 background-color: #473A3A;
563 border: none;
564 border-radius: 4px;
565 cursor: pointer;
566 `;
567
568 // Assemble modal
569 modalContent.appendChild(modalHeader);
570 modalContent.appendChild(form);
571 modalContent.appendChild(trustedUsersList);
572 modalContent.appendChild(cacheControls);
573 modalContent.appendChild(closeButton);
574 settingsModal.appendChild(modalContent);
575
576 // Add to document
577 document.body.appendChild(settingsModal);
578
579 // Event listeners
580 closeButton.addEventListener("click", () => {
581 settingsModal.style.display = "none";
582 });
583
584 // Add trusted user button event
585 document
586 .getElementById("addTrustedUserBtn")
587 .addEventListener("click", () => {
588 const input = document.getElementById("trustedUserInput");
589 const handle = input.value.trim();
590 if (handle) {
591 addTrustedUser(handle);
592 input.value = "";
593 updateTrustedUsersList();
594 }
595 });
596
597 // Close modal when clicking outside
598 settingsModal.addEventListener("click", (e) => {
599 if (e.target === settingsModal) {
600 settingsModal.style.display = "none";
601 }
602 });
603
604 // Initialize the list
605 updateTrustedUsersList();
606 };
607
608 // Function to create the settings UI if it doesn't exist yet
609 const createSettingsUI = () => {
610 // Create pill with buttons
611 createPillButtons();
612
613 // Create the settings modal if it doesn't exist yet
614 if (!settingsModal) {
615 createSettingsModal();
616 }
617 };
618
619 // Function to check the current profile
620 const checkCurrentProfile = () => {
621 const currentUrl = window.location.href;
622 // Only trigger on profile pages
623 if (
624 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) ||
625 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) ||
626 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/)
627 ) {
628 const handle = currentUrl.split("/profile/")[1].split("/")[0];
629 console.log("Detected profile page for:", handle);
630
631 // Create and add the settings UI (only once)
632 createSettingsUI();
633
634 // Fetch user profile data
635 fetch(
636 `https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
637 )
638 .then((response) => response.json())
639 .then((data) => {
640 console.log("User profile data:", data);
641
642 // Extract the DID from the profile data
643 const did = data.uri.split("/")[2];
644 console.log("User DID:", did);
645
646 // Check if any trusted users have verified this profile using the DID
647 checkTrustedUserVerifications(did);
648 })
649 .catch((error) => {
650 console.error("Error checking profile:", error);
651 });
652
653 console.log("Bluesky profile detected");
654 } else {
655 // Not on a profile page, reset state
656 currentProfileDid = null;
657 profileVerifiers = [];
658
659 // Remove UI elements if present
660 const existingBadge = document.getElementById(
661 "user-trusted-verification-badge",
662 );
663 if (existingBadge) {
664 existingBadge.remove();
665 }
666
667 const existingPill = document.getElementById(
668 "trusted-users-pill-container",
669 );
670 if (existingPill) {
671 existingPill.remove();
672 }
673 }
674 };
675
676 // Initial check
677 checkCurrentProfile();
678
679 // Set up a MutationObserver to watch for URL changes
680 const observeUrlChanges = () => {
681 let lastUrl = location.href;
682
683 const observer = new MutationObserver(() => {
684 if (location.href !== lastUrl) {
685 const oldUrl = lastUrl;
686 lastUrl = location.href;
687 console.log("URL changed from:", oldUrl, "to:", location.href);
688
689 // Reset current profile DID
690 currentProfileDid = null;
691 profileVerifiers = [];
692
693 // Clean up UI elements
694 const existingBadge = document.getElementById(
695 "user-trusted-verification-badge",
696 );
697 if (existingBadge) {
698 existingBadge.remove();
699 }
700
701 const existingPill = document.getElementById(
702 "trusted-users-pill-container",
703 );
704 if (existingPill) {
705 existingPill.remove();
706 }
707
708 // Check if we're on a profile page now
709 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated
710 }
711 });
712
713 observer.observe(document, { subtree: true, childList: true });
714 };
715
716 // Start observing for URL changes
717 observeUrlChanges();
718})();