├── .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 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 106 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | [![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://img.shields.io/badge/python-3.8-blue.svg) 11 | [![PyPi](https://img.shields.io/pypi/v/chatgptpy.svg)](https://pypi.python.org/pypi/chatgptpy) 12 | [![PyPi](https://img.shields.io/pypi/dm/chatgptpy.svg)](https://pypi.python.org/pypi/chatgptpy) 13 | 14 | *⭐️ Like this repo? please star & consider donating to keep it maintained* 15 | 16 | Buy Me A Coffee 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 | [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/yizhangliu/chatGPT) 36 | 37 |

Chatting

38 | 39 | ![Screenshot 1](https://media.discordapp.net/attachments/1038565125482881027/1049255804366237736/image.png) 40 | 41 | [//]: # (Italic centred text saying screenshots) 42 |

Creating a token

43 | 44 | ![Screenshot 2](https://media.discordapp.net/attachments/1038565125482881027/1049072247442264094/image.png?width=2468&height=885) 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 --------------------------------------------------------------------------------