Last active
February 10, 2026 01:36
-
-
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.20 + Verified-No-Followback + Start/Stop Toggle + Dark Mode (All Text White) + Auto-Restart Toggle | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.20 | |
| // @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), and optional auto-restart after 60 seconds on completion | |
| // @match https://x.com/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // ------------------- DARK MODE GLOBALS & STYLE ------------------- | |
| let darkModeEnabled = localStorage.getItem('my_dark_mode') === "true"; | |
| function initDarkModeStyles() { | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| .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'; | |
| const storageKeyAutoRestart = '__my_auto_restart_enabled'; // NEW | |
| // ------------------- 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); | |
| let unfollowVerifiedNoFollowback = loadBooleanSetting(storageKeyVerifiedNoFollowback, false); | |
| let autoRestartEnabled = localStorage.getItem(storageKeyAutoRestart) !== "false"; // default true | |
| const seenHandles = new Set(); | |
| const allUsersArr = []; | |
| const recentProcessed = []; | |
| let recentProcessedDiv = null; | |
| let countDiv = null; | |
| let ignoreTextArea = null; | |
| let isRunning = false; | |
| 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; | |
| 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' | |
| }); | |
| if (darkModeEnabled) { | |
| container.classList.add("dark-mode"); | |
| } | |
| document.body.appendChild(container); | |
| // Buttons | |
| if (isFollowingPage()) { | |
| unfollowButton = createButton("Remove Unverified Accounts - Start", '#28a745'); | |
| container.appendChild(unfollowButton); | |
| unfollowButton.addEventListener('click', () => { | |
| if (!isRunning) { | |
| startMainRoutine(); | |
| unfollowButton.innerText = "Stop Unfollowing"; | |
| } else { | |
| abortNow = true; | |
| notify("Stop requested from button!"); | |
| unfollowButton.innerText = "Stopping..."; | |
| } | |
| }); | |
| } | |
| if (isFollowersPage()) { | |
| removeFollowersButton = createButton("Remove Unverified Followers", '#dc3545'); | |
| container.appendChild(removeFollowersButton); | |
| removeFollowersButton.addEventListener('click', () => { | |
| if (!isRunning) { | |
| startMainRoutine(); | |
| removeFollowersButton.innerText = "Stop Removing"; | |
| } else { | |
| 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"; | |
| }); | |
| // NEW: Auto-restart toggle checkbox | |
| const restartDiv = document.createElement('div'); | |
| restartDiv.style.display = 'flex'; | |
| restartDiv.style.alignItems = 'center'; | |
| restartDiv.style.marginTop = '6px'; | |
| const restartCheckbox = document.createElement('input'); | |
| restartCheckbox.type = 'checkbox'; | |
| restartCheckbox.checked = autoRestartEnabled; | |
| restartCheckbox.id = 'restart_checkbox'; | |
| restartDiv.appendChild(restartCheckbox); | |
| const restartLabel = document.createElement('label'); | |
| restartLabel.htmlFor = 'restart_checkbox'; | |
| restartLabel.innerText = "Auto-restart after finish (60s)"; | |
| restartLabel.style.marginLeft = '8px'; | |
| restartLabel.style.color = darkModeEnabled ? '#ddd' : '#222'; | |
| restartDiv.appendChild(restartLabel); | |
| container.appendChild(restartDiv); | |
| restartCheckbox.addEventListener('change', () => { | |
| autoRestartEnabled = restartCheckbox.checked; | |
| localStorage.setItem(storageKeyAutoRestart, autoRestartEnabled ? "true" : "false"); | |
| notify(`Auto-restart ${autoRestartEnabled ? "enabled" : "disabled"}`); | |
| }); | |
| // 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 (unfollow page) | |
| if (isFollowingPage()) { | |
| 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); | |
| }); | |
| 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." | |
| ); | |
| }); | |
| } | |
| // Remove limit (followers page) | |
| if (isFollowersPage()) { | |
| 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 / Shortcuts | |
| 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/anon42' target='_blank' style='color: darkblue;'>@anon42</a></div>`; | |
| applyStyles(descDiv, { textAlign: 'center' }); | |
| container.appendChild(descDiv); | |
| applyHoverEffects(container); | |
| } | |
| // ~~~~~~~~~~~~~~~~~~~~~ MAIN ROUTINE (with conditional restart) ~~~~~~~~~~~~~~~~~~~~~ | |
| 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!"); | |
| if (autoRestartEnabled) { | |
| notify("Auto-restarting in 60 seconds..."); | |
| setTimeout(() => { | |
| if (!abortNow) { | |
| startMainRoutine(); | |
| } | |
| }, 60000); | |
| } | |
| } | |
| 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!"); | |
| if (autoRestartEnabled) { | |
| notify("Auto-restarting in 60 seconds..."); | |
| setTimeout(() => { | |
| if (!abortNow) { | |
| startMainRoutine(); | |
| } | |
| }, 60000); | |
| } | |
| } | |
| 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; | |
| if (ignoreList.includes(userHandle.toLowerCase())) continue; | |
| const verifiedIcon = accountElement.querySelector('svg[aria-label="Verified account"]'); | |
| const isVerified = !!verifiedIcon; | |
| if (isVerified) { | |
| if (!unfollowVerifiedNoFollowback) continue; | |
| if (doesFollowMe(accountElement)) continue; | |
| } | |
| 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) { | |
| 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(); | |
| } | |
| 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') || ''; | |
| return urlPath.split('/').pop().replace('@', '').toLowerCase(); | |
| } | |
| 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); | |
| } | |
| 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