Bluesky Content Manager Test

Customize your Bluesky feed by filtering and removing specific content

当前为 2024-11-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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() });
})();