├── popup ├── popup.html ├── popup.js ├── options.html ├── options.css └── options.js ├── LICENSE ├── manifest.json ├── src ├── login.js └── content.js ├── background.js └── README.md /popup/popup.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ChatGPT-Hackers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT API Client", 3 | "description": "A module for interacting with the ChatGPT API", 4 | "version": "1.2", 5 | "manifest_version": 2, 6 | "homepage_url": "https://github.com/ChatGPT-Hackers/firefox-client", 7 | "background": { 8 | "scripts": ["background.js"] 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [ 13 | "https://chat.openai.com/chat", 14 | "https://chat.openai.com/chat/*" 15 | ], 16 | "js": ["src/content.js"], 17 | "run_at": "document_end" 18 | }, 19 | { 20 | "matches": ["https://chat.openai.com/auth/login", "https://auth0.openai.com/u/login/*"], 21 | "js": ["src/login.js"], 22 | "run_at": "document_idle" 23 | } 24 | ], 25 | "permissions": [ 26 | "cookies", 27 | "webRequest", 28 | "storage", 29 | "tabs", 30 | "contextualIdentities", 31 | "" 32 | ], 33 | "browser_action": { 34 | "default_title": "Click here to open ChatGPT" 35 | }, 36 | "options_ui": { 37 | "page": "popup/options.html", 38 | "open_in_tab": true 39 | }, 40 | "browser_specific_settings": { 41 | "gecko": { 42 | "id": "acheong@student.dalat.org" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /popup/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
EmailPassword
38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/login.js: -------------------------------------------------------------------------------- 1 | /// login.js 2 | console.log("login.js loaded"); 3 | 4 | // Find the button with the text "Log in" 5 | let button = document.querySelector( 6 | ".btn.flex.justify-center.gap-2.btn-primary" 7 | ); 8 | 9 | if (button && button.innerText === "Log in") { 10 | button.click(); 11 | } else { 12 | console.error("button not found"); 13 | } 14 | 15 | // Get credentials from cookies 16 | const cookies = document.cookie.split("; "); 17 | // Get email and password from cookies 18 | let email = cookies.find((cookie) => cookie.startsWith("email=")).split("=")[1]; 19 | let password = cookies 20 | .find((cookie) => cookie.startsWith("password=")) 21 | .split("=")[1]; 22 | 23 | function setUsername() { 24 | let emailForm = document.getElementById("username"); 25 | if (emailForm) { 26 | emailForm.value = email; 27 | console.log("email: ", email); 28 | } else { 29 | setTimeout(setUsername, 1000); 30 | } 31 | } 32 | 33 | setUsername(); 34 | 35 | function setPassword() { 36 | let passwordForm = document.getElementById("password"); 37 | if (passwordForm) { 38 | passwordForm.value = password; 39 | console.log("password: ", password); 40 | } else { 41 | setTimeout(setPassword, 1000); 42 | } 43 | } 44 | setPassword(); 45 | 46 | // c8fca5323 cb6b7c993 cee1c07cc c850d9a60 _button-login-password 47 | button = document.querySelector("._button-login-password"); 48 | if (button) { 49 | button.click(); 50 | } else { 51 | console.error("button not found"); 52 | } 53 | -------------------------------------------------------------------------------- /popup/options.css: -------------------------------------------------------------------------------- 1 | /* options.css */ 2 | 3 | /* Style the "Add Credential" button */ 4 | button#add-credential { 5 | background-color: #4caf50; 6 | color: white; 7 | padding: 12px 20px; 8 | border: none; 9 | border-radius: 4px; 10 | cursor: pointer; 11 | margin-top: 20px; 12 | } 13 | 14 | button#add-credential:hover { 15 | background-color: #45a049; 16 | } 17 | 18 | /* Style the credential popup */ 19 | .popup { 20 | display: none; /* Hide the popup by default */ 21 | position: fixed; /* Stay in place */ 22 | z-index: 1; /* Sit on top */ 23 | left: 0; 24 | top: 0; 25 | width: 100%; /* Full width */ 26 | height: 100%; /* Full height */ 27 | overflow: auto; /* Enable scroll if needed */ 28 | background-color: rgb(0, 0, 0); /* Fallback color */ 29 | background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ 30 | } 31 | 32 | /* Style the form in the popup */ 33 | .popup form { 34 | background-color: #fefefe; 35 | margin: 15% auto; /* 15% from the top and centered */ 36 | padding: 20px; 37 | border: 1px solid #888; 38 | width: 80%; 39 | } 40 | 41 | /* Style the "Save" button in the popup */ 42 | .popup button[type="submit"] { 43 | background-color: #4caf50; 44 | color: white; 45 | padding: 12px 20px; 46 | border: none; 47 | border-radius: 4px; 48 | cursor: pointer; 49 | } 50 | 51 | .popup button[type="submit"]:hover { 52 | background-color: #45a049; 53 | } 54 | 55 | /* Style the "Save" button in the form */ 56 | button[type="submit"] { 57 | background-color: #4caf50; 58 | color: white; 59 | padding: 12px 20px; 60 | border: none; 61 | border-radius: 4px; 62 | cursor: pointer; 63 | margin-top: 20px; 64 | } 65 | 66 | button[type="submit"]:hover { 67 | background-color: #45a049; 68 | } 69 | 70 | /* Style the credential list */ 71 | .credential-list { 72 | margin-top: 20px; 73 | } 74 | 75 | table { 76 | /* Table border */ 77 | border: 1px solid rgb(0, 0, 0); 78 | border-collapse: collapse; 79 | width: 80%; 80 | margin: 1rem 81 | } 82 | 83 | td{ 84 | border: 1px solid rgb(0, 0, 0); 85 | padding: 5px; 86 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // Add an event listener for the browser action icon click event 2 | browser.browserAction.onClicked.addListener(() => { 3 | // Get credentials from storage 4 | browser.storage.local.get("credentials").then((results) => { 5 | for (let i = 0; i < results.credentials.length; i++) { 6 | // Open a new tab for each credential 7 | new_tab(results.credentials[i].email, results.credentials[i].password); 8 | } 9 | console.log("credentials: ", results); 10 | console.log("credentials.length: ", results.credentials.length); 11 | }); 12 | }); 13 | 14 | browser.tabs.onRemoved.addListener((tabID) => { 15 | // Delete the container with the tabID as the name OpenAI (tabID) 16 | browser.contextualIdentities.query({}).then((containers) => { 17 | containers.forEach((container) => { 18 | if (container.name == "OpenAI (" + tabID + ")") { 19 | browser.contextualIdentities.remove(container.cookieStoreId); 20 | } 21 | }); 22 | }); 23 | }); 24 | 25 | function new_tab(email, password) { 26 | // Create a new container with tabID 27 | browser.contextualIdentities 28 | .create({ 29 | name: "OpenAI (tmp)", 30 | color: "blue", 31 | icon: "circle", 32 | }) 33 | .then((container) => { 34 | // Save email and password to container's cookies 35 | browser.cookies.set({ 36 | url: "https://auth0.openai.com", 37 | name: "email", 38 | value: email, 39 | storeId: container.cookieStoreId, 40 | }); 41 | browser.cookies.set({ 42 | url: "https://auth0.openai.com", 43 | name: "password", 44 | value: password, 45 | storeId: container.cookieStoreId, 46 | }); 47 | // Create a new tab in the container with the tabID and URL 48 | browser.tabs 49 | .create({ 50 | cookieStoreId: container.cookieStoreId, 51 | url: "https://chat.openai.com", 52 | }) 53 | .then((tabID) => { 54 | // Add the tabID to the container 55 | browser.contextualIdentities.update(container.cookieStoreId, { 56 | name: "OpenAI (" + tabID.id + ")", 57 | }); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT API Agent (Firefox version) 2 | 3 | # Setup 4 | 1. Download from [Mozilla Addons](https://addons.mozilla.org/en-US/firefox/addon/chatgpt-api-client/) 5 | 6 | # Running 7 | 1. Go to extension preferences 8 | ![image](https://user-images.githubusercontent.com/36258159/209443449-73ca41c3-39ad-4429-b1b7-028b508dddff.png) 9 | ![image](https://user-images.githubusercontent.com/36258159/209443463-7ca046e3-758b-4541-8b9d-f0f5eeebbc58.png) 10 | 2. Configure endpoint from the [server](https://github.com/ChatGPT-Hackers/ChatGPT-API-server) 11 | 3. Add emails/passwords to the preferences 12 | 4. Press save 13 | ![image](https://user-images.githubusercontent.com/36258159/209443551-ce03ce90-d1de-4e42-8b35-df46bb70c62b.png) 14 | 5. Click on the extension 15 | ![image](https://user-images.githubusercontent.com/36258159/209443565-6bb9866a-99d2-4947-96e9-2934c93db80c.png) 16 | This will spawn the same number of tabs as there are emails/passwords 17 | 6. Wait for it to load 18 | 7. Complete the captcha and press continue (email/password autofills) 19 | ![image](https://user-images.githubusercontent.com/36258159/209443617-d96ee8d2-a016-4fa1-85da-f815a38e0087.png) 20 | 8. After that, it will autofill the password and continue to the chat site. 21 | 22 | Done. It connects to the endpoint and you can leave it open. 23 | 24 |
25 |
26 |
27 | 28 | # Firefox Docker (optional) 29 | 30 | ```yaml 31 | version: '3.3' 32 | services: 33 | firefox: 34 | container_name: firefox 35 | ports: 36 | - '5800:5800' 37 | volumes: 38 | - ':/config:rw' 39 | image: jlesage/firefox 40 | 41 | ``` 42 | 1. create a folder that will contain the app data for firefox 43 | 2. access container via `:5800` and finish the firefox setup 44 | 3. procceed to follow step 1 in Setup section 45 | 4. now follow steps in Running section 46 | 47 | # Contributing 48 | In order to develop locally you need to use guide provided by Mozilla: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#trying_it_out and follow next steps: 49 | 1. Clone this repository 50 | 2. Go to `about:debugging` in Firefox 51 | 3. Specify this directory as a temporary extension 52 | 4. It will be loaded on top of the existing extension if you have one 53 | 5. You can debug your new feature. 54 | -------------------------------------------------------------------------------- /popup/options.js: -------------------------------------------------------------------------------- 1 | // options.js 2 | let credentials = []; 3 | 4 | // Get references to the elements we will be interacting with 5 | const addCredentialButton = document.getElementById("add-credential"); 6 | const credentialPopup = document.getElementById("credential-popup"); 7 | const credentialList = document.getElementById("credential-list"); 8 | const submitCredentialButton = document.getElementById("submit-credential"); 9 | let save = document.querySelector("#save"); 10 | let clear = document.querySelector("#clear"); 11 | 12 | // Get the endpoint string from the extension's storage (if it exists) 13 | browser.storage.local.get("endpoint").then((result) => { 14 | // If the endpoint string exists, set the value of the form's endpoint input 15 | if (result.endpoint) { 16 | // If endpoint is empty, set it to the default value 17 | if (result.endpoint == "") { 18 | result.endpoint = "localhost:8080"; 19 | } 20 | document.querySelector("#endpoint").value = result.endpoint; 21 | } 22 | }); 23 | 24 | // Get the credentials array from the extension's storage (if it exists) 25 | browser.storage.local.get("credentials").then((result) => { 26 | // If the credentials array exists, add each credential to the list 27 | if (result.credentials) { 28 | credentials = result.credentials; 29 | for (let credential of result.credentials) { 30 | const tableRow = document.createElement("tr"); 31 | const emailCell = document.createElement("td"); 32 | emailCell.textContent = credential.email; 33 | const passwordCell = document.createElement("td"); 34 | passwordCell.textContent = credential.password; 35 | tableRow.appendChild(emailCell); 36 | tableRow.appendChild(passwordCell); 37 | credentialList.appendChild(tableRow); 38 | } 39 | } 40 | }); 41 | 42 | // Add an event listener to the "Add Credential" button 43 | addCredentialButton.addEventListener("click", () => { 44 | // Show the credential popup 45 | credentialPopup.style.display = "block"; 46 | }); 47 | 48 | // Add an event listener to the "Save" button in the popup 49 | submitCredentialButton.addEventListener("click", (event) => { 50 | event.preventDefault(); // prevent the form from being submitted 51 | 52 | // Get the email and password from the form 53 | const email = document.getElementById("email").value; 54 | const password = document.getElementById("password").value; 55 | 56 | // Add the credentials to the list 57 | const tableRow = document.createElement("tr"); 58 | const emailCell = document.createElement("td"); 59 | emailCell.textContent = email; 60 | const passwordCell = document.createElement("td"); 61 | passwordCell.textContent = password; 62 | tableRow.appendChild(emailCell); 63 | tableRow.appendChild(passwordCell); 64 | credentialList.appendChild(tableRow); 65 | 66 | // Save the credentials to credentials array 67 | credentials.push({ email: email, password: password }); 68 | 69 | // Clear the form and close the popup 70 | document.getElementById("email").value = ""; 71 | document.getElementById("password").value = ""; 72 | credentialPopup.style.display = "none"; 73 | 74 | // Save the credentials to the extension's storage 75 | browser.storage.local.set({ credentials: credentials }); 76 | }); 77 | 78 | // Add an event listener for the submit event 79 | save.addEventListener("click", function (event) { 80 | // Prevent the form from being submitted 81 | event.preventDefault(); 82 | 83 | // Get the endpoint string from the form 84 | const endpoint = document.querySelector("#endpoint").value; 85 | 86 | // Save the endpoint string to the extension's storage 87 | browser.storage.local.set({ endpoint: endpoint }); 88 | }); 89 | 90 | clear.addEventListener("click", function (event) { 91 | // Prevent the form from being submitted 92 | event.preventDefault(); 93 | 94 | // Clear the endpoint string from the extension's storage 95 | browser.storage.local.remove("endpoint"); 96 | 97 | // Clear the endpoint string from the form 98 | document.querySelector("#endpoint").value = ""; 99 | 100 | // Clear credentials 101 | credentials = []; 102 | browser.storage.local.remove("credentials"); 103 | // Refresh the page 104 | location.reload(); 105 | }); -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = "https://chat.openai.com"; 2 | const CHAT_URL = `${BASE_URL}/chat`; 3 | const BACKEND_URL = `${BASE_URL}/backend-api/conversation`; 4 | const SESSION_URL = `${BASE_URL}/api/auth/session`; 5 | 6 | browser.storage.local.get("endpoint").then((result) => { 7 | const endpoint = result.endpoint || "localhost:8080"; 8 | const wsRoute = "ws://" + endpoint + "/client/register"; 9 | const ws = new WebSocket(wsRoute); 10 | console.info("Connecting to " + wsRoute); 11 | 12 | window.onunload = function () { 13 | console.info("Connection closed"); 14 | ws.close(); 15 | }; 16 | 17 | ws.onerror = function (error) { 18 | console.error("An error occured"); 19 | console.error(error); 20 | console.info("Connection closed"); 21 | ws.close(); 22 | }; 23 | 24 | ws.onopen = function () { 25 | console.info("Connection opened"); 26 | ws.onmessage = function (event) { 27 | const data = JSON.parse(event.data); 28 | 29 | console.log(`data ${JSON.stringify(data)}`); 30 | switch (data.message) { 31 | case "Connection id": 32 | handleConnectionId(ws, data); 33 | break; 34 | case "ping": 35 | handlePing(ws, data); 36 | break; 37 | case "ChatGptRequest": 38 | handleChatGptRequest(ws, data); 39 | break; 40 | default: 41 | console.error("Unknown message: " + data.message); 42 | break; 43 | } 44 | }; 45 | }; 46 | 47 | ws.onclose = function () { 48 | console.info("Connection closed"); 49 | delete connectionId; 50 | }; 51 | }); 52 | 53 | function handleConnectionId(ws, data) { 54 | // Get connection id from cookies (if it exists) 55 | const cookies = document.cookie.split(";"); 56 | let storedConnectionId = ""; 57 | for (let i = 0; i < cookies.length; i++) { 58 | const cookie = cookies[i]; 59 | if (cookie.includes("connectionId")) { 60 | storedConnectionId = cookie.split("=")[1]; 61 | } 62 | } 63 | console.debug(`storedConnectionId ${storedConnectionId}`); 64 | // If it exists, send it to the server 65 | if (storedConnectionId) { 66 | sendWebSocketMessage(ws, storedConnectionId, "Connection id", ""); 67 | } else { 68 | sendWebSocketMessage(ws, data.id, "Connection id", ""); 69 | // Store connectionId in cookie 70 | document.cookie = "connectionId=" + data.id; 71 | } 72 | } 73 | 74 | function handlePing(ws, data) { 75 | sendWebSocketMessage(ws, data.id, "pong", ""); 76 | } 77 | 78 | async function handleChatGptRequest(ws, data) { 79 | // Construct API request 80 | const requestData = JSON.parse(data.data); 81 | // If conversation_id is "", make it undefined 82 | if (requestData.conversation_id == "") { 83 | requestData.conversation_id = undefined; 84 | } 85 | // Payload 86 | const payload = { 87 | action: "next", 88 | messages: [ 89 | { 90 | id: requestData.message_id, 91 | role: "user", 92 | content: { content_type: "text", parts: [requestData.content] }, 93 | }, 94 | ], 95 | parent_message_id: requestData.parent_id, 96 | conversation_id: requestData.conversation_id, 97 | model: "text-davinci-002-render", 98 | }; 99 | 100 | try { 101 | const accessToken = await getAccessToken(); 102 | const conversationResponse = await sendChatRequest(accessToken, payload); 103 | const responseData = createResponseData(conversationResponse); 104 | sendWebSocketMessage(ws, data.id, "ChatGptResponse", responseData); 105 | } catch (error) { 106 | console.error(error); 107 | sendWebSocketMessage(ws, data.id, "error", "Unknown error", error); 108 | } 109 | } 110 | 111 | async function getAccessToken() { 112 | try { 113 | const sessionResponse = await fetchData(SESSION_URL); 114 | const sessionResponseJson = await sessionResponse.json(); 115 | return sessionResponseJson.accessToken; 116 | } catch (error) { 117 | console.error(error); 118 | throw error; 119 | } 120 | } 121 | 122 | async function sendChatRequest(accessToken, payload) { 123 | try { 124 | const response = await fetchData(BACKEND_URL, { 125 | method: "POST", 126 | headers: { 127 | Accept: "text/event-stream", 128 | Authorization: `Bearer ${accessToken}`, 129 | "Content-Type": "application/json", 130 | "X-Openai-Assistant-App-Id": "", 131 | Connection: "close", 132 | Referer: CHAT_URL, 133 | }, 134 | body: JSON.stringify(payload), 135 | }); 136 | const conversationResponse = await response.text(); 137 | try { 138 | // Check if conversationResponse can be parsed as JSON 139 | const respJson = JSON.parse(conversationResponse); 140 | if (respJson.detail) { 141 | console.error(`Error: ${respJson.detail}`); 142 | throw new Error(`Error: ${respJson.detail}`); 143 | } 144 | } catch (e) { 145 | console.log("Not JSON"); 146 | } 147 | // Split data on "data: " prefix 148 | const dataArray = conversationResponse.split("data: "); 149 | // Get the second last element of the array 150 | const lastElement = JSON.parse(dataArray[dataArray.length - 2]); 151 | console.log(lastElement); 152 | return lastElement; 153 | } catch (error) { 154 | console.error(error); 155 | throw error; 156 | } 157 | } 158 | 159 | function createResponseData(conversationResponse) { 160 | return JSON.stringify({ 161 | response_id: conversationResponse.message.id, 162 | conversation_id: conversationResponse.conversation_id, 163 | content: conversationResponse.message.content.parts[0], 164 | }); 165 | } 166 | 167 | /** 168 | * @param {WebSocket} ws The websocket server 169 | * @param {string} id The data id 170 | * @param {string} message The message type 171 | * @param {string} data The data to send 172 | * @param {string} error If there is an error, the error message 173 | */ 174 | function sendWebSocketMessage(ws, id, message, data, error) { 175 | try { 176 | const wsMessage = { 177 | id, 178 | message, 179 | data, 180 | error, 181 | }; 182 | ws.send(JSON.stringify(wsMessage)); 183 | } catch (error) { 184 | console.error("Error sending websocket message"); 185 | console.error(error); 186 | } 187 | } 188 | 189 | /** 190 | * Fetch data from an url with optional options. 191 | * It will throw an error if the response is not ok, or if the status code is not 200. 192 | * 193 | * @async 194 | * @param {String} url The url to fetch from 195 | * @param {RequestInit} options The options to pass to fetch 196 | * @returns The response 197 | */ 198 | async function fetchData(url, options = {}) { 199 | const response = await window.fetch(url, options); 200 | if (!response.ok) { 201 | throw new Error(`Request failed with status code: ${response.status}`); 202 | } 203 | 204 | if (response.status !== 200) { 205 | // This isn't the best way to handle the status code check, but it works for now 206 | throw new Error(`Wrong response code: ${response.status}`); 207 | } 208 | 209 | return response; 210 | } 211 | --------------------------------------------------------------------------------