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.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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