tweepy-self 1.9.0__tar.gz → 1.10.0b1__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/PKG-INFO +2 -2
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/pyproject.toml +2 -2
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/serializer.py +1 -1
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/account.py +5 -5
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/base/client.py +3 -3
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/client.py +166 -80
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/errors.py +13 -7
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/models.py +14 -24
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/README.md +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/__init__.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/__init__.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/__init__.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/base.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/config.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/enum.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/fun_captcha.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/base/__init__.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/base/session.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/enums.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/__init__.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/file.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/html.py +0 -0
- {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/other.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: tweepy-self
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.10.0b1
|
4
4
|
Summary: Twitter (selfbot) for Python!
|
5
5
|
Home-page: https://github.com/alenkimov/tweepy-self
|
6
6
|
Author: Alen
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Requires-Dist: aiohttp (>=3.9,<4.0)
|
13
13
|
Requires-Dist: beautifulsoup4 (>=4,<5)
|
14
14
|
Requires-Dist: better-proxy (>=1.1,<2.0)
|
15
|
-
Requires-Dist: curl_cffi (==0.6.
|
15
|
+
Requires-Dist: curl_cffi (==0.6.2)
|
16
16
|
Requires-Dist: loguru (>=0.7,<0.8)
|
17
17
|
Requires-Dist: lxml (>=5,<6)
|
18
18
|
Requires-Dist: pydantic (>=1)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "tweepy-self"
|
3
|
-
version = "1.
|
3
|
+
version = "1.10.0.b1"
|
4
4
|
description = "Twitter (selfbot) for Python!"
|
5
5
|
authors = ["Alen <alen.kimov@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -12,7 +12,7 @@ Source = "https://github.com/alenkimov/tweepy-self"
|
|
12
12
|
|
13
13
|
[tool.poetry.dependencies]
|
14
14
|
python = "^3.11"
|
15
|
-
curl_cffi =
|
15
|
+
curl_cffi = "0.6.2"
|
16
16
|
better-proxy = "^1.1"
|
17
17
|
beautifulsoup4 = "^4"
|
18
18
|
pydantic = ">=1"
|
@@ -45,7 +45,7 @@ class CaptchaResponseSer(ResponseSer):
|
|
45
45
|
solution: Dict[str, Any] = Field(None, description="Task result data. Different for each type of task.")
|
46
46
|
|
47
47
|
class Config:
|
48
|
-
|
48
|
+
populate_by_name = True
|
49
49
|
|
50
50
|
|
51
51
|
class ControlResponseSer(ResponseSer):
|
@@ -12,11 +12,11 @@ from .models import User
|
|
12
12
|
class Account(User):
|
13
13
|
# fmt: off
|
14
14
|
auth_token: str | None = Field(default=None, pattern=r"^[a-f0-9]{40}$")
|
15
|
-
ct0: str | None = None
|
16
|
-
password: str | None = None
|
17
|
-
email: str | None = None
|
18
|
-
totp_secret: str | None = None
|
19
|
-
backup_code: str | None = None
|
15
|
+
ct0: str | None = None # 160
|
16
|
+
password: str | None = None # 128
|
17
|
+
email: str | None = None # 254
|
18
|
+
totp_secret: str | None = None # 16
|
19
|
+
backup_code: str | None = None # 12
|
20
20
|
status: AccountStatus = AccountStatus.UNKNOWN
|
21
21
|
# fmt: on
|
22
22
|
|
@@ -2,6 +2,7 @@ from typing import Any, Literal, Iterable
|
|
2
2
|
from time import time
|
3
3
|
import asyncio
|
4
4
|
import base64
|
5
|
+
import json
|
5
6
|
import re
|
6
7
|
|
7
8
|
from loguru import logger
|
@@ -44,7 +45,6 @@ class Client(BaseHTTPClient):
|
|
44
45
|
"authority": "twitter.com",
|
45
46
|
"origin": "https://twitter.com",
|
46
47
|
"x-twitter-active-user": "yes",
|
47
|
-
# 'x-twitter-auth-type': 'OAuth2Session',
|
48
48
|
"x-twitter-client-language": "en",
|
49
49
|
}
|
50
50
|
_GRAPHQL_URL = "https://twitter.com/i/api/graphql"
|
@@ -84,6 +84,8 @@ class Client(BaseHTTPClient):
|
|
84
84
|
wait_on_rate_limit: bool = True,
|
85
85
|
capsolver_api_key: str = None,
|
86
86
|
max_unlock_attempts: int = 5,
|
87
|
+
auto_relogin: bool = True,
|
88
|
+
request_self_on_startup: bool = True,
|
87
89
|
**session_kwargs,
|
88
90
|
):
|
89
91
|
super().__init__(**session_kwargs)
|
@@ -91,13 +93,21 @@ class Client(BaseHTTPClient):
|
|
91
93
|
self.wait_on_rate_limit = wait_on_rate_limit
|
92
94
|
self.capsolver_api_key = capsolver_api_key
|
93
95
|
self.max_unlock_attempts = max_unlock_attempts
|
96
|
+
self.auto_relogin = auto_relogin
|
97
|
+
self.request_self_on_startup = request_self_on_startup
|
94
98
|
|
95
|
-
async def
|
99
|
+
async def __aenter__(self):
|
100
|
+
await self.on_startup()
|
101
|
+
return await super().__aenter__()
|
102
|
+
|
103
|
+
async def _request(
|
96
104
|
self,
|
97
105
|
method,
|
98
106
|
url,
|
107
|
+
*,
|
99
108
|
auth: bool = True,
|
100
109
|
bearer: bool = True,
|
110
|
+
wait_on_rate_limit: bool = None,
|
101
111
|
**kwargs,
|
102
112
|
) -> tuple[requests.Response, Any]:
|
103
113
|
cookies = kwargs["cookies"] = kwargs.get("cookies") or {}
|
@@ -105,6 +115,7 @@ class Client(BaseHTTPClient):
|
|
105
115
|
|
106
116
|
if bearer:
|
107
117
|
headers["authorization"] = f"Bearer {self._BEARER_TOKEN}"
|
118
|
+
# headers["x-twitter-auth-type"] = "OAuth2Session"
|
108
119
|
|
109
120
|
if auth:
|
110
121
|
if not self.account.auth_token:
|
@@ -116,7 +127,8 @@ class Client(BaseHTTPClient):
|
|
116
127
|
headers["x-csrf-token"] = self.account.ct0
|
117
128
|
|
118
129
|
# fmt: off
|
119
|
-
log_message = f"{self.account}
|
130
|
+
log_message = (f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
131
|
+
f" ==> Request {method} {url}")
|
120
132
|
if kwargs.get('data'): log_message += f"\nRequest data: {kwargs.get('data')}"
|
121
133
|
if kwargs.get('json'): log_message += f"\nRequest data: {kwargs.get('json')}"
|
122
134
|
logger.debug(log_message)
|
@@ -135,23 +147,52 @@ class Client(BaseHTTPClient):
|
|
135
147
|
|
136
148
|
data = response.text
|
137
149
|
# fmt: off
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
150
|
+
logger.debug(f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
151
|
+
f" <== Response {method} {url}"
|
152
|
+
f"\nStatus code: {response.status_code}"
|
153
|
+
f"\nResponse data: {data}")
|
142
154
|
# fmt: on
|
143
155
|
|
144
|
-
if
|
156
|
+
if ct0 := self._session.cookies.get("ct0", domain=".twitter.com"):
|
157
|
+
self.account.ct0 = ct0
|
158
|
+
|
159
|
+
auth_token = self._session.cookies.get("auth_token")
|
160
|
+
if auth_token and auth_token != self.account.auth_token:
|
161
|
+
self.account.auth_token = auth_token
|
162
|
+
logger.info(
|
163
|
+
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
164
|
+
f" Requested new auth_token!"
|
165
|
+
)
|
166
|
+
|
167
|
+
try:
|
145
168
|
data = response.json()
|
169
|
+
except json.decoder.JSONDecodeError:
|
170
|
+
pass
|
146
171
|
|
147
|
-
if response.status_code
|
148
|
-
if
|
149
|
-
|
150
|
-
|
151
|
-
if
|
152
|
-
|
153
|
-
|
154
|
-
|
172
|
+
if 300 > response.status_code >= 200:
|
173
|
+
if isinstance(data, dict) and "errors" in data:
|
174
|
+
exc = HTTPException(response, data)
|
175
|
+
|
176
|
+
if 141 in exc.api_codes:
|
177
|
+
self.account.status = AccountStatus.SUSPENDED
|
178
|
+
raise Suspended(exc, self.account)
|
179
|
+
|
180
|
+
if 326 in exc.api_codes:
|
181
|
+
for error_data in exc.api_errors:
|
182
|
+
if (
|
183
|
+
error_data.get("code") == 326
|
184
|
+
and error_data.get("bounce_location")
|
185
|
+
== "/i/flow/consent_flow"
|
186
|
+
):
|
187
|
+
self.account.status = AccountStatus.CONSENT_LOCKED
|
188
|
+
raise ConsentLocked(exc, self.account)
|
189
|
+
|
190
|
+
self.account.status = AccountStatus.LOCKED
|
191
|
+
raise Locked(exc, self.account)
|
192
|
+
raise exc
|
193
|
+
|
194
|
+
self.account.status = AccountStatus.GOOD
|
195
|
+
return response, data
|
155
196
|
|
156
197
|
if response.status_code == 400:
|
157
198
|
raise BadRequest(response, data)
|
@@ -168,10 +209,6 @@ class Client(BaseHTTPClient):
|
|
168
209
|
if response.status_code == 403:
|
169
210
|
exc = Forbidden(response, data)
|
170
211
|
|
171
|
-
if 353 in exc.api_codes and "ct0" in response.cookies:
|
172
|
-
self.account.ct0 = response.cookies["ct0"]
|
173
|
-
return await self.request(method, url, auth, bearer, **kwargs)
|
174
|
-
|
175
212
|
if 64 in exc.api_codes:
|
176
213
|
self.account.status = AccountStatus.SUSPENDED
|
177
214
|
raise Suspended(exc, self.account)
|
@@ -186,52 +223,82 @@ class Client(BaseHTTPClient):
|
|
186
223
|
raise ConsentLocked(exc, self.account)
|
187
224
|
|
188
225
|
self.account.status = AccountStatus.LOCKED
|
189
|
-
|
190
|
-
raise Locked(exc, self.account)
|
191
|
-
|
192
|
-
await self.unlock()
|
193
|
-
return await self.request(method, url, auth, bearer, **kwargs)
|
226
|
+
raise Locked(exc, self.account)
|
194
227
|
|
195
228
|
raise exc
|
196
229
|
|
197
230
|
if response.status_code == 404:
|
198
231
|
raise NotFound(response, data)
|
199
232
|
|
233
|
+
if response.status_code == 429:
|
234
|
+
if wait_on_rate_limit is None:
|
235
|
+
wait_on_rate_limit = self.wait_on_rate_limit
|
236
|
+
if not wait_on_rate_limit:
|
237
|
+
raise RateLimited(response, data)
|
238
|
+
|
239
|
+
reset_time = int(response.headers["x-rate-limit-reset"])
|
240
|
+
sleep_time = reset_time - int(time()) + 1
|
241
|
+
if sleep_time > 0:
|
242
|
+
logger.info(
|
243
|
+
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
244
|
+
f"Rate limited! Sleep time: {sleep_time} sec."
|
245
|
+
)
|
246
|
+
await asyncio.sleep(sleep_time)
|
247
|
+
return await self._request(
|
248
|
+
method,
|
249
|
+
url,
|
250
|
+
auth=auth,
|
251
|
+
bearer=bearer,
|
252
|
+
wait_on_rate_limit=wait_on_rate_limit,
|
253
|
+
**kwargs,
|
254
|
+
)
|
255
|
+
|
200
256
|
if response.status_code >= 500:
|
201
257
|
raise ServerError(response, data)
|
202
258
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
259
|
+
async def request(
|
260
|
+
self,
|
261
|
+
method,
|
262
|
+
url,
|
263
|
+
*,
|
264
|
+
auto_unlock: bool = True,
|
265
|
+
auto_relogin: bool = None,
|
266
|
+
**kwargs,
|
267
|
+
) -> tuple[requests.Response, Any]:
|
268
|
+
try:
|
269
|
+
return await self._request(method, url, **kwargs)
|
208
270
|
|
209
|
-
|
210
|
-
|
211
|
-
raise
|
271
|
+
except Locked:
|
272
|
+
if not self.capsolver_api_key or not auto_unlock:
|
273
|
+
raise
|
212
274
|
|
213
|
-
|
214
|
-
|
215
|
-
if (
|
216
|
-
error_data.get("code") == 326
|
217
|
-
and error_data.get("bounce_location") == "/i/flow/consent_flow"
|
218
|
-
):
|
219
|
-
self.account.status = AccountStatus.CONSENT_LOCKED
|
220
|
-
raise ConsentLocked(exc, self.account)
|
275
|
+
await self.unlock()
|
276
|
+
return await self._request(method, url, **kwargs)
|
221
277
|
|
222
|
-
|
223
|
-
|
224
|
-
|
278
|
+
except BadToken:
|
279
|
+
if auto_relogin is None:
|
280
|
+
auto_relogin = self.auto_relogin
|
281
|
+
if (
|
282
|
+
not auto_relogin
|
283
|
+
or not self.account.password
|
284
|
+
or not (self.account.email or self.account.username)
|
285
|
+
):
|
286
|
+
raise
|
225
287
|
|
226
|
-
|
227
|
-
|
288
|
+
await self.relogin()
|
289
|
+
return await self._request(method, url, **kwargs)
|
228
290
|
|
229
|
-
|
291
|
+
except Forbidden as exc:
|
292
|
+
if 353 in exc.api_codes and "ct0" in exc.response.cookies:
|
293
|
+
return await self._request(method, url, **kwargs)
|
294
|
+
else:
|
295
|
+
raise
|
230
296
|
|
231
|
-
|
232
|
-
|
297
|
+
async def on_startup(self):
|
298
|
+
if self.request_self_on_startup:
|
299
|
+
await self.request_user()
|
233
300
|
|
234
|
-
async def
|
301
|
+
async def _request_oauth2_auth_code(
|
235
302
|
self,
|
236
303
|
client_id: str,
|
237
304
|
code_challenge: str,
|
@@ -255,7 +322,7 @@ class Client(BaseHTTPClient):
|
|
255
322
|
auth_code = response_json["auth_code"]
|
256
323
|
return auth_code
|
257
324
|
|
258
|
-
async def
|
325
|
+
async def _confirm_oauth2(self, auth_code: str):
|
259
326
|
data = {
|
260
327
|
"approval": "true",
|
261
328
|
"code": auth_code,
|
@@ -268,7 +335,7 @@ class Client(BaseHTTPClient):
|
|
268
335
|
data=data,
|
269
336
|
)
|
270
337
|
|
271
|
-
async def
|
338
|
+
async def oauth2(
|
272
339
|
self,
|
273
340
|
client_id: str,
|
274
341
|
code_challenge: str,
|
@@ -284,15 +351,13 @@ class Client(BaseHTTPClient):
|
|
284
351
|
Привязка (бинд, линк) приложения.
|
285
352
|
|
286
353
|
:param client_id: Идентификатор клиента, используемый для OAuth.
|
287
|
-
:param code_challenge: Код-вызов, используемый для PKCE (Proof Key for Code Exchange).
|
288
354
|
:param state: Уникальная строка состояния для предотвращения CSRF-атак.
|
289
355
|
:param redirect_uri: URI перенаправления, на который будет отправлен ответ.
|
290
|
-
:param code_challenge_method: Метод, используемый для преобразования code_verifier в code_challenge.
|
291
356
|
:param scope: Строка областей доступа, запрашиваемых у пользователя.
|
292
357
|
:param response_type: Тип ответа, который ожидается от сервера авторизации.
|
293
358
|
:return: Код авторизации (привязки).
|
294
359
|
"""
|
295
|
-
auth_code = await self.
|
360
|
+
auth_code = await self._request_oauth2_auth_code(
|
296
361
|
client_id,
|
297
362
|
code_challenge,
|
298
363
|
state,
|
@@ -301,7 +366,7 @@ class Client(BaseHTTPClient):
|
|
301
366
|
scope,
|
302
367
|
response_type,
|
303
368
|
)
|
304
|
-
await self.
|
369
|
+
await self._confirm_oauth2(auth_code)
|
305
370
|
return auth_code
|
306
371
|
|
307
372
|
async def _oauth(self, oauth_token: str, **oauth_params) -> requests.Response:
|
@@ -437,7 +502,7 @@ class Client(BaseHTTPClient):
|
|
437
502
|
response, data = await self.request(
|
438
503
|
"POST", url, data=payload, timeout=timeout
|
439
504
|
)
|
440
|
-
return Media
|
505
|
+
return Media(**data)
|
441
506
|
except (HTTPException, requests.errors.RequestsError) as exc:
|
442
507
|
if (
|
443
508
|
attempt < attempts - 1
|
@@ -1051,7 +1116,7 @@ class Client(BaseHTTPClient):
|
|
1051
1116
|
async def establish_status(self):
|
1052
1117
|
url = "https://twitter.com/i/api/1.1/account/update_profile.json"
|
1053
1118
|
try:
|
1054
|
-
await self.request("POST", url)
|
1119
|
+
await self.request("POST", url, auto_unlock=False, auto_relogin=False)
|
1055
1120
|
except BadAccount:
|
1056
1121
|
pass
|
1057
1122
|
|
@@ -1105,6 +1170,20 @@ class Client(BaseHTTPClient):
|
|
1105
1170
|
event_data = data["event"]
|
1106
1171
|
return event_data # TODO Возвращать модель, а не словарь
|
1107
1172
|
|
1173
|
+
async def send_message_to_conversation(
|
1174
|
+
self, conversation_id: int | str, text: str
|
1175
|
+
) -> dict:
|
1176
|
+
"""
|
1177
|
+
requires OAuth1 or OAuth2
|
1178
|
+
|
1179
|
+
:return: Event data
|
1180
|
+
"""
|
1181
|
+
url = f"https://api.twitter.com/2/dm_conversations/{conversation_id}/messages"
|
1182
|
+
payload = {"text": text}
|
1183
|
+
response, response_json = await self.request("POST", url, json=payload)
|
1184
|
+
event_data = response_json["event"]
|
1185
|
+
return event_data
|
1186
|
+
|
1108
1187
|
async def request_messages(self) -> list[dict]:
|
1109
1188
|
"""
|
1110
1189
|
:return: Messages data
|
@@ -1213,8 +1292,19 @@ class Client(BaseHTTPClient):
|
|
1213
1292
|
else:
|
1214
1293
|
funcaptcha["captcha_type"] = FunCaptchaTypeEnm.FunCaptchaTaskProxyLess
|
1215
1294
|
|
1216
|
-
while needs_unlock:
|
1295
|
+
while needs_unlock and attempt <= self.max_unlock_attempts:
|
1217
1296
|
solution = await FunCaptcha(**funcaptcha).aio_captcha_handler()
|
1297
|
+
if solution.errorId:
|
1298
|
+
logger.warning(
|
1299
|
+
f"{self.account} Failed to solve funcaptcha:"
|
1300
|
+
f"\n\tUnlock attempt: {attempt}/{self.max_unlock_attempts}"
|
1301
|
+
f"\n\tError ID: {solution.errorId}"
|
1302
|
+
f"\n\tError code: {solution.errorCode}"
|
1303
|
+
f"\n\tError description: {solution.errorDescription}"
|
1304
|
+
)
|
1305
|
+
attempt += 1
|
1306
|
+
continue
|
1307
|
+
|
1218
1308
|
token = solution.solution["token"]
|
1219
1309
|
response, html = await self._confirm_unlock(
|
1220
1310
|
authenticity_token,
|
@@ -1222,12 +1312,8 @@ class Client(BaseHTTPClient):
|
|
1222
1312
|
verification_string=token,
|
1223
1313
|
)
|
1224
1314
|
|
1225
|
-
if
|
1226
|
-
|
1227
|
-
or response.url == "https://twitter.com/?lang=en"
|
1228
|
-
):
|
1229
|
-
await self.establish_status()
|
1230
|
-
return
|
1315
|
+
if response.url == "https://twitter.com/?lang=en":
|
1316
|
+
break
|
1231
1317
|
|
1232
1318
|
(
|
1233
1319
|
authenticity_token,
|
@@ -1251,6 +1337,8 @@ class Client(BaseHTTPClient):
|
|
1251
1337
|
|
1252
1338
|
attempt += 1
|
1253
1339
|
|
1340
|
+
await self.establish_status()
|
1341
|
+
|
1254
1342
|
async def _task(self, **kwargs):
|
1255
1343
|
"""
|
1256
1344
|
:return: flow_token, subtasks
|
@@ -1415,7 +1503,7 @@ class Client(BaseHTTPClient):
|
|
1415
1503
|
"fieldToggles": to_json(field_toggles),
|
1416
1504
|
"variables": to_json(variables),
|
1417
1505
|
}
|
1418
|
-
return self.request("GET", url, params=params)
|
1506
|
+
return await self.request("GET", url, params=params)
|
1419
1507
|
|
1420
1508
|
async def _request_guest_token(self) -> str:
|
1421
1509
|
"""
|
@@ -1454,18 +1542,9 @@ class Client(BaseHTTPClient):
|
|
1454
1542
|
flow_token
|
1455
1543
|
)
|
1456
1544
|
|
1457
|
-
# TODO Делать это автоматически в методе request
|
1458
|
-
self.account.auth_token = self._session.cookies["auth_token"]
|
1459
|
-
self.account.ct0 = self._session.cookies["ct0"]
|
1460
|
-
|
1461
1545
|
await self._finish_task(flow_token)
|
1462
1546
|
|
1463
|
-
async def
|
1464
|
-
if self.account.auth_token:
|
1465
|
-
await self.establish_status()
|
1466
|
-
if self.account.status != "BAD_TOKEN":
|
1467
|
-
return
|
1468
|
-
|
1547
|
+
async def relogin(self):
|
1469
1548
|
if not self.account.email and not self.account.username:
|
1470
1549
|
raise ValueError("No email or username")
|
1471
1550
|
|
@@ -1473,8 +1552,17 @@ class Client(BaseHTTPClient):
|
|
1473
1552
|
raise ValueError("No password")
|
1474
1553
|
|
1475
1554
|
await self._login()
|
1555
|
+
await self._viewer()
|
1476
1556
|
await self.establish_status()
|
1477
1557
|
|
1558
|
+
async def login(self):
|
1559
|
+
if self.account.auth_token:
|
1560
|
+
await self.establish_status()
|
1561
|
+
if self.account.status not in ("BAD_TOKEN", "CONSENT_LOCKED"):
|
1562
|
+
return
|
1563
|
+
|
1564
|
+
await self.relogin()
|
1565
|
+
|
1478
1566
|
async def totp_is_enabled(self):
|
1479
1567
|
if not self.account.id:
|
1480
1568
|
await self.request_user()
|
@@ -1489,7 +1577,7 @@ class Client(BaseHTTPClient):
|
|
1489
1577
|
"""
|
1490
1578
|
:return: flow_token, subtask_ids
|
1491
1579
|
"""
|
1492
|
-
|
1580
|
+
query = {
|
1493
1581
|
"flow_name": "two-factor-auth-app-enrollment",
|
1494
1582
|
}
|
1495
1583
|
payload = {
|
@@ -1543,7 +1631,7 @@ class Client(BaseHTTPClient):
|
|
1543
1631
|
"web_modal": 1,
|
1544
1632
|
},
|
1545
1633
|
}
|
1546
|
-
return await self._task(params=
|
1634
|
+
return await self._task(params=query, json=payload)
|
1547
1635
|
|
1548
1636
|
async def _two_factor_enrollment_verify_password_subtask(self, flow_token: str):
|
1549
1637
|
subtask_inputs = [
|
@@ -1641,6 +1729,4 @@ class Client(BaseHTTPClient):
|
|
1641
1729
|
if await self.totp_is_enabled():
|
1642
1730
|
return
|
1643
1731
|
|
1644
|
-
# TODO Осторожно, костыль! Перед началом работы вызываем request_user_data, чтоб убедиться что нет других ошибок
|
1645
|
-
await self.request_user()
|
1646
1732
|
await self._enable_totp()
|
@@ -20,7 +20,6 @@ __all__ = [
|
|
20
20
|
]
|
21
21
|
|
22
22
|
|
23
|
-
# TODO Возвращать аккаунт в теле исключения
|
24
23
|
class TwitterException(Exception):
|
25
24
|
pass
|
26
25
|
|
@@ -62,12 +61,17 @@ class HTTPException(TwitterException):
|
|
62
61
|
self.api_codes: list[int] = []
|
63
62
|
self.api_messages: list[str] = []
|
64
63
|
self.detail: str | None = None
|
64
|
+
self.html: str | None = None
|
65
65
|
|
66
66
|
# Если ответ — строка, то это html
|
67
67
|
if isinstance(data, str):
|
68
|
-
|
69
|
-
|
70
|
-
|
68
|
+
if not data:
|
69
|
+
exception_message = (
|
70
|
+
f"(response status: {response.status_code}) Empty response body."
|
71
|
+
)
|
72
|
+
else:
|
73
|
+
self.html = data
|
74
|
+
exception_message = f"(response status: {response.status_code}) HTML Response:\n{self.html}"
|
71
75
|
if response.status_code == 429:
|
72
76
|
exception_message = (
|
73
77
|
f"(response status: {response.status_code}) Rate limit exceeded."
|
@@ -147,7 +151,9 @@ class BadAccount(TwitterException):
|
|
147
151
|
|
148
152
|
class BadToken(BadAccount):
|
149
153
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
150
|
-
exception_message =
|
154
|
+
exception_message = (
|
155
|
+
"Bad Twitter account's auth_token. Relogin to get new token."
|
156
|
+
)
|
151
157
|
super().__init__(http_exception, account, exception_message)
|
152
158
|
|
153
159
|
|
@@ -155,14 +161,14 @@ class Locked(BadAccount):
|
|
155
161
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
156
162
|
exception_message = (
|
157
163
|
f"Twitter account is locked."
|
158
|
-
f" Set CapSolver API key (capsolver_api_key) to
|
164
|
+
f" Set CapSolver API key (capsolver_api_key) to auto-unlock."
|
159
165
|
)
|
160
166
|
super().__init__(http_exception, account, exception_message)
|
161
167
|
|
162
168
|
|
163
169
|
class ConsentLocked(BadAccount):
|
164
170
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
165
|
-
exception_message = f"Twitter account is
|
171
|
+
exception_message = f"Twitter account is locked."
|
166
172
|
super().__init__(http_exception, account, exception_message)
|
167
173
|
|
168
174
|
|
@@ -1,50 +1,40 @@
|
|
1
1
|
from typing import Optional
|
2
2
|
from datetime import datetime, timedelta
|
3
3
|
|
4
|
-
from pydantic import BaseModel
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
5
5
|
|
6
6
|
from .utils import to_datetime, tweet_url
|
7
7
|
|
8
8
|
|
9
9
|
class Image(BaseModel):
|
10
|
-
type: str
|
11
|
-
width: int
|
12
|
-
height: int
|
10
|
+
type: str = Field(..., alias="image_type")
|
11
|
+
width: int = Field(..., alias="w")
|
12
|
+
height: int = Field(..., alias="h")
|
13
13
|
|
14
14
|
|
15
15
|
class Media(BaseModel):
|
16
|
-
id: int
|
16
|
+
id: int = Field(..., alias="media_id")
|
17
17
|
image: Image
|
18
18
|
size: int
|
19
|
-
expires_at: datetime
|
19
|
+
expires_at: datetime = Field(..., alias="expires_after_secs")
|
20
|
+
|
21
|
+
@field_validator("expires_at", mode="before")
|
22
|
+
@classmethod
|
23
|
+
def set_expires_at(cls, v):
|
24
|
+
return datetime.now() + timedelta(seconds=v)
|
20
25
|
|
21
26
|
def __str__(self):
|
22
27
|
return str(self.id)
|
23
28
|
|
24
|
-
@classmethod
|
25
|
-
def from_raw_data(cls, data: dict):
|
26
|
-
expires_at = datetime.now() + timedelta(seconds=data["expires_after_secs"])
|
27
|
-
values = {
|
28
|
-
"image": {
|
29
|
-
"type": data["image"]["image_type"],
|
30
|
-
"width": data["image"]["w"],
|
31
|
-
"height": data["image"]["h"],
|
32
|
-
},
|
33
|
-
"size": data["size"],
|
34
|
-
"id": data["media_id"],
|
35
|
-
"expires_at": expires_at,
|
36
|
-
}
|
37
|
-
return cls(**values)
|
38
|
-
|
39
29
|
|
40
30
|
class User(BaseModel):
|
41
31
|
# fmt: off
|
42
32
|
id: int | None = None
|
43
33
|
username: str | None = None
|
44
|
-
name: str | None = None
|
34
|
+
name: str | None = None # 50
|
45
35
|
created_at: datetime | None = None
|
46
|
-
description: str | None = None
|
47
|
-
location: str | None = None
|
36
|
+
description: str | None = None # 160
|
37
|
+
location: str | None = None # 30
|
48
38
|
followers_count: int | None = None
|
49
39
|
friends_count: int | None = None
|
50
40
|
raw_data: dict | None = None
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|