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.
Files changed (23) hide show
  1. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/PKG-INFO +2 -2
  2. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/pyproject.toml +2 -2
  3. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/serializer.py +1 -1
  4. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/account.py +5 -5
  5. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/base/client.py +3 -3
  6. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/client.py +166 -80
  7. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/errors.py +13 -7
  8. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/models.py +14 -24
  9. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/README.md +0 -0
  10. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/__init__.py +0 -0
  11. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/__init__.py +0 -0
  12. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/__init__.py +0 -0
  13. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/base.py +0 -0
  14. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/config.py +0 -0
  15. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/core/enum.py +0 -0
  16. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/_capsolver/fun_captcha.py +0 -0
  17. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/base/__init__.py +0 -0
  18. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/base/session.py +0 -0
  19. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/enums.py +0 -0
  20. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/__init__.py +0 -0
  21. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/file.py +0 -0
  22. {tweepy_self-1.9.0 → tweepy_self-1.10.0b1}/twitter/utils/html.py +0 -0
  23. {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.9.0
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.0b9)
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.9.0"
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 = {version = "0.6.0b9", allow-prereleases = true}
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
- allow_population_by_field_name = True
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
 
@@ -14,7 +14,7 @@ class BaseHTTPClient:
14
14
  return self
15
15
 
16
16
  async def __aexit__(self, *args):
17
- self.close()
17
+ await self.close()
18
18
 
19
- def close(self):
20
- self._session.close()
19
+ async def close(self):
20
+ await self._session.close()
@@ -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 request(
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} Request {method} {url}"
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
- log_message = (f"{self.account} Response {method} {url}"
139
- f"\nStatus code: {response.status_code}"
140
- f"\nResponse data: {data}")
141
- logger.debug(log_message)
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 response.headers["content-type"].startswith("application/json"):
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 == 429:
148
- if self.wait_on_rate_limit:
149
- reset_time = int(response.headers["x-rate-limit-reset"])
150
- sleep_time = reset_time - int(time()) + 1
151
- if sleep_time > 0:
152
- await asyncio.sleep(sleep_time)
153
- return await self.request(method, url, auth, bearer, **kwargs)
154
- raise RateLimited(response, data)
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
- if not self.capsolver_api_key:
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
- if not 200 <= response.status_code < 300:
204
- raise HTTPException(response, data)
205
-
206
- if isinstance(data, dict) and "errors" in data:
207
- exc = HTTPException(response, data)
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
- if 141 in exc.api_codes:
210
- self.account.status = AccountStatus.SUSPENDED
211
- raise Suspended(exc, self.account)
271
+ except Locked:
272
+ if not self.capsolver_api_key or not auto_unlock:
273
+ raise
212
274
 
213
- if 326 in exc.api_codes:
214
- for error_data in exc.api_errors:
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
- self.account.status = AccountStatus.LOCKED
223
- if not self.capsolver_api_key:
224
- raise Locked(exc, self.account)
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
- await self.unlock()
227
- return await self.request(method, url, auth, bearer, **kwargs)
288
+ await self.relogin()
289
+ return await self._request(method, url, **kwargs)
228
290
 
229
- raise exc
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
- self.account.status = AccountStatus.GOOD
232
- return response, data
297
+ async def on_startup(self):
298
+ if self.request_self_on_startup:
299
+ await self.request_user()
233
300
 
234
- async def _request_oauth_2_auth_code(
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 _confirm_oauth_2(self, auth_code: str):
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 oauth_2(
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._request_oauth_2_auth_code(
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._confirm_oauth_2(auth_code)
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.from_raw_data(data)
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
- attempt > self.max_unlock_attempts
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 login(self):
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
- params = {
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=params, json=payload)
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
- exception_message = (
69
- f"(response status: {response.status_code}) HTML Response:\n{data}"
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 = "Bad Twitter account's auth_token."
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 autounlock."
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 consent locked. Relogin to unlock."
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