X/Twitter Mass Blocker (v7 - Anti-401 & Auto-Sync)

Fetches existing blocks, diffs with list, blocks concurrent. Refreshes tokens to fix 401 errors.

当前为 2025-11-25 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         X/Twitter Mass Blocker (v7 - Anti-401 & Auto-Sync)
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  Fetches existing blocks, diffs with list, blocks concurrent. Refreshes tokens to fix 401 errors.
// @author       Haolong
// @match        https://x.com/*
// @match        https://twitter.com/*
// @connect      pluto0x0.github.io
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const LIST_URL = "https://pluto0x0.github.io/X_based_china/";
    // This is the standard public web client token. It rarely changes.
    const BEARER_TOKEN = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
    const COOLDOWN_TIME = 180000; // 3 minutes pause on 429 error

    // --- State ---
    let isPaused = false;
    let activeThreads = 0;
    let successCount = 0;
    let todoList = [];
    let concurrency = 2; // Conservative default
    let existingBlocks = new Set();
    let stopSignal = false;

    // --- Helpers ---
    // FIX FOR 401: We fetch the cookie freshly every time we need it.
    function getCsrfToken() {
        const match = document.cookie.match(/(^|;\s*)ct0=([^;]*)/);
        return match ? match[2] : null;
    }

    const sleep = (ms) => new Promise(r => setTimeout(r, ms));

    // --- UI Construction ---
    function createUI() {
        if (document.getElementById("xb-panel")) return;
        const panel = document.createElement('div');
        panel.id = "xb-panel";
        Object.assign(panel.style, {
            position: "fixed", bottom: "20px", left: "20px", zIndex: "99999",
            background: "rgba(10, 10, 10, 0.98)", color: "#e7e9ea", padding: "16px",
            borderRadius: "12px", width: "340px", fontFamily: "system-ui, -apple-system, sans-serif",
            border: "1px solid #333", boxShadow: "0 8px 32px rgba(0,0,0,0.6)"
        });

        panel.innerHTML = `
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
                <span style="font-weight:800;color:#f91880;font-size:14px;">Blocker v7 (Anti-401)</span>
                <span id="xb-threads-disp" style="font-size:10px;background:#333;padding:2px 6px;borderRadius:4px;">Threads: 2</span>
            </div>

            <div style="margin-bottom:12px;">
                <input type="range" id="xb-speed" min="1" max="5" value="2" style="width:100%;accent-color:#f91880;">
            </div>

            <div id="xb-log" style="height:120px;overflow-y:auto;background:#000;border:1px solid #333;padding:8px;font-size:11px;color:#888;margin-bottom:12px;border-radius:4px;font-family:monospace;white-space:pre-wrap;">Ready.</div>

            <div style="background:#333;height:6px;width:100%;margin-bottom:12px;border-radius:3px;overflow:hidden;">
                <div id="xb-bar" style="background:#f91880;height:100%;width:0%;transition:width 0.3s ease;"></div>
            </div>

            <div style="display:flex;gap:10px;">
                <button id="xb-btn" style="flex:1;padding:10px;background:#f91880;color:white;border:none;border-radius:20px;cursor:pointer;font-weight:bold;font-size:13px;">START</button>
                <button id="xb-stop" style="flex:0.4;padding:10px;background:#333;color:white;border:none;border-radius:20px;cursor:pointer;font-weight:bold;font-size:13px;">STOP</button>
            </div>
        `;
        document.body.appendChild(panel);

        document.getElementById("xb-btn").onclick = runFullProcess;
        document.getElementById("xb-stop").onclick = () => {
            stopSignal = true;
            log("🛑 Stopping...", "red");
            document.getElementById("xb-btn").disabled = false;
        };

        const slider = document.getElementById("xb-speed");
        slider.oninput = (e) => {
            concurrency = parseInt(e.target.value);
            document.getElementById("xb-threads-disp").innerText = `Threads: ${concurrency}`;
        };
    }

    function log(msg, color="#888") {
        const el = document.getElementById("xb-log");
        const time = new Date().toLocaleTimeString([], {hour12:false});
        el.innerHTML = `<div style="color:${color}"><span style="opacity:0.5">[${time}]</span> ${msg}</div>` + el.innerHTML;
    }

    function updateProgress(done, total) {
        if(total < 1) return;
        const pct = Math.floor((done / total) * 100);
        document.getElementById("xb-bar").style.width = `${pct}%`;
        document.getElementById("xb-btn").innerText = `${pct}% (${done})`;
    }

    // --- API Calls ---

    // Step 1: Get Existing Block List
    async function fetchExistingBlocks() {
        log("🔄 Syncing existing blocks...", "#1d9bf0");
        let cursor = -1;
        existingBlocks.clear();
        stopSignal = false;

        try {
            while (cursor !== 0 && cursor !== "0" && !stopSignal) {
                // Get fresh token
                const csrf = getCsrfToken();
                if (!csrf) throw new Error("Logged out");

                const url = `https://x.com/i/api/1.1/blocks/ids.json?count=5000&cursor=${cursor}&stringify_ids=true`;

                const res = await fetch(url, {
                    headers: {
                        "authorization": BEARER_TOKEN,
                        "x-csrf-token": csrf, // Fresh token
                        "x-twitter-active-user": "yes",
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json"
                    }
                });

                if (!res.ok) {
                    if(res.status === 401) throw new Error("401 Unauthorized - Please Re-login");
                    if(res.status === 429) {
                        log("⚠️ Sync Rate Limit. Waiting 30s...", "orange");
                        await sleep(30000);
                        continue;
                    }
                    throw new Error(`API Error ${res.status}`);
                }

                const data = await res.json();
                if (data.ids) data.ids.forEach(id => existingBlocks.add(String(id)));

                cursor = data.next_cursor_str;
                await sleep(250);
            }
            if(stopSignal) return false;
            log(`✅ Sync Complete: ${existingBlocks.size} blocked.`, "#00ba7c");
            return true;

        } catch (e) {
            log(`❌ Error syncing: ${e.message}`, "red");
            alert(e.message);
            return false;
        }
    }

    // --- Main Logic ---
    async function runFullProcess() {
        const btn = document.getElementById("xb-btn");
        btn.disabled = true;
        stopSignal = false;

        // 1. Check Login
        if (!getCsrfToken()) {
            log("❌ Error: You are logged out.", "red");
            btn.disabled = false;
            return;
        }

        // 2. Sync
        const syncSuccess = await fetchExistingBlocks();
        if (!syncSuccess) {
            btn.disabled = false;
            btn.innerText = "Retry";
            return;
        }

        // 3. Download GitHub List
        log("⬇️ Fetching target list...", "#1d9bf0");
        GM_xmlhttpRequest({
            method: "GET", url: LIST_URL,
            onload: async function(res) {
                if(res.status !== 200) {
                    log("❌ GitHub Download failed.", "red");
                    btn.disabled = false;
                    return;
                }

                const matches = [...res.responseText.matchAll(/ID:\s*(\d+)/g)];
                const githubIds = [...new Set(matches.map(m => m[1]))];

                // 4. Diffing
                todoList = githubIds.filter(id => !existingBlocks.has(id));
                const total = todoList.length;

                if (total === 0) {
                    log("🎉 All targets already blocked!", "#00ba7c");
                    updateProgress(1,1);
                    btn.disabled = false;
                    btn.innerText = "Done";
                    return;
                }

                log(`🎯 Targets: ${total} (Diff: ${githubIds.length - total} blocked)`, "#f91880");

                // 5. Start Manager
                startManager(total);
            }
        });
    }

    async function startManager(totalInitial) {
        let processedCount = 0; // Success + Failed

        while ((todoList.length > 0 || activeThreads > 0) && !stopSignal) {
            if (isPaused) { await sleep(1000); continue; }

            // Spawn workers
            while (activeThreads < concurrency && todoList.length > 0 && !isPaused && !stopSignal) {
                const uid = todoList.shift();
                processUser(uid, totalInitial);
            }

            await sleep(200);
        }

        document.getElementById("xb-btn").innerText = stopSignal ? "Stopped" : "Finished";
        document.getElementById("xb-btn").disabled = false;
        if(!stopSignal) log("🏁 Job Finished.", "#00ba7c");
    }

    async function processUser(uid, totalInitial) {
        activeThreads++;

        try {
            await sleep(Math.floor(Math.random() * 500) + 300);

            // Fetch Token IMMEDIATELY before request
            const csrf = getCsrfToken();
            if(!csrf) throw new Error("Logout detected");

            const res = await fetch("https://x.com/i/api/1.1/blocks/create.json", {
                method: "POST",
                headers: {
                    "authorization": BEARER_TOKEN,
                    "x-csrf-token": csrf, // KEY FIX for 401
                    "content-type": "application/x-www-form-urlencoded",
                    "x-twitter-active-user": "yes",
                    "x-twitter-auth-type": "OAuth2Session"
                },
                body: `user_id=${uid}`
            });

            if (res.ok || res.status === 200 || res.status === 403 || res.status === 404) {
                successCount++;
                if (successCount % 5 === 0) log(`Blocked: ${uid}`);
            } else if (res.status === 401) {
                // SESSION DIED
                log(`❌ 401 Unauthorized. Stopping.`, "red");
                stopSignal = true;
                alert("Session expired (401). Please reload the page and log in again.");
            } else if (res.status === 429) {
                if (!isPaused) {
                    isPaused = true;
                    log(`🛑 Rate Limit 429. Pausing 3m...`, "red");
                    setTimeout(() => {
                        isPaused = false;
                        log("🟢 Resuming...", "#00ba7c");
                    }, COOLDOWN_TIME);
                }
                todoList.push(uid); // Retry later
                successCount--;
            } else {
                log(`⚠️ ${res.status} on ${uid}`, "orange");
            }

        } catch (e) {
            log(`❌ Err: ${e.message}`, "red");
            if(e.message.includes("Logout")) stopSignal = true;
            else todoList.push(uid); // Retry net errors
            successCount--;
        }

        activeThreads--;
        updateProgress(successCount, totalInitial);
    }

    setTimeout(createUI, 1500);
    GM_registerMenuCommand("Open Blocker", createUI);

})();