// ==UserScript== // @name Bulk Delete Chats for ChatGPT // @namespace https://github.com/cu-sanjay/ // @version 1.3.1 // @description Add bulk delete functionality for ChatGPT chats // @author Sanjay Choudhary // @match https://chatgpt.com/* // @icon https://img.icons8.com/?size=100&id=iGqse5s20iex&format=png&color=000000 // @grant none // ==/UserScript== (function () { "use strict"; const globalData = {}; const initGlobalData = () => { globalData.token = ""; globalData.tokenError = false; globalData.selectedChats = {}; globalData.extensionOutdated = false; }; const checkBoxHandler = (e) => { e.stopPropagation(); e.preventDefault(); // hack. Without preventDefault each click of the checkbox reloads the page on firefox. I never discovered why. // When preventing default the checkbox state does not persist to the DOM // after the user clicks. We need to manually set the DOM state. However, doing it directly // is rolled back by the browser. So we do it via setTimeout to make it work. setTimeout(()=>{ e.target.checked=!e.target.checked; },1) const liElement = e.target.closest("li"); const keys = Object.keys(liElement); let chatObj = {}; for (const key in keys) { if (keys[key].includes("reactProps")) { const propsKey = keys[key]; if (liElement[propsKey].children && liElement[propsKey].children.props) { if(!liElement[propsKey].children.props.conversation){ // the frontend has changed since we last updated this script. Make it clear that the extension does not work by disabling the checkbox. e.target.checked = false; e.target.disabled = true; e.target.style.opacity = 0.5; // TODO mark all checkboxes as disabled globalData.extensionOutdated = true; return; } const chatData = liElement[propsKey].children.props.conversation; const textContent = chatData.title; const chatId = chatData.id; chatObj = { id: chatId, text: textContent, projectionId: liElement.dataset.projectionId, }; } } } if (chatObj.id) { if (e.target.checked) { globalData.selectedChats[chatObj.id] = chatObj; } else { delete globalData.selectedChats[chatObj.id]; } } }; const addCheckboxesToChatsIfNeeded = () => { // is a chat item and doesn't already have a checkbox const chats = document.querySelectorAll( 'nav li:not([data-projection-id=""]):not(.customCheckbox)' ); chats.forEach((chat) => { if (chat.querySelector(".customCheckbox")) { return; } const inputElement = document.createElement("input"); inputElement.setAttribute("type", "checkbox"); inputElement.setAttribute("class", "customCheckbox"); inputElement.onclick = checkBoxHandler; chat.querySelector("a").insertAdjacentElement("afterbegin", inputElement); }); }; const closeDialog = () => { const dialogElement = document.getElementById("customDeleteDialogModal"); dialogElement.remove(); const inputs = document.querySelectorAll(".customCheckbox"); inputs.forEach((input) => { input.disabled = false; }); }; const getSecChUaString = () => { if (navigator.userAgentData && navigator.userAgentData.brands) { return navigator.userAgentData.brands .map((brand) => { return `"${brand.brand}";v="${brand.version}"`; }) .join(", "); } else { // fallback return '"Chromium";v="118", "Google Chrome";v="118", "Not=A?Brand";v="99"'; } }; const getPlatform = () => { if (navigator.userAgentData && navigator.userAgentData.platform) { return navigator.userAgentData.platform; } else { return `"Linux"`; } }; const getToken = () => { //https://chatgpt.com/api/auth/session return fetch("https://chatgpt.com/api/auth/session", { headers: { accept: "*/*", "accept-language": "en-US", "cache-control": "no-cache", "content-type": "application/json", pragma: "no-cache", "sec-ch-ua": getSecChUaString(), "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": getPlatform(), "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", }, referrer: "https://chatgpt.com/", referrerPolicy: "same-origin", }) .then((res) => res.json()) .then((res) => { globalData.token = res.accessToken; return res.accessToken; }) .catch((err) => { console.log(err); globalData.tokenError = true; }); }; const doDelete = (chatId) => { return fetch(`https://chatgpt.com/backend-api/conversation/${chatId}`, { headers: { accept: "*/*", "accept-language": "en-US", authorization: `Bearer ${globalData.token}`, "cache-control": "no-cache", "content-type": "application/json", pragma: "no-cache", "sec-ch-ua": getSecChUaString(), "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": getPlatform(), "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", }, referrer: `https://chatgpt.com/c/${chatId}`, referrerPolicy: "same-origin", body: '{"is_visible":false}', method: "PATCH", mode: "cors", credentials: "include", }).then((res) => res.json()); }; const setDialogError = (error) => { const errorDiv = document.getElementById("customErrorDiv"); errorDiv.innerHTML = `${error}`; }; const addBulkDeleteButton = () => { const html = `
`; document.querySelector('nav').querySelector('div').insertAdjacentHTML("afterend", html); document.getElementById("customOpenBulkDeleteDialog").onclick = showDeleteDialog; }; const deleteSelectedChats = () => { const selectedChatIds = Object.keys(globalData.selectedChats); const doDeleteLocal = (chatId) => { const dialogChatElement = document.getElementById(`custom${chatId}`); return doDelete(chatId) .then((res) => { if (res.success || res.success === false) { // remove from chats const chatElement = document.querySelector( `li[data-projection-id="${globalData.selectedChats[chatId].projectionId}"]` ); // removing the elements breaks the react client state. We'll offer to do a page refresh instead. // chatElement.closest("li").remove(); // keep globalData in sync delete globalData.selectedChats[chatId]; // strike through in dialog box and green dialogChatElement.innerHTML = `