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.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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