Greasy Fork

Greasy Fork is available in English.

Twitter MTF killer

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(function() {
    'use strict';

    // --- 关键词配置 ---
    const BLOCKED_KEYWORDS_RAW = [
        '男娘',
        '伪娘',
        '药娘',
        '男同',
        'mtf',
        '🏳️‍⚧️',
        '🏳️‍🌈',
        '跨性别',
        '扶她',
        'futa',
        '性转',
        'LGBT',
        '🍥',
        'furry',
        '男童',
        '福瑞'
    ];


    const keywords = BLOCKED_KEYWORDS_RAW.map(k => k.trim().toLowerCase()).filter(k => k.length > 0);
    const userBioCache = new Map();
    let isCurrentProfileBlocked = false;
    let lastCheckedUrl = '';


    GM_addStyle(`
        #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; }
    `);
    const initToastContainer = () => {
        if (!document.getElementById('blocker-toast-container')) {
            const container = document.createElement('div');
            container.id = 'blocker-toast-container';
            document.body.appendChild(container);
        }
    };
    const 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());
        }, 2500);
    };


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


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


    const checkUserBioInBackground = (username, tweetElement) => {
        if (userBioCache.get(username)) return;

        userBioCache.set(username, 'checking');

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

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

    const hideTweet = (tweetElement, reason, source) => {
        const message = `已屏蔽 (<b>${source}</b>)<br>原因: ${reason}`;
        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';
        }
    };

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

        if (isCurrentProfileBlocked) {
            hideTweet(tweetElement, "当前主页已被屏蔽", "主页状态");
            return;
        }

        let matchedKeyword, reason, source;

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

        const userLinkElement = tweetElement.querySelector('[data-testid="User-Name"] a[href^="/"]');
        if (!userLinkElement) return;

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

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

        const cacheResult = userBioCache.get(username);
        if (cacheResult === 'blocked') {
            hideTweet(tweetElement, "简介(来自缓存)", source);
            return;
        }

        if (!cacheResult) {
            checkUserBioInBackground(username, tweetElement);
        }
    };

    const processProfile = () => {
        const bioElement = document.querySelector('[data-testid="UserDescription"]');
        if (bioElement && !bioElement.dataset.blockerChecked) {
            bioElement.dataset.blockerChecked = 'true';
            const bioText = getElementTextWithEmojiAlt(bioElement);
            const matchedKeyword = findMatchingKeyword(bioText);

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

    const scanAndBlock = (forceScan = false) => {
        if (window.location.href !== lastCheckedUrl) {
            lastCheckedUrl = window.location.href;
            isCurrentProfileBlocked = false;
        }

        const path = window.location.pathname;
        const isProfilePage = path.split('/').length === 2 && path.length > 1 && !path.includes('/i/') && !/^\/(home|explore|notifications|messages|search|settings)/.test(path);


        if (isProfilePage) {
            processProfile();
        }

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

    const observer = new MutationObserver(() => scanAndBlock());

    const start = () => {
        if (keywords.length === 0) return console.log('[Twitter Blocker] 关键词列表为空,脚本未启动。');
        console.log('[Twitter Blocker] 智能屏蔽脚本已启动,当前关键词:', keywords);

        initToastContainer();
        
        window.requestIdleCallback ? requestIdleCallback(scanAndBlock) : setTimeout(scanAndBlock, 500);

        observer.observe(document.body, { childList: true, subtree: true });
    };

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

})();