Menghapus tweet secara massal, tanpa ekstensi

Skrip gratis yang ditempel ke konsol Chrome untuk merapikan unggahan X lama Anda: simulasi dulu, tweet terbaru terlindungi, penghapusan per batch dengan batas. Tanpa ekstensi, tanpa layanan pihak ketiga, tidak ada yang keluar dari peramban Anda.

Sebelum mulai: penghapusan sebuah tweet bersifat permanen. Skrip disediakan dalam mode simulasi (DRY_RUN = true): skrip mendaftar apa yang akan dihapus tanpa menyentuh apa pun. Unduh arsip X Anda terlebih dahulu, periksa simulasinya, dan baru setelah itu beralih ke penghapusan sungguhan. Otomatisasi antarmuka tidak diatur dalam ketentuan penggunaan X: lakukan perlahan, atas risiko Anda sendiri.

Cara melakukannya

  1. Buka profil Anda: kunjungi x.com/nama_pengguna_anda, tab Posts, dalam keadaan masuk ke akun Anda.
  2. Buka konsol: tekan F12 (atau Cmd+Option+J di Mac), lalu klik tab Console.
  3. Atur skrip: salin kode di bawah, ganti nama_pengguna_anda dengan ID X Anda (tanpa @), sesuaikan KEEP_RECENT (jumlah tweet terbaru yang disimpan) dan, bila perlu, daftarkan pada KEEP_STATUS_IDS ID tweet yang tidak boleh dihapus.
  4. Simulasikan: tempel skrip ke konsol lalu tekan Enter. Dengan DRY_RUN = true (pengaturan bawaan), skrip menampilkan daftar tweet yang akan dihapus, tanpa menghapus apa pun.
  5. Hapus: jika hasil simulasi sesuai, ubah DRY_RUN menjadi false lalu jalankan lagi. Skrip menghapus maksimal 40 tweet per eksekusi, dengan jeda 6 sampai 14 detik.
  6. Ulangi: jalankan skrip sebanyak yang diperlukan untuk memproses sisanya, per batch.

Skripnya

Konsol Chrome · x.com
(() => {
  // ============ KONFIGURASI ============
  const HANDLE          = "nama_pengguna_anda"; // ID X Anda, tanpa @
  const KEEP_RECENT     = 15;           // JANGAN PERNAH hapus 15 tweet terbaru (paling atas)
  const KEEP_STATUS_IDS = [             // perlindungan TAMBAHAN (opsional): ID yang tidak boleh dihapus
    "1234567890123456789",
  ];
  let   DRY_RUN         = true;         // true = simulasi (hanya daftar). false = PENGHAPUSAN SUNGGUHAN.
  const MIN_DELAY_MS    = 6000;
  const MAX_DELAY_MS    = 14000;
  const MAX_DELETIONS   = 40;           // batas per eksekusi (jalankan lagi untuk melanjutkan)
  // ================================

  const me = HANDLE.toLowerCase();
  const KEEP = new Set(KEEP_STATUS_IDS.map(String));
  const protectedRecent = new Set();
  const RX_DELETE = /(Hapus|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[BERHENTI] Buka https://x.com/${HANDLE} (tab Posts) lalu jalankan lagi.`,
      "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;
  };

  // Memuat dan melindungi KEEP_RECENT tweet terbaru (yang paling atas).
  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;   // salah satu yang terbaru
    if (KEEP.has(id)) return null;              // ID yang dilindungi tambahan
    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[SIMULASI] Tidak ada yang akan dihapus. Menghitung...", "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[SIMULASI] ${seen.size} tweet AKAN dihapus (yang terbaru terlindungi).`,
      "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 ? "dihapus" : "dilewati"} ${id}  (total: ${count}/${MAX_DELETIONS})`);
      await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
    }
    console.log(`%c[SELESAI] ${count} dihapus putaran ini. Jalankan lagi skrip jika masih ada sisa.`,
      "color:#00ba7c;font-weight:bold");
  }

  (async () => {
    console.log(`Memuat dan melindungi ${KEEP_RECENT} tweet terbaru...`);
    const recent = await protectRecent();
    if (recent.length < KEEP_RECENT) {
      console.warn(`%c[BERHENTI] Hanya ${recent.length} tweet yang termuat (X membatasi laju). Muat ulang halaman (F5) dan coba lagi.`,
        "color:#e0245e;font-weight:bold");
      return;
    }
    console.log("%cTerlindungi (yang terbaru):", "color:#00ba7c;font-weight:bold", recent);
    if (DRY_RUN) { await dryScan(); return; }
    await run();
  })();
})();

