8000 GitHub - ajwdd/XDeleter: πŸ—‘οΈ The best method to delete and unretweet tweets on Twitter/X.
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

ajwdd/XDeleter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

XDeleter πŸ—‘οΈ

JavaScript Stars Size License: MIT IsMaintained

Want to rebrand? Made some bad decisions? Change your position?

No matter your need XDeleter is the perfect fit.

πŸ”§ Features

  • Batch Removal: Rapid Tweet deletion/unretweet.
  • User-Friendly: Simple to use, including a straight-forward UI for all input.
  • Many Options: Unretweet, Ignore Pinned Tweet, Delete if Keywords Match (comma-separated list), Ignore Tweet IDs (comma-separated list), Delete Specific Tweet IDs Only (comma-separated list), Before and End Date Deletion Ranges, Delete Non-media Tweets Only, and Delete Tweets with Links Only.

πŸ“œ Instructions

  1. Navigate to your profile's Replies page: https://www.x.com/yourusername/with_replies.

  2. Open the browser's console and navigate to "Network":

    • Linux, Windows, ChromeOS: Ctrl + Shift + J
    • macOS: Cmd + Option + J
  3. Refresh the page (if token file for step 5 & 6 doesn't appear).

  4. Copy the entirety of XDeleter.js and paste into the console, press Enter.

  5. Locate Bearer Token.

    authtoken

  6. Locate Client ID.

    xclientid

  7. Enter username.

  8. Select options.

  9. Click the start button.

πŸ’Ύ Script:

// __  ______       _      _
// \ \/ /  _ \  ___| | ___| |_ ___ _ __
//  \  /| | | |/ _ \ |/ _ \ __/ _ \ '__|
//  /  \| |_| |  __/ |  __/ ||  __/ |
// /_/\_\____/ \___|_|\___|\__\___|_|
// v1.0.1    https://github.com/ajwdd

// DON'T EDIT THIS FILE DIRECTLY!
(function () {
  let authorization = "";
  let client_tid = "";
  let username = "";
  let csrf_token = "";
  let user_id = "";
  let ua = "";
  let language_code = "";

  // Configuration for deletion criteria
  let delete_options = {
    unretweet: true,
    do_not_remove_pinned_tweet: true,
    delete_message_with_url_only: false,
    delete_tweets_without_media: false,
    delete_specific_ids_only: [""],
    match_any_keywords: [""],
    tweets_to_ignore: [],
    after_date: new Date("1900-01-01"),
    before_date: new Date("2100-01-01"),
  };

  const random_resource = "dGUkXXo_EoCL_NimyHrJ-Q";
  const delete_tweet_graphql_id = "VaenaVgh5q5ih7kvyVjgtg";
  const delete_tweet_transaction_id =
    "LuSa1GYxAMxWEugf+FtQ/wjCAUkipMAU3jpjkil3ujj7oq6munDCtNaMaFmZ8bcm7CaNvi4GIXj32jp7q32nZU8zc5CyLw";

  // Runtime variables
  let tweets_to_delete = [];
  let stop_signal = false;
  let is_processing = false;

  const IGNORE_IDS_STORAGE_KEY = "xdeleter_ignore_ids";
  const BEARER_TOKEN_STORAGE_KEY = "xdeleter_bearer_token";
  const USERNAME_STORAGE_KEY = "xdeleter_username";

  // UI Element Variables
  let uiContainer,
    logAreaElement,
    authTokenInput,
    clientIdInput,
    usernameInput,
    unretweetCheckbox,
    keepPinnedCheckbox,
    urlOnlyCheckbox,
    deleteWithoutMediaCheckbox,
    keywordsInput,
    ignoreIdsInput,
    afterDateInput,
    beforeDateInput,
    specificIdsInput,
    startButton,
    stopButton,
    closeButton;

  // CSS styles for the UI
  const styles = `
        :root {
            --xdeleter-primary-color: #1DA1F2;
            --xdeleter-primary-hover-color: #0c85d0;
            --xdeleter-danger-color: #E0245E;
            --xdeleter-danger-hover-color: #c01f4c;
            --xdeleter-warning-color: #FFAD1F;
            --xdeleter-text-color: #e1e8ed;
            --xdeleter-bg-color: rgba(21, 32, 43, 0.97);
            --xdeleter-border-color: rgba(56, 68, 77, 0.7);
            --xdeleter-input-bg-color: rgba(30, 39, 50, 0.8);
            --xdeleter-container-shadow: rgba(0, 0, 0, 0.6);
            --xdeleter-button-shadow: rgba(0, 0, 0, 0.3);
            --xdeleter-border-radius: 12px;
            --xdeleter-transition-speed-fast: 0.15s;
            --xdeleter-font-family: "TwitterChirp", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }
        .xdeleter-ui-container {
            position: fixed; top: 20px; right: 20px; width: 380px;
            max-height: calc(100vh - 40px); overflow-y: auto; overflow-x: hidden; /* Adjusted max-height */
            background-color: var(--xdeleter-bg-color); color: var(--xdeleter-text-color);
            padding: 20px; border-radius: var(--xdeleter-border-radius); z-index: 100001;
            font-family: var(--xdeleter-font-family);
            box-shadow: 0 8px 25px var(--xdeleter-container-shadow);
            backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
            border: 1px solid var(--xdeleter-border-color);
            display: flex; flex-direction: column;
        }
        .xdeleter-ui-container h2, .xdeleter-ui-container h3 {
            color: var(--xdeleter-primary-color); text-align: center; margin-bottom: 15px;
        }
        .xdeleter-ui-container h2 { font-size: 1.4em; }
        .xdeleter-ui-container h3 {
            margin-top: 20px; border-bottom: 1px solid var(--xdeleter-border-color);
            padding-bottom: 8px; font-size: 1.1em;
        }
        .xdeleter-ui-close-btn {
            position: absolute; top: 12px; right: 15px; cursor: pointer;
            font-size: 28px; line-height: 1; font-weight: bold;
            color: #8899a6; transition: color var(--xdeleter-transition-speed-fast) ease, transform var(--xdeleter-transition-speed-fast) ease;
        }
        .xdeleter-ui-close-btn:hover { color: var(--xdeleter-primary-color); transform: rotate(90deg) scale(1.1); }

        .xdeleter-section { margin-bottom: 15px; }
        .xdeleter-input-group, .xdeleter-option-group { margin-bottom: 12px; }
        .xdeleter-input-group label, .xdeleter-option-group label {
            display: block; margin-bottom: 6px; font-size: 14px; color: #bdc3c7;
        }
        .xdeleter-input-group input[type="text"],
        .xdeleter-input-group input[type="date"],
        .xdeleter-input-group textarea {
            width: calc(100% - 22px); padding: 9px 10px; border-radius: 6px;
            border: 1px solid var(--xdeleter-border-color); background-color: var(--xdeleter-input-bg-color);
            color: var(--xdeleter-text-color); font-size: 14px;
            font-family: var(--xdeleter-font-family);
        }
        .xdeleter-input-group textarea { resize: vertical; min-height: 60px; }
        .xdeleter-input-group input[type="text"]:focus,
        .xdeleter-input-group input[type="date"]:focus,
        .xdeleter-input-group textarea:focus {
            border-color: var(--xdeleter-primary-color); outline: none; box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.3);
        }
        .xdeleter-option-group input[type="checkbox"] {
            margin-right: 8px; vertical-align: middle; accent-color: var(--xdeleter-primary-color);
        }
        .xdeleter-button-group {
            display: flex; justify-content: space-around; margin-top: 20px; margin-bottom: 10px;
        }
        .xdeleter-control-button {
            padding: 10px 18px; border: none; border-radius: 20px; cursor: pointer;
            font-weight: bold; font-size: 14px; color: white;
            transition: background-color var(--xdeleter-transition-speed-fast) ease, transform var(--xdeleter-transition-speed-fast) ease, box-shadow var(--xdeleter-transition-speed-fast) ease;
            box-shadow: 0 2px 4px var(--xdeleter-button-shadow);
        }
        .xdeleter-control-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 8px var(--xdeleter-button-shadow); }
        .xdeleter-control-button:disabled { opacity: 0.6; cursor: not-allowed; }
        .xdeleter-btn-start { background-color: var(--xdeleter-primary-color); }
        .xdeleter-btn-start:hover:not(:disabled) { background-color: var(--xdeleter-primary-hover-color); }
        .xdeleter-btn-stop { background-color: var(--xdeleter-danger-color); }
        .xdeleter-btn-stop:hover:not(:disabled) { background-color: var(--xdeleter-danger-hover-color); }

        .xdeleter-log-area {
            background-color: rgba(0,0,0,0.25); border: 1px solid var(--xdeleter-border-color);
            border-radius: 6px; padding: 10px; min-height: 100px; max-height: 250px; /* Ensure log area is scrollable */
            overflow-y: auto; font-size: 12px; line-height: 1.6; white-space: pre-wrap;
            word-break: break-word;
        }
        .xdeleter-ui-container::-webkit-scrollbar, .xdeleter-log-area::-webkit-scrollbar { width: 8px; }
        .xdeleter-ui-container::-webkit-scrollbar-track, .xdeleter-log-area::-webkit-scrollbar-track {
            background: rgba(0,0,0,0.1); border-radius: 10px;
        }
        .xdeleter-ui-container::-webkit-scrollbar-thumb, .xdeleter-log-area::-webkit-scrollbar-thumb {
            background-color: var(--xdeleter-primary-color); border-radius: 10px;
            border: 2px solid var(--xdeleter-bg-color);
        }
    `;

  function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(";").shift();
    return null;
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function buildAcceptLanguageString() {
    const languages = navigator.languages;
    if (!languages || languages.length === 0) {
      return "en-US,en;q=0.9"; // Default fallback
    }
    let q = 1;
    const decrement = 0.1;
    return languages
      .map((lang) => {
        const currentQ = q;
        q = Math.max(0.1, q - decrement);
        if (currentQ < 1) {
          return `${lang};q=${currentQ.toFixed(1)}`;
        }
        return lang;
      })
      .join(",");
  }

  function logToUI(message, isError = false) {
    if (logAreaElement) {
      const timestamp = new Date().toLocaleTimeString();
      const logEntry = document.createElement("div");
      logEntry.textContent = `[${timestamp}] ${message}`;
      if (isError) {
        logEntry.style.color = "var(--xdeleter-danger-color)";
        logEntry.style.fontWeight = "bold";
      }
      logAreaElement.appendChild(logEntry);
      logAreaElement.scrollTop = logAreaElement.scrollHeight; // Auto-scroll to bottom
    }
    if (isError) {
      console.error(message);
    } else {
      console.log(message);
    }
  }

  function createUI() {
    uiContainer = document.createElement("div");
    uiContainer.id = "xdeleter-ui";
    uiContainer.className = "xdeleter-ui-container";

    const styleSheet = document.createElement("style");
    styleSheet.type = "text/css";
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);

    // Main UI structure
    uiContainer.innerHTML = `
            <span id="xdeleter-close-btn" class="xdeleter-ui-close-btn">&times;</span>
            <h2>XDeleter</h2>
            <p style="text-align:center; font-size: 0.8em; margin-top:-10px; margin-bottom:15px;">
                <a href="https://github.com/ajwdd" target="_blank" style="color: var(--xdeleter-primary-color); text-decoration:none;">
                    github.com/ajwdd
                </a>
            </p>
        `;

    // Authentication section
    const authSection = document.createElement("div");
    authSection.className = "xdeleter-section";
    authSection.innerHTML = `
            <h3>Authentication</h3>
            <div class="xdeleter-input-group">
                <label for="xdeleter-auth-token">Bearer Token:</label>
                <input type="text" id="xdeleter-auth-token" placeholder="Starts with Bearer XXXXX...">
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-client-id">Client ID (x-client-transaction-id):</label>
                <input type="text" id="xdeleter-client-id" placeholder="Enter Client Transaction ID">
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-username">X.com Username (without @):</label>
                <input type="text" id="xdeleter-username" placeholder="Enter your username">
            </div>
        `;
    uiContainer.appendChild(authSection);

    // Deletion options section
    const optionsSection = document.createElement("div");
    optionsSection.className = "xdeleter-section";
    optionsSection.innerHTML = `
            <h3>Deletion Options</h3>
            <div class="xdeleter-option-group">
                <label><input type="checkbox" id="xdeleter-unretweet" checked> Unretweet Tweets</label>
            </div>
            <div class="xdeleter-option-group">
                <label><input type="checkbox" id="xdeleter-keep-pinned" checked> Keep Pinned Tweet</label>
            </div>
            <div class="xdeleter-option-group">
                <label><input type="checkbox" id="xdeleter-url-only">Delete Tweets with Links Only</label>
            </div>
            <div class="xdeleter-option-group"> <label><input type="checkbox" id="xdeleter-no-media-only">Delete Tweets without Media Only</label>
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-keywords">Delete if Keywords Match (comma-separated):</label>
                <input type="text" id="xdeleter-keywords" placeholder="e.g., giveaway,contest">
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-ignore-ids">Tweet IDs to Ignore (comma-separated):</label>
                <textarea id="xdeleter-ignore-ids" placeholder="e.g., 123...,456..."></textarea>
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-after-date">Delete Tweets After Date:</label>
                <input type="date" id="xdeleter-after-date">
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-before-date">Delete Tweets Before Date:</label>
                <input type="date" id="xdeleter-before-date">
            </div>
            <div class="xdeleter-input-group">
                <label for="xdeleter-specific-ids">Only Delete Specific IDs (comma-separated, overrides other filters):</label>
                <textarea id="xdeleter-specific-ids" placeholder="e.g., 123...,456..."></textarea>
            </div>
        `;
    uiContainer.appendChild(optionsSection);

    // Control buttons
    const buttonsGroup = document.createElement("div");
    buttonsGroup.className = "xdeleter-button-group";
    buttonsGroup.innerHTML = `
            <button id="xdeleter-start-btn" class="xdeleter-control-button xdeleter-btn-start">Start Deletion</button>
            <button id="xdeleter-stop-btn" class="xdeleter-control-button xdeleter-btn-stop" disabled>Stop Deletion</button>
        `;
    uiContainer.appendChild(buttonsGroup);

    // Log area
    const logSection = document.createElement("div");
    logSection.className = "xdeleter-section";
    logSection.innerHTML = `
            <h3>Log</h3>
            <div id="xdeleter-log-area" class="xdeleter-log-area">Welcome to XDeleter! Configure and press Start.</div>
        `;
    uiContainer.appendChild(logSection);

    document.body.appendChild(uiContainer);

    // Assign UI elements to variables
    logAreaElement = document.getElementById("xdeleter-log-area");
    authTokenInput = document.getElementById("xdeleter-auth-token");
    clientIdInput = document.getElementById("xdeleter-client-id");
    usernameInput = document.getElementById("xdeleter-username");
    unretweetCheckbox = document.getElementById("xdeleter-unretweet");
    keepPinnedCheckbox = document.getElementById("xdeleter-keep-pinned");
    urlOnlyCheckbox = document.getElementById("xdeleter-url-only");
    deleteWithoutMediaCheckbox = document.getElementById(
      "xdeleter-no-media-only"
    );
    keywordsInput = document.getElementById("xdeleter-keywords");
    ignoreIdsInput = document.getElementById("xdeleter-ignore-ids");
    afterDateInput = document.getElementById("xdeleter-after-date");
    beforeDateInput = document.getElementById("xdeleter-before-date");
    specificIdsInput = document.getElementById("xdeleter-specific-ids");
    startButton = document.getElementById("xdeleter-start-btn");
    stopButton = document.getElementById("xdeleter-stop-btn");
    closeButton = document.getElementById("xdeleter-close-btn");

    // Load saved settings from local storage
    const storedIgnoreIds = localStorage.getItem(IGNORE_IDS_STORAGE_KEY);
    if (storedIgnoreIds) {
      ignoreIdsInput.value = storedIgnoreIds;
      logToUI("Loaded 'Tweets to Ignore' from local storage.");
    }
    const storedBearerToken = localStorage.getItem(BEARER_TOKEN_STORAGE_KEY);
    if (storedBearerToken) {
      authTokenInput.value = storedBearerToken;
      logToUI("Loaded Bearer Token from local storage.");
    }
    const storedUsername = localStorage.getItem(USERNAME_STORAGE_KEY);
    if (storedUsername) {
      usernameInput.value = storedUsername;
      logToUI("Loaded Username from local storage.");
    }
  }

  function setupEventListeners() {
    startButton.addEventListener("click", startDeletionProcess);
    stopButton.addEventListener("click", () => {
      logToUI("Stop button clicked. Attempting to halt operations...");
      stop_signal = true;
      stopButton.disabled = true; // Disable stop button after click
    });
    closeButton.addEventListener("click", () => {
      if (is_processing) {
        if (
          !confirm(
            "Deletion is in progress. Are you sure you want to close? This might not stop the current operation immediately."
          )
        ) {
          return;
        }
      }
      uiContainer.style.display = "none"; // Hide UI instead of removing
    });
  }

  async function fetch_tweets(cursor, retry = 0) {
    if (stop_signal) {
      logToUI("Fetch tweets operation cancelled by stop signal.");
      return [];
    }
    const count = "20"; // Number of tweets to fetch per request
    const current_resource = random_resource;
    const endpoint = "UserTweetsAndReplies";

    // API request parameters
    let variables_obj = {
      userId: user_id,
      count: count,
      ...(cursor && { cursor: cursor }), // Add cursor if it exists
      includePromotedContent: true,
      withCommunity: true,
      withVoice: true,
    };
    let features_obj = {
      rweb_video_screen_enabled: false,
      profile_label_improvements_pcf_label_in_post_enabled: true,
      rweb_tipjar_consumption_enabled: true,
      verified_phone_label_enabled: false,
      creator_subscriptions_tweet_preview_api_enabled: true,
      responsive_web_graphql_timeline_navigation_enabled: true,
      responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
      premium_content_api_read_enabled: false,
      communities_web_enable_tweet_community_results_fetch: true,
      c9s_tweet_anatomy_moderator_badge_enabled: true,
      responsive_web_grok_analyze_button_fetch_trends_enabled: false,
      responsive_web_grok_analyze_post_followups_enabled: true,
      responsive_web_jetfuel_frame: false,
      responsive_web_grok_share_attachment_enabled: true,
      articles_preview_enabled: true,
      responsive_web_edit_tweet_api_enabled: true,
      graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
      view_counts_everywhere_api_enabled: true,
      longform_notetweets_consumption_enabled: true,
      responsive_web_twitter_article_tweet_consumption_enabled: true,
      tweet_awards_web_tipping_enabled: false,
      responsive_web_grok_show_grok_translated_post: false,
      responsive_web_grok_analysis_button_from_backend: false,
      creator_subscriptions_quote_tweet_preview_enabled: false,
      freedom_of_speech_not_reach_fetch_enabled: true,
      standardized_nudges_misinfo: true,
      tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
      longform_notetweets_rich_text_read_enabled: true,
      longform_notetweets_inline_media_enabled: true,
      responsive_web_grok_image_annotation_enabled: true,
      responsive_web_enhance_cards_enabled: false,
    };
    const fieldToggles_obj = { withArticlePlainText: false };

    // Encode parameters for the URL
    const variables_encoded = encodeURIComponent(JSON.stringify(variables_obj));
    const features_encoded = encodeURIComponent(JSON.stringify(features_obj));
    const fieldToggles_encoded = encodeURIComponent(
      JSON.stringify(fieldToggles_obj)
    );

    let final_url = `https://x.com/i/api/graphql/${current_resource}/${endpoint}?variables=${variables_encoded}&features=${features_encoded}&fieldToggles=${fieldToggles_encoded}`;

    logToUI(`Fetching tweets... Cursor: ${cursor || "initial"}`);

    try {
      const response = await fetch(final_url, {
        method: "GET",
        headers: {
          accept: "*/*",
          "accept-language": language_code
            ? buildAcceptLanguageString()
            : "en-US,en;q=0.9",
          authorization: authorization,
          "content-type": "application/json",
          "sec-ch-ua":
            ua ||
            `"Chromium";v="100", "Not A;Brand";v="99", "Google Chrome";v="100"`,
          "sec-ch-ua-mobile": "?0",
          "sec-ch-ua-platform": navigator.platform
            ? `"${navigator.platform}"`
            : '"Linux"',
          "sec-fetch-dest": "empty",
          "sec-fetch-mode": "cors",
          "sec-fetch-site": "same-origin",
          "x-client-transaction-id": client_tid,
          "x-csrf-token": csrf_token,
          "x-twitter-active-user": "yes",
          "x-twitter-auth-type": "OAuth2Session",
          "x-twitter-client-language": language_code || "en",
        },
        referrer: `https://x.com/${username}/with_replies`,
        referrerPolicy: "strict-origin-when-cross-origin",
        mode: "cors",
        credentials: "include",
      });

      if (!response.ok) {
        const responseText = await response.text();
        logToUI(
          `Fetch error: Status ${
            response.status
          }. Response: ${responseText.substring(0, 200)}...`,
          true
        );
        if (response.status === 401 || response.status === 403) {
          logToUI(
         
F438
   "Authorization error (401/403). Check credentials and ensure you are logged in.",
            true
          );
          stop_signal = true; // Stop on critical auth errors
          return [];
        }
        if (response.status === 429) {
          // Rate limit handling
          logToUI(
            "Rate limit reached. Waiting 1 minute before retrying.",
            true
          );
          await sleep(60 * 1000);
          return fetch_tweets(cursor, retry + 1); // Retry after waiting
        }
        if (retry >= 3) {
          // Max retries
          logToUI("Max retries reached for fetching tweets. Stopping.", true);
          stop_signal = true;
          return [];
        }
        logToUI(`Retrying fetch in ${10 * (retry + 1)} seconds.`);
        await sleep(10000 * (retry + 1));
        return fetch_tweets(cursor, retry + 1);
      }

      const data = await response.json();
      // Navigate through the complex X API response structure to find tweet instructions
      let instructions;
      if (data?.data?.user?.result?.timeline_v2?.timeline?.instructions) {
        instructions = data.data.user.result.timeline_v2.timeline.instructions;
      } else if (data?.data?.user?.result?.timeline?.timeline?.instructions) {
        // Fallback for older structures
        instructions = data.data.user.result.timeline.timeline.instructions;
      } else {
        logToUI(
          "Unexpected API response structure: Cannot find timeline instructions.",
          true
        );
        console.dir(data); // Log the problematic data structure
        return [];
      }

      let entries = [];
      // Find 'TimelineAddEntries' instruction which usually contains the tweets
      const addEntriesInstruction = instructions.find(
        (instr) => instr.type === "TimelineAddEntries"
      );
      if (addEntriesInstruction && addEntriesInstruction.entries) {
        entries = addEntriesInstruction.entries;
      } else {
        // Fallback if 'TimelineAddEntries' is not the primary way entries are listed
        for (const instruction of instructions) {
          if (instruction.entries && instruction.entries.length > 0) {
            entries.push(...instruction.entries);

            break;
          }
        }
        if (entries.length === 0) {
          logToUI(
            "No 'TimelineAddEntries' found, and no other entries extracted from instructions."
          );
        }
      }
      return entries;
    } catch (error) {
      logToUI(`Network or parsing error during fetch: ${error.message}`, true);
      if (retry >= 3) {
        logToUI(
          "Max retries reached after network/parsing error. Stopping.",
          true
        );
        stop_signal = true;
        return [];
      }
      await sleep(10000 * (retry + 1));
      return fetch_tweets(cursor, retry + 1);
    }
  }

  async function log_tweets_and_get_cursor(entries) {
    if (stop_signal) {
      logToUI("Log tweets operation cancelled.");
      return "finished";
    }
    if (!entries || entries.length === 0) {
      logToUI("No entries to process in log_tweets.");
      return "finished";
    }

    let next_cursor = null;
    for (const item of entries) {
      if (item.entryId) {
        // Check for tweet content items
        if (
          item.entryId.startsWith("tweet-") ||
          item.entryId.startsWith("profile-conversation-") ||
          item.entryId.startsWith("conversationthread-")
        ) {
          findTweetIdsRecursive(
            item.content?.itemContent?.tweet_results?.result ||
              item.content?.tweet_results?.result ||
              item
          );
        } else if (item.entryId.startsWith("cursor-bottom-")) {
          // Check for bottom cursor
          next_cursor =
            item.content?.value ||
            item.content?.cursor?.value || // Alternative path for cursor value
            item.content?.itemContent?.value;
          // Handle specific cursor structure if needed
          if (
            !next_cursor &&
            item.content?.itemContent?.itemType === "TimelineTimelineCursor"
          ) {
            next_cursor = item.content.itemContent.value;
          }
        } else if (
          // Another way cursors might be identified
          item.content?.cursorType === "Bottom" ||
          item.content?.itemContent?.cursorType === "Bottom"
        ) {
          next_cursor = item.content?.value || item.content?.itemContent?.value;
        }
      } else if (item.tweet_results?.result) {
        // Direct tweet result without entryId wrapper
        findTweetIdsRecursive(item.tweet_results.result);
      }
    }

    if (entries.length <= 2 && !next_cursor) {
      const hasActualTweetContent = entries.some(
        (e) =>
          e.entryId?.startsWith("tweet-") ||
          e.content?.itemContent?.tweet_results?.result
      );
      if (!hasActualTweetContent) {
        logToUI(
          "Reached end of timeline (no bottom cursor and few non-tweet entries)."
        );
        return "finished";
      }
    }
    return next_cursor || "finished"; // Return cursor or "finished"
  }

  function findTweetIdsRecursive(obj) {
    if (stop_signal || !obj || typeof obj !== "object") {
      // Base cases for recursion
      return;
    }

    let tweetData = null;
    // Identify if the current object is a tweet structure
    if (obj.__typename === "Tweet" || obj.rest_id) {
      // Primary way to identify a tweet object
      tweetData = obj;
    } else if (
      obj.tweet &&
      (obj.tweet.__typename === "Tweet" || obj.tweet.rest_id)
    ) {
      // Nested tweet object
      tweetData = obj.tweet;
    }

    if (tweetData) {
      const tweet_id_str = tweetData.rest_id || tweetData.legacy?.id_str; // Get tweet ID
      const tweet_user_id_str = // Get user ID associated with the tweet
        tweetData.legacy?.user_id_str ||
        tweetData.core?.user_results?.result?.rest_id;

      // Process user's own tweets
      if (tweet_id_str && tweet_user_id_str === user_id) {
        if (
          delete_options.do_not_remove_pinned_tweet &&
          tweetData.legacy?.pinned_tweet_ids_str?.includes(tweet_id_str) // Check if it's a pinned tweet
        ) {
          logToUI(`Skipping pinned tweet: ${tweet_id_str}`);
        } else if (check_filter_conditions(tweetData, tweet_id_str)) {
          // Apply filters
          if (!tweets_to_delete.includes(tweet_id_str)) {
            // Avoid duplicates
            tweets_to_delete.push(tweet_id_str);
            const text =
              tweetData.legacy?.full_text?.substring(0, 70) ||
              "No text preview";
            logToUI(`Found tweet to delete: ${tweet_id_str} ("${text}...")`);
          }
        }
      } else if (
        tweet_id_str &&
        tweetData.legacy?.retweeted_status_result?.result?.legacy // Check if it's a retweet by the user
          ?.user_id_str === user_id &&
        delete_options.unretweet
      ) {
        if (check_filter_conditions(tweetData, tweet_id_str, true)) {
          if (!tweets_to_delete.includes(tweet_id_str)) {
            tweets_to_delete.push(tweet_id_str);
            const text =
              tweetData.legacy?.full_text?.substring(0, 70) ||
              "No text preview";
            logToUI(
              `Found retweet to unretweet: ${tweet_id_str} ("${text}...")`
            );
          }
        }
      }
    }

    // Recursively search through all properties of the object
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        findTweetIdsRecursive(obj[key]);
      }
    }
  }

  function check_filter_conditions(
    tweetData,
    tweet_id_str,
    isRetweetCheck = false
  ) {
    const legacy = tweetData.legacy || {}; // Access legacy tweet data

    if (delete_options.tweets_to_ignore.includes(tweet_id_str)) {
      logToUI(`Skipping ignored tweet: ${tweet_id_str}`);
      return false;
    }

    const text_to_check = legacy.full_text || "";
    const created_at_str = legacy.created_at;
    if (created_at_str) {
      const tweet_date = new Date(created_at_str);
      if (
        !(
          // Check if tweet date is outside the specified range
          (
            tweet_date >= delete_options.after_date &&
            tweet_date <= delete_options.before_date
          )
        )
      ) {
        if (
          tweet_date < delete_options.after_date &&
          !delete_options.delete_specific_ids_only[0]
        ) {
          logToUI(
            `Tweet ${tweet_id_str} is older than 'After Date'. Stopping further fetching if applicable.`
          );

          if (!delete_options.delete_specific_ids_only[0]) {
            // Only stop if not targeting specific IDs
            stop_signal = true;
          }
        }
        return false; // Tweet is outside date range
      }
    } else {
      logToUI(
        `Warning: Tweet ${tweet_id_str} has no creation date. Applying other filters.`,
        true
      );
    }

    if (delete_options.delete_tweets_without_media) {
      // Check if legacy.entities.media exists and has items (photo, video, gif)
      const hasMedia =
        legacy.entities &&
        legacy.entities.media &&
        legacy.entities.media.length > 0;
      if (hasMedia) {
        logToUI(
          `Tweet ${tweet_id_str} has media. Skipping due to 'Delete Tweets without Media Only' option.`
        );
        return false;
      }
    }

    if (delete_options.delete_message_with_url_only) {
      const hasUrl = legacy.entities?.urls?.length > 0;
      if (!hasUrl) return false;
    }

    
    if (
      delete_options.match_any_keywords.length > 0 &&
      delete_options.match_any_keywords[0] !== ""
    ) {
      const foundKeyword = delete_options.match_any_keywords.some((keyword) =>
        text_to_check.toLowerCase().includes(keyword.toLowerCase())
      );
      if (!foundKeyword) return false;
    }

    if (isRetweetCheck && delete_options.unretweet) return true;

    return true;
  }

  async function delete_tweets_api(id_list) {
    if (stop_signal) {
      logToUI("Deletion operation cancelled by stop signal.");
      return;
    }
    const total_to_delete = id_list.length;
    logToUI(`Attempting to delete ${total_to_delete} tweets/retweets...`);
    let deleted_count = 0;
    let consecutive_errors = 0;

    for (let i = 0; i < total_to_delete; i++) {
      if (stop_signal) {
        logToUI(
          `Deletion loop cancelled. ${deleted_count}/${total_to_delete} processed before stop.`
        );
        break;
      }
      const tweet_id = id_list[i];
      let retry = 0;
      let success = false;

      while (retry < 3 && !success) {
        if (stop_signal) break;
        try {
          const payload = {
            variables: { tweet_id: tweet_id, dark_request: false },
            queryId: delete_tweet_graphql_id,
          };

          const response = await fetch(
            `https://x.com/i/api/graphql/${delete_tweet_graphql_id}/DeleteTweet`,
            {
              method: "POST",
              headers: {
                accept: "*/*",
                "accept-language": language_code
                  ? buildAcceptLanguageString()
                  : "en-US,en;q=0.9",
                authorization: authorization,
                "content-type": "application/json",
                "sec-ch-ua":
                  ua ||
                  `"Chromium";v="100", "Not A;Brand";v="99", "Google Chrome";v="100"`,
                "sec-ch-ua-mobile": "?0",
                "sec-ch-ua-platform": navigator.platform
                  ? `"${navigator.platform}"`
                  : '"Linux"',
                "sec-fetch-dest": "empty",
                "sec-fetch-mode": "cors",
                "sec-fetch-site": "same-origin",
                "x-client-transaction-id": delete_tweet_transaction_id, // Specific transaction ID for delete
                "x-csrf-token": csrf_token,
                "x-twitter-active-user": "yes",
                "x-twitter-auth-type": "OAuth2Session",
                "x-twitter-client-language": language_code || "en",
              },
              body: JSON.stringify(payload),
              referrer: `https://x.com/${username}`, // Referrer for the request
              referrerPolicy: "strict-origin-when-cross-origin",
              mode: "cors",
              credentials: "include",
            }
          );

          if (response.ok) {
            const responseData = await response.json();
            // Check for errors in the API response
            if (responseData.errors && responseData.errors.length > 0) {
              logToUI(
                `API error deleting ${tweet_id}: ${responseData.errors[0].message}. Might already be deleted.`,
                true
              );
              success = true;
              if (
                responseData.errors[0].message
                  .toLowerCase()
                  .includes("not found") ||
                responseData.errors[0].message
                  .toLowerCase()
                  .includes("already been deleted") ||
                responseData.errors[0].message
                  .toLowerCase()
                  .includes("no status found with that id")
              ) {
                consecutive_errors = 0;
              } else {
                consecutive_errors++;
              }
            } else if (
              // Check for successful deletion/unretweet indicators
              responseData.data &&
              (responseData.data.delete_tweet || responseData.data.unretweet) // 'unretweet' field for unretweeting
            ) {
              logToUI(
                `Successfully deleted/unretweeted ${tweet_id} (${
                  deleted_count + 1
                }/${total_to_delete})`
              );
              deleted_count++;
              success = true;
              consecutive_errors = 0; // Reset error count on success
            } else {
              // Handle unexpected successful response structure
              logToUI(
                `Deletion of ${tweet_id} - HTTP .ok but unclear data structure: ${JSON.stringify(
                  responseData
                ).substring(0, 100)}. Assuming handled.`,
                true
              );
              success = true; // Assume handled to avoid retrying indefinitely
              consecutive_errors = 0;
            }
          } else {
            // Handle non-OK HTTP responses
            const errorText = await response.text();
            logToUI(
              `Failed to delete ${tweet_id}. Status: ${
                response.status
              }. Resp: ${errorText.substring(0, 150)}...`,
              true
            );
            if (response.status === 401 || response.status === 403) {
              // Auth error
              logToUI("Auth error during delete. Stopping.", true);
              stop_signal = true;
              break;
            }
            if (response.status === 404) {
              // Not found, assume already deleted
              logToUI(
                `Tweet ${tweet_id} not found (404). Assuming already deleted.`
              );
              success = true;
              consecutive_errors = 0;
            } else if (response.status === 429) {
              // Rate limit
              logToUI("Rate limit on delete. Waiting 1 minute.", true);
              await sleep(60 * 1000); // Wait and retry current tweet
              continue; // Skip to next iteration of while loop for current tweet
            } else {
              retry++;
              consecutive_errors++;
              if (retry < 3) await sleep(5000 * retry); // Exponential backoff for retries
            }
          }
        } catch (error) {
          // Network or other errors
          logToUI(`Network error deleting ${tweet_id}: ${error.message}`, true);
          retry++;
          consecutive_errors++;
          if (retry < 3) await sleep(5000 * retry);
        }
      } // End of retry while loop

      if (!success && !stop_signal) {
        logToUI(
          `Failed to delete ${tweet_id} after multiple retries. Skipping.`,
          true
        );
      }
      // Pause if too many consecutive errors
      if (consecutive_errors >= 5 && !stop_signal) {
        logToUI(
          "Too many consecutive deletion failures. Pausing for 2 minutes.",
          true
        );
        await sleep(2 * 60 * 1000);
        consecutive_errors = 0; // Reset after pause
      }
      if (!stop_signal) await sleep(Math.random() * 500 + 300); // Short random delay between deletions
    }
    logToUI(
      `Deletion batch finished. Processed ${deleted_count} of ${id_list.length} targeted tweets.`
    );
  }

  async function startDeletionProcess() {
    logToUI("Initiating deletion process...");
    is_processing = true;
    startButton.disabled = true;
    stopButton.disabled = false;
    stop_signal = false;
    tweets_to_delete = [];

    // Get authentication details from UI
    authorization = authTokenInput.value.trim();
    client_tid = clientIdInput.value.trim();
    username = usernameInput.value.trim();

    // Validate required inputs
    if (!authorization || !client_tid || !username) {
      logToUI(
        "Error: Bearer Token, Client ID, and Username are all required!",
        true
      );
      alert("Error: Authorization, Client ID, and Username are required!");
      is_processing = false;
      startButton.disabled = false;
      stopButton.disabled = true;
      return;
    }
    // Save auth details to local storage
    localStorage.setItem(BEARER_TOKEN_STORAGE_KEY, authorization);
    logToUI("Bearer Token saved to local storage.");
    localStorage.setItem(USERNAME_STORAGE_KEY, username);
    logToUI("Username saved to local storage.");

    // Ensure Bearer token format
    if (!authorization.toLowerCase().startsWith("bearer ")) {
      authorization = "Bearer " + authorization;
    }

    // Retrieve CSRF token and User ID
    csrf_token = getCookie("ct0");
    const twidCookie = getCookie("twid");
    let temp_user_id = null;
    if (twidCookie) {
      const decodedTwidValue = decodeURIComponent(twidCookie);
      const parts = decodedTwidValue.split("=");
      if (parts.length === 2 && parts[0] === "u" && /^\d+$/.test(parts[1])) {
        temp_user_id = parts[1];
      } else if (parts.length === 1 && /^\d+$/.test(parts[0])) {
        temp_user_id = parts[0];
      }
    }
    user_id = temp_user_id;

    if (!csrf_token || !user_id) {
      logToUI(
        "Error: Could not retrieve essential cookies (ct0 for CSRF, twid for User ID). Ensure twid cookie is valid (e.g., u=NUMERIC_ID or just NUMERIC_ID) and you are logged into X.com.",
        true
      );
      alert(
        "Error: Missing or invalid essential cookies (ct0, twid). Are you logged into X.com?"
      );
      is_processing = false;
      startButton.disabled = false;
      stopButton.disabled = true;
      return;
    }
    logToUI(
      `User ID: ${user_id}, CSRF Token: ${csrf_token ? "found" : "NOT FOUND"}`
    );

    if (
      navigator.userAgentData &&
      typeof navigator.userAgentData.getHighEntropyValues === "function"
    ) {
      try {
        const highEntropyValues =
          await navigator.userAgentData.getHighEntropyValues([
            "platform",
            "platformVersion",
            "architecture",
            "model",
            "uaFullVersion",
          ]);
        ua = navigator.userAgentData.brands
          .map((brand) => `"${brand.brand}";v="${brand.version}"`)
          .join(", ");
        ua += `, "${highEntropyValues.platform}";v="${highEntropyValues.platformVersion}"`; // Append platform info
      } catch (e) {
        // Fallback if high-entropy values fail
        ua =
          navigator.userAgent ||
          `"Chromium";v="100", "Not A;Brand";v="99", "Google Chrome";v="100"`;
      }
    } else {
      // Fallback to standard userAgent
      ua =
        navigator.userAgent ||
        `"Chromium";v="100", "Not A;Brand";v="99", "Google Chrome";v="100"`;
    }
    language_code = navigator.language
      ? navigator.language.split("-")[0] // Get primary language code
      : "en";

    delete_options.unretweet = unretweetCheckbox.checked;
    delete_options.do_not_remove_pinned_tweet = keepPinnedCheckbox.checked;
    delete_options.delete_message_with_url_only = urlOnlyCheckbox.checked;
    delete_options.delete_tweets_without_media =
      deleteWithoutMediaCheckbox.checked;

    const keywordsRaw = keywordsInput.value.trim();
    delete_options.match_any_keywords = keywordsRaw
      ? keywordsRaw
          .split(",")
          .map((k) => k.trim().toLowerCase())
          .filter((k) => k) // Process keywords
      : [""]; // Default to empty if no keywords

    const ignoreIdsRaw = ignoreIdsInput.value.trim();
    delete_options.tweets_to_ignore = ignoreIdsRaw
      ? ignoreIdsRaw
          .split(/[\s,]+/)
          .map((id) => id.trim())
          .filter((id) => id) // Process ignored IDs
      : [];
    localStorage.setItem(IGNORE_IDS_STORAGE_KEY, ignoreIdsRaw); // Save ignored IDs
    if (ignoreIdsRaw) {
      logToUI("'Tweets to Ignore' saved to local storage.");
    }

    const specificIdsRaw = specificIdsInput.value.trim();
    delete_options.delete_specific_ids_only = specificIdsRaw
      ? specificIdsRaw
          .split(/[\s,]+/)
          .map((id) => id.trim())
          .filter((id) => id)
      : [""];

    // Parse date inputs, providing defaults if empty
    const afterDateVal = afterDateInput.value;
    delete_options.after_date = afterDateVal
      ? new Date(afterDateVal + "T00:00:00Z")
      : new Date("1900-01-01T00:00:00Z");
    const beforeDateVal = beforeDateInput.value;
    delete_options.before_date = beforeDateVal
      ? new Date(beforeDateVal + "T23:59:59Z")
      : new Date("2100-01-01T23:59:59Z");

    logToUI(
      "Options configured: " +
        JSON.stringify(
          // Log configured options for debugging
          delete_options,
          (key, value) => (value instanceof Date ? value.toISOString() : value), // Serialize dates properly
          2
        )
    );

    try {
      // If specific IDs are provided, only delete those
      if (
        delete_options.delete_specific_ids_only.length > 0 &&
        delete_options.delete_specific_ids_only[0] !== ""
      ) {
        logToUI(
          `Deleting specific tweet IDs: ${delete_options.delete_specific_ids_only.join(
            ", "
          )}`
        );
        await delete_tweets_api(delete_options.delete_specific_ids_only);
      } else {
        let current_cursor = null;
        let fetch_count = 0;
        const MAX_FETCHES = 200;
        while (
          current_cursor !== "finished" &&
          !stop_signal &&
          fetch_count < MAX_FETCHES
        ) {
          fetch_count++;
          logToUI(
            `Fetching page ${fetch_count}... Cursor: ${
              current_cursor || "(initial)"
            }`
          );
          const entries = await fetch_tweets(current_cursor);
          if (stop_signal) break;

          tweets_to_delete = [];
          current_cursor = await log_tweets_and_get_cursor(entries);
          if (stop_signal) break;

          if (tweets_to_delete.length > 0) {
            logToUI(
              `Found ${tweets_to_delete.length} tweets in this batch to delete.`
            );
            await delete_tweets_api([...tweets_to_delete]);
          } else {
            logToUI("No tweets matching criteria in this batch.");
          }

          if (current_cursor === "finished") {
            logToUI("Reached end of timeline or no more tweets to process.");
            break;
          }
          if (!stop_signal) await sleep(Math.random() * 2000 + 3000);
        }
        if (fetch_count >= MAX_FETCHES) {
          logToUI("Reached maximum fetch limit for timeline. Stopping.", true);
        }
      }
      logToUI("XDeleter process finished or stopped.");
    } catch (error) {
      logToUI(`An critical error occurred: ${error.message}`, true);
      console.error(error);
    } finally {
      is_processing = false;
      startButton.disabled = false;
      stopButton.disabled = true;
    }
  }

  function init() {
    if (document.getElementById("xdeleter-ui")) {
      logToUI("XDeleter UI already exists. Please close or refresh.", true);
      return;
    }
    createUI();
    setupEventListeners();
    logToUI(
      "XDeleter UI initialized. Please configure authentication and options."
    );
  }

  if (
    window.location.hostname.includes("x.com") ||
    window.location.hostname.includes("twitter.com")
  ) {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", init);
    } else {
      init();
    }
  } else {
    console.warn(
      "Use x.com/twitter.com domain."
    );
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", init);
    } else {
      init();
    }
  }
})();

About

πŸ—‘οΈ The best method to delete and unretweet tweets on Twitter/X.

Topics

Resources

License

Stars

Watchers

Forks

2EE6
0