// ==UserScript== // @name X.com Chain Blocker // @name:zh-CN X.com 九族拉黑 // @namespace http://tampermonkey.net/ // @version 2.4 // @description Block author, retweeters, repliers, and auto-block users based on rules (length, content, keywords). 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 // @downloadURL https://update.greasyfork.icu/scripts/541237/Xcom%20%E4%B9%9D%E6%97%8F%E6%8B%89%E9%BB%91.user.js // @updateURL https://update.greasyfork.icu/scripts/541237/Xcom%20%E4%B9%9D%E6%97%8F%E6%8B%89%E9%BB%91.meta.js // ==/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%;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-url-textarea{height:80px}.nuke-keywords-textarea{height:60px}.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}.nuke-settings-label{display:block;font-size:14px;color:#8899a6;margin-top:10px;margin-bottom:10px}`); // --- CONFIGURATION MANAGEMENT --- async function loadConfig() { const defaultConfig = { autoBlockEnabled: true, autoBlockUrls: ['https://x.com/*/status/*', 'https://x.com/search*'], blockLogLimit: 500, blockKeywords: [], // For long names blockKeywordsStandard: [] // For any name }; const savedConfig = await GM_getValue(CONFIG_STORAGE_KEY, {}); scriptConfig = { ...defaultConfig, ...savedConfig }; 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 = `
${message}
`; 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 ? `${userName}` : `${userName}`; if (type === 'log') { const timestamp = entry.blockTimestamp ? new Date(entry.blockTimestamp).toLocaleString() : '未知时间'; el.innerHTML = `${timestamp}`; 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 = `移除`; 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; let foundNewUsersInPage = false; 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); foundNewUsersInPage = true; } } } } 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); foundNewUsersInPage = true; } } else if (entry.entryId.startsWith('cursor-bottom-')) { nextCursor = entry.content.value; } } if (cursor === nextCursor || !foundNewUsersInPage) 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 = `