old.reddit.com regex Post Filter (Block subreddits by regex, users, block titles by regex)

Filters posts on old.reddit.com by subreddit, user, or title regex. Persistently stores all rules and hides or colors posts with minimal CPU usage.

当前为 2025-11-23 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         old.reddit.com regex Post Filter (Block subreddits by regex, users, block titles by regex)
// @namespace    jjenkx
// @version      1.0
// @license      MIT
// @description  Filters posts on old.reddit.com by subreddit, user, or title regex. Persistently stores all rules and hides or colors posts with minimal CPU usage.
// @match        https://old.reddit.com/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';

    // The script uses color codes to highlight why a post matched a rule
    // when filters are disabled. When filters are enabled posts are hidden.
    const COLORS = {
        titleRegex: '#ff9999',
        blockedSubreddits: '#dcdcdc',
        subredditRegexList: '#ffd27f',
        blockedUsers: '#a6c8ff',
        default: ''
    };

    // Keys for persistent storage. All rule lists and state are saved through GM.setValue.
    const KEY_BLOCKED_SUBS = 'blockedSubreddits';
    const KEY_BLOCKED_USERS = 'blockedUsers';
    const KEY_SUB_REGEX_LIST = 'subredditRegexList';
    const KEY_TITLE_REGEX_LIST = 'titleRegexList';
    const KEY_FILTERS_ENABLED = 'filtersEnabled';

    // Working sets and arrays for rules. Regex lists are compiled at load for efficiency.
    let blockedSubreddits = new Set();
    let blockedUsers = new Set();
    let subredditRegexList = [];
    let titleRegexList = [];
    let compiledSubredditRegexList = [];
    let compiledTitleRegexList = [];
    let filtersEnabled = true;
    let hiddenPostCount = 0;

    // These caches store recent subreddit and user checks to avoid repeating work.
    const subCache = new Map();
    const userCache = new Map();

    let observer, postContainer;

    // Load all persistent values. Everything is read once at startup.
    const loadAll = async () => {
        const [subs, users, subRegexes, titleRegexes, enabled] = await Promise.all([
            GM.getValue(KEY_BLOCKED_SUBS, '[]'),
            GM.getValue(KEY_BLOCKED_USERS, '[]'),
            GM.getValue(KEY_SUB_REGEX_LIST, '[]'),
            GM.getValue(KEY_TITLE_REGEX_LIST, '[]'),
            GM.getValue(KEY_FILTERS_ENABLED, true)
        ]);

        blockedSubreddits = new Set(JSON.parse(subs));
        blockedUsers = new Set(JSON.parse(users));

        try { subredditRegexList = JSON.parse(subRegexes); } catch { subredditRegexList = []; }
        try { titleRegexList = JSON.parse(titleRegexes); } catch { titleRegexList = []; }

        compiledSubredditRegexList = subredditRegexList.map(compileRegexSafely).filter(Boolean);
        compiledTitleRegexList = titleRegexList.map(compileRegexSafely).filter(Boolean);
        filtersEnabled = enabled;
    };

    // Save helpers
    const save = (key, value) => GM.setValue(key, JSON.stringify([...value]));
    const saveFiltersEnabled = () => GM.setValue(KEY_FILTERS_ENABLED, filtersEnabled);

    const saveRegexList = async () => {
        await GM.setValue(KEY_SUB_REGEX_LIST, JSON.stringify(subredditRegexList));
        compiledSubredditRegexList = subredditRegexList.map(compileRegexSafely).filter(Boolean);
    };

    const saveTitleRegexList = async () => {
        await GM.setValue(KEY_TITLE_REGEX_LIST, JSON.stringify(titleRegexList));
        compiledTitleRegexList = titleRegexList.map(compileRegexSafely).filter(Boolean);
    };

    // Turns a user provided regex string into a RegExp object without throwing.
    function compileRegexSafely(src) {
        if (typeof src !== 'string' || !src.trim()) return null;
        const s = src.trim();
        try {
            const m = s.match(/^\/(.+)\/([a-z]*)$/i);
            return m ? new RegExp(m[1], m[2]) : new RegExp(s, 'i');
        } catch {
            return null;
        }
    }

    // Matches a subreddit string against the compiled regex list.
    function matchSubredditRegex(sub) {
        for (let i = 0; i < compiledSubredditRegexList.length; i++) {
            const rx = compiledSubredditRegexList[i];
            try { if (rx.test(sub)) return i; } catch {}
        }
        return -1;
    }

    // Matches a title against the compiled title regex list.
    function matchTitleRegex(title) {
        for (let i = 0; i < compiledTitleRegexList.length; i++) {
            const rx = compiledTitleRegexList[i];
            try { if (rx.test(title)) return i; } catch {}
        }
        return -1;
    }

    // Updates the counter displayed on the toggle button.
    const updateButtonCounter = () => {
        const toggle = document.querySelector('.toggle-filters-button');
        if (toggle) {
            toggle.textContent = filtersEnabled
                ? 'Disable Filters (' + hiddenPostCount + ')'
                : 'Enable Filters';
        }
    };

    // Ensures a button exists next to a subreddit or user link. The action toggles block rules.
    const ensureButton = (el, selector, text, onClick) => {
        let btn = el.parentNode.querySelector(selector);
        if (!btn) {
            btn = document.createElement('button');
            btn.className = selector.replace('.', '');
            Object.assign(btn.style, {
                marginLeft: '3px', cursor: 'pointer', fontSize: '5px', padding: '0 1px'
            });
            btn.addEventListener('click', onClick);
            el.insertAdjacentElement('afterend', btn);
        }
        btn.textContent = text;
    };

    // Applies or updates block buttons for subreddit and user for a specific post.
    function manageButtons(post, sub, subEl, user, userEl) {
        if (subEl) {
            const blocked = blockedSubreddits.has(sub);
            ensureButton(subEl, '.block-subreddit-button', blocked ? 'Unblock Sub' : 'Block Sub', async () => {
                if (blocked) {
                    blockedSubreddits.delete(sub);
                } else {
                    blockedSubreddits.add(sub);
                    post.remove();
                }
                await save(KEY_BLOCKED_SUBS, blockedSubreddits);
                queueMicrotask(() => hideAllPosts());
            });
        }

        if (userEl) {
            const blocked = blockedUsers.has(user);
            ensureButton(userEl, '.block-user-button', blocked ? 'Unblock User' : 'Block User', async () => {
                blocked ? blockedUsers.delete(user) : blockedUsers.add(user);
                await save(KEY_BLOCKED_USERS, blockedUsers);
                hideSinglePost(post);
            });
        }
    }

    // Adds a toggle button to enable or disable all filters without removing them.
    function addFilterToggleButton() {
        const menu = document.querySelector('.tabmenu');
        if (menu && !document.querySelector('.toggle-filters-button')) {
            const btn = document.createElement('button');
            btn.className = 'toggle-filters-button';
            Object.assign(btn.style, {
                marginLeft: '5px', cursor: 'pointer', fontSize: '10px', padding: '0 2px'
            });
            btn.addEventListener('click', async () => {
                filtersEnabled = !filtersEnabled;
                await saveFiltersEnabled();
                hideAllPosts();
            });
            menu.appendChild(btn);
            updateButtonCounter();
        }
    }

    // Checks if a subreddit is blocked by name or by regex. Uses cache to avoid repeats.
    function isBlockedSub(sub) {
        if (subCache.has(sub)) return subCache.get(sub);
        const idx = matchSubredditRegex(sub);
        const res = { exact: blockedSubreddits.has(sub), regexIndex: idx };
        subCache.set(sub, res);
        return res;
    }

    // Checks if a user is blocked. Uses cache.
    function isBlockedUser(user) {
        if (userCache.has(user)) return userCache.get(user);
        const res = blockedUsers.has(user);
        userCache.set(user, res);
        return res;
    }

    // This function evaluates a single post, decides if it should be hidden, colored,
    // or left alone, and applies user control buttons.
    function hideSinglePost(post) {
        const titleEl = post.querySelector('a.title');
        const subEl = post.querySelector('a.subreddit');
        const userEl = post.querySelector('a.author');
        if (!titleEl || !subEl || !userEl) return;

        const title = titleEl.textContent;
        const sub = subEl.textContent.replace('/r/', '').trim();
        const user = userEl.textContent.trim();

        const tMatchIndex = matchTitleRegex(title);
        const tMatch = tMatchIndex >= 0;
        const subRes = isBlockedSub(sub);
        const sMatch = subRes.exact;
        const rMatch = subRes.regexIndex >= 0;
        const uMatch = isBlockedUser(user);

        const shouldHide = filtersEnabled && (tMatch || sMatch || rMatch || uMatch);

        if (shouldHide) {
            if (post.style.display !== 'none') post.style.display = 'none';
            hiddenPostCount++;
        } else {
            if (post.style.display === 'none') post.style.display = '';
            let color = COLORS.default;
            if (!filtersEnabled) {
                if (tMatch) color = COLORS.titleRegex;
                else if (sMatch) color = COLORS.blockedSubreddits;
                else if (rMatch) color = COLORS.subredditRegexList;
                else if (uMatch) color = COLORS.blockedUsers;
            }
            if (post.style.backgroundColor !== color) post.style.backgroundColor = color;
            manageButtons(post, sub, subEl, user, userEl);
        }
    }

    // Reprocesses all posts in the listing and reconnects the observer afterward.
    function hideAllPosts() {
        if (observer) observer.disconnect();
        hiddenPostCount = 0;
        for (const post of document.querySelectorAll('.thing')) hideSinglePost(post);
        updateButtonCounter();
        if (observer) observePostContainer();
    }

    // Mutation observer callback. Only reacts to new post nodes, minimizing CPU usage.
    function observePostContainer() {
        postContainer = document.querySelector('#siteTable');
        if (!postContainer) return;

        observer = new MutationObserver((mutations) => {
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (node.nodeType === 1 && node.matches('.thing')) hideSinglePost(node);
                }
            }
        });

        observer.observe(postContainer, { childList: true, subtree: false });
    }

    // Exposes menus to modify regex lists interactively.
    function registerMenu() {
        if (typeof GM_registerMenuCommand !== 'function') return;
        if (window.__regexMenuBound) return;
        window.__regexMenuBound = true;

        GM_registerMenuCommand('Add Subreddit Regex', async () => {
            const input = prompt('Enter subreddit regex to add:');
            if (input === null) return;
            subredditRegexList.push(input);
            await saveRegexList();
            hideAllPosts();
        });

        GM_registerMenuCommand('List Subreddit Regexes', () => {
            alert(subredditRegexList.length
                ? subredditRegexList.map((s, i) => i + ': ' + s).join('\n')
                : '(none)');
        });

        GM_registerMenuCommand('Remove Subreddit Regex', async () => {
            if (!subredditRegexList.length) return alert('Empty list.');
            const idx = Number(prompt(subredditRegexList.map((s, i) => i + ': ' + s).join('\n')));
            if (Number.isInteger(idx) && idx >= 0 && idx < subredditRegexList.length) {
                subredditRegexList.splice(idx, 1);
                await saveRegexList();
                hideAllPosts();
                alert('Removed.');
            }
        });

        GM_registerMenuCommand('Clear All Subreddit Regexes', async () => {
            if (!subredditRegexList.length) return alert('Already empty.');
            if (!confirm('Clear all subreddit regex entries?')) return;
            subredditRegexList = [];
            await saveRegexList();
            hideAllPosts();
        });

        GM_registerMenuCommand('Add Title Regex', async () => {
            const input = prompt('Enter title regex to add:');
            if (input === null) return;
            titleRegexList.push(input);
            await saveTitleRegexList();
            hideAllPosts();
        });

        GM_registerMenuCommand('List Title Regexes', () => {
            alert(titleRegexList.length
                ? titleRegexList.map((s, i) => i + ': ' + s).join('\n')
                : '(none)');
        });

        GM_registerMenuCommand('Remove Title Regex', async () => {
            if (!titleRegexList.length) return alert('Empty list.');
            const idx = Number(prompt(titleRegexList.map((s, i) => i + ': ' + s).join('\n')));
            if (Number.isInteger(idx) && idx >= 0 && idx < titleRegexList.length) {
                titleRegexList.splice(idx, 1);
                await saveTitleRegexList();
                hideAllPosts();
                alert('Removed.');
            }
        });

        GM_registerMenuCommand('Clear All Title Regexes', async () => {
            if (!titleRegexList.length) return alert('Already empty.');
            if (!confirm('Clear all title regex entries?')) return;
            titleRegexList = [];
            await saveTitleRegexList();
            hideAllPosts();
        });
    }

    // Initialization sequence: loads state, registers menus, and sets up filtering once the page is idle.
    (async () => {
        await loadAll();
        registerMenu();
        window.addEventListener('load', () => {
            requestIdleCallback(() => {
                addFilterToggleButton();
                hideAllPosts();
                observePostContainer();
            });
        });
    })();

})();