Greasy Fork is available in English.
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();
});
})();