Greasy Fork

Greasy Fork is available in English.

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.

当前为 2025-07-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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