Last active
February 13, 2026 02:39
-
-
Save durkes/e3f95c76530df0a0cb3f2278f32c9ffc to your computer and use it in GitHub Desktop.
Script to delete all X (Twitter) posts and replies
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
| /* While signed in, navigate to your Profile > Replies page | |
| and run the following script from DevTools Console */ | |
| (() => { | |
| const sleep = (ms) => new Promise(r => setTimeout(r, ms)); | |
| const normalize = (s) => (s || "").replace(/\s+/g, " ").trim().toLowerCase(); | |
| const state = { | |
| running: false, | |
| processed: new Set(), | |
| deleted: 0, | |
| undidReposts: 0, | |
| skipped: 0, | |
| step: 0, | |
| noNewRounds: 0, | |
| lastProcessedCount: 0, | |
| lastTargetKey: null | |
| }; | |
| const getArticleKey = (article) => { | |
| const link = article.querySelector('a[href*="/status/"]')?.href; | |
| if (link) return link; | |
| const dt = article.querySelector("time")?.getAttribute("datetime") || ""; | |
| const txt = article.innerText?.slice(0, 120) || ""; | |
| return normalize(dt + "|" + txt); | |
| }; | |
| const findCaretButton = (article) => { | |
| return article.querySelector('[data-testid="caret"]'); | |
| }; | |
| const findRepostButton = (article) => { | |
| const buttons = article.querySelectorAll('button[data-testid="unretweet"], button[aria-label*="Repost"], button[aria-label*="repost"]'); | |
| for (const btn of buttons) { | |
| const label = normalize(btn.getAttribute("aria-label") || btn.innerText || ""); | |
| if (label.includes("repost") || label.includes("reposted") || label.includes("undo") || label.includes("remove")) { | |
| return btn; | |
| } | |
| } | |
| return null; | |
| }; | |
| const isRepost = (article) => { | |
| return Array.from(article.querySelectorAll('*')).some(el => | |
| normalize(el.innerText).includes("you reposted") || normalize(el.innerText).includes("reposted") | |
| ); | |
| }; | |
| const highlight = (article) => { | |
| article.style.outline = "3px solid red"; | |
| article.style.outlineOffset = "4px"; | |
| }; | |
| const clearHighlights = () => { | |
| document.querySelectorAll("article").forEach(a => { | |
| a.style.outline = ""; | |
| a.style.outlineOffset = ""; | |
| }); | |
| }; | |
| const findMenuItem = (textIncludes) => { | |
| const nodes = Array.from(document.querySelectorAll("[role='menuitem']")); | |
| for (const n of nodes) { | |
| const t = normalize(n.innerText); | |
| if (t.includes(textIncludes)) return n; | |
| } | |
| return null; | |
| }; | |
| const findConfirmButton = () => { | |
| // Primary: reliable data-testid used in all recent scripts | |
| let btn = document.querySelector('[data-testid="confirmationSheetConfirm"]'); | |
| if (btn) return btn; | |
| // Fallback: look for button with "Delete" text (red button) | |
| const buttons = document.querySelectorAll('button, [role="button"]'); | |
| for (const b of buttons) { | |
| if (normalize(b.innerText) === "delete") { | |
| return b; | |
| } | |
| } | |
| return null; | |
| }; | |
| const findNextTarget = () => { | |
| const articles = Array.from(document.querySelectorAll("article")); | |
| for (const article of articles) { | |
| const key = getArticleKey(article); | |
| if (!key || state.processed.has(key)) continue; | |
| const caret = findCaretButton(article); | |
| const repostBtn = findRepostButton(article); | |
| if (repostBtn || caret) { | |
| return { article, caret, repostBtn, key, isRepost: !!repostBtn && isRepost(article) }; | |
| } | |
| } | |
| return null; | |
| }; | |
| const slowScrollLoad = async () => { | |
| window.scrollTo(0, document.body.scrollHeight); | |
| await sleep(2000); | |
| }; | |
| const runStep = async () => { | |
| state.step++; | |
| clearHighlights(); | |
| const t = findNextTarget(); | |
| if (!t) return false; | |
| state.lastTargetKey = t.key; | |
| highlight(t.article); | |
| t.article.scrollIntoView({ behavior: "smooth", block: "center" }); | |
| await sleep(1500); | |
| let success = false; | |
| if (t.repostBtn && t.isRepost) { | |
| console.log(`[step ${state.step}] Undoing repost...`); | |
| t.repostBtn.click(); | |
| await sleep(1200); | |
| for (let i = 0; i < 3; i++) { | |
| const undoItem = findMenuItem("undo repost") || findMenuItem("remove repost"); | |
| if (undoItem) { | |
| undoItem.click(); | |
| await sleep(2000); | |
| state.undidReposts++; | |
| success = true; | |
| break; | |
| } | |
| await sleep(800); | |
| } | |
| } else if (t.caret) { | |
| console.log(`[step ${state.step}] Deleting own post/reply...`); | |
| t.caret.click(); | |
| await sleep(1200); | |
| for (let i = 0; i < 3; i++) { | |
| const deleteItem = findMenuItem("delete"); | |
| if (deleteItem) { | |
| deleteItem.click(); | |
| await sleep(1500); // Slightly longer wait for confirmation dialog | |
| for (let j = 0; j < 5; j++) { // More retries for confirmation | |
| const confirmBtn = findConfirmButton(); | |
| if (confirmBtn) { | |
| confirmBtn.click(); | |
| console.log("Confirmation 'Delete' button clicked."); | |
| await sleep(2500); | |
| state.deleted++; | |
| success = true; | |
| break; | |
| } | |
| await sleep(800); | |
| } | |
| break; | |
| } | |
| await sleep(800); | |
| } | |
| } | |
| if (success) { | |
| state.processed.add(t.key); | |
| return true; | |
| } else { | |
| console.warn("Failed to process this post. Skipping to avoid infinite loop."); | |
| state.skipped++; | |
| state.processed.add(t.key); | |
| return true; | |
| } | |
| }; | |
| const loop = async () => { | |
| if (state.running) return; | |
| state.running = true; | |
| console.log("Running full cleanup: deleting posts/replies + undoing reposts. Use window.__cleanupHelper.stop() to stop."); | |
| while (state.running) { | |
| const did = await runStep(); | |
| if (did) continue; | |
| await slowScrollLoad(); | |
| if (state.processed.size === state.lastProcessedCount) state.noNewRounds++; | |
| else state.noNewRounds = 0; | |
| state.lastProcessedCount = state.processed.size; | |
| if (state.noNewRounds >= 8) { | |
| console.log("No more posts found after extensive scrolling. Done!"); | |
| console.log(`Stats: Deleted: ${state.deleted}, Undid reposts: ${state.undidReposts}, Skipped: ${state.skipped}`); | |
| break; | |
| } | |
| } | |
| state.running = false; | |
| }; | |
| window.__cleanupHelper = { | |
| start: () => loop(), | |
| stop: () => { state.running = false; console.log("Stopped."); }, | |
| stats: () => ({ | |
| steps: state.step, | |
| deleted: state.deleted, | |
| undidReposts: state.undidReposts, | |
| skipped: state.skipped, | |
| processed: state.processed.size, | |
| lastTarget: state.lastTargetKey | |
| }) | |
| }; | |
| loop(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Well done! This is working as of 2026-02-12