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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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