X.com Auto Comment Blocker

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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

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