├── sengpt ├── __init__.py ├── __main__.py ├── re_gpt │ ├── __init__.py │ ├── errors.py │ ├── utils.py │ ├── sync_chatgpt.py │ └── async_chatgpt.py ├── utils.py ├── config.py ├── argparser.py └── main.py ├── .github ├── FUNDING.yml └── assets │ └── demo.gif ├── .gitignore ├── pyproject.toml ├── requirements.txt ├── README.md ├── poetry.lock └── LICENSE /sengpt/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: SenZmaKi 4 | -------------------------------------------------------------------------------- /.github/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenZmaKi/Sengpt/HEAD/.github/assets/demo.gif -------------------------------------------------------------------------------- /sengpt/__main__.py: -------------------------------------------------------------------------------- 1 | from sengpt.main import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /sengpt/re_gpt/__init__.py: -------------------------------------------------------------------------------- 1 | from .async_chatgpt import AsyncChatGPT 2 | from .sync_chatgpt import SyncChatGPT 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | .git/ 3 | __pycache__/ 4 | .scraps/ 5 | .ruff_cache/ 6 | .pytest_cache/ 7 | funcaptcha_bin/ 8 | dist/ 9 | 10 | .env 11 | crap.py 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | keywords = [ 3 | "chatgpt", 4 | "gpt", 5 | "cli", 6 | "command line", 7 | "terminal", 8 | "free gpt", 9 | "free", 10 | "no api key", 11 | "no api", 12 | "openai", 13 | "openai chatgpt", 14 | "chatgpt free", 15 | ] 16 | name = "sengpt" 17 | version = "0.1.2" 18 | description = "ChatGPT in your terminal, no OpenAI API key required" 19 | authors = ["SenZmaKi "] 20 | license = "GPL-3.0-only" 21 | homepage = "https://github.com/SenZmaKi/Sengpt" 22 | readme = "README.md" 23 | repository = "https://github.com/SenZmaKi/Sengpt" 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.11" 27 | curl-cffi = "^0.5.10" 28 | appdirs = "^1.4.4" 29 | pyperclip = "^1.8.2" 30 | 31 | [tool.poetry.urls] 32 | "Bug Tracker" = "https://github.com/SenZmaKi/Sengpt/issues" 33 | 34 | [tool.poetry.scripts] 35 | sengpt = "sengpt.main:main" 36 | 37 | [build-system] 38 | requires = ["poetry-core"] 39 | build-backend = "poetry.core.masonry.api" 40 | -------------------------------------------------------------------------------- /sengpt/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import NoReturn 3 | import os 4 | import sys 5 | from appdirs import user_config_dir 6 | 7 | 8 | GLOW_INSTALLATION_URL = ( 9 | "https://github.com/charmbracelet/glow" 10 | ) 11 | APP_NAME = "Sengpt" 12 | APP_NAME_LOWER = "sengpt" 13 | REPO_URL = "https://github.com/SenZmaKi/Sengpt" 14 | DESCRIPTION = "ChatGPT in your terminal, no OpenAI API key required" 15 | REPO_TAGS_URL = "https://api.github.com/repos/SenZmaKi/Sengpt/tags" 16 | # FIXME: Update to False on push 17 | DEBUG = False 18 | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 19 | VERSION = "0.1.2" 20 | V_VERSION = f"v{VERSION}" 21 | 22 | 23 | class OsUtils: 24 | is_windows = sys.platform == "win32" 25 | is_linux = sys.platform == "linux" 26 | is_mac = sys.platform == "mac" 27 | config_dir = ROOT_DIR if DEBUG else user_config_dir(APP_NAME) 28 | if is_windows: 29 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 30 | 31 | 32 | def print_and_exit(text: str) -> NoReturn: 33 | print(text) 34 | sys.exit() 35 | 36 | 37 | def check_repo_print(text: str, exit=True) -> None | NoReturn: 38 | check_repo_text = f"{text}\nCheck {REPO_URL} for help" 39 | if exit: 40 | print_and_exit(check_repo_text) 41 | print(check_repo_text) 42 | 43 | 44 | def mkdir(path: str) -> None: 45 | # Incase some weirdo has a file with the same name as the folder 46 | if os.path.isfile(path): 47 | os.unlink(path) 48 | if not os.path.isdir(path): 49 | os.mkdir(path) 50 | 51 | -------------------------------------------------------------------------------- /sengpt/re_gpt/errors.py: -------------------------------------------------------------------------------- 1 | class TokenNotProvided(Exception): 2 | def __init__(self): 3 | self.message = "Token not provided. Please pass your '__Secure-next-auth.session-token' as an argument (e.g., ChatGPT.init(session_token=YOUR_TOKEN))." 4 | super().__init__(self.message) 5 | 6 | 7 | class InvalidSessionToken(Exception): 8 | def __init__(self): 9 | self.message = "Invalid session token provided." 10 | super().__init__(self.message) 11 | 12 | 13 | class RetryError(Exception): 14 | def __init__(self, website, message="Exceeded maximum retries"): 15 | self.website = website 16 | self.message = f"{message} for website: {website}" 17 | super().__init__(self.message) 18 | 19 | 20 | class BackendError(Exception): 21 | def __init__(self, error_code): 22 | self.error_code = error_code 23 | self.message = ( 24 | f"An error occurred on the backend. Error code: {self.error_code}" 25 | ) 26 | super().__init__(self.message) 27 | 28 | 29 | class UnexpectedResponseError(Exception): 30 | def __init__(self, original_exception, server_response): 31 | self.original_exception = original_exception 32 | self.server_response = server_response 33 | self.message = f"An unexpected error occurred. Error message: {self.original_exception}.\nThis is what the server returned: {self.server_response}." 34 | super().__init__(self.message) 35 | 36 | 37 | class InvalidModelName(Exception): 38 | def __init__(self, model, avalible_models): 39 | self.model = model 40 | self.avalible_models = avalible_models 41 | self.message = f'"{model}" is not a valid model. Avalible models: {[model for model in avalible_models]}' 42 | super().__init__(self.message) 43 | -------------------------------------------------------------------------------- /sengpt/config.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | from sengpt.argparser import SYS_ARGS 5 | from .utils import APP_NAME, mkdir, OsUtils, check_repo_print 6 | from appdirs import user_config_dir 7 | import os 8 | from typing import Any, NoReturn, TypeVar 9 | import json 10 | 11 | T = TypeVar("T") 12 | 13 | 14 | class Config: 15 | @staticmethod 16 | def update_session_token(): 17 | if len(SYS_ARGS.non_args) != 1: 18 | check_repo_print( 19 | "Invalid usage!!!\nUsage: --session_token " 20 | ) 21 | session_token = SYS_ARGS.non_args[0] 22 | Config.session_token = session_token 23 | Config.update_json("session_token", session_token) 24 | 25 | @staticmethod 26 | def setup_config_file_path() -> str: 27 | config_dir = user_config_dir(APP_NAME) 28 | if OsUtils.is_windows: 29 | # On windows it resolves the config directory to Sengpt/Sengpt 30 | config_dir = os.path.dirname(config_dir) 31 | mkdir(config_dir) 32 | return os.path.join(config_dir, "config.json") 33 | 34 | @staticmethod 35 | def info() -> str: 36 | with open(Config.file_path, "r") as f: 37 | return f"{f.read()}\n\nConfig file location: {Config.file_path}" 38 | 39 | @staticmethod 40 | def update_json(key: str, value: str | None | bool) -> None: 41 | Config.json[key] = value 42 | Config.dump() 43 | 44 | @staticmethod 45 | async def update_json_async(key: str, value: str | None | bool) -> None: 46 | Config.update_json(key, value) 47 | 48 | @staticmethod 49 | def dump() -> None: 50 | with open(Config.file_path, "w") as f: 51 | json.dump(Config.json, f, indent=4) 52 | 53 | @staticmethod 54 | def get_from_json_config( 55 | key: str, default: T, json_config: dict[str, Any] 56 | ) -> T | NoReturn: 57 | value = json_config.get(key, default) 58 | val_type = type(value) 59 | def_type = type(default) 60 | if val_type == def_type: 61 | return value 62 | print( 63 | f'Error loading config: Expected value at "{key}" to be of type {def_type} but instead got "{value}" which is of type {val_type}' 64 | ) 65 | sys.exit() 66 | 67 | @staticmethod 68 | def load_json_config( 69 | config_file_path: str, 70 | ) -> NoReturn | dict[str, Any]: 71 | try: 72 | with open(config_file_path) as f: 73 | contents = f.read() 74 | try: 75 | return json.loads(contents) 76 | except json.JSONDecodeError: 77 | last_2_chars = contents.replace(" ", "").replace("\n", "")[-2:] 78 | info_to_add = ( 79 | '\nRemove the "," before the last "}"' 80 | if ",}" == last_2_chars 81 | else "" 82 | ) 83 | check_repo_print( 84 | f'Your config file at: "{config_file_path}" is invalid!!!{info_to_add}' 85 | ) 86 | except FileNotFoundError: 87 | pass 88 | return {} # This is here instead of in the except block to avoid type errors 89 | 90 | file_path = setup_config_file_path() 91 | 92 | json = load_json_config(file_path) 93 | username = get_from_json_config("username", "You", json) 94 | session_token = get_from_json_config("session_token", "", json) 95 | model = get_from_json_config("model", "gpt-3.5", json) 96 | recent_conversation_id = get_from_json_config("recent_conversation_id", "", json) 97 | preconfigured_prompts = get_from_json_config("preconfigured_prompts", {}, json) 98 | no_glow = get_from_json_config("no_glow", False, json) 99 | save = get_from_json_config("save", False, json) 100 | delete = get_from_json_config("delete", False, json) 101 | copy = get_from_json_config("copy", False, json) 102 | default_mode = get_from_json_config("default_mode", "interactive", json) 103 | -------------------------------------------------------------------------------- /sengpt/argparser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .utils import APP_NAME_LOWER, DESCRIPTION, VERSION 4 | 5 | 6 | class ArgParser: 7 | def __init__(self, args: list[str]) -> None: 8 | self.args, self.non_args = ArgParser.parse_args(args) 9 | 10 | @staticmethod 11 | def parse_args(args: list[str]) -> tuple[list[str], list[str]]: 12 | non_args: list[str] = [] 13 | extracted_args: list[str] = [] 14 | for a in args: 15 | if a.startswith("-"): 16 | extracted_args.append(a) 17 | continue 18 | non_args.append(a) 19 | return extracted_args, non_args 20 | 21 | @staticmethod 22 | def version_info() -> str: 23 | return f"{APP_NAME_LOWER} {VERSION}" 24 | 25 | @staticmethod 26 | def help_info() -> str: 27 | return f""" 28 | {DESCRIPTION} 29 | 30 | Usage: sengpt [prompt] [options] 31 | 32 | -h, --help Show help message and exit 33 | -v, --version Show the version information 34 | -cf, --config_file Show the config file's contents and location 35 | -st, --session_token Set session token 36 | 37 | -ng, --no_glow Disable pretty printing with Glow, 38 | this can be set to be the default behaviour in the config file 39 | 40 | -c, --copy Copy the prompt response to the clipboard, 41 | this can be set to be the default behaviour in the config file 42 | 43 | -p, --paste Append the most recently copied clipboard text to the sent prompt 44 | -rc, --recent_conversation Use the most recently saved conversation as context 45 | -pp, --preconfigured_prompt Append a preconfigured prompt to the sent prompt, 46 | replace "preconfigured_prompt" with the prompt's name 47 | as it appears in the config file 48 | 49 | -q, query Use query mode i.e., print ChatGPT's response and exit, 50 | this flag is only necessary if "default_mode" in config file is interactive 51 | 52 | -s, --save By default conversations in query mode are deleted on exit, 53 | this saves the conversation instead, 54 | this can be set to be the default behaviour in the config file 55 | 56 | -i, --interactive Use interactive mode i.e., back and forth interaction with ChatGPT, 57 | this flag is only necessary if "default_mode" in the config file is query 58 | 59 | -d, --delete By default conversations in interactive mode are saved on exit, 60 | this deletes then exits the interactive mode session, 61 | this can be set to be the default behaviour in the config file 62 | """ 63 | 64 | @staticmethod 65 | def short_and_long(name: str) -> tuple[str, str]: 66 | long = f"--{name}" 67 | chars: list[str] = [] 68 | for prev_char, char in zip(name, name[1:]): 69 | if prev_char == "_": 70 | chars.append(char) 71 | short = f'-{name[0]}{"".join(chars)}' 72 | return short, long 73 | 74 | @staticmethod 75 | def abstract_is_set(flag_name: str, args: list[str]) -> bool: 76 | short, long = ArgParser.short_and_long(flag_name) 77 | return short in args or long in args 78 | 79 | def is_set(self, flag_name: str) -> bool: 80 | return ArgParser.abstract_is_set(flag_name, self.args) 81 | 82 | 83 | SYS_ARGS = ArgParser(sys.argv[1:]) 84 | 85 | -------------------------------------------------------------------------------- /sengpt/re_gpt/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import platform 4 | 5 | current_os = platform.system() 6 | current_file_directory = "/".join( 7 | __file__.split("\\" if current_os == "Windows" else "/")[0:-1] 8 | ) 9 | 10 | funcaptcha_bin_folder_path = f"{current_file_directory}/funcaptcha_bin" 11 | latest_release_url = ( 12 | "https://api.github.com/repos/Zai-Kun/reverse-engineered-chatgpt/releases" 13 | ) 14 | 15 | binary_file_name = {"Windows": "windows_arkose.dll", "Linux": "linux_arkose.so"}.get( 16 | current_os 17 | ) 18 | 19 | binary_path = { 20 | "Windows": f"{funcaptcha_bin_folder_path}/{binary_file_name}", 21 | "Linux": f"{funcaptcha_bin_folder_path}/{binary_file_name}", 22 | }.get(current_os) 23 | 24 | 25 | def calculate_file_md5(file_path): 26 | with open(file_path, "rb") as file: 27 | file_content = file.read() 28 | md5_hash = hashlib.md5(file_content).hexdigest() 29 | return md5_hash 30 | 31 | 32 | def get_file_url(json_data): 33 | for release in json_data: 34 | if release["tag_name"].startswith("funcaptcha_bin"): 35 | file_url = next( 36 | asset["browser_download_url"] 37 | for asset in release["assets"] 38 | if asset["name"] == binary_file_name 39 | ) 40 | return file_url 41 | 42 | 43 | # async 44 | async def async_download_binary(session, output_path, file_url): 45 | with open(output_path, "wb") as output_file: 46 | response = await session.get( 47 | url=file_url, content_callback=lambda chunk: output_file.write(chunk) 48 | ) 49 | 50 | 51 | async def async_get_binary_path(session): 52 | if binary_path is None: 53 | return None 54 | 55 | if not os.path.exists(funcaptcha_bin_folder_path) or not os.path.isdir( 56 | funcaptcha_bin_folder_path 57 | ): 58 | os.mkdir(funcaptcha_bin_folder_path) 59 | 60 | if os.path.isfile(binary_path): 61 | try: 62 | local_binary_hash = calculate_file_md5(binary_path) 63 | response = await session.get(latest_release_url) 64 | json_data = response.json() 65 | 66 | for line in json_data["body"].splitlines(): 67 | if line.startswith(current_os): 68 | latest_binary_hash = line.split("=")[-1] 69 | break 70 | 71 | if local_binary_hash != latest_binary_hash: 72 | file_url = get_file_url(json_data) 73 | 74 | await async_download_binary(session, binary_path, file_url) 75 | except: 76 | return binary_path 77 | else: 78 | response = await session.get(latest_release_url) 79 | json_data = response.json() 80 | file_url = get_file_url(json_data) 81 | 82 | await async_download_binary(session, binary_path, file_url) 83 | 84 | return binary_path 85 | 86 | 87 | # sync 88 | def sync_download_binary(session, output_path, file_url): 89 | with open(output_path, "wb") as output_file: 90 | response = session.get( 91 | url=file_url, content_callback=lambda chunk: output_file.write(chunk) 92 | ) 93 | 94 | 95 | def sync_get_binary_path(session): 96 | if binary_path is None: 97 | return None 98 | 99 | if not os.path.exists(funcaptcha_bin_folder_path) or not os.path.isdir( 100 | funcaptcha_bin_folder_path 101 | ): 102 | os.mkdir(funcaptcha_bin_folder_path) 103 | 104 | if os.path.isfile(binary_path): 105 | try: 106 | local_binary_hash = calculate_file_md5(binary_path) 107 | response = session.get(latest_release_url) 108 | json_data = response.json() 109 | 110 | for line in json_data["body"].splitlines(): 111 | if line.startswith(current_os): 112 | latest_binary_hash = line.split("=")[-1] 113 | break 114 | 115 | if local_binary_hash != latest_binary_hash: 116 | file_url = get_file_url(json_data) 117 | 118 | sync_download_binary(session, binary_path, file_url) 119 | except: 120 | return binary_path 121 | else: 122 | response = session.get(latest_release_url) 123 | json_data = response.json() 124 | file_url = get_file_url(json_data) 125 | 126 | sync_download_binary(session, binary_path, file_url) 127 | 128 | return binary_path 129 | 130 | 131 | def get_model_slug(chat): 132 | for _, message in chat.get("mapping", {}).items(): 133 | if "message" in message: 134 | if message["message"]: 135 | role = message["message"]["author"]["role"] 136 | if role == "assistant": 137 | return message["message"]["metadata"]["model_slug"] 138 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 ; python_version >= "3.11" and python_version < "4.0" \ 2 | --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ 3 | --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 4 | cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0" \ 5 | --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ 6 | --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ 7 | --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ 8 | --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ 9 | --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ 10 | --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ 11 | --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ 12 | --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ 13 | --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ 14 | --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ 15 | --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ 16 | --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ 17 | --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ 18 | --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ 19 | --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ 20 | --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ 21 | --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ 22 | --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ 23 | --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ 24 | --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ 25 | --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ 26 | --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ 27 | --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ 28 | --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ 29 | --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ 30 | --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ 31 | --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ 32 | --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ 33 | --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ 34 | --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ 35 | --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ 36 | --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ 37 | --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ 38 | --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ 39 | --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ 40 | --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ 41 | --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ 42 | --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ 43 | --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ 44 | --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ 45 | --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ 46 | --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ 47 | --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ 48 | --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ 49 | --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ 50 | --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ 51 | --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ 52 | --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ 53 | --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ 54 | --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ 55 | --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ 56 | --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 57 | curl-cffi==0.5.10 ; python_version >= "3.11" and python_version < "4.0" \ 58 | --hash=sha256:1b2bc8822d23415f6533c8b750475e9bbc76764025fe1dcb5866dc033607fd7b \ 59 | --hash=sha256:55bac4b73e2d80ceeaabea33270fc8ca6ace594128a46710242f2e688b4f8bfc \ 60 | --hash=sha256:892603dab5e56fb72bfff7ae969136138971f63f63defe98232e1ec55cb0f1c6 \ 61 | --hash=sha256:9937b8e13b1a6963c63e155b6621ec74649965105efedb919bc226fe731861cc \ 62 | --hash=sha256:b537595b9610a4dd0927c09823925b4e32b1ce0fd04385bfc5bb72ab830720e6 \ 63 | --hash=sha256:f9a1874b860c4e8db49bdfd9b9d4dc39999a1397d271ec78624c35c838e9e92a 64 | pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0" \ 65 | --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ 66 | --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 67 | pyperclip==1.8.2 ; python_version >= "3.11" and python_version < "4.0" \ 68 | --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !!! NOTICE !!! 2 | Dead project since [re_gpt](https://github.com/Zai-Kun/reverse-engineered-chatgpt) is not maintained anymore 3 | 4 |

