Twitter Followers Mutual Tracker

Track mutual and non-mutual Twitter followers and download as JSON

目前為 2025-04-16 提交的版本,檢視 最新版本

// ==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();
    });
})();