├── 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 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
Email
33 |
Password
34 |
35 |
36 |
37 |
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 | 
9 | 
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 | 
14 | 5. Click on the extension
15 | 
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 | 
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 |
--------------------------------------------------------------------------------