// ==UserScript==
// @name X.com Profile Cleaner
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Delete all your tweets and undo reposts from your X.com profile
// @author d_g_t_l (https://x.com/d_g_t_l)
// @match https://x.com/*
// @match https://twitter.com/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let isRunning = false;
let deletedCount = 0;
let unrepostedCount = 0;
let targetUsername = GM_getValue('targetUsername', '');
// Draggable functionality
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = GM_getValue('panelX', 0);
let yOffset = GM_getValue('panelY', 0);
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Timeout waiting for ${selector}`));
}, timeout);
});
}
function waitForElementGone(element, timeout = 10000) {
return new Promise((resolve, reject) => {
if (!document.contains(element)) {
return resolve();
}
const observer = new MutationObserver(() => {
if (!document.contains(element)) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error('Timeout waiting for element removal'));
}, timeout);
});
}
function isMyTweet(article) {
if (article.hasAttribute('data-not-mine')) {
return false;
}
if (!targetUsername) {
return false;
}
const usernameElement = article.querySelector('[data-testid="User-Name"]');
if (!usernameElement) {
return false;
}
const usernameText = usernameElement.textContent;
const hasUsername = usernameText.includes('@' + targetUsername);
return hasUsername;
}
function isMyRepost(article) {
const repostIndicator = article.querySelector('[data-testid="socialContext"]');
if (repostIndicator && repostIndicator.textContent.includes('You reposted')) {
return true;
}
return false;
}
function markAsNotMine(article) {
article.setAttribute('data-not-mine', 'true');
article.style.opacity = '0.3';
console.log('Marked tweet as not mine');
}
function findDeleteButton() {
const buttons = document.querySelectorAll('[role="button"], button');
for (const button of buttons) {
const text = button.textContent.toLowerCase().trim();
if (text === 'delete') {
return button;
}
}
const confirmButton = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmButton) {
return confirmButton;
}
return null;
}
async function clickDeleteConfirmation() {
return new Promise((resolve) => {
let clicked = false;
const checkAndClick = () => {
if (clicked) return;
const deleteBtn = findDeleteButton();
if (deleteBtn) {
clicked = true;
console.log('Clicking Delete confirmation');
deleteBtn.click();
observer.disconnect();
clearTimeout(timeoutId);
resolve(true);
}
};
checkAndClick();
if (clicked) return;
const observer = new MutationObserver(() => {
checkAndClick();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const timeoutId = setTimeout(() => {
observer.disconnect();
if (!clicked) {
console.log('No confirmation dialog found');
resolve(false);
}
}, 3000);
});
}
async function undoRepost(article) {
try {
console.log('Undoing repost');
const repostButton = article.querySelector('[data-testid="unretweet"]');
if (!repostButton) {
console.log('Repost button not found');
return false;
}
repostButton.click();
await waitForElement('[role="menuitem"]', 3000);
const menuItems = document.querySelectorAll('[role="menuitem"]');
let undoItem = null;
for (const item of menuItems) {
const text = item.textContent.toLowerCase();
if (text.includes('undo')) {
undoItem = item;
break;
}
}
if (undoItem) {
console.log('Clicking Undo repost');
undoItem.click();
await waitForElementGone(article, 5000);
console.log('Repost undone');
unrepostedCount++;
updateCounter();
return true;
} else {
console.log('Undo option not found');
document.body.click();
return false;
}
} catch (error) {
console.error('Error undoing repost:', error);
document.body.click();
return false;
}
}
async function deleteSingleTweet(moreButton) {
try {
const article = moreButton.closest('article[data-testid="tweet"]');
if (!article) {
console.log('Could not find article element');
return false;
}
console.log('Clicking More button');
moreButton.click();
await waitForElement('[role="menuitem"]', 3000);
const menuItems = document.querySelectorAll('[role="menuitem"]');
let deleteMenuItem = null;
for (const item of menuItems) {
const text = item.textContent.toLowerCase();
if (text.includes('delete')) {
deleteMenuItem = item;
break;
}
}
if (deleteMenuItem) {
console.log('Clicking Delete menu item');
deleteMenuItem.click();
await clickDeleteConfirmation();
await waitForElementGone(article, 5000);
console.log('Tweet deleted');
deletedCount++;
updateCounter();
return true;
} else {
console.log('No Delete option, this is not my tweet');
document.body.click();
markAsNotMine(article);
return false;
}
} catch (error) {
console.error('Error deleting tweet:', error);
document.body.click();
return false;
}
}
function findActionableItems() {
const items = [];
const articles = document.querySelectorAll('article[data-testid="tweet"]');
for (const article of articles) {
if (article.hasAttribute('data-not-mine')) {
continue;
}
if (isMyRepost(article)) {
const repostButton = article.querySelector('[data-testid="unretweet"]');
if (repostButton) {
items.push({ type: 'repost', article, button: repostButton });
}
} else if (isMyTweet(article)) {
const moreButton = article.querySelector('[data-testid="caret"]');
if (moreButton) {
items.push({ type: 'tweet', article, button: moreButton });
}
}
}
return items;
}
async function processAllVisibleItems() {
while (isRunning) {
const items = findActionableItems();
if (items.length === 0) {
console.log('No items to process');
return false;
}
console.log(`Found ${items.length} items to process`);
const firstItem = items[0];
if (firstItem.type === 'repost') {
await undoRepost(firstItem.article);
} else {
await deleteSingleTweet(firstItem.button);
}
await new Promise(resolve => setTimeout(resolve, 50));
}
}
function waitForNewContent(currentCount, timeout = 5000) {
return new Promise((resolve) => {
const startTime = Date.now();
const checkInterval = setInterval(() => {
const newCount = findActionableItems().length;
const elapsed = Date.now() - startTime;
if (newCount > currentCount) {
console.log(`New items appeared: ${newCount - currentCount}`);
clearInterval(checkInterval);
resolve(true);
} else if (elapsed > timeout) {
console.log('Timeout waiting for new items');
clearInterval(checkInterval);
resolve(false);
}
}, 200);
});
}
async function scrollAndWait() {
const currentCount = findActionableItems().length;
console.log(`Scrolling down... Current items: ${currentCount}`);
window.scrollTo(0, document.documentElement.scrollHeight);
const loaded = await waitForNewContent(currentCount, 3000);
return loaded;
}
function updateCounter() {
countDisplay.textContent = `Deleted: ${deletedCount} | Unreposted: ${unrepostedCount}`;
}
function saveUsername() {
const username = usernameInput.value.trim().replace('@', '');
if (username) {
targetUsername = username;
GM_setValue('targetUsername', username);
statusText.textContent = `Active for @${username}`;
statusText.style.color = '#0f0';
console.log('Username saved:', username);
} else {
statusText.textContent = 'Enter username first!';
statusText.style.color = '#f00';
}
}
async function toggleDeleting() {
if (!targetUsername) {
statusText.textContent = 'Enter username first!';
statusText.style.color = '#f00';
return;
}
if (isRunning) {
isRunning = false;
toggleBtn.textContent = 'Start Cleaning';
toggleBtn.style.background = '#fff';
toggleBtn.style.color = '#1d9bf0';
console.log('Stopped');
return;
}
isRunning = true;
toggleBtn.textContent = 'Stop';
toggleBtn.style.background = '#fff';
toggleBtn.style.color = '#f00';
console.log('Starting deletion...');
let noNewContentCount = 0;
while (isRunning) {
await processAllVisibleItems();
if (!isRunning) break;
const hasMore = findActionableItems().length > 0;
if (!hasMore) {
const loaded = await scrollAndWait();
if (!loaded) {
noNewContentCount++;
if (noNewContentCount >= 3) {
console.log('No more items to load');
statusText.textContent = 'Finished cleaning!';
statusText.style.color = '#0f0';
break;
}
} else {
noNewContentCount = 0;
}
}
}
isRunning = false;
toggleBtn.textContent = 'Start Cleaning';
toggleBtn.style.background = '#fff';
toggleBtn.style.color = '#1d9bf0';
console.log('Finished');
}
// Draggable functions
function dragStart(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') {
return;
}
e.preventDefault();
e.stopPropagation();
if (e.type === 'touchstart') {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
isDragging = true;
controlPanel.style.cursor = 'grabbing';
document.body.style.overflow = 'hidden';
document.body.style.userSelect = 'none';
}
function drag(e) {
if (isDragging) {
e.preventDefault();
e.stopPropagation();
if (e.type === 'touchmove') {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, controlPanel);
}
}
function dragEnd(e) {
if (isDragging) {
e.preventDefault();
e.stopPropagation();
initialX = currentX;
initialY = currentY;
isDragging = false;
controlPanel.style.cursor = 'grab';
document.body.style.overflow = '';
document.body.style.userSelect = '';
// Save position
GM_setValue('panelX', xOffset);
GM_setValue('panelY', yOffset);
}
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
// Create control panel
const controlPanel = document.createElement('div');
controlPanel.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
z-index: 10000;
background: #1d9bf0;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
gap: 10px;
align-items: stretch;
min-width: 250px;
cursor: grab;
user-select: none;
touch-action: none;
`;
// Set initial position
setTranslate(xOffset, yOffset, controlPanel);
// Add drag event listeners
controlPanel.addEventListener('mousedown', dragStart, { passive: false });
controlPanel.addEventListener('touchstart', dragStart, { passive: false });
document.addEventListener('mousemove', drag, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('mouseup', dragEnd, { passive: false });
document.addEventListener('touchend', dragEnd, { passive: false });
// Drag handle indicator
const dragHandle = document.createElement('div');
dragHandle.textContent = '⋮⋮';
dragHandle.style.cssText = `
color: rgba(255,255,255,0.5);
font-size: 16px;
text-align: center;
letter-spacing: 2px;
margin: -5px 0 5px 0;
cursor: grab;
touch-action: none;
`;
// Username input
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'Enter your username';
usernameInput.value = targetUsername;
usernameInput.style.cssText = `
padding: 8px;
border: none;
border-radius: 5px;
font-size: 14px;
cursor: text;
`;
// Save button
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save Username';
saveBtn.style.cssText = `
padding: 8px 15px;
background: #fff;
color: #1d9bf0;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
`;
saveBtn.onclick = saveUsername;
// Status text
const statusText = document.createElement('div');
statusText.textContent = targetUsername ? `Active for @${targetUsername}` : 'No username set';
statusText.style.cssText = `
color: ${targetUsername ? '#0f0' : '#fff'};
font-weight: bold;
font-size: 12px;
text-align: center;
`;
// Toggle button
const toggleBtn = document.createElement('button');
toggleBtn.textContent = 'Start Cleaning';
toggleBtn.style.cssText = `
padding: 10px 20px;
background: #fff;
color: #1d9bf0;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
`;
toggleBtn.onclick = toggleDeleting;
// Counter display
const countDisplay = document.createElement('div');
countDisplay.textContent = `Deleted: ${deletedCount} | Unreposted: ${unrepostedCount}`;
countDisplay.style.cssText = `
color: #fff;
font-weight: bold;
font-size: 14px;
text-align: center;
`;
// Divider
const divider = document.createElement('hr');
divider.style.cssText = `
border: none;
border-top: 1px solid rgba(255,255,255,0.3);
margin: 5px 0;
`;
controlPanel.appendChild(dragHandle);
controlPanel.appendChild(usernameInput);
controlPanel.appendChild(saveBtn);
controlPanel.appendChild(statusText);
controlPanel.appendChild(divider);
controlPanel.appendChild(toggleBtn);
controlPanel.appendChild(countDisplay);
document.body.appendChild(controlPanel);
console.log('X.com Profile Cleaner loaded. Enter your username to start.');
})();