您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Track mutual and non-mutual Twitter followers and download as JSON
当前为
// ==UserScript== // @name Twitter Followers Mutual Tracker // @description Track mutual and non-mutual Twitter followers and download as JSON // @namespace https://github.com/kaubu // @version 1.0.0 // @author kaubu (https://github.com/kaubu) // @match https://twitter.com/*/followers // @match https://twitter.com/*/following // @match https://x.com/*/followers // @match https://x.com/*/following // @grant none // @license 0BSD // ==/UserScript== (function() { 'use strict'; // Arrays to store mutuals and non-mutuals let mutuals = []; let nonMutuals = []; // Function to add the status div and download button function addStatusDiv() { // Remove any existing status div const existingDiv = document.getElementById('mutual-tracker-status'); if (existingDiv) { existingDiv.remove(); } // Create the status div const statusDiv = document.createElement('div'); statusDiv.id = 'mutual-tracker-status'; statusDiv.style.position = 'fixed'; statusDiv.style.bottom = '20px'; statusDiv.style.right = '20px'; statusDiv.style.backgroundColor = '#1DA1F2'; statusDiv.style.color = 'white'; statusDiv.style.padding = '10px'; statusDiv.style.borderRadius = '5px'; statusDiv.style.zIndex = '10000'; statusDiv.style.fontSize = '14px'; statusDiv.style.fontFamily = 'Arial, sans-serif'; statusDiv.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; statusDiv.style.display = 'flex'; statusDiv.style.flexDirection = 'column'; statusDiv.style.gap = '10px'; // Create the status text const statusText = document.createElement('div'); statusText.textContent = `Mutuals: ${mutuals.length} | Non-Mutuals: ${nonMutuals.length}`; statusDiv.appendChild(statusText); // Create the download button const downloadButton = document.createElement('button'); downloadButton.textContent = 'Download Data'; downloadButton.style.padding = '5px 10px'; downloadButton.style.borderRadius = '3px'; downloadButton.style.border = 'none'; downloadButton.style.backgroundColor = 'white'; downloadButton.style.color = '#1DA1F2'; downloadButton.style.cursor = 'pointer'; downloadButton.style.fontWeight = 'bold'; // Add event listener to download button downloadButton.addEventListener('click', function() { downloadData(); }); statusDiv.appendChild(downloadButton); document.body.appendChild(statusDiv); } // Function to download the data function downloadData() { const data = { mutuals: mutuals, nonMutuals: nonMutuals, totalMutuals: mutuals.length, totalNonMutuals: nonMutuals.length }; const dataStr = JSON.stringify(data, null, 2); const dataBlob = new Blob([dataStr], {type: 'application/json'}); const url = URL.createObjectURL(dataBlob); const a = document.createElement('a'); a.href = url; a.download = 'x_twitter_mutual_data.json'; a.click(); URL.revokeObjectURL(url); } // Function to check if an element is visible function isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } // Function to process user cells function processUserCells() { const userCells = document.querySelectorAll('[data-testid="UserCell"]'); userCells.forEach(cell => { // Skip if already processed if (cell.dataset.processed === 'true') { return; } // Only process visible cells if (!isElementInViewport(cell)) { return; } try { // Get display name const displayNameElement = cell.querySelector('.css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3:not([style*="display: none"])'); const displayName = displayNameElement ? displayNameElement.textContent.trim() : ''; // Get username const usernameElement = cell.querySelector('[style*="color: rgb(113, 118, 123)"] .css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3'); const username = usernameElement ? usernameElement.textContent.trim() : ''; // Get URL const url = username ? `https://x.com/${username.replace('@', '')}` : ''; // Check if mutual (has "Follows you" indicator) const followsYouIndicator = cell.querySelector('[data-testid="userFollowIndicator"]'); // Create user object const userObject = { displayName: displayName, username: username, url: url }; // Add to appropriate array if (followsYouIndicator && !mutuals.some(m => m.username === username)) { mutuals.push(userObject); } else if (!followsYouIndicator && !nonMutuals.some(nm => nm.username === username) && username) { nonMutuals.push(userObject); } // Mark as processed cell.dataset.processed = 'true'; // Update status addStatusDiv(); } catch (error) { console.error('Error processing user cell:', error); } }); } // Function to initialize the observer function initObserver() { const observer = new MutationObserver(function(mutations) { processUserCells(); }); observer.observe(document.body, { childList: true, subtree: true }); // Initial processing processUserCells(); // Add status initially addStatusDiv(); } // Initialize when page is loaded window.addEventListener('load', function() { setTimeout(initObserver, 1500); }); // Also process on scroll window.addEventListener('scroll', function() { processUserCells(); }); })();