Last active
March 28, 2025 00:59
-
-
Save TwoXTwentyOne/8ceffec11bdb9cfe231ee1d41af5554a to your computer and use it in GitHub Desktop.
TamperMonkey - Unfollow Unverified Account
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name X (Twitter) - v1.18 + Verified-No-Followback + Start/Stop Toggle + Dark Mode (All Text White) | |
// @namespace http://tampermonkey.net/ | |
// @version 1.18 | |
// @description Contextual counters, S=Start, A=Abort, P=Pause, E=Export, ignore list, plus an option to unfollow verified if they don’t follow back, with Start/Stop toggles and Dark Mode (all text white) | |
// @match https://x.com/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// ------------------- DARK MODE GLOBALS & STYLE ------------------- | |
// Load the dark mode setting from localStorage (default to false) | |
let darkModeEnabled = localStorage.getItem('my_dark_mode') === "true"; | |
// Inject dark mode CSS styles | |
function initDarkModeStyles() { | |
const style = document.createElement("style"); | |
style.textContent = ` | |
/* Dark Mode Styles: all text will be white */ | |
.dark-mode { | |
background-color: #333 !important; | |
color: #fff !important; | |
border: 1px solid #555 !important; | |
} | |
.dark-mode * { | |
color: #fff !important; | |
} | |
.dark-mode button { | |
background-color: #555 !important; | |
color: #fff !important; | |
} | |
.dark-mode textarea { | |
background-color: #555 !important; | |
color: #fff !important; | |
border: 1px solid #777 !important; | |
} | |
.dark-mode a { | |
color: #66aaff !important; | |
} | |
`; | |
document.head.appendChild(style); | |
} | |
initDarkModeStyles(); | |
// ------------------- STORAGE KEYS ------------------- | |
const panelId = '__sedgwickz__unfollow_id'; | |
const storageKeyIgnoreList = '__my_ignore_list'; | |
const storageKeyUnfollowLimit = '__my_unfollow_limit'; | |
const storageKeyRemoveLimit = '__my_remove_limit'; | |
const storageKeyVerifiedNoFollowback = '__unfollow_verified_no_followback'; | |
// ------------------- GLOBALS ------------------- | |
let ignoreList = loadIgnoreList(); | |
let isPaused = false; | |
let abortNow = false; | |
let unfollowedCount = 0; | |
let removedFollowersCount = 0; | |
let userUnfollowLimit = loadLimit(storageKeyUnfollowLimit, 50000); | |
let userRemoveLimit = loadLimit(storageKeyRemoveLimit, 50000); | |
// Whether to unfollow verified who do not follow me | |
let unfollowVerifiedNoFollowback = loadBooleanSetting(storageKeyVerifiedNoFollowback, false); | |
const seenHandles = new Set(); | |
const allUsersArr = []; | |
const recentProcessed = []; | |
let recentProcessedDiv = null; | |
let countDiv = null; | |
let ignoreTextArea = null; | |
let isRunning = false; | |
// We’ll store references to the main “Start”/“Stop” buttons so we can toggle their text. | |
let unfollowButton = null; | |
let removeFollowersButton = null; | |
// --------------- MAIN ENTRY --------------- | |
function start() { | |
if (isFollowingPage() || isFollowersPage()) { | |
createPanel(); | |
} else { | |
removePanel(); | |
} | |
} | |
function removePanel() { | |
const panel = document.getElementById(panelId); | |
if (panel) panel.remove(); | |
} | |
// Refresh the page every 25 min | |
setTimeout(() => { | |
location.reload(); | |
}, 1500000); | |
// SPA transitions | |
if (window?.navigation?.addEventListener) { | |
window.navigation.addEventListener("navigate", () => { | |
setTimeout(start, 200); | |
}); | |
} | |
if (window.onurlchange === null) { | |
window.addEventListener('urlchange', () => { | |
setTimeout(start, 200); | |
}); | |
} | |
// --------------- KEYBOARD SHORTCUTS --------------- | |
document.addEventListener('keydown', (e) => { | |
const k = e.key.toLowerCase(); | |
if (k === 'p') { | |
isPaused = !isPaused; | |
notify(isPaused ? "Paused (P key)" : "Resumed (P key)"); | |
updatePauseButtonText(isPaused); | |
} | |
else if (k === 'a') { | |
abortNow = true; | |
notify("Abort requested (A key). Loops will halt!"); | |
} | |
else if (k === 'e') { | |
if (isFollowingPage() || isFollowersPage()) { | |
notify("Keyboard Export triggered..."); | |
exportChronologicalList(); | |
} | |
} | |
else if (k === 's') { | |
if (!isRunning && (isFollowingPage() || isFollowersPage())) { | |
notify("Starting main routine (S key)..."); | |
startMainRoutine(); | |
} | |
} | |
}); | |
function updatePauseButtonText(isPaused) { | |
const panel = document.getElementById(panelId); | |
if (!panel) return; | |
const pauseBtn = panel.querySelector('button[data-role="pauseBtn"]'); | |
if (!pauseBtn) return; | |
pauseBtn.innerText = isPaused ? "Resume" : "Pause"; | |
} | |
start(); // On script load | |
// --------------- CREATE UI PANEL --------------- | |
function createPanel() { | |
removePanel(); | |
abortNow = false; | |
isRunning = false; | |
const container = document.createElement('div'); | |
container.id = panelId; | |
// Base (light mode) styles: | |
applyStyles(container, { | |
position: 'fixed', | |
zIndex: 9999999, | |
padding: '10px', | |
top: '0', | |
right: '0', | |
background: '#e0e0e0', | |
border: '1px solid #ccc', | |
borderRadius: '5px', | |
boxShadow: '0 2px 10px rgba(0,0,0,0.1)', | |
display: 'flex', | |
flexDirection: 'column', | |
justifyContent: 'center', | |
alignItems: 'flex-start', | |
gap: '6px', | |
width: '280px' | |
}); | |
// Apply dark mode if enabled: | |
if (darkModeEnabled) { | |
container.classList.add("dark-mode"); | |
} | |
document.body.appendChild(container); | |
// ~~~~~~~~~~~~~~~~~~~~~ BUTTONS ~~~~~~~~~~~~~~~~~~~~~ | |
if (isFollowingPage()) { | |
// Green button (unfollow) | |
unfollowButton = createButton("Remove Unverified Accounts - Start", '#28a745'); | |
container.appendChild(unfollowButton); | |
unfollowButton.addEventListener('click', () => { | |
if (!isRunning) { | |
// Not running => start | |
startMainRoutine(); | |
unfollowButton.innerText = "Stop Unfollowing"; | |
} else { | |
// Already running => request abort | |
abortNow = true; | |
notify("Stop requested from button!"); | |
unfollowButton.innerText = "Stopping..."; | |
} | |
}); | |
} | |
if (isFollowersPage()) { | |
// Red button (remove followers) | |
removeFollowersButton = createButton("Remove Unverified Followers", '#dc3545'); | |
container.appendChild(removeFollowersButton); | |
removeFollowersButton.addEventListener('click', () => { | |
if (!isRunning) { | |
// Not running => start | |
startMainRoutine(); | |
removeFollowersButton.innerText = "Stop Removing"; | |
} else { | |
// Already running => request abort | |
abortNow = true; | |
notify("Stop requested from button!"); | |
removeFollowersButton.innerText = "Stopping..."; | |
} | |
}); | |
} | |
// Export | |
if (isFollowingPage() || isFollowersPage()) { | |
const exportButton = createButton("Export Chronological (Bottom->Top)", '#007bff'); | |
container.appendChild(exportButton); | |
exportButton.addEventListener('click', async () => { | |
exportButton.innerText = "Gathering users from bottom..."; | |
await exportChronologicalList(); | |
exportButton.innerText = "Export Chronological (Bottom->Top)"; | |
}); | |
} | |
// Pause/Resume | |
const pauseButton = createButton("Pause", '#ffc107'); | |
pauseButton.setAttribute('data-role', 'pauseBtn'); | |
container.appendChild(pauseButton); | |
pauseButton.addEventListener('click', () => { | |
isPaused = !isPaused; | |
pauseButton.innerText = isPaused ? "Resume" : "Pause"; | |
}); | |
// Dark Mode Toggle Button | |
const darkModeToggle = createButton(darkModeEnabled ? "Light Mode" : "Dark Mode", '#6c757d'); | |
container.appendChild(darkModeToggle); | |
darkModeToggle.addEventListener("click", () => { | |
darkModeEnabled = !darkModeEnabled; | |
localStorage.setItem('my_dark_mode', darkModeEnabled ? "true" : "false"); | |
container.classList.toggle("dark-mode", darkModeEnabled); | |
darkModeToggle.innerText = darkModeEnabled ? "Light Mode" : "Dark Mode"; | |
}); | |
// ~~~~~~~~~~~~~~~~~~~~~ Count Display ~~~~~~~~~~~~~~~~~~~~~ | |
countDiv = document.createElement('div'); | |
updateCountDisplay(countDiv); | |
container.appendChild(countDiv); | |
// ~~~~~~~~~~~~~~~~~~~~~ Limits & Checkboxes ~~~~~~~~~~~~~~~~~~~~~ | |
if (isFollowingPage()) { | |
// Unfollow limit | |
const limitLabel = document.createElement('div'); | |
limitLabel.innerText = "Unfollow Limit (0=Unlimited):"; | |
applyStyles(limitLabel, { | |
fontWeight: 'bold', | |
marginTop: '5px', | |
color: '#444' | |
}); | |
container.appendChild(limitLabel); | |
const inputUnfollowLimit = document.createElement('input'); | |
inputUnfollowLimit.type = 'number'; | |
inputUnfollowLimit.min = '0'; | |
inputUnfollowLimit.value = (userUnfollowLimit === Number.MAX_SAFE_INTEGER) ? '0' : String(userUnfollowLimit); | |
inputUnfollowLimit.style.width = '120px'; | |
container.appendChild(inputUnfollowLimit); | |
const saveLimitBtn = createButton("Save Unfollow Limit", '#6c757d'); | |
container.appendChild(saveLimitBtn); | |
saveLimitBtn.addEventListener('click', () => { | |
const newLimit = parseInt(inputUnfollowLimit.value, 10) || 0; | |
userUnfollowLimit = (newLimit === 0) ? Number.MAX_SAFE_INTEGER : newLimit; | |
localStorage.setItem(storageKeyUnfollowLimit, String(newLimit)); | |
notify("Unfollow Limit saved!"); | |
updateCountDisplay(countDiv); | |
}); | |
// Checkbox: Unfollow Verified Who Don’t Follow Me | |
const verifyDiv = document.createElement('div'); | |
verifyDiv.style.marginTop = '6px'; | |
verifyDiv.style.display = 'flex'; | |
verifyDiv.style.alignItems = 'center'; | |
const verifyCheckbox = document.createElement('input'); | |
verifyCheckbox.type = 'checkbox'; | |
verifyCheckbox.checked = unfollowVerifiedNoFollowback; | |
verifyCheckbox.id = '__verify_checkbox_id'; | |
verifyDiv.appendChild(verifyCheckbox); | |
const verifyLabel = document.createElement('label'); | |
verifyLabel.htmlFor = '__verify_checkbox_id'; | |
verifyLabel.innerText = "Unfollow Verified Who Don’t Follow Me"; | |
verifyLabel.style.marginLeft = '6px'; | |
verifyLabel.style.color = '#222'; | |
verifyDiv.appendChild(verifyLabel); | |
container.appendChild(verifyDiv); | |
verifyCheckbox.addEventListener('change', () => { | |
unfollowVerifiedNoFollowback = verifyCheckbox.checked; | |
localStorage.setItem(storageKeyVerifiedNoFollowback, String(unfollowVerifiedNoFollowback)); | |
notify( | |
"Setting updated: " + | |
(unfollowVerifiedNoFollowback ? "Will" : "Won't") + | |
" remove verified who don't follow back." | |
); | |
}); | |
} | |
if (isFollowersPage()) { | |
// Remove limit | |
const limitLabel = document.createElement('div'); | |
limitLabel.innerText = "Remove Limit (0=Unlimited):"; | |
applyStyles(limitLabel, { | |
fontWeight: 'bold', | |
marginTop: '5px', | |
color: '#444' | |
}); | |
container.appendChild(limitLabel); | |
const inputRemoveLimit = document.createElement('input'); | |
inputRemoveLimit.type = 'number'; | |
inputRemoveLimit.min = '0'; | |
inputRemoveLimit.value = (userRemoveLimit === Number.MAX_SAFE_INTEGER) ? '0' : String(userRemoveLimit); | |
inputRemoveLimit.style.width = '120px'; | |
container.appendChild(inputRemoveLimit); | |
const saveLimitBtn = createButton("Save Remove Limit", '#6c757d'); | |
container.appendChild(saveLimitBtn); | |
saveLimitBtn.addEventListener('click', () => { | |
const newLimit = parseInt(inputRemoveLimit.value, 10) || 0; | |
userRemoveLimit = (newLimit === 0) ? Number.MAX_SAFE_INTEGER : newLimit; | |
localStorage.setItem(storageKeyRemoveLimit, String(newLimit)); | |
notify("Remove Limit saved!"); | |
updateCountDisplay(countDiv); | |
}); | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ Ignore List ~~~~~~~~~~~~~~~~~~~~~ | |
const ignoreLabel = document.createElement('div'); | |
ignoreLabel.innerText = "Ignore List (User won't be removed):"; | |
applyStyles(ignoreLabel, { | |
fontWeight: 'bold', | |
marginTop: '10px', | |
color: '#444' | |
}); | |
container.appendChild(ignoreLabel); | |
ignoreTextArea = document.createElement('textarea'); | |
ignoreTextArea.rows = 4; | |
ignoreTextArea.cols = 22; | |
ignoreTextArea.value = ignoreList.join("\n"); | |
container.appendChild(ignoreTextArea); | |
const saveIgnoreButton = createButton("Save Ignore List", '#6c757d'); | |
container.appendChild(saveIgnoreButton); | |
saveIgnoreButton.addEventListener('click', () => { | |
const lines = ignoreTextArea.value | |
.split("\n") | |
.map(s => s.trim()) | |
.filter(Boolean); | |
ignoreList = lines.map(h => h.toLowerCase()); | |
saveIgnoreList(ignoreList); | |
notify("Ignore List saved!"); | |
}); | |
// ~~~~~~~~~~~~~~~~~~~~~ Recently Processed ~~~~~~~~~~~~~~~~~~~~~ | |
const recentLabel = document.createElement('div'); | |
recentLabel.innerText = "Recently Processed (Last 25):"; | |
applyStyles(recentLabel, { | |
fontWeight: 'bold', | |
marginTop: '10px', | |
color: '#444' | |
}); | |
container.appendChild(recentLabel); | |
recentProcessedDiv = document.createElement('div'); | |
applyStyles(recentProcessedDiv, { | |
maxHeight: '120px', | |
overflowY: 'auto', | |
border: '1px solid #ccc', | |
padding: '3px', | |
width: '100%' | |
}); | |
container.appendChild(recentProcessedDiv); | |
updateRecentProcessedUI(); | |
// ~~~~~~~~~~~~~~~~~~~~~ Help/Feedback ~~~~~~~~~~~~~~~~~~~~~ | |
const helpDiv = document.createElement('div'); | |
helpDiv.innerHTML = ` | |
<div style="font-weight:bold; margin-top:10px; color:#444;">Keyboard Shortcuts:</div> | |
<ul style="margin:0; padding-left:18px; font-size:12px; color:#333;"> | |
<li><strong>S</strong>: Start Process</li> | |
<li><strong>A</strong>: Abort Immediately</li> | |
<li><strong>P</strong>: Pause/Resume</li> | |
<li><strong>E</strong>: Export Chronological</li> | |
</ul> | |
`; | |
container.appendChild(helpDiv); | |
const descDiv = document.createElement('div'); | |
descDiv.innerHTML = ` | |
<div style='font-weight: bold; color: #555;'>Feedback & Support</div> | |
<div><a href='https://x.com/eric_periard' target='_blank' style='color: darkblue;'>@eric_periard</a></div>`; | |
applyStyles(descDiv, { textAlign: 'center' }); | |
container.appendChild(descDiv); | |
applyHoverEffects(container); | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ MAIN ROUTINE --------------------- | |
async function startMainRoutine() { | |
if (isRunning) return; | |
isRunning = true; | |
abortNow = false; | |
if (isFollowingPage()) { | |
notify("Starting unfollow routine..."); | |
document.documentElement.scrollTo(0, 0); | |
await sleep(2000); | |
while (!abortNow && unfollowedCount < userUnfollowLimit) { | |
await unFollow(); | |
} | |
if (abortNow) { | |
notify("Aborted unfollow routine!"); | |
} else { | |
notify("Unfollow process completed!"); | |
} | |
// reset button label | |
if (unfollowButton) { | |
unfollowButton.innerText = "Remove Unverified Accounts - Start"; | |
} | |
} | |
else if (isFollowersPage()) { | |
notify("Starting remove-follower routine..."); | |
document.documentElement.scrollTo(0, 0); | |
await sleep(2000); | |
while (!abortNow && removedFollowersCount < userRemoveLimit) { | |
await removeUnverifiedFollowers(); | |
} | |
if (abortNow) { | |
notify("Aborted remove-follower routine!"); | |
} else { | |
notify("Remove-follower process completed!"); | |
} | |
// reset button label | |
if (removeFollowersButton) { | |
removeFollowersButton.innerText = "Remove Unverified Followers"; | |
} | |
} | |
isRunning = false; | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ RECORD PROCESSED --------------------- | |
function recordProcessedAction(handle, actionType) { | |
const entry = `${actionType} @${handle}`; | |
recentProcessed.unshift(entry); | |
if (recentProcessed.length > 25) recentProcessed.pop(); | |
updateRecentProcessedUI(); | |
} | |
function updateRecentProcessedUI() { | |
if (!recentProcessedDiv) return; | |
const lines = recentProcessed | |
.map(item => `<div style="font-size: 12px; color: #333;">${item}</div>`) | |
.join(""); | |
recentProcessedDiv.innerHTML = lines || "<div style='font-size:12px; color:#999;'>No recent actions</div>"; | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ UNFOLLOW LOGIC --------------------- | |
async function unFollow() { | |
const container = document.querySelector('[aria-label*="Following"]'); | |
if (!container) { | |
console.log("No main container found for 'Following'"); | |
return; | |
} | |
const buttons = container.querySelectorAll("button[role='button']"); | |
for (const item of buttons) { | |
if (unfollowedCount >= userUnfollowLimit || abortNow) break; | |
while (isPaused) { | |
await sleep(500); | |
if (abortNow) break; | |
} | |
if (abortNow) break; | |
if (item.innerText === 'Following') { | |
const accountElement = item.closest('[data-testid="UserCell"]'); | |
if (!accountElement) continue; | |
const userHandle = getHandleFromUserCell(accountElement); | |
if (!userHandle) continue; | |
// skip if in ignore list | |
if (ignoreList.includes(userHandle.toLowerCase())) { | |
continue; | |
} | |
const verifiedIcon = accountElement.querySelector('svg[aria-label="Verified account"]'); | |
const isVerified = !!verifiedIcon; | |
if (isVerified) { | |
if (!unfollowVerifiedNoFollowback) { | |
// user didn't check the box => skip verified | |
continue; | |
} | |
// user DID check => unfollow verified only if "does not follow me" | |
if (doesFollowMe(accountElement)) { | |
// they DO follow me => skip | |
continue; | |
} | |
} | |
// else if unverified => just proceed | |
// highlight + random delay | |
accountElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
highlightElement(accountElement); | |
await sleep(randomDelay(2000, 5000)); | |
if (abortNow) break; | |
item.click(); | |
await sleep(1000); | |
if (abortNow) break; | |
const confirmButton = getConfirmButton(); | |
if (confirmButton) confirmButton.click(); | |
unfollowedCount++; | |
updateCountDisplay(countDiv); | |
recordProcessedAction(userHandle, "Unfollowed"); | |
} | |
} | |
document.documentElement.scrollTo(0, 999999999); | |
await sleep(2000); | |
} | |
function doesFollowMe(accountElement) { | |
const label = [...accountElement.querySelectorAll('span, div')] | |
.find(el => el.innerText && el.innerText.includes("Follows you")); | |
return !!label; | |
} | |
function getConfirmButton() { | |
return [...document.querySelectorAll("button[role='button']")] | |
.find(item => item.innerText === 'Unfollow'); | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ REMOVE FOLLOWERS --------------------- | |
async function removeUnverifiedFollowers() { | |
const container = document.querySelector('[aria-label*="Followers"]'); | |
if (!container) { | |
console.log("No main container found for 'Followers'"); | |
return; | |
} | |
const allUserCells = container.querySelectorAll('[data-testid="UserCell"]'); | |
for (const cell of allUserCells) { | |
if (removedFollowersCount >= userRemoveLimit || abortNow) break; | |
while (isPaused) { | |
await sleep(500); | |
if (abortNow) break; | |
} | |
if (abortNow) break; | |
const userHandle = getHandleFromUserCell(cell); | |
if (!userHandle) continue; | |
if (ignoreList.includes(userHandle.toLowerCase())) { | |
continue; | |
} | |
const isVerified = !!cell.querySelector('svg[aria-label="Verified account"]'); | |
if (!isVerified) { | |
// highlight + random delay | |
cell.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
highlightElement(cell); | |
await sleep(randomDelay(1500, 3000)); | |
if (abortNow) break; | |
const menuButton = findThreeDotMenu(cell); | |
if (!menuButton) continue; | |
menuButton.click(); | |
await sleep(1200); | |
if (abortNow) break; | |
const removeBtn = [...document.querySelectorAll('span, div')] | |
.find(el => el.innerText === 'Remove this follower'); | |
if (!removeBtn) { | |
closeAnyMenu(); | |
continue; | |
} | |
removeBtn.click(); | |
await sleep(1000); | |
if (abortNow) break; | |
const confirmRemoveButton = [...document.querySelectorAll('span, div')] | |
.find(el => el.innerText === 'Remove'); | |
if (confirmRemoveButton) confirmRemoveButton.click(); | |
removedFollowersCount++; | |
updateCountDisplay(countDiv); | |
recordProcessedAction(userHandle, "Removed"); | |
} | |
} | |
document.documentElement.scrollTo(0, 999999999); | |
await sleep(2000); | |
} | |
function findThreeDotMenu(cell) { | |
let menuButton = cell.querySelector('[aria-label="More"]'); | |
if (!menuButton) { | |
menuButton = cell.querySelector('[data-testid="UserCellOverflowButton"]'); | |
} | |
if (!menuButton) { | |
menuButton = [...cell.querySelectorAll('span, div, button')] | |
.find(el => el.innerText === '…' || el.innerText === '...'); | |
} | |
return menuButton; | |
} | |
function closeAnyMenu() { | |
const overlay = document.querySelector('[data-testid="sheetDialog"]'); | |
if (overlay) overlay.click(); | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ HIGHLIGHT --------------------- | |
function highlightElement(el) { | |
el.style.transition = "background-color 0.5s ease"; | |
el.style.backgroundColor = "yellow"; | |
setTimeout(() => { | |
el.style.backgroundColor = ""; | |
}, 2000); | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ EXPORT LOGIC --------------------- | |
async function exportChronologicalList() { | |
const MAX_SCROLL_ITERATIONS = 200; | |
const MAX_STABLE_SCROLLS = 5; | |
let previousCount = 0; | |
let stableScrolls = 0; | |
for (let i = 0; i < MAX_SCROLL_ITERATIONS; i++) { | |
while (isPaused) { | |
await sleep(500); | |
if (abortNow) return; | |
} | |
if (abortNow) return; | |
gatherVisibleUsers(); | |
window.scrollTo(0, document.body.scrollHeight); | |
await sleep(2000); | |
const currentCount = seenHandles.size; | |
if (currentCount === previousCount) { | |
stableScrolls++; | |
} else { | |
stableScrolls = 0; | |
} | |
previousCount = currentCount; | |
if (stableScrolls >= MAX_STABLE_SCROLLS) break; | |
} | |
gatherVisibleUsers(); | |
const reversed = [...allUsersArr].reverse(); | |
const csv = generateCSV(reversed); | |
downloadCSV(csv); | |
notify(`Exported ${reversed.length} users (oldest at the top)`); | |
} | |
function gatherVisibleUsers() { | |
let container = isFollowersPage() | |
? document.querySelector('[aria-label*="Followers"]') | |
: document.querySelector('[aria-label*="Following"]'); | |
if (!container) { | |
console.log("No main container found for export"); | |
return; | |
} | |
const cells = container.querySelectorAll('[data-testid="UserCell"]'); | |
for (const cell of cells) { | |
const handle = getHandleFromUserCell(cell); | |
if (!handle) continue; | |
if (!seenHandles.has(handle)) { | |
seenHandles.add(handle); | |
const displayName = getDisplayNameFromCell(cell) || ""; | |
const verified = !!cell.querySelector('svg[aria-label="Verified account"]'); | |
allUsersArr.push({ handle, displayName, verified }); | |
} | |
} | |
} | |
function getDisplayNameFromCell(cell) { | |
const nameEl = cell.querySelector('[data-testid="User-Name"] span'); | |
if (nameEl && nameEl.innerText) { | |
return nameEl.innerText.trim(); | |
} | |
const fallbackEl = cell.querySelector('span'); | |
return fallbackEl ? fallbackEl.innerText.trim() : null; | |
} | |
function generateCSV(dataArray) { | |
const header = ["Handle", "DisplayName", "Verified"]; | |
const rows = dataArray.map(user => [ | |
user.handle, | |
user.displayName.replace(/,/g, ""), | |
user.verified ? "Yes" : "No" | |
]); | |
return [header.join(","), ...rows.map(r => r.join(","))].join("\n"); | |
} | |
function downloadCSV(csv) { | |
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement("a"); | |
a.href = url; | |
a.download = "x_users_chronological.csv"; | |
a.click(); | |
URL.revokeObjectURL(url); | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~ HELPERS --------------------- | |
function getHandleFromUserCell(cell) { | |
const link = cell.querySelector('a[href*="/"]'); | |
if (!link) return null; | |
const urlPath = link.getAttribute('href') || ''; | |
const user = urlPath.split('/').pop().replace('@', '').toLowerCase(); | |
return user; | |
} | |
function isFollowingPage() { | |
return location.pathname.endsWith('/following'); | |
} | |
function isFollowersPage() { | |
return location.pathname.endsWith('/followers'); | |
} | |
function sleep(ms) { | |
return new Promise(res => setTimeout(res, ms)); | |
} | |
function randomDelay(min, max) { | |
return Math.floor(Math.random() * (max - min + 1)) + min; | |
} | |
function createButton(text, bgColor) { | |
const button = document.createElement('button'); | |
button.innerText = text; | |
applyStyles(button, { | |
backgroundColor: bgColor, | |
color: '#fff', | |
border: 'none', | |
borderRadius: '5px', | |
padding: '8px 14px', | |
cursor: 'pointer', | |
fontWeight: 'bold', | |
marginRight: '5px' | |
}); | |
return button; | |
} | |
function applyStyles(element, styles) { | |
Object.assign(element.style, styles); | |
} | |
function applyHoverEffects(container) { | |
const style = document.createElement('style'); | |
style.textContent = ` | |
#${panelId} button:hover { | |
opacity: 0.8; | |
} | |
#${panelId} button:active { | |
transform: scale(0.95); | |
} | |
`; | |
document.head.appendChild(style); | |
} | |
function updateCountDisplay(div) { | |
if (!div) return; | |
if (isFollowingPage()) { | |
const remaining = (userUnfollowLimit === Number.MAX_SAFE_INTEGER) | |
? 'Unlimited' | |
: Math.max(0, userUnfollowLimit - unfollowedCount); | |
div.innerHTML = ` | |
<div style="color: red"> | |
Unfollowed: ${unfollowedCount} | Remaining: ${remaining} | |
</div> | |
`; | |
} | |
else if (isFollowersPage()) { | |
const remaining = (userRemoveLimit === Number.MAX_SAFE_INTEGER) | |
? 'Unlimited' | |
: Math.max(0, userRemoveLimit - removedFollowersCount); | |
div.innerHTML = ` | |
<div style="color: purple"> | |
Removed: ${removedFollowersCount} | Remaining: ${remaining} | |
</div> | |
`; | |
} else { | |
div.innerHTML = ""; | |
} | |
} | |
function notify(message) { | |
if (Notification.permission === "granted") { | |
new Notification(message); | |
} else if (Notification.permission !== "denied") { | |
Notification.requestPermission().then(permission => { | |
if (permission === "granted") new Notification(message); | |
}); | |
} | |
console.log(message); // fallback | |
} | |
function loadIgnoreList() { | |
try { | |
const data = localStorage.getItem(storageKeyIgnoreList); | |
if (!data) return []; | |
return JSON.parse(data); | |
} catch (e) { | |
return []; | |
} | |
} | |
function saveIgnoreList(list) { | |
localStorage.setItem(storageKeyIgnoreList, JSON.stringify(list)); | |
} | |
function loadLimit(key, defaultValue) { | |
const data = localStorage.getItem(key); | |
if (!data) return defaultValue; | |
let val = parseInt(data, 10); | |
if (isNaN(val)) { | |
return defaultValue; | |
} | |
if (val === 0) { | |
return Number.MAX_SAFE_INTEGER; | |
} | |
return val; | |
} | |
function loadBooleanSetting(key, defaultVal) { | |
const data = localStorage.getItem(key); | |
if (!data) return defaultVal; | |
return data === "true"; | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment