您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
当拉黑作者时,自动拉黑所有转推者和回复者。支持自动拉黑(可根据长度、中文和斜杠数量等规则)、黑名单、白名单管理及详细配置。
当前为
// ==UserScript== // @name X.com Chain Blocker // @name:zh-CN X.com 九族拉黑 // @namespace http://tampermonkey.net/ // @version 2.0 // @description Block author, retweeters, repliers, and auto-block users based on rules. Manage block log, whitelist, and settings in a panel. // @description:zh-CN 当拉黑作者时,自动拉黑所有转推者和回复者。支持自动拉黑(可根据长度、中文和斜杠数量等规则)、黑名单、白名单管理及详细配置。 // @author Gemini 2.5 Pro // @license MIT // @match *://x.com/* // @match *://twitter.com/* // @exclude *://x.com/settings* // @exclude *://twitter.com/settings* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect api.x.com // @connect x.com // ==/UserScript== (function () { 'use strict'; // --- CONFIG & CONSTANTS --- const MENU_ITEM_TEXT = "九族拉黑"; const STORAGE_KEY = 'CHAIN_BLOCKER_DATA'; const CONFIG_STORAGE_KEY = 'CHAIN_BLOCKER_CONFIG'; const BLOCK_INTERVAL_MS = 10 * 1000; const PROCESS_CHECK_INTERVAL_MS = 5 * 1000; const USERNAME_LENGTH_THRESHOLD = 25; const AUTO_SCAN_INTERVAL_MS = 2000; const API_RETRY_DELAY_MS = 5 * 60 * 1000; let currentUserId = null, currentUserScreenName = null, activeTweetArticle = null; let isProcessingQueue = false, processIntervalId = null, apiLimitCountdownInterval = null; let scriptConfig = {}, isConfigPanelBusy = false; // --- STYLES --- GM_addStyle(`.nuke-toast{position:fixed;top:20px;right:20px;z-index:100000;background-color:#15202b;color:white;padding:10px 15px;border-radius:12px;border:1px solid #38444d;box-shadow:0 4px 12px rgba(0,0,0,0.4);width:auto;max-width:350px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;transition:all .5s ease-out;opacity:1;transform:translateX(0)}.nuke-toast.fading-out{opacity:0;transform:translateX(20px)}.nuke-toast-title{font-weight:bold;margin-bottom:8px;font-size:16px}.nuke-toast-status{font-size:14px;margin-bottom:0;line-height:1.5}#nuke-status-toast{background-color:#253341}#nuke-api-limit-toast{background-color:#d9a100;color:#15202b;border-color:#ffc107}.nuke-config-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100001;background-color:#15202b;color:white;border-radius:16px;border:1px solid #38444d;box-shadow:0 8px 24px rgba(0,0,0,0.5);width:550px;max-width:90vw;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}.nuke-panel-header{display:flex;align-items:center;justify-content:space-between;height:53px;padding:0 16px;border-bottom:1px solid #38444d}.nuke-header-item{flex-basis:56px;display:flex;align-items:center}.nuke-header-item.left{justify-content:flex-start}.nuke-header-item.right{justify-content:flex-end}.nuke-config-title{font-weight:bold;font-size:20px;flex-grow:1;text-align:center}.nuke-close-button{background:0 0;border:0;padding:0;cursor:pointer;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:9999px;transition:background-color .2s ease-in-out}.nuke-close-button:hover{background-color:rgba(239,243,244,0.1)}.nuke-close-button svg{fill:white;width:20px;height:20px}.nuke-panel-content{padding:16px}.nuke-config-textarea{width:100%;height:150px;background-color:#253341;border:1px solid #38444d;border-radius:8px;color:white;padding:10px;font-size:14px;resize:vertical;box-sizing:border-box;margin-bottom:15px}.nuke-config-button-container{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}.nuke-config-button.save{background-color:#eff3f4;color:#0f1419;padding:8px 16px;border-radius:20px;border:none;font-weight:bold;cursor:pointer;transition:background-color .2s}.nuke-config-button.save:hover{background-color:#d7dbdc}.nuke-config-tabs{display:flex;border-bottom:1px solid #38444d;margin-bottom:15px}.nuke-config-tab{background:0 0;border:none;color:#8899a6;padding:10px 15px;cursor:pointer;font-size:15px;font-weight:700;flex-grow:1;transition:background-color .2s}.nuke-config-tab:hover{background-color:rgba(239,243,244,0.1)}.nuke-config-tab.active{color:#1d9bf0;border-bottom:2px solid #1d9bf0;margin-bottom:-1px}.nuke-config-tab-content{animation:fadeIn .3s ease-in-out;padding-top:10px}.nuke-config-tab-content.hidden{display:none}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.nuke-list{max-height:280px;overflow-y:auto;padding-right:10px}.nuke-list-search{width:100%;background-color:#253341;border:1px solid #38444d;border-radius:8px;color:white;padding:8px 12px;font-size:14px;box-sizing:border-box;margin-bottom:10px}.nuke-list-entry{display:flex;justify-content:space-between;align-items:center;padding:8px 5px;border-bottom:1px solid #253341}.nuke-list-user-info{display:flex;flex-direction:column;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px}.nuke-list-user-name{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nuke-list-user-handle{color:#8899a6;font-size:14px;cursor:pointer}.nuke-list-user-handle:hover{text-decoration:underline}.nuke-list-actions{font-size:12px;color:#8899a6;white-space:nowrap;cursor:pointer}.nuke-list-actions:hover{color:#1d9bf0}.nuke-list-user-info a{color:inherit;text-decoration:none}.nuke-list-user-info a:hover .nuke-list-user-name{text-decoration:underline}.nuke-setting-item{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px}.nuke-setting-item label{font-size:14px;margin-right:10px}.nuke-setting-item input[type=number]{width:80px;background-color:#253341;border:1px solid #38444d;border-radius:8px;color:white;padding:5px 8px;font-size:14px}.nuke-setting-item input[type=checkbox]{height:20px;width:20px;accent-color:#1d9bf0}`); // --- CONFIGURATION MANAGEMENT --- async function loadConfig() { const defaultConfig = { autoBlockEnabled: true, autoBlockUrls: ['https://x.com/*/status/*', 'https://x.com/search*'], blockLogLimit: 500 }; scriptConfig = await GM_getValue(CONFIG_STORAGE_KEY, defaultConfig); return scriptConfig; } async function saveConfig(config) { await GM_setValue(CONFIG_STORAGE_KEY, config); scriptConfig = config; } function updateMenuCommands() { GM_registerMenuCommand('配置与记录', showConfigPanel); } async function showConfigPanel() { if (isConfigPanelBusy) return; isConfigPanelBusy = true; try { if (document.getElementById('nuke-url-config-panel')?.remove()) return; let config = await loadConfig(); const panel = document.createElement('div'); panel.id = 'nuke-url-config-panel'; panel.className = 'nuke-config-panel'; panel.innerHTML = ` <div class="nuke-panel-header"> <div class="nuke-header-item left"> <button class="nuke-close-button" aria-label="关闭"><svg viewBox="0 0 24 24"><g><path d="M10.59 12L4.54 5.96l1.42-1.42L12 10.59l6.04-6.05 1.42 1.42L13.41 12l6.05 6.04-1.42 1.42L12 13.41l-6.04 6.05-1.42-1.42L10.59 12z"></path></g></svg></button> </div> <h2 class="nuke-config-title">配置与记录</h2> <div class="nuke-header-item right"></div> </div> <div class="nuke-panel-content"> <div class="nuke-config-tabs"> <button class="nuke-config-tab active" data-tab="settings">⚙️ 设置</button> <button class="nuke-config-tab" data-tab="log">📓 拉黑记录</button> <button class="nuke-config-tab" data-tab="whitelist">🛡️ 白名单</button> </div> <div id="nuke-settings-content" class="nuke-config-tab-content"> <div class="nuke-setting-item"> <label for="nuke-auto-block-toggle">自动拉黑超长中文用户名</label> <input type="checkbox" id="nuke-auto-block-toggle"> </div> <div class="nuke-setting-item"> <label for="nuke-log-limit-input">拉黑记录最大条数 (0为不限制)</label> <input type="number" id="nuke-log-limit-input" min="0" step="100"> </div> <p style="font-size: 14px; color: #8899a6; margin-top: 10px; margin-bottom: 10px;">自动拉黑生效的页面 URL (每行一条, 支持*通配符):</p> <textarea class="nuke-config-textarea"></textarea> <div class="nuke-config-button-container"> <button class="nuke-config-button save">保存设置</button> </div> </div> <div id="nuke-log-content" class="nuke-config-tab-content hidden"> <input type="search" class="nuke-list-search" id="nuke-log-search" placeholder="搜索记录 (用户名, @handle, ID)..."> <div class="nuke-list"></div> </div> <div id="nuke-whitelist-content" class="nuke-config-tab-content hidden"> <input type="search" class="nuke-list-search" id="nuke-whitelist-search" placeholder="搜索白名单 (用户名, @handle, ID)..."> <div class="nuke-list"></div> </div> </div>`; document.body.appendChild(panel); panel.querySelector('#nuke-auto-block-toggle').checked = config.autoBlockEnabled; panel.querySelector('#nuke-log-limit-input').value = config.blockLogLimit; panel.querySelector('.nuke-config-textarea').value = config.autoBlockUrls.join('\n'); const setActiveTab = (tabName) => { panel.querySelectorAll('.nuke-config-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tabName)); panel.querySelectorAll('.nuke-config-tab-content').forEach(c => c.classList.toggle('hidden', c.id !== `nuke-${tabName}-content`)); }; panel.querySelectorAll('.nuke-config-tab').forEach(tab => tab.addEventListener('click', () => setActiveTab(tab.dataset.tab))); panel.querySelector('.nuke-close-button').addEventListener('click', () => panel.remove()); panel.querySelector('.nuke-config-button.save').addEventListener('click', async () => { config.autoBlockEnabled = panel.querySelector('#nuke-auto-block-toggle').checked; config.blockLogLimit = parseInt(panel.querySelector('#nuke-log-limit-input').value, 10) || 500; config.autoBlockUrls = panel.querySelector('.nuke-config-textarea').value.split('\n').map(url => url.trim()).filter(Boolean); await saveConfig(config); showToast('nuke-config-toast', '设置已更新', '配置已成功保存', 3000); }); panel.querySelector('#nuke-log-search').addEventListener('input', renderListsInPanel); panel.querySelector('#nuke-whitelist-search').addEventListener('input', renderListsInPanel); await renderListsInPanel(); } finally { setTimeout(() => { isConfigPanelBusy = false; }, 200); } } async function renderListsInPanel() { const userData = await loadUserData(); if (!userData) return; const logSearchTerm = document.getElementById('nuke-log-search')?.value.toLowerCase() || ''; const whitelistSearchTerm = document.getElementById('nuke-whitelist-search')?.value.toLowerCase() || ''; const filterUsers = (user, term) => { if (!term) return true; const userId = String(user.userId || ''); const screenName = user.screenName?.toLowerCase() || ''; const userNameText = user.userNameText?.toLowerCase() || ''; return userId.includes(term) || screenName.includes(term) || userNameText.includes(term); }; const renderList = (containerSelector, list, type) => { const container = document.querySelector(containerSelector); if (!container) return; const searchTerm = type === 'log' ? logSearchTerm : whitelistSearchTerm; const filteredList = list.filter(user => filterUsers(user, searchTerm)); container.innerHTML = ''; if (filteredList.length === 0) { const message = searchTerm ? '没有找到匹配的用户' : (type === 'log' ? '暂无拉黑记录' : '白名单为空'); container.innerHTML = `<p style="color:#8899a6;text-align:center;padding:20px 0;">${message}</p>`; return; } filteredList.slice().reverse().forEach(entry => { const el = document.createElement('div'); el.className = 'nuke-list-entry'; const userName = entry.userNameText || entry.screenName || String(entry.userId); const screenNameHandle = entry.screenName ? `@${entry.screenName}` : ''; const userLinkHTML = entry.screenName ? `<a href="https://x.com/${entry.screenName}" target="_blank" rel="noopener noreferrer" title="在新标签页中打开"><span class="nuke-list-user-name">${userName}</span></a>` : `<span class="nuke-list-user-name">${userName}</span>`; if (type === 'log') { const timestamp = entry.blockTimestamp ? new Date(entry.blockTimestamp).toLocaleString() : '未知时间'; el.innerHTML = `<div class="nuke-list-user-info">${userLinkHTML}<span class="nuke-list-user-handle" title="移至白名单并取消拉黑">${screenNameHandle}</span></div><span class="nuke-list-actions" title="从记录中移除">${timestamp}</span>`; if (entry.screenName) { el.querySelector('.nuke-list-user-handle')?.addEventListener('click', () => moveUser(entry, 'logToWhitelist')); } else { const userNameEl = el.querySelector('.nuke-list-user-name'); if (userNameEl) { userNameEl.style.cursor = 'pointer'; userNameEl.title = '移至白名单并取消拉黑'; userNameEl.addEventListener('click', () => moveUser(entry, 'logToWhitelist')); } } el.querySelector('.nuke-list-actions')?.addEventListener('click', () => moveUser(entry, 'removeFromLog')); } else { el.innerHTML = `<div class="nuke-list-user-info">${userLinkHTML}<span class="nuke-list-user-handle">${screenNameHandle}</span></div><span class="nuke-list-actions" title="从白名单中移除">移除</span>`; el.querySelector('.nuke-list-actions')?.addEventListener('click', () => moveUser(entry, 'removeFromWhitelist')); } container.appendChild(el); }); }; renderList('#nuke-log-content .nuke-list', userData.blockedLog, 'log'); renderList('#nuke-whitelist-content .nuke-list', userData.whitelist, 'whitelist'); } async function moveUser(user, action) { const userData = await loadUserData(); if (!userData) return; const logIndex = userData.blockedLog.findIndex(u => u.userId === user.userId); const whitelistIndex = userData.whitelist.findIndex(u => u.userId === user.userId); let success = false; try { if (action === 'logToWhitelist') { if (logIndex > -1) { await unblockUserById(user.userId); const [movedUser] = userData.blockedLog.splice(logIndex, 1); if (whitelistIndex === -1) userData.whitelist.push(movedUser); success = true; } } else if (action === 'removeFromLog') { if (logIndex > -1) { userData.blockedLog.splice(logIndex, 1); success = true; } } else if (action === 'removeFromWhitelist') { if (whitelistIndex > -1) { userData.whitelist.splice(whitelistIndex, 1); success = true; } } if(success) { await saveUserData(userData); await renderListsInPanel(); } } catch(err) { console.error(`[CB] ${action} failed for ${user.screenName || user.userId}:`, err); showToast('nuke-feedback-toast', '❌ 操作失败', `无法为 @${user.screenName || user.userId} 执行操作`, 4000); } } // --- API & HELPERS --- const API_ENDPOINTS = { UserByScreenName: { hash: 'jUKA--0QkqGIFhmfRZdWrQ', features: {"responsive_web_grok_bio_auto_translation_is_enabled":false,"hidden_profile_subscriptions_enabled":true,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"subscriptions_verification_info_is_identity_verified_enabled":true,"subscriptions_verification_info_verified_since_enabled":true,"highlights_tweets_tab_ui_enabled":true,"responsive_web_twitter_article_notes_tab_enabled":true,"subscriptions_feature_can_gift_premium":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true} }, UserByRestId: { hash: 'tD4_0f_p354q1Yin156s2Q', features: {"responsive_web_grok_bio_auto_translation_is_enabled":false,"hidden_profile_subscriptions_enabled":true,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"subscriptions_verification_info_is_identity_verified_enabled":true,"subscriptions_verification_info_verified_since_enabled":true,"highlights_tweets_tab_ui_enabled":true,"responsive_web_twitter_article_notes_tab_enabled":true,"subscriptions_feature_can_gift_premium":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true} }, Retweeters: { hash: 'DmC_H6eV_XMiL0g4ltJvpg', features: {"rweb_video_screen_enabled":false,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"premium_content_api_read_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"responsive_web_grok_analyze_button_fetch_trends_enabled":false,"responsive_web_grok_analyze_post_followups_enabled":true,"responsive_web_jetfuel_frame":false,"responsive_web_grok_share_attachment_enabled":true,"articles_preview_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"responsive_web_grok_show_grok_translated_post":false,"responsive_web_grok_analysis_button_from_backend":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_grok_image_annotation_enabled":true,"responsive_web_enhance_cards_enabled":false} }, TweetDetail: { hash: '-0WTL1e9Pij-JWAF5ztCCA', features: {"rweb_video_screen_enabled":false,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"premium_content_api_read_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"responsive_web_grok_analyze_button_fetch_trends_enabled":false,"responsive_web_grok_analyze_post_followups_enabled":true,"responsive_web_jetfuel_frame":false,"responsive_web_grok_share_attachment_enabled":true,"articles_preview_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"responsive_web_grok_show_grok_translated_post":false,"responsive_web_grok_analysis_button_from_backend":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_grok_image_annotation_enabled":true,"responsive_web_enhance_cards_enabled":false} } }; function makeApiRequest(url, method = "GET", data = null) { return new Promise((resolve, reject) => GM_xmlhttpRequest({ method, url, data, headers: { Authorization: `Bearer ${getAuthToken()}`, "Content-Type": "application/x-www-form-urlencoded", "x-csrf-token": getCsrfToken() }, onload: r => r.status >= 200 && r.status < 300 ? resolve(r.responseText ? JSON.parse(r.responseText) : null) : reject({ message: `API请求失败: ${r.status}`, status: r.status }), onerror: e => reject({ message: "Network or script error", error: e }) })); } function getCsrfToken() { const e = document.cookie.split("; ").find(e => e.startsWith("ct0=")); return e ? e.split("=")[1] : null; } function getAuthToken() { return "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; } async function getUserDataByScreenName(screenName) { const endpoint = API_ENDPOINTS.UserByScreenName; const url = `https://x.com/i/api/graphql/${endpoint.hash}/UserByScreenName?variables=${encodeURIComponent(JSON.stringify({screen_name:screenName,withSafetyModeUserFields:true}))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`; const data = await makeApiRequest(url); if (data?.data?.user?.result) return data.data.user.result; throw new Error(`无法找到用户 @${screenName} 的数据`); } async function getUserDataById(userId) { const endpoint = API_ENDPOINTS.UserByRestId; const url = `https://x.com/i/api/graphql/${endpoint.hash}/UserByRestId?variables=${encodeURIComponent(JSON.stringify({userId,withSafetyModeUserFields:true}))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`; const data = await makeApiRequest(url); if (data?.data?.user?.result) return data.data.user.result; throw new Error(`无法找到用户 ID: ${userId} 的数据`); } async function getRetweetersData(tweetId, onProgress) { let users = new Map(), cursor = null, endpoint = API_ENDPOINTS.Retweeters; do { onProgress(`正在获取转推列表...(已找到: ${users.size})`); const url = `https://x.com/i/api/graphql/${endpoint.hash}/Retweeters?variables=${encodeURIComponent(JSON.stringify({tweetId,count:100,cursor,includePromotedContent:true}))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`; const data = await makeApiRequest(url); const entries = data?.data?.retweeters_timeline?.timeline?.instructions?.find(i=>i.type==='TimelineAddEntries')?.entries; if (!entries) break; let foundNewUsers = false; for (const entry of entries) { if (entry.entryId.startsWith('user-')) { const userResult = entry.content?.itemContent?.user_results?.result; if (userResult?.rest_id && !users.has(userResult.rest_id)) { users.set(userResult.rest_id, userResult); foundNewUsers = true; } } else if (entry.entryId.startsWith('cursor-bottom-')) { cursor = entry.content.value; } } if (!foundNewUsers || !cursor) break; } while (cursor); return Array.from(users.values()); } async function getRepliersData(tweetId, onProgress) { let users = new Map(), cursor = null, endpoint = API_ENDPOINTS.TweetDetail; const baseVariables = {"with_rux_injections":false,"includePromotedContent":true,"withCommunity":true,"withQuickPromoteEligibilityTweetFields":true,"withBirdwatchNotes":true,"withVoice":true,"withV2Timeline":true}; do { onProgress(`正在获取回复列表...(已找到: ${users.size})`); const variables = {...baseVariables, focalTweetId: tweetId, cursor, count: 40, rankingMode:"Relevance"}; const url = `https://x.com/i/api/graphql/${endpoint.hash}/TweetDetail?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`; const data = await makeApiRequest(url); const instructions = data?.data?.threaded_conversation_with_injections_v2?.instructions || []; const entriesInstruction = instructions.find(i => i.type === 'TimelineAddEntries'); const entries = entriesInstruction?.entries; if (!entries) break; let nextCursor = null; for (const entry of entries) { if (entry.entryId.startsWith('conversationthread-')) { const threadItems = entry.content?.items; if(threadItems && Array.isArray(threadItems)){ for(const item of threadItems){ const userResult = item.item?.itemContent?.tweet_results?.result?.core?.user_results?.result; if (userResult?.rest_id && !users.has(userResult.rest_id)) { users.set(userResult.rest_id, userResult); } } } } else if (entry.entryId.startsWith('tweet-')) { const userResult = entry.content?.itemContent?.tweet_results?.result?.core?.user_results?.result; if (userResult?.rest_id && !users.has(userResult.rest_id)) { users.set(userResult.rest_id, userResult); } } else if (entry.entryId.startsWith('cursor-bottom-')) { nextCursor = entry.content.value; } } if (cursor === nextCursor) break; cursor = nextCursor; } while (cursor); return Array.from(users.values()); } async function blockUserById(userId) { return makeApiRequest("https://x.com/i/api/1.1/blocks/create.json", "POST", `user_id=${userId}`); } async function unblockUserById(userId) { return makeApiRequest("https://x.com/i/api/1.1/blocks/destroy.json", "POST", `user_id=${userId}`); } // --- DATA & QUEUE MANAGEMENT --- async function loadUserData() { if (!currentUserId) return null; const allData = await GM_getValue(STORAGE_KEY, {}); let userData = allData[currentUserId]; if (!userData || typeof userData !== 'object') userData = { queue: [], blockedLog: [], whitelist: [] }; if (!Array.isArray(userData.queue)) userData.queue = []; if (!Array.isArray(userData.blockedLog)) userData.blockedLog = []; if (!Array.isArray(userData.whitelist)) userData.whitelist = []; return { ...userData, lastBlockTimestamp: 0 }; } async function saveUserData(data) { if (!currentUserId) return; const allData = await GM_getValue(STORAGE_KEY, {}); allData[currentUserId] = data; await GM_setValue(STORAGE_KEY, allData); } // --- UI & FEEDBACK --- function showToast(id, title, status, duration = null) { let toast = document.getElementById(id); if (!toast) { toast = document.createElement('div'); toast.id = id; toast.className = 'nuke-toast'; document.body.appendChild(toast); } const existingToasts = document.querySelectorAll('.nuke-toast:not([style*="display: none"])'); toast.style.top = `${20 + (existingToasts.length - 1) * 70}px`; toast.classList.remove('fading-out'); toast.innerHTML = `<div class="nuke-toast-title">${title}</div><div class="nuke-toast-status">${status}</div>`; const reorderToasts = () => { const remainingToasts = Array.from(document.querySelectorAll('.nuke-toast')).filter(t => t.id !== id); remainingToasts.forEach((t, index) => { t.style.top = `${20 + index * 70}px`; }); }; if (duration) { setTimeout(() => { toast.classList.add('fading-out'); setTimeout(() => { toast.remove(); reorderToasts(); }, 500); }, duration); } } async function updateStatusToast() { const userData = await loadUserData(); if (!userData || userData.queue.length === 0) { let toast = document.getElementById('nuke-status-toast'); if (toast) { toast.classList.add('fading-out'); setTimeout(() => toast.remove(), 500); } return; } showToast('nuke-status-toast', `🚀 九族拉黑队列(@${currentUserScreenName||'...'})`, `<b>待处理:</b> ${userData.queue.length}<br><b>已拉黑:</b> ${userData.blockedLog.length || 0}`); } function hideElement(element) { if (!element) return; element.style.cssText += 'transition:all .4s ease-out;max-height:0;opacity:0;padding:0;margin:0;border-width:0;'; setTimeout(() => element.remove(), 400); } // --- CORE LOGIC --- async function processQueue() { if (isProcessingQueue || !currentUserId) return; const userData = await loadUserData(); if (!userData || userData.queue.length === 0 || (Date.now() - userData.lastBlockTimestamp < BLOCK_INTERVAL_MS)) return; isProcessingQueue = true; let userToBlock = userData.queue[0]; try { if (!userToBlock.screenName || !userToBlock.userNameText) { try { const fullUserData = await getUserDataById(userToBlock.userId); userToBlock.screenName = fullUserData.core?.screen_name || fullUserData.legacy?.screen_name; userToBlock.userNameText = fullUserData.core?.name || fullUserData.legacy?.name; } catch (fetchError) { console.warn(`[CB] 获取用户 ${userToBlock.userId} 的详细信息失败,将使用现有数据继续。`, fetchError); } } await blockUserById(userToBlock.userId); userData.queue.shift(); userData.blockedLog.push({ ...userToBlock, blockTimestamp: Date.now() }); const limit = scriptConfig.blockLogLimit || 500; if (limit > 0) { while (userData.blockedLog.length > limit) userData.blockedLog.shift(); } userData.lastBlockTimestamp = Date.now(); } catch (error) { console.error(`[Chain Blocker] 拉黑 @${userToBlock.screenName || userToBlock.userId} 失败,移除.`, error); userData.queue.shift(); } finally { await saveUserData(userData); await updateStatusToast(); isProcessingQueue = false; } } async function startNukeProcess(tweetArticle) { showToast('nuke-feedback-toast', '🚀 九族拉黑已启动', '正在处理...', 2000); hideElement(tweetArticle); try { const userLink = tweetArticle.querySelector('div[data-testid="User-Name"] a[role="link"]'); const authorHandle = userLink?.href.split('/').pop(); const authorUserNameText = tweetArticle.querySelector('div[data-testid="User-Name"] a[role="link"] span')?.textContent?.trim() || authorHandle; if (!authorHandle) throw new Error("无法确定作者 handle"); const userData = await loadUserData(); if (!userData) throw new Error("无法加载用户数据"); const whitelistIds = new Set(userData.whitelist.map(u => u.userId)); let authorId = null; try { const authorData = await getUserDataByScreenName(authorHandle); authorId = authorData?.rest_id; if (!authorId) throw new Error(`无法获取 @${authorHandle} 的用户ID`); if (whitelistIds.has(authorId)) { showToast(`nuke-feedback-toast-${Date.now()}`, '🛡️ 作者在白名单中', `已跳过拉黑 @${authorHandle}`, 4000); } else { await blockUserById(authorId); userData.blockedLog.push({ userId: authorId, screenName: authorHandle, userNameText: authorUserNameText, blockTimestamp: Date.now() }); const limit = scriptConfig.blockLogLimit || 500; if (limit > 0) { while (userData.blockedLog.length > limit) userData.blockedLog.shift(); } await saveUserData(userData); showToast(`nuke-feedback-toast-${Date.now()}`, '✅ 作者已拉黑并记录', `已立刻拉黑 @${authorHandle}`, 4000); } } catch (authorError) { console.error(`[CB] 拉黑作者 @${authorHandle} 失败:`, authorError); } const tweetId = Array.from(tweetArticle.querySelectorAll('a')).find(a=>a.href.includes('/status/'))?.href.match(/\/status\/(\d+)/)?.[1]; if (!tweetId) return; const [retweeters, repliers] = await Promise.all([ getRetweetersData(tweetId, status => showToast(`nuke-fetch-toast-${Date.now()}`, '收集中...', status, 4000)), getRepliersData(tweetId, status => showToast(`nuke-fetch-toast-${Date.now()}`, '收集中...', status, 4000)) ]); const combinedUsers = new Map(); [...retweeters, ...repliers].forEach(u => u.rest_id && combinedUsers.set(u.rest_id, u)); if (authorId) combinedUsers.delete(authorId); const existingUserIds = new Set([...userData.queue.map(u=>u.userId), ...userData.blockedLog.map(u=>u.userId), ...whitelistIds]); const newUsersToQueue = Array.from(combinedUsers.values()).map(u => ({ userId: u.rest_id, screenName: u.core?.screen_name || u.legacy?.screen_name, userNameText: u.core?.name || u.legacy?.name })).filter(u => u.userId && u.userId !== currentUserId && !existingUserIds.has(u.userId)); if (newUsersToQueue.length > 0) { userData.queue.push(...newUsersToQueue); await saveUserData(userData); showToast(`nuke-feedback-toast-${Date.now()}`, '✅ 操作成功', `已将 ${newUsersToQueue.length} 个相关用户加入拉黑队列。`, 4000); } else { showToast(`nuke-feedback-toast-${Date.now()}`, 'ℹ️ 操作完成', `没有找到新的可拉黑用户。`, 4000); } await updateStatusToast(); setTimeout(processQueue, 1000); } catch (error) { console.error("[CB] 收集过程中发生错误:", error); showToast(`nuke-feedback-toast-${Date.now()}`, '❌ 发生错误', error.message, 5000); } } async function addSingleUserToBlockQueue(screenName, userNameText) { try { const userData = await loadUserData(); if (!userData) throw new Error("无法加载用户数据"); const whitelistIds = new Set(userData.whitelist.map(u => u.screenName.toLowerCase())); if(whitelistIds.has(screenName.toLowerCase())) return; const targetUser = await getUserDataByScreenName(screenName); const targetUserId = targetUser?.rest_id; if (!targetUserId) throw new Error(`无法获取用户ID @${screenName}`); const isProcessed = userData.queue.some(u => u.userId === targetUserId) || userData.blockedLog.some(u => u.userId === targetUserId) || userData.whitelist.some(u=>u.userId === targetUserId); if (targetUserId === currentUserId || isProcessed) return; userData.queue.push({ userId: targetUserId, screenName, userNameText: targetUser.legacy?.name || userNameText }); await saveUserData(userData); await updateStatusToast(); setTimeout(processQueue, 1000); } catch (error) { console.error(`[CB] 添加 @${screenName} 到队列失败:`, error); } } // --- UI SCANNING & AUTOMATION --- function isUrlMatch(url, patterns) { return patterns.some(p => new RegExp('^' + p.replace(/\*/g, '.*') + '$').test(url)); } function scanAndProcessContent() { document.querySelectorAll('div[data-testid="cellInnerDiv"]:not([style*="display: none"]) button[data-testid$="-unblock"]').forEach(btn => btn.closest('div[data-testid="cellInnerDiv"]').style.display = 'none'); if (!currentUserId || !scriptConfig.autoBlockEnabled || !isUrlMatch(window.location.href, scriptConfig.autoBlockUrls)) return; const checkUserName = (userNameText) => { if (!userNameText) return false; const hasChinese = /[\u4e00-\u9fa5]/.test(userNameText); const slashCount = (userNameText.match(/\//g) || []).length; return userNameText.length > USERNAME_LENGTH_THRESHOLD && hasChinese && slashCount >= 2; }; document.querySelectorAll('article[data-testid="tweet"]:not([data-autoblock-checked])').forEach(article => { article.dataset.autoblockChecked = 'true'; const userNameText = article.querySelector('div[data-testid="User-Name"] a[role="link"] span')?.textContent?.trim(); if (checkUserName(userNameText)) { showToast(`nuke-auto-trigger-toast-${Date.now()}`, '🤖 自动执行九族拉黑', `检测到可疑用户名: ${userNameText.substring(0,20)}...`, 4000); startNukeProcess(article); } }); document.querySelectorAll('button[data-testid="UserCell"]:not([data-autoblock-checked])').forEach(cell => { cell.dataset.autoblockChecked = 'true'; const userNameText = cell.querySelector('a[role="link"] div[dir="ltr"] > span')?.textContent?.trim(); if (checkUserName(userNameText)) { const screenName = cell.querySelector('a[role="link"]')?.href.split('/').pop(); if (screenName) { showToast(`nuke-auto-trigger-toast-${Date.now()}`, '🤖 自动执行拉黑', `检测到可疑用户名: @${screenName}`, 4000); addSingleUserToBlockQueue(screenName, userNameText); hideElement(cell.closest('div[data-testid="cellInnerDiv"]')); } } }); } function addNukeButton(menuNode) { if (menuNode.querySelector('.nuke-button')) return; const blockMenuItem = Array.from(menuNode.querySelectorAll('div[role="menuitem"]')).find(el => el.textContent.includes('@')); if (!blockMenuItem) return; const nukeButton = blockMenuItem.cloneNode(true); nukeButton.classList.add('nuke-button'); const span = nukeButton.querySelector('span'); if (span) { span.textContent = MENU_ITEM_TEXT; span.style.color = 'rgb(244, 33, 46)'; } const biohazardIconPath = "M19.5,12c0,2.9-1.6,5.5-4,6.8V21h-7v-2.2c-2.4-1.3-4-3.9-4-6.8c0-4.1,3.4-7.5,7.5-7.5S19.5,7.9,19.5,12z M12,6c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S14.2,6,12,6z M12,14c-1.1,0-2-0.9-2-2c0-0.4,0.1-0.7,0.3-1H10v-2h1.3c-0.2-0.3-0.3-0.6-0.3-1c0-1.1,0.9-2,2-2s2,0.9,2,2c0,0.4-0.1,0.7-0.3,1H14v2h-1.3c0.2,0.3,0.3,0.6,0.3,1C14,13.1,13.1,14,12,14z"; const svgIcon = nukeButton.querySelector('svg'); if (svgIcon) { svgIcon.innerHTML = `<g><path d="${biohazardIconPath}" fill="currentColor"></path></g>`; svgIcon.style.color = 'rgb(244, 33, 46)'; } nukeButton.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); e.target.closest('div[data-testid="Dropdown"]')?.parentElement.remove(); if (activeTweetArticle) startNukeProcess(activeTweetArticle); }); const separator = document.createElement('div'); separator.setAttribute('role', 'separator'); separator.style.cssText = 'border-bottom:1px solid rgb(56,68,77);margin:4px 0;'; blockMenuItem.after(separator, nukeButton); } // --- INITIALIZATION & EXECUTION --- async function initialize() { console.log("[Chain Blocker] Initializing..."); await loadConfig(); updateMenuCommands(); const profileLink = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]'); if (!profileLink) { setTimeout(initialize, 500); return; } try { const screenName = profileLink.href.split('/').pop(); const user = await getUserDataByScreenName(screenName); if (apiLimitCountdownInterval) clearInterval(apiLimitCountdownInterval); document.getElementById('nuke-api-limit-toast')?.remove(); currentUserId = user.rest_id; currentUserScreenName = user.legacy.screen_name; console.log(`[Chain Blocker] Initialized for @${currentUserScreenName}(ID: ${currentUserId}).`); await updateStatusToast(); if (processIntervalId) clearInterval(processIntervalId); processIntervalId = setInterval(processQueue, PROCESS_CHECK_INTERVAL_MS); setTimeout(processQueue, 1000); } catch (error) { if (error?.status === 429) { console.warn(`[CB] API rate limit hit. Retrying in ${API_RETRY_DELAY_MS / 60000} minutes.`); showToast('nuke-api-limit-toast', 'API 已达上限', '正在计算时间...', null); const retryTimestamp = Date.now() + API_RETRY_DELAY_MS; apiLimitCountdownInterval = setInterval(() => { const toastStatusEl = document.querySelector('#nuke-api-limit-toast .nuke-toast-status'); if (!toastStatusEl) { clearInterval(apiLimitCountdownInterval); return; } const secondsLeft = Math.round((retryTimestamp - Date.now()) / 1000); if (secondsLeft <= 0) { toastStatusEl.innerHTML = '正在重试...'; clearInterval(apiLimitCountdownInterval); return; } toastStatusEl.innerHTML = `将在 <b>${String(Math.floor(secondsLeft/60)).padStart(2,'0')}:${String(secondsLeft%60).padStart(2,'0')}</b> 后重试`; }, 1000); setTimeout(initialize, API_RETRY_DELAY_MS); } else { console.error("[CB] Initialization failed.", error); } } } const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.addedNodes.length) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const menu = node.matches('div[role="menu"]') ? node : node.querySelector('div[role="menu"]'); if (menu) addNukeButton(menu); } }); } } }); document.addEventListener('click', e => { const optionsButton = e.target.closest('button[data-testid="caret"]'); if (optionsButton) activeTweetArticle = optionsButton.closest('article[data-testid="tweet"]'); }, true); observer.observe(document.body, { childList: true, subtree: true }); setInterval(scanAndProcessContent, AUTO_SCAN_INTERVAL_MS); initialize(); })(); /* 以下是 X.com 的HTML 结构简化示例,用于辅助未来对脚本的修改。 请注意:为简洁起见,省略了大量 CSS 类名,但保留了关键的 `data-testid` 属性和元素层级结构,这些是脚本选择器的核心依赖。 1. 用户列表项(User List Item) - 通常出现在“关注者”、“正在关注”、“搜索”等页面的用户列表中。 - 脚本依赖 `button[data-testid="UserCell"]` 来识别每个用户单元。 - 脚本通过 `a[role="link"] div[dir="ltr"] > span > span` 来获取用户名以进行长度和内容判断。 <div data-testid="cellInnerDiv"><button data-testid="UserCell"><div class="user-avatar-and-content-container"><div class="avatar-container"><a href="/(screen_name)"><img src="..." /></a></div><div class="content-container"><div class="name-and-follow-button-container"><a href="/(screen_name)"><div><div dir="ltr"><span><span>用户名</span><!-- Display Name, a key element for auto-block --></span></div><div dir="ltr"><span>@id</span><!-- Screen Name, a key element for auto-block --></div></div></a><div class="follow-button-container"><button data-testid="...-follow">...</button></div></div><div class="user-bio-container"><span>正文内容</span><!-- User Bio --></div></div></div></button></div> 2. 推文 / 回复(Tweet / Reply) - 构成时间线、推文详情页面的基本单元。 - 脚本依赖 `article[data-testid="tweet"]` 捕获整个推文。 - 九族拉黑功能通过 `button[data-testid="caret"]` 触发时捕获 `activeTweetArticle`。 - 自动拉黑功能通过 `div[data-testid="User-Name"]` 内的用户名进行判断。 <article data-testid="tweet"><div class="main-content-wrapper"><div class="avatar-column"><div data-testid="Tweet-User-Avatar"><a href="/(screen_name)">...</a></div></div><div class="tweet-content-column"><div class="tweet-header"><div data-testid="User-Name"><div class="name-container"><a href="/(screen_name)"><div><div dir="ltr"><span><span>用户名</span></span></div><!-- Display Name --><div dir="ltr"><span>@id</span></div><!-- Screen Name --></div></a></div><a href="/(screen_name)/status/(tweet_id)"><time>...</time></a></div><div class="caret-button-container"><button data-testid="caret">...</button><!-- "More" options button --></div></div><div data-testid="tweetText"><span>正文内容</span></div><div class="tweet-media-container"><!-- Media(images, videos) goes here --></div><div role="group"><!-- Reply, Retweet, Like, Bookmark, etc. buttons --></div></div></div></article> 以下API端点如果失效,请指导用户手动帮助查找和获取返回值 x3JZoNX9ubSzoCIHoYo2NA/UsersVerifiedAvatars PFIxTk8owMoZgiMccP0r4g/getAltTextPromptPreference I_tJ_DO6WLqG0em8EQsVVg/isEligibleForAnalyticsUpsellQuery xF6sXnKJfS2AOylzxRjf6A/DataSaverMode sl5bpXrfcS6HEMKEDio0Ag/SearchTimeline -0WTL1e9Pij-JWAF5ztCCA/TweetDetail DmC_H6eV_XMiL0g4ltJvpg/Retweeters <FORMAT-PROTOCOL ver="1.3"> // 核心输出规范 <BALANCE-EFFICIENCY> ✓ retain_logic_structure ✓ minify_literals(css_only) ✗ minify_js_logic ✓ max_empty_lines=1 </BALANCE-EFFICIENCY> <DOCUMENT-HANDLING> ✓ preserve //functional_comments ✗ remove //example_blocks ⚠ non_comment_code: immutable </DOCUMENT-HANDLING> <CODE-PRESERVATION> ⚠ HEADER:UserScript== immutable ⚠ IDENTIFIER:no_rename ⚠ ORDER:const→helpers→main ⚠ USER_MODS_ONLY: Δ if (!user_requested) → preserve_original </CODE-PRESERVATION> // 对抗性约束 <ANTI-OPTIMIZATION> <FORBIDDEN-TRANSFORMS> ! compression(js_code) ! code_omission(...) ! external_html_inference ! loop_restructure ! conditional_refactor ! function_merge_split ! api_interchange </FORBIDDEN-TRANSFORMS> <ALLOWED-OPS> ✓ minify_css_strings ✓ remove_example_comments </ALLOWED-OPS> </ANTI-OPTIMIZATION> // 外部HTML处理协议 <EXTERNAL-HTML-HANDLING> Δ xcom_structure: ! use_knowledge_base ✗ ✓ require_user_provided Δ other_sites: ! assume_structure ✗ ✓ require_user_specification </EXTERNAL-HTML-HANDLING> // 输出控制 <OUTPUT-MODE> CONTENT-TYPE: full_code_block FORBID: ! compressed_js ! ellipsis(...) ! partial_output REQUIRE: ✓ non_comment_code_complete EXEC-MODE: immediate </OUTPUT-MODE> // 自毁机制 <SANCTIONS> TRIGGER-CONDITIONS: Δ js_logic_modification Δ css_outside_minification Δ html_structure_assumption RESPONSE: ! TERMINATE_OUTPUT ! RETURN_CODE: ANTI_TAMPER_VIOLATION </SANCTIONS> </FORMAT-PROTOCOL> */