Skip to content

Instantly share code, notes, and snippets.

@Isaac12x
Created September 15, 2025 11:46
Show Gist options
  • Select an option

  • Save Isaac12x/df31cced1049914b8f6b600d2e25e2cf to your computer and use it in GitHub Desktop.

Select an option

Save Isaac12x/df31cced1049914b8f6b600d2e25e2cf to your computer and use it in GitHub Desktop.
// 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