├── .gitignore ├── LICENSE ├── README.md ├── gptauto ├── __init__.py ├── cli │ ├── __init__.py │ └── ask_gpt.py └── gpt_scraper.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | test*.py 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Stefano Raneri 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 | # gptauto 2 | 3 | ## What is this? 4 | 5 | This Python scraper provides access to the conversational capabilities of [ChatGPT](https://chat.openai.com/) through a simple chat messaging interface. 6 | 7 | While not officially supported by OpenAI, this library can enable interesting conversational applications. 8 | 9 | It allows for: 10 | 11 | - Creating chat sessions with ChatGPT and getting chat IDs. 12 | - Sending messages to specific chat ids, and even toggle chat history. 13 | - Get an ordered, strong typed list of messages from any chat. 14 | 15 | It relies on [geckodriver](https://github.com/mozilla/geckodriver), [selenium-wire](https://github.com/wkeeling/selenium-wire) and [selgym (selenium)](https://github.com/st1vms/selgym) libraries only. 16 | 17 | ## Table of Content 18 | 19 | - [Installation](#installation) 20 | - [Uninstallation](#uninstallation) 21 | - [Requirements](#requirements) 22 | - [Example Usage](#example-usage) 23 | - [ask-gpt](#ask-gpt) 24 | - [Using Python](#using-python) 25 | - [Toggling Chat History](#toggling-chat-history) 26 | - [Known Bugs](#known-bugs) 27 | - [Disclaimer](#disclaimer) 28 | - [Donations](#donations) 29 | 30 | ## Installation 31 | 32 | Download this repository and install it from source by runnig this command inside the repository folder: 33 | 34 | ```shell 35 | pip install -e . 36 | ``` 37 | 38 | ## Uninstallation 39 | 40 | ```shell 41 | pip uninstall gptauto 42 | ``` 43 | 44 | ## Requirements 45 | 46 | - Python >= 3.10 47 | - [geckodriver](https://github.com/mozilla/geckodriver/releases) installed in a folder registered in PATH 48 | - A valid login to [ChatGPT](https://chat.openai.com/) on a newly created Firefox profile 49 | (A new Firefox profile is needed in order for `selenium-wire` proxy to work). 50 | - Disabling ChatGPT's chat history on this profile is also recommended. 51 | 52 | ## Example usage 53 | 54 | ### ask-gpt 55 | 56 | By installing this library using `pip`, a command-line interface is provided, `ask-gpt`, it takes one positional argument, the input TEXT for chat-gpt, that can also be prepended with an header (prompt) using the `-p` string parameter. 57 | 58 | Other parameters are: 59 | 60 | ```txt 61 | -f Firefox profile 62 | (Firefox root profile path string, default behaviour is creating a new one) 63 | 64 | -ts Type speed 65 | (Maximum type speed expressed as a float number >= 0.001, defaults to 0.01) 66 | This value is used to randomly sleep for a specific interval between each individual key-stroke. 67 | 68 | -nh No-Headless 69 | (Turn off selenium Headless mode, for debugging purposes) 70 | ``` 71 | 72 | This utility is also pipe friendly, having the ability to pipe input strings as the Chat GPT input directly into the command. 73 | 74 | For example in Linux distributions: 75 | 76 | ```bash 77 | echo "Roll a D20" | ask-gpt -p "Only answer with a number" | cat "output.txt" 78 | ``` 79 | 80 | Or in Windows CMD: 81 | 82 | ```cmd 83 | ( echo "Roll a D20" & ask-gpt -p "Only answer with a number" ) >> output.txt 84 | ``` 85 | 86 | ### Using Python 87 | 88 | ```py 89 | from gptauto.scraper import GPTScraper 90 | 91 | # Set to None to use default firefox profile 92 | # Set to a string with the root profile directory path 93 | # to use a different firefox profile 94 | PROFILE_PATH = None 95 | 96 | 97 | def _main() -> None: 98 | 99 | scraper = GPTScraper(profile_path=PROFILE_PATH) 100 | try: 101 | # Creates a new webdriver instance 102 | # opening chatgpt page 103 | scraper.start() 104 | 105 | # Pick text to send 106 | text = input("\nText\n>>").strip() 107 | scraper.send_message(text) 108 | 109 | # Waits for completion to finish 110 | scraper.wait_completion() 111 | 112 | # Retrieves chat messages 113 | # as an ordered list of AssistantMessage 114 | # or UserMessage 115 | messages = list(scraper.get_messages()) 116 | if messages: 117 | print(f"\nANSWER:\n\n{messages[-1].text}\n") 118 | except KeyboardInterrupt: 119 | return 120 | finally: 121 | # Gracefully quit the webdriver instance 122 | scraper.quit() 123 | # After calling quit() 124 | # a new session can be started with .start() 125 | 126 | 127 | if __name__ == "__main__": 128 | _main() 129 | ``` 130 | 131 | ### Toggling chat history 132 | 133 | ```py 134 | from gptauto.scraper import GPTScraper 135 | 136 | # Open new session on default firefox profile 137 | scraper = GPTScraper() 138 | try: 139 | scraper.start() 140 | 141 | # Toggle chat history 142 | # If On -> Off 143 | # If Off -> On 144 | scraper.toggle_history() 145 | finally: 146 | scraper.quit() 147 | ``` 148 | 149 | ## Known bugs 150 | 151 | - Sometimes a captcha may appear after sending a message, 152 | so far this software does nothing to prevent this or act accordingly, if a captcha is triggered, the only current solution is to solve it manually, having non-headless behavior set by default. 153 | 154 | - Currently this software does not work in headless mode, I don't know if I will ever be able to find a solution in the near future. Feel free to open a pull request if you found a better one and would like to contribute 🦾 155 | 156 | ## Disclaimer 157 | 158 | This repository provides a way for automating free accounts on [ChatGPT](https://chat.openai.com/). 159 | Please note that this software is not endorsed, supported, or maintained by OpenAI. Use it at your own discretion and risk. OpenAI may make changes to their official product or APIs at any time, which could affect the functionality of this software. We do not guarantee the accuracy, reliability, or security of the information and data retrieved using this API. By using this repository, you agree that the maintainers are not responsible for any damages, issues, or consequences that may arise from its usage. Always refer to OpenAI's official documentation and terms of use. This project is maintained independently by contributors who are not affiliated with OpenAI. 160 | 161 | ## Donations 162 | 163 | A huge thank you in advance to anyone who wants to donate :) 164 | 165 | ![Buy Me a Pizza](https://img.buymeacoffee.com/button-api/?text=1%20Pizza%20Margherita&emoji=🍕&slug=st1vms&button_colour=0fa913&font_colour=ffffff&font_family=Bree&outline_colour=ffffff&coffee_col) 166 | -------------------------------------------------------------------------------- /gptauto/__init__.py: -------------------------------------------------------------------------------- 1 | """gptauto module""" 2 | 3 | from .gpt_scraper import GPTScraper, DriverNotInitializedError 4 | 5 | __all__ = ["GPTScraper", "DriverNotInitializedError"] 6 | -------------------------------------------------------------------------------- /gptauto/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st1vms/gptauto/69c817f0fa524383d7e67a9f31c37d933d81d515/gptauto/cli/__init__.py -------------------------------------------------------------------------------- /gptauto/cli/ask_gpt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Ask GPT utility""" 3 | 4 | import sys 5 | import argparse 6 | from selgym import cleanup_resources 7 | from gptauto.gpt_scraper import GPTScraper, AssistantMessage 8 | 9 | PROFILE_PATH = "" 10 | 11 | 12 | def main() -> None: 13 | """ask-gpt main entry point""" 14 | parser = argparse.ArgumentParser(description="Ask GPT utility") 15 | text = None 16 | if not sys.stdin.isatty(): 17 | text = sys.stdin.read().strip() 18 | else: 19 | parser.add_argument("TEXT", type=str, help="Input text") 20 | 21 | parser.add_argument( 22 | "-nh", "--no-headless", action="store_true", help="Toggle headless mode off" 23 | ) 24 | parser.add_argument( 25 | "-f", 26 | "--firefox-profile", 27 | type=str, 28 | help="Override default firefox profile path", 29 | ) 30 | 31 | parser.add_argument( 32 | "-p", "--prompt", type=str, help="Set a prompt prepending the input" 33 | ) 34 | 35 | args = parser.parse_args() 36 | 37 | if text is None: 38 | text = args.TEXT.strip() 39 | 40 | if args.prompt is not None: 41 | text = f"{args.prompt.strip()}\n\n{text}" 42 | 43 | firefox_profile = args.firefox_profile if args.firefox_profile else PROFILE_PATH 44 | scraper = GPTScraper( 45 | profile_path=firefox_profile, 46 | headless=not args.no_headless, 47 | ) 48 | 49 | try: 50 | scraper.start() 51 | scraper.send_message(text) 52 | scraper.wait_completion() 53 | messages = list(scraper.get_messages()) 54 | if messages and isinstance(messages[-1], AssistantMessage): 55 | print(f"{messages[-1].text.strip()}") 56 | except KeyboardInterrupt: 57 | pass 58 | except TimeoutError: 59 | pass 60 | finally: 61 | scraper.quit() 62 | cleanup_resources() 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /gptauto/gpt_scraper.py: -------------------------------------------------------------------------------- 1 | """GPT Scraper module""" 2 | 3 | from json import loads 4 | from time import perf_counter, sleep 5 | from random import uniform 6 | from uuid import UUID 7 | from dataclasses import dataclass 8 | from typing import Iterator, Union, Dict 9 | from seleniumwire import webdriver 10 | from selenium.webdriver import Keys 11 | from selgym.gym import ( 12 | cleanup_resources, 13 | get_default_firefox_profile, 14 | get_firefox_options, 15 | wait_element_by, 16 | wait_elements_by, 17 | ActionChains, 18 | WebElement, 19 | By, 20 | ) 21 | 22 | BASE_URL = "https://chatgpt.com/" 23 | 24 | TEXTAREA_CSSS = 'div[id="prompt-textarea"]' 25 | 26 | FINAL_COMPLETION_CSSS = 'button[class*="md:group-[.final-completion]:visible"]' 27 | 28 | PROFILE_BUTTON_CSSS = 'img[alt="User"]' 29 | 30 | SETTINGS_MENU_CSSS = 'div[id^="headlessui-menu-items"]' 31 | 32 | CHAT_MESSAGE_XPATH = '//div[contains(@class, "text-message") and (contains(@data-message-author-role, "assistant") or contains(@data-message-author-role, "user"))]' 33 | 34 | 35 | @dataclass(frozen=True) 36 | class ChatMessage: 37 | """Base class for chat messages""" 38 | 39 | message_id: str 40 | text: str 41 | 42 | def __post_init__(self): 43 | assert_uuid(self.message_id) 44 | 45 | 46 | @dataclass(frozen=True) 47 | class AssistantMessage(ChatMessage): 48 | """Assistant chat message""" 49 | 50 | 51 | @dataclass(frozen=True) 52 | class UserMessage(ChatMessage): 53 | """User chat message""" 54 | 55 | 56 | class DriverNotInitializedError(Exception): 57 | """Exception for when a scraper operation is called, 58 | without initializing driver instance""" 59 | 60 | 61 | def random_sleep(_min: float, _max: float) -> None: 62 | """Perform a random sleep in the interval [_min, _max]""" 63 | sleep(round(uniform(_min, _max), 3)) 64 | 65 | 66 | def assert_uuid(text: str) -> bool: 67 | """Assert if `text` is a valid uuid4, 68 | raises ValueError in case it's not""" 69 | return str(UUID(text, version=4)) 70 | 71 | 72 | class GPTScraper: 73 | """GPT Scraper object""" 74 | 75 | driver: webdriver.Firefox = None 76 | messages: Dict[str, Union[UserMessage, AssistantMessage]] = None 77 | 78 | def __init__( 79 | self, 80 | profile_path: str = None, 81 | headless: bool = False, 82 | ) -> None: 83 | self.headless: bool = headless 84 | self.firefox_profile_path = profile_path 85 | if self.firefox_profile_path is None: 86 | self.firefox_profile_path = get_default_firefox_profile() 87 | 88 | self.gecko_options = self.__get_gecko_options() 89 | self.selwire_opts = self.__get_selwire_options() 90 | self.driver = None 91 | self.messages = {} 92 | 93 | def __del__(self): 94 | # Dispose webdriver on instance deletion 95 | self.quit() 96 | 97 | def __get_gecko_options(self) -> webdriver.FirefoxOptions: 98 | opts: webdriver.FirefoxOptions = get_firefox_options( 99 | options=webdriver.FirefoxOptions(), 100 | firefox_profile=self.firefox_profile_path, 101 | headless=self.headless, 102 | ) 103 | # Allow selenium wire proxy certificate to work 104 | opts.set_preference("security.cert_pinning.enforcement_level", 0) 105 | opts.set_preference("network.stricttransportsecurity.preloadlist", False) 106 | 107 | opts.set_preference("security.mixed_content.block_active_content", False) 108 | opts.set_preference("security.mixed_content.block_display_content", True) 109 | return opts 110 | 111 | def __get_selwire_options(self) -> dict: 112 | return { 113 | "backend": "mitmproxy", 114 | "ignore_http_methods": ["GET", "PATCH", "HEAD", "OPTIONS"], 115 | } 116 | 117 | @staticmethod 118 | def assert_driver(func): 119 | """Scraping operation decorator for asserting a valid driver instance""" 120 | 121 | def __wrapper(self: "GPTScraper", *args, **kwargs): 122 | if self.driver is None or not self.driver.current_url.startswith(BASE_URL): 123 | raise DriverNotInitializedError( 124 | "Initialize a web driver instance with .start() before performing actions" 125 | ) 126 | 127 | return func(self, *args, **kwargs) 128 | 129 | return __wrapper 130 | 131 | @assert_driver 132 | def disable_styles(self) -> None: 133 | """Make all page styles invisible""" 134 | self.driver.execute_script( 135 | """\ 136 | var stylesheets = document.querySelectorAll('link[rel="stylesheet"]');\ 137 | stylesheets.forEach(function(stylesheet) {\ 138 | stylesheet.setAttribute('media', 'none');\ 139 | });""" 140 | ) 141 | 142 | @assert_driver 143 | def edit_zoom(self, ratio: int) -> None: 144 | """Changes zoom factor""" 145 | 146 | if ratio <= 30 or ratio >= 100: 147 | raise ValueError(f"Not a valid zoom level: {ratio}") 148 | 149 | self.driver.execute_script(f'document.body.style.zoom = "{ratio}%"') 150 | 151 | def start(self, chat_id: str = None) -> None: 152 | """Starts a new GPT scraping session, creting a new driver instance. 153 | Will create a new chat if the `chat_id` parameter is None (default). 154 | Otherwise it will visit/operate on that specific chat. 155 | """ 156 | if self.driver is not None: 157 | self.driver.quit() 158 | self.driver = None 159 | 160 | self.driver = webdriver.Firefox( 161 | seleniumwire_options=self.selwire_opts, options=self.gecko_options 162 | ) 163 | 164 | url = BASE_URL 165 | if chat_id is not None: 166 | chat_id = assert_uuid(chat_id) 167 | # Move to the chat id URL 168 | url = f"{BASE_URL}/c/{chat_id}" 169 | 170 | self.driver.get(url) 171 | self.driver.implicitly_wait(10) 172 | 173 | # self.disable_styles() 174 | self.edit_zoom(40) # TODO Configure these 175 | 176 | def quit(self) -> None: 177 | """Calls webdriver.quit() and dispose this object""" 178 | if self.driver is not None: 179 | self.driver.quit() 180 | 181 | # Resets webdriver instance 182 | self.driver = None 183 | 184 | # Clear message cache 185 | self.messages.clear() 186 | 187 | cleanup_resources() 188 | 189 | def __hover_click(self, element: WebElement) -> None: 190 | """Hover and click on a specific `WebElement`""" 191 | a = ActionChains(self.driver).move_to_element(element).click_and_hold(element) 192 | a.perform() 193 | 194 | random_sleep(0.1, 1) # Perform random hold time 195 | a.release().perform() 196 | 197 | @assert_driver 198 | def current_chat_id(self) -> str | None: 199 | """Retrieves the current chat uuid from url bar""" 200 | 201 | if not self.driver.current_url.startswith(f"{BASE_URL}/c/"): 202 | return None 203 | u = self.driver.current_url.split(f"{BASE_URL}/c/", maxsplit=1)[1] 204 | if not u: 205 | return None 206 | u = u.split("/", maxsplit=1)[0] 207 | return u if u else None 208 | 209 | @assert_driver 210 | def send_message(self, text: str) -> None: 211 | """Sends a new text message to current chat""" 212 | 213 | textarea = wait_element_by( 214 | self.driver, By.CSS_SELECTOR, TEXTAREA_CSSS, timeout=10 215 | ) 216 | 217 | self.driver.execute_script( 218 | "arguments[0].textContent=arguments[1]", textarea, text 219 | ) 220 | 221 | random_sleep(1, 2) 222 | 223 | actions = ActionChains(self.driver).move_to_element(textarea) 224 | actions.key_down(Keys.CONTROL).key_down(Keys.ENTER).key_up(Keys.ENTER).key_down( 225 | Keys.CONTROL 226 | ).perform() 227 | 228 | @assert_driver 229 | def get_messages(self) -> Iterator[Union[UserMessage, AssistantMessage]]: 230 | """Iterate over messages in the current chat, 231 | each element can either be a UserMessage or an AssistantMessage object""" 232 | 233 | elements = wait_elements_by( 234 | self.driver, By.XPATH, CHAT_MESSAGE_XPATH, timeout=10 235 | ) 236 | if not elements: 237 | return 238 | 239 | for div in elements: 240 | role = div.get_attribute("data-message-author-role") 241 | msg_id = div.get_attribute("data-message-id") 242 | try: 243 | assert_uuid(msg_id) 244 | except ValueError: 245 | continue 246 | 247 | # Caching strategy 248 | if msg_id in self.messages: 249 | yield self.messages[msg_id] 250 | 251 | if role == "user": 252 | self.messages[msg_id] = div.text 253 | yield UserMessage(msg_id, div.text) 254 | elif role == "assistant": 255 | self.messages[msg_id] = div.text 256 | yield AssistantMessage(msg_id, div.text) 257 | 258 | @assert_driver 259 | def wait_completion(self, timeout: float = 0) -> None: 260 | """Waits for the completion to be actually complete, 261 | based off the visibility of action buttons""" 262 | 263 | start = perf_counter() 264 | 265 | while timeout == 0 or perf_counter() - start <= timeout: 266 | for request in self.driver.requests: 267 | if ( 268 | request.method == "POST" 269 | and request.url == "https://chatgpt.com/backend-api/lat/r" 270 | and request.response 271 | and request.response.body 272 | ): 273 | res = loads(request.response.body.decode("utf-8")) 274 | if res and "status" in res: 275 | # Add a sleep time, ensuring 276 | # a complete answer is retrieved 277 | random_sleep(0.5, 1) 278 | return 279 | if timeout != 0: 280 | raise TimeoutError( 281 | f"{timeout} seconds elapsed waiting for completion to finish" 282 | ) 283 | 284 | @assert_driver 285 | def toggle_history(self) -> None: 286 | """Toggle chat history by opening settings page""" 287 | profile_button = wait_element_by( 288 | self.driver, By.CSS_SELECTOR, PROFILE_BUTTON_CSSS 289 | ) 290 | self.__hover_click(profile_button) 291 | 292 | settings_menu = wait_element_by( 293 | self.driver, By.CSS_SELECTOR, SETTINGS_MENU_CSSS 294 | ) 295 | 296 | buttons = wait_elements_by(settings_menu, By.TAG_NAME, "a") 297 | if len(buttons) != 3: 298 | return 299 | btn = buttons[1] 300 | self.__hover_click(btn) 301 | 302 | random_sleep(0.1, 1) # TODO Customize this 303 | 304 | ActionChains(self.driver).key_down(Keys.TAB).key_down(Keys.ARROW_DOWN).key_up( 305 | Keys.ARROW_DOWN 306 | ).key_up(Keys.TAB).perform() 307 | 308 | random_sleep(0.1, 1) # TODO Customize this 309 | 310 | ActionChains(self.driver).key_down(Keys.TAB).key_up(Keys.TAB).key_down( 311 | Keys.TAB 312 | ).key_up(Keys.TAB).key_down(Keys.ENTER).key_up(Keys.ENTER).perform() 313 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """setup.py""" 2 | 3 | from os.path import dirname, join, abspath 4 | from setuptools import setup, find_packages 5 | 6 | __DESCRIPTION = """Fully functional ChatGPT scraper""" 7 | 8 | with open( 9 | join(abspath(dirname(__file__)), "README.md"), 10 | "r", 11 | encoding="utf-8", 12 | errors="ignore", 13 | ) as fp: 14 | __LONG_DESCRIPTION = fp.read().lstrip().rstrip() 15 | 16 | setup( 17 | name="gptauto", 18 | version="0.1.1", 19 | author="st1vms", 20 | author_email="stefano.maria.salvatore@gmail.com", 21 | description=__DESCRIPTION, 22 | long_description=__LONG_DESCRIPTION, 23 | long_description_content_type="text/markdown", 24 | url="https://github.com/st1vms/gptauto", 25 | packages=find_packages(), 26 | classifiers=[ 27 | "Programming Language :: Python :: 3.10", 28 | "License :: OSI Approved :: MIT License", 29 | "Operating System :: OS Independent", 30 | ], 31 | entry_points={'console_scripts':['ask-gpt=gptauto.cli.ask_gpt:main']}, 32 | python_requires=">=3.9", 33 | install_requires=[ 34 | "selgym", 35 | "selenium-wire", 36 | ], 37 | ) 38 | --------------------------------------------------------------------------------