Skip to content

Instantly share code, notes, and snippets.

@genzj
Last active July 22, 2025 22:59
Show Gist options
  • Save genzj/5ce3d035830591f360446bd3bbd7dc3e to your computer and use it in GitHub Desktop.
Save genzj/5ce3d035830591f360446bd3bbd7dc3e to your computer and use it in GitHub Desktop.
[Bsky.App] Replace the username and avatar with random pseudonyms.

为了保持点赞中立写了这个油猴脚本,将蓝天的用户名和头像替换为随机生成的,同时隐藏了用户ID,这样就能排除对用户印象或亲疏关系的干扰,仅根据内容进行点赞、转发和评论 😃

安装

效果预览

preview

功能

// ==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; }');
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment