X.com Profile Cleaner

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

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