Created
September 15, 2025 11:46
-
-
Save Isaac12x/df31cced1049914b8f6b600d2e25e2cf to your computer and use it in GitHub Desktop.
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
| // Modified script from: https://github.com/nirholas/UnfollowX | |
| (() => { | |
| // Collect usernames from the "Following" / "Followers" list without clicking anything. | |
| // It keeps scrolling, extracting unique handles + URLs, then downloads a CSV at the end. | |
| const $followButtons = '[data-testid$="-unfollow"], [data-testid$="-follow"]'; // works on Following/Followers | |
| const $userCell = '[data-testid="UserCell"]'; | |
| const retry = { count: 0, limit: 3 }; | |
| const seen = new Set(); // track handles to avoid duplicates | |
| const rows = []; // [{username, url}] | |
| let lastCount = 0; | |
| const scrollToTheBottom = () => window.scrollTo(0, document.body.scrollHeight); | |
| const retryLimitReached = () => retry.count >= retry.limit; | |
| const resetRetry = () => (retry.count = 0); | |
| const addNewRetry = () => retry.count++; | |
| const sleep = (seconds) => | |
| new Promise((proceed) => { | |
| console.log(`WAITING FOR ${seconds} SECONDS...`); | |
| setTimeout(proceed, seconds * 1000); | |
| }); | |
| // Try to extract the profile anchor for a given user cell or button | |
| const findProfileAnchor = (rootEl) => { | |
| if (!rootEl) return null; | |
| // Common profile link pattern: /username (1-15 chars, letters/digits/underscore), not /status/... etc. | |
| const anchors = rootEl.querySelectorAll('a[role="link"][href^="/"]'); | |
| const profileRe = /^\/([A-Za-z0-9_]{1,15})\/?$/; | |
| for (const a of anchors) { | |
| const href = a.getAttribute('href') || ''; | |
| if (profileRe.test(href)) return a; | |
| } | |
| return null; | |
| }; | |
| const asAbsoluteUrl = (path) => { | |
| try { | |
| return new URL(path, location.origin).toString(); | |
| } catch { | |
| return `${location.origin}${path}`; | |
| } | |
| }; | |
| const recordUser = (username, url) => { | |
| const handle = username.replace(/^@/, ''); | |
| if (handle && !seen.has(handle)) { | |
| seen.add(handle); | |
| rows.push({ username: `@${handle}`, url }); | |
| } | |
| }; | |
| const extractFromUserCells = () => { | |
| const cells = Array.from(document.querySelectorAll($userCell)); | |
| let found = 0; | |
| for (const cell of cells) { | |
| const a = findProfileAnchor(cell); | |
| if (!a) continue; | |
| const href = a.getAttribute('href'); | |
| const match = href && href.match(/^\/([A-Za-z0-9_]{1,15})\/?$/); | |
| if (!match) continue; | |
| const handle = match[1]; | |
| const abs = asAbsoluteUrl(`/${handle}`); | |
| if (!seen.has(handle)) { | |
| recordUser(handle, abs); | |
| found++; | |
| } | |
| } | |
| return found; | |
| }; | |
| const extractFromFollowButtons = () => { | |
| // Fallback path: for each follow/unfollow button, climb to its user cell and grab the anchor | |
| const buttons = Array.from(document.querySelectorAll($followButtons)); | |
| let found = 0; | |
| for (const btn of buttons) { | |
| const cell = btn.closest($userCell) || btn.parentElement; | |
| const a = findProfileAnchor(cell || btn); | |
| if (!a) continue; | |
| const href = a.getAttribute('href'); | |
| const match = href && href.match(/^\/([A-Za-z0-9_]{1,15})\/?$/); | |
| if (!match) continue; | |
| const handle = match[1]; | |
| const abs = asAbsoluteUrl(`/${handle}`); | |
| if (!seen.has(handle)) { | |
| recordUser(handle, abs); | |
| found++; | |
| } | |
| } | |
| return found; | |
| }; | |
| const downloadCsv = () => { | |
| // Build CSV | |
| const header = 'username,url'; | |
| const lines = rows.map(({ username, url }) => { | |
| // Escape quotes if any (unlikely) | |
| const u = `"${username.replace(/"/g, '""')}"`; | |
| const l = `"${url.replace(/"/g, '""')}"`; | |
| return `${u},${l}`; | |
| }); | |
| const csv = [header, ...lines].join('\n'); | |
| const stamp = new Date() | |
| .toISOString() | |
| .replace(/[-:]/g, '') | |
| .replace(/\..+/, ''); | |
| const filename = `x_usernames_${stamp}.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 = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| console.log(`Downloaded ${rows.length} records to ${filename}`); | |
| }; | |
| const nextBatch = async () => { | |
| // Extract what's currently in view | |
| const foundCells = extractFromUserCells(); | |
| const foundBtns = extractFromFollowButtons(); | |
| const newCount = seen.size; | |
| if (newCount > lastCount) { | |
| console.log(`Collected ${newCount} unique profiles so far...`); | |
| lastCount = newCount; | |
| resetRetry(); | |
| } else { | |
| addNewRetry(); | |
| console.log(`No new profiles found (retry ${retry.count}/${retry.limit})`); | |
| } | |
| // Scroll to load more | |
| scrollToTheBottom(); | |
| await sleep(1.5); | |
| if (retryLimitReached()) { | |
| console.log(`No more new profiles detected. Finishing up...`); | |
| downloadCsv(); | |
| return; | |
| } else { | |
| await sleep(1.0); | |
| return nextBatch(); | |
| } | |
| }; | |
| console.log('Starting username/url collection...'); | |
| nextBatch(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment