├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .idea
├── .gitignore
├── PyChatGPT.iml
├── aws.xml
├── discord.xml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── LICENSE
├── README.md
├── pyproject.toml
└── src
├── __init__.py
└── pychatgpt
├── __init__.py
├── classes
├── chat.py
├── exceptions.py
├── headers.py
├── openai.py
└── spinner.py
├── main.py
└── requirements.txt
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: rawandahmad698
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Version Info (please complete the following information):**
27 | - Chatgptpy version
28 | - Dependency version
29 |
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
34 | ## Provide information on each section, or your issue will be closed.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[Feature Request]"
5 | labels: enhancement
6 | assignees: rawandahmad698
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.json
2 | *.txt
3 | dist
4 | venv
5 | __pycache__
6 | .DS_Store
7 | .env
8 | *egg-info
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/PyChatGPT.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/aws.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Rawand Ahmed Shaswar
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [Discord Discussion](https://discord.gg/MqeaZsy4F5)
2 | Current State: Not maintained. Not Working.
3 |
4 | Sorry guys! Really busy with private projects. This was very fun!
5 |
6 |
7 | # 🔥 PyChatGPT
8 | [Read More - How OpenAI filters requests made by bots/scrapers](https://github.com/rawandahmad698/PyChatGPT/discussions/103)
9 |
10 | [](https://img.shields.io/badge/python-3.8-blue.svg)
11 | [](https://pypi.python.org/pypi/chatgptpy)
12 | [](https://pypi.python.org/pypi/chatgptpy)
13 |
14 | *⭐️ Like this repo? please star & consider donating to keep it maintained*
15 |
16 |
17 |
18 | *💡 If OpenAI change their API, I will fix it as soon as possible, so Watch the repo if you want to be notified*
19 |
20 | ### Features
21 | - [x] Save Conversations to a file
22 | - [x] Resume conversations even after closing the program
23 | - [x] Proxy Support
24 | - [x] Automatically login without involving a browser
25 | - [x] Automatically grab Access Token
26 | - [x] Get around the login **captcha** (If you try to log in subsequently, you will be prompted to solve a captcha)
27 | - [x] Saves the access token to a file, so you don't have to log in again
28 | - [x] Automatically refreshes the access token when it expires
29 | - [x] Uses colorama to colorize the output, because why not?
30 | - [x] Smart Conversation Tracking
31 |
32 | ## Web Demo
33 | Integrated into [Huggingface Spaces 🤗](https://huggingface.co/spaces) using [Gradio](https://github.com/gradio-app/gradio). Try out the Web Demo
34 |
35 | [](https://huggingface.co/spaces/yizhangliu/chatGPT)
36 |
37 |
Chatting
38 |
39 | 
40 |
41 | [//]: # (Italic centred text saying screenshots)
42 | Creating a token
43 |
44 | 
45 |
46 | ```
47 | You: Hi there, My name is Rawa
48 | Chat GPT: Hello Rawa, nice to meet you. Is there something you would like to talk about or ask me? I'm here to help with any questions you may have.
49 | You: great, now say my name like Heisenberg
50 | Chat GPT: Sure, Rawa like Heisenberg. Is there anything else you would like to talk about? I'm here to help with any questions you may have.
51 | You: Sorry I meant like the episode of Breaking Bad where Walter White says Heisenberg
52 | Chat GPT: Ah, I see. In that case, you could try saying it like this: "My name is Rawa, like Heisenberg." This is a reference to the character Walter White from the TV show Breaking Bad, who often used the pseudonym "Heisenberg" when conducting illegal activities. The character was known for his cool and calculated demeanor, so saying your name like Heisenberg in this context would mean saying it with confidence and authority.
53 | ```
54 |
55 | ## Install
56 | ```
57 | pip install chatgptpy --upgrade
58 | ```
59 |
60 | ## Usage
61 | [**NEW**] Pass a `options()` object to the `ChatGPT()` constructor to customize the session
62 |
63 | [**NEW**] You can now save your conversations to a file
64 |
65 | ```python
66 | from pychatgpt import Chat, Options
67 |
68 | options = Options()
69 |
70 | # [New] Pass Moderation. https://github.com/rawandahmad698/PyChatGPT/discussions/103
71 | # options.pass_moderation = False
72 |
73 | # [New] Enable, Disable logs
74 | options.log = True
75 |
76 | # Track conversation
77 | options.track = True
78 |
79 | # Use a proxy
80 | options.proxies = 'http://localhost:8080'
81 |
82 | # Optionally, you can pass a file path to save the conversation
83 | # They're created if they don't exist
84 |
85 | # options.chat_log = "chat_log.txt"
86 | # options.id_log = "id_log.txt"
87 |
88 | # Create a Chat object
89 | chat = Chat(email="email", password="password", options=options)
90 | answer = chat.ask("How are you?")
91 | print(answer)
92 | ```
93 |
94 | [**NEW**] Resume a conversation
95 | ```python
96 | from pychatgpt import Chat
97 |
98 | # Create a Chat object
99 | chat = Chat(email="email", password="password",
100 | conversation_id="Parent Conversation ID",
101 | previous_convo_id="Previous Conversation ID")
102 |
103 | answer, parent_conversation_id, conversation_id = chat.ask("How are you?")
104 |
105 | print(answer)
106 |
107 | # Or change the conversation id later
108 | answer, _, _ = chat.ask("How are you?",
109 | previous_convo_id="Parent Conversation ID",
110 | conversation_id="Previous Conversation ID")
111 | print(answer)
112 |
113 | ```
114 | Start a CLI Session
115 | ```python
116 | from pychatgpt import Chat
117 |
118 | chat = Chat(email="email", password="password")
119 | chat.cli_chat()
120 | ```
121 |
122 | Ask a one time question
123 | ```python
124 | from pychatgpt import Chat
125 |
126 | # Initializing the chat class will automatically log you in, check access_tokens
127 | chat = Chat(email="email", password="password")
128 | answer, parent_conversation_id, conversation_id = chat.ask("Hello!")
129 | ```
130 |
131 | #### You could also manually set, get the token
132 | ```python
133 | import time
134 | from pychatgpt import OpenAI
135 |
136 | # Manually set the token
137 | OpenAI.Auth(email_address="email", password="password").save_access_token(access_token="", expiry=time.time() + 3600)
138 |
139 | # Get the token, expiry
140 | access_token, expiry = OpenAI.get_access_token()
141 |
142 | # Check if the token is valid
143 | is_expired = OpenAI.token_expired() # Returns True or False
144 | ```
145 | [//]: # (Add A changelog here)
146 | Change Log
147 |
148 | #### Update using `pip install chatgptpy --upgrade`
149 |
150 | #### 1.0.8
151 | - Fixes an issue when reading from id_log.txt
152 | - Introduces a new `pass_moderation` parameter to the `options()` class, defaults to `False`
153 | - Adds proxies to moderation.
154 | - If `pass_moderation` is True, the function is invoked in another thread, so it doesn't block the main thread.
155 |
156 | #### 1.0.7
157 | - Make a request to the mod endpoint first, otherwise a crippled version of the response is returned
158 |
159 | #### 1.0.6
160 | - New option to turn off logs.
161 | - Better Error handling.
162 | - Enhanced conversation tracking
163 | - Ask now returns a tuple of `answer, previous_convo, convo_id`
164 | - Better docs
165 |
166 | #### 1.0.5
167 | - Pull requests/minor fixes
168 |
169 | #### 1.0.4
170 | - Fixes for part 8 of token authentication
171 |
172 | #### 1.0.3
173 | - a new `options()` class method to set the options for the chat session
174 | - save the conversation to a file
175 | - resume the conversation even after closing the program
176 |
177 |
178 | #### 1.0.2
179 | - ChatGPT API switches from `action=next` to `action=variant`, frequently. This library is now using `action=variant` instead of `action=next` to get the next response from the API.
180 | - Sometimes when the server is overloaded, the API returns a `502 Bad Gateway` error.
181 | - Added Error handling if the auth.json file is not found/corrupt
182 |
183 | #### 1.0.0
184 | - Initial Release via PyPi
185 |
186 |
187 | ### Other notes
188 | If the token creation process is failing:
189 | 1. Try to use a proxy (I recommend using this always)
190 | 2. Don't try to log in too fast. At least wait 10 minutes if you're being rate limited.
191 | 3. If you're still having issues, try to use a VPN. On a VPN, the script should work fine.
192 |
193 |
194 | ### What's next?
195 | I'm planning to add a few more features, such as:
196 | - [x] A python module that can be imported and used in other projects
197 | - [x] A way to save the conversation
198 | - [ ] Better error handling
199 | - [ ] Multi-user chatting
200 |
201 | ### The whole process
202 | I have been looking for a way to interact with the new Chat GPT API, but most of the sources here on GitHub
203 | require you to have a Chromium instance running in the background. or by using the Web Inspector to grab Access Token manually.
204 |
205 | No more. I have been able to reverse engineer the API and use a TLS client to mimic a real user, allowing the script to login without setting off any bot detection techniques by Auth0
206 |
207 | Basically, the script logs in on your behalf, using a TLS client, then grabs the Access Token. It's pretty fast.
208 |
209 | First, I'd like to tell you that "just making http" requests is not going to be enough, Auth0 is smart, each process is guarded by a
210 | `state` token, which is a JWT token. This token is used to prevent CSRF attacks, and it's also used to prevent bots from logging in.
211 | If you look at the `auth.py` file, there are over nine functions, each one of them is responsible for a different task, and they all
212 | work together to create a token for you. `allow-redirects` played a huge role in this, as it allowed to navigate through the login process
213 |
214 | I work at MeshMonitors.io, We make amazing tools (Check it out yo!). I decided not to spend too much time on this, but here we are.
215 |
216 | ### Why did I do this?
217 | No one has been able to do this, and I wanted to see if I could.
218 |
219 | ### Credits
220 | - [OpenAI](https://openai.com/) for creating the ChatGPT API
221 | - [FlorianREGAZ](https://github.com/FlorianREGAZ) for the TLS Client
222 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "chatgptpy"
7 | version = "1.0.8"
8 | authors = [
9 | { name="Rawand Ahmed Shaswar", email="pychatgpt@rawa.dev" },
10 | ]
11 | description = "⚡️TLS-based ChatGPT API with auto token regeneration, conversation tracking, proxy support and more."
12 | readme = "README.md"
13 | requires-python = ">=3.9"
14 | dependencies = [
15 | "tls-client",
16 | "requests",
17 | "colorama",
18 | "svglib",
19 | "bs4",
20 | "colorama",
21 | "reportlab"
22 | ]
23 | classifiers = [
24 | "Programming Language :: Python :: 3",
25 | "License :: OSI Approved :: MIT License",
26 | "Operating System :: OS Independent",
27 | ]
28 |
29 | [project.urls]
30 | "Homepage" = "https://github.com/rawandahmad698/PyChatGPT"
31 | "Bug Tracker" = "https://github.com/rawandahmad698/PyChatGPT/issues"
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rawandahmad698/PyChatGPT/a00babdad3ccb97d4c7aba622a16917a5d3bcf55/src/__init__.py
--------------------------------------------------------------------------------
/src/pychatgpt/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import Chat
2 | from .main import Options
3 | from .classes import openai as OpenAI
--------------------------------------------------------------------------------
/src/pychatgpt/classes/chat.py:
--------------------------------------------------------------------------------
1 | # Builtins
2 | import json
3 | import os
4 | import re
5 | import threading
6 | import uuid
7 | from typing import Tuple
8 | import time
9 |
10 | # Requests
11 | import requests
12 |
13 | # Local
14 | from . import headers as Headers
15 |
16 | # Colorama
17 | import colorama
18 | colorama.init(autoreset=True)
19 |
20 | session = requests.Session()
21 | __hm = Headers.mod
22 |
23 | def _called(r, *args, **kwargs):
24 | if r.status_code == 200 and 'json' in r.headers['Content-Type']:
25 | # TODO: Add a way to check if the response is valid
26 | pass
27 |
28 |
29 | def __pass_mo(access_token: str, text: str):
30 | __pg = [
31 | 3, 4, 36, 3, 7, 50, 1, 257, 4, 47, # I had to
32 | 12, 3, 16, 1, 2, 7, 10, 15, 12, 9,
33 | 89, 47, 1, 2, 257
34 | ]
35 |
36 | payload = json.dumps({
37 | "input": text,
38 | "model": ''.join([f"{''.join([f'{k}{v}' for k, v in __hm.items()])}"[i] for i in __pg])
39 | })
40 | __hm['Authorization'] = f'Bearer {access_token}'
41 | __ux = [
42 | 58, 3, 3, 10, 25, 63, 23, 23, 17, 58, 12, 3, 70, 1, 10, 4, 2, 12,
43 | 16, 70, 17, 1, 50, 23, 180, 12, 17, 204, 4, 2, 257, 7, 12, 10, 16,
44 | 23, 50, 1, 257, 4, 47, 12, 3, 16, 1, 2, 25 # Make you look :D
45 | ]
46 |
47 | session.post(''.join([f"{''.join([f'{k}{v}' for k, v in __hm.items()])}"[i] for i in __ux]),
48 | headers=__hm,
49 | hooks={'response': _called},
50 | data=payload)
51 |
52 | def ask(
53 | auth_token: Tuple,
54 | prompt: str,
55 | conversation_id: str or None,
56 | previous_convo_id: str or None,
57 | proxies: str or dict or None,
58 | pass_moderation: bool = False,
59 | ) -> Tuple[str, str or None, str or None]:
60 | auth_token, expiry = auth_token
61 |
62 | headers = {
63 | 'Content-Type': 'application/json',
64 | 'Authorization': f'Bearer {auth_token}',
65 | 'Accept': 'text/event-stream',
66 | 'Referer': 'https://chat.openai.com/chat',
67 | 'Origin': 'https://chat.openai.com',
68 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
69 | 'X-OpenAI-Assistant-App-Id': ''
70 | }
71 |
72 | if previous_convo_id is None:
73 | previous_convo_id = str(uuid.uuid4())
74 |
75 | if conversation_id is not None and len(conversation_id) == 0:
76 | # Empty string
77 | conversation_id = None
78 |
79 | if proxies is not None:
80 | if isinstance(proxies, str):
81 | proxies = {'http': proxies, 'https': proxies}
82 |
83 | # Set the proxies
84 | session.proxies.update(proxies)
85 |
86 | if not pass_moderation:
87 | threading.Thread(target=__pass_mo, args=(auth_token, prompt)).start()
88 | time.sleep(0.5)
89 |
90 | data = {
91 | "action": "variant",
92 | "messages": [
93 | {
94 | "id": str(uuid.uuid4()),
95 | "role": "user",
96 | "content": {"content_type": "text", "parts": [str(prompt)]},
97 | }
98 | ],
99 | "conversation_id": conversation_id,
100 | "parent_message_id": previous_convo_id,
101 | "model": "text-davinci-002-render"
102 | }
103 | try:
104 | response = session.post(
105 | "https://chat.openai.com/backend-api/conversation",
106 | headers=headers,
107 | data=json.dumps(data)
108 | )
109 | if response.status_code == 200:
110 | response_text = response.text.replace("data: [DONE]", "")
111 | data = re.findall(r'data: (.*)', response_text)[-1]
112 | as_json = json.loads(data)
113 | return as_json["message"]["content"]["parts"][0], as_json["message"]["id"], as_json["conversation_id"]
114 | elif response.status_code == 401:
115 | # Check if auth.json exists, if so, delete it
116 | if os.path.exists("auth.json"):
117 | os.remove("auth.json")
118 |
119 | return f"[Status Code] 401 | [Response Text] {response.text}", None, None
120 | elif response.status_code >= 500:
121 | print(">> Looks like the server is either overloaded or down. Try again later.")
122 | return f"[Status Code] {response.status_code} | [Response Text] {response.text}", None, None
123 | else:
124 | return f"[Status Code] {response.status_code} | [Response Text] {response.text}", None, None
125 | except Exception as e:
126 | print(">> Error when calling OpenAI API: " + str(e))
127 | return "400", None, None
128 |
--------------------------------------------------------------------------------
/src/pychatgpt/classes/exceptions.py:
--------------------------------------------------------------------------------
1 | # Exceptions Class
2 |
3 | class PyChatGPTException(Exception):
4 | def __init__(self, message):
5 | self.message = message
6 |
7 |
8 | class Auth0Exception(PyChatGPTException):
9 | def __init__(self, message):
10 | self.message = message
11 |
12 |
13 | class IPAddressRateLimitException(PyChatGPTException):
14 | def __init__(self, message):
15 | self.message = message
16 |
17 |
--------------------------------------------------------------------------------
/src/pychatgpt/classes/headers.py:
--------------------------------------------------------------------------------
1 | mod = {
2 | 'Content-Type': 'application/json',
3 | 'Accept': 'text/event-stream',
4 | 'Referer': 'https://chat.openai.com/chat',
5 | 'Origin': 'https://chat.openai.com',
6 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
7 | 'OpenAI-Client-Id': 'chat',
8 | }
--------------------------------------------------------------------------------
/src/pychatgpt/classes/openai.py:
--------------------------------------------------------------------------------
1 | # Builtins
2 | import json
3 | import os
4 | import time
5 | import urllib.parse
6 | from io import BytesIO
7 | import re
8 | import base64
9 | from typing import Tuple
10 |
11 | # Client (Thank you!.. https://github.com/FlorianREGAZ)
12 | import tls_client
13 |
14 | # BeautifulSoup
15 | from bs4 import BeautifulSoup
16 |
17 | # Svg lib
18 | from svglib.svglib import svg2rlg
19 | from reportlab.graphics import renderPM
20 |
21 | # Local
22 | from . import exceptions as Exceptions
23 |
24 | # Fancy stuff
25 | import colorama
26 | from colorama import Fore
27 |
28 | colorama.init(autoreset=True)
29 |
30 |
31 | def token_expired() -> bool:
32 | """
33 | Check if the creds have expired
34 | returns:
35 | bool: True if expired, False if not
36 | """
37 | try:
38 | # Get path using os, it's in ./classes/auth.json
39 | path = os.path.dirname(os.path.abspath(__file__))
40 | path = os.path.join(path, "auth.json")
41 |
42 | with open(path, 'r') as f:
43 | creds = json.load(f)
44 | expires_at = float(creds['expires_at'])
45 | if time.time() > expires_at + 3600:
46 | return True
47 | else:
48 | return False
49 | except KeyError:
50 | return True
51 | except FileNotFoundError:
52 | return True
53 |
54 |
55 | def get_access_token() -> Tuple[str or None, str or None]:
56 | """
57 | Get the access token
58 | returns:
59 | str: The access token
60 | """
61 | try:
62 | # Get path using os, it's in ./Classes/auth.json
63 | path = os.path.dirname(os.path.abspath(__file__))
64 | path = os.path.join(path, "auth.json")
65 |
66 | with open(path, 'r') as f:
67 | creds = json.load(f)
68 | return creds['access_token'], creds['expires_at']
69 | except FileNotFoundError:
70 | return None, None
71 |
72 |
73 | class Auth:
74 | def __init__(self, email_address: str, password: str, proxy: str = None):
75 | self.email_address = email_address
76 | self.password = password
77 | self.proxy = proxy
78 | self.__session = tls_client.Session(
79 | client_identifier="chrome_105"
80 | )
81 |
82 | @staticmethod
83 | def _url_encode(string: str) -> str:
84 | """
85 | URL encode a string
86 | :param string:
87 | :return:
88 | """
89 | return urllib.parse.quote(string)
90 |
91 | def create_token(self):
92 | """
93 | Begin the auth process
94 | """
95 | if not self.email_address or not self.password:
96 | print(f"{Fore.RED}[OpenAI] {Fore.WHITE}Please provide an email address and password")
97 | raise Exceptions.PyChatGPTException("Please provide an email address and password")
98 | else:
99 | # Print email address and password
100 | print(f"{Fore.GREEN}[OpenAI] {Fore.WHITE}Email address: {self.email_address}")
101 | # Show 3 characters of password, then hide the rest
102 | print(f"{Fore.GREEN}[OpenAI] {Fore.WHITE}Password: {self.password[:3]}{'*' * len(self.password[3:])}")
103 |
104 | if self.proxy is not None:
105 | if isinstance(self.proxy, str):
106 | proxies = {
107 | "http": self.proxy,
108 | "https": self.proxy
109 | }
110 | else:
111 | proxies = self.proxy
112 | print(f"{Fore.GREEN}[OpenAI] {Fore.WHITE}Using proxy: {self.proxy}")
113 | self.__session.proxies = proxies
114 |
115 | print(f"{Fore.GREEN}[OpenAI] {Fore.WHITE}Beginning auth process")
116 | # First, make a request to https://chat.openai.com/auth/login
117 | url = "https://chat.openai.com/auth/login"
118 | headers = {
119 | "Host": "ask.openai.com",
120 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
121 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
122 | "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
123 | "Accept-Encoding": "gzip, deflate, br",
124 | "Connection": "keep-alive",
125 | }
126 | print(f"{Fore.GREEN}[OpenAI][1] {Fore.WHITE}Making request to {url}")
127 |
128 | response = self.__session.get(url=url, headers=headers)
129 | if response.status_code == 200:
130 | print(f"{Fore.GREEN}[OpenAI][1] {Fore.WHITE}Request was " + Fore.GREEN + "successful")
131 | self._part_two()
132 | else:
133 | raise Exceptions.Auth0Exception("Failed to make the first request, Try that again!")
134 |
135 | def _part_two(self):
136 | """
137 | In part two, We make a request to https://chat.openai.com/api/auth/csrf and grab a fresh csrf token
138 | """
139 |
140 | print(f"{Fore.GREEN}[OpenAI][2] {Fore.WHITE}Beginning part two")
141 | url = "https://chat.openai.com/api/auth/csrf"
142 | headers = {
143 | "Host": "ask.openai.com",
144 | "Accept": "*/*",
145 | "Connection": "keep-alive",
146 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
147 | "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
148 | "Referer": "https://chat.openai.com/auth/login",
149 | "Accept-Encoding": "gzip, deflate, br",
150 | }
151 | print(f"{Fore.GREEN}[OpenAI][2] {Fore.WHITE}Grabbing CSRF token from {url}")
152 | response = self.__session.get(url=url, headers=headers)
153 | if response.status_code == 200 and 'json' in response.headers['Content-Type']:
154 | print(f"{Fore.GREEN}[OpenAI][2] {Fore.WHITE}Request was " + Fore.GREEN + "successful")
155 | csrf_token = response.json()["csrfToken"]
156 | print(f"{Fore.GREEN}[OpenAI][2] {Fore.WHITE}CSRF Token: {csrf_token}")
157 | self._part_three(token=csrf_token)
158 | else:
159 | raise Exceptions.Auth0Exception("[OpenAI][2] Failed to make the request, Try that again!")
160 |
161 | def _part_three(self, token: str):
162 | """
163 | We reuse the token from part to make a request to /api/auth/signin/auth0?prompt=login
164 | """
165 | print(f"{Fore.GREEN}[OpenAI][3] {Fore.WHITE}Beginning part three")
166 | url = "https://chat.openai.com/api/auth/signin/auth0?prompt=login"
167 |
168 | payload = f'callbackUrl=%2F&csrfToken={token}&json=true'
169 | headers = {
170 | 'Host': 'ask.openai.com',
171 | 'Origin': 'https://chat.openai.com',
172 | 'Connection': 'keep-alive',
173 | 'Accept': '*/*',
174 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
175 | 'Referer': 'https://chat.openai.com/auth/login',
176 | 'Content-Length': '100',
177 | 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
178 | 'Content-Type': 'application/x-www-form-urlencoded',
179 | }
180 | print(f"{Fore.GREEN}[OpenAI][3] {Fore.WHITE}Making request to {url}")
181 | response = self.__session.post(url=url, headers=headers, data=payload)
182 | if response.status_code == 200 and 'json' in response.headers['Content-Type']:
183 | print(f"{Fore.GREEN}[OpenAI][3] {Fore.WHITE}Request was " + Fore.GREEN + "successful")
184 | url = response.json()["url"]
185 | if url == "https://chat.openai.com/api/auth/error?error=OAuthSignin" or 'error' in url:
186 | print(f"{Fore.GREEN}[OpenAI][3] {Fore.WHITE}Error: " + Fore.RED + "You have been rate limited")
187 | raise Exceptions.PyChatGPTException("You have been rate limited.")
188 |
189 | print(f"{Fore.GREEN}[OpenAI][3] {Fore.WHITE}Callback URL: {url}")
190 | self._part_four(url=url)
191 | elif response.status_code == 400:
192 | raise Exceptions.IPAddressRateLimitException("[OpenAI][3] Bad request from your IP address, "
193 | "try again in a few minutes")
194 | else:
195 | raise Exceptions.Auth0Exception("[OpenAI][3] Failed to make the request, Try that again!")
196 |
197 | def _part_four(self, url: str):
198 | """
199 | We make a GET request to url
200 | :param url:
201 | :return:
202 | """
203 | headers = {
204 | 'Host': 'auth0.openai.com',
205 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
206 | 'Connection': 'keep-alive',
207 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
208 | 'Accept-Language': 'en-US,en;q=0.9',
209 | 'Referer': 'https://chat.openai.com/',
210 | }
211 | print(f"{Fore.GREEN}[OpenAI][4] {Fore.WHITE}Making request to {url}")
212 | response = self.__session.get(url=url, headers=headers)
213 | if response.status_code == 302:
214 | print(f"{Fore.GREEN}[OpenAI][4] {Fore.WHITE}Request was " + Fore.GREEN + "successful")
215 | state = re.findall(r"state=(.*)", response.text)[0]
216 | state = state.split('"')[0]
217 | print(f"{Fore.GREEN}[OpenAI][4] {Fore.WHITE}Current State: {state}")
218 | self._part_five(state=state)
219 | else:
220 | raise Exceptions.Auth0Exception("[OpenAI][4] Failed to make the request, Try that again!")
221 |
222 | def _part_five(self, state: str):
223 | """
224 | We use the state to get the login page & check for a captcha
225 | """
226 | url = f"https://auth0.openai.com/u/login/identifier?state={state}"
227 |
228 | headers = {
229 | 'Host': 'auth0.openai.com',
230 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
231 | 'Connection': 'keep-alive',
232 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
233 | 'Accept-Language': 'en-US,en;q=0.9',
234 | 'Referer': 'https://chat.openai.com/',
235 | }
236 | print(f"{Fore.GREEN}[OpenAI][5] {Fore.WHITE}Making request to {url}")
237 | response = self.__session.get(url, headers=headers)
238 | if response.status_code == 200:
239 | print(f"{Fore.GREEN}[OpenAI][5] {Fore.WHITE}Request was " + Fore.GREEN + "successful")
240 | soup = BeautifulSoup(response.text, 'lxml')
241 | if soup.find('img', alt='captcha'):
242 | print(f"{Fore.RED}[OpenAI][5] {Fore.RED}Captcha detected")
243 |
244 | svg_captcha = soup.find('img', alt='captcha')['src'].split(',')[1]
245 | decoded_svg = base64.decodebytes(svg_captcha.encode("ascii"))
246 |
247 | # Convert decoded svg to png
248 | drawing = svg2rlg(BytesIO(decoded_svg))
249 |
250 | # Better quality
251 | renderPM.drawToFile(drawing, "captcha.png", fmt="PNG", dpi=300)
252 | print(f"{Fore.GREEN}[OpenAI][5] {Fore.WHITE}Captcha saved to {Fore.GREEN}captcha.png"
253 | + f" {Fore.WHITE}in the current directory")
254 |
255 | # Wait.
256 | captcha_input = input(f"{Fore.GREEN}[OpenAI][5] {Fore.WHITE}Please solve the captcha and "
257 | f"press enter to continue: ")
258 | if captcha_input:
259 | print(f"{Fore.GREEN}[OpenAI][5] {Fore.WHITE}Continuing...")
260 | self._part_six(state=state, captcha=captcha_input)
261 | else:
262 | raise Exceptions.PyChatGPTException("[OpenAI][5] You didn't enter anything.")
263 |
264 | else:
265 | print(f"{Fore.GREEN}[OpenAI][5] {Fore.GREEN}No captcha detected")
266 | self._part_six(state=state, captcha=None)
267 | else:
268 | raise Exceptions.Auth0Exception("[OpenAI][5] Failed to make the request, Try that again!")
269 |
270 | def _part_six(self, state: str, captcha: str or None):
271 | """
272 | We make a POST request to the login page with the captcha, email
273 | :param state:
274 | :param captcha:
275 | :return:
276 | """
277 | print(f"{Fore.GREEN}[OpenAI][6] {Fore.WHITE}Making request to https://auth0.openai.com/u/login/identifier")
278 | url = f"https://auth0.openai.com/u/login/identifier?state={state}"
279 | email_url_encoded = self._url_encode(self.email_address)
280 | payload = f'state={state}&username={email_url_encoded}&captcha={captcha}&js-available=true&webauthn-available=true&is-brave=false&webauthn-platform-available=true&action=default'
281 |
282 | if captcha is None:
283 | payload = f'state={state}&username={email_url_encoded}&js-available=false&webauthn-available=true&is-brave=false&webauthn-platform-available=true&action=default'
284 |
285 | headers = {
286 | 'Host': 'auth0.openai.com',
287 | 'Origin': 'https://auth0.openai.com',
288 | 'Connection': 'keep-alive',
289 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
290 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
291 | 'Referer': f'https://auth0.openai.com/u/login/identifier?state={state}',
292 | 'Accept-Language': 'en-US,en;q=0.9',
293 | 'Content-Type': 'application/x-www-form-urlencoded',
294 | }
295 | response = self.__session.post(url, headers=headers, data=payload)
296 | if response.status_code == 302:
297 | print(f"{Fore.GREEN}[OpenAI][6] {Fore.WHITE}Email found")
298 | self._part_seven(state=state)
299 | else:
300 | raise Exceptions.Auth0Exception("[OpenAI][6] Email not found, Check your email address and try again!")
301 |
302 | def _part_seven(self, state: str):
303 | """
304 | We enter the password
305 | :param state:
306 | :return:
307 | """
308 | print(f"{Fore.GREEN}[OpenAI][7] {Fore.WHITE}Entering password...")
309 | url = f"https://auth0.openai.com/u/login/password?state={state}"
310 |
311 | email_url_encoded = self._url_encode(self.email_address)
312 | password_url_encoded = self._url_encode(self.password)
313 | payload = f'state={state}&username={email_url_encoded}&password={password_url_encoded}&action=default'
314 | headers = {
315 | 'Host': 'auth0.openai.com',
316 | 'Origin': 'https://auth0.openai.com',
317 | 'Connection': 'keep-alive',
318 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
319 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
320 | 'Referer': f'https://auth0.openai.com/u/login/password?state={state}',
321 | 'Accept-Language': 'en-US,en;q=0.9',
322 | 'Content-Type': 'application/x-www-form-urlencoded',
323 | }
324 | response = self.__session.post(url, headers=headers, data=payload)
325 | is_302 = response.status_code == 302
326 | if is_302:
327 | print(f"{Fore.GREEN}[OpenAI][7] {Fore.WHITE}Password was " + Fore.GREEN + "correct")
328 | new_state = re.findall(r"state=(.*)", response.text)[0]
329 | new_state = new_state.split('"')[0]
330 | print(f"{Fore.GREEN}[OpenAI][7] {Fore.WHITE}Old state: {Fore.GREEN}{state}")
331 | print(f"{Fore.GREEN}[OpenAI][7] {Fore.WHITE}New State: {Fore.GREEN}{new_state}")
332 | self._part_eight(old_state=state, new_state=new_state)
333 | else:
334 | raise Exceptions.Auth0Exception("[OpenAI][7] Password was incorrect or captcha was wrong")
335 |
336 | def _part_eight(self, old_state: str, new_state):
337 | url = f"https://auth0.openai.com/authorize/resume?state={new_state}"
338 | print(f"{Fore.GREEN}[OpenAI][8] {Fore.WHITE}Making request to {Fore.GREEN}{url}")
339 | headers = {
340 | 'Host': 'auth0.openai.com',
341 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
342 | 'Connection': 'keep-alive',
343 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
344 | 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
345 | 'Referer': f'https://auth0.openai.com/u/login/password?state={old_state}',
346 | }
347 | response = self.__session.get(url, headers=headers, allow_redirects=True)
348 | is_200 = response.status_code == 200
349 | if is_200:
350 | print(f"{Fore.GREEN}[OpenAI][8] {Fore.WHITE}All good")
351 | soup = BeautifulSoup(response.text, 'lxml')
352 | # Find __NEXT_DATA__, which contains the data we need, the get accessToken
353 | next_data = soup.find("script", {"id": "__NEXT_DATA__"})
354 | # Access Token
355 | access_token = re.findall(r"accessToken\":\"(.*)\"", next_data.text)
356 | if access_token:
357 | access_token = access_token[0]
358 | access_token = access_token.split('"')[0]
359 | print(f"{Fore.GREEN}[OpenAI][8] {Fore.WHITE}Access Token: {Fore.GREEN}{access_token}")
360 | # Save access_token
361 | self.save_access_token(access_token=access_token)
362 | else:
363 | print(f"{Fore.GREEN}[OpenAI][8][CRITICAL] {Fore.WHITE}Access Token: {Fore.RED}Not found"
364 | f" Auth0 did not issue an access token.")
365 | self.part_nine()
366 |
367 | def part_nine(self):
368 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.WHITE}"
369 | f"Attempting to get access token from: https://chat.openai.com/api/auth/session")
370 | url = "https://chat.openai.com/api/auth/session"
371 | headers = {
372 | "Host": "ask.openai.com",
373 | "Connection": "keep-alive",
374 | "If-None-Match": "\"bwc9mymkdm2\"",
375 | "Accept": "*/*",
376 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
377 | "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
378 | "Referer": "https://chat.openai.com/chat",
379 | "Accept-Encoding": "gzip, deflate, br",
380 | }
381 | response = self.__session.get(url, headers=headers)
382 | is_200 = response.status_code == 200
383 | if is_200:
384 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.GREEN}Request was successful")
385 | if 'json' in response.headers['Content-Type']:
386 | json_response = response.json()
387 | access_token = json_response['accessToken']
388 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.WHITE}Access Token: {Fore.GREEN}{access_token}")
389 | self.save_access_token(access_token=access_token)
390 | else:
391 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.WHITE}Access Token: {Fore.RED}Not found, "
392 | f"Please try again with a proxy (or use a new proxy if you are using one)")
393 | else:
394 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.WHITE}Access Token: {Fore.RED}Not found, "
395 | f"Please try again with a proxy (or use a new proxy if you are using one)")
396 |
397 | @staticmethod
398 | def save_access_token(access_token: str, expiry: int or None = None):
399 | """
400 | Save access_token and an hour from now on CHATGPT_ACCESS_TOKEN CHATGPT_ACCESS_TOKEN_EXPIRY environment variables
401 | :param expiry:
402 | :param access_token:
403 | :return:
404 | """
405 | try:
406 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.WHITE}Saving access token...")
407 | expiry = expiry or int(time.time()) + 3600
408 |
409 | # Get path using os, it's in ./classes/auth.json
410 | path = os.path.dirname(os.path.abspath(__file__))
411 | path = os.path.join(path, "auth.json")
412 | with open(path, "w") as f:
413 | f.write(json.dumps({"access_token": access_token, "expires_at": expiry}))
414 |
415 | print(f"{Fore.GREEN}[OpenAI][8] {Fore.WHITE}Saved access token")
416 | except Exception as e:
417 | raise e
418 |
--------------------------------------------------------------------------------
/src/pychatgpt/classes/spinner.py:
--------------------------------------------------------------------------------
1 | # Thanks: @Dorcioman
2 |
3 | from itertools import cycle
4 | import threading
5 | import time
6 |
7 |
8 | class Spinner:
9 | __default_spinner_symbols_list = ['|-----|', '|#----|', '|-#---|', '|--#--|', '|---#-|', '|----#|']
10 |
11 | def __init__(self, spinner_symbols_list: [str] = None):
12 | spinner_symbols_list = spinner_symbols_list if spinner_symbols_list else Spinner.__default_spinner_symbols_list
13 | self.__screen_lock = threading.Event()
14 | self.__spinner = cycle(spinner_symbols_list)
15 | self.__stop_event = False
16 | self.__thread = None
17 |
18 | def get_spin(self):
19 | return self.__spinner
20 |
21 | def start(self, spinner_message: str):
22 | self.__stop_event = False
23 | time.sleep(0.3)
24 |
25 | def run_spinner(message):
26 | while not self.__stop_event:
27 | print("\r{message} {spinner}".format(message=message, spinner=next(self.__spinner)), end="")
28 | time.sleep(0.3)
29 |
30 | self.__screen_lock.set()
31 |
32 | self.__thread = threading.Thread(target=run_spinner, args=(spinner_message,), daemon=True)
33 | self.__thread.start()
34 |
35 | def stop(self):
36 | self.__stop_event = True
37 | if self.__screen_lock.is_set():
38 | self.__screen_lock.wait()
39 | self.__screen_lock.clear()
40 | print("\r", end="")
41 |
42 | print("\r", end="")
--------------------------------------------------------------------------------
/src/pychatgpt/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | # Builtins
5 | import sys
6 | import time
7 | import os
8 | from queue import Queue
9 | from typing import Tuple
10 |
11 | # Local
12 | from .classes import openai as OpenAI
13 | from .classes import chat as ChatHandler
14 | from .classes import spinner as Spinner
15 | from .classes import exceptions as Exceptions
16 |
17 | # Fancy stuff
18 | import colorama
19 | from colorama import Fore
20 |
21 | colorama.init(autoreset=True)
22 |
23 | class Options:
24 | def __init__(self):
25 | self.log: bool = True
26 | self.proxies: str or dict or None = None
27 | self.track: bool or None = False
28 | self.verify: bool = True
29 | self.pass_moderation: bool = False
30 | self.chat_log: str or None = None
31 | self.id_log: str or None = None
32 |
33 | def __repr__(self):
34 | return f""
37 |
38 | class Chat:
39 | def __init__(self,
40 | email: str,
41 | password: str,
42 | options: Options or None = None,
43 | conversation_id: str or None = None,
44 | previous_convo_id: str or None = None):
45 | self.email = email
46 | self.password = password
47 | self.options = options
48 |
49 | self.conversation_id = conversation_id
50 | self.previous_convo_id = previous_convo_id
51 |
52 | self.__auth_access_token: str or None = None
53 | self.__auth_access_token_expiry: int or None = None
54 | self.__chat_history: list or None = None
55 |
56 | self._setup()
57 |
58 | @staticmethod
59 | def _create_if_not_exists(file: str):
60 | if not os.path.exists(file):
61 | with open(file, 'w') as f:
62 | f.write("")
63 |
64 | def log(self, inout):
65 | if self.options is not None and self.options.log:
66 | print(inout, file=sys.stderr)
67 |
68 | def _setup(self):
69 | if self.options is not None:
70 | # If track is enabled, create the chat log and id log files if they don't exist
71 | if not isinstance(self.options.track, bool):
72 | raise Exceptions.PyChatGPTException("Options to track conversation must be a boolean.")
73 | if not isinstance(self.options.log, bool):
74 | raise Exceptions.PyChatGPTException("Options to log must be a boolean.")
75 |
76 | if self.options.track:
77 | if self.options.chat_log is not None:
78 | self._create_if_not_exists(os.path.abspath(self.options.chat_log))
79 | self.options.chat_log = os.path.abspath(self.options.chat_log)
80 | else:
81 | # Create a chat log file called chat_log.txt
82 | self.options.chat_log = "chat_log.txt"
83 | self._create_if_not_exists(self.options.chat_log)
84 |
85 | if self.options.id_log is not None:
86 | self._create_if_not_exists(os.path.abspath(self.options.id_log))
87 | self.options.id_log = os.path.abspath(self.options.id_log)
88 | else:
89 | # Create a chat log file called id_log.txt
90 | self.options.id_log = "id_log.txt"
91 | self._create_if_not_exists(self.options.id_log)
92 |
93 | if self.options.proxies is not None:
94 | if not isinstance(self.options.proxies, dict):
95 | if not isinstance(self.options.proxies, str):
96 | raise Exceptions.PyChatGPTException("Proxies must be a string or dictionary.")
97 | else:
98 | self.proxies = {"http": self.options.proxies, "https": self.options.proxies}
99 | self.log(f"{Fore.GREEN}>> Using proxies: True.")
100 |
101 | if self.options.track:
102 | self.log(f"{Fore.GREEN}>> Tracking conversation enabled.")
103 | if not isinstance(self.options.chat_log, str) or not isinstance(self.options.id_log, str):
104 | raise Exceptions.PyChatGPTException(
105 | "When saving a chat, file paths for chat_log and id_log must be strings.")
106 | elif len(self.options.chat_log) == 0 or len(self.options.id_log) == 0:
107 | raise Exceptions.PyChatGPTException(
108 | "When saving a chat, file paths for chat_log and id_log cannot be empty.")
109 |
110 | self.__chat_history = []
111 | else:
112 | self.options = Options()
113 |
114 |
115 | if not self.email or not self.password:
116 | self.log(f"{Fore.RED}>> You must provide an email and password when initializing the class.")
117 | raise Exceptions.PyChatGPTException("You must provide an email and password when initializing the class.")
118 |
119 | if not isinstance(self.email, str) or not isinstance(self.password, str):
120 | self.log(f"{Fore.RED}>> Email and password must be strings.")
121 | raise Exceptions.PyChatGPTException("Email and password must be strings.")
122 |
123 | if len(self.email) == 0 or len(self.password) == 0:
124 | self.log(f"{Fore.RED}>> Email cannot be empty.")
125 | raise Exceptions.PyChatGPTException("Email cannot be empty.")
126 |
127 | if self.options is not None and self.options.track:
128 | try:
129 | with open(self.options.id_log, "r") as f:
130 | # Check if there's any data in the file
131 | if os.path.getsize(self.options.id_log) > 0:
132 | self.previous_convo_id = f.readline().strip()
133 | self.conversation_id = f.readline().strip()
134 | else:
135 | self.conversation_id = None
136 | except IOError:
137 | raise Exceptions.PyChatGPTException("When resuming a chat, conversation id and previous conversation id in id_log must be separated by newlines.")
138 | except Exception:
139 | raise Exceptions.PyChatGPTException("When resuming a chat, there was an issue reading id_log, make sure that it is formatted correctly.")
140 |
141 | # Check for access_token & access_token_expiry in env
142 | if OpenAI.token_expired():
143 | self.log(f"{Fore.RED}>> Access Token missing or expired."
144 | f" {Fore.GREEN}Attempting to create them...")
145 | self._create_access_token()
146 | else:
147 | access_token, expiry = OpenAI.get_access_token()
148 | self.__auth_access_token = access_token
149 | self.__auth_access_token_expiry = expiry
150 |
151 | try:
152 | self.__auth_access_token_expiry = int(self.__auth_access_token_expiry)
153 | except ValueError:
154 | self.log(f"{Fore.RED}>> Expiry is not an integer.")
155 | raise Exceptions.PyChatGPTException("Expiry is not an integer.")
156 |
157 | if self.__auth_access_token_expiry < time.time():
158 | self.log(f"{Fore.RED}>> Your access token is expired. {Fore.GREEN}Attempting to recreate it...")
159 | self._create_access_token()
160 |
161 | def _create_access_token(self) -> bool:
162 | openai_auth = OpenAI.Auth(email_address=self.email, password=self.password, proxy=self.options.proxies)
163 | openai_auth.create_token()
164 |
165 | # If after creating the token, it's still expired, then something went wrong.
166 | is_still_expired = OpenAI.token_expired()
167 | if is_still_expired:
168 | self.log(f"{Fore.RED}>> Failed to create access token.")
169 | return False
170 |
171 | # If created, then return True
172 | return True
173 |
174 | def ask(self, prompt: str,
175 | previous_convo_id: str or None = None,
176 | conversation_id: str or None = None,
177 | rep_queue: Queue or None = None
178 | ) -> Tuple[str or None, str or None, str or None] or None:
179 |
180 | if prompt is None:
181 | self.log(f"{Fore.RED}>> Enter a prompt.")
182 | raise Exceptions.PyChatGPTException("Enter a prompt.")
183 |
184 | if not isinstance(prompt, str):
185 | raise Exceptions.PyChatGPTException("Prompt must be a string.")
186 |
187 | if len(prompt) == 0:
188 | raise Exceptions.PyChatGPTException("Prompt cannot be empty.")
189 |
190 | if rep_queue is not None and not isinstance(rep_queue, Queue):
191 | raise Exceptions.PyChatGPTException("Cannot enter a non-queue object as the response queue for threads.")
192 |
193 | # Check if the access token is expired
194 | if OpenAI.token_expired():
195 | self.log(f"{Fore.RED}>> Your access token is expired. {Fore.GREEN}Attempting to recreate it...")
196 | did_create = self._create_access_token()
197 | if did_create:
198 | self.log(f"{Fore.GREEN}>> Successfully recreated access token.")
199 | else:
200 | self.log(f"{Fore.RED}>> Failed to recreate access token.")
201 | raise Exceptions.PyChatGPTException("Failed to recreate access token.")
202 |
203 | # Get access token
204 | access_token = OpenAI.get_access_token()
205 |
206 | # Set conversation IDs if supplied
207 | if previous_convo_id is not None:
208 | self.previous_convo_id = previous_convo_id
209 | if conversation_id is not None:
210 | self.conversation_id = conversation_id
211 |
212 | answer, previous_convo, convo_id = ChatHandler.ask(auth_token=access_token, prompt=prompt,
213 | conversation_id=self.conversation_id,
214 | previous_convo_id=self.previous_convo_id,
215 | proxies=self.options.proxies,
216 | pass_moderation=self.options.pass_moderation)
217 |
218 | if rep_queue is not None:
219 | rep_queue.put((prompt, answer))
220 |
221 | if answer == "400" or answer == "401":
222 | self.log(f"{Fore.RED}>> Failed to get a response from the API.")
223 | return None
224 |
225 | self.conversation_id = convo_id
226 | self.previous_convo_id = previous_convo
227 |
228 | if self.options.track:
229 | self.__chat_history.append("You: " + prompt)
230 | self.__chat_history.append("Chat GPT: " + answer)
231 | self.save_data()
232 |
233 | return answer, previous_convo, convo_id
234 |
235 | def save_data(self):
236 | if self.options.track:
237 | try:
238 | with open(self.options.chat_log, "a") as f:
239 | f.write("\n".join(self.__chat_history) + "\n")
240 |
241 | with open(self.options.id_log, "w") as f:
242 | f.write(str(self.previous_convo_id) + "\n")
243 | f.write(str(self.conversation_id) + "\n")
244 |
245 | except Exception as ex:
246 | self.log(f"{Fore.RED}>> Failed to save chat and ids to chat log and id_log."
247 | f"{ex}")
248 | finally:
249 | self.__chat_history = []
250 |
251 | def cli_chat(self, rep_queue: Queue or None = None):
252 | """
253 | Start a CLI chat session.
254 | :param rep_queue: A queue to put the prompt and response in.
255 | :return:
256 | """
257 | if rep_queue is not None and not isinstance(rep_queue, Queue):
258 | self.log(f"{Fore.RED}>> Entered a non-queue object to hold responses for another thread.")
259 | raise Exceptions.PyChatGPTException("Cannot enter a non-queue object as the response queue for threads.")
260 |
261 | # Check if the access token is expired
262 | if OpenAI.token_expired():
263 | self.log(f"{Fore.RED}>> Your access token is expired. {Fore.GREEN}Attempting to recreate it...")
264 | did_create = self._create_access_token()
265 | if did_create:
266 | self.log(f"{Fore.GREEN}>> Successfully recreated access token.")
267 | else:
268 | self.log(f"{Fore.RED}>> Failed to recreate access token.")
269 | raise Exceptions.PyChatGPTException("Failed to recreate access token.")
270 | else:
271 | self.log(f"{Fore.GREEN}>> Access token is valid.")
272 | self.log(f"{Fore.GREEN}>> Starting CLI chat session...")
273 | self.log(f"{Fore.GREEN}>> Type 'exit' to exit the chat session.")
274 |
275 |
276 | # Get access token
277 | access_token = OpenAI.get_access_token()
278 |
279 | while True:
280 | try:
281 | prompt = input("You: ")
282 | if prompt.replace("You: ", "") == "exit":
283 | self.save_data()
284 | break
285 |
286 | spinner = Spinner.Spinner()
287 | spinner.start(Fore.YELLOW + "Chat GPT is typing...")
288 | answer, previous_convo, convo_id = ChatHandler.ask(auth_token=access_token, prompt=prompt,
289 | conversation_id=self.conversation_id,
290 | previous_convo_id=self.previous_convo_id,
291 | proxies=self.options.proxies,
292 | pass_moderation=self.options.pass_moderation)
293 |
294 | if rep_queue is not None:
295 | rep_queue.put((prompt, answer))
296 |
297 | if answer == "400" or answer == "401":
298 | self.log(f"{Fore.RED}>> Failed to get a response from the API.")
299 | return None
300 |
301 | self.conversation_id = convo_id
302 | self.previous_convo_id = previous_convo
303 | spinner.stop()
304 | print(f"Chat GPT: {answer}")
305 |
306 | if self.options.track:
307 | self.__chat_history.append("You: " + prompt)
308 | self.__chat_history.append("Chat GPT: " + answer)
309 |
310 | except KeyboardInterrupt:
311 | print(f"{Fore.RED}>> Exiting...")
312 | break
313 | finally:
314 | self.save_data()
315 |
--------------------------------------------------------------------------------
/src/pychatgpt/requirements.txt:
--------------------------------------------------------------------------------
1 | tls-client
2 | requests~=2.27.1
3 | colorama~=0.4.4
4 | svglib~=1.4.1
5 | bs4~=0.0.1
6 | beautifulsoup4~=4.10.0
7 | reportlab~=3.6.12
--------------------------------------------------------------------------------