Skip to content
This repository has been archived by the owner on Aug 10, 2023. It is now read-only.

Experimental pure Python Cloudflare bypass #1382

Merged
merged 4 commits into from
May 28, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 88 additions & 46 deletions src/revChatGPT/V1.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import logging
import time
import uuid
import tempfile
from functools import wraps
from os import environ
from os import getenv
Expand All @@ -19,9 +20,9 @@
from typing import Generator
from typing import NoReturn


import httpx
import requests
from httpx import AsyncClient
from curl_cffi import requests
acheong08 marked this conversation as resolved.
Show resolved Hide resolved
from OpenAIAuth import Authenticator
from OpenAIAuth import Error as AuthError

Expand Down Expand Up @@ -81,7 +82,7 @@ def wrapper(*args, **kwargs):
return decorator


BASE_URL = environ.get("CHATGPT_BASE_URL") or "https://bypass.churchless.tech/"
BASE_URL = environ.get("CHATGPT_BASE_URL") or "https://bypass.churchless.tech/" # "https://chat.openai.com/backend-api/"
acheong08 marked this conversation as resolved.
Show resolved Hide resolved

bcolors = t.Colors()

Expand All @@ -99,7 +100,6 @@ def __init__(
config: dict[str, str],
conversation_id: str | None = None,
parent_id: str | None = None,
session_client=None,
lazy_loading: bool = True,
base_url: str | None = None,
) -> None:
Expand All @@ -116,7 +116,6 @@ def __init__(
More details on these are available at https://github.com/acheong08/ChatGPT#configuration
conversation_id (str | None, optional): Id of the conversation to continue on. Defaults to None.
parent_id (str | None, optional): Id of the previous response message to continue on. Defaults to None.
session_client (_type_, optional): _description_. Defaults to None.

Raises:
Exception: _description_
Expand All @@ -134,7 +133,8 @@ def __init__(
self.cache_path = Path(user_home, ".config", "revChatGPT", "cache.json")

self.config = config
self.session = session_client() if session_client else requests.Session()
self.session = requests.Session()

if "email" in config and "password" in config:
try:
cached_access_token = self.__get_cached_access_token(
Expand All @@ -155,14 +155,7 @@ def __init__(
"http": config["proxy"],
"https": config["proxy"],
}
if isinstance(self.session, AsyncClient):
proxies = {
"http://": config["proxy"],
"https://": config["proxy"],
}
self.session = AsyncClient(proxies=proxies) # type: ignore
else:
self.session.proxies.update(proxies)
self.session.proxies.update(proxies)

self.conversation_id = conversation_id
self.parent_id = parent_id
Expand Down Expand Up @@ -347,18 +340,24 @@ def __send_request(

self.conversation_id_prev_queue.append(cid)
self.parent_id_prev_queue.append(pid)
response = self.session.post(
url=f"{self.base_url}conversation",
data=json.dumps(data),
timeout=timeout,
stream=True,
)

conversation_stream = self.handle_conversation_stream(step=1)

with open(conversation_stream.name, "wb") as response_file:
response = self.session.post(
url=f"{self.base_url}conversation",
data=json.dumps(data),
timeout=timeout,
impersonate='chrome110',
content_callback=response_file.write, # a hack around curl_cffi not supporting stream=True
)
self.__check_response(response)

finish_details = None
for line in response.iter_lines():
# remove b' and ' at the beginning and end and ignore case
line = str(line)[2:-1]

response_lst = self.handle_conversation_stream(file=conversation_stream, step=2)

for line in response_lst:
if line.lower() == "internal server error":
log.error(f"Internal Server Error: {line}")
error = t.Error(
Expand All @@ -374,9 +373,12 @@ def __send_request(
if line == "[DONE]":
break

"""
# this seems to just cut off parts of some messages
line = line.replace('\\"', '"')
line = line.replace("\\'", "'")
line = line.replace("\\\\", "\\")
"""

try:
line = json.loads(line)
Expand Down Expand Up @@ -692,7 +694,7 @@ def get_conversations(
:param limit: Integer
"""
url = f"{self.base_url}conversations?offset={offset}&limit={limit}"
response = self.session.get(url)
response = self.session.get(url, impersonate='chrome110')
self.__check_response(response)
if encoding is not None:
response.encoding = encoding
Expand All @@ -707,7 +709,7 @@ def get_msg_history(self, convo_id: str, encoding: str | None = None) -> list:
:param encoding: String
"""
url = f"{self.base_url}conversation/{convo_id}"
response = self.session.get(url)
response = self.session.get(url, impersonate='chrome110')
self.__check_response(response)
if encoding is not None:
response.encoding = encoding
Expand All @@ -723,6 +725,7 @@ def gen_title(self, convo_id: str, message_id: str) -> str:
data=json.dumps(
{"message_id": message_id, "model": "text-davinci-002-render"},
),
impersonate='chrome110'
)
self.__check_response(response)
return response.json().get("title", "Error generating title")
Expand All @@ -735,7 +738,7 @@ def change_title(self, convo_id: str, title: str) -> None:
:param title: String
"""
url = f"{self.base_url}conversation/{convo_id}"
response = self.session.patch(url, data=json.dumps({"title": title}))
response = self.session.patch(url, data=json.dumps({"title": title}), impersonate='chrome110')
self.__check_response(response)

@logger(is_timed=True)
Expand All @@ -745,7 +748,7 @@ def delete_conversation(self, convo_id: str) -> None:
:param id: UUID of conversation
"""
url = f"{self.base_url}conversation/{convo_id}"
response = self.session.patch(url, data='{"is_visible": false}')
response = self.session.patch(url, data='{"is_visible": false}', impersonate='chrome110')
self.__check_response(response)

@logger(is_timed=True)
Expand All @@ -754,7 +757,7 @@ def clear_conversations(self) -> None:
Delete all conversations
"""
url = f"{self.base_url}conversations"
response = self.session.patch(url, data='{"is_visible": false}')
response = self.session.patch(url, data='{"is_visible": false}', impersonate='chrome110')
self.__check_response(response)

@logger(is_timed=False)
Expand Down Expand Up @@ -788,7 +791,7 @@ def rollback_conversation(self, num: int = 1) -> None:
@logger(is_timed=True)
def get_plugins(self, offset: int = 0, limit: int = 250, status: str = "approved"):
url = f"{self.base_url}aip/p?offset={offset}&limit={limit}&statuses={status}"
response = self.session.get(url)
response = self.session.get(url, impersonate='chrome110')
self.__check_response(response)
# Parse as JSON
return json.loads(response.text)
Expand All @@ -797,9 +800,19 @@ def get_plugins(self, offset: int = 0, limit: int = 250, status: str = "approved
def install_plugin(self, plugin_id: str):
url = f"{self.base_url}aip/p/{plugin_id}/user-settings"
payload = {"is_installed": True}
response = self.session.patch(url, data=json.dumps(payload))
response = self.session.patch(url, data=json.dumps(payload), impersonate='chrome110')
self.__check_response(response)

@logger(is_timed=False)
def handle_conversation_stream(self, file = None, step: int = 1):
if step == 1:
return tempfile.NamedTemporaryFile(delete=False)
elif step == 2 and file:
with open(file.name, "r") as response_file:
response_lst = response_file.read().splitlines()
file.close()
Path(file.name).unlink()
return response_lst

class AsyncChatbot(Chatbot):
"""Async Chatbot class for ChatGPT"""
Expand All @@ -818,10 +831,14 @@ def __init__(
config=config,
conversation_id=conversation_id,
parent_id=parent_id,
session_client=AsyncClient,
base_url=base_url,
)

# overwrite inherited normal session with async
headers_transfer = self.session.headers
self.session = requests.AsyncSession()
self.session.headers = headers_transfer

async def __send_request(
self,
data: dict,
Expand All @@ -836,27 +853,53 @@ async def __send_request(

finish_details = None
response = None
async with self.session.stream(
method="POST",
url=f"{self.base_url}conversation",
data=json.dumps(data),
timeout=timeout,
) as response:

conversation_stream = self.handle_conversation_stream(step=1)

async with self.session as s:
with open(conversation_stream.name, "wb") as response_file:
response = await s.post(
url=f"{self.base_url}conversation",
data=json.dumps(data),
timeout=timeout,
impersonate='chrome110',
content_callback=response_file.write,
)
await self.__check_response(response)
async for line in response.aiter_lines():

response_lst = self.handle_conversation_stream(file=conversation_stream, step=2)

for line in response_lst:
if line.lower() == "internal server error":
log.error(f"Internal Server Error: {line}")
error = t.Error(
source="ask",
message="Internal Server Error",
code=t.ErrorType.SERVER_ERROR,
)
raise error
if not line or line is None:
continue
if "data: " in line:
line = line[6:]
if "[DONE]" in line:
break

"""
# this seems to just cut off parts of some messages
line = line.replace('\\"', '"')
line = line.replace("\\'", "'")
line = line.replace("\\\\", "\\")
"""

try:
line = json.loads(line)
except json.decoder.JSONDecodeError:
continue
if not self.__check_fields(line):
raise ValueError(f"Field missing. Details: {str(line)}")
if line.get("message").get("author").get("role") != "assistant":
continue

message: str = line["message"]["content"]["parts"][0]
cid = line["conversation_id"]
Expand Down Expand Up @@ -1094,7 +1137,7 @@ async def get_conversations(self, offset: int = 0, limit: int = 20) -> list:
:param limit: Integer
"""
url = f"{self.base_url}conversations?offset={offset}&limit={limit}"
response = await self.session.get(url)
response = await self.session.get(url, impersonate='chrome110')
await self.__check_response(response)
data = json.loads(response.text)
return data["items"]
Expand All @@ -1109,7 +1152,7 @@ async def get_msg_history(
:param id: UUID of conversation
"""
url = f"{self.base_url}conversation/{convo_id}"
response = await self.session.get(url)
response = await self.session.get(url, impersonate='chrome110')
if encoding is not None:
response.encoding = encoding
await self.__check_response(response)
Expand All @@ -1125,7 +1168,7 @@ async def gen_title(self, convo_id: str, message_id: str) -> None:
url,
data=json.dumps(
{"message_id": message_id, "model": "text-davinci-002-render"},
),
), impersonate='chrome110'
)
await self.__check_response(response)

Expand All @@ -1136,7 +1179,7 @@ async def change_title(self, convo_id: str, title: str) -> None:
:param title: String
"""
url = f"{self.base_url}conversation/{convo_id}"
response = await self.session.patch(url, data=f'{{"title": "{title}"}}')
response = await self.session.patch(url, data=f'{{"title": "{title}"}}', impersonate='chrome110')
await self.__check_response(response)

async def delete_conversation(self, convo_id: str) -> None:
Expand All @@ -1145,15 +1188,15 @@ async def delete_conversation(self, convo_id: str) -> None:
:param convo_id: UUID of conversation
"""
url = f"{self.base_url}conversation/{convo_id}"
response = await self.session.patch(url, data='{"is_visible": false}')
response = await self.session.patch(url, data='{"is_visible": false}', impersonate='chrome110')
await self.__check_response(response)

async def clear_conversations(self) -> None:
"""
Delete all conversations
"""
url = f"{self.base_url}conversations"
response = await self.session.patch(url, data='{"is_visible": false}')
response = await self.session.patch(url, data='{"is_visible": false}', impersonate='chrome110')
await self.__check_response(response)

async def __map_conversations(self) -> None:
Expand Down Expand Up @@ -1294,8 +1337,7 @@ def handle_commands(command: str) -> bool:
log.exception("Please include plugin name in command")
print("Please include plugin name in command")
elif command == "!exit":
if isinstance(chatbot.session, httpx.AsyncClient):
chatbot.session.aclose()
chatbot.session.close()
exit()
else:
return False
Expand Down