Created
December 27, 2025 13:11
-
-
Save wilcollins/b9f2816aebe687ad6e8e9b0f759c3886 to your computer and use it in GitHub Desktop.
X Undo Reposts
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
| (async () => { | |
| // ============================ | |
| // X / Twitter "Undo Repost" bot (DOM clicker) | |
| // Paste into Chrome console on your profile page (Posts tab). | |
| // Stop anytime: window.__STOP_UNREPOST = true | |
| // ============================ | |
| window.__STOP_UNREPOST = false; | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // Try to find the "Undo repost" menu item after clicking the repost button | |
| const clickUndoFromMenu = async () => { | |
| // Give the menu time to appear | |
| await sleep(250); | |
| const menuItems = Array.from(document.querySelectorAll('div[role="menuitem"], a[role="menuitem"]')); | |
| const target = menuItems.find((el) => | |
| /undo repost|remove repost|unrepost|undo retweet|unretweet/i.test(el.innerText || "") | |
| ); | |
| if (!target) return false; | |
| target.click(); | |
| // Allow UI + network to settle | |
| await sleep(650); | |
| return true; | |
| }; | |
| // Detect if an article card is a repost *by you* | |
| const isRepostArticle = (article) => { | |
| // This usually exists above the post content: "You reposted" | |
| const social = article.querySelector('div[data-testid="socialContext"]'); | |
| if (social && /you reposted|reposted/i.test(social.innerText || "")) return true; | |
| // Fallback: look for any "reposted" context inside the article | |
| const text = article.innerText || ""; | |
| if (/you reposted/i.test(text)) return true; | |
| return false; | |
| }; | |
| // Try to find and click the repost/retweet button inside an article | |
| const clickRepostButton = async (article) => { | |
| // X has historically used data-testid="retweet" for the repost button | |
| let btn = | |
| article.querySelector('button[data-testid="retweet"]') || | |
| article.querySelector('button[aria-label*="Repost"]') || | |
| article.querySelector('button[aria-label*="repost"]') || | |
| article.querySelector('button[aria-label*="Retweet"]') || | |
| article.querySelector('button[aria-label*="retweet"]'); | |
| if (!btn) return false; | |
| btn.click(); | |
| await sleep(200); | |
| return true; | |
| }; | |
| // Avoid re-processing the same cards forever | |
| const seen = new WeakSet(); | |
| // Main loop controls | |
| const MAX_RUNTIME_MIN = 20; // safety cap | |
| const SCROLL_PAUSE_MS = 1200; // time to wait after scroll | |
| const BETWEEN_ACTION_MS = 500; // throttle clicks | |
| const start = Date.now(); | |
| console.log("[unrepost] Started. Set window.__STOP_UNREPOST = true to stop."); | |
| let totalUndone = 0; | |
| let idleRounds = 0; | |
| while (!window.__STOP_UNREPOST) { | |
| const runtimeMin = (Date.now() - start) / 60000; | |
| if (runtimeMin > MAX_RUNTIME_MIN) { | |
| console.warn(`[unrepost] Safety stop: exceeded ${MAX_RUNTIME_MIN} minutes.`); | |
| break; | |
| } | |
| const articles = Array.from(document.querySelectorAll("article")); | |
| let didSomethingThisRound = false; | |
| for (const article of articles) { | |
| if (window.__STOP_UNREPOST) break; | |
| if (seen.has(article)) continue; | |
| seen.add(article); | |
| if (!isRepostArticle(article)) continue; | |
| // Try to undo repost | |
| const clickedRepost = await clickRepostButton(article); | |
| if (!clickedRepost) continue; | |
| const undone = await clickUndoFromMenu(); | |
| if (undone) { | |
| totalUndone += 1; | |
| didSomethingThisRound = true; | |
| console.log(`[unrepost] Undid reposts: ${totalUndone}`); | |
| await sleep(BETWEEN_ACTION_MS); | |
| } else { | |
| // If menu didn’t show expected option, try closing any open menu with Escape | |
| document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); | |
| await sleep(150); | |
| } | |
| } | |
| if (!didSomethingThisRound) { | |
| idleRounds += 1; | |
| } else { | |
| idleRounds = 0; | |
| } | |
| // If we’ve gone idle a few rounds, scroll to load more | |
| if (idleRounds >= 2) { | |
| const before = document.body.scrollHeight; | |
| window.scrollBy(0, Math.floor(window.innerHeight * 0.9)); | |
| await sleep(SCROLL_PAUSE_MS); | |
| const after = document.body.scrollHeight; | |
| // If we aren't loading more, scroll a bit more aggressively | |
| if (after === before) { | |
| window.scrollBy(0, Math.floor(window.innerHeight * 1.5)); | |
| await sleep(SCROLL_PAUSE_MS); | |
| } | |
| idleRounds = 0; | |
| } else { | |
| // Small pause even if we did stuff, so the feed can reflow | |
| await sleep(300); | |
| } | |
| } | |
| console.log(`[unrepost] Stopped. Total undone: ${totalUndone}`); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment