X Reply Deleter

Deletes your replies on X's /with_replies page. Run on x.com/yourusername/with_replies. Resumes after page reloads. Follow X's ToS.

// ==UserScript==
// @name         X Reply Deleter
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Deletes your replies on X's /with_replies page. Run on x.com/yourusername/with_replies. Resumes after page reloads. Follow X's ToS.
// @author       You
// @match        https://x.com/*/with_replies
// @grant        none
// @license AGPL
// ==/UserScript==

(async () => {
  'use strict';

  // Function to wait for an element
  const waitForElement = (selector, context = document, timeout = 8000, retries = 3) => {
    return new Promise((resolve, reject) => {
      let attempts = 0;
      const tryFind = () => {
        const element = context.querySelector(selector);
        if (element && element.offsetParent !== null) {
          console.log(`Found element ${selector}: Visible = ${element.offsetParent !== null}`);
          return resolve(element);
        }
        if (attempts >= retries) return reject(new Error(`Timeout waiting for ${selector} after ${retries} attempts`));
        attempts++;
        const start = Date.now();
        const interval = setInterval(() => {
          const el = context.querySelector(selector);
          if (el && el.offsetParent !== null) {
            clearInterval(interval);
            resolve(el);
          } else if (Date.now() - start > timeout) {
            clearInterval(interval);
            tryFind();
          }
        }, 200);
      };
      tryFind();
    });
  };

  // Function to simulate a click
  const clickElement = async (element, description) => {
    if (!element) {
      console.error(`No ${description} found.`);
      return false;
    }
    element.scrollIntoView({ behavior: 'smooth', block: 'center' });
    await new Promise(resolve => setTimeout(resolve, 500));
    element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    console.log(`Clicked ${description}`);
    return true;
  };

  // Function to scroll and wait for new content
  const scrollAndWait = async () => {
    let lastHeight = document.body.scrollHeight;
    let attempts = 0;
    const maxAttempts = 3;
    while (attempts < maxAttempts) {
      window.scrollTo(0, document.body.scrollHeight);
      await new Promise(resolve => setTimeout(resolve, 4000));
      const newHeight = document.body.scrollHeight;
      if (newHeight === lastHeight) break;
      lastHeight = newHeight;
      attempts++;
    }
    console.log("Finished scrolling. Waiting for replies to load...");
    const initialReplyCount = document.querySelectorAll('article[data-testid="tweet"]').length;
    await new Promise(resolve => setTimeout(resolve, 12000));
    const newReplyCount = document.querySelectorAll('article[data-testid="tweet"]').length;
    console.log(`Replies loaded: ${newReplyCount} (was ${initialReplyCount})`);
  };

  // Function to save progress
  const saveProgress = (batchCount, processedReplies, isRunning) => {
    sessionStorage.setItem('xReplyDeleterProgress', JSON.stringify({ batchCount, processedReplies, isRunning }));
    console.log(`Saved progress: batch ${batchCount}, processed ${processedReplies} replies, running: ${isRunning}`);
  };

  // Function to load progress
  const loadProgress = () => {
    const progress = sessionStorage.getItem('xReplyDeleterProgress');
    return progress ? JSON.parse(progress) : { batchCount: 0, processedReplies: 0, isRunning: false };
  };

  // Function to process a single reply
  const deleteReply = async (article, index) => {
    try {
      const tweetText = article.querySelector('div[data-testid="tweetText"]')?.innerText || "No text available";
      const userLink = article.querySelector('a[href*="/"]')?.href || "Unknown user";
      console.log(`Reply ${index + 1}: Processing reply from ${userLink}: "${tweetText.slice(0, 50)}..."`);
      console.log(`Reply ${index + 1}: Article HTML: ${article.outerHTML.slice(0, 200)}...`);

      article.scrollIntoView({ behavior: 'smooth', block: 'center' });
      await new Promise(resolve => setTimeout(resolve, 1000));

      const moreButtonSelector = 'button[data-testid="caret"]';
      let moreButton;
      try {
        moreButton = await waitForElement(moreButtonSelector, article, 8000, 3);
      } catch (error) {
        console.warn(`Reply ${index + 1}: ${error.message}. Trying alternative selector...`);
        moreButton = article.querySelector('button[aria-label="More"]');
        console.log(`Reply ${index + 1}: Alternative 'More' button: ${moreButton ? moreButton.outerHTML.slice(0, 200) : "Not found"}...`);
        if (!moreButton) return false;
      }
      if (!await clickElement(moreButton, "'More' button")) return false;
      await new Promise(resolve => setTimeout(resolve, 1500));

      const deleteButtonSelector = 'div[role="menuitem"]';
      const menuItems = document.querySelectorAll(deleteButtonSelector);
      let deleteButton = null;
      for (const item of menuItems) {
        const spans = item.querySelectorAll('span');
        for (const span of spans) {
          if (span.innerText.toLowerCase().includes("delete")) {
            deleteButton = item;
            break;
          }
        }
        if (deleteButton) break;
      }
      if (!deleteButton) {
        console.warn(`Reply ${index + 1}: No 'Delete' option found. Menu HTML: ${document.querySelector('div[role="menu"]')?.outerHTML.slice(0, 200) || "No menu"}...`);
        return false;
      }
      if (!await clickElement(deleteButton, "'Delete' option")) return false;
      await new Promise(resolve => setTimeout(resolve, 3000));

      const modal = document.querySelector('div[class*="css-175oi2r"][class*="r-13qz1uu"]') || document.body;
      console.log(`Reply ${index + 1}: Modal HTML: ${modal.outerHTML.slice(0, 200)}...`);

      const confirmButtonSelector = 'button[data-testid="confirmationSheetConfirm"]';
      const confirmButton = await waitForElement(confirmButtonSelector, document, 8000, 3);
      if (!await clickElement(confirmButton, "'Confirm Delete' button")) return false;
      console.log(`Reply ${index + 1}: Successfully deleted.`);
      return true;
    } catch (error) {
      console.error(`Reply ${index + 1}: Error deleting: ${error.message}`);
      return false;
    }
  };

  // Create control panel
  const createControlPanel = () => {
    const panel = document.createElement('div');
    panel.style.position = 'fixed';
    panel.style.top = '10px';
    panel.style.right = '10px';
    panel.style.zIndex = '10000';
    panel.style.background = '#fff';
    panel.style.border = '1px solid #000';
    panel.style.padding = '10px';
    panel.style.borderRadius = '5px';
    panel.innerHTML = `
      <h3>X Reply Deleter</h3>
      <p>Status: <span id="deleterStatus">Stopped</span></p>
      <p>Processed: <span id="deleterProcessed">0</span> replies in <span id="deleterBatch">0</span> batches</p>
      <button id="startDeleter">Start</button>
      <button id="stopDeleter" disabled>Stop</button>
      <p><small>Run on x.com/yourusername/with_replies. Follow X's ToS.</small></p>
    `;
    document.body.appendChild(panel);
    return panel;
  };

  // Main function
  const main = async () => {
    let { batchCount, processedReplies, isRunning } = loadProgress();
    let isStopped = !isRunning;
    const panel = createControlPanel();
    const status = panel.querySelector('#deleterStatus');
    const processedDisplay = panel.querySelector('#deleterProcessed');
    const batchDisplay = panel.querySelector('#deleterBatch');
    const startButton = panel.querySelector('#startDeleter');
    const stopButton = panel.querySelector('#stopDeleter');

    const updateUI = () => {
      status.textContent = isStopped ? 'Stopped' : 'Running';
      processedDisplay.textContent = processedReplies;
      batchDisplay.textContent = batchCount;
      startButton.disabled = !isStopped;
      stopButton.disabled = isStopped;
    };
    updateUI();

    startButton.addEventListener('click', () => {
      isStopped = false;
      isRunning = true;
      saveProgress(batchCount, processedReplies, isRunning);
      updateUI();
      runLoop();
    });

    stopButton.addEventListener('click', () => {
      isStopped = true;
      isRunning = false;
      saveProgress(batchCount, processedReplies, isRunning);
      updateUI();
      console.log('Stopped by user.');
    });

    const runLoop = async () => {
      let consecutiveBatchFailures = 0;
      const maxConsecutiveBatchFailures = 3;
      let isReloading = false;

      window.addEventListener('beforeunload', () => {
        if (!isReloading) {
          saveProgress(batchCount, processedReplies, isRunning);
          console.log('Page is refreshing. Progress saved.');
        }
      });

      while (!isStopped) {
        if (!window.location.href.includes('/with_replies')) {
          console.log('Not on /with_replies page. Stopping.');
          isStopped = true;
          isRunning = false;
          saveProgress(batchCount, processedReplies, isRunning);
          updateUI();
          return;
        }

        console.log(`Batch ${batchCount + 1}: Waiting for DOM to stabilize...`);
        await new Promise(resolve => setTimeout(resolve, 3000));

        const username = window.location.pathname.split('/')[1];
        const replies = Array.from(document.querySelectorAll('article[data-testid="tweet"]')).filter(article => {
          const userLink = article.querySelector('a[href*="/' + username + '"]');
          return userLink !== null;
        });

        if (!replies.length) {
          console.log('No more replies found. Attempting to load more...');
          await scrollAndWait();
          const newReplies = Array.from(document.querySelectorAll('article[data-testid="tweet"]')).filter(article => {
            const userLink = article.querySelector('a[href*="/' + username + '"]');
            return userLink !== null;
          });
          if (!newReplies.length) {
            console.log('No more replies found after scrolling. Refreshing page...');
            isReloading = true;
            saveProgress(batchCount, processedReplies, isRunning);
            location.reload();
            await new Promise(resolve => setTimeout(resolve, 5000));
            consecutiveBatchFailures++;
            if (consecutiveBatchFailures >= maxConsecutiveBatchFailures) {
              console.log('Too many consecutive batch failures. Exiting script.');
              isStopped = true;
              isRunning = false;
              saveProgress(batchCount, processedReplies, isRunning);
              sessionStorage.removeItem('xReplyDeleterProgress');
              updateUI();
              return;
            }
            continue;
          }
          replies.push(...newReplies);
        } else {
          consecutiveBatchFailures = 0;
        }

        console.log(`Batch ${batchCount + 1}: Found ${replies.length} replies on this page.`);
        let batchFailures = 0;
        const maxBatchFailures = 3;
        const batchTimeout = 60000;
        const batchStart = Date.now();

        for (const [index, reply] of replies.entries()) {
          if (isStopped) break;
          if (Date.now() - batchStart > batchTimeout) {
            console.log(`Batch ${batchCount + 1}: Timeout reached. Refreshing page...`);
            isReloading = true;
            saveProgress(batchCount, processedReplies, isRunning);
            location.reload();
            await new Promise(resolve => setTimeout(resolve, 5000));
            break;
          }
          if (await deleteReply(reply, index)) {
            batchFailures = 0;
            processedReplies++;
            saveProgress(batchCount, processedReplies, isRunning);
            updateUI();
          } else {
            batchFailures++;
            if (batchFailures >= maxBatchFailures) {
              console.log(`Batch ${batchCount + 1}: Too many failures (${batchFailures}). Refreshing page...`);
              isReloading = true;
              saveProgress(batchCount, processedReplies, isRunning);
              location.reload();
              await new Promise(resolve => setTimeout(resolve, 5000));
              break;
            }
          }
          await new Promise(resolve => setTimeout(resolve, 5000 + Math.random() * 1000));
        }

        if (isStopped) break;
        batchCount++;
        saveProgress(batchCount, processedReplies, isRunning);
        updateUI();
        console.log(`Batch ${batchCount}: Finished processing visible replies. Scrolling to load more...`);
        await scrollAndWait();
      }

      console.log(`Script completed. Processed ${processedReplies} replies across ${batchCount} batches.`);
      sessionStorage.removeItem('xReplyDeleterProgress');
      updateUI();
    };

    // Auto-start if was running before refresh
    if (isRunning) {
      console.log('Resuming from previous session...');
      runLoop();
    }
  };

  // Initialize
  main();
})();