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