Skip to content

Instantly share code, notes, and snippets.

@apoorvlathey
Last active November 4, 2025 00:44
Show Gist options
  • Select an option

  • Save apoorvlathey/1bff6476e46c58ef4ab504d9a6a51e2c to your computer and use it in GitHub Desktop.

Select an option

Save apoorvlathey/1bff6476e46c58ef4ab504d9a6a51e2c 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
// 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