Skip to content

Instantly share code, notes, and snippets.

@wilcollins
Created December 27, 2025 13:11
Show Gist options
  • Select an option

  • Save wilcollins/b9f2816aebe687ad6e8e9b0f759c3886 to your computer and use it in GitHub Desktop.

Select an option

Save wilcollins/b9f2816aebe687ad6e8e9b0f759c3886 to your computer and use it in GitHub Desktop.
X Undo Reposts
(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