Greasy Fork is available in English.
Fetches existing blocks, diffs with list, blocks concurrent. Refreshes tokens to fix 401 errors.
当前为
// ==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);
})();