Массовое удаление твитов, без расширений

Бесплатный скрипт для вставки в консоль Chrome, чтобы навести порядок в старых постах X: сначала симуляция, свежие твиты под защитой, удаление ограниченными пакетами. Никаких расширений, никаких сторонних сервисов, ничто не покидает ваш браузер.

Прежде чем начать: удаление твита необратимо. Скрипт поставляется в режиме симуляции (DRY_RUN = true): он показывает список того, что будет удалено, ничего не трогая. Заранее скачайте свой архив X, проверьте симуляцию и только затем переходите к реальному удалению. Автоматизация интерфейса не предусмотрена условиями использования X: действуйте осторожно и на свой страх и риск.

Порядок действий

  1. Откройте свой профиль: перейдите на x.com/vash_login, вкладка Посты, войдя в свою учётную запись.
  2. Откройте консоль: нажмите F12 (или Cmd+Option+J на Mac), затем перейдите на вкладку Console.
  3. Настройте скрипт: скопируйте код ниже, замените vash_login своим идентификатором X (без @), настройте KEEP_RECENT (сколько свежих твитов сохранить) и при необходимости перечислите в KEEP_STATUS_IDS идентификаторы твитов, которые никогда не следует удалять.
  4. Запустите симуляцию: вставьте скрипт в консоль и подтвердите клавишей Enter. При DRY_RUN = true (настройка по умолчанию) он покажет список твитов, которые были бы удалены, ничего не удаляя.
  5. Удалите: если симуляция вас устраивает, переключите DRY_RUN на false и запустите заново. Скрипт удаляет не более 40 твитов за один запуск, с паузами от 6 до 14 секунд.
  6. Повторите: запускайте скрипт столько раз, сколько нужно, чтобы обработать остальное, пакетами.

Скрипт

