gemini-webapi 1.17.3__py3-none-any.whl → 1.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gemini_webapi/client.py +663 -306
- gemini_webapi/components/gem_mixin.py +46 -23
- gemini_webapi/constants.py +12 -8
- gemini_webapi/types/candidate.py +2 -0
- gemini_webapi/types/image.py +8 -7
- gemini_webapi/types/modeloutput.py +8 -0
- gemini_webapi/utils/__init__.py +1 -6
- gemini_webapi/utils/decorators.py +75 -30
- gemini_webapi/utils/get_access_token.py +55 -34
- gemini_webapi/utils/parsing.py +207 -37
- gemini_webapi/utils/rotate_1psidts.py +40 -21
- gemini_webapi/utils/upload_file.py +51 -18
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/METADATA +52 -10
- gemini_webapi-1.18.1.dist-info/RECORD +25 -0
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/WHEEL +1 -1
- gemini_webapi-1.17.3.dist-info/RECORD +0 -25
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/licenses/LICENSE +0 -0
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ import orjson as json
|
|
|
5
5
|
from ..constants import GRPC
|
|
6
6
|
from ..exceptions import APIError
|
|
7
7
|
from ..types import Gem, GemJar, RPCData
|
|
8
|
-
from ..utils import
|
|
8
|
+
from ..utils import extract_json_from_response, get_nested_value, logger
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class GemMixin:
|
|
@@ -41,8 +41,9 @@ class GemMixin:
|
|
|
41
41
|
|
|
42
42
|
return self._gems
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
async def fetch_gems(
|
|
45
|
+
self, include_hidden: bool = False, language: str = "en", **kwargs
|
|
46
|
+
) -> GemJar:
|
|
46
47
|
"""
|
|
47
48
|
Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
|
|
48
49
|
|
|
@@ -54,6 +55,8 @@ class GemMixin:
|
|
|
54
55
|
include_hidden: `bool`, optional
|
|
55
56
|
There are some predefined gems that by default are not shown to users (and therefore may not work properly).
|
|
56
57
|
Set this parameter to `True` to include them in the fetched gem list.
|
|
58
|
+
language: `str`, optional
|
|
59
|
+
Language code for the gems to fetch. Default is 'en'.
|
|
57
60
|
|
|
58
61
|
Returns
|
|
59
62
|
-------
|
|
@@ -65,12 +68,16 @@ class GemMixin:
|
|
|
65
68
|
[
|
|
66
69
|
RPCData(
|
|
67
70
|
rpcid=GRPC.LIST_GEMS,
|
|
68
|
-
payload=
|
|
71
|
+
payload=(
|
|
72
|
+
f"[4,['{language}'],0]"
|
|
73
|
+
if include_hidden
|
|
74
|
+
else f"[3,['{language}'],0]"
|
|
75
|
+
),
|
|
69
76
|
identifier="system",
|
|
70
77
|
),
|
|
71
78
|
RPCData(
|
|
72
79
|
rpcid=GRPC.LIST_GEMS,
|
|
73
|
-
payload="[2]",
|
|
80
|
+
payload=f"[2,['{language}'],0]",
|
|
74
81
|
identifier="custom",
|
|
75
82
|
),
|
|
76
83
|
],
|
|
@@ -78,24 +85,32 @@ class GemMixin:
|
|
|
78
85
|
)
|
|
79
86
|
|
|
80
87
|
try:
|
|
81
|
-
response_json =
|
|
88
|
+
response_json = extract_json_from_response(response.text)
|
|
82
89
|
|
|
83
90
|
predefined_gems, custom_gems = [], []
|
|
84
91
|
|
|
85
92
|
for part in response_json:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
93
|
+
try:
|
|
94
|
+
identifier = get_nested_value(part, [-1])
|
|
95
|
+
part_body_str = get_nested_value(part, [2])
|
|
96
|
+
if not part_body_str:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
part_body = json.loads(part_body_str)
|
|
100
|
+
if identifier == "system":
|
|
101
|
+
predefined_gems = get_nested_value(part_body, [2], [])
|
|
102
|
+
elif identifier == "custom":
|
|
103
|
+
custom_gems = get_nested_value(part_body, [2], [])
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
continue
|
|
91
106
|
|
|
92
107
|
if not predefined_gems and not custom_gems:
|
|
93
108
|
raise Exception
|
|
94
109
|
except Exception:
|
|
95
110
|
await self.close()
|
|
96
|
-
logger.debug(f"
|
|
111
|
+
logger.debug(f"Unexpected response data structure: {response.text}")
|
|
97
112
|
raise APIError(
|
|
98
|
-
"Failed to fetch gems.
|
|
113
|
+
"Failed to fetch gems. Unexpected response data structure. Client will try to re-initialize on next request."
|
|
99
114
|
)
|
|
100
115
|
|
|
101
116
|
self._gems = GemJar(
|
|
@@ -131,7 +146,6 @@ class GemMixin:
|
|
|
131
146
|
|
|
132
147
|
return self._gems
|
|
133
148
|
|
|
134
|
-
@running(retry=2)
|
|
135
149
|
async def create_gem(self, name: str, prompt: str, description: str = "") -> Gem:
|
|
136
150
|
"""
|
|
137
151
|
Create a new custom gem.
|
|
@@ -175,19 +189,26 @@ class GemMixin:
|
|
|
175
189
|
[],
|
|
176
190
|
]
|
|
177
191
|
]
|
|
178
|
-
).decode(),
|
|
192
|
+
).decode("utf-8"),
|
|
179
193
|
)
|
|
180
194
|
]
|
|
181
195
|
)
|
|
182
196
|
|
|
183
197
|
try:
|
|
184
|
-
response_json =
|
|
185
|
-
|
|
198
|
+
response_json = extract_json_from_response(response.text)
|
|
199
|
+
part_body_str = get_nested_value(response_json, [0, 2], verbose=True)
|
|
200
|
+
if not part_body_str:
|
|
201
|
+
raise Exception
|
|
202
|
+
|
|
203
|
+
part_body = json.loads(part_body_str)
|
|
204
|
+
gem_id = get_nested_value(part_body, [0], verbose=True)
|
|
205
|
+
if not gem_id:
|
|
206
|
+
raise Exception
|
|
186
207
|
except Exception:
|
|
187
208
|
await self.close()
|
|
188
|
-
logger.debug(f"
|
|
209
|
+
logger.debug(f"Unexpected response data structure: {response.text}")
|
|
189
210
|
raise APIError(
|
|
190
|
-
"Failed to create gem.
|
|
211
|
+
"Failed to create gem. Unexpected response data structure. Client will try to re-initialize on next request."
|
|
191
212
|
)
|
|
192
213
|
|
|
193
214
|
return Gem(
|
|
@@ -198,7 +219,6 @@ class GemMixin:
|
|
|
198
219
|
predefined=False,
|
|
199
220
|
)
|
|
200
221
|
|
|
201
|
-
@running(retry=2)
|
|
202
222
|
async def update_gem(
|
|
203
223
|
self, gem: Gem | str, name: str, prompt: str, description: str = ""
|
|
204
224
|
) -> Gem:
|
|
@@ -253,7 +273,7 @@ class GemMixin:
|
|
|
253
273
|
0,
|
|
254
274
|
],
|
|
255
275
|
]
|
|
256
|
-
).decode(),
|
|
276
|
+
).decode("utf-8"),
|
|
257
277
|
)
|
|
258
278
|
]
|
|
259
279
|
)
|
|
@@ -266,7 +286,6 @@ class GemMixin:
|
|
|
266
286
|
predefined=False,
|
|
267
287
|
)
|
|
268
288
|
|
|
269
|
-
@running(retry=2)
|
|
270
289
|
async def delete_gem(self, gem: Gem | str, **kwargs) -> None:
|
|
271
290
|
"""
|
|
272
291
|
Delete a custom gem.
|
|
@@ -283,6 +302,10 @@ class GemMixin:
|
|
|
283
302
|
gem_id = gem
|
|
284
303
|
|
|
285
304
|
await self._batch_execute(
|
|
286
|
-
[
|
|
305
|
+
[
|
|
306
|
+
RPCData(
|
|
307
|
+
rpcid=GRPC.DELETE_GEM, payload=json.dumps([gem_id]).decode("utf-8")
|
|
308
|
+
)
|
|
309
|
+
],
|
|
287
310
|
**kwargs,
|
|
288
311
|
)
|
gemini_webapi/constants.py
CHANGED
|
@@ -18,6 +18,7 @@ class GRPC(StrEnum):
|
|
|
18
18
|
# Chat methods
|
|
19
19
|
LIST_CHATS = "MaZiqc"
|
|
20
20
|
READ_CHAT = "hNvQHb"
|
|
21
|
+
DELETE_CHAT = "GzXR5e"
|
|
21
22
|
|
|
22
23
|
# Gem methods
|
|
23
24
|
LIST_GEMS = "CNgdBe"
|
|
@@ -25,6 +26,9 @@ class GRPC(StrEnum):
|
|
|
25
26
|
UPDATE_GEM = "kHv0Vd"
|
|
26
27
|
DELETE_GEM = "UXcSJb"
|
|
27
28
|
|
|
29
|
+
# Activity methods
|
|
30
|
+
BARD_ACTIVITY = "ESY5D"
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
class Headers(Enum):
|
|
30
34
|
GEMINI = {
|
|
@@ -32,7 +36,7 @@ class Headers(Enum):
|
|
|
32
36
|
"Host": "gemini.google.com",
|
|
33
37
|
"Origin": "https://gemini.google.com",
|
|
34
38
|
"Referer": "https://gemini.google.com/",
|
|
35
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
39
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
36
40
|
"X-Same-Domain": "1",
|
|
37
41
|
}
|
|
38
42
|
ROTATE_COOKIES = {
|
|
@@ -46,21 +50,21 @@ class Model(Enum):
|
|
|
46
50
|
G_3_0_PRO = (
|
|
47
51
|
"gemini-3.0-pro",
|
|
48
52
|
{
|
|
49
|
-
"x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]'
|
|
53
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4],null,null,1]'
|
|
50
54
|
},
|
|
51
55
|
False,
|
|
52
56
|
)
|
|
53
|
-
|
|
54
|
-
"gemini-
|
|
57
|
+
G_3_0_FLASH = (
|
|
58
|
+
"gemini-3.0-flash",
|
|
55
59
|
{
|
|
56
|
-
"x-goog-ext-525001261-jspb": '[1,null,null,null,"
|
|
60
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]'
|
|
57
61
|
},
|
|
58
62
|
False,
|
|
59
63
|
)
|
|
60
|
-
|
|
61
|
-
"gemini-
|
|
64
|
+
G_3_0_FLASH_THINKING = (
|
|
65
|
+
"gemini-3.0-flash-thinking",
|
|
62
66
|
{
|
|
63
|
-
"x-goog-ext-525001261-jspb": '[1,null,null,null,"
|
|
67
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]'
|
|
64
68
|
},
|
|
65
69
|
False,
|
|
66
70
|
)
|
gemini_webapi/types/candidate.py
CHANGED
gemini_webapi/types/image.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
|
-
from httpx import AsyncClient, HTTPError
|
|
6
|
+
from httpx import AsyncClient, Cookies, HTTPError
|
|
6
7
|
from pydantic import BaseModel, field_validator
|
|
7
8
|
|
|
8
9
|
from ..utils import logger
|
|
@@ -39,7 +40,7 @@ class Image(BaseModel):
|
|
|
39
40
|
self,
|
|
40
41
|
path: str = "temp",
|
|
41
42
|
filename: str | None = None,
|
|
42
|
-
cookies: dict | None = None,
|
|
43
|
+
cookies: dict | Cookies | None = None,
|
|
43
44
|
verbose: bool = False,
|
|
44
45
|
skip_invalid_filename: bool = False,
|
|
45
46
|
) -> str | None:
|
|
@@ -81,7 +82,7 @@ class Image(BaseModel):
|
|
|
81
82
|
return None
|
|
82
83
|
|
|
83
84
|
async with AsyncClient(
|
|
84
|
-
follow_redirects=True, cookies=cookies, proxy=self.proxy
|
|
85
|
+
http2=True, follow_redirects=True, cookies=cookies, proxy=self.proxy
|
|
85
86
|
) as client:
|
|
86
87
|
response = await client.get(self.url)
|
|
87
88
|
if response.status_code == 200:
|
|
@@ -121,16 +122,16 @@ class GeneratedImage(Image):
|
|
|
121
122
|
|
|
122
123
|
Parameters
|
|
123
124
|
----------
|
|
124
|
-
cookies: `dict`
|
|
125
|
+
cookies: `dict | httpx.Cookies`
|
|
125
126
|
Cookies used for requesting the content of the generated image, inherit from GeminiClient object or manually set.
|
|
126
127
|
Should contain valid "__Secure-1PSID" and "__Secure-1PSIDTS" values.
|
|
127
128
|
"""
|
|
128
129
|
|
|
129
|
-
cookies:
|
|
130
|
+
cookies: Any
|
|
130
131
|
|
|
131
132
|
@field_validator("cookies")
|
|
132
133
|
@classmethod
|
|
133
|
-
def validate_cookies(cls, v:
|
|
134
|
+
def validate_cookies(cls, v: Any) -> Any:
|
|
134
135
|
if len(v) == 0:
|
|
135
136
|
raise ValueError(
|
|
136
137
|
"GeneratedImage is designed to be initialized with same cookies as GeminiClient."
|
|
@@ -142,7 +143,7 @@ class GeneratedImage(Image):
|
|
|
142
143
|
self,
|
|
143
144
|
path: str = "temp",
|
|
144
145
|
filename: str | None = None,
|
|
145
|
-
cookies: dict | None = None,
|
|
146
|
+
cookies: dict | Cookies | None = None,
|
|
146
147
|
verbose: bool = False,
|
|
147
148
|
skip_invalid_filename: bool = False,
|
|
148
149
|
full_size: bool = True,
|
|
@@ -32,10 +32,18 @@ class ModelOutput(BaseModel):
|
|
|
32
32
|
def text(self) -> str:
|
|
33
33
|
return self.candidates[self.chosen].text
|
|
34
34
|
|
|
35
|
+
@property
|
|
36
|
+
def text_delta(self) -> str:
|
|
37
|
+
return self.candidates[self.chosen].text_delta or ""
|
|
38
|
+
|
|
35
39
|
@property
|
|
36
40
|
def thoughts(self) -> str | None:
|
|
37
41
|
return self.candidates[self.chosen].thoughts
|
|
38
42
|
|
|
43
|
+
@property
|
|
44
|
+
def thoughts_delta(self) -> str:
|
|
45
|
+
return self.candidates[self.chosen].thoughts_delta or ""
|
|
46
|
+
|
|
39
47
|
@property
|
|
40
48
|
def images(self) -> list[Image]:
|
|
41
49
|
return self.candidates[self.chosen].images
|
gemini_webapi/utils/__init__.py
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# flake8: noqa
|
|
2
2
|
|
|
3
|
-
from asyncio import Task
|
|
4
|
-
|
|
5
3
|
from .decorators import running
|
|
6
4
|
from .get_access_token import get_access_token
|
|
7
5
|
from .load_browser_cookies import load_browser_cookies
|
|
8
6
|
from .logger import logger, set_log_level
|
|
9
|
-
from .parsing import extract_json_from_response, get_nested_value
|
|
7
|
+
from .parsing import extract_json_from_response, get_nested_value, parse_stream_frames
|
|
10
8
|
from .rotate_1psidts import rotate_1psidts
|
|
11
9
|
from .upload_file import upload_file, parse_file_name
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
rotate_tasks: dict[str, Task] = {}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import functools
|
|
3
|
+
import inspect
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
|
|
5
|
-
from ..exceptions import APIError
|
|
6
|
+
from ..exceptions import APIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DELAY_FACTOR = 5
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
def running(retry: int = 0) -> Callable:
|
|
9
13
|
"""
|
|
10
14
|
Decorator to check if GeminiClient is running before making a request.
|
|
15
|
+
Supports both regular async functions and async generators.
|
|
11
16
|
|
|
12
17
|
Parameters
|
|
13
18
|
----------
|
|
@@ -16,38 +21,78 @@ def running(retry: int = 0) -> Callable:
|
|
|
16
21
|
"""
|
|
17
22
|
|
|
18
23
|
def decorator(func):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
if inspect.isasyncgenfunction(func):
|
|
25
|
+
|
|
26
|
+
@functools.wraps(func)
|
|
27
|
+
async def wrapper(client, *args, current_retry=None, **kwargs):
|
|
28
|
+
if current_retry is None:
|
|
29
|
+
current_retry = retry
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
if not client._running:
|
|
33
|
+
await client.init(
|
|
34
|
+
timeout=client.timeout,
|
|
35
|
+
auto_close=client.auto_close,
|
|
36
|
+
close_delay=client.close_delay,
|
|
37
|
+
auto_refresh=client.auto_refresh,
|
|
38
|
+
refresh_interval=client.refresh_interval,
|
|
39
|
+
verbose=client.verbose,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not client._running:
|
|
43
|
+
raise APIError(
|
|
44
|
+
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async for item in func(client, *args, **kwargs):
|
|
48
|
+
yield item
|
|
49
|
+
except APIError:
|
|
50
|
+
if current_retry > 0:
|
|
51
|
+
delay = (retry - current_retry + 1) * DELAY_FACTOR
|
|
52
|
+
await asyncio.sleep(delay)
|
|
53
|
+
async for item in wrapper(
|
|
54
|
+
client, *args, current_retry=current_retry - 1, **kwargs
|
|
55
|
+
):
|
|
56
|
+
yield item
|
|
57
|
+
else:
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
else:
|
|
62
|
+
|
|
63
|
+
@functools.wraps(func)
|
|
64
|
+
async def wrapper(client, *args, current_retry=None, **kwargs):
|
|
65
|
+
if current_retry is None:
|
|
66
|
+
current_retry = retry
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if not client._running:
|
|
70
|
+
await client.init(
|
|
71
|
+
timeout=client.timeout,
|
|
72
|
+
auto_close=client.auto_close,
|
|
73
|
+
close_delay=client.close_delay,
|
|
74
|
+
auto_refresh=client.auto_refresh,
|
|
75
|
+
refresh_interval=client.refresh_interval,
|
|
76
|
+
verbose=client.verbose,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not client._running:
|
|
80
|
+
raise APIError(
|
|
81
|
+
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
82
|
+
)
|
|
83
|
+
|
|
39
84
|
return await func(client, *args, **kwargs)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
85
|
+
except APIError:
|
|
86
|
+
if current_retry > 0:
|
|
87
|
+
delay = (retry - current_retry + 1) * DELAY_FACTOR
|
|
88
|
+
await asyncio.sleep(delay)
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
90
|
+
return await wrapper(
|
|
91
|
+
client, *args, current_retry=current_retry - 1, **kwargs
|
|
92
|
+
)
|
|
48
93
|
|
|
49
|
-
|
|
94
|
+
raise
|
|
50
95
|
|
|
51
|
-
|
|
96
|
+
return wrapper
|
|
52
97
|
|
|
53
98
|
return decorator
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
from asyncio import Task
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from httpx import AsyncClient, Response
|
|
7
|
+
from httpx import AsyncClient, Cookies, Response
|
|
8
8
|
|
|
9
9
|
from ..constants import Endpoint, Headers
|
|
10
10
|
from ..exceptions import AuthError
|
|
@@ -13,27 +13,30 @@ from .logger import logger
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
async def send_request(
|
|
16
|
-
cookies: dict, proxy: str | None = None
|
|
17
|
-
) -> tuple[Response | None,
|
|
16
|
+
cookies: dict | Cookies, proxy: str | None = None
|
|
17
|
+
) -> tuple[Response | None, Cookies]:
|
|
18
18
|
"""
|
|
19
19
|
Send http request with provided cookies.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
async with AsyncClient(
|
|
23
|
+
http2=True,
|
|
23
24
|
proxy=proxy,
|
|
24
25
|
headers=Headers.GEMINI.value,
|
|
25
26
|
cookies=cookies,
|
|
26
27
|
follow_redirects=True,
|
|
27
|
-
verify=False,
|
|
28
28
|
) as client:
|
|
29
|
-
response = await client.get(Endpoint.INIT
|
|
29
|
+
response = await client.get(Endpoint.INIT)
|
|
30
30
|
response.raise_for_status()
|
|
31
|
-
return response, cookies
|
|
31
|
+
return response, client.cookies
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
async def get_access_token(
|
|
35
|
-
base_cookies: dict
|
|
36
|
-
|
|
35
|
+
base_cookies: dict | Cookies,
|
|
36
|
+
proxy: str | None = None,
|
|
37
|
+
verbose: bool = False,
|
|
38
|
+
verify: bool = True,
|
|
39
|
+
) -> tuple[str, Cookies, str | None, str | None]:
|
|
37
40
|
"""
|
|
38
41
|
Send a get request to gemini.google.com for each group of available cookies and return
|
|
39
42
|
the value of "SNlM0e" as access token on the first successful request.
|
|
@@ -45,19 +48,19 @@ async def get_access_token(
|
|
|
45
48
|
|
|
46
49
|
Parameters
|
|
47
50
|
----------
|
|
48
|
-
base_cookies : `dict`
|
|
51
|
+
base_cookies : `dict | httpx.Cookies`
|
|
49
52
|
Base cookies to be used in the request.
|
|
50
53
|
proxy: `str`, optional
|
|
51
54
|
Proxy URL.
|
|
52
55
|
verbose: `bool`, optional
|
|
53
56
|
If `True`, will print more infomation in logs.
|
|
57
|
+
verify: `bool`, optional
|
|
58
|
+
Whether to verify SSL certificates.
|
|
54
59
|
|
|
55
60
|
Returns
|
|
56
61
|
-------
|
|
57
|
-
`str`
|
|
58
|
-
|
|
59
|
-
`dict`
|
|
60
|
-
Cookies of the successful request.
|
|
62
|
+
`tuple[str, str | None, str | None, Cookies]`
|
|
63
|
+
By order: access token; build label; session id; cookies of the successful request.
|
|
61
64
|
|
|
62
65
|
Raises
|
|
63
66
|
------
|
|
@@ -65,18 +68,23 @@ async def get_access_token(
|
|
|
65
68
|
If all requests failed.
|
|
66
69
|
"""
|
|
67
70
|
|
|
68
|
-
async with AsyncClient(
|
|
69
|
-
|
|
71
|
+
async with AsyncClient(
|
|
72
|
+
http2=True, proxy=proxy, follow_redirects=True, verify=verify
|
|
73
|
+
) as client:
|
|
74
|
+
response = await client.get(Endpoint.GOOGLE)
|
|
70
75
|
|
|
71
|
-
extra_cookies =
|
|
76
|
+
extra_cookies = Cookies()
|
|
72
77
|
if response.status_code == 200:
|
|
73
78
|
extra_cookies = response.cookies
|
|
74
79
|
|
|
75
80
|
tasks = []
|
|
76
81
|
|
|
77
82
|
# Base cookies passed directly on initializing client
|
|
83
|
+
# We use a Jar to merge extra_cookies and base_cookies safely (preserving domains)
|
|
78
84
|
if "__Secure-1PSID" in base_cookies and "__Secure-1PSIDTS" in base_cookies:
|
|
79
|
-
|
|
85
|
+
jar = Cookies(extra_cookies)
|
|
86
|
+
jar.update(base_cookies)
|
|
87
|
+
tasks.append(Task(send_request(jar, proxy=proxy)))
|
|
80
88
|
elif verbose:
|
|
81
89
|
logger.debug(
|
|
82
90
|
"Skipping loading base cookies. Either __Secure-1PSID or __Secure-1PSIDTS is not provided."
|
|
@@ -88,18 +96,25 @@ async def get_access_token(
|
|
|
88
96
|
and Path(GEMINI_COOKIE_PATH)
|
|
89
97
|
or (Path(__file__).parent / "temp")
|
|
90
98
|
)
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
|
|
100
|
+
# Safely get __Secure-1PSID value
|
|
101
|
+
if isinstance(base_cookies, Cookies):
|
|
102
|
+
secure_1psid = base_cookies.get(
|
|
103
|
+
"__Secure-1PSID", domain=".google.com"
|
|
104
|
+
) or base_cookies.get("__Secure-1PSID")
|
|
105
|
+
else:
|
|
106
|
+
secure_1psid = base_cookies.get("__Secure-1PSID")
|
|
107
|
+
|
|
108
|
+
if secure_1psid:
|
|
109
|
+
filename = f".cached_1psidts_{secure_1psid}.txt"
|
|
93
110
|
cache_file = cache_dir / filename
|
|
94
111
|
if cache_file.is_file():
|
|
95
112
|
cached_1psidts = cache_file.read_text()
|
|
96
113
|
if cached_1psidts:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
tasks.append(Task(send_request(cached_cookies, proxy=proxy)))
|
|
114
|
+
jar = Cookies(extra_cookies)
|
|
115
|
+
jar.update(base_cookies)
|
|
116
|
+
jar.set("__Secure-1PSIDTS", cached_1psidts, domain=".google.com")
|
|
117
|
+
tasks.append(Task(send_request(jar, proxy=proxy)))
|
|
103
118
|
elif verbose:
|
|
104
119
|
logger.debug("Skipping loading cached cookies. Cache file is empty.")
|
|
105
120
|
elif verbose:
|
|
@@ -110,12 +125,11 @@ async def get_access_token(
|
|
|
110
125
|
for cache_file in cache_files:
|
|
111
126
|
cached_1psidts = cache_file.read_text()
|
|
112
127
|
if cached_1psidts:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
tasks.append(Task(send_request(cached_cookies, proxy=proxy)))
|
|
128
|
+
jar = Cookies(extra_cookies)
|
|
129
|
+
psid = cache_file.stem[16:]
|
|
130
|
+
jar.set("__Secure-1PSID", psid, domain=".google.com")
|
|
131
|
+
jar.set("__Secure-1PSIDTS", cached_1psidts, domain=".google.com")
|
|
132
|
+
tasks.append(Task(send_request(jar, proxy=proxy)))
|
|
119
133
|
valid_caches += 1
|
|
120
134
|
|
|
121
135
|
if valid_caches == 0 and verbose:
|
|
@@ -174,13 +188,20 @@ async def get_access_token(
|
|
|
174
188
|
for i, future in enumerate(asyncio.as_completed(tasks)):
|
|
175
189
|
try:
|
|
176
190
|
response, request_cookies = await future
|
|
177
|
-
|
|
178
|
-
if
|
|
191
|
+
snlm0e = re.search(r'"SNlM0e":\s*"(.*?)"', response.text)
|
|
192
|
+
if snlm0e:
|
|
193
|
+
cfb2h = re.search(r'"cfb2h":\s*"(.*?)"', response.text)
|
|
194
|
+
fdrfje = re.search(r'"FdrFJe":\s*"(.*?)"', response.text)
|
|
179
195
|
if verbose:
|
|
180
196
|
logger.debug(
|
|
181
197
|
f"Init attempt ({i + 1}/{len(tasks)}) succeeded. Initializing client..."
|
|
182
198
|
)
|
|
183
|
-
return
|
|
199
|
+
return (
|
|
200
|
+
snlm0e.group(1),
|
|
201
|
+
cfb2h.group(1) if cfb2h else None,
|
|
202
|
+
fdrfje.group(1) if fdrfje else None,
|
|
203
|
+
request_cookies,
|
|
204
|
+
)
|
|
184
205
|
elif verbose:
|
|
185
206
|
logger.debug(
|
|
186
207
|
f"Init attempt ({i + 1}/{len(tasks)}) failed. Cookies invalid."
|