Twitter MTF killer

荷包蛋自用净化推特贴文的脚本,全自动隐藏MTF相关贴文,检测内容包括贴文正文,贴文标签,用户名,用户简介。支持UI管理关键词

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Twitter MTF killer
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  荷包蛋自用净化推特贴文的脚本,全自动隐藏MTF相关贴文,检测内容包括贴文正文,贴文标签,用户名,用户简介。支持UI管理关键词
// @author       Ayase
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      x.com
// @connect      twitter.com
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 默认关键词配置 ---
    const DEFAULT_KEYWORDS = [
        '男娘', '伪娘', '药娘', '男同', 'mtf', '🏳️‍⚧️', '🏳️‍🌈', '跨性别', '扶她', 'futa',
        '性转', 'LGBT', '🍥', 'furry', '男童', '福瑞', '僞娘', '同性戀', '同性恋', '藥娘',
        '南娘', '男の娘', 'femboy', '三性', '#TS', '雌堕', '南梁', '女装', 'otokonoko',
        '木桶饭', '酷儿', '⚧️', 'lesbian', '#gay', '人妖', '补佳乐', '雌激素', '糖糖',
        '色普隆', 'trap', 'sissy', 'crossdresser', '扶他', 'boylove', 'twink', '可攻可受',
        '受受', '兽设', '神楽坂', '真寻', '#CD', '#男孩子', '#可爱的男孩子'
    ];


    /**
     * 配置管理器
     * 负责加载、保存和管理关键词列表
     */
    const Config = {
        keywords: new Set(),
        load() {
            let storedKeywords = GM_getValue('blockedKeywords');
            if (!storedKeywords || !Array.isArray(storedKeywords) || storedKeywords.length === 0) {
                console.log('[Twitter Blocker] 未找到已存关键词,加载默认列表。');
                storedKeywords = DEFAULT_KEYWORDS;
                this.save(storedKeywords);
            }
            this.keywords = new Set(storedKeywords.map(k => k.trim().toLowerCase()).filter(Boolean));
            console.log('[Twitter Blocker] 关键词加载完成,当前数量:', this.keywords.size);
        },
        save(keywordsArray) {
            const cleanedKeywords = [...new Set(keywordsArray.map(k => k.trim().toLowerCase()).filter(Boolean))];
            GM_setValue('blockedKeywords', cleanedKeywords);
            this.keywords = new Set(cleanedKeywords);
            console.log('[Twitter Blocker] 关键词已保存。');
        },
        getKeywords() {
            return [...this.keywords];
        },
        resetToDefault() {
            this.save(DEFAULT_KEYWORDS);
            console.log('[Twitter Blocker] 已重置为默认关键词列表');
        }
    };

    /**
     * UI管理器
     * 负责创建和管理所有界面元素,如通知、设置按钮和设置面板
     */
    const UI = {
        init() {
            this.injectStyles();
            this.createToastContainer();
            this.createDraggableSettingsButton();
            this.createSettingsPanel();
        },

        injectStyles() {
            GM_addStyle(`
                :root { --blocker-primary-color: #1D9BF0; }
                /* Toast 通知 */
                #blocker-toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; }
                .blocker-toast-message { background-color: rgba(29, 155, 240, 0.9); color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); opacity: 0; transform: translateX(100%); transition: all 0.4s cubic-bezier(0.21, 1.02, 0.73, 1); font-size: 14px; line-height: 1.4; }
                .blocker-toast-message.show { opacity: 1; transform: translateX(0); }
                .blocker-toast-message b { font-weight: bold; }

                /* 主页屏蔽提示 */
                .blocker-profile-overlay { padding: 40px 20px; text-align: center; border: 1px dashed #555; border-radius: 12px; margin: 20px; background-color: rgba(0,0,0,0.03); }
                .blocker-profile-overlay h3 { font-size: 18px; font-weight: bold; margin-bottom: 10px; }

                /* 可拖拽设置按钮 */
                #blocker-settings-btn { position: fixed; bottom: 80px; right: 25px; z-index: 99998; width: 48px; height: 48px; background-color: var(--blocker-primary-color); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); transition: transform 0.2s, background-color 0.2s; user-select: none; touch-action: none; }
                #blocker-settings-btn:hover { background-color: #1a8cd8; transform: scale(1.1); }
                #blocker-settings-btn.dragging { opacity: 0.8; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }

                /* 设置面板 */
                #blocker-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 100000; display: none; align-items: center; justify-content: center; }
                #blocker-settings-panel { background-color: #15202B; color: #F7F9F9; width: 90%; max-width: 500px; border-radius: 16px; box-shadow: 0 0 20px rgba(0,0,0,0.3); display: flex; flex-direction: column; max-height: 80vh; }
                .blocker-panel-header { padding: 16px; border-bottom: 1px solid #38444D; font-size: 20px; font-weight: bold; }
                .blocker-panel-body { padding: 16px; overflow-y: auto; }
                .blocker-keywords-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
                .blocker-keyword-tag { background-color: #38444D; padding: 5px 10px; border-radius: 16px; font-size: 14px; display: flex; align-items: center; }
                .blocker-keyword-tag span { margin-right: 8px; }
                .blocker-keyword-tag .delete-btn { cursor: pointer; color: #8899A6; font-weight: bold; }
                .blocker-keyword-tag .delete-btn:hover { color: white; }
                .blocker-input-group { margin-top: 20px; display: flex; gap: 10px; }
                #blocker-new-keyword-input { flex-grow: 1; background-color: #253341; border: 1px solid #38444D; border-radius: 8px; padding: 10px; color: white; }
                .blocker-panel-footer { padding: 16px; border-top: 1px solid #38444D; display: flex; justify-content: space-between; gap: 12px; }
                .blocker-panel-footer-left { display: flex; gap: 12px; }
                .blocker-panel-footer-right { display: flex; gap: 12px; }
                .blocker-btn { padding: 10px 20px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; transition: all 0.2s; }
                .blocker-btn-primary { background-color: var(--blocker-primary-color); color: white; }
                .blocker-btn-primary:hover { opacity: 0.9; }
                .blocker-btn-secondary { background-color: #657786; color: white; }
                .blocker-btn-secondary:hover { opacity: 0.9; }
                .blocker-btn-warning { background-color: #f0ad4e; color: white; }
                .blocker-btn-warning:hover { background-color: #ec971f; }
            `);
        },

        createToastContainer() {
            if (!document.getElementById('blocker-toast-container')) {
                const container = document.createElement('div');
                container.id = 'blocker-toast-container';
                document.body.appendChild(container);
            }
        },

        showNotification(message) {
            const container = document.getElementById('blocker-toast-container');
            if (!container) return;
            const toast = document.createElement('div');
            toast.className = 'blocker-toast-message';
            toast.innerHTML = message;
            container.appendChild(toast);
            setTimeout(() => toast.classList.add('show'), 10);
            setTimeout(() => {
                toast.classList.remove('show');
                toast.addEventListener('transitionend', () => toast.remove());
            }, 3000);
        },

        createDraggableSettingsButton() {
            const btn = document.createElement('div');
            btn.id = 'blocker-settings-btn';
            btn.innerHTML = '⚙️';
            btn.title = '屏蔽脚本设置 (可拖拽)';
            
            const savedPosition = GM_getValue('settingsButtonPosition', { x: 25, y: 80 });
            btn.style.right = `${savedPosition.x}px`;
            btn.style.bottom = `${savedPosition.y}px`;
            
            btn.onclick = (e) => {
                if (!btn.classList.contains('dragging')) {
                    document.getElementById('blocker-settings-overlay').style.display = 'flex';
                    this.renderKeywordsList();
                }
            };
            
            let isDragging = false;
            let startX, startY, startRight, startBottom;
            
            btn.addEventListener('mousedown', startDrag);
            btn.addEventListener('touchstart', startDrag);
            
            function startDrag(e) {
                e.preventDefault();
                e.stopPropagation();
                
                isDragging = true;
                btn.classList.add('dragging');
                
                const rect = btn.getBoundingClientRect();
                startRight = parseInt(btn.style.right) || 25;
                startBottom = parseInt(btn.style.bottom) || 80;
                
                if (e.type === 'mousedown') {
                    startX = e.clientX;
                    startY = e.clientY;
                    document.addEventListener('mousemove', onDrag);
                    document.addEventListener('mouseup', stopDrag);
                } else if (e.type === 'touchstart') {
                    startX = e.touches[0].clientX;
                    startY = e.touches[0].clientY;
                    document.addEventListener('touchmove', onDrag);
                    document.addEventListener('touchend', stopDrag);
                }
            }
            
            function onDrag(e) {
                if (!isDragging) return;
                
                e.preventDefault();
                
                let clientX, clientY;
                if (e.type === 'mousemove') {
                    clientX = e.clientX;
                    clientY = e.clientY;
                } else if (e.type === 'touchmove') {
                    clientX = e.touches[0].clientX;
                    clientY = e.touches[0].clientY;
                }
                

                const deltaX = startX - clientX;
                const deltaY = startY - clientY;
                
                let newRight = startRight + deltaX;
                let newBottom = startBottom + deltaY;
                
                const maxRight = window.innerWidth - btn.offsetWidth - 10;
                const maxBottom = window.innerHeight - btn.offsetHeight - 10;
                
                newRight = Math.max(10, Math.min(newRight, maxRight));
                newBottom = Math.max(10, Math.min(newBottom, maxBottom));
                
                btn.style.right = `${newRight}px`;
                btn.style.bottom = `${newBottom}px`;
            }
            
            function stopDrag(e) {
                if (!isDragging) return;
                
                isDragging = false;
                btn.classList.remove('dragging');
                

                document.removeEventListener('mousemove', onDrag);
                document.removeEventListener('mouseup', stopDrag);
                document.removeEventListener('touchmove', onDrag);
                document.removeEventListener('touchend', stopDrag);
                
                const currentRight = parseInt(btn.style.right);
                const currentBottom = parseInt(btn.style.bottom);
                GM_setValue('settingsButtonPosition', { x: currentRight, y: currentBottom });
            }
            
            document.body.appendChild(btn);
        },

        createSettingsPanel() {
            const overlay = document.createElement('div');
            overlay.id = 'blocker-settings-overlay';
            overlay.innerHTML = `
                <div id="blocker-settings-panel">
                    <div class="blocker-panel-header">屏蔽关键词管理</div>
                    <div class="blocker-panel-body">
                        <p>在此处添加或删除屏蔽词。修改后请点击"保存并刷新"以生效。</p>
                        <div class="blocker-keywords-list"></div>
                        <div class="blocker-input-group">
                            <input type="text" id="blocker-new-keyword-input" placeholder="输入新关键词...">
                            <button id="blocker-add-keyword-btn" class="blocker-btn blocker-btn-primary">添加</button>
                        </div>
                    </div>
                    <div class="blocker-panel-footer">
                        <div class="blocker-panel-footer-left">
                            <button id="blocker-reset-btn" class="blocker-btn blocker-btn-warning">恢复默认</button>
                        </div>
                        <div class="blocker-panel-footer-right">
                            <button id="blocker-close-btn" class="blocker-btn blocker-btn-secondary">关闭</button>
                            <button id="blocker-save-btn" class="blocker-btn blocker-btn-primary">保存并刷新</button>
                        </div>
                    </div>
                </div>
            `;
            document.body.appendChild(overlay);


            document.getElementById('blocker-close-btn').onclick = () => overlay.style.display = 'none';
            
            document.getElementById('blocker-reset-btn').onclick = () => {
                if (confirm('确定要恢复默认关键词列表吗?这将删除所有自定义关键词。')) {
                    Config.resetToDefault();
                    this.renderKeywordsList();
                    this.showNotification('已恢复默认关键词列表');
                }
            };
            
            overlay.onclick = (e) => { if (e.target === overlay) overlay.style.display = 'none'; };
            
            const addKeyword = () => {
                const input = document.getElementById('blocker-new-keyword-input');
                const newKeyword = input.value.trim();
                if (newKeyword) {
                    const currentKeywords = this.getCurrentKeywordsFromPanel();
                    if (!currentKeywords.includes(newKeyword.toLowerCase())) {
                        this.addKeywordToPanel(newKeyword);
                    }
                    input.value = '';
                    input.focus();
                }
            };
            document.getElementById('blocker-add-keyword-btn').onclick = addKeyword;
            document.getElementById('blocker-new-keyword-input').onkeydown = (e) => {
                if (e.key === 'Enter') addKeyword();
            };

            document.getElementById('blocker-save-btn').onclick = () => {
                const keywords = this.getCurrentKeywordsFromPanel();
                Config.save(keywords);
                overlay.style.display = 'none';
                this.showNotification('关键词已保存,正在刷新页面...');
                setTimeout(() => window.location.reload(), 1500);
            };
        },

        getCurrentKeywordsFromPanel() {
            const list = document.querySelector('#blocker-settings-panel .blocker-keywords-list');
            return Array.from(list.children).map(tag => tag.querySelector('span').textContent);
        },

        renderKeywordsList() {
            const list = document.querySelector('#blocker-settings-panel .blocker-keywords-list');
            list.innerHTML = '';
            Config.getKeywords().forEach(kw => this.addKeywordToPanel(kw, list));
        },
        
        addKeywordToPanel(keyword, listElement) {
            const list = listElement || document.querySelector('#blocker-settings-panel .blocker-keywords-list');
            const tag = document.createElement('div');
            tag.className = 'blocker-keyword-tag';
            tag.innerHTML = `<span>${keyword}</span><a class="delete-btn">×</a>`;
            tag.querySelector('.delete-btn').onclick = () => tag.remove();
            list.appendChild(tag);
        }
    };

    /**
     * 核心屏蔽器
     * 负责检测、屏蔽推文和主页
     */
    const Blocker = {
        userBioCache: new Map(),
        isCurrentProfileBlocked: false,
        lastCheckedUrl: '',
        scanTimeout: null,

        init() {
            if (Config.keywords.size === 0) {
                console.log('[Twitter Blocker] 关键词列表为空,脚本未启动。');
                return;
            }

            window.requestIdleCallback ? requestIdleCallback(() => this.scanAndBlock()) : setTimeout(() => this.scanAndBlock(), 500);

            const observer = new MutationObserver(() => {
                clearTimeout(this.scanTimeout);
                this.scanTimeout = setTimeout(() => this.scanAndBlock(), 300);
            });
            observer.observe(document.body, { childList: true, subtree: true });
        },

        getElementTextWithEmojiAlt(element) {
            if (!element) return '';
            let fullText = element.textContent || '';
            element.querySelectorAll('img[alt]').forEach(img => {
                fullText += ` ${img.alt}`;
            });
            return fullText.trim();
        },

        findMatchingKeyword(text) {
            if (!text || Config.keywords.size === 0) return null;
            const lowerText = text.toLowerCase();
            for (const keyword of Config.keywords) {
                if (lowerText.includes(keyword)) return keyword;
            }
            return null;
        },

        hideTweet(tweetElement, reason, source) {
            const message = `已屏蔽 (<b>${source}</b>)<br>原因: ${reason}`;
            UI.showNotification(message);
            console.log(`[Twitter Blocker] ${message.replace(/<br>|<b>|<\/b>/g, ' ')}`);

            const parentCell = tweetElement.closest('div[data-testid="cellInnerDiv"]');
            if (parentCell) {
                parentCell.style.display = 'none';
            } else {
                tweetElement.style.display = 'none';
            }
        },

        checkUserBioInBackground(username, tweetElement) {
            if (this.userBioCache.has(username)) return;
            this.userBioCache.set(username, 'checking');

            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://x.com/${username}`,
                onload: (response) => {
                    const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
                    const bioElement = doc.querySelector('[data-testid="UserDescription"]');
                    const bioText = this.getElementTextWithEmojiAlt(bioElement);
                    const matchedKeyword = this.findMatchingKeyword(bioText);

                    if (matchedKeyword) {
                        this.userBioCache.set(username, { blocked: true, keyword: matchedKeyword });
                        this.hideTweet(tweetElement, `简介含 "<b>${matchedKeyword}</b>"`, `@${username}`);
                    } else {
                        this.userBioCache.set(username, { blocked: false });
                    }
                },
                onerror: (response) => {
                    console.error(`[Twitter Blocker] 获取 @${username} 的主页失败:`, response);
                    this.userBioCache.set(username, { blocked: false });
                }
            });
        },

        processTweet(tweetElement) {
            if (tweetElement.dataset.blockerChecked) return;
            tweetElement.dataset.blockerChecked = 'true';

            if (this.isCurrentProfileBlocked) {
                tweetElement.style.display = 'none';
                return;
            }

            // 1. 检查推文内容
            const tweetTextElement = tweetElement.querySelector('[data-testid="tweetText"]');
            const tweetText = this.getElementTextWithEmojiAlt(tweetTextElement);
            let matchedKeyword = this.findMatchingKeyword(tweetText);
            if (matchedKeyword) {
                this.hideTweet(tweetElement, `内容含 "<b>${matchedKeyword}</b>"`, "推文内容");
                return;
            }

            // 2. 检查用户名和昵称
            const userLinkElement = tweetElement.querySelector('[data-testid="User-Name"] a[href^="/"]');
            if (!userLinkElement) return;

            const username = userLinkElement.getAttribute('href').substring(1);
            const userDisplayName = this.getElementTextWithEmojiAlt(userLinkElement.querySelector('div > div > span'));
            const source = `<b>${userDisplayName || ''}</b> (@${username})`;

            matchedKeyword = this.findMatchingKeyword(username) || this.findMatchingKeyword(userDisplayName);
            if (matchedKeyword) {
                this.hideTweet(tweetElement, `用户名/昵称含 "<b>${matchedKeyword}</b>"`, source);
                return;
            }

            // 3. 检查用户简介 (使用缓存或后台请求)
            const cacheResult = this.userBioCache.get(username);
            if (cacheResult) {
                if (cacheResult.blocked) {
                    this.hideTweet(tweetElement, `简介含 "<b>${cacheResult.keyword}</b>" (缓存)`, source);
                }
            } else {
                this.checkUserBioInBackground(username, tweetElement);
            }
        },

        processProfilePage() {
            if (document.body.dataset.profileChecked) return;

            const bioElement = document.querySelector('[data-testid="UserDescription"]');
            if (bioElement) {
                document.body.dataset.profileChecked = 'true';
                const bioText = this.getElementTextWithEmojiAlt(bioElement);
                const matchedKeyword = this.findMatchingKeyword(bioText);

                if (matchedKeyword) {
                    this.isCurrentProfileBlocked = true;
                    const message = `用户主页已屏蔽<br>原因: 简介含 "<b>${matchedKeyword}</b>"`;
                    UI.showNotification(message);
                    console.log(`[Twitter Blocker] ${message.replace(/<br>|<b>|<\/b>/g, ' ')}`);

                    const timeline = document.querySelector('div[data-testid="primaryColumn"]');
                    if (timeline) {
                        const nav = timeline.querySelector('nav');
                        timeline.innerHTML = '';
                        if(nav) timeline.appendChild(nav); 

                        const overlay = document.createElement('div');
                        overlay.className = 'blocker-profile-overlay';
                        overlay.innerHTML = `<h3>此用户主页已被屏蔽</h3><p>原因:简介中包含屏蔽词 "<b>${matchedKeyword}</b>"</p>`;
                        timeline.appendChild(overlay);
                    }
                }
            }
        },

        scanAndBlock() {
            if (window.location.href !== this.lastCheckedUrl) {
                this.lastCheckedUrl = window.location.href;
                this.isCurrentProfileBlocked = false;
                delete document.body.dataset.profileChecked;
            }

            const path = window.location.pathname;
            const isProfilePage = /^\/[a-zA-Z0-9_]{1,15}$/.test(path) || /^\/[a-zA-Z0-9_]{1,15}\/(with_replies|media|likes)$/.test(path);

            if (isProfilePage && !this.isCurrentProfileBlocked) {
                this.processProfilePage();
            }

            if (this.isCurrentProfileBlocked) return;

            document.querySelectorAll('article[data-testid="tweet"]:not([data-blocker-checked])').forEach(tweet => this.processTweet(tweet));
        }
    };

    function start() {
        Config.load();
        UI.init();
        Blocker.init();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', start);
    } else {
        start();
    }

})();