ChatGPT Bulk Deleter

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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