Greasy Fork

Greasy Fork is available in English.

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暴力猴,之后才能安装此脚本。

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

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

您需要先安装一个扩展,例如 篡改猴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);

})();