// ==UserScript== // @name ChatGPT Backup 100% // @namespace @reinaldyalaratte // @version 1.1 // @description ChatGPT backup history chat become HTML in local storage (Include video, image, pdf etc.) // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @icon https://static.vecteezy.com/system/resources/previews/021/059/825/non_2x/chatgpt-logo-chat-gpt-icon-on-green-background-free-vector.jpg // @license CC-BY-NC // @grant GM_registerMenuCommand // @downloadURL https://update.greasyfork.icu/scripts/558073/ChatGPT%20Backup%20100%25.user.js // @updateURL https://update.greasyfork.icu/scripts/558073/ChatGPT%20Backup%20100%25.meta.js // ==/UserScript== (function () { 'use strict'; const dataUrlCache = new Map(); GM_registerMenuCommand('Backup chat ini (ChatGPT)', () => { backupChat().catch(e => { console.error('Error backup:', e); alert('Terjadi error saat backup. Lihat console (F12).'); }); }); async function backupChat() { const pageUrl = location.href; const pageTitle = document.title || 'ChatGPT Conversation'; const timestamp = new Date().toISOString(); const anchorNodes = Array.from(document.querySelectorAll('[data-message-author-role]')); if (anchorNodes.length === 0) { alert('Tidak ditemukan pesan di halaman ini.'); return; } const anchorInfos = anchorNodes.map((a) => { const role = (a.dataset && a.dataset.messageAuthorRole ? a.dataset.messageAuthorRole : 'unknown').toLowerCase(); const root = a.closest('div[data-testid="conversation-turn"]') || a.closest('article') || a.parentElement || a; return { anchor: a, root, role }; }); const anchorInfoByNode = new Map(anchorInfos.map(info => [info.anchor, info])); const gptImgSelector = 'img[alt="Gambar yang dibuat"], img[alt="Image generated"], img[alt="Generated image"]'; const streamNodes = Array.from( document.querySelectorAll('[data-message-author-role], ' + gptImgSelector) ); const messages = []; const gallery = []; const gallerySeen = new Set(); let userCount = 0; let assistantCount = 0; let systemCount = 0; const GPT_ALT_SET = new Set([ 'Gambar yang dibuat', 'Image generated', 'Generated image' ]); function shouldSkipImageSrc(src, alt) { if (src && src.startsWith('data:image') && (!alt || !alt.trim())) { return true; } return false; } async function addImageToMessage(msg, embedded, alt) { if (!msg) return; if (!embedded) return; if (shouldSkipImageSrc(embedded, alt)) return; if (!msg.images.some(i => i.src === embedded)) { msg.images.push({ src: embedded, alt }); } if (!gallerySeen.has(embedded)) { gallerySeen.add(embedded); gallery.push({ src: embedded, alt }); } } function findContainingMessage(messagesArr, node) { for (let i = messagesArr.length - 1; i >= 0; i--) { if (messagesArr[i]._root && messagesArr[i]._root.contains(node)) { return messagesArr[i]; } } return null; } for (const node of streamNodes) { if (node.hasAttribute && node.hasAttribute('data-message-author-role')) { const info = anchorInfoByNode.get(node); if (!info) continue; const role = info.role; let displayIndex; if (role === 'user') { userCount += 1; displayIndex = userCount; } else if (role === 'assistant') { assistantCount += 1; displayIndex = assistantCount; } else { systemCount += 1; displayIndex = systemCount; } const text = extractText(info.root); const attachments = await extractAttachments(info.root); const msg = { role, displayIndex, text, images: [], attachments, _root: info.root }; const imgs = Array.from(info.root.querySelectorAll('img')); const seenSrc = new Set(); for (const img of imgs) { if (!img.src) continue; if (seenSrc.has(img.src)) continue; seenSrc.add(img.src); const alt = img.alt || ''; if (GPT_ALT_SET.has(alt)) continue; if (shouldSkipImageSrc(img.src, alt)) continue; const embedded = await toEmbeddedSrc(img.src); await addImageToMessage(msg, embedded, alt); } messages.push(msg); } else if (node.tagName && node.tagName.toLowerCase() === 'img') { const alt = node.alt || ''; if (!GPT_ALT_SET.has(alt)) continue; if (node.src && node.src.startsWith('data:image')) { continue; } if (shouldSkipImageSrc(node.src, alt)) continue; const embedded = await toEmbeddedSrc(node.src); if (!embedded || shouldSkipImageSrc(embedded, alt)) continue; let targetMsg = findContainingMessage(messages, node); if (!targetMsg) { const last = messages[messages.length - 1]; if (last && last.role === 'assistant') { targetMsg = last; } } if (!targetMsg) { assistantCount += 1; targetMsg = { role: 'assistant', displayIndex: assistantCount, text: '', images: [], attachments: [], _root: null }; messages.push(targetMsg); } await addImageToMessage(targetMsg, embedded, alt); } } messages.forEach(m => { delete m._root; }); const html = buildHtml({ meta: { title: pageTitle, url: pageUrl, exported_at: timestamp }, messages, galleryImages: gallery }); const filename = `chatgpt_backup_${timestamp.replace(/[:.]/g, '-')}.html`; downloadHtml(html, filename); } function extractText(root) { const clone = root.cloneNode(true); clone.querySelectorAll('button,input,textarea,svg,nav,header,footer,[role="button"]').forEach(el => el.remove()); return (clone.innerText || clone.textContent || '').trim(); } async function extractAttachments(root) { const links = Array.from(root.querySelectorAll('a[href]')); const out = []; const seen = new Set(); for (const a of links) { let href = a.getAttribute('href'); if (!href) continue; const lower = href.toLowerCase(); const isFileLike = a.hasAttribute('download') || /\.(zip|rar|7z|pdf|docx?|xlsx?|pptx?|csv|txt|json|mp4|mov|avi|mkv|webm|mp3|wav|flac|ogg)$/i.test(lower) || lower.startsWith('blob:') || lower.includes('files.openai.com'); if (!isFileLike) continue; if (seen.has(href)) continue; seen.add(href); const nameFromAttr = a.getAttribute('download'); const nameFromUrl = href.split(/[?#]/)[0].split('/').pop() || 'file'; const filename = nameFromAttr || nameFromUrl; const src = await toEmbeddedSrc(href); out.push({ name: filename, src }); } return out; } async function toEmbeddedSrc(url) { try { if (url.startsWith('data:')) return url; if (dataUrlCache.has(url)) return dataUrlCache.get(url); const res = await fetch(url); const blob = await res.blob(); const dataUrl = await new Promise((resolve, reject) => { const r = new FileReader(); r.onloadend = () => resolve(r.result); r.onerror = reject; r.readAsDataURL(blob); }); dataUrlCache.set(url, dataUrl); return dataUrl; } catch (e) { console.warn('Gagal embed resource, pakai URL asli:', url, e); return url; } } function downloadHtml(html, filename) { const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function buildHtml(data) { const { meta, messages, galleryImages } = data; return ` Backup ChatGPT - ${escapeHtml(meta.title)}
@reinaldyalaratte

ChatGPT Backup 100%

Tittle: ${escapeHtml(meta.title)}
URL: ${escapeHtml(meta.url)}
Date & Times: ${escapeHtml(meta.exported_at)}
Total Messages: ${messages.length}
${messages.map(m => `
[${m.role.toUpperCase()} #${m.displayIndex}]
${escapeHtml(m.text || '')}
${m.images && m.images.length ? `
${m.images.map(img => ` ${escapeHtml(img.alt || '')} `).join('')}
` : ''} ${m.attachments && m.attachments.length ? ` ` : ''}
`).join('')} ${galleryImages && galleryImages.length ? ` ` : ''}
`; } function escapeHtml(str) { if (str == null) return ''; return String(str) .replace(/&/g, '&') .replace(/