Last active
October 14, 2025 13:20
-
-
Save ajohnclark/50ef0ba4b550c872170b3ef664136f5b to your computer and use it in GitHub Desktop.
Remove all X tweets/retweets
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
| // 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