Консоль Chrome · x.com
(() => {
  // ============ КОНФИГУРАЦИЯ ============
  const HANDLE          = "vash_login"; // ваш идентификатор X, без @
  const KEEP_RECENT     = 15;           // НИКОГДА не удаляет 15 самых свежих твитов (сверху)
  const KEEP_STATUS_IDS = [             // дополнительная защита (по желанию): идентификаторы, которые нельзя удалять
    "1234567890123456789",
  ];
  let   DRY_RUN         = true;         // true = симуляция (только список). false = РЕАЛЬНОЕ УДАЛЕНИЕ.
  const MIN_DELAY_MS    = 6000;
  const MAX_DELAY_MS    = 14000;
  const MAX_DELETIONS   = 40;           // предел за один запуск (запустите снова, чтобы продолжить)
  // =====================================

  const me = HANDLE.toLowerCase();
  const KEEP = new Set(KEEP_STATUS_IDS.map(String));
  const protectedRecent = new Set();
  const RX_DELETE = /(Удалить|delete|supprimer)/i;
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const rand  = (a, b) => Math.floor(a + Math.random() * (b - a));
  const skipped = new Set();

  if (!location.pathname.toLowerCase().startsWith("/" + me)) {
    console.warn(`%c[СТОП] Перейдите на https://x.com/${HANDLE} (вкладка Посты) и запустите снова.`,
      "color:#e0245e;font-weight:bold");
    return;
  }

  const articles = () => [...document.querySelectorAll('article[data-testid="tweet"]')];

  const idOf = (art) => {
    const href = [...art.querySelectorAll('a[href*="/status/"]')]
      .map((x) => x.getAttribute("href"))
      .find((h) => new RegExp(`^/${HANDLE}/status/\\d+`, "i").test(h));
    if (!href) return null;
    const m = href.match(/status\/(\d+)/);
    return m ? m[1] : null;
  };

  // Загружает и защищает KEEP_RECENT самых свежих твитов (те, что сверху).
  async function protectRecent() {
    const order = [], seen = new Set();
    let noNew = 0;
    window.scrollTo(0, 0);
    await sleep(1200);
    while (order.length < KEEP_RECENT && noNew < 6) {
      let added = 0;
      for (const art of articles()) {
        const id = idOf(art);
        if (id && !seen.has(id)) { seen.add(id); order.push(id); added++; }
      }
      noNew = added ? 0 : noNew + 1;
      if (order.length >= KEEP_RECENT) break;
      const cs = articles();
      (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
      await sleep(1400);
    }
    for (const id of order.slice(0, KEEP_RECENT)) protectedRecent.add(id);
    return order.slice(0, KEEP_RECENT);
  }

  const eligible = (art) => {
    const id = idOf(art);
    if (!id) return null;
    if (protectedRecent.has(id)) return null;   // один из самых свежих
    if (KEEP.has(id)) return null;              // ID под дополнительной защитой
    if (skipped.has(id)) return null;
    return id;
  };

  async function deleteOne(art, id) {
    const caret = art.querySelector('[data-testid="caret"]');
    if (!caret) { skipped.add(id); return false; }
    caret.click();
    await sleep(700);
    const del = [...document.querySelectorAll('[role="menuitem"]')]
      .find((i) => RX_DELETE.test((i.innerText || "").trim()));
    if (!del) { document.body.click(); skipped.add(id); return false; }
    del.click();
    await sleep(700);
    const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
    if (!confirmBtn) { skipped.add(id); return false; }
    confirmBtn.click();
    await sleep(1500);
    return true;
  }

  async function dryScan() {
    const seen = new Set();
    let noNew = 0;
    console.log("%c[СИМУЛЯЦИЯ] Ничего не будет удалено. Подсчёт...", "color:#1d9bf0;font-weight:bold");
    while (noNew < 4) {
      let added = 0;
      for (const art of articles()) {
        const id = eligible(art);
        if (id && !seen.has(id)) { seen.add(id); added++; }
      }
      noNew = added ? 0 : noNew + 1;
      const cs = articles();
      (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
      await sleep(1200);
    }
    console.log(`%c[СИМУЛЯЦИЯ] БЫЛО БЫ удалено твитов: ${seen.size} (самые свежие под защитой).`,
      "color:#1d9bf0;font-weight:bold");
    console.log([...seen].slice(0, 60));
  }

  async function run() {
    let count = 0, guard = 0, noHit = 0, started = false;
    while (count < MAX_DELETIONS && guard++ < 1500) {
      const pair = articles().map((a) => [a, eligible(a)]).find(([, id]) => id);
      if (!pair) {
        if ((started && noHit >= 6) || noHit >= 40) break;
        const cs = articles();
        (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
        await sleep(1500);
        noHit++;
        continue;
      }
      started = true; noHit = 0;
      const [node, id] = pair;
      node.scrollIntoView({ block: "center" });
      await sleep(600);
      const ok = await deleteOne(node, id);
      if (ok) count++;
      console.log(`${ok ? "удалён" : "пропущен"} ${id}  (всего: ${count}/${MAX_DELETIONS})`);
      await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
    }
    console.log(`%c[ГОТОВО] Удалено за этот проход: ${count}. Запустите скрипт снова, если что-то осталось.`,
      "color:#00ba7c;font-weight:bold");
  }

  (async () => {
    console.log(`Загрузка и защита ${KEEP_RECENT} самых свежих твитов...`);
    const recent = await protectRecent();
    if (recent.length < KEEP_RECENT) {
      console.warn(`%c[СТОП] Загружено только ${recent.length} твитов (X ограничил скорость). Обновите страницу (F5) и попробуйте снова.`,
        "color:#e0245e;font-weight:bold");
      return;
    }
    console.log("%cПод защитой (самые свежие):", "color:#00ba7c;font-weight:bold", recent);
    if (DRY_RUN) { await dryScan(); return; }
    await run();
  })();
})();

Как скрипт вас защищает

В него встроены четыре предохранителя. Прежде всего симуляция по умолчанию: пока DRY_RUN равен true, ничего не удаляется, вы получаете лишь список затронутых твитов. Затем защита свежих: скрипт сначала запоминает ваши самые свежие твиты (15 по умолчанию) и никогда их не трогает. Далее белый список: любой идентификатор, помещённый в KEEP_STATUS_IDS, неприкосновенен, что удобно для закреплённых твитов или важных объявлений. Наконец, осторожный ритм: случайные паузы от 6 до 14 секунд между удалениями и автоматическая остановка после 40, чтобы не вызвать ограничения X.

Перед большой уборкой сохраните то, что важно: извлечение e-mail и URL соберёт все ссылки из выгрузки твитов, счётчик слов покажет, сколько весили ваши годы постов, а очистка текста приведёт в порядок вставленный из архива фрагмент. А для будущих твитов пригодятся стилизованный текст Unicode и конвертер регистра. Все инструменты из набора инструментов работают в вашем браузере, как и это руководство, в духе нашей политики конфиденциальности.

Частые вопросы

Можно ли отменить удаление?

Нет. Удалённый твит исчезает навсегда, в X нет корзины. Именно поэтому скрипт поставляется в режиме симуляции (DRY_RUN = true): сначала запустите его как есть, чтобы получить список того, что будет удалено, проверьте его и переключайте на false только тогда, когда полностью уверены. Также заранее скачайте свой архив X (Настройки, затем Ваша учётная запись, затем Скачать архив данных).

Может ли скрипт удалить чужие твиты?

Нет. X показывает кнопку «Удалить» только на ваших собственных постах: скрипт может работать лишь с той учётной записью, под которой вы вошли, на вашей же странице профиля. Он не трогает ни чужие ответы, ни любой сторонний контент.

Зачем ограничение в 40 удалений и случайные паузы?

Чтобы вести себя бережно с платформой. Слишком быстрые или слишком массовые удаления могут вызвать ограничения X и даже временную блокировку учётной записи. Скрипт ждёт от 6 до 14 секунд между удалениями и останавливается после 40: запустите его снова, чтобы продолжить пакетами.

Разрешено ли это правилами X?

Вы удаляете собственный контент, что является вашим правом. Однако автоматизация веб-интерфейса не предусмотрена условиями использования X: применяйте этот скрипт умеренно и на свой страх и риск. Официальная альтернатива: удаление вручную или специализированные сервисы, работающие через API.

Как защитить некоторые конкретные твиты?

Две защиты действуют одновременно: KEEP_RECENT автоматически сохраняет ваши самые свежие твиты (15 по умолчанию), а KEEP_STATUS_IDS принимает список идентификаторов, которые никогда не будут удалены. Идентификатор твита: это большое число в конце его адреса: x.com/login/status/1234567890123456789.