diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 90558f09..96e274b1 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" cache: 'pip' - name: Install requirements run: pip install -r requirements.txt diff --git a/g4f/Provider/You.py b/g4f/Provider/You.py index 91a195cf..001f775d 100644 --- a/g4f/Provider/You.py +++ b/g4f/Provider/You.py @@ -1,40 +1,155 @@ from __future__ import annotations import json +import base64 +import uuid +from aiohttp import ClientSession, FormData -from ..requests import StreamSession -from ..typing import AsyncGenerator, Messages -from .base_provider import AsyncGeneratorProvider, format_prompt - +from ..typing import AsyncGenerator, Messages, ImageType, Cookies +from .base_provider import AsyncGeneratorProvider +from .helper import get_connector, format_prompt +from ..image import to_bytes +from ..defaults import DEFAULT_HEADERS class You(AsyncGeneratorProvider): url = "https://you.com" working = True supports_gpt_35_turbo = True - + supports_gpt_4 = True + _cookies = None + _cookies_used = 0 @classmethod async def create_async_generator( cls, model: str, messages: Messages, + image: ImageType = None, + image_name: str = None, proxy: str = None, - timeout: int = 120, + chat_mode: str = "default", **kwargs, ) -> AsyncGenerator: - async with StreamSession(proxies={"https": proxy}, impersonate="chrome107", timeout=timeout) as session: + async with ClientSession( + connector=get_connector(kwargs.get("connector"), proxy), + headers=DEFAULT_HEADERS + ) as client: + if image: + chat_mode = "agent" + elif model == "gpt-4": + chat_mode = model + cookies = await cls.get_cookies(client) if chat_mode != "default" else None + upload = json.dumps([await cls.upload_file(client, cookies, to_bytes(image), image_name)]) if image else "" + #questions = [message["content"] for message in messages if message["role"] == "user"] + # chat = [ + # {"question": questions[idx-1], "answer": message["content"]} + # for idx, message in enumerate(messages) + # if message["role"] == "assistant" + # and idx < len(questions) + # ] headers = { "Accept": "text/event-stream", "Referer": f"{cls.url}/search?fromSearchBar=true&tbm=youchat", } - data = {"q": format_prompt(messages), "domain": "youchat", "chat": ""} - async with session.get( + data = { + "userFiles": upload, + "q": format_prompt(messages), + "domain": "youchat", + "selectedChatMode": chat_mode, + #"chat": json.dumps(chat), + } + async with (client.post if chat_mode == "default" else client.get)( f"{cls.url}/api/streamingSearch", - params=data, - headers=headers + data=data, + headers=headers, + cookies=cookies ) as response: response.raise_for_status() - start = b'data: {"youChatToken": ' - async for line in response.iter_lines(): - if line.startswith(start): - yield json.loads(line[len(start):-1]) + async for line in response.content: + if line.startswith(b'event: '): + event = line[7:-1] + elif line.startswith(b'data: '): + if event == b"youChatUpdate" or event == b"youChatToken": + data = json.loads(line[6:-1]) + if event == b"youChatToken" and "youChatToken" in data: + yield data["youChatToken"] + elif event == b"youChatUpdate" and "t" in data: + yield data["t"] + + @classmethod + async def upload_file(cls, client: ClientSession, cookies: Cookies, file: bytes, filename: str = None) -> dict: + async with client.get( + f"{cls.url}/api/get_nonce", + cookies=cookies, + ) as response: + response.raise_for_status() + upload_nonce = await response.text() + data = FormData() + data.add_field('file', file, filename=filename) + async with client.post( + f"{cls.url}/api/upload", + data=data, + headers={ + "X-Upload-Nonce": upload_nonce, + }, + cookies=cookies + ) as response: + if not response.ok: + raise RuntimeError(f"Response: {await response.text()}") + result = await response.json() + result["user_filename"] = filename + result["size"] = len(file) + return result + + @classmethod + async def get_cookies(cls, client: ClientSession) -> Cookies: + if not cls._cookies or cls._cookies_used >= 5: + cls._cookies = await cls.create_cookies(client) + cls._cookies_used = 0 + cls._cookies_used += 1 + return cls._cookies + + @classmethod + def get_sdk(cls) -> str: + return base64.standard_b64encode(json.dumps({ + "event_id":f"event-id-{str(uuid.uuid4())}", + "app_session_id":f"app-session-id-{str(uuid.uuid4())}", + "persistent_id":f"persistent-id-{uuid.uuid4()}", + "client_sent_at":"","timezone":"", + "stytch_user_id":f"user-live-{uuid.uuid4()}", + "stytch_session_id":f"session-live-{uuid.uuid4()}", + "app":{"identifier":"you.com"}, + "sdk":{"identifier":"Stytch.js Javascript SDK","version":"3.3.0" + }}).encode()).decode() + + def get_auth() -> str: + auth_uuid = "507a52ad-7e69-496b-aee0-1c9863c7c8" + auth_token = f"public-token-live-{auth_uuid}bb:public-token-live-{auth_uuid}19" + auth = base64.standard_b64encode(auth_token.encode()).decode() + return f"Basic {auth}" + + @classmethod + async def create_cookies(cls, client: ClientSession) -> Cookies: + user_uuid = str(uuid.uuid4()) + async with client.post( + "https://web.stytch.com/sdk/v1/passwords", + headers={ + "Authorization": cls.get_auth(), + "X-SDK-Client": cls.get_sdk(), + "X-SDK-Parent-Host": cls.url + }, + json={ + "email": f"{user_uuid}@gmail.com", + "password": f"{user_uuid}#{user_uuid}", + "session_duration_minutes": 129600 + } + ) as response: + if not response.ok: + raise RuntimeError(f"Response: {await response.text()}") + session = (await response.json())["data"] + return { + "stytch_session": session["session_token"], + 'stytch_session_jwt': session["session_jwt"], + 'ydc_stytch_session': session["session_token"], + 'ydc_stytch_session_jwt': session["session_jwt"], + } \ No newline at end of file diff --git a/g4f/cookies.py b/g4f/cookies.py index af0e6192..a38488c2 100644 --- a/g4f/cookies.py +++ b/g4f/cookies.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import time try: from platformdirs import user_config_dir @@ -72,7 +73,8 @@ def load_cookies_from_browsers(domain_name: str, raise_requirements_error: bool print(f"Read cookies from {cookie_fn.__name__} for {domain_name}") for cookie in cookie_jar: if cookie.name not in cookies: - cookies[cookie.name] = cookie.value + if not cookie.expires or cookie.expires > time.time(): + cookies[cookie.name] = cookie.value if single_browser and len(cookie_jar): break except BrowserCookieError: diff --git a/g4f/gui/__init__.py b/g4f/gui/__init__.py index 46b4f56a..dff720ac 100644 --- a/g4f/gui/__init__.py +++ b/g4f/gui/__init__.py @@ -7,6 +7,9 @@ except ImportError: raise MissingRequirementsError('Install "flask" package for the gui') def run_gui(host: str = '0.0.0.0', port: int = 8080, debug: bool = False) -> None: + if debug: + import g4f + g4f.debug.logging = True config = { 'host' : host, 'port' : port, diff --git a/g4f/gui/client/css/style.css b/g4f/gui/client/css/style.css index e03f36d2..8752dee5 100644 --- a/g4f/gui/client/css/style.css +++ b/g4f/gui/client/css/style.css @@ -404,7 +404,7 @@ body { display: none; } -#image, #file { +#image, #file, #camera { display: none; } @@ -412,20 +412,37 @@ label[for="image"]:has(> input:valid){ color: var(--accent); } +label[for="camera"]:has(> input:valid){ + color: var(--accent); +} + label[for="file"]:has(> input:valid){ color: var(--accent); } -label[for="image"], label[for="file"] { +label[for="image"], label[for="file"], label[for="camera"] { cursor: pointer; position: absolute; top: 10px; left: 10px; } -label[for="file"] { +label[for="image"] { top: 32px; - left: 10px; +} + +label[for="camera"] { + top: 54px; +} + +label[for="camera"] { + display: none; +} + +@media (pointer:none), (pointer:coarse) { + label[for="camera"] { + display: block; + } } .buttons input[type="checkbox"] { diff --git a/g4f/gui/client/html/index.html b/g4f/gui/client/html/index.html index 55b54b48..70b8c75f 100644 --- a/g4f/gui/client/html/index.html +++ b/g4f/gui/client/html/index.html @@ -114,10 +114,14 @@