Customize your Bluesky feed by filtering and removing specific content
当前为
// ==UserScript==
// @name Bluesky Content Manager
// @namespace https://greasyfork.org/en/users/567951-stuart-saddler
// @version 1.0
// @description Customize your Bluesky feed by filtering and removing specific content
// @license MIT
// @icon https://images.seeklogo.com/logo-png/52/2/bluesky-logo-png_seeklogo-520643.png
// @match https://bsky.app/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @connect bsky.social
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
#filter-config {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 10000;
display: none;
color: #000000;
}
#filter-config h3,
#filter-config p {
color: #000000;
}
#filter-config textarea {
width: 300px;
height: 150px;
margin: 10px 0;
}
#filter-config button {
margin: 5px;
padding: 5px 10px;
}
`);
const filteredTerms = JSON.parse(GM_getValue('filteredTerms', '[]'));
const processedPosts = new WeakSet();
let sessionToken = null;
const profileCache = new Map();
let blockedCount = 0;
let menuCommandId = null;
function updateMenuCommand() {
if (menuCommandId) {
GM_unregisterMenuCommand(menuCommandId);
}
menuCommandId = GM_registerMenuCommand(`Configure blocklist (${blockedCount} blocked)`, () => {
document.getElementById('filter-config').style.display = 'block';
});
}
function createConfigUI() {
const div = document.createElement('div');
div.id = 'filter-config';
div.innerHTML = `
<h3>Configure Filter Terms</h3>
<p>Enter one term per line:</p>
<textarea id="filter-terms">${filteredTerms.join('\n')}</textarea>
<br>
<button id="save-filters">Save & Reload</button>
<button id="cancel-filters">Cancel</button>
`;
document.body.appendChild(div);
document.getElementById('save-filters').addEventListener('click', () => {
const newTerms = document.getElementById('filter-terms').value
.split('\n')
.map(t => t.trim())
.filter(t => t);
GM_setValue('filteredTerms', JSON.stringify(newTerms));
div.style.display = 'none';
window.location.reload();
});
document.getElementById('cancel-filters').addEventListener('click', () => {
div.style.display = 'none';
});
}
function debugLog(type, data = null) {
console.log(`🔍 [Profile Filter] ${type}:`, data || '');
}
function listStorage() {
debugLog('Listing localStorage');
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`localStorage[${key}]:`, value);
}
}
function waitForAuth() {
return new Promise((resolve, reject) => {
const maxAttempts = 30;
let attempts = 0;
const checkAuth = () => {
attempts++;
let session = localStorage.getItem('BSKY_STORAGE');
if (session) {
try {
const parsed = JSON.parse(session);
if (parsed.session?.accounts?.[0]?.accessJwt) {
sessionToken = parsed.session.accounts[0].accessJwt;
debugLog('Auth Success', 'Token retrieved');
resolve(true);
return;
}
} catch (e) {
debugLog('Auth Error', e);
}
}
if (attempts === 1) {
listStorage();
}
if (attempts >= maxAttempts) {
reject('Authentication timeout');
return;
}
setTimeout(checkAuth, 1000);
};
checkAuth();
});
}
async function fetchProfile(did) {
if (!sessionToken) {
debugLog('Fetch Profile Error', 'No session token available');
return null;
}
if (profileCache.has(did)) {
debugLog('Fetch Profile', 'Using cached profile', did);
return profileCache.get(did);
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://bsky.social/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`,
headers: {
'Authorization': `Bearer ${sessionToken}`,
'Accept': 'application/json'
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
debugLog('Profile Data', {
did: did,
description: data.description
});
profileCache.set(did, data);
resolve(data);
} catch (e) {
debugLog('Profile Parsing Error', e);
reject(e);
}
} else if (response.status === 401) {
debugLog('Auth Expired', 'Session token expired');
sessionToken = null;
reject('Auth expired');
} else {
debugLog('Profile Fetch Error', `HTTP ${response.status}`);
reject(`HTTP ${response.status}`);
}
},
onerror: function(error) {
debugLog('Fetch Profile Error', error);
reject(error);
}
});
});
}
async function processPost(post) {
if (processedPosts.has(post)) return;
processedPosts.add(post);
const authorLink = post.querySelector('a[href^="/profile/"]');
if (!authorLink) {
debugLog('Process Post', 'Author link not found');
return;
}
const didMatch = authorLink.href.match(/\/profile\/(.+)/);
if (!didMatch || !didMatch[1]) {
debugLog('Process Post', 'DID not found in URL');
return;
}
const did = decodeURIComponent(didMatch[1]);
if (!did) {
debugLog('Process Post', 'Empty DID');
return;
}
debugLog('Processing Post', { did });
// **Extracting Post Content**
let postText = '';
const postContentElement = post.querySelector('div[data-testid="postText"]');
if (postContentElement) {
postText = postContentElement.innerText.toLowerCase();
debugLog('Post Content', postText);
} else {
debugLog('Process Post', 'Post content not found');
}
// **Checking Post Content for Filtered Terms**
const textContainsFilteredTerm = filteredTerms.some(term =>
postText.includes(term.toLowerCase())
);
// **If Post Content Contains Filtered Term, Remove the Post**
if (textContainsFilteredTerm) {
const postContainer = authorLink.closest('div[role="link"]').closest('div.css-175oi2r');
if (postContainer) {
postContainer.remove();
blockedCount++;
updateMenuCommand();
debugLog('Filtered Post by Text', {
did: did,
postText: postText
});
return; // Exit the function since the post is removed
} else {
debugLog('Process Post', 'Post container not found when removing by text');
}
}
// **Proceed to Check Profile Description**
try {
const profile = await fetchProfile(did);
if (profile?.description) {
const descriptionLower = profile.description.toLowerCase();
const shouldHide = filteredTerms.some(term =>
descriptionLower.includes(term.toLowerCase())
);
if (shouldHide) {
const postContainer = authorLink.closest('div[role="link"]').closest('div.css-175oi2r');
if (postContainer) {
postContainer.remove();
blockedCount++;
updateMenuCommand();
debugLog('Filtered Post by Profile', {
did: did,
description: profile.description
});
} else {
debugLog('Process Post', 'Post container not found when removing by profile');
}
}
}
} catch (error) {
if (error === 'Auth expired') {
debugLog('Auth Expired', 'Attempting to re-authenticate');
try {
await waitForAuth();
await processPost(post);
} catch (authError) {
debugLog('Re-authentication Failed', authError);
}
} else {
debugLog('Process Error', error);
}
}
}
function observePosts() {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
const addedNodes = Array.from(mutation.addedNodes).filter(node =>
node.nodeType === Node.ELEMENT_NODE
);
if (addedNodes.length > 0) {
debugLog('Observer', `Detected ${addedNodes.length} added node(s)`);
}
addedNodes.forEach(node => {
const authorLinks = node.querySelectorAll('a[href^="/profile/"]');
if (authorLinks.length > 0) {
debugLog('Observer', `Detected ${authorLinks.length} new post(s)`);
authorLinks.forEach(authorLink => {
const container = authorLink.closest('div[role="link"]').closest('div.css-175oi2r');
if (container) {
processPost(container);
} else {
debugLog('Observer', 'Post container not found for a new post');
}
});
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
debugLog('Observer Started', '');
}
waitForAuth().then(() => {
observePosts();
}).catch((err) => {
debugLog('Initialization Error', err);
});
document.addEventListener('DOMContentLoaded', () => {
const authorLinks = document.querySelectorAll('a[href^="/profile/"]');
debugLog('DOMContentLoaded', `Found ${authorLinks.length} initial post(s)`);
authorLinks.forEach(authorLink => {
const container = authorLink.closest('div[role="link"]').closest('div.css-175oi2r');
if (container) {
processPost(container);
} else {
debugLog('DOMContentLoaded', 'Post container not found for an initial post');
}
});
});
createConfigUI();
updateMenuCommand();
debugLog('Script Loaded', {
filteredTerms,
timestamp: new Date().toISOString()
});
})();