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 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 // Legacy function kept for compatibility but redirects to the new implementation
294 const addRecheckButton = () => {
295 createPillButtons();
296 };
297
298 // Function to display verification badge on the profile
299 const displayVerificationBadge = (verifierHandles) => {
300 // Find the profile header or name element to add the badge to
301 const nameElement = document.querySelector(
302 '[data-testid="profileHeaderDisplayName"]',
303 );
304
305 if (nameElement) {
306 // Check if badge already exists
307 if (!document.getElementById("user-trusted-verification-badge")) {
308 const badge = document.createElement("span");
309 badge.id = "user-trusted-verification-badge";
310 badge.innerHTML = "✓";
311
312 // Create tooltip text with all verifiers
313 const verifiersText =
314 verifierHandles.length > 1
315 ? `Verified by: ${verifierHandles.join(", ")}`
316 : `Verified by ${verifierHandles[0]}`;
317
318 badge.title = verifiersText;
319 badge.style.cssText = `
320 background-color: #0070ff;
321 color: white;
322 border-radius: 50%;
323 width: 18px;
324 height: 18px;
325 margin-left: 8px;
326 font-size: 12px;
327 font-weight: bold;
328 cursor: help;
329 display: inline-flex;
330 align-items: center;
331 justify-content: center;
332 `;
333
334 // Add a click event to show all verifiers
335 badge.addEventListener("click", (e) => {
336 e.stopPropagation();
337 showVerifiersPopup(verifierHandles);
338 });
339
340 nameElement.appendChild(badge);
341 }
342 }
343
344 // Also add pill buttons when verification is found
345 createPillButtons();
346 };
347
348 // Function to show a popup with all verifiers
349 const showVerifiersPopup = (verifierHandles) => {
350 // Remove existing popup if any
351 const existingPopup = document.getElementById("verifiers-popup");
352 if (existingPopup) {
353 existingPopup.remove();
354 }
355
356 // Create popup
357 const popup = document.createElement("div");
358 popup.id = "verifiers-popup";
359 popup.style.cssText = `
360 position: fixed;
361 top: 50%;
362 left: 50%;
363 transform: translate(-50%, -50%);
364 background-color: #24273A;
365 padding: 20px;
366 border-radius: 10px;
367 z-index: 10002;
368 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
369 max-width: 400px;
370 width: 90%;
371 `;
372
373 // Create popup content
374 popup.innerHTML = `
375 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3>
376 <div style="max-height: 300px; overflow-y: auto;">
377 ${verifierHandles
378 .map(
379 (handle) => `
380 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;">
381 ${handle}
382 </div>
383 `,
384 )
385 .join("")}
386 </div>
387 <button id="close-verifiers-popup" style="
388 margin-top: 15px;
389 padding: 8px 15px;
390 background-color: #473A3A;
391 color: white;
392 border: none;
393 border-radius: 4px;
394 cursor: pointer;
395 ">Close</button>
396 `;
397
398 // Add to body
399 document.body.appendChild(popup);
400
401 // Add close handler
402 document
403 .getElementById("close-verifiers-popup")
404 .addEventListener("click", () => {
405 popup.remove();
406 });
407
408 // Close when clicking outside
409 document.addEventListener("click", function closePopup(e) {
410 if (!popup.contains(e.target)) {
411 popup.remove();
412 document.removeEventListener("click", closePopup);
413 }
414 });
415 };
416
417 // Create settings modal
418 let settingsModal = null;
419
420 // Function to update the list of trusted users in the UI
421 const updateTrustedUsersList = () => {
422 const trustedUsersList = document.getElementById("trustedUsersList");
423 if (!trustedUsersList) return;
424
425 const users = getTrustedUsers();
426 trustedUsersList.innerHTML = "";
427
428 if (users.length === 0) {
429 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>";
430 return;
431 }
432
433 for (const user of users) {
434 const userItem = document.createElement("div");
435 userItem.style.cssText = `
436 display: flex;
437 justify-content: space-between;
438 align-items: center;
439 padding: 8px 0;
440 border-bottom: 1px solid #eee;
441 `;
442
443 userItem.innerHTML = `
444 <span>${user}</span>
445 <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>
446 `;
447
448 trustedUsersList.appendChild(userItem);
449 }
450
451 // Add event listeners to remove buttons
452 const removeButtons = document.querySelectorAll(".remove-user");
453 for (const btn of removeButtons) {
454 btn.addEventListener("click", (e) => {
455 const handle = e.target.getAttribute("data-handle");
456 removeTrustedUser(handle);
457 updateTrustedUsersList();
458 });
459 }
460 };
461
462 // Function to create the settings modal
463 const createSettingsModal = () => {
464 // Create modal container
465 settingsModal = document.createElement("div");
466 settingsModal.id = "bsky-trusted-settings-modal";
467 settingsModal.style.cssText = `
468 display: none;
469 position: fixed;
470 top: 0;
471 left: 0;
472 width: 100%;
473 height: 100%;
474 background-color: rgba(0, 0, 0, 0.5);
475 z-index: 10001;
476 justify-content: center;
477 align-items: center;
478 `;
479
480 // Create modal content
481 const modalContent = document.createElement("div");
482 modalContent.style.cssText = `
483 background-color: #24273A;
484 padding: 20px;
485 border-radius: 10px;
486 width: 400px;
487 max-height: 80vh;
488 overflow-y: auto;
489 `;
490
491 // Create modal header
492 const modalHeader = document.createElement("div");
493 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`;
494
495 // Create input form
496 const form = document.createElement("div");
497 form.innerHTML = `
498 <p>Add Bluesky handles you trust:</p>
499 <div style="display: flex; margin-bottom: 15px;">
500 <input id="trustedUserInput" type="text" placeholder="username.bsky.social" style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;">
501 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button>
502 </div>
503 `;
504
505 // Create trusted users list
506 const trustedUsersList = document.createElement("div");
507 trustedUsersList.id = "trustedUsersList";
508 trustedUsersList.style.cssText = `
509 margin-top: 15px;
510 border-top: 1px solid #eee;
511 padding-top: 15px;
512 `;
513
514 // Create cache control buttons
515 const cacheControls = document.createElement("div");
516 cacheControls.style.cssText = `
517 margin-top: 15px;
518 padding-top: 15px;
519 border-top: 1px solid #eee;
520 `;
521
522 const clearCacheButton = document.createElement("button");
523 clearCacheButton.textContent = "Clear Verification Cache";
524 clearCacheButton.style.cssText = `
525 padding: 8px 15px;
526 background-color: #735A5A;
527 color: white;
528 border: none;
529 border-radius: 4px;
530 cursor: pointer;
531 margin-right: 10px;
532 `;
533 clearCacheButton.addEventListener("click", () => {
534 clearCache();
535 alert(
536 "Verification cache cleared. Fresh data will be fetched on next check.",
537 );
538 });
539
540 cacheControls.appendChild(clearCacheButton);
541
542 // Create close button
543 const closeButton = document.createElement("button");
544 closeButton.textContent = "Close";
545 closeButton.style.cssText = `
546 margin-top: 20px;
547 padding: 8px 15px;
548 background-color: #473A3A;
549 border: none;
550 border-radius: 4px;
551 cursor: pointer;
552 `;
553
554 // Assemble modal
555 modalContent.appendChild(modalHeader);
556 modalContent.appendChild(form);
557 modalContent.appendChild(trustedUsersList);
558 modalContent.appendChild(cacheControls);
559 modalContent.appendChild(closeButton);
560 settingsModal.appendChild(modalContent);
561
562 // Add to document
563 document.body.appendChild(settingsModal);
564
565 // Event listeners
566 closeButton.addEventListener("click", () => {
567 settingsModal.style.display = "none";
568 });
569
570 // Add trusted user button event
571 document
572 .getElementById("addTrustedUserBtn")
573 .addEventListener("click", () => {
574 const input = document.getElementById("trustedUserInput");
575 const handle = input.value.trim();
576 if (handle) {
577 addTrustedUser(handle);
578 input.value = "";
579 updateTrustedUsersList();
580 }
581 });
582
583 // Close modal when clicking outside
584 settingsModal.addEventListener("click", (e) => {
585 if (e.target === settingsModal) {
586 settingsModal.style.display = "none";
587 }
588 });
589
590 // Initialize the list
591 updateTrustedUsersList();
592 };
593
594 // Function to create the settings UI if it doesn't exist yet
595 const createSettingsUI = () => {
596 // Create pill with buttons
597 createPillButtons();
598
599 // Create the settings modal if it doesn't exist yet
600 if (!settingsModal) {
601 createSettingsModal();
602 }
603 };
604
605 // Function to check the current profile
606 const checkCurrentProfile = () => {
607 const currentUrl = window.location.href;
608 if (currentUrl.includes("bsky.app/profile/")) {
609 const handle = currentUrl.split("/profile/")[1].split("/")[0];
610 console.log("Extracted handle:", handle);
611
612 // Create and add the settings UI (only once)
613 createSettingsUI();
614
615 // Fetch user profile data
616 fetch(
617 `https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
618 )
619 .then((response) => response.json())
620 .then((data) => {
621 console.log("User profile data:", data);
622
623 // Extract the DID from the profile data
624 const did = data.uri.split("/")[2];
625 console.log("User DID:", did);
626
627 // Now fetch the app.bsky.graph.verification data specifically
628 fetch(
629 `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=app.bsky.graph.verification`,
630 )
631 .then((response) => response.json())
632 .then((verificationData) => {
633 console.log("Verification data:", verificationData);
634 if (
635 verificationData.records &&
636 verificationData.records.length > 0
637 ) {
638 console.log(
639 "User has app.bsky.graph.verification:",
640 verificationData.records,
641 );
642 } else {
643 console.log("User does not have app.bsky.graph.verification");
644 }
645
646 // Check if any trusted users have verified this profile using the DID
647 checkTrustedUserVerifications(did);
648 })
649 .catch((verificationError) => {
650 console.error(
651 "Error fetching verification data:",
652 verificationError,
653 );
654 });
655 })
656 .catch((error) => {
657 console.error("Error checking profile:", error);
658 });
659
660 console.log("Bluesky profile detected");
661 }
662 };
663
664 // Initial check
665 checkCurrentProfile();
666
667 // Set up a MutationObserver to watch for URL changes
668 const observeUrlChanges = () => {
669 let lastUrl = location.href;
670
671 const observer = new MutationObserver(() => {
672 if (location.href !== lastUrl) {
673 lastUrl = location.href;
674 console.log("URL changed to:", location.href);
675
676 // Remove any existing badges when URL changes
677 const existingBadge = document.getElementById(
678 "user-trusted-verification-badge",
679 );
680 if (existingBadge) {
681 existingBadge.remove();
682 }
683
684 // Remove the pill container when URL changes
685 const existingPill = document.getElementById(
686 "trusted-users-pill-container",
687 );
688 if (existingPill) {
689 existingPill.remove();
690 }
691
692 // Check if we're on a profile page now
693 checkCurrentProfile();
694 }
695 });
696
697 observer.observe(document, { subtree: true, childList: true });
698 };
699
700 // Start observing for URL changes
701 observeUrlChanges();
702})();