X.com Profile Cleaner

Delete all your tweets and undo reposts from your X.com profile

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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.');
})();