X/Twitter Mass Blocker (v8.1 - Custom Page Crawler)

Crawls index.html, page2.html, page3.html... until 404. Syncs & Blocks.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         X/Twitter Mass Blocker (v8.1 - Custom Page Crawler)
// @namespace    http://tampermonkey.net/
// @version      8.1
// @description  Crawls index.html, page2.html, page3.html... until 404. Syncs & Blocks.
// @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 BASE_URL = "https://pluto0x0.github.io/X_based_china/";
    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;
    let existingBlocks = new Set();
    let stopSignal = false;

    // --- 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: "-apple-system, BlinkMacSystemFont, 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;">Mass Blocker v8.1</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:130px;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-speed").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})`;
    }

    // --- Helpers ---
    function getCsrfToken() {
        const match = document.cookie.match(/(^|;\s*)ct0=([^;]*)/);
        return match ? match[2] : null;
    }

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

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

        try {
            while (cursor !== 0 && cursor !== "0" && !stopSignal) {
                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,
                        "content-type": "application/json"
                    }
                });

                if (!res.ok) {
                    if(res.status === 429) {
                        log("⚠️ Sync 429. 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(200); 
            }
            if(stopSignal) return false;
            log(`✅ Sync Complete: ${existingBlocks.size} already blocked.`, "#00ba7c");
            return true;
        } catch (e) {
            log(`❌ Sync Error: ${e.message}`, "red");
            return false;
        }
    }

    // --- Step 2: Crawl (Corrected URL Pattern) ---
    function fetchUrlText(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: (res) => {
                    if (res.status === 200) resolve(res.responseText);
                    else reject(res.status);
                },
                onerror: () => reject("Network Error")
            });
        });
    }

    async function crawlTargetList() {
        let allFoundIds = new Set();
        let page = 1;
        let keepCrawling = true;
        
        log("🕸️ Starting Crawler...", "#1d9bf0");

        while(keepCrawling && !stopSignal) {
            // URL Logic: Page 1 = index.html, Page 2 = page2.html
            let url = (page === 1) 
                ? `${BASE_URL}index.html` 
                : `${BASE_URL}page${page}.html`;
            
            document.getElementById("xb-btn").innerText = `Scan Pg ${page}`;

            try {
                const html = await fetchUrlText(url);
                
                // Extract IDs
                const matches = [...html.matchAll(/ID:\s*(\d+)/g)];
                const idsOnPage = matches.map(m => m[1]);

                if (idsOnPage.length === 0) {
                    log(`⚠️ Page ${page} loaded but no IDs found.`);
                } else {
                    idsOnPage.forEach(id => allFoundIds.add(id));
                }

                page++;
                await sleep(200); // Gentle crawl delay

            } catch (err) {
                if (err === 404) {
                    log(`✅ End of list at Page ${page-1}.`, "#00ba7c");
                    keepCrawling = false; // Stop on 404
                } else {
                    log(`❌ Crawl Error Pg ${page}: ${err}`, "red");
                    keepCrawling = false;
                }
            }
        }
        return [...allFoundIds];
    }

    // --- Main Logic ---
    async function runFullProcess() {
        const btn = document.getElementById("xb-btn");
        btn.disabled = true;
        stopSignal = false;
        
        // 1. Check Login
        if (!getCsrfToken()) {
            log("❌ Error: Logged out.", "red");
            btn.disabled = false;
            return;
        }

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

        // 3. Crawl Pages
        const githubIds = await crawlTargetList();
        if (!githubIds || githubIds.length === 0) {
            log("❌ No IDs found during crawl.", "red");
            btn.disabled = false;
            return;
        }

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

        log(`📄 Found: ${githubIds.length} total.`);
        log(`⏭️ Skipped: ${blockedAlready} (Already blocked).`);
        
        if (total === 0) {
            log("🎉 All targets are already blocked!", "#00ba7c");
            updateProgress(1,1);
            btn.disabled = false;
            btn.innerText = "Done";
            return;
        }

        log(`🎯 <b>Queued to Block: ${total}</b>`, "#f91880");
        
        // 5. Start Blocking
        startManager(total);
    }

    async function startManager(totalInitial) {
        let processedCount = 0; 

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

            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");
        alert("Batch Complete");
    }

    async function processUser(uid, totalInitial) {
        activeThreads++;
        try {
            await sleep(Math.floor(Math.random() * 500) + 300);

            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,
                    "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) {
                log(`❌ 401 Session Died. Stopping.`, "red");
                stopSignal = true; 
            } 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); 
                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);
            successCount--;
        }

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

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

})();