ChatGPT Bulk Deleter

Delete all chats with visible log that shows while running and hides when done. Auto remounts UI on changes.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         ChatGPT Bulk Deleter
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Delete all chats with visible log that shows while running and hides when done. Auto remounts UI on changes.
// @author       Kes
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        GM_addStyle
// @license      MIT
// @noframes
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // single supervisor object that can remount as needed
  window.__BD_SUP__ = window.__BD_SUP__ || {
    mounted: false,
    running: false,
    armed: false,
    logStore: [],
    ensureTimer: null,
    lastUrl: location.href
  };
  const S = window.__BD_SUP__;

  // ---------- UI ----------
  GM_addStyle(`
    #bd-btn{position:fixed;top:12px;left:12px;z-index:2147483647;padding:10px 14px;border:none;border-radius:10px;background:#ef4444;color:#fff;font-size:14px;font-weight:600;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,.35)}
    #bd-btn[disabled]{opacity:.6;cursor:not-allowed}
    #bd-log{position:fixed;bottom:12px;left:12px;width:460px;max-height:55vh;overflow:auto;border:1px solid #2e2e2e;background:#111;color:#ddd;border-radius:10px;z-index:2147483647;box-shadow:0 8px 24px rgba(0,0,0,.35);font:12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;display:none}
    #bd-log header{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid #2e2e2e;background:#181818;border-top-left-radius:10px;border-top-right-radius:10px}
    #bd-log header b{font-size:12px}
    #bd-log header button{background:#333;border:1px solid #444;color:#eee;border-radius:6px;padding:4px 8px;cursor:pointer}
    #bd-log pre{white-space:pre-wrap;margin:0;padding:10px;line-height:1.35}
  `);

  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const base = () => window.location.origin;
  const get = sel => document.querySelector(sel);

  function getCookie(name){
    return document.cookie.split('; ').find(r => r.startsWith(name + '='))?.split('=')[1];
  }

  function showLog(show){
    const el = get('#bd-log');
    if (el) el.style.display = show ? 'block' : 'none';
  }

  function log(...a){
    const line = a.map(x => typeof x === 'string' ? x : JSON.stringify(x)).join(' ');
    S.logStore.push(line);
    const pre = get('#bd-pre');
    if (pre) pre.textContent = S.logStore.join('\n');
    console.log('[BulkDeleter]', ...a);
  }

  async function getBearer(){
    try{
      const res = await fetch(`${base()}/api/auth/session`, { credentials:'include', cache:'no-store' });
      if (!res.ok) return null;
      const j = await res.json();
      if (j && j.accessToken){
        log('token ok');
        return j.accessToken;
      }
    }catch(e){}
    log('no bearer');
    return null;
  }

  async function http(method, url, body, bearer){
    const csrfCookie = getCookie('csrfToken');
    const csrf = csrfCookie ? decodeURIComponent(csrfCookie) : '';
    const did  = getCookie('oai-did') || '';

    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrf,
      'OAI-Device-Id': did
    };
    if (bearer) headers['Authorization'] = `Bearer ${bearer}`;

    const opts = { method, headers, credentials: 'include', mode: 'same-origin' };
    if (body !== undefined) opts.body = JSON.stringify(body);

    try{
      const res = await fetch(url, opts);
      const text = await res.text().catch(()=> '');
      const ok = res.ok || res.status === 204 || res.status === 404;
      return { ok, status: res.status, text, url };
    }catch(e){
      return { ok:false, status:0, text:String(e), url };
    }
  }

  async function listPage(offset, limit, bearer){
    const tries = [
      `${base()}/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated`,
      `${base()}/api/conversations?offset=${offset}&limit=${limit}&order=updated`,
      `${base()}/backend-api/conversations?cursor=${offset}&limit=${limit}&order=updated`
    ];
    for (const u of tries){
      try{
        const res = await fetch(u, { credentials:'include', cache:'no-store', headers: bearer ? { Authorization: `Bearer ${bearer}` } : undefined });
        if (!res.ok) continue;
        const j = await res.json();
        const items = j.items || j.conversations || j.data || [];
        const ids = [];
        for (const it of items){
          const id = it.id || it.conversation_id || it.conversationId;
          if (id) ids.push(id);
        }
        const total = Number(j.total ?? j.total_conversations ?? j.count ?? ids.length);
        const hasMore = Boolean(j.has_more ?? (offset + ids.length < total));
        return { ids, total, hasMore };
      }catch(e){}
    }
    return { ids: [], total: 0, hasMore: false };
  }

  async function listAllIds(bearer){
    const page = 100;
    let offset = 0;
    const all = new Set();

    const first = await listPage(0, page, bearer);
    first.ids.forEach(id => all.add(id));
    log(`found ${all.size} on first page total ${first.total || all.size}`);

    while (first.hasMore && all.size < (first.total || 999999)){
      offset += page;
      const next = await listPage(offset, page, bearer);
      next.ids.forEach(id => all.add(id));
      log(`page offset ${offset} added ${next.ids.length}`);
      if (!next.hasMore || next.ids.length === 0) break;
      await sleep(120);
    }
    return Array.from(all);
  }

  async function delSoftHard(id, bearer){
    const urls = [
      `${base()}/backend-api/conversation/${id}`,
      `${base()}/backend-api/conversations/${id}`,
      `${base()}/api/conversations/${id}`,
      `${base()}/api/conversation/${id}`
    ];
    for (const u of urls){
      const r = await http('PATCH', u, { is_visible:false }, bearer);
      log(r.ok ? `soft ok ${id} ${r.status}` : `soft fail ${id} ${r.status} ${u}`);
      if (r.ok) return true;
    }
    {
      const r = await http('POST', `${base()}/backend-api/conversations/delete`, { conversation_ids:[id] }, bearer);
      log(r.ok ? `bulk soft ok ${id} ${r.status}` : `bulk soft fail ${id} ${r.status}`);
      if (r.ok) return true;
    }
    for (const u of urls){
      const r = await http('DELETE', u, undefined, bearer);
      log(r.ok ? `hard ok ${id} ${r.status}` : `hard fail ${id} ${r.status} ${u}`);
      if (r.ok) return true;
    }
    return false;
  }

  async function delGraphQL(id, bearer){
    try{
      const payloads = [
        {
          operationName: 'deleteConversation',
          variables: { conversationId: id },
          query: 'mutation deleteConversation($conversationId:ID!){deleteConversation(conversationId:$conversationId){id}}'
        },
        {
          operationName: 'DeleteConversationMutation',
          variables: { id },
          query: 'mutation DeleteConversationMutation($id:ID!){conversationDelete(id:$id){success}}'
        }
      ];
      for (const p of payloads){
        const r = await http('POST', `${base()}/backend-api/graphql`, p, bearer);
        log(r.ok ? `gql ok ${id} ${r.status}` : `gql fail ${id} ${r.status}`);
        if (r.ok) return true;
      }
    }catch(e){
      log('gql error ' + e);
    }
    return false;
  }

  async function deleteOne(id, bearer){
    if (await delSoftHard(id, bearer)) return true;
    if (await delGraphQL(id, bearer)) return true;
    return false;
  }

  // ---------- Mount and resilience ----------
  function mountUI(){
    // create or refresh button
    let btn = get('#bd-btn');
    if (!btn){
      btn = document.createElement('button');
      btn.id = 'bd-btn';
      btn.textContent = 'Delete All Chats';
      btn.addEventListener('click', onButtonClick, { passive: true });
      document.body.appendChild(btn);
    }
    // create or refresh log
    let box = get('#bd-log');
    if (!box){
      box = document.createElement('div');
      box.id = 'bd-log';
      box.innerHTML = `
        <header>
          <b>Bulk Deleter Log</b>
          <div>
            <button id="bd-copy">Copy</button>
            <button id="bd-clear">Clear</button>
          </div>
        </header>
        <pre id="bd-pre"></pre>
      `;
      document.body.appendChild(box);
    }
    // hook actions
    const clearBtn = get('#bd-clear');
    const copyBtn  = get('#bd-copy');
    if (clearBtn && !clearBtn.__bdHooked){
      clearBtn.__bdHooked = true;
      clearBtn.onclick = () => { S.logStore.length = 0; const pre = get('#bd-pre'); if (pre) pre.textContent = ''; };
    }
    if (copyBtn && !copyBtn.__bdHooked){
      copyBtn.__bdHooked = true;
      copyBtn.onclick = () => navigator.clipboard.writeText(S.logStore.join('\n')).catch(()=>{});
    }

    S.mounted = true;
  }

  function ensureUI(){
    if (!document.body) return;
    if (!get('#bd-btn') || !get('#bd-log')) mountUI();
  }

  function hookSPARouteChanges(){
    // detect URL changes and try remount
    const origPush = history.pushState;
    const origReplace = history.replaceState;
    function onChange(){
      if (S.lastUrl !== location.href){
        S.lastUrl = location.href;
        setTimeout(ensureUI, 50);
        setTimeout(ensureUI, 300);
        setTimeout(ensureUI, 1200);
      }
    }
    history.pushState = function(...args){ const r = origPush.apply(this, args); onChange(); return r; };
    history.replaceState = function(...args){ const r = origReplace.apply(this, args); onChange(); return r; };
    window.addEventListener('popstate', onChange);
    new MutationObserver(() => ensureUI()).observe(document.documentElement, { childList: true, subtree: true });
  }

  // rescue hotkey: Ctrl Alt D remounts, Shift L D toggles log
  window.addEventListener('keydown', e => {
    if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'd'){ ensureUI(); }
    if (e.shiftKey && e.key.toLowerCase() === 'd'){
      const el = get('#bd-log');
      if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
    }
  });

  // button logic with two click arm
  async function onButtonClick(){
    if (S.running) return;
    const btn = get('#bd-btn');

    // first click scans and arms
    if (!S.armed){
      S.running = true;
      showLog(true);
      btn.disabled = true;
      log('starting');
      const bearer = await getBearer();
      btn.textContent = 'Scanning...';
      const ids = await listAllIds(bearer);
      window.__BD_IDS__ = ids;
      btn.disabled = false;
      S.running = false;
      showLog(false);

      if (!ids.length){
        btn.textContent = 'Delete All Chats';
        log('no ids found');
        return;
      }
      S.armed = true;
      btn.textContent = `Click again to delete ${ids.length} chats`;
      log(`armed with ${ids.length} ids`);
      return;
    }

    // second click executes
    if (S.armed){
      S.armed = false;
      S.running = true;
      showLog(true);
      const bearer = await getBearer();
      const ids = Array.isArray(window.__BD_IDS__) ? window.__BD_IDS__ : [];
      btn.disabled = true;

      let ok = 0, fail = 0;
      for (let i = 0; i < ids.length; i++){
        const id = ids[i];
        btn.textContent = `Deleting ${i + 1}/${ids.length}...`;
        const good = await deleteOne(id, bearer);
        if (good) ok++; else fail++;
        await sleep(120);
      }

      log(`done ok ${ok} fail ${fail}`);
      btn.textContent = 'Delete All Chats';
      btn.disabled = false;
      S.running = false;
      showLog(false);
      if (ok > 0) setTimeout(() => location.replace('https://chatgpt.com/?new'), 1200);
    }
  }

  // wait for body then mount and keep it alive
  function boot(){
    if (!document.body){ requestAnimationFrame(boot); return; }
    ensureUI();
    hookSPARouteChanges();
    if (!S.ensureTimer){
      S.ensureTimer = setInterval(ensureUI, 1500);
    }
  }

  boot();
})();