Enter keywords (comma-separated), hit Start Scanning, and the script auto-blocks users whose replies on X status pages contain any keyword. Must be logged in to X. State and keywords are saved locally; panel is minimizable.
// ==UserScript==
// @name X.com Auto Comment Blocker
// @name:zh-CN X 评论自动屏蔽
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Enter keywords (comma-separated), hit Start Scanning, and the script auto-blocks users whose replies on X status pages contain any keyword. Must be logged in to X. State and keywords are saved locally; panel is minimizable.
// @description:zh-CN 输入关键词(逗号分隔),点击开始扫描,脚本会自动屏蔽在 X 状态页回复中包含任何关键词的用户。需登录 X。关键词和状态保存在本地;面板可最小化。
// @author DarkoDev
// @match *://*.x.com/*
// @match *://*.twitter.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// X Web App standard Bearer Token (Public)
const BEARER_TOKEN = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
// State
let keywords = GM_getValue('x_block_keywords', []);
let isRunning = GM_getValue('x_block_running', false);
let isMinimized = GM_getValue('x_block_minimized', false);
const scannedTweets = new WeakSet();
// --- UI Setup ---
function createUI() {
GM_addStyle(`
#x-auto-blocker {
position: fixed; bottom: 100px; left: 20px; width: 280px;
background: #15202b; color: #fff; border: 1px solid #38444d;
border-radius: 12px; padding: 15px; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
transition: height 0.2s ease;
}
#x-ab-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 10px;
}
#x-ab-header h4 { margin: 0; font-size: 14px; font-weight: bold; }
#x-minimize-btn {
background: none; border: none; color: #8899a6; font-size: 14px;
cursor: pointer; font-family: monospace; font-weight: bold; padding: 0 5px;
}
#x-minimize-btn:hover { color: #fff; }
#x-auto-blocker textarea {
width: 100%; height: 60px; background: #22303c; color: #fff;
border: 1px solid #38444d; border-radius: 6px; padding: 5px;
box-sizing: border-box; resize: none; margin-bottom: 10px;
}
#x-auto-blocker .controls { display: flex; justify-content: space-between; }
#x-auto-blocker button.action-btn {
background: #1d9bf0; color: #fff; border: none; padding: 6px 12px;
border-radius: 9999px; cursor: pointer; font-weight: bold; font-size: 12px;
}
#x-auto-blocker button.action-btn:hover { background: #1a8cd8; }
#x-auto-blocker button.danger { background: #f4212e; }
#x-auto-blocker button.danger:hover { background: #e0245e; }
/* Minimized State: hide body except the Start/Stop toggle */
#x-auto-blocker.x-minimized textarea,
#x-auto-blocker.x-minimized #x-save-btn,
#x-auto-blocker.x-minimized #x-block-log { display: none; }
#x-auto-blocker.x-minimized #x-ab-header { margin-bottom: 6px; }
#x-auto-blocker.x-minimized .controls { justify-content: flex-end; }
#x-auto-blocker.x-minimized { padding: 10px 15px; width: auto; }
`);
const container = document.createElement('div');
container.id = 'x-auto-blocker';
if (isMinimized) container.classList.add('x-minimized');
container.innerHTML = `
<div id="x-ab-header">
<h4>Auto Comment Blocker</h4>
<button id="x-minimize-btn" title="Toggle Minimize">${isMinimized ? '[+]' : '[-]'}</button>
</div>
<div id="x-ab-body">
<textarea id="x-keywords-input" placeholder="Keywords (comma separated) e.g., crypto, airdrop">${keywords.join(', ')}</textarea>
<div class="controls">
<button id="x-save-btn" class="action-btn">Save</button>
<button id="x-toggle-btn" class="action-btn ${isRunning ? 'danger' : ''}">${isRunning ? 'Stop Scanning' : 'Start Scanning'}</button>
</div>
<div id="x-block-log" style="font-size:11px; margin-top:10px; color:#8899a6; max-height:45px; overflow-y:auto;"></div>
</div>
`;
document.body.appendChild(container);
// Minimize Toggle Event
document.getElementById('x-minimize-btn').addEventListener('click', (e) => {
isMinimized = !isMinimized;
GM_setValue('x_block_minimized', isMinimized);
container.classList.toggle('x-minimized', isMinimized);
e.target.innerText = isMinimized ? '[+]' : '[-]';
});
// Save Event
document.getElementById('x-save-btn').addEventListener('click', () => {
const val = document.getElementById('x-keywords-input').value;
keywords = val.split(',').map(k => k.trim().toLowerCase()).filter(k => k);
GM_setValue('x_block_keywords', keywords);
logMsg(`Saved ${keywords.length} keywords.`);
});
// Start/Stop Event
document.getElementById('x-toggle-btn').addEventListener('click', (e) => {
isRunning = !isRunning;
GM_setValue('x_block_running', isRunning);
e.target.innerText = isRunning ? 'Stop Scanning' : 'Start Scanning';
e.target.classList.toggle('danger', isRunning);
logMsg(isRunning ? 'Scanner started.' : 'Scanner stopped.');
});
}
function logMsg(msg) {
const logBox = document.getElementById('x-block-log');
if (logBox) {
logBox.innerHTML = `<div>> ${msg}</div>` + logBox.innerHTML;
}
console.log(`[X-Blocker] ${msg}`);
}
// --- API Logic ---
function getCsrfToken() {
const match = document.cookie.match(/(?:^|;\s*)ct0=([^;]*)/);
return match ? match[1] : null;
}
async function blockUser(screenName) {
const csrf = getCsrfToken();
if (!csrf) {
logMsg(`Failed to get CSRF for ${screenName}`);
return false;
}
try {
const res = await fetch('https://x.com/i/api/1.1/blocks/create.json', {
method: 'POST',
headers: {
'authorization': BEARER_TOKEN,
'x-csrf-token': csrf,
'content-type': 'application/x-www-form-urlencoded'
},
body: `screen_name=${screenName}`
});
if (res.ok) {
logMsg(`Blocked @${screenName}`);
return true;
} else {
logMsg(`Block failed @${screenName} (${res.status})`);
return false;
}
} catch (err) {
logMsg(`Error blocking @${screenName}`);
return false;
}
}
// --- DOM Parsing Logic ---
function processTweet(article) {
if (!window.location.pathname.match(/\/\w+\/status\/\d+/)) return;
const pathAuthor = window.location.pathname.split('/')[1];
const userInfo = article.querySelector('[data-testid="User-Name"]');
if (!userInfo) return;
const screenNameMatch = userInfo.innerText.match(/@([\w_]+)/);
if (!screenNameMatch) return;
const screenName = screenNameMatch[1];
if (screenName.toLowerCase() === pathAuthor.toLowerCase()) return;
const tweetTextNode = article.querySelector('[data-testid="tweetText"]');
if (!tweetTextNode) return;
const textContent = tweetTextNode.innerText.toLowerCase();
for (const keyword of keywords) {
if (textContent.includes(keyword)) {
article.style.opacity = '0.2';
article.style.border = '1px solid red';
blockUser(screenName);
break;
}
}
}
// --- Mutation Observer ---
const observer = new MutationObserver((mutations) => {
if (!isRunning || keywords.length === 0) return;
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
const articles = node.tagName === 'ARTICLE' ? [node] : node.querySelectorAll('article[data-testid="tweet"]');
articles.forEach(article => {
if (!scannedTweets.has(article)) {
scannedTweets.add(article);
setTimeout(() => processTweet(article), 200);
}
});
}
});
});
});
// --- Init ---
function init() {
createUI();
observer.observe(document.body, { childList: true, subtree: true });
logMsg('Script initialized.');
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();