Greasy Fork is available in English.

Twitter MTF killer

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();