Customize your Bluesky feed by filtering and removing specific content
目前為
// ==UserScript==
// @name Bluesky Content Manager Test
// @namespace https://greasyfork.org/en/users/567951-stuart-saddler
// @version 1.3
// @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');
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);
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);
}
});
});
}
function removePost(post) {
if (!post) return false;
post.remove();
blockedCount++;
updateMenuCommand();
return true;
}
async function processPost(post) {
if (!post || processedPosts.has(post)) return;
processedPosts.add(post);
const authorLink = post.querySelector('a[href^="/profile/"]');
if (!authorLink) return;
// Check author's name first
const nameElement = authorLink.querySelector('span');
if (nameElement) {
const authorName = nameElement.textContent.toLowerCase();
const nameContainsFilteredTerm = filteredTerms.some(term =>
authorName.includes(term.toLowerCase())
);
if (nameContainsFilteredTerm) {
debugLog('Filtered by Name', authorName);
removePost(post);
return;
}
}
const didMatch = authorLink.href.match(/\/profile\/(.+)/);
if (!didMatch || !didMatch[1]) return;
const did = decodeURIComponent(didMatch[1]);
if (!did) return;
// Check post content
const postContentElement = post.querySelector('div[data-testid="postText"]');
if (postContentElement) {
const postText = postContentElement.textContent.toLowerCase();
const textContainsFilteredTerm = filteredTerms.some(term =>
postText.includes(term.toLowerCase())
);
if (textContainsFilteredTerm) {
debugLog('Filtered by Content', postText);
removePost(post);
return;
}
}
// Check profile
try {
const profile = await fetchProfile(did);
if (profile?.description || profile?.displayName) {
const descriptionLower = (profile.description || '').toLowerCase();
const displayNameLower = (profile.displayName || '').toLowerCase();
const shouldHide = filteredTerms.some(term => {
const termLower = term.toLowerCase();
return descriptionLower.includes(termLower) || displayNameLower.includes(termLower);
});
if (shouldHide) {
debugLog('Filtered by Profile', { description: profile.description, displayName: profile.displayName });
removePost(post);
}
}
} 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);
}
}
}
}
function observePosts() {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Process the node itself if it's a post
if (node.querySelector('a[href^="/profile/"]')) {
processPost(node);
}
// Process any posts within the node
const posts = node.querySelectorAll('div[role="article"]');
posts.forEach(post => processPost(post));
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
debugLog('Observer Started');
}
function initialScan() {
const posts = document.querySelectorAll('div[role="article"]');
posts.forEach(post => processPost(post));
debugLog('Initial Scan Complete', `Processed ${posts.length} posts`);
}
// Start the script
waitForAuth().then(() => {
initialScan();
observePosts();
}).catch((err) => {
debugLog('Initialization Error', err);
});
createConfigUI();
updateMenuCommand();
debugLog('Script Loaded', { filteredTerms, timestamp: new Date().toISOString() });
})();