diff --git a/src/main/Companies/OpenAI/chatgpt.js b/src/main/Companies/OpenAI/chatgpt.js index af7b6343..90b6361e 100644 --- a/src/main/Companies/OpenAI/chatgpt.js +++ b/src/main/Companies/OpenAI/chatgpt.js @@ -36,14 +36,56 @@ async function exportChatgpt(company, runID) { confirmExport.click(); // TODO: automatically go to user's email and download the file + await wait(3) + bigStepper(runID) + ipcRenderer.sendToHost('change-url', 'https://gmail.com', runID) // later this will not be hardcoded +} + +async function continueExportChatgpt(id){ + // Check for the email every second + if (document.querySelector('h1')) { + ipcRenderer.send('connect-website', company); + return; + } + + let emailFound = false; + const checkEmails = async () => { + const emails = await waitForElement("div.xS[role='link']", 'Download Email', true); + for (const email of emails) { + if (email.innerText.includes('ChatGPT - Your data export is ready')) { + bigStepper(id) + email.click(); + emailFound = true; + break; + } + } + }; - // ipcRenderer.sendToHost('new-url', 'https://gmail.com') + while (!emailFound) { + await checkEmails(); + if (!emailFound) { + await wait(1); // Wait for 1 second before checking again + } + } - // ipcRenderer.on('new-url-success', (event, url) => { - // customConsoleLog('New URL:', url); - // // execute clicking the email + downloading! - // }); + // Wait for the email to load + await wait(2); + let downloadBtns = []; + while (downloadBtns.length === 0) { + downloadBtns = await waitForElement( + 'a[href*="https://proddatamgmtqueue.blob.core.windows.net/exportcontainer/"]', + 'Download button', + true + ); + if (downloadBtns.length === 0) { + await wait(1); // Wait for 1 second before checking again + } + } + customConsoleLog('downloadBtns: ', downloadBtns); + bigStepper(id) + downloadBtns[downloadBtns.length - 1].click(); + } -module.exports = exportChatgpt; \ No newline at end of file +module.exports = { exportChatgpt, continueExportChatgpt }; \ No newline at end of file diff --git a/src/main/Companies/OpenAI/chatgpt.md b/src/main/Companies/OpenAI/chatgpt.md index 13f9e729..b6a4479a 100644 --- a/src/main/Companies/OpenAI/chatgpt.md +++ b/src/main/Companies/OpenAI/chatgpt.md @@ -8,6 +8,7 @@ The `exportChatgpt()` function performs these tasks: 1. Checks if the user is connected to ChatGPT. 2. Navigates through the ChatGPT interface to reach the export dialog. 3. Initiates the export of all conversation history. +4. Goes to gmail and downloads the exported file. ## Implementation @@ -22,10 +23,11 @@ The ChatGPT export process is integrated into the main application via `preloadW 1. DOM Manipulation: The module relies on specific element attributes and text content to navigate the ChatGPT interface. 2. Timing: Fixed timeouts are used to account for page load times. 3. Element Visibility: The function waits for specific elements to appear before interacting with them. +4. Dynamic Download: The function goes to gmail and downloads the exported file. ## Future Improvements 1. Error Handling: Implement more robust error handling for each step of the process. 2. Dynamic Waiting: Replace fixed timeouts with dynamic waiting for elements to appear. -3. Automatic Download: Implement automatic access to the user's email to download the exported file. +3. Dynamic Download: While this works on gmail, not every user has a gmail account. Thus, other platforms need to be handled. 4. Progress Tracking: Implement a way to track and report the export progress. diff --git a/src/main/main.ts b/src/main/main.ts index 71de6137..ac5dcf79 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -155,7 +155,8 @@ export const createWindow = async (visible: boolean = true) => { details.url.includes('about:blank') || details.url.includes('file://') || details.url.includes('https://www.notion.so/verifyNoPopupBlocker') || - details.url.includes('https://appleid.apple.com/auth/') + details.url.includes('https://appleid.apple.com/auth/') || + details.url.includes('https://proddatamgmtqueue.blob.core.windows.net/exportcontainer/') ) { console.log('ALLOWING THIS URL: ', details.url); return { action: 'allow' }; @@ -217,7 +218,7 @@ export const createWindow = async (visible: boolean = true) => { if (process.env.START_MINIMIZED) { mainWindow.minimize(); } else { - mainWindow.show(); + mainWindow.show(); } }); @@ -316,21 +317,35 @@ export const createWindow = async (visible: boolean = true) => { const userData = app.getPath('userData'); const surferDataPath = path.join(userData, 'surfer_data'); - let platformPath; + let companyPath: string; + let platformPath: string; if (url.includes('file.notion.so')) { - platformPath = path.join(surferDataPath, 'Notion'); + companyPath = path.join(surferDataPath, 'Notion'); + platformPath = path.join(companyPath, 'Notion'); + } else if (url.includes('proddatamgmtqueue.blob.core.windows.net/exportcontainer/')) { + companyPath = path.join(surferDataPath, 'OpenAI'); + platformPath = path.join(companyPath, 'ChatGPT'); + } else { + console.error('Unknown download URL:', url); + return; } // Create surfer_data folder if it doesn't exist if (!fs.existsSync(surferDataPath)) { fs.mkdirSync(surferDataPath); } - // Create platform_name folder if it doesn't exist, and clear it if it does + + // Create company folder if it doesn't exist + if (!fs.existsSync(companyPath)) { + fs.mkdirSync(companyPath); + } + + // Create or clear platform_name folder if (!fs.existsSync(platformPath)) { fs.mkdirSync(platformPath); } else { - fs.readdirSync(platformPath).forEach((file) => { + fs.readdirSync(platformPath).forEach((file: string) => { const filePath = path.join(platformPath, file); if (fs.lstatSync(filePath).isDirectory()) { fs.rmSync(filePath, { recursive: true }); @@ -340,8 +355,6 @@ export const createWindow = async (visible: boolean = true) => { }); } - // Create a workspace-specific folder within the downloads directory - event.preventDefault(); console.log('Starting download:', url); @@ -349,9 +362,9 @@ export const createWindow = async (visible: boolean = true) => { download(mainWindow, url, { directory: platformPath, filename: fileName, - onStarted: (downloadItem) => { + onStarted: (downloadItem: Electron.DownloadItem) => { console.log('Download started:', url); - downloadItem.on('done', (event, state) => { + downloadItem.on('done', (event: Electron.Event, state: string) => { if (state === 'completed') { console.log('Download completed successfully:', url); } else if (state === 'cancelled') { @@ -359,7 +372,7 @@ export const createWindow = async (visible: boolean = true) => { } }); }, - onProgress: (percent) => { + onProgress: (percent: number) => { console.log(`Download progress for ${url}: ${percent}%`); mainWindow?.webContents.send('download-progress', { fileName, @@ -368,7 +381,7 @@ export const createWindow = async (visible: boolean = true) => { }, saveAs: false, }) - .then((dl) => { + .then((dl: Electron.DownloadItem) => { console.log('Download completed:', dl.getSavePath()); const filePath = dl.getSavePath(); @@ -380,7 +393,7 @@ export const createWindow = async (visible: boolean = true) => { fs.unlinkSync(filePath); console.log('Original zip file removed:', filePath); - mainWindow?.webContents.send('export-complete', 'Notion', 'Notion', 0, platformPath); + mainWindow?.webContents.send('export-complete', path.basename(companyPath), path.basename(platformPath), 0, platformPath); mainWindow?.webContents.send('download-complete', { fileName, filePath: platformPath, @@ -388,7 +401,7 @@ export const createWindow = async (visible: boolean = true) => { extracted: true, }); }) - .catch((error) => { + .catch((error: Error) => { console.error('Error extracting zip file:', error); mainWindow?.webContents.send('download-error', { fileName, @@ -405,7 +418,7 @@ export const createWindow = async (visible: boolean = true) => { }); } }) - .catch((error) => { + .catch((error: Error) => { console.error('Download failed:', error); console.error('Error stack:', error.stack); mainWindow?.webContents.send('download-error', { @@ -414,8 +427,7 @@ export const createWindow = async (visible: boolean = true) => { }); }); }); - -}; +} ipcMain.on('open-external', (event, url) => { diff --git a/src/main/preloadWebview.js b/src/main/preloadWebview.js index b1a4c630..6d11effe 100644 --- a/src/main/preloadWebview.js +++ b/src/main/preloadWebview.js @@ -17,7 +17,7 @@ const exportTwitter = require('./Companies/X Corp/twitter'); const electronHandler = require('./preloadElectron'); const exportGmail = require('./Companies/Google/gmail'); const exportYouTube = require('./Companies/Google/youtube'); -const exportChatgpt = require('./Companies/OpenAI/chatgpt'); +const { exportChatgpt, continueExportChatgpt } = require('./Companies/OpenAI/chatgpt'); contextBridge.exposeInMainWorld('electron', electronHandler); ipcRenderer.on('export-website', async (event, company, name, runID) => { @@ -44,7 +44,7 @@ ipcRenderer.on('export-website', async (event, company, name, runID) => { await exportYouTube(company, name, runID); break; case 'ChatGPT': - await exportChatgpt(company, name, runID); + await exportChatgpt(company, runID); break; } }); @@ -54,3 +54,9 @@ ipcRenderer.on('export-website', async (event, company, name, runID) => { await continueExportGithub(); } })(); + +ipcRenderer.on('change-url-success', async (event, url, id) => { + if (id.includes('chatgpt-001')) { + await continueExportChatgpt(id); + } +}) diff --git a/src/renderer/components/profile/DataExtractionTable.jsx b/src/renderer/components/profile/DataExtractionTable.jsx index 886e25e5..a4ee349c 100644 --- a/src/renderer/components/profile/DataExtractionTable.jsx +++ b/src/renderer/components/profile/DataExtractionTable.jsx @@ -13,6 +13,7 @@ import { Checkbox } from "../ui/checkbox"; import { formatDistanceToNow, parseISO } from 'date-fns'; import { Progress } from "../ui/progress"; import RunDetailsPage from './RunDetailsPage'; +import { platform } from 'os'; const DataExtractionTable = ({ onPlatformClick, webviewRef }) => { const dispatch = useDispatch(); @@ -68,19 +69,18 @@ const DataExtractionTable = ({ onPlatformClick, webviewRef }) => { // }, []) useEffect(() => { - const handleExportComplete = (platformId, name, runID, namePath) => { + const handleExportComplete = (company, name, runID, namePath) => { - if (name === 'Notion') { - console.log('stopping notion run: ', runs) - // change this to .filter or smth else later to account for multiple notion runs - const notionRun = runs.filter(run => run.platformId === 'notion-001')[0]; - - dispatch(updateExportStatus(platformId, name, notionRun.id, namePath)); + if (runID === 0) { + console.log('stopping download run: ', runs) + const downloadRun = runs.filter(run => run.platformId === `${name.toLowerCase()}-001`)[0]; + // change this to .filter or smth else later to account for multiple download runs + dispatch(updateExportStatus(company, name, downloadRun.id, namePath)); } else { - console.log('stopping run for platform id: ', platformId, ', and name: ', name, ', and runID: ', runID) - dispatch(updateExportStatus(platformId, name, runID, namePath)); + console.log('stopping run for platform id: ', company, name, ', and runID: ', runID) + dispatch(updateExportStatus(company, name, runID, namePath)); } @@ -123,7 +123,9 @@ const DataExtractionTable = ({ onPlatformClick, webviewRef }) => { ); }; - const filteredPlatforms = platforms.filter(platform => + const filteredPlatforms = platforms + .filter(platform => platform.steps) + .filter(platform => platform.name.toLowerCase().includes(searchTerm.toLowerCase()) || platform.company.toLowerCase().includes(searchTerm.toLowerCase()) ); diff --git a/src/renderer/components/profile/WebviewManager.tsx b/src/renderer/components/profile/WebviewManager.tsx index e668b533..510a25a8 100644 --- a/src/renderer/components/profile/WebviewManager.tsx +++ b/src/renderer/components/profile/WebviewManager.tsx @@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import { ChevronLeft, ChevronRight, X, Square, Bug } from 'lucide-react'; import { IAppState } from '../../types/interfaces'; -import { setActiveRunIndex, closeRun, toggleRunVisibility, stopRun, adjustActiveRunIndex, updateExportStatus } from '../../state/actions'; +import { setActiveRunIndex, closeRun, toggleRunVisibility, stopRun, adjustActiveRunIndex, updateRunURL } from '../../state/actions'; import { platforms } from '../../config/platforms'; import { useTheme } from '../ui/theme-provider'; import { openDB } from 'idb'; // Import openDB for IndexedDB operations @@ -199,11 +199,17 @@ const WebviewManager: React.FC = ({ webviewRef, isConnected // UPDATE IN REDUX HERE! } - if (channel === 'new-url') { - console.log('new url: ', args[0]) - webview.src = args[0]; + if (channel === 'change-url') { + const url = args[0] + const id = args[1] + console.log('this runs: ', runs) + console.log('this url: ', url) + console.log('this id: ', id) + const run = runs.find(run => run.id === id) + console.log('this run: ', run) + dispatch(updateRunURL(id, url)); await new Promise((resolve) => setTimeout(resolve, 2000)) - webview.send('new-url-success') + webview.send('change-url-success', url, id) } }; diff --git a/src/renderer/config/platforms.ts b/src/renderer/config/platforms.ts index b764d7f1..1b863b3a 100644 --- a/src/renderer/config/platforms.ts +++ b/src/renderer/config/platforms.ts @@ -465,7 +465,7 @@ export const platforms: IPlatform[] = [ light: ChatGPTLight, dark: ChatGPTDark, }, - company: 'Open AI', + company: 'OpenAI', companyLogo: '/assets/logos/openai.png', home_url: 'https://chatgpt.com/#settings/DataControls', subRuns: [], @@ -474,6 +474,9 @@ export const platforms: IPlatform[] = [ { id: 'step-001', name: 'Go to ChatGPT Data Controls', status: 'pending' }, { id: 'step-002', name: 'Click on Export', status: 'pending' }, { id: 'step-003', name: 'Click on Confirm Export', status: 'pending' }, + { id: 'step-004', name: 'Go to Gmail and wait for export email', status: 'pending' }, + { id: 'step-005', name: 'Click on export email', status: 'pending' }, + { id: 'step-007', name: 'Download export', status: 'pending' }, ], }, { diff --git a/src/renderer/state/actions.ts b/src/renderer/state/actions.ts index 8aed9e06..85d4b962 100644 --- a/src/renderer/state/actions.ts +++ b/src/renderer/state/actions.ts @@ -110,9 +110,9 @@ export const stopRun = (runId: string) => ({ payload: runId, }); -export const updateExportStatus = (platformId: string, name: string, runID: string, exportPath: string) => ({ +export const updateExportStatus = (company: string, name: string, runID: string, exportPath: string) => ({ type: 'UPDATE_EXPORT_STATUS', - payload: { platformId, name, runID, exportPath }, + payload: { company, name, runID, exportPath }, }); export const setExportRunning = (platformId: string, isRunning: boolean) => ({ @@ -148,3 +148,8 @@ export const updateBreadcrumbToIndex = (index: number) => ({ export const stopAllJobs = () => ({ type: 'STOP_ALL_JOBS', }); + +export const updateRunURL = (runId: string, newUrl: string) => ({ + type: 'UPDATE_RUN_URL', + payload: { runId, newUrl } +}); \ No newline at end of file diff --git a/src/renderer/state/reducers.ts b/src/renderer/state/reducers.ts index 494400f6..5a3005c2 100644 --- a/src/renderer/state/reducers.ts +++ b/src/renderer/state/reducers.ts @@ -124,22 +124,28 @@ const runsReducer = (state: IRun[] = initialAppState.runs, action: any): IRun[] ? { ...run, status: action.payload.isRunning ? 'running' : 'pending' } : run ); - case 'STOP_ALL_JOBS': - return state.map(run => ({ - ...run, - status: run.status === 'running' ? 'stopped' : run.status, - endDate: run.status === 'running' ? new Date().toISOString() : run.endDate, - tasks: run.tasks.map(task => ({ - ...task, - status: task.status === 'running' ? 'stopped' : task.status, - endTime: task.status === 'running' ? new Date().toISOString() : task.endTime, - steps: task.steps.map(step => ({ - ...step, - status: step.status === 'running' ? 'stopped' : step.status, - endTime: step.status === 'running' ? new Date().toISOString() : step.endTime, - })), + case 'STOP_ALL_JOBS': + return state.map(run => ({ + ...run, + status: run.status === 'running' ? 'stopped' : run.status, + endDate: run.status === 'running' ? new Date().toISOString() : run.endDate, + tasks: run.tasks.map(task => ({ + ...task, + status: task.status === 'running' ? 'stopped' : task.status, + endTime: task.status === 'running' ? new Date().toISOString() : task.endTime, + steps: task.steps.map(step => ({ + ...step, + status: step.status === 'running' ? 'stopped' : step.status, + endTime: step.status === 'running' ? new Date().toISOString() : step.endTime, })), - })); + })), + })); + case 'UPDATE_RUN_URL': + return state.map(run => + run.id === action.payload.runId + ? { ...run, url: action.payload.newUrl } + : run + ); default: return state; } @@ -270,4 +276,4 @@ const rootReducer = customCombineReducers({ isMac: isMacReducer, }); -export default rootReducer; +export default rootReducer; \ No newline at end of file