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.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

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