Bagaimana skrip melindungi Anda

Ada empat pengaman bawaan. Pertama, simulasi secara bawaan: selama DRY_RUN bernilai true, tidak ada yang dihapus, Anda hanya mendapatkan daftar tweet yang terpengaruh. Kedua, perlindungan tweet terbaru: skrip diawali dengan mengingat tweet terbaru Anda (15 secara bawaan) dan tidak akan pernah menyentuhnya. Lalu daftar putih: setiap ID yang ditempatkan di KEEP_STATUS_IDS tidak bisa disentuh, berguna untuk tweet yang disematkan atau pengumuman Anda. Terakhir, ritme yang hati-hati: jeda acak 6 sampai 14 detik di setiap penghapusan dan berhenti otomatis setelah 40, agar tidak memicu pembatasan X.

Sebelum bebersih besar, jangan lupa mengarsipkan yang penting: pengekstrak email dan URL mengambil semua tautan dari ekspor tweet, penghitung kata memberi tahu seberapa besar bobot bertahun-tahun unggahan Anda, dan pembersih teks merapikan salinan arsip. Untuk tweet Anda ke depan, ada teks bergaya Unicode dan pengubah kapitalisasi. Semua alat di kotak alat bekerja di peramban Anda, seperti panduan ini, sejalan dengan kebijakan privasi kami.

Pertanyaan umum

Apakah penghapusan bisa dibatalkan?

Tidak. Tweet yang dihapus hilang selamanya, X tidak menyediakan tempat sampah. Karena itu skrip disediakan dalam mode simulasi (DRY_RUN = true): jalankan dulu apa adanya untuk mendapatkan daftar apa saja yang akan dihapus, periksa daftar itu, dan ubah ke false hanya jika Anda sudah yakin. Jangan lupa juga mengunduh arsip X Anda terlebih dahulu (Pengaturan, lalu Akun Anda, lalu Unduh arsip data Anda).

Bisakah skrip menghapus tweet orang lain?

Tidak. X hanya menampilkan tombol Hapus pada unggahan Anda sendiri: skrip hanya bisa bekerja pada akun yang sedang Anda masuki, di halaman profil Anda sendiri. Skrip tidak menyentuh balasan orang lain maupun konten pihak ketiga mana pun.

Mengapa ada batas 40 penghapusan dan jeda acak?

Agar tetap lembut terhadap platform. Penghapusan yang terlalu cepat atau terlalu banyak bisa memicu pembatasan X, bahkan pemblokiran akun sementara. Skrip menunggu antara 6 dan 14 detik di setiap penghapusan dan berhenti setelah 40: jalankan lagi untuk melanjutkan per batch.

Apakah ini diizinkan oleh X?

Anda menghapus konten Anda sendiri, dan itu adalah hak Anda. Namun, otomatisasi antarmuka web tidak diatur dalam ketentuan penggunaan X: gunakan skrip ini secukupnya dan atas risiko Anda sendiri. Alternatif resminya adalah penghapusan manual, atau layanan khusus yang bekerja lewat API.

Bagaimana cara melindungi tweet tertentu?

Dua perlindungan bisa digabungkan: KEEP_RECENT otomatis menyimpan tweet terbaru Anda (15 secara bawaan), dan KEEP_STATUS_IDS menerima daftar ID yang tidak akan pernah dihapus. ID sebuah tweet adalah angka besar di akhir alamatnya: x.com/nama_pengguna/status/1234567890123456789.