Skip to content

Instantly share code, notes, and snippets.

@ajohnclark
Last active October 14, 2025 13:20
Show Gist options
  • Select an option

  • Save ajohnclark/50ef0ba4b550c872170b3ef664136f5b to your computer and use it in GitHub Desktop.

Select an option

Save ajohnclark/50ef0ba4b550c872170b3ef664136f5b to your computer and use it in GitHub Desktop.
Remove all X tweets/retweets
// Working as of 10/2025. Just go to profile page -> run in dev console. Deletes all retweets and tweets you made. May break / need to run a few times, janky but works.
// Run on profile page in dev console. Fix: avoid clicking anchors — only click button / role="button" elements.
(async function deleteAndUndoRetweets_safe({
actionDelay = 700,
scrollDelay = 1200,
maxScrolls = 800,
menuWait = 1200
} = {}) {
const sleep = ms => new Promise(r => setTimeout(r, ms));
const visible = el => !!el && el.offsetParent !== null;
const seen = new Set();
let deletedCount = 0;
let unretweetedCount = 0;
let scrolls = 0;
console.log('Started (safe) deleteAndUndoRetweets — actionDelay=', actionDelay);
function clickElement(el) {
if (!el) return false;
try {
el.click();
return true;
} catch (e) {
console.warn('click failed for', el, e);
return false;
}
}
// Click only a BUTTON or non-<a> element with role="button".
function findClickableButtons(container) {
return Array.from(container.querySelectorAll('button, [role="button"]'))
.filter(el => visible(el) && el.tagName !== 'A');
}
async function waitForMenu(timeout = menuWait) {
const start = Date.now();
while (Date.now() - start < timeout) {
const menu = Array.from(document.querySelectorAll('[role="menu"]')).find(visible);
if (menu) return menu;
await sleep(100);
}
return null;
}
while (scrolls < maxScrolls) {
const tweets = Array.from(document.querySelectorAll('article[data-testid="tweet"]'));
let anyActionThisPass = false;
for (const t of tweets) {
const statusLink = t.querySelector('a[href*="/status/"]');
const key = statusLink ? statusLink.getAttribute('href') : (t.innerText || '').slice(0, 80);
if (seen.has(key)) continue;
seen.add(key);
try { t.scrollIntoView({behavior: 'auto', block: 'center'}); } catch (e) {}
await sleep(actionDelay / 3);
// === RETWEET HANDLING (SAFELY: only button-like elements, no anchors) ===
const candidates = findClickableButtons(t);
const retweetBtn = candidates.find(el => {
const dt = (el.getAttribute && el.getAttribute('data-testid')) || '';
const aria = (el.getAttribute && el.getAttribute('aria-label')) || '';
const txt = (el.innerText || '').trim();
return /retweet|repost|undo repost|unretweet/i.test(dt + ' ' + aria + ' ' + txt);
});
if (retweetBtn) {
// click the retweet icon/button (does NOT navigate if it's a button)
if (clickElement(retweetBtn)) {
// wait for the small popover/menu to appear
const menu = await waitForMenu(menuWait);
if (menu) {
// prefer the exact data-testid first (your sample)
let undo = menu.querySelector('[data-testid="unretweetConfirm"]');
if (!undo) {
// fallback: text match inside the menu
undo = Array.from(menu.querySelectorAll('button, [role="menuitem"], [role="button"], div, span'))
.find(el => visible(el) && /undo\s*repost|undo\s*retweet|unretweet|remove\s*repost/i.test((el.innerText || '').trim()));
}
if (undo) {
// click undo — prefer a BUTTON if available inside or the element itself (but do NOT .closest('a'))
const undoButtons = findClickableButtons(undo);
const target = undoButtons[0] || undo;
if (clickElement(target)) {
unretweetedCount++;
anyActionThisPass = true;
console.log('Undid retweet (safe):', key, 'totalUndone:', unretweetedCount);
// allow UI to update and menu to close
await sleep(actionDelay);
continue; // next tweet
} else {
console.warn('Failed clicking undo target for', key);
}
} else {
// nothing to undo in the menu — close it cleanly
document.body.click();
await sleep(actionDelay / 2);
}
} else {
// no menu appeared — maybe the button itself toggled state; pause then continue
await sleep(actionDelay / 2);
}
} else {
console.warn('Could not click retweet button (safe) for', key);
}
}
// === DELETE FLOW (caret) - prefer buttons, avoid anchors when possible ===
const caretCandidates = findClickableButtons(t).filter(el => {
const dt = (el.getAttribute && el.getAttribute('data-testid')) || '';
const aria = (el.getAttribute && el.getAttribute('aria-haspopup')) || '';
return /caret|menu|more/i.test(dt) || aria === 'menu' || el.getAttribute('aria-haspopup') === 'true';
});
const caret = caretCandidates[0] || t.querySelector('button[data-testid="caret"], button[aria-haspopup="menu"]');
if (!caret) {
continue; // can't delete
}
if (!clickElement(caret)) {
await sleep(actionDelay);
continue;
}
await sleep(actionDelay);
// look for a Delete item inside the menu
let deleteBtn = Array.from(document.querySelectorAll('[role="menu"] button, [role="menuitem"], [role="menuitem"] *'))
.find(el => /delete/i.test((el.innerText || '').trim()) && visible(el));
if (!deleteBtn) {
deleteBtn = Array.from(document.querySelectorAll('button, [role="menuitem"], [role="button"], div, span'))
.find(el => visible(el) && /(^|\s)delete(\s|$)/i.test((el.innerText || '').trim()));
}
if (!deleteBtn) {
document.body.click();
await sleep(actionDelay / 2);
continue;
}
// Click delete (prefer a button)
const deleteButtons = findClickableButtons(deleteBtn);
const deleteTarget = deleteButtons[0] || deleteBtn;
if (!clickElement(deleteTarget)) {
document.body.click();
await sleep(actionDelay / 2);
continue;
}
await sleep(actionDelay);
const confirm = document.querySelector('[data-testid="confirmationSheetConfirm"], button[data-testid="confirmationSheetConfirm"]');
if (confirm && visible(confirm)) {
clickElement(confirm);
deletedCount++;
anyActionThisPass = true;
console.log('Deleted tweet:', key, 'totalDeleted:', deletedCount);
await sleep(actionDelay);
} else {
const modalDelete = Array.from(document.querySelectorAll('button, div, span, a'))
.find(el => visible(el) && /(^|\s)delete(\s|$)/i.test((el.innerText || '').trim()));
if (modalDelete) {
const modalBtns = findClickableButtons(modalDelete);
const modalTarget = modalBtns[0] || modalDelete;
clickElement(modalTarget);
deletedCount++;
anyActionThisPass = true;
console.log('Deleted via fallback:', key, 'totalDeleted:', deletedCount);
} else {
console.warn('Delete confirmation not found for', key);
document.body.click();
}
}
await sleep(actionDelay + 150);
} // end for tweets
// scroll to load more
const prevHeight = document.body.scrollHeight;
window.scrollTo({ top: document.body.scrollHeight, behavior: 'auto' });
await sleep(scrollDelay);
scrolls++;
const newHeight = document.body.scrollHeight;
if (!anyActionThisPass && newHeight === prevHeight) {
console.log('No actions this pass and page height unchanged — finished.');
break;
}
} // end while
console.log('Done. deleted:', deletedCount, 'unretweeted:', unretweetedCount, 'scrolls:', scrolls);
return { deleted: deletedCount, unretweeted: unretweetedCount, scrolls };
})().catch(e => console.error('Error in deleteAndUndoRetweets_safe:', e));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment