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