5 | Sengpt 6 |

7 |

8 | ChatGPT in your terminal, runs on 9 | re_gpt so no OpenAI API key required 10 |

11 |

12 |

13 | Installation • 14 | Configuration • 15 | Usage • 16 | Building from source • 17 | Support 18 |

19 |
20 | demo 21 |
22 | 23 | ## Installation 24 | 25 | Ensure you have [Python 3.11](https://www.python.org/downloads/release/python-3111) and [Glow](https://github.com/charmbracelet/glow) installed. 26 | 27 | ```bash 28 | pip install sengpt 29 | ``` 30 | 31 | ## Configuration 32 | 33 | ### Session token 34 | 35 | - Go to https://chat.openai.com and log in or sign up. 36 | - Open the browser developer tools (right click and click "inspect" or "inspect element"). 37 | - Go to the Application tab (try expanding the dev tools window if you can't find it) and expand the Cookies section. 38 | - Look for https://chat.openai.com. 39 | - Copy the value for \_\_Secure-next-auth.session-token. 40 | - Run `sengpt --session_token `. 41 | 42 | ### Preconfigured prompts 43 | 44 | - Open the config file, run `sengpt --config_file` to see its location. 45 | - Add a field named `preconfigured_prompts` and set its value to key value pairs of prompt name and value e.g., 46 | 47 | ````json 48 | { 49 | "preconfigured_prompts": { 50 | "readme": "generate a README.md for this project", 51 | "expain": "briefly explain what this code does", 52 | "refactor": "refactor this code to improve readability", 53 | "generate_command": "No explanation. Output as literal string plain text i.e., echo Hello World instead of ```bash\\necho Hello World\\n```. Command as LITERAL string NO Markdown formatting. I want to be able to take your raw output and paste it into my terminal and it just works without me tinkering with it. Generate a command that " 54 | } 55 | } 56 | ```` 57 | 58 | - To pass the prompt run `sengpt --prompt_name` or `sengpt -pn`. 59 | - Warning!!! Make sure the short version of the prompt name doesn't clash with each other or with any of sengpt's default flags, i.e., a prompt name like `script_tags` will clash with `session_token` so every time you try and use it sengpt will think you want to set your session token. 60 | - The preconfigured prompts are appended to the final prompt i.e., `some_project.py | sengpt --readme make it as brief as possible` 61 | 62 | ### Modes 63 | 64 | #### Interactive mode 65 | 66 | Back and forth interaction with ChatGPT, saves the conversation on exit. 67 | 68 | Currently doesn't support piped inputs i.e., `cat README.md | sengpt summarise this document`, if piped inputs are passed Query mode will be used instead. 69 | 70 | Press `Ctrl + C` to exit. 71 | 72 | #### Query mode 73 | 74 | Print ChatGPT's response, delete the conversation and exit. 75 | 76 | #### Default mode 77 | 78 | The default mode is interactive mode but you can change this in the config 79 | 80 | ```json 81 | { 82 | "default_mode": "query" 83 | } 84 | ``` 85 | 86 | With this configuration to use interactive mode run `sengpt --interactive` 87 | 88 | ### Models 89 | 90 | Either `gpt-3.5` or `gpt-4` can be used, the default is `gpt-3.5`. `gpt-4` requires a ChatGPT Plus account and is slower. To switch to `gpt-4` add this in your config file. 91 | 92 | ```json 93 | { 94 | "model": "gpt-4" 95 | } 96 | ``` 97 | 98 | ### Username 99 | 100 | How your username appears in the conversation, the default is `You`. 101 | 102 | ```json 103 | { 104 | "username": "Sen" 105 | } 106 | ``` 107 | 108 | ## Usage 109 | 110 | ``` 111 | 112 | Usage: sengpt [prompt] [options] 113 | 114 | -h, --help Show help message and exit 115 | -v, --version Show the version information 116 | -cf, --config_file Show the config file's contents and location 117 | -st, --session_token Set session token 118 | 119 | -ng, --no_glow Disable pretty printing with Glow, 120 | this can be set to be the default behaviour in the config file 121 | 122 | -c, --copy Copy the prompt response to the clipboard, 123 | this can be set to be the default behaviour in the config file 124 | 125 | -p, --paste Append the most recently copied clipboard text to the sent prompt 126 | -rc, --recent_conversation Use the most recently saved conversation as context 127 | -pp, --preconfigured_prompt Append a preconfigured prompt to the sent prompt, 128 | replace "preconfigured_prompt" with the prompt's name 129 | as it appears in the config file 130 | 131 | -q, query Use query mode i.e., print ChatGPT's response and exit, 132 | this flag is only necessary if "default_mode" in config file is interactive 133 | 134 | -s, --save By default conversations in query mode are deleted on exit, 135 | this saves the conversation instead, 136 | this can be set to be the default behaviour in the config file 137 | 138 | -i, --interactive Use interactive mode i.e., back and forth interaction with ChatGPT, 139 | this flag is only necessary if "default_mode" in the config file is query 140 | 141 | -d, --delete By default conversations in interactive mode are saved on exit, 142 | this deletes then exits the interactive mode session, 143 | this can be set to be the default behaviour in the config file 144 | 145 | ``` 146 | 147 | ## Building from Source 148 | 149 | Ensure you have [Python 3.11](https://www.python.org/downloads/release/python-3111) and [Git](https://github.com/git-guides/install-git) installed. 150 | 151 | 1. **Set everything up.** 152 | 153 | ``` 154 | git clone https://github.com/SenZmaKi/Sengpt && cd Sengpt && pip install poetry && poetry install 155 | ``` 156 | 157 | 2. **Run Sengpt.** 158 | 159 | ``` 160 | poetry run sengpt 161 | ``` 162 | 163 | 3. **Build the package to install with pip**. 164 | 165 | ``` 166 | poetry build 167 | ``` 168 | 169 | - The `tar` and `wheel` will be built at `Sengpt/dist` 170 | 171 | ## Support 172 | 173 | - You can support the development of Sengpt through donations on [GitHub Sponsors](https://github.com/sponsors/SenZmaKi) 174 | - You can also star the github repository for other terminal enthusiasts and freedom fighters to know about it 175 | -------------------------------------------------------------------------------- /sengpt/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import NoReturn, cast 3 | from curl_cffi.requests.errors import RequestsError 4 | from curl_cffi.requests.session import AsyncSession 5 | from .re_gpt import AsyncChatGPT 6 | import sys 7 | import subprocess 8 | 9 | from .re_gpt.async_chatgpt import AsyncConversation 10 | from .re_gpt.errors import UnexpectedResponseError 11 | from .re_gpt.sync_chatgpt import InvalidSessionToken 12 | from .utils import ( 13 | REPO_TAGS_URL, 14 | GLOW_INSTALLATION_URL, 15 | V_VERSION, 16 | OsUtils, 17 | check_repo_print, 18 | print_and_exit, 19 | ) 20 | from .config import Config 21 | from .argparser import ArgParser, SYS_ARGS 22 | import pyperclip 23 | import os 24 | 25 | PRINT_WITH_GLOW = not (SYS_ARGS.is_set("no_glow") or Config.no_glow) 26 | # isatty == is a teletypewriter == is a terminal == program invoked without piping input 27 | INPUT_WAS_PIPED = not sys.stdin.isatty() 28 | IS_QUERY_MODE = ( 29 | SYS_ARGS.is_set("query") 30 | # Couldn't find a way to get piped inputs to work together with normal user inputs 31 | # cause stdin gets set to the piped input and but I couldn't find a way to set it to the terminal stdin 32 | or INPUT_WAS_PIPED 33 | or (Config.default_mode == "query" and not SYS_ARGS.is_set("interactive")) 34 | ) 35 | 36 | 37 | def input_handler(msg: str) -> str: 38 | try: 39 | user_input = input(msg) 40 | except EOFError: # When they press Ctrl + C while input is "waiting" 41 | print() # Move out of the msg line onto the next line 42 | user_input = "" 43 | return user_input 44 | 45 | 46 | def till_one_works(commands: tuple[str, ...]) -> bool: 47 | for c in commands: 48 | try: 49 | subprocess.run(c) 50 | return True 51 | except Exception: # I can't be asked to catch all the specific errors that can arise from these commands 52 | continue 53 | return False 54 | 55 | 56 | def try_installing_glow() -> bool: 57 | if OsUtils.is_windows: 58 | return till_one_works( 59 | ( 60 | "winget install charmbracelet.glow", 61 | "choco install glow", 62 | "scoop install glow", 63 | ) 64 | ) 65 | elif OsUtils.is_linux: 66 | return till_one_works( 67 | ( 68 | 'sudo mkdir -p /etc/apt/keyrings && curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg && echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list && sudo apt update && sudo apt install glow', 69 | "pacman -S glow", 70 | "xbps-install -S glow", 71 | "nix-env -iA nixpkgs.glow", 72 | "pkg install glow", 73 | "eopkg install glow", 74 | ) 75 | ) 76 | return till_one_works(("brew install glow", "sudo port install glow")) 77 | 78 | 79 | def get_piped_input() -> str: 80 | if not INPUT_WAS_PIPED: 81 | return "" 82 | full_input = "" 83 | try: 84 | while True: 85 | full_input = f"{full_input}\n{input()}" 86 | except EOFError: 87 | pass 88 | return full_input 89 | 90 | 91 | def generate_prompt(args: ArgParser) -> str: 92 | args_prompt = " ".join(args.non_args) 93 | if args_prompt: 94 | args_prompt = f"{args_prompt}" 95 | preconfigured_prompt = " ".join( 96 | value for key, value in Config.preconfigured_prompts.items() if args.is_set(key) 97 | ) 98 | if preconfigured_prompt: 99 | preconfigured_prompt = f"{preconfigured_prompt}\n\n" 100 | clipboard_text = f"{pyperclip.paste()}" if args.is_set("paste") else "" 101 | if clipboard_text: 102 | clipboard_text = f"{clipboard_text}\n\n" 103 | passed_input = get_piped_input() if IS_QUERY_MODE else "" 104 | passed_input = f"{passed_input}\n\n" if passed_input else "" 105 | return f"{passed_input}{clipboard_text}{preconfigured_prompt}{args_prompt}" 106 | 107 | 108 | async def loading_animation(event: asyncio.Event) -> None: 109 | animation = (". ", ".. ", ".. .", " ") 110 | while not event.is_set(): 111 | for a in animation: 112 | print(f" Thinking {a}", end="\r") 113 | await asyncio.sleep(0.1) 114 | print(" " * (len("Thinking") + 8), end="\r") 115 | 116 | 117 | def printer(text: str) -> None: 118 | if PRINT_WITH_GLOW: 119 | return glow_print(text) 120 | print(text) 121 | 122 | 123 | def glow_print(text: str) -> None: 124 | try: 125 | subprocess.run("glow", input=text.encode()) 126 | except FileNotFoundError: 127 | print("Glow is required to pretty print output!!!") 128 | user_choice = input_handler("Would you like to install it? [y/n]\n> ").lower() 129 | if user_choice == "y" or user_choice == "yes": 130 | installation_was_succesful = try_installing_glow() 131 | if installation_was_succesful: 132 | return glow_print(text) 133 | print("Automatic installation failed X(") 134 | print( 135 | f'Check {GLOW_INSTALLATION_URL} for an installation guide\nAlternatively you can set "no_glow" to true in your config file or pass the --no_glow flag to the program' 136 | ) 137 | os._exit( 138 | 1 139 | ) # os._exit instead of sys.exit to avoid asyncio errors leaking cause running tasks don't respect sys.exit 140 | 141 | 142 | def load_conversation(gpt: AsyncChatGPT) -> tuple[AsyncConversation, bool]: 143 | conversation_id = ( 144 | Config.recent_conversation_id 145 | if SYS_ARGS.is_set("recent_conversation") 146 | else None 147 | ) 148 | if conversation_id == "": 149 | conversation_id = None 150 | 151 | conversation = gpt.create_new_conversation(model=Config.model) 152 | conversation.conversation_id = conversation_id 153 | save_conversation = ( 154 | conversation_id is not None or Config.save or SYS_ARGS.is_set("save") 155 | ) 156 | return conversation, save_conversation 157 | 158 | 159 | async def fetch_prompt_response(prompt: str, conversation: AsyncConversation) -> str: 160 | prompt_response = "" 161 | event = asyncio.Event() 162 | loading_task = asyncio.create_task(loading_animation(event)) 163 | async for resp_json in conversation.chat(prompt): 164 | content = resp_json["content"] 165 | if PRINT_WITH_GLOW: 166 | prompt_response += content 167 | continue 168 | if not event.is_set(): 169 | event.set() 170 | await loading_task 171 | print(content, end="", flush=True) 172 | if PRINT_WITH_GLOW: 173 | event.set() 174 | await loading_task 175 | return prompt_response 176 | 177 | 178 | async def interactive_mode( 179 | args: ArgParser, 180 | conversation: AsyncConversation, 181 | is_first_iteration=True, 182 | ) -> None: 183 | if is_first_iteration: 184 | prompt = prepare_prompt(args) 185 | else: 186 | prompt = generate_prompt(args) 187 | printer("\n# ChatGPT") 188 | prompt_response = await fetch_prompt_response(prompt, conversation) 189 | printer(f"{prompt_response}\n\n# {Config.username}") 190 | handle_coping_to_clip(args, prompt_response) 191 | if user_input := input_handler("> "): 192 | if user_input == "-d" or user_input == "--delete": 193 | await conversation.delete() 194 | return 195 | await interactive_mode(ArgParser(user_input.split(" ")), conversation) 196 | return 197 | if Config.delete: 198 | await conversation.delete() 199 | return 200 | await Config.update_json_async( 201 | "recent_conversation_id", conversation.conversation_id 202 | ) 203 | 204 | 205 | def handle_coping_to_clip(args: ArgParser, prompt_response: str) -> None: 206 | if args.is_set("copy") or Config.copy: 207 | pyperclip.copy(prompt_response) 208 | 209 | 210 | def prepare_prompt(args: ArgParser) -> str: 211 | prompt = generate_prompt(args) 212 | preprompt_text = f"# {Config.username}\n{prompt}\n\n# ChatGPT\n" 213 | printer(preprompt_text) 214 | return prompt 215 | 216 | 217 | async def query_mode( 218 | args: ArgParser, conversation: AsyncConversation, save_conversation: bool 219 | ) -> None: 220 | prompt = prepare_prompt(args) 221 | prompt_response = await fetch_prompt_response(prompt, conversation) 222 | if save_conversation: 223 | Config.recent_conversation_id = cast(str, conversation.conversation_id) 224 | task = asyncio.create_task( 225 | Config.update_json_async( 226 | "recent_conversation_id", conversation.conversation_id 227 | ) 228 | ) 229 | else: 230 | task = asyncio.create_task(conversation.delete()) 231 | printer(prompt_response) 232 | handle_coping_to_clip(args, prompt_response) 233 | await task 234 | 235 | 236 | async def gpt_coroutine(gpt: AsyncChatGPT) -> None: 237 | try: 238 | async with gpt: 239 | conversation, save_conversation = load_conversation(gpt) 240 | if IS_QUERY_MODE: 241 | await query_mode(SYS_ARGS, conversation, save_conversation) 242 | return 243 | await interactive_mode(SYS_ARGS, conversation) 244 | 245 | except (UnexpectedResponseError, InvalidSessionToken) as e: 246 | if isinstance(e, asyncio.CancelledError): 247 | return 248 | elif isinstance(e, InvalidSessionToken): 249 | print_and_exit("Invalid session token, make a new one") 250 | elif "token_expired" in e.message: 251 | check_repo_print("Your session token has expired, make a new one") 252 | else: 253 | raise 254 | 255 | 256 | async def update_check_coroutine(session: AsyncSession) -> bool: 257 | response = await session.get(REPO_TAGS_URL) 258 | tags = response.json() 259 | # Incase I delete all tags for whatever reason or the repo gets taken down 260 | if not tags or not isinstance(tags, list): 261 | return False 262 | latest_tag = tags[0] 263 | if latest_tag["name"] != V_VERSION: 264 | return True 265 | return False 266 | 267 | 268 | async def async_main() -> None: 269 | gpt = AsyncChatGPT(session_token=Config.session_token) 270 | session = AsyncSession(impersonate="chrome110") 271 | try: 272 | _, update_is_available = await asyncio.gather( 273 | gpt_coroutine(gpt), update_check_coroutine(session) 274 | ) 275 | if update_is_available: 276 | print('\n\nUpdate available run "pip update sengpt" to install it') 277 | except RequestsError: 278 | print("Check your internet!!!") 279 | 280 | 281 | def validate_session_token() -> None | NoReturn: 282 | if not Config.session_token: 283 | check_repo_print("Session token must be provided during initial configuration") 284 | 285 | 286 | 287 | def handle_static_args() -> None | NoReturn: 288 | if SYS_ARGS.is_set("version"): 289 | print(ArgParser.version_info()) 290 | sys.exit() 291 | if SYS_ARGS.is_set("help"): 292 | print(ArgParser.help_info()) 293 | sys.exit() 294 | if SYS_ARGS.is_set("config_file"): 295 | print(Config.info()) 296 | sys.exit() 297 | if SYS_ARGS.is_set("session_token"): 298 | Config.update_session_token() 299 | print("\nSuccessfully set session token") 300 | sys.exit() 301 | 302 | 303 | def main(): 304 | handle_static_args() 305 | validate_session_token() 306 | try: 307 | asyncio.run(async_main()) 308 | except KeyboardInterrupt: 309 | return 310 | 311 | 312 | if __name__ == "__main__": 313 | main() 314 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "appdirs" 5 | version = "1.4.4" 6 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 11 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 12 | ] 13 | 14 | [[package]] 15 | name = "cffi" 16 | version = "1.16.0" 17 | description = "Foreign Function Interface for Python calling C code." 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 22 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 23 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 24 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 25 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 26 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 27 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 28 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 29 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 30 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 31 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 32 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 33 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 34 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 35 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 36 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 37 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 38 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 39 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 40 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 41 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 42 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 43 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 44 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 45 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 46 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 47 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 48 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 49 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 50 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 51 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 52 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 53 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 54 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 55 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 56 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 57 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 58 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 59 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 60 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 61 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 62 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 63 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 64 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 65 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 66 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 67 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 68 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 69 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 70 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 71 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 72 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 73 | ] 74 | 75 | [package.dependencies] 76 | pycparser = "*" 77 | 78 | [[package]] 79 | name = "curl-cffi" 80 | version = "0.5.10" 81 | description = "libcurl ffi bindings for Python, with impersonation support" 82 | optional = false 83 | python-versions = ">=3.7" 84 | files = [ 85 | {file = "curl_cffi-0.5.10-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:892603dab5e56fb72bfff7ae969136138971f63f63defe98232e1ec55cb0f1c6"}, 86 | {file = "curl_cffi-0.5.10-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9937b8e13b1a6963c63e155b6621ec74649965105efedb919bc226fe731861cc"}, 87 | {file = "curl_cffi-0.5.10-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b537595b9610a4dd0927c09823925b4e32b1ce0fd04385bfc5bb72ab830720e6"}, 88 | {file = "curl_cffi-0.5.10-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b2bc8822d23415f6533c8b750475e9bbc76764025fe1dcb5866dc033607fd7b"}, 89 | {file = "curl_cffi-0.5.10-cp37-abi3-win_amd64.whl", hash = "sha256:f9a1874b860c4e8db49bdfd9b9d4dc39999a1397d271ec78624c35c838e9e92a"}, 90 | {file = "curl_cffi-0.5.10.tar.gz", hash = "sha256:55bac4b73e2d80ceeaabea33270fc8ca6ace594128a46710242f2e688b4f8bfc"}, 91 | ] 92 | 93 | [package.dependencies] 94 | cffi = ">=1.12.0" 95 | 96 | [package.extras] 97 | build = ["cibuildwheel", "wheel"] 98 | dev = ["autoflake (==1.4)", "black (==22.8.0)", "coverage (==6.4.1)", "cryptography (==38.0.3)", "flake8 (==6.0.0)", "flake8-bugbear (==22.7.1)", "flake8-pie (==0.15.0)", "httpx (==0.23.1)", "isort (==5.10.1)", "mypy (==0.971)", "pytest (==7.1.2)", "pytest-asyncio (==0.19.0)", "pytest-trio (==0.7.0)", "trio (==0.21.0)", "trio-typing (==0.7.0)", "trustme (==0.9.0)", "types-certifi (==2021.10.8.2)", "uvicorn (==0.18.3)"] 99 | test = ["cryptography (==38.0.3)", "httpx (==0.23.1)", "pytest (==7.1.2)", "pytest-asyncio (==0.19.0)", "pytest-trio (==0.7.0)", "trio (==0.21.0)", "trio-typing (==0.7.0)", "trustme (==0.9.0)", "types-certifi (==2021.10.8.2)", "uvicorn (==0.18.3)"] 100 | 101 | [[package]] 102 | name = "pycparser" 103 | version = "2.21" 104 | description = "C parser in Python" 105 | optional = false 106 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 107 | files = [ 108 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 109 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 110 | ] 111 | 112 | [[package]] 113 | name = "pyperclip" 114 | version = "1.8.2" 115 | description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" 116 | optional = false 117 | python-versions = "*" 118 | files = [ 119 | {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, 120 | ] 121 | 122 | [metadata] 123 | lock-version = "2.0" 124 | python-versions = "^3.11" 125 | content-hash = "028f5733366a093b747a4cf3f797bad3e29e99b30ac7af8b90e030e0fc59affa" 126 | -------------------------------------------------------------------------------- /sengpt/re_gpt/sync_chatgpt.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import inspect 3 | import time 4 | import uuid 5 | import websockets 6 | from websockets.exceptions import ConnectionClosed 7 | import json 8 | import base64 9 | import asyncio 10 | from queue import Queue 11 | from threading import Thread 12 | from typing import Callable, Generator, Optional 13 | 14 | from curl_cffi.requests import Session 15 | 16 | from .async_chatgpt import ( 17 | BACKUP_ARKOSE_TOKEN_GENERATOR, 18 | CHATGPT_API, 19 | USER_AGENT, 20 | AsyncChatGPT, 21 | AsyncConversation, 22 | MODELS, 23 | WS_REGISTER_URL, 24 | ) 25 | from .errors import ( 26 | BackendError, 27 | InvalidSessionToken, 28 | RetryError, 29 | TokenNotProvided, 30 | UnexpectedResponseError, 31 | InvalidModelName, 32 | ) 33 | from .utils import sync_get_binary_path, get_model_slug 34 | 35 | 36 | class SyncConversation(AsyncConversation): 37 | def __init__(self, chatgpt, conversation_id: Optional[str] = None, model=None): 38 | super().__init__(chatgpt, conversation_id, model) 39 | 40 | def fetch_chat(self) -> dict: 41 | """ 42 | Fetches the chat of the conversation from the API. 43 | 44 | Returns: 45 | dict: The JSON response from the API containing the chat if the conversation_id is not none, else returns an empty dict. 46 | 47 | Raises: 48 | UnexpectedResponseError: If the response is not a valid JSON object or if the response json is not in the expected format 49 | """ 50 | if not self.conversation_id: 51 | return {} 52 | 53 | url = CHATGPT_API.format(f"conversation/{self.conversation_id}") 54 | response = self.chatgpt.session.get( 55 | url=url, headers=self.chatgpt.build_request_headers() 56 | ) 57 | 58 | error = None 59 | try: 60 | chat = response.json() 61 | self.parent_id = list(chat.get("mapping", {}))[-1] 62 | model_slug = get_model_slug(chat) 63 | self.model = [ 64 | key for key, value in MODELS.items() if value["slug"] == model_slug 65 | ][0] 66 | except Exception as e: 67 | error = e 68 | if error is not None: 69 | raise UnexpectedResponseError(error, response.text) 70 | 71 | return chat 72 | 73 | def chat(self, user_input: str) -> Generator[dict, None, None]: 74 | """ 75 | As the name implies, chat with ChatGPT. 76 | 77 | Args: 78 | user_input (str): The user's input message. 79 | 80 | Yields: 81 | dict: A dictionary representing assistant responses. 82 | 83 | Returns: 84 | Generator[dict, None]: A generator object that yields assistant responses. 85 | 86 | Raises: 87 | UnexpectedResponseError: If the response is not a valid JSON object or if the response json is not in the expected format 88 | """ 89 | 90 | payload = self.build_message_payload(user_input) 91 | 92 | server_response = ( 93 | "" # To store what the server returned for debugging in case of an error 94 | ) 95 | error = None 96 | try: 97 | full_message = None 98 | while True: 99 | response = self.send_message(payload=payload) if not self.chatgpt.websocket_mode else self.send_websocket_message(payload=payload) 100 | for chunk in response: 101 | decoded_chunk = chunk.decode() if not self.chatgpt.websocket_mode else chunk 102 | 103 | server_response += decoded_chunk 104 | for line in decoded_chunk.splitlines(): 105 | if not line.startswith("data: "): 106 | continue 107 | 108 | raw_json_data = line[6:] 109 | if not (decoded_json := self.decode_raw_json(raw_json_data)): 110 | continue 111 | 112 | if ( 113 | "message" in decoded_json 114 | and decoded_json["message"]["author"]["role"] == "assistant" 115 | ): 116 | processed_response = self.filter_response(decoded_json) 117 | if full_message: 118 | prev_resp_len = len( 119 | full_message["message"]["content"]["parts"][0] 120 | ) 121 | processed_response["content"] = processed_response[ 122 | "content" 123 | ][prev_resp_len::] 124 | 125 | yield processed_response 126 | full_message = decoded_json 127 | self.conversation_id = full_message["conversation_id"] 128 | self.parent_id = full_message["message"]["id"] 129 | if ( 130 | full_message["message"]["metadata"]["finish_details"]["type"] 131 | == "max_tokens" 132 | ): 133 | payload = self.build_message_continuation_payload() 134 | else: 135 | break 136 | except Exception as e: 137 | error = e 138 | 139 | # raising the error outside the 'except' block to prevent the 'During handling of the above exception, another exception occurred' error 140 | if error is not None: 141 | raise UnexpectedResponseError(error, server_response) 142 | 143 | def send_message(self, payload: dict) -> Generator[bytes, None, None]: 144 | """ 145 | Send a message payload to the server and receive the response. 146 | 147 | Args: 148 | payload (dict): Payload containing message information. 149 | 150 | Yields: 151 | bytes: Chunk of data received as a response. 152 | """ 153 | response_queue = Queue() 154 | 155 | def perform_request(): 156 | def content_callback(chunk): 157 | response_queue.put(chunk) 158 | 159 | url = CHATGPT_API.format("conversation") 160 | headers = self.chatgpt.build_request_headers() 161 | # Add Chat Requirements Token 162 | chat_requriments_token = self.chatgpt.create_chat_requirements_token() 163 | if chat_requriments_token: 164 | headers["openai-sentinel-chat-requirements-token"] = chat_requriments_token 165 | 166 | response = self.chatgpt.session.post( 167 | url=url, 168 | headers=headers, 169 | json=payload, 170 | content_callback=content_callback, 171 | ) 172 | response_queue.put(None) 173 | 174 | Thread(target=perform_request).start() 175 | 176 | while True: 177 | chunk = response_queue.get() 178 | if chunk is None: 179 | break 180 | yield chunk 181 | 182 | def send_websocket_message(self, payload: dict) -> Generator[str, None, None]: 183 | """ 184 | Send a message payload via WebSocket and receive the response. 185 | 186 | Args: 187 | payload (dict): Payload containing message information. 188 | 189 | Yields: 190 | str: Chunk of data received as a response. 191 | """ 192 | 193 | response_queue = Queue() 194 | websocket_request_id = None 195 | 196 | def perform_request(): 197 | nonlocal websocket_request_id 198 | 199 | url = CHATGPT_API.format("conversation") 200 | headers = self.chatgpt.build_request_headers() 201 | # Add Chat Requirements Token 202 | chat_requriments_token = self.chatgpt.create_chat_requirements_token() 203 | if chat_requriments_token: 204 | headers["openai-sentinel-chat-requirements-token"] = chat_requriments_token 205 | 206 | response = (self.chatgpt.session.post( 207 | url=url, 208 | headers=headers, 209 | json=payload, 210 | )).json() 211 | 212 | websocket_request_id = response.get("websocket_request_id") 213 | 214 | if websocket_request_id is None: 215 | raise UnexpectedResponseError("WebSocket request ID not found in response", response) 216 | 217 | if websocket_request_id not in self.chatgpt.ws_conversation_map: 218 | self.chatgpt.ws_conversation_map[websocket_request_id] = response_queue 219 | 220 | Thread(target=perform_request).start() 221 | 222 | while True: 223 | chunk = response_queue.get() 224 | if chunk is None: 225 | break 226 | yield chunk 227 | 228 | del self.chatgpt.ws_conversation_map[websocket_request_id] 229 | 230 | def build_message_payload(self, user_input: str) -> dict: 231 | """ 232 | Build a payload for sending a user message. 233 | 234 | Returns: 235 | dict: Payload containing message information. 236 | """ 237 | if self.conversation_id and (self.parent_id is None or self.model is None): 238 | self.fetch_chat() # it will automatically fetch the chat and set the parent id 239 | 240 | payload = { 241 | "conversation_mode": {"conversation_mode": {"kind": "primary_assistant"}}, 242 | "conversation_id": self.conversation_id, 243 | "action": "next", 244 | "arkose_token": self.arkose_token_generator() 245 | if self.chatgpt.generate_arkose_token 246 | or MODELS[self.model]["needs_arkose_token"] 247 | else None, 248 | "force_paragen": False, 249 | "history_and_training_disabled": False, 250 | "messages": [ 251 | { 252 | "author": {"role": "user"}, 253 | "content": {"content_type": "text", "parts": [user_input]}, 254 | "id": str(uuid.uuid4()), 255 | "metadata": {}, 256 | } 257 | ], 258 | "model": MODELS[self.model]["slug"], 259 | "parent_message_id": str(uuid.uuid4()) 260 | if not self.parent_id 261 | else self.parent_id, 262 | "websocket_request_id": str(uuid.uuid4()) 263 | if self.chatgpt.websocket_mode 264 | else None, 265 | } 266 | 267 | return payload 268 | 269 | def build_message_continuation_payload(self) -> dict: 270 | """ 271 | Build a payload for continuing ChatGPT's cut off response. 272 | 273 | Returns: 274 | dict: Payload containing message information for continuation. 275 | """ 276 | payload = { 277 | "conversation_mode": {"conversation_mode": {"kind": "primary_assistant"}}, 278 | "action": "continue", 279 | "arkose_token": self.arkose_token_generator() 280 | if self.chatgpt.generate_arkose_token 281 | or MODELS[self.model]["needs_arkose_token"] 282 | else None, 283 | "conversation_id": self.conversation_id, 284 | "force_paragen": False, 285 | "history_and_training_disabled": False, 286 | "model": MODELS[self.model]["slug"], 287 | "parent_message_id": self.parent_id, 288 | "timezone_offset_min": -300, 289 | } 290 | 291 | return payload 292 | 293 | def arkose_token_generator(self) -> str: 294 | """ 295 | Generate an Arkose token. 296 | 297 | Returns: 298 | str: Arkose token. 299 | """ 300 | if not self.chatgpt.tried_downloading_binary: 301 | self.chatgpt.binary_path = sync_get_binary_path(self.chatgpt.session) 302 | 303 | if self.chatgpt.binary_path: 304 | self.chatgpt.arkose = ctypes.CDLL(self.chatgpt.binary_path) 305 | self.chatgpt.arkose.GetToken.restype = ctypes.c_char_p 306 | 307 | self.chatgpt.tried_downloading_binary = True 308 | 309 | if self.chatgpt.binary_path: 310 | try: 311 | result = self.chatgpt.arkose.GetToken() 312 | return ctypes.string_at(result).decode("utf-8") 313 | except: 314 | pass 315 | 316 | for _ in range(5): 317 | response = self.chatgpt.session.get(BACKUP_ARKOSE_TOKEN_GENERATOR) 318 | if response.text == "null": 319 | raise BackendError(error_code=505) 320 | try: 321 | return response.json()["token"] 322 | except: 323 | time.sleep(0.7) 324 | 325 | raise RetryError(website=BACKUP_ARKOSE_TOKEN_GENERATOR) 326 | 327 | def delete(self) -> None: 328 | """ 329 | Deletes the conversation. 330 | """ 331 | if self.conversation_id: 332 | self.chatgpt.delete_conversation(self.conversation_id) 333 | 334 | self.conversation_id = None 335 | self.parent_id = None 336 | 337 | 338 | class SyncChatGPT(AsyncChatGPT): 339 | def __init__( 340 | self, 341 | proxies: Optional[dict] = None, 342 | session_token: Optional[str] = None, 343 | exit_callback_function: Optional[Callable] = None, 344 | auth_token: Optional[str] = None, 345 | websocket_mode: Optional[bool] = False, 346 | ): 347 | """ 348 | Initializes an instance of the class. 349 | 350 | Args: 351 | proxies (Optional[dict]): A dictionary of proxy settings. Defaults to None. 352 | session_token (Optional[str]): A session token. Defaults to None. 353 | exit_callback_function (Optional[callable]): A function to be called on exit. Defaults to None. 354 | auth_token (Optional[str]): An authentication token. Defaults to None. 355 | websocket_mode (Optional[bool]): Toggle whether to use WebSocket for chat. Defaults to False. 356 | """ 357 | super().__init__( 358 | proxies=proxies, 359 | session_token=session_token, 360 | exit_callback_function=exit_callback_function, 361 | auth_token=auth_token, 362 | websocket_mode=websocket_mode, 363 | ) 364 | 365 | self.stop_websocket_flag = False 366 | self.stop_websocket = None 367 | 368 | def __enter__(self): 369 | self.session = Session( 370 | impersonate="chrome110", timeout=99999, proxies=self.proxies 371 | ) 372 | 373 | if self.generate_arkose_token: 374 | self.binary_path = sync_get_binary_path(self.session) 375 | 376 | if self.binary_path: 377 | self.arkose = ctypes.CDLL(self.binary_path) 378 | self.arkose.GetToken.restype = ctypes.c_char_p 379 | 380 | self.tried_downloading_binary = True 381 | 382 | if not self.auth_token: 383 | if self.session_token is None: 384 | raise TokenNotProvided 385 | self.auth_token = self.fetch_auth_token() 386 | 387 | # automaticly check the status of websocket_mode 388 | if not self.websocket_mode: 389 | self.websocket_mode = self.check_websocket_availability() 390 | 391 | if self.websocket_mode: 392 | def run_websocket(): 393 | asyncio.run(self.ensure_websocket()) 394 | self.ws_loop = Thread(target=run_websocket) 395 | self.ws_loop.start() 396 | 397 | return self 398 | 399 | def __exit__(self, *args): 400 | try: 401 | if self.exit_callback_function and callable(self.exit_callback_function): 402 | if not inspect.iscoroutinefunction(self.exit_callback_function): 403 | self.exit_callback_function(self) 404 | finally: 405 | self.session.close() 406 | 407 | if self.websocket_mode: 408 | self.stop_websocket_flag = True 409 | self.ws_loop.join() 410 | 411 | def get_conversation(self, conversation_id: str) -> SyncConversation: 412 | """ 413 | Makes an instance of class Conversation and return it. 414 | 415 | Args: 416 | conversation_id (str): The ID of the conversation to fetch. 417 | 418 | Returns: 419 | Conversation: Conversation object. 420 | """ 421 | 422 | return SyncConversation(self, conversation_id) 423 | 424 | def create_new_conversation( 425 | self, model: Optional[str] = "gpt-3.5" 426 | ) -> SyncConversation: 427 | if model not in MODELS: 428 | raise InvalidModelName(model, MODELS) 429 | return SyncConversation(self, model=model) 430 | 431 | def delete_conversation(self, conversation_id: str) -> dict: 432 | """ 433 | Delete a conversation. 434 | 435 | Args: 436 | conversation_id (str): Unique identifier for the conversation. 437 | 438 | Returns: 439 | dict: Server response json. 440 | """ 441 | url = CHATGPT_API.format(f"conversation/{conversation_id}") 442 | response = self.session.patch( 443 | url=url, headers=self.build_request_headers(), json={"is_visible": False} 444 | ) 445 | 446 | return response.json() 447 | 448 | def fetch_auth_token(self) -> str: 449 | """ 450 | Fetch the authentication token for the session. 451 | 452 | Raises: 453 | InvalidSessionToken: If the session token is invalid. 454 | 455 | Returns: authentication token. 456 | """ 457 | url = "https://chat.openai.com/api/auth/session" 458 | cookies = {"__Secure-next-auth.session-token": self.session_token} 459 | 460 | headers = { 461 | "User-Agent": USER_AGENT, 462 | "Accept": "*/*", 463 | "Accept-Language": "en-US,en;q=0.5", 464 | "Alt-Used": "chat.openai.com", 465 | "Connection": "keep-alive", 466 | "Sec-Fetch-Dest": "empty", 467 | "Sec-Fetch-Mode": "cors", 468 | "Sec-Fetch-Site": "same-origin", 469 | "Sec-GPC": "1", 470 | "Cookie": "; ".join( 471 | [ 472 | f"{cookie_key}={cookie_value}" 473 | for cookie_key, cookie_value in cookies.items() 474 | ] 475 | ), 476 | } 477 | 478 | response = self.session.get(url=url, headers=headers) 479 | response_json = response.json() 480 | 481 | if "accessToken" in response_json: 482 | return response_json["accessToken"] 483 | 484 | raise InvalidSessionToken 485 | 486 | def set_custom_instructions( 487 | self, 488 | about_user: Optional[str] = "", 489 | about_model: Optional[str] = "", 490 | enable_for_new_chats: Optional[bool] = True, 491 | ) -> dict: 492 | """ 493 | Set cuteom instructions for ChatGPT. 494 | 495 | Args: 496 | about_user (str): What would you like ChatGPT to know about you to provide better responses? 497 | about_model (str): How would you like ChatGPT to respond? 498 | enable_for_new_chats (bool): Enable for new chats. 499 | Returns: 500 | dict: Server response json. 501 | """ 502 | data = { 503 | "about_user_message": about_user, 504 | "about_model_message": about_model, 505 | "enabled": enable_for_new_chats, 506 | } 507 | url = CHATGPT_API.format("user_system_messages") 508 | response = self.session.post( 509 | url=url, headers=self.build_request_headers(), json=data 510 | ) 511 | 512 | return response.json() 513 | 514 | def retrieve_chats( 515 | self, offset: Optional[int] = 0, limit: Optional[int] = 28 516 | ) -> dict: 517 | params = { 518 | "offset": offset, 519 | "limit": limit, 520 | "order": "updated", 521 | } 522 | url = CHATGPT_API.format("conversations") 523 | response = self.session.get( 524 | url=url, params=params, headers=self.build_request_headers() 525 | ) 526 | 527 | return response.json() 528 | 529 | def check_websocket_availability(self) -> bool: 530 | """ 531 | Check if WebSocket is available. 532 | 533 | Returns: 534 | bool: True if WebSocket is available, otherwise False. 535 | """ 536 | url = CHATGPT_API.format("accounts/check/v4-2023-04-27") 537 | response = (self.session.get( 538 | url=url, headers=self.build_request_headers() 539 | )).json() 540 | 541 | if 'account_ordering' in response and 'accounts' in response: 542 | account_id = response['account_ordering'][0] 543 | if account_id in response['accounts']: 544 | return 'shared_websocket' in response['accounts'][account_id]['features'] 545 | 546 | return False 547 | 548 | async def ensure_websocket(self): 549 | ws_url_rsp = self.session.post(WS_REGISTER_URL, headers=self.build_request_headers()).json() 550 | ws_url = ws_url_rsp['wss_url'] 551 | access_token = self.extract_access_token(ws_url) 552 | asyncio.create_task(self.ensure_close_websocket()) 553 | await self.listen_to_websocket(ws_url, access_token) 554 | 555 | async def ensure_close_websocket(self): 556 | while True: 557 | if self.stop_websocket_flag: 558 | break 559 | await asyncio.sleep(1) 560 | await self.stop_websocket() 561 | 562 | async def listen_to_websocket(self, ws_url: str, access_token: str): 563 | headers = {'Authorization': f'Bearer {access_token}'} 564 | async with websockets.connect(ws_url, extra_headers=headers) as websocket: 565 | async def stop_websocket(): 566 | await websocket.close() 567 | self.stop_websocket = stop_websocket 568 | 569 | while True: 570 | message = None 571 | try: 572 | message = await websocket.recv() 573 | except ConnectionClosed: 574 | break 575 | message_data = json.loads(message) 576 | body_encoded = message_data.get("body", "") 577 | ws_id = message_data.get("websocket_request_id", "") 578 | decoded_body = base64.b64decode(body_encoded).decode('utf-8') 579 | response_queue = self.ws_conversation_map.get(ws_id) 580 | if response_queue is None: 581 | continue 582 | response_queue.put_nowait(decoded_body) 583 | if '[DONE]' in decoded_body or '[ERROR]' in decoded_body: 584 | response_queue.put(None) 585 | continue 586 | 587 | def create_chat_requirements_token(self): 588 | """ 589 | Get a chat requirements token from chatgpt server 590 | 591 | Returns: 592 | str: chat requirements token 593 | """ 594 | url = CHATGPT_API.format("sentinel/chat-requirements") 595 | response = self.session.post( 596 | url=url, headers=self.build_request_headers() 597 | ) 598 | body = response.json() 599 | token = body.get("token", None) 600 | return token -------------------------------------------------------------------------------- /sengpt/re_gpt/async_chatgpt.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import ctypes 3 | import inspect 4 | import json 5 | import uuid 6 | import re 7 | import websockets 8 | import base64 9 | from typing import AsyncGenerator, Callable, Optional 10 | 11 | from curl_cffi.requests import AsyncSession 12 | from .errors import ( 13 | BackendError, 14 | InvalidSessionToken, 15 | RetryError, 16 | TokenNotProvided, 17 | UnexpectedResponseError, 18 | InvalidModelName, 19 | ) 20 | from .utils import async_get_binary_path, get_model_slug 21 | 22 | # Constants 23 | USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" 24 | CHATGPT_API = "https://chat.openai.com/backend-api/{}" 25 | BACKUP_ARKOSE_TOKEN_GENERATOR = "https://arkose-token-generator.zaieem.repl.co/token" 26 | WS_REGISTER_URL = CHATGPT_API.format("register-websocket") 27 | 28 | MODELS = { 29 | "gpt-4": {"slug": "gpt-4", "needs_arkose_token": True}, 30 | "gpt-3.5": {"slug": "text-davinci-002-render-sha", "needs_arkose_token": False}, 31 | } 32 | 33 | 34 | class AsyncConversation: 35 | def __init__(self, chatgpt, conversation_id=None, model=None): 36 | self.chatgpt = chatgpt 37 | self.conversation_id = conversation_id 38 | self.parent_id = None 39 | self.model = model 40 | 41 | async def fetch_chat(self) -> dict: 42 | """ 43 | Fetches the chat of the conversation from the API. 44 | 45 | Returns: 46 | dict: The JSON response from the API containing the chat if the conversation_id is not none, else returns an empty dict. 47 | 48 | Raises: 49 | UnexpectedResponseError: If the response is not a valid JSON object or if the response json is not in the expected format 50 | """ 51 | if not self.conversation_id: 52 | return {} 53 | 54 | url = CHATGPT_API.format(f"conversation/{self.conversation_id}") 55 | response = await self.chatgpt.session.get( 56 | url=url, headers=self.chatgpt.build_request_headers() 57 | ) 58 | 59 | error = None 60 | try: 61 | chat = response.json() 62 | self.parent_id = list(chat.get("mapping", {}))[-1] 63 | model_slug = get_model_slug(chat) 64 | self.model = [ 65 | key for key, value in MODELS.items() if value["slug"] == model_slug 66 | ][0] 67 | except Exception as e: 68 | error = e 69 | if error is not None: 70 | raise UnexpectedResponseError(error, response.text) 71 | 72 | return chat 73 | 74 | async def chat(self, user_input: str) -> AsyncGenerator[dict, None]: 75 | """ 76 | As the name implies, chat with ChatGPT. 77 | 78 | Args: 79 | user_input (str): The user's input message. 80 | 81 | Yields: 82 | dict: A dictionary representing assistant responses. 83 | 84 | Returns: 85 | AsyncGenerator[dict, None]: An asynchronous generator object that yields assistant responses. 86 | 87 | Raises: 88 | UnexpectedResponseError: If the response is not a valid JSON object or if the response json is not in the expected format 89 | """ 90 | 91 | payload = await self.build_message_payload(user_input) 92 | 93 | server_response = ( 94 | "" # To store what the server returned for debugging in case of an error 95 | ) 96 | error = None 97 | try: 98 | full_message = None 99 | while True: 100 | response = self.send_message(payload=payload) if not self.chatgpt.websocket_mode else self.send_websocket_message(payload=payload) 101 | async for chunk in response: 102 | decoded_chunk = chunk.decode() if isinstance(chunk, bytes) else chunk 103 | 104 | server_response += decoded_chunk 105 | for line in decoded_chunk.splitlines(): 106 | if not line.startswith("data: "): 107 | continue 108 | 109 | raw_json_data = line[6:] 110 | if not (decoded_json := self.decode_raw_json(raw_json_data)): 111 | continue 112 | 113 | if ( 114 | "message" in decoded_json 115 | and decoded_json["message"]["author"]["role"] == "assistant" 116 | ): 117 | processed_response = self.filter_response(decoded_json) 118 | if full_message: 119 | prev_resp_len = len( 120 | full_message["message"]["content"]["parts"][0] 121 | ) 122 | processed_response["content"] = processed_response[ 123 | "content" 124 | ][prev_resp_len::] 125 | 126 | yield processed_response 127 | full_message = decoded_json 128 | self.conversation_id = full_message["conversation_id"] 129 | self.parent_id = full_message["message"]["id"] 130 | if ( 131 | full_message["message"]["metadata"]["finish_details"]["type"] 132 | == "max_tokens" 133 | ): 134 | payload = await self.build_message_continuation_payload() 135 | else: 136 | break 137 | except Exception as e: 138 | error = e 139 | 140 | # raising the error outside the 'except' block to prevent the 'During handling of the above exception, another exception occurred' error 141 | if error is not None: 142 | raise UnexpectedResponseError(error, server_response) 143 | 144 | async def send_message(self, payload: dict) -> AsyncGenerator[bytes, None]: 145 | """ 146 | Send a message payload to the server and receive the response. 147 | 148 | Args: 149 | payload (dict): Payload containing message information. 150 | 151 | Yields: 152 | bytes: Chunk of data received as a response. 153 | """ 154 | response_queue = asyncio.Queue() 155 | 156 | async def perform_request(): 157 | def content_callback(chunk): 158 | response_queue.put_nowait(chunk) 159 | 160 | url = CHATGPT_API.format("conversation") 161 | 162 | headers = self.chatgpt.build_request_headers() 163 | # Add Chat Requirements Token 164 | chat_requriments_token = await self.chatgpt.create_chat_requirements_token() 165 | if chat_requriments_token: 166 | headers["openai-sentinel-chat-requirements-token"] = chat_requriments_token 167 | 168 | await self.chatgpt.session.post( 169 | url=url, 170 | headers=headers, 171 | json=payload, 172 | content_callback=content_callback, 173 | ) 174 | await response_queue.put(None) 175 | 176 | asyncio.create_task(perform_request()) 177 | 178 | while True: 179 | chunk = await response_queue.get() 180 | if chunk is None: 181 | break 182 | yield chunk 183 | 184 | async def send_websocket_message(self, payload: dict) -> AsyncGenerator[str, None]: 185 | """ 186 | Send a message payload via WebSocket and receive the response. 187 | 188 | Args: 189 | payload (dict): Payload containing message information. 190 | 191 | Yields: 192 | str: Chunk of data received as a response. 193 | """ 194 | await self.chatgpt.ensure_websocket() 195 | 196 | response_queue = asyncio.Queue() 197 | websocket_request_id = None 198 | 199 | async def perform_request(): 200 | nonlocal websocket_request_id 201 | 202 | url = CHATGPT_API.format("conversation") 203 | headers = self.chatgpt.build_request_headers() 204 | # Add Chat Requirements Token 205 | chat_requriments_token = await self.chatgpt.create_chat_requirements_token() 206 | if chat_requriments_token: 207 | headers["openai-sentinel-chat-requirements-token"] = chat_requriments_token 208 | 209 | response = (await self.chatgpt.session.post( 210 | url=url, 211 | headers=headers, 212 | json=payload, 213 | )).json() 214 | 215 | websocket_request_id = response.get("websocket_request_id") 216 | 217 | if websocket_request_id is None: 218 | raise UnexpectedResponseError("WebSocket request ID not found in response", response) 219 | 220 | if websocket_request_id not in self.chatgpt.ws_conversation_map: 221 | self.chatgpt.ws_conversation_map[websocket_request_id] = response_queue 222 | 223 | asyncio.create_task(perform_request()) 224 | 225 | while True: 226 | chunk = await response_queue.get() 227 | if chunk is None: 228 | break 229 | yield chunk 230 | 231 | del self.chatgpt.ws_conversation_map[websocket_request_id] 232 | 233 | 234 | async def build_message_payload(self, user_input: str) -> dict: 235 | """ 236 | Build a payload for sending a user message. 237 | 238 | Returns: 239 | dict: Payload containing message information. 240 | """ 241 | if self.conversation_id and (self.parent_id is None or self.model is None): 242 | await self.fetch_chat() # it will automatically fetch the chat and set the parent id 243 | 244 | payload = { 245 | "conversation_mode": {"conversation_mode": {"kind": "primary_assistant"}}, 246 | "conversation_id": self.conversation_id, 247 | "action": "next", 248 | "arkose_token": await self.arkose_token_generator() 249 | if self.chatgpt.generate_arkose_token 250 | or MODELS[self.model]["needs_arkose_token"] 251 | else None, 252 | "force_paragen": False, 253 | "history_and_training_disabled": False, 254 | "messages": [ 255 | { 256 | "author": {"role": "user"}, 257 | "content": {"content_type": "text", "parts": [user_input]}, 258 | "id": str(uuid.uuid4()), 259 | "metadata": {}, 260 | } 261 | ], 262 | "model": MODELS[self.model]["slug"], 263 | "parent_message_id": str(uuid.uuid4()) 264 | if not self.parent_id 265 | else self.parent_id, 266 | "websocket_request_id": str(uuid.uuid4()) 267 | if self.chatgpt.websocket_mode 268 | else None, 269 | } 270 | 271 | return payload 272 | 273 | async def build_message_continuation_payload(self) -> dict: 274 | """ 275 | Build a payload for continuing ChatGPT's cut off response. 276 | 277 | Returns: 278 | dict: Payload containing message information for continuation. 279 | """ 280 | payload = { 281 | "conversation_mode": {"conversation_mode": {"kind": "primary_assistant"}}, 282 | "action": "continue", 283 | "arkose_token": await self.arkose_token_generator() 284 | if self.chatgpt.generate_arkose_token 285 | or MODELS[self.model]["needs_arkose_token"] 286 | else None, 287 | "conversation_id": self.conversation_id, 288 | "force_paragen": False, 289 | "history_and_training_disabled": False, 290 | "model": MODELS[self.model]["slug"], 291 | "parent_message_id": self.parent_id, 292 | "timezone_offset_min": -300, 293 | } 294 | 295 | return payload 296 | 297 | async def arkose_token_generator(self) -> str: 298 | """ 299 | Generate an Arkose token. 300 | 301 | Returns: 302 | str: Arkose token. 303 | """ 304 | if not self.chatgpt.tried_downloading_binary: 305 | self.chatgpt.binary_path = await async_get_binary_path(self.chatgpt.session) 306 | 307 | if self.chatgpt.binary_path: 308 | self.chatgpt.arkose = ctypes.CDLL(self.chatgpt.binary_path) 309 | self.chatgpt.arkose.GetToken.restype = ctypes.c_char_p 310 | 311 | self.chatgpt.tried_downloading_binary = True 312 | 313 | if self.chatgpt.binary_path: 314 | try: 315 | result = self.chatgpt.arkose.GetToken() 316 | return ctypes.string_at(result).decode("utf-8") 317 | except: 318 | pass 319 | 320 | for _ in range(5): 321 | response = await self.chatgpt.session.get(BACKUP_ARKOSE_TOKEN_GENERATOR) 322 | if response.text == "null": 323 | raise BackendError(error_code=505) 324 | try: 325 | return response.json()["token"] 326 | except: 327 | await asyncio.sleep(0.7) 328 | 329 | raise RetryError(website=BACKUP_ARKOSE_TOKEN_GENERATOR) 330 | 331 | async def delete(self) -> None: 332 | """ 333 | Deletes the conversation. 334 | """ 335 | if self.conversation_id: 336 | await self.chatgpt.delete_conversation(self.conversation_id) 337 | 338 | self.conversation_id = None 339 | self.parent_id = None 340 | 341 | @staticmethod 342 | def decode_raw_json(raw_json_data: str) -> dict or bool: 343 | """ 344 | Decode JSON. 345 | 346 | Args: 347 | raw_json_data (str): JSON as a string. 348 | 349 | Returns: 350 | dict: Decoded JSON. 351 | """ 352 | try: 353 | decoded_json = json.loads(raw_json_data.strip()) 354 | return decoded_json 355 | except: 356 | return False 357 | 358 | @staticmethod 359 | def filter_response(response): 360 | processed_response = { 361 | "content": response["message"]["content"]["parts"][0], 362 | "message_id": response["message"]["id"], 363 | "parent_id": response["message"]["metadata"]["parent_id"], 364 | "conversation_id": response["conversation_id"], 365 | } 366 | 367 | return processed_response 368 | 369 | 370 | class AsyncChatGPT: 371 | def __init__( 372 | self, 373 | proxies: Optional[dict] = None, 374 | session_token: Optional[str] = None, 375 | exit_callback_function: Optional[Callable] = None, 376 | auth_token: Optional[str] = None, 377 | generate_arkose_token: Optional[bool] = False, 378 | websocket_mode: Optional[bool] = False, 379 | ): 380 | """ 381 | Initializes an instance of the class. 382 | 383 | Args: 384 | proxies (Optional[dict]): A dictionary of proxy settings. Defaults to None. 385 | session_token (Optional[str]): A session token. Defaults to None. 386 | exit_callback_function (Optional[callable]): A function to be called on exit. Defaults to None. 387 | auth_token (Optional[str]): An authentication token. Defaults to None. 388 | generate_arkose_token (Optional[bool]): Toggle whether to generate and send arkose-token in the payload. Defaults to False. 389 | websocket_mode (Optional[bool]): Toggle whether to use WebSocket for chat. Defaults to False. 390 | """ 391 | self.proxies = proxies 392 | self.exit_callback_function = exit_callback_function 393 | 394 | self.arkose = None 395 | self.binary_path = None 396 | self.tried_downloading_binary = False 397 | self.generate_arkose_token = generate_arkose_token 398 | 399 | self.session_token = session_token 400 | self.auth_token = auth_token 401 | self.session = None 402 | 403 | self.websocket_mode = websocket_mode 404 | self.ws_loop = None 405 | self.ws_conversation_map = {} 406 | 407 | async def __aenter__(self): 408 | self.session = AsyncSession( 409 | impersonate="chrome110", timeout=99999, proxies=self.proxies 410 | ) 411 | if self.generate_arkose_token: 412 | self.binary_path = await async_get_binary_path(self.session) 413 | 414 | if self.binary_path: 415 | self.arkose = ctypes.CDLL(self.binary_path) 416 | self.arkose.GetToken.restype = ctypes.c_char_p 417 | 418 | self.tried_downloading_binary = True 419 | 420 | if not self.auth_token: 421 | if self.session_token is None: 422 | raise TokenNotProvided 423 | self.auth_token = await self.fetch_auth_token() 424 | 425 | if not self.websocket_mode: 426 | self.websocket_mode = await self.check_websocket_availability() 427 | 428 | if self.websocket_mode: 429 | await self.ensure_websocket() 430 | 431 | return self 432 | 433 | async def __aexit__(self, *_): 434 | try: 435 | if self.exit_callback_function and callable(self.exit_callback_function): 436 | if not inspect.iscoroutinefunction(self.exit_callback_function): 437 | self.exit_callback_function(self) 438 | finally: 439 | self.session.close() 440 | 441 | def build_request_headers(self) -> dict: 442 | """ 443 | Build headers for HTTP requests. 444 | 445 | Returns: 446 | dict: Request headers. 447 | """ 448 | headers = { 449 | "User-Agent": USER_AGENT, 450 | "Accept": "text/event-stream", 451 | "Accept-Language": "en-US", 452 | "Accept-Encoding": "gzip, deflate, br", 453 | "Content-Type": "application/json", 454 | "Authorization": f"Bearer {self.auth_token}", 455 | "Origin": "https://chat.openai.com", 456 | "Alt-Used": "chat.openai.com", 457 | "Connection": "keep-alive", 458 | } 459 | 460 | return headers 461 | 462 | def get_conversation(self, conversation_id: str) -> AsyncConversation: 463 | """ 464 | Makes an instance of class Conversation and return it. 465 | 466 | Args: 467 | conversation_id (str): The ID of the conversation to fetch. 468 | 469 | Returns: 470 | Conversation: Conversation object. 471 | """ 472 | 473 | return AsyncConversation(self, conversation_id) 474 | 475 | def create_new_conversation( 476 | self, model: Optional[str] = "gpt-3.5" 477 | ) -> AsyncConversation: 478 | if model not in MODELS: 479 | raise InvalidModelName(model, MODELS) 480 | return AsyncConversation(self, model=model) 481 | 482 | async def delete_conversation(self, conversation_id: str) -> dict: 483 | """ 484 | Delete a conversation. 485 | 486 | Args: 487 | conversation_id (str): Unique identifier for the conversation. 488 | 489 | Returns: 490 | dict: Server response json. 491 | """ 492 | url = CHATGPT_API.format(f"conversation/{conversation_id}") 493 | response = await self.session.patch( 494 | url=url, headers=self.build_request_headers(), json={"is_visible": False} 495 | ) 496 | 497 | return response.json() 498 | 499 | async def fetch_auth_token(self) -> str: 500 | """ 501 | Fetch the authentication token for the session. 502 | 503 | Raises: 504 | InvalidSessionToken: If the session token is invalid. 505 | 506 | Returns: authentication token. 507 | """ 508 | url = "https://chat.openai.com/api/auth/session" 509 | cookies = {"__Secure-next-auth.session-token": self.session_token} 510 | 511 | headers = { 512 | "User-Agent": USER_AGENT, 513 | "Accept": "*/*", 514 | "Accept-Language": "en-US,en;q=0.5", 515 | "Alt-Used": "chat.openai.com", 516 | "Connection": "keep-alive", 517 | "Sec-Fetch-Dest": "empty", 518 | "Sec-Fetch-Mode": "cors", 519 | "Sec-Fetch-Site": "same-origin", 520 | "Sec-GPC": "1", 521 | "Cookie": "; ".join( 522 | [ 523 | f"{cookie_key}={cookie_value}" 524 | for cookie_key, cookie_value in cookies.items() 525 | ] 526 | ), 527 | } 528 | 529 | response = await self.session.get(url=url, headers=headers) 530 | response_json = response.json() 531 | 532 | if "accessToken" in response_json: 533 | return response_json["accessToken"] 534 | 535 | raise InvalidSessionToken 536 | 537 | async def set_custom_instructions( 538 | self, 539 | about_user: Optional[str] = "", 540 | about_model: Optional[str] = "", 541 | enable_for_new_chats: Optional[bool] = True, 542 | ) -> dict: 543 | """ 544 | Set cuteom instructions for ChatGPT. 545 | 546 | Args: 547 | about_user (str): What would you like ChatGPT to know about you to provide better responses? 548 | about_model (str): How would you like ChatGPT to respond? 549 | enable_for_new_chats (bool): Enable for new chats. 550 | Returns: 551 | dict: Server response json. 552 | """ 553 | data = { 554 | "about_user_message": about_user, 555 | "about_model_message": about_model, 556 | "enabled": enable_for_new_chats, 557 | } 558 | url = CHATGPT_API.format("user_system_messages") 559 | response = await self.session.post( 560 | url=url, headers=self.build_request_headers(), json=data 561 | ) 562 | 563 | return response.json() 564 | 565 | async def retrieve_chats( 566 | self, offset: Optional[int] = 0, limit: Optional[int] = 28 567 | ) -> dict: 568 | params = { 569 | "offset": offset, 570 | "limit": limit, 571 | "order": "updated", 572 | } 573 | url = CHATGPT_API.format("conversations") 574 | response = await self.session.get( 575 | url=url, params=params, headers=self.build_request_headers() 576 | ) 577 | 578 | return response.json() 579 | 580 | async def check_websocket_availability(self) -> bool: 581 | """ 582 | Check if WebSocket is available. 583 | 584 | Returns: 585 | bool: True if WebSocket is available, otherwise False. 586 | """ 587 | url = CHATGPT_API.format("accounts/check/v4-2023-04-27") 588 | response = (await self.session.get( 589 | url=url, headers=self.build_request_headers() 590 | )).json() 591 | 592 | if 'account_ordering' in response and 'accounts' in response: 593 | account_id = response['account_ordering'][0] 594 | if account_id in response['accounts']: 595 | return 'shared_websocket' in response['accounts'][account_id]['features'] 596 | 597 | return False 598 | 599 | async def ensure_websocket(self): 600 | if not self.ws_loop: 601 | ws_url_rsp = (await self.session.post(WS_REGISTER_URL, headers=self.build_request_headers())).json() 602 | ws_url = ws_url_rsp['wss_url'] 603 | access_token = self.extract_access_token(ws_url) 604 | self.ws_loop = asyncio.create_task(self.listen_to_websocket(ws_url, access_token)) 605 | 606 | def extract_access_token(self, url): 607 | match = re.search(r'access_token=([^&]*)', url) 608 | if match: 609 | return match.group(1) 610 | else: 611 | return None 612 | 613 | async def listen_to_websocket(self, ws_url: str, access_token: str): 614 | headers = {'Authorization': f'Bearer {access_token}'} 615 | async with websockets.connect(ws_url, extra_headers=headers) as websocket: 616 | while True: 617 | message = await websocket.recv() 618 | message_data = json.loads(message) 619 | body_encoded = message_data.get("body", "") 620 | ws_id = message_data.get("websocket_request_id", "") 621 | decoded_body = base64.b64decode(body_encoded).decode('utf-8') 622 | response_queue = self.ws_conversation_map.get(ws_id) 623 | if response_queue is None: 624 | continue 625 | if 'title_generation' in decoded_body: 626 | # skip 627 | continue 628 | response_queue.put_nowait(decoded_body) 629 | if '[DONE]' in decoded_body or '[ERROR]' in decoded_body: 630 | await response_queue.put(None) 631 | continue 632 | 633 | async def create_chat_requirements_token(self): 634 | """ 635 | Get a chat requirements token from chatgpt server 636 | 637 | Returns: 638 | str: chat requirements token 639 | """ 640 | url = CHATGPT_API.format("sentinel/chat-requirements") 641 | response = await self.session.post( 642 | url=url, headers=self.build_request_headers() 643 | ) 644 | body = response.json() 645 | token = body.get("token", None) 646 | return token 647 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------