+420
bluesky-community-verifications.js
+420
bluesky-community-verifications.js
···
1
+
(() => {
2
+
// Define a storage key for trusted users
3
+
const TRUSTED_USERS_STORAGE_KEY = "bsky_trusted_users";
4
+
5
+
// Function to get trusted users from local storage
6
+
const getTrustedUsers = () => {
7
+
const storedUsers = localStorage.getItem(TRUSTED_USERS_STORAGE_KEY);
8
+
return storedUsers ? JSON.parse(storedUsers) : [];
9
+
};
10
+
11
+
// Function to save trusted users to local storage
12
+
const saveTrustedUsers = (users) => {
13
+
localStorage.setItem(TRUSTED_USERS_STORAGE_KEY, JSON.stringify(users));
14
+
};
15
+
16
+
// Function to add a trusted user
17
+
const addTrustedUser = (handle) => {
18
+
const users = getTrustedUsers();
19
+
if (!users.includes(handle)) {
20
+
users.push(handle);
21
+
saveTrustedUsers(users);
22
+
}
23
+
};
24
+
25
+
// Function to remove a trusted user
26
+
const removeTrustedUser = (handle) => {
27
+
const users = getTrustedUsers();
28
+
const updatedUsers = users.filter((user) => user !== handle);
29
+
saveTrustedUsers(updatedUsers);
30
+
};
31
+
32
+
// Store all verifiers for a profile
33
+
let profileVerifiers = [];
34
+
35
+
// Function to check if a trusted user has verified the current profile
36
+
const checkTrustedUserVerifications = async (currentProfileDid) => {
37
+
const trustedUsers = getTrustedUsers();
38
+
profileVerifiers = []; // Reset the verifiers list
39
+
40
+
if (trustedUsers.length === 0) {
41
+
console.log("No trusted users to check for verifications");
42
+
return false;
43
+
}
44
+
45
+
console.log(
46
+
`Checking if any trusted users have verified ${currentProfileDid}`,
47
+
);
48
+
49
+
let isVerifiedByTrustedUser = false;
50
+
51
+
for (const trustedUser of trustedUsers) {
52
+
try {
53
+
const response = await fetch(
54
+
`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${trustedUser}&collection=app.bsky.graph.verification`,
55
+
);
56
+
const data = await response.json();
57
+
58
+
if (data.records && data.records.length > 0) {
59
+
for (const record of data.records) {
60
+
if (record.value && record.value.subject === currentProfileDid) {
61
+
console.log(
62
+
`${currentProfileDid} is verified by trusted user ${trustedUser}`,
63
+
);
64
+
65
+
// Add to verifiers list
66
+
profileVerifiers.push(trustedUser);
67
+
isVerifiedByTrustedUser = true;
68
+
}
69
+
}
70
+
}
71
+
} catch (error) {
72
+
console.error(
73
+
`Error checking verifications from ${trustedUser}:`,
74
+
error,
75
+
);
76
+
}
77
+
}
78
+
79
+
// If we have verifiers, display the badge
80
+
if (profileVerifiers.length > 0) {
81
+
displayVerificationBadge(profileVerifiers);
82
+
return true;
83
+
}
84
+
85
+
console.log(`${currentProfileDid} is not verified by any trusted users`);
86
+
return false;
87
+
};
88
+
89
+
// Function to display verification badge on the profile
90
+
const displayVerificationBadge = (verifierHandles) => {
91
+
// Find the profile header or name element to add the badge to
92
+
const nameElement = document.querySelector(
93
+
'[data-testid="profileHeaderDisplayName"]',
94
+
);
95
+
96
+
if (nameElement) {
97
+
// Check if badge already exists
98
+
if (!document.getElementById("user-trusted-verification-badge")) {
99
+
const badge = document.createElement("span");
100
+
badge.id = "user-trusted-verification-badge";
101
+
badge.innerHTML = "✓";
102
+
103
+
// Create tooltip text with all verifiers
104
+
const verifiersText =
105
+
verifierHandles.length > 1
106
+
? `Verified by: ${verifierHandles.join(", ")}`
107
+
: `Verified by ${verifierHandles[0]}`;
108
+
109
+
badge.title = verifiersText;
110
+
badge.style.cssText = `
111
+
background-color: #0070ff;
112
+
color: white;
113
+
border-radius: 50%;
114
+
width: 18px;
115
+
height: 18px;
116
+
margin-left: 8px;
117
+
font-size: 12px;
118
+
font-weight: bold;
119
+
cursor: help;
120
+
display: inline-flex;
121
+
align-items: center;
122
+
justify-content: center;
123
+
`;
124
+
125
+
// Add a click event to show all verifiers
126
+
badge.addEventListener("click", (e) => {
127
+
e.stopPropagation();
128
+
showVerifiersPopup(verifierHandles);
129
+
});
130
+
131
+
nameElement.appendChild(badge);
132
+
}
133
+
}
134
+
};
135
+
136
+
// Function to show a popup with all verifiers
137
+
const showVerifiersPopup = (verifierHandles) => {
138
+
// Remove existing popup if any
139
+
const existingPopup = document.getElementById("verifiers-popup");
140
+
if (existingPopup) {
141
+
existingPopup.remove();
142
+
}
143
+
144
+
// Create popup
145
+
const popup = document.createElement("div");
146
+
popup.id = "verifiers-popup";
147
+
popup.style.cssText = `
148
+
position: fixed;
149
+
top: 50%;
150
+
left: 50%;
151
+
transform: translate(-50%, -50%);
152
+
background-color: #24273A;
153
+
padding: 20px;
154
+
border-radius: 10px;
155
+
z-index: 10002;
156
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
157
+
max-width: 400px;
158
+
width: 90%;
159
+
`;
160
+
161
+
// Create popup content
162
+
popup.innerHTML = `
163
+
<h3 style="margin-top: 0; color: white;">Profile Verifiers</h3>
164
+
<div style="max-height: 300px; overflow-y: auto;">
165
+
${verifierHandles
166
+
.map(
167
+
(handle) => `
168
+
<div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;">
169
+
${handle}
170
+
</div>
171
+
`,
172
+
)
173
+
.join("")}
174
+
</div>
175
+
<button id="close-verifiers-popup" style="
176
+
margin-top: 15px;
177
+
padding: 8px 15px;
178
+
background-color: #473A3A;
179
+
color: white;
180
+
border: none;
181
+
border-radius: 4px;
182
+
cursor: pointer;
183
+
">Close</button>
184
+
`;
185
+
186
+
// Add to body
187
+
document.body.appendChild(popup);
188
+
189
+
// Add close handler
190
+
document
191
+
.getElementById("close-verifiers-popup")
192
+
.addEventListener("click", () => {
193
+
popup.remove();
194
+
});
195
+
196
+
// Close when clicking outside
197
+
document.addEventListener("click", function closePopup(e) {
198
+
if (!popup.contains(e.target)) {
199
+
popup.remove();
200
+
document.removeEventListener("click", closePopup);
201
+
}
202
+
});
203
+
};
204
+
205
+
// Create settings button and modal
206
+
const createSettingsUI = () => {
207
+
// Create settings button
208
+
const settingsButton = document.createElement("button");
209
+
settingsButton.textContent = "Trusted Users Settings";
210
+
settingsButton.style.cssText = `
211
+
position: fixed;
212
+
bottom: 20px;
213
+
right: 20px;
214
+
z-index: 10000;
215
+
padding: 10px 15px;
216
+
background-color: #2D578D;
217
+
color: white;
218
+
border: none;
219
+
border-radius: 20px;
220
+
cursor: pointer;
221
+
font-weight: bold;
222
+
`;
223
+
224
+
// Create modal container
225
+
const modal = document.createElement("div");
226
+
modal.style.cssText = `
227
+
display: none;
228
+
position: fixed;
229
+
top: 0;
230
+
left: 0;
231
+
width: 100%;
232
+
height: 100%;
233
+
background-color: rgba(0, 0, 0, 0.5);
234
+
z-index: 10001;
235
+
justify-content: center;
236
+
align-items: center;
237
+
`;
238
+
239
+
// Create modal content
240
+
const modalContent = document.createElement("div");
241
+
modalContent.style.cssText = `
242
+
background-color: #24273A;
243
+
padding: 20px;
244
+
border-radius: 10px;
245
+
width: 400px;
246
+
max-height: 80vh;
247
+
overflow-y: auto;
248
+
`;
249
+
250
+
// Create modal header
251
+
const modalHeader = document.createElement("div");
252
+
modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`;
253
+
254
+
// Create input form
255
+
const form = document.createElement("div");
256
+
form.innerHTML = `
257
+
<p>Add Bluesky handles you trust:</p>
258
+
<div style="display: flex; margin-bottom: 15px;">
259
+
<input id="trustedUserInput" type="text" placeholder="username.bsky.social" style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;">
260
+
<button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button>
261
+
</div>
262
+
`;
263
+
264
+
// Create trusted users list
265
+
const trustedUsersList = document.createElement("div");
266
+
trustedUsersList.id = "trustedUsersList";
267
+
trustedUsersList.style.cssText = `
268
+
margin-top: 15px;
269
+
border-top: 1px solid #eee;
270
+
padding-top: 15px;
271
+
`;
272
+
273
+
// Create close button
274
+
const closeButton = document.createElement("button");
275
+
closeButton.textContent = "Close";
276
+
closeButton.style.cssText = `
277
+
margin-top: 20px;
278
+
padding: 8px 15px;
279
+
background-color: #473A3A;
280
+
border: none;
281
+
border-radius: 4px;
282
+
cursor: pointer;
283
+
`;
284
+
285
+
// Assemble modal
286
+
modalContent.appendChild(modalHeader);
287
+
modalContent.appendChild(form);
288
+
modalContent.appendChild(trustedUsersList);
289
+
modalContent.appendChild(closeButton);
290
+
modal.appendChild(modalContent);
291
+
292
+
// Add elements to the document
293
+
document.body.appendChild(settingsButton);
294
+
document.body.appendChild(modal);
295
+
296
+
// Function to update the list of trusted users in the UI
297
+
const updateTrustedUsersList = () => {
298
+
const users = getTrustedUsers();
299
+
trustedUsersList.innerHTML = "";
300
+
301
+
if (users.length === 0) {
302
+
trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>";
303
+
return;
304
+
}
305
+
306
+
for (const user of users) {
307
+
const userItem = document.createElement("div");
308
+
userItem.style.cssText = `
309
+
display: flex;
310
+
justify-content: space-between;
311
+
align-items: center;
312
+
padding: 8px 0;
313
+
border-bottom: 1px solid #eee;
314
+
`;
315
+
316
+
userItem.innerHTML = `
317
+
<span>${user}</span>
318
+
<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>
319
+
`;
320
+
321
+
trustedUsersList.appendChild(userItem);
322
+
}
323
+
324
+
// Add event listeners to remove buttons
325
+
const removeButtons = document.querySelectorAll(".remove-user");
326
+
for (const btn of removeButtons) {
327
+
btn.addEventListener("click", (e) => {
328
+
const handle = e.target.getAttribute("data-handle");
329
+
removeTrustedUser(handle);
330
+
updateTrustedUsersList();
331
+
});
332
+
}
333
+
};
334
+
335
+
// Event listeners
336
+
settingsButton.addEventListener("click", () => {
337
+
modal.style.display = "flex";
338
+
updateTrustedUsersList();
339
+
});
340
+
341
+
closeButton.addEventListener("click", () => {
342
+
modal.style.display = "none";
343
+
});
344
+
345
+
document
346
+
.getElementById("addTrustedUserBtn")
347
+
.addEventListener("click", () => {
348
+
const input = document.getElementById("trustedUserInput");
349
+
const handle = input.value.trim();
350
+
if (handle) {
351
+
addTrustedUser(handle);
352
+
input.value = "";
353
+
updateTrustedUsersList();
354
+
}
355
+
});
356
+
357
+
// Close modal when clicking outside
358
+
modal.addEventListener("click", (e) => {
359
+
if (e.target === modal) {
360
+
modal.style.display = "none";
361
+
}
362
+
});
363
+
};
364
+
365
+
const currentUrl = window.location.href;
366
+
if (currentUrl.includes("bsky.app/profile/")) {
367
+
const handle = currentUrl.split("/profile/")[1].split("/")[0];
368
+
console.log("Extracted handle:", handle);
369
+
370
+
// Create and add the settings UI
371
+
createSettingsUI();
372
+
373
+
// Fetch user profile data
374
+
fetch(
375
+
`https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
376
+
)
377
+
.then((response) => response.json())
378
+
.then((data) => {
379
+
console.log("User profile data:", data);
380
+
381
+
// Extract the DID from the profile data
382
+
const did = data.uri.split("/")[2];
383
+
console.log("User DID:", did);
384
+
385
+
// Now fetch the app.bsky.graph.verification data specifically
386
+
fetch(
387
+
`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=app.bsky.graph.verification`,
388
+
)
389
+
.then((response) => response.json())
390
+
.then((verificationData) => {
391
+
console.log("Verification data:", verificationData);
392
+
if (
393
+
verificationData.records &&
394
+
verificationData.records.length > 0
395
+
) {
396
+
console.log(
397
+
"User has app.bsky.graph.verification:",
398
+
verificationData.records,
399
+
);
400
+
} else {
401
+
console.log("User does not have app.bsky.graph.verification");
402
+
}
403
+
404
+
// Check if any trusted users have verified this profile using the DID
405
+
checkTrustedUserVerifications(did);
406
+
})
407
+
.catch((verificationError) => {
408
+
console.error(
409
+
"Error fetching verification data:",
410
+
verificationError,
411
+
);
412
+
});
413
+
})
414
+
.catch((error) => {
415
+
console.error("Error checking profile:", error);
416
+
});
417
+
418
+
console.log("Example domain detected");
419
+
}
420
+
})();
+27
bun.lock
+27
bun.lock
···
3
3
"workspaces": {
4
4
"": {
5
5
"name": "bunplayground",
6
+
"dependencies": {
7
+
"terser": "^5.39.0",
8
+
},
6
9
"devDependencies": {
7
10
"@types/bun": "latest",
8
11
},
···
12
15
},
13
16
},
14
17
"packages": {
18
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
19
+
20
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
21
+
22
+
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
23
+
24
+
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="],
25
+
26
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
27
+
28
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
29
+
15
30
"@types/bun": ["@types/bun@1.2.6", "", { "dependencies": { "bun-types": "1.2.6" } }, "sha512-fY9CAmTdJH1Llx7rugB0FpgWK2RKuHCs3g2cFDYXUutIy1QGiPQxKkGY8owhfZ4MXWNfxwIbQLChgH5gDsY7vw=="],
16
31
17
32
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
18
33
19
34
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
20
35
36
+
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
37
+
38
+
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
39
+
21
40
"bun-types": ["bun-types@1.2.6", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-FbCKyr5KDiPULUzN/nm5oqQs9nXCHD8dVc64BArxJadCvbNzAI6lUWGh9fSJZWeDIRD38ikceBU8Kj/Uh+53oQ=="],
41
+
42
+
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
43
+
44
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
45
+
46
+
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
47
+
48
+
"terser": ["terser@5.39.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw=="],
22
49
23
50
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
24
51
+43
package-bookmarklet.ts
+43
package-bookmarklet.ts
···
1
+
#!/usr/bin/env bun
2
+
3
+
import { readFileSync } from "node:fs";
4
+
import { resolve } from "node:path";
5
+
import { minify as terserMinify } from "terser";
6
+
7
+
const main = async () => {
8
+
// Get the file path from command line arguments
9
+
const filePath = process.argv[2];
10
+
11
+
if (!filePath) {
12
+
console.error("Error: Please provide a file path as an argument");
13
+
process.exit(1);
14
+
}
15
+
16
+
try {
17
+
// Read the JavaScript file
18
+
const fullPath = resolve(process.cwd(), filePath);
19
+
const fileContent = readFileSync(fullPath, "utf8");
20
+
21
+
// Minify the JavaScript using terser
22
+
const result = await terserMinify(fileContent, {
23
+
mangle: true,
24
+
compress: true,
25
+
});
26
+
27
+
if (!result.code) {
28
+
throw new Error("Minification failed");
29
+
}
30
+
31
+
const minified = result.code;
32
+
33
+
// Create the bookmarklet by prefixing with "javascript:"
34
+
const bookmarklet = `javascript:${encodeURIComponent(minified)}`;
35
+
36
+
console.log(bookmarklet);
37
+
} catch (error) {
38
+
console.error(`Error: ${(error as Error).message}`);
39
+
process.exit(1);
40
+
}
41
+
};
42
+
43
+
main();