|
// ==UserScript== |
|
// @name BlueSky Cloak |
|
// @namespace Violentmonkey Scripts |
|
// @match https://bsky.app/* |
|
// @grant none |
|
// @version 1.0 |
|
// @author genzj |
|
// @description 7/18/2025, 2:49:10 PM |
|
// @run-at document-start |
|
// ==/UserScript== |
|
|
|
|
|
(function() { |
|
'use strict'; |
|
|
|
let fakerLib = undefined; |
|
let nameMap = new Map(); |
|
|
|
// GM_addStyle cannot be used because adding it to @grant delays script loading, |
|
// making the patching of window.fetch ineffective. |
|
function addStyle(content) { |
|
const style = document.createElement('style'); |
|
style.type = 'text/css'; |
|
// Set your CSS rules here, as a string |
|
style.textContent = content; |
|
document.head.appendChild(style); |
|
return style; |
|
} |
|
|
|
async function enableFaker() { |
|
if (fakerLib) { |
|
return fakerLib; |
|
} |
|
|
|
const { faker } = await import('https://esm.sh/@faker-js/faker'); |
|
window.faker = faker; |
|
fakerLib = faker; |
|
return fakerLib; |
|
} |
|
|
|
function generateHash(s) { |
|
let hash = 0; |
|
for (const char of s) { |
|
hash = (hash << 5) - hash + char.charCodeAt(0); |
|
hash |= 0; // Constrain to 32bit integer |
|
} |
|
return hash.toString(); |
|
}; |
|
|
|
async function deepMap(obj, key, fn) { |
|
if (typeof obj !== 'object') { |
|
return obj; |
|
} |
|
|
|
if (Array.isArray(obj)) { |
|
return await Promise.all(obj.map(async ele => await deepMap(ele, key, fn))); |
|
} |
|
|
|
const mapped = {}; |
|
for (const k in obj) { |
|
mapped[k] = k === key ? await fn(obj[k]) : await deepMap(obj[k], key, fn); |
|
} |
|
return mapped; |
|
} |
|
|
|
async function patchAvatar(original) { |
|
console.debug(`patching avatar ${original}`); |
|
return `https://api.dicebear.com/9.x/big-smile/svg?seed=${generateHash(original)}`; |
|
} |
|
|
|
async function patchDisplayName(original) { |
|
if (nameMap.has(original)) { |
|
return nameMap.get(original); |
|
} |
|
|
|
const faker = await enableFaker(); |
|
nameMap.set(original, faker.internet.displayName()); |
|
return nameMap.get(original); |
|
} |
|
|
|
const OUT_OF_SCOPE_API = [ |
|
// login info |
|
'/com.atproto.server.getSession', |
|
|
|
// feed info and name |
|
'/app.bsky.feed.getFeedGenerators', |
|
|
|
// current user's profile |
|
'/app.bsky.actor.getProfiles', |
|
]; |
|
|
|
const originalFetch = window.fetch; |
|
window.fetch = async function(...args) { |
|
// Optionally inspect the arguments (URL, options) |
|
const response = await originalFetch.apply(this, args); |
|
|
|
// Clone and modify the response here |
|
const cloned = response.clone(); |
|
const {url} = cloned; |
|
console.debug(`fetched: ${url}`); |
|
|
|
if (!url.includes('/xrpc/')) { |
|
console.debug(`${url} isn't a xrpc, skip patching`); |
|
return response; |
|
} |
|
|
|
if (OUT_OF_SCOPE_API.some(api => url.includes(api))) { |
|
console.debug(`${url} isn't a candidate API, skip patching`); |
|
return response; |
|
} |
|
|
|
const text = await cloned.text(); |
|
|
|
// Example: Modify response text (say, inject extra data into JSON) |
|
let data; |
|
try { |
|
data = JSON.parse(text); |
|
data = await deepMap(data, 'avatar', patchAvatar); |
|
data = await deepMap(data, 'displayName', patchDisplayName); |
|
data.injected = true; // Modify the response |
|
console.debug(`patched data of ${url}: ${JSON.stringify(data)}`); |
|
} catch (e) { |
|
// Not JSON; return as-is |
|
console.warn(e); |
|
return response; |
|
} |
|
|
|
// Create and return a new response |
|
const modified = new Response( |
|
JSON.stringify(data), |
|
{ |
|
status: response.status, |
|
statusText: response.statusText, |
|
headers: response.headers |
|
} |
|
); |
|
return modified; |
|
}; |
|
|
|
setTimeout(() => { |
|
// handler in the timeline |
|
addStyle('a[aria-label="View profile"]:last-child {color: transparent !important; background: rgba(128, 128, 128, 0.5) !important; margin-left: 0.5em !important; border-radius: 5px !important;}'); |
|
addStyle('a[aria-label="View profile"]:last-child::before{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.4),transparent);transform:skewX(-20deg);animation: 5s linear infinite glareAnimation}@keyframes glareAnimation{0%,100%{left:-100%}50%{left:100%}}'); |
|
// handler in the name card overlay |
|
addStyle('main ~ div a[aria-label="View profile"] > div > div:last-child {display: none !important; }'); |
|
}); |
|
})(); |