Forked from apoorvlathey/x-twitter-fetch-commenters-usernames.js
Created
November 3, 2025 04:36
-
-
Save ironheart122/37ef296f031128b8eb5bc7fe79f011bc to your computer and use it in GitHub Desktop.
Auto scrolls and lists all the unique commenters that replied to a X (Twitter) post. Outputs easy to copy CSV
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
| // Slow smooth auto-scroll + @username extractor for X (Twitter) replies | |
| // - Human-like pacing | |
| // - Safely scoped clicks | |
| // - Outputs Array + CSV (copied to clipboard with fallbacks) | |
| async function extractAllUsernamesWithScroll({ | |
| scrollDelay = 2500, // wait between scroll cycles | |
| maxScrolls = 80, | |
| clickExpanders = true, | |
| scrollStep = 200, // px per mini step (smaller = smoother) | |
| stepDelay = 250, // ms between mini steps (longer = slower) | |
| exclude = [ | |
| '@apoorveth', | |
| '@Viraz04', | |
| '@gopiinho', | |
| '@basemaxxing', | |
| '@vikiival', | |
| '@Rozengarden_eth', | |
| '@BreakThePoint', | |
| ], | |
| } = {}) { | |
| const norm = s => s?.trim().toLowerCase(); | |
| const excludedSet = new Set(exclude.map(norm)); | |
| const seen = new Map(); | |
| const getConversationRoot = () => | |
| document.querySelector('div[aria-label="Timeline: Conversation"]') || | |
| document.querySelector('div[aria-label="Timeline: Replies"]') || | |
| document.querySelector('main[role="main"] section'); | |
| const extractUsernames = () => { | |
| const root = getConversationRoot() || document; | |
| const nodes = root.querySelectorAll('[data-testid="User-Name"] a[href^="/"] span'); | |
| nodes.forEach(n => { | |
| const t = n.textContent?.trim(); | |
| if (!t || !t.startsWith('@')) return; | |
| const lc = norm(t); | |
| if (!excludedSet.has(lc) && !seen.has(lc)) seen.set(lc, t); | |
| }); | |
| }; | |
| const clickMoreReplies = () => { | |
| if (!clickExpanders) return 0; | |
| const root = getConversationRoot(); | |
| if (!root) return 0; | |
| let clicked = 0; | |
| const btns = Array.from(root.querySelectorAll('button[role="button"]')); | |
| for (const b of btns) { | |
| if (b.getAttribute('disabled') !== null || b.offsetParent === null) continue; | |
| const label = (b.getAttribute('aria-label') || b.innerText || '').toLowerCase().trim(); | |
| const isReplyExpander = | |
| label.includes('show more replies') || | |
| label.includes('view more replies') || | |
| (label.includes('show more') && label.includes('repl')) || | |
| (label.includes('more') && label.includes('repl')); | |
| const inArticleOrReply = !!(b.closest('article,[data-testid="cellInnerDiv"]')); | |
| if (!isReplyExpander || !inArticleOrReply) continue; | |
| try { b.click(); clicked++; } catch {} | |
| } | |
| return clicked; | |
| }; | |
| const smoothScrollToBottom = async () => { | |
| let guard = 0; | |
| while (guard < 4000) { | |
| const maxY = document.documentElement.scrollHeight - window.innerHeight; | |
| if (window.scrollY >= maxY - 2) break; | |
| window.scrollBy(0, scrollStep); | |
| guard += scrollStep; | |
| await new Promise(r => setTimeout(r, stepDelay + Math.random() * 150)); | |
| } | |
| }; | |
| async function copyToClipboardSafe(text) { | |
| try { | |
| window.focus(); | |
| await new Promise(r => setTimeout(r, 300)); | |
| await navigator.clipboard.writeText(text); | |
| console.log('π CSV copied to clipboard!'); | |
| } catch (err) { | |
| console.warn('Clipboard write failed:', err); | |
| prompt('Copy usernames (one per line):', text); | |
| try { | |
| localStorage.setItem('extractedUsernamesCSV', text); | |
| console.log('πΎ Saved to localStorage as "extractedUsernamesCSV".'); | |
| } catch {} | |
| } | |
| } | |
| console.log('π Starting slow smooth auto-scroll...'); | |
| extractUsernames(); | |
| console.log(`Initial: ${seen.size} usernames (after exclusions)`); | |
| let lastCount = 0; | |
| let scrolls = 0; | |
| while (scrolls < maxScrolls) { | |
| const expanded = clickMoreReplies(); | |
| if (expanded > 0) { | |
| await new Promise(r => setTimeout(r, Math.max(800, scrollDelay / 2))); | |
| extractUsernames(); | |
| console.log(`βͺοΈ Expanded ${expanded}. Found so far: ${seen.size}`); | |
| } | |
| await smoothScrollToBottom(); | |
| await new Promise(r => setTimeout(r, scrollDelay + Math.random() * 500)); | |
| extractUsernames(); | |
| scrolls++; | |
| console.log(`Scroll #${scrolls}: ${seen.size} usernames (filtered)`); | |
| if (seen.size === lastCount) { | |
| console.log('π No new usernames detected, stopping.'); | |
| break; | |
| } | |
| lastCount = seen.size; | |
| } | |
| const result = Array.from(seen.values()); | |
| const csvText = result.join('\n'); | |
| console.log(`β Done! Total unique usernames (excluding ${excludedSet.size}): ${result.length}`); | |
| console.log('Array:', result); | |
| console.log('π CSV (one per line β paste into Google Sheets):\n' + csvText); | |
| await copyToClipboardSafe(csvText); | |
| return { result, csvText }; | |
| } | |
| // Run it: | |
| extractAllUsernamesWithScroll(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment