tweepy-self 1.4.0__py3-none-any.whl → 1.5.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
twitter/client.py CHANGED
@@ -32,33 +32,33 @@ from .utils import remove_at_sign, parse_oauth_html, parse_unlock_html
32
32
 
33
33
 
34
34
  class Client(BaseClient):
35
- _BEARER_TOKEN = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'
35
+ _BEARER_TOKEN = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
36
36
  _DEFAULT_HEADERS = {
37
- 'authority': 'twitter.com',
38
- 'origin': 'https://twitter.com',
39
- 'x-twitter-active-user': 'yes',
37
+ "authority": "twitter.com",
38
+ "origin": "https://twitter.com",
39
+ "x-twitter-active-user": "yes",
40
40
  # 'x-twitter-auth-type': 'OAuth2Session',
41
- 'x-twitter-client-language': 'en',
41
+ "x-twitter-client-language": "en",
42
42
  }
43
- _GRAPHQL_URL = 'https://twitter.com/i/api/graphql'
43
+ _GRAPHQL_URL = "https://twitter.com/i/api/graphql"
44
44
  _ACTION_TO_QUERY_ID = {
45
- 'CreateRetweet': "ojPdsZsimiJrUGLR1sjUtA",
46
- 'FavoriteTweet': "lI07N6Otwv1PhnEgXILM7A",
47
- 'UnfavoriteTweet': "ZYKSe-w7KEslx3JhSIk5LA",
48
- 'CreateTweet': "SoVnbfCycZ7fERGCwpZkYA",
49
- 'TweetResultByRestId': "V3vfsYzNEyD9tsf4xoFRgw",
50
- 'ModerateTweet': "p'jF:GVqCjTcZol0xcBJjw",
51
- 'DeleteTweet': "VaenaVgh5q5ih7kvyVjgtg",
52
- 'UserTweets': "V1ze5q3ijDS1VeLwLY0m7g",
53
- 'TweetDetail': 'VWFGPVAGkZMGRKGe3GFFnA',
54
- 'ProfileSpotlightsQuery': '9zwVLJ48lmVUk8u_Gh9DmA',
55
- 'Following': 't-BPOrMIduGUJWO_LxcvNQ',
56
- 'Followers': '3yX7xr2hKjcZYnXt6cU6lQ',
57
- 'UserByScreenName': 'G3KGOASz96M-Qu0nwmGXNg',
58
- 'Viewer': 'W62NnYgkgziw9bwyoVht0g',
45
+ "CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
46
+ "FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
47
+ "UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
48
+ "CreateTweet": "SoVnbfCycZ7fERGCwpZkYA",
49
+ "TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
50
+ "ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
51
+ "DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
52
+ "UserTweets": "V1ze5q3ijDS1VeLwLY0m7g",
53
+ "TweetDetail": "VWFGPVAGkZMGRKGe3GFFnA",
54
+ "ProfileSpotlightsQuery": "9zwVLJ48lmVUk8u_Gh9DmA",
55
+ "Following": "t-BPOrMIduGUJWO_LxcvNQ",
56
+ "Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
57
+ "UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
58
+ "Viewer": "W62NnYgkgziw9bwyoVht0g",
59
59
  }
60
- _CAPTCHA_URL = 'https://twitter.com/account/access'
61
- _CAPTCHA_SITE_KEY = '0152B4EB-D2DC-460A-89A1-629838B529C9'
60
+ _CAPTCHA_URL = "https://twitter.com/account/access"
61
+ _CAPTCHA_SITE_KEY = "0152B4EB-D2DC-460A-89A1-629838B529C9"
62
62
 
63
63
  @classmethod
64
64
  def _action_to_url(cls, action: str) -> tuple[str, str]:
@@ -70,13 +70,13 @@ class Client(BaseClient):
70
70
  return url, query_id
71
71
 
72
72
  def __init__(
73
- self,
74
- account: Account,
75
- *,
76
- wait_on_rate_limit: bool = True,
77
- capsolver_api_key: str = None,
78
- max_unlock_attempts: int = 4,
79
- **session_kwargs,
73
+ self,
74
+ account: Account,
75
+ *,
76
+ wait_on_rate_limit: bool = True,
77
+ capsolver_api_key: str = None,
78
+ max_unlock_attempts: int = 4,
79
+ **session_kwargs,
80
80
  ):
81
81
  super().__init__(**session_kwargs)
82
82
  self.account = account
@@ -85,12 +85,12 @@ class Client(BaseClient):
85
85
  self.max_unlock_attempts = max_unlock_attempts
86
86
 
87
87
  async def request(
88
- self,
89
- method,
90
- url,
91
- auth: bool = True,
92
- bearer: bool = True,
93
- **kwargs,
88
+ self,
89
+ method,
90
+ url,
91
+ auth: bool = True,
92
+ bearer: bool = True,
93
+ **kwargs,
94
94
  ) -> tuple[requests.Response, Any]:
95
95
  cookies = kwargs["cookies"] = kwargs.get("cookies") or {}
96
96
  headers = kwargs["headers"] = kwargs.get("headers") or {}
@@ -111,12 +111,15 @@ class Client(BaseClient):
111
111
  response = await self._session.request(method, url, **kwargs)
112
112
  except requests.errors.RequestsError as exc:
113
113
  if exc.code == 35:
114
- msg = "The IP address may have been blocked by Twitter. Blocked countries: Russia. " + str(exc)
114
+ msg = (
115
+ "The IP address may have been blocked by Twitter. Blocked countries: Russia. "
116
+ + str(exc)
117
+ )
115
118
  raise requests.errors.RequestsError(msg, 35, exc.response)
116
119
  raise
117
120
 
118
121
  data = response.text
119
- if response.headers['content-type'].startswith('application/json'):
122
+ if response.headers["content-type"].startswith("application/json"):
120
123
  data = response.json()
121
124
 
122
125
  if response.status_code == 429:
@@ -153,8 +156,10 @@ class Client(BaseClient):
153
156
 
154
157
  if 326 in exc.api_codes:
155
158
  for error_data in exc.api_errors:
156
- if (error_data.get("code") == 326 and
157
- error_data.get("bounce_location") == "/i/flow/consent_flow"):
159
+ if (
160
+ error_data.get("code") == 326
161
+ and error_data.get("bounce_location") == "/i/flow/consent_flow"
162
+ ):
158
163
  self.account.status = AccountStatus.CONSENT_LOCKED
159
164
  raise ConsentLocked(exc, self.account)
160
165
 
@@ -185,8 +190,10 @@ class Client(BaseClient):
185
190
 
186
191
  if 326 in exc.api_codes:
187
192
  for error_data in exc.api_errors:
188
- if (error_data.get("code") == 326 and
189
- error_data.get("bounce_location") == "/i/flow/consent_flow"):
193
+ if (
194
+ error_data.get("code") == 326
195
+ and error_data.get("bounce_location") == "/i/flow/consent_flow"
196
+ ):
190
197
  self.account.status = AccountStatus.CONSENT_LOCKED
191
198
  raise ConsentLocked(exc, self.account)
192
199
 
@@ -203,14 +210,14 @@ class Client(BaseClient):
203
210
  return response, data
204
211
 
205
212
  async def _request_oauth_2_auth_code(
206
- self,
207
- client_id: str,
208
- code_challenge: str,
209
- state: str,
210
- redirect_uri: str,
211
- code_challenge_method: str,
212
- scope: str,
213
- response_type: str,
213
+ self,
214
+ client_id: str,
215
+ code_challenge: str,
216
+ state: str,
217
+ redirect_uri: str,
218
+ code_challenge_method: str,
219
+ scope: str,
220
+ response_type: str,
214
221
  ) -> str:
215
222
  url = "https://twitter.com/i/api/2/oauth2/authorize"
216
223
  querystring = {
@@ -228,21 +235,26 @@ class Client(BaseClient):
228
235
 
229
236
  async def _confirm_oauth_2(self, auth_code: str):
230
237
  data = {
231
- 'approval': 'true',
232
- 'code': auth_code,
238
+ "approval": "true",
239
+ "code": auth_code,
233
240
  }
234
- headers = {'content-type': 'application/x-www-form-urlencoded'}
235
- await self.request("POST", 'https://twitter.com/i/api/2/oauth2/authorize', headers=headers, data=data)
241
+ headers = {"content-type": "application/x-www-form-urlencoded"}
242
+ await self.request(
243
+ "POST",
244
+ "https://twitter.com/i/api/2/oauth2/authorize",
245
+ headers=headers,
246
+ data=data,
247
+ )
236
248
 
237
249
  async def oauth_2(
238
- self,
239
- client_id: str,
240
- code_challenge: str,
241
- state: str,
242
- redirect_uri: str,
243
- code_challenge_method: str,
244
- scope: str,
245
- response_type: str,
250
+ self,
251
+ client_id: str,
252
+ code_challenge: str,
253
+ state: str,
254
+ redirect_uri: str,
255
+ code_challenge_method: str,
256
+ scope: str,
257
+ response_type: str,
246
258
  ):
247
259
  """
248
260
  Запрашивает код авторизации для OAuth 2.0 авторизации.
@@ -259,7 +271,13 @@ class Client(BaseClient):
259
271
  :return: Код авторизации (привязки).
260
272
  """
261
273
  auth_code = await self._request_oauth_2_auth_code(
262
- client_id, code_challenge, state, redirect_uri, code_challenge_method, scope, response_type,
274
+ client_id,
275
+ code_challenge,
276
+ state,
277
+ redirect_uri,
278
+ code_challenge_method,
279
+ scope,
280
+ response_type,
263
281
  )
264
282
  await self._confirm_oauth_2(auth_code)
265
283
  return auth_code
@@ -274,16 +292,18 @@ class Client(BaseClient):
274
292
  response, _ = await self.request("GET", url, params=oauth_params)
275
293
 
276
294
  if response.status_code == 403:
277
- raise ValueError("The request token (oauth_token) for this page is invalid."
278
- " It may have already been used, or expired because it is too old.")
295
+ raise ValueError(
296
+ "The request token (oauth_token) for this page is invalid."
297
+ " It may have already been used, or expired because it is too old."
298
+ )
279
299
 
280
300
  return response
281
301
 
282
302
  async def _confirm_oauth(
283
- self,
284
- oauth_token: str,
285
- authenticity_token: str,
286
- redirect_after_login_url: str,
303
+ self,
304
+ oauth_token: str,
305
+ authenticity_token: str,
306
+ redirect_after_login_url: str,
287
307
  ) -> requests.Response:
288
308
  url = "https://api.twitter.com/oauth/authorize"
289
309
  params = {
@@ -299,12 +319,18 @@ class Client(BaseClient):
299
319
  :return: authenticity_token, redirect_url
300
320
  """
301
321
  response = await self._oauth(oauth_token, **oauth_params)
302
- authenticity_token, redirect_url, redirect_after_login_url = parse_oauth_html(response.text)
322
+ authenticity_token, redirect_url, redirect_after_login_url = parse_oauth_html(
323
+ response.text
324
+ )
303
325
 
304
326
  # Первая привязка требует подтверждения
305
327
  if redirect_after_login_url:
306
- response = await self._confirm_oauth(oauth_token, authenticity_token, redirect_after_login_url)
307
- authenticity_token, redirect_url, redirect_after_login_url = parse_oauth_html(response.text)
328
+ response = await self._confirm_oauth(
329
+ oauth_token, authenticity_token, redirect_after_login_url
330
+ )
331
+ authenticity_token, redirect_url, redirect_after_login_url = (
332
+ parse_oauth_html(response.text)
333
+ )
308
334
 
309
335
  return authenticity_token, redirect_url
310
336
 
@@ -373,25 +399,27 @@ class Client(BaseClient):
373
399
  async def _follow_action(self, action: str, user_id: int | str) -> bool:
374
400
  url = f"https://twitter.com/i/api/1.1/friendships/{action}.json"
375
401
  params = {
376
- 'include_profile_interstitial_type': '1',
377
- 'include_blocking': '1',
378
- 'include_blocked_by': '1',
379
- 'include_followed_by': '1',
380
- 'include_want_retweets': '1',
381
- 'include_mute_edge': '1',
382
- 'include_can_dm': '1',
383
- 'include_can_media_tag': '1',
384
- 'include_ext_has_nft_avatar': '1',
385
- 'include_ext_is_blue_verified': '1',
386
- 'include_ext_verified_type': '1',
387
- 'include_ext_profile_image_shape': '1',
388
- 'skip_status': '1',
389
- 'user_id': user_id,
402
+ "include_profile_interstitial_type": "1",
403
+ "include_blocking": "1",
404
+ "include_blocked_by": "1",
405
+ "include_followed_by": "1",
406
+ "include_want_retweets": "1",
407
+ "include_mute_edge": "1",
408
+ "include_can_dm": "1",
409
+ "include_can_media_tag": "1",
410
+ "include_ext_has_nft_avatar": "1",
411
+ "include_ext_is_blue_verified": "1",
412
+ "include_ext_verified_type": "1",
413
+ "include_ext_profile_image_shape": "1",
414
+ "skip_status": "1",
415
+ "user_id": user_id,
390
416
  }
391
417
  headers = {
392
- 'content-type': 'application/x-www-form-urlencoded',
418
+ "content-type": "application/x-www-form-urlencoded",
393
419
  }
394
- response, response_json = await self.request("POST", url, params=params, headers=headers)
420
+ response, response_json = await self.request(
421
+ "POST", url, params=params, headers=headers
422
+ )
395
423
  return bool(response_json)
396
424
 
397
425
  async def follow(self, user_id: str | int) -> bool:
@@ -403,11 +431,8 @@ class Client(BaseClient):
403
431
  async def _interact_with_tweet(self, action: str, tweet_id: int) -> dict:
404
432
  url, query_id = self._action_to_url(action)
405
433
  json_payload = {
406
- 'variables': {
407
- 'tweet_id': tweet_id,
408
- 'dark_request': False
409
- },
410
- 'queryId': query_id
434
+ "variables": {"tweet_id": tweet_id, "dark_request": False},
435
+ "queryId": query_id,
411
436
  }
412
437
  response, response_json = await self.request("POST", url, json=json_payload)
413
438
  return response_json
@@ -418,98 +443,109 @@ class Client(BaseClient):
418
443
 
419
444
  :return: Tweet ID
420
445
  """
421
- response_json = await self._interact_with_tweet('CreateRetweet', tweet_id)
422
- retweet_id = int(response_json['data']['create_retweet']['retweet_results']['result']['rest_id'])
446
+ response_json = await self._interact_with_tweet("CreateRetweet", tweet_id)
447
+ retweet_id = int(
448
+ response_json["data"]["create_retweet"]["retweet_results"]["result"][
449
+ "rest_id"
450
+ ]
451
+ )
423
452
  return retweet_id
424
453
 
425
454
  async def like(self, tweet_id: int) -> bool:
426
- response_json = await self._interact_with_tweet('FavoriteTweet', tweet_id)
427
- is_liked = response_json['data']['favorite_tweet'] == 'Done'
455
+ response_json = await self._interact_with_tweet("FavoriteTweet", tweet_id)
456
+ is_liked = response_json["data"]["favorite_tweet"] == "Done"
428
457
  return is_liked
429
458
 
430
459
  async def unlike(self, tweet_id: int) -> dict:
431
- response_json = await self._interact_with_tweet('UnfavoriteTweet', tweet_id)
432
- is_unliked = 'data' in response_json and response_json['data']['unfavorite_tweet'] == 'Done'
460
+ response_json = await self._interact_with_tweet("UnfavoriteTweet", tweet_id)
461
+ is_unliked = (
462
+ "data" in response_json
463
+ and response_json["data"]["unfavorite_tweet"] == "Done"
464
+ )
433
465
  return is_unliked
434
466
 
435
467
  async def delete_tweet(self, tweet_id: int | str) -> bool:
436
- url, query_id = self._action_to_url('DeleteTweet')
468
+ url, query_id = self._action_to_url("DeleteTweet")
437
469
  json_payload = {
438
- 'variables': {
439
- 'tweet_id': tweet_id,
440
- 'dark_request': False,
470
+ "variables": {
471
+ "tweet_id": tweet_id,
472
+ "dark_request": False,
441
473
  },
442
- 'queryId': query_id,
474
+ "queryId": query_id,
443
475
  }
444
476
  response, response_json = await self.request("POST", url, json=json_payload)
445
477
  is_deleted = "data" in response_json and "delete_tweet" in response_json["data"]
446
478
  return is_deleted
447
479
 
448
480
  async def pin_tweet(self, tweet_id: str | int) -> bool:
449
- url = 'https://api.twitter.com/1.1/account/pin_tweet.json'
481
+ url = "https://api.twitter.com/1.1/account/pin_tweet.json"
450
482
  data = {
451
- 'tweet_mode': 'extended',
452
- 'id': str(tweet_id),
483
+ "tweet_mode": "extended",
484
+ "id": str(tweet_id),
453
485
  }
454
486
  headers = {
455
- 'content-type': 'application/x-www-form-urlencoded',
487
+ "content-type": "application/x-www-form-urlencoded",
456
488
  }
457
- response, response_json = await self.request("POST", url, headers=headers, data=data)
489
+ response, response_json = await self.request(
490
+ "POST", url, headers=headers, data=data
491
+ )
458
492
  is_pinned = bool(response_json["pinned_tweets"])
459
493
  return is_pinned
460
494
 
461
495
  async def _tweet(
462
- self,
463
- text: str = None,
464
- *,
465
- media_id: int | str = None,
466
- tweet_id_to_reply: str | int = None,
467
- attachment_url: str = None,
496
+ self,
497
+ text: str = None,
498
+ *,
499
+ media_id: int | str = None,
500
+ tweet_id_to_reply: str | int = None,
501
+ attachment_url: str = None,
468
502
  ) -> int:
469
- url, query_id = self._action_to_url('CreateTweet')
503
+ url, query_id = self._action_to_url("CreateTweet")
470
504
  payload = {
471
- 'variables': {
472
- 'tweet_text': text if text is not None else "",
473
- 'dark_request': False,
474
- 'media': {
475
- 'media_entities': [],
476
- 'possibly_sensitive': False},
477
- 'semantic_annotation_ids': [],
505
+ "variables": {
506
+ "tweet_text": text if text is not None else "",
507
+ "dark_request": False,
508
+ "media": {"media_entities": [], "possibly_sensitive": False},
509
+ "semantic_annotation_ids": [],
478
510
  },
479
- 'features': {
480
- 'tweetypie_unmention_optimization_enabled': True,
481
- 'responsive_web_edit_tweet_api_enabled': True,
482
- 'graphql_is_translatable_rweb_tweet_is_translatable_enabled': True,
483
- 'view_counts_everywhere_api_enabled': True,
484
- 'longform_notetweets_consumption_enabled': True,
485
- 'tweet_awards_web_tipping_enabled': False,
486
- 'longform_notetweets_rich_text_read_enabled': True,
487
- 'longform_notetweets_inline_media_enabled': True,
488
- 'responsive_web_graphql_exclude_directive_enabled': True,
489
- 'verified_phone_label_enabled': False,
490
- 'freedom_of_speech_not_reach_fetch_enabled': True,
491
- 'standardized_nudges_misinfo': True,
492
- 'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled': False,
493
- 'responsive_web_graphql_skip_user_profile_image_extensions_enabled': False,
494
- 'responsive_web_graphql_timeline_navigation_enabled': True,
495
- 'responsive_web_enhance_cards_enabled': False,
496
- 'responsive_web_twitter_article_tweet_consumption_enabled': False,
497
- 'responsive_web_media_download_video_enabled': False
511
+ "features": {
512
+ "tweetypie_unmention_optimization_enabled": True,
513
+ "responsive_web_edit_tweet_api_enabled": True,
514
+ "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
515
+ "view_counts_everywhere_api_enabled": True,
516
+ "longform_notetweets_consumption_enabled": True,
517
+ "tweet_awards_web_tipping_enabled": False,
518
+ "longform_notetweets_rich_text_read_enabled": True,
519
+ "longform_notetweets_inline_media_enabled": True,
520
+ "responsive_web_graphql_exclude_directive_enabled": True,
521
+ "verified_phone_label_enabled": False,
522
+ "freedom_of_speech_not_reach_fetch_enabled": True,
523
+ "standardized_nudges_misinfo": True,
524
+ "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": False,
525
+ "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
526
+ "responsive_web_graphql_timeline_navigation_enabled": True,
527
+ "responsive_web_enhance_cards_enabled": False,
528
+ "responsive_web_twitter_article_tweet_consumption_enabled": False,
529
+ "responsive_web_media_download_video_enabled": False,
498
530
  },
499
- 'queryId': query_id,
531
+ "queryId": query_id,
500
532
  }
501
533
  if attachment_url:
502
- payload['variables']['attachment_url'] = attachment_url
534
+ payload["variables"]["attachment_url"] = attachment_url
503
535
  if tweet_id_to_reply:
504
- payload['variables']['reply'] = {
505
- 'in_reply_to_tweet_id': str(tweet_id_to_reply),
506
- 'exclude_reply_user_ids': [],
536
+ payload["variables"]["reply"] = {
537
+ "in_reply_to_tweet_id": str(tweet_id_to_reply),
538
+ "exclude_reply_user_ids": [],
507
539
  }
508
540
  if media_id:
509
- payload['variables']['media']['media_entities'].append({'media_id': str(media_id), 'tagged_users': []})
541
+ payload["variables"]["media"]["media_entities"].append(
542
+ {"media_id": str(media_id), "tagged_users": []}
543
+ )
510
544
 
511
545
  response, response_json = await self.request("POST", url, json=payload)
512
- tweet_id = response_json['data']['create_tweet']['tweet_results']['result']['rest_id']
546
+ tweet_id = response_json["data"]["create_tweet"]["tweet_results"]["result"][
547
+ "rest_id"
548
+ ]
513
549
  return tweet_id
514
550
 
515
551
  async def tweet(self, text: str, *, media_id: int | str = None) -> int:
@@ -518,19 +554,25 @@ class Client(BaseClient):
518
554
  """
519
555
  return await self._tweet(text, media_id=media_id)
520
556
 
521
- async def reply(self, tweet_id: str | int, text: str, *, media_id: int | str = None) -> int:
557
+ async def reply(
558
+ self, tweet_id: str | int, text: str, *, media_id: int | str = None
559
+ ) -> int:
522
560
  """
523
561
  :return: Tweet ID
524
562
  """
525
563
  return await self._tweet(text, media_id=media_id, tweet_id_to_reply=tweet_id)
526
564
 
527
- async def quote(self, tweet_url: str, text: str, *, media_id: int | str = None) -> int:
565
+ async def quote(
566
+ self, tweet_url: str, text: str, *, media_id: int | str = None
567
+ ) -> int:
528
568
  """
529
569
  :return: Tweet ID
530
570
  """
531
571
  return await self._tweet(text, media_id=media_id, attachment_url=tweet_url)
532
572
 
533
- async def vote(self, tweet_id: int | str, card_id: int | str, choice_number: int) -> dict:
573
+ async def vote(
574
+ self, tweet_id: int | str, card_id: int | str, choice_number: int
575
+ ) -> dict:
534
576
  """
535
577
  :return: Raw vote information
536
578
  """
@@ -545,12 +587,14 @@ class Client(BaseClient):
545
587
  response, response_json = await self.request("POST", url, params=params)
546
588
  return response_json
547
589
 
548
- async def _request_users(self, action: str, user_id: int | str, count: int) -> list[UserData]:
590
+ async def _request_users(
591
+ self, action: str, user_id: int | str, count: int
592
+ ) -> list[UserData]:
549
593
  url, query_id = self._action_to_url(action)
550
594
  variables = {
551
- 'userId': str(user_id),
552
- 'count': count,
553
- 'includePromotedContent': False,
595
+ "userId": str(user_id),
596
+ "count": count,
597
+ "includePromotedContent": False,
554
598
  }
555
599
  features = {
556
600
  "rweb_lists_timeline_redesign_enabled": True,
@@ -572,49 +616,57 @@ class Client(BaseClient):
572
616
  "longform_notetweets_rich_text_read_enabled": True,
573
617
  "longform_notetweets_inline_media_enabled": True,
574
618
  "responsive_web_media_download_video_enabled": False,
575
- "responsive_web_enhance_cards_enabled": False
619
+ "responsive_web_enhance_cards_enabled": False,
576
620
  }
577
621
  params = {
578
- 'variables': to_json(variables),
579
- 'features': to_json(features),
622
+ "variables": to_json(variables),
623
+ "features": to_json(features),
580
624
  }
581
625
  response, response_json = await self.request("GET", url, params=params)
582
626
 
583
627
  users = []
584
- if 'result' in response_json['data']['user']:
585
- entries = response_json['data']['user']['result']['timeline']['timeline']['instructions'][-1]['entries']
628
+ if "result" in response_json["data"]["user"]:
629
+ entries = response_json["data"]["user"]["result"]["timeline"]["timeline"][
630
+ "instructions"
631
+ ][-1]["entries"]
586
632
  for entry in entries:
587
- if entry['entryId'].startswith('user'):
588
- user_data_dict = entry["content"]["itemContent"]["user_results"]["result"]
633
+ if entry["entryId"].startswith("user"):
634
+ user_data_dict = entry["content"]["itemContent"]["user_results"][
635
+ "result"
636
+ ]
589
637
  users.append(UserData.from_raw_user_data(user_data_dict))
590
638
  return users
591
639
 
592
- async def request_followers(self, user_id: int | str = None, count: int = 10) -> list[UserData]:
640
+ async def request_followers(
641
+ self, user_id: int | str = None, count: int = 10
642
+ ) -> list[UserData]:
593
643
  """
594
644
  :param user_id: Текущий пользователь, если не передан ID иного пользователя.
595
645
  :param count: Количество подписчиков.
596
646
  """
597
647
  if user_id:
598
- return await self._request_users('Followers', user_id, count)
648
+ return await self._request_users("Followers", user_id, count)
599
649
  else:
600
650
  if not self.account.id:
601
651
  await self.request_user_data()
602
- return await self._request_users('Followers', self.account.id, count)
652
+ return await self._request_users("Followers", self.account.id, count)
603
653
 
604
- async def request_followings(self, user_id: int | str = None, count: int = 10) -> list[UserData]:
654
+ async def request_followings(
655
+ self, user_id: int | str = None, count: int = 10
656
+ ) -> list[UserData]:
605
657
  """
606
658
  :param user_id: Текущий пользователь, если не передан ID иного пользователя.
607
659
  :param count: Количество подписчиков.
608
660
  """
609
661
  if user_id:
610
- return await self._request_users('Following', user_id, count)
662
+ return await self._request_users("Following", user_id, count)
611
663
  else:
612
664
  if not self.account.id:
613
665
  await self.request_user_data()
614
- return await self._request_users('Following', self.account.id, count)
666
+ return await self._request_users("Following", self.account.id, count)
615
667
 
616
668
  async def _request_tweet_data(self, tweet_id: int) -> dict:
617
- action = 'TweetDetail'
669
+ action = "TweetDetail"
618
670
  url, query_id = self._action_to_url(action)
619
671
  variables = {
620
672
  "focalTweetId": str(tweet_id),
@@ -647,33 +699,35 @@ class Client(BaseClient):
647
699
  "responsive_web_enhance_cards_enabled": False,
648
700
  }
649
701
  params = {
650
- 'variables': to_json(variables),
651
- 'features': to_json(features),
702
+ "variables": to_json(variables),
703
+ "features": to_json(features),
652
704
  }
653
705
  response, response_json = await self.request("GET", url, params=params)
654
706
  return response_json
655
707
 
656
- async def _update_profile_image(self, type: Literal["banner", "image"], media_id: str | int) -> str:
708
+ async def _update_profile_image(
709
+ self, type: Literal["banner", "image"], media_id: str | int
710
+ ) -> str:
657
711
  """
658
712
  :return: Image URL
659
713
  """
660
714
  url = f"https://api.twitter.com/1.1/account/update_profile_{type}.json"
661
715
  params = {
662
- 'media_id': str(media_id),
663
- 'include_profile_interstitial_type': '1',
664
- 'include_blocking': '1',
665
- 'include_blocked_by': '1',
666
- 'include_followed_by': '1',
667
- 'include_want_retweets': '1',
668
- 'include_mute_edge': '1',
669
- 'include_can_dm': '1',
670
- 'include_can_media_tag': '1',
671
- 'include_ext_has_nft_avatar': '1',
672
- 'include_ext_is_blue_verified': '1',
673
- 'include_ext_verified_type': '1',
674
- 'include_ext_profile_image_shape': '1',
675
- 'skip_status': '1',
676
- 'return_user': 'true',
716
+ "media_id": str(media_id),
717
+ "include_profile_interstitial_type": "1",
718
+ "include_blocking": "1",
719
+ "include_blocked_by": "1",
720
+ "include_followed_by": "1",
721
+ "include_want_retweets": "1",
722
+ "include_mute_edge": "1",
723
+ "include_can_dm": "1",
724
+ "include_can_media_tag": "1",
725
+ "include_ext_has_nft_avatar": "1",
726
+ "include_ext_is_blue_verified": "1",
727
+ "include_ext_verified_type": "1",
728
+ "include_ext_profile_image_shape": "1",
729
+ "skip_status": "1",
730
+ "return_user": "true",
677
731
  }
678
732
  response, response_json = await self.request("POST", url, params=params)
679
733
  image_url = response_json[f"profile_{type}_url"]
@@ -711,7 +765,7 @@ class Client(BaseClient):
711
765
  data = {
712
766
  "current_password": self.account.password,
713
767
  "password": password,
714
- "password_confirmation": password
768
+ "password_confirmation": password,
715
769
  }
716
770
  response, response_json = await self.request("POST", url, data=data)
717
771
  is_changed = response_json["status"] == "ok"
@@ -721,11 +775,11 @@ class Client(BaseClient):
721
775
  return is_changed
722
776
 
723
777
  async def update_profile(
724
- self,
725
- name: str = None,
726
- description: str = None,
727
- location: str = None,
728
- website: str = None,
778
+ self,
779
+ name: str = None,
780
+ description: str = None,
781
+ location: str = None,
782
+ website: str = None,
729
783
  ) -> bool:
730
784
  """
731
785
  Locks an account!
@@ -736,16 +790,29 @@ class Client(BaseClient):
736
790
  url = "https://twitter.com/i/api/1.1/account/update_profile.json"
737
791
  headers = {"content-type": "application/x-www-form-urlencoded"}
738
792
  # Создаем словарь data, включая в него только те ключи, для которых значения не равны None
739
- data = {k: v for k, v in [
740
- ("name", name),
741
- ("description", description),
742
- ("location", location),
743
- ("url", website),
744
- ] if v is not None}
745
- response, response_json = await self.request("POST", url, headers=headers, data=data)
793
+ data = {
794
+ k: v
795
+ for k, v in [
796
+ ("name", name),
797
+ ("description", description),
798
+ ("location", location),
799
+ ("url", website),
800
+ ]
801
+ if v is not None
802
+ }
803
+ response, response_json = await self.request(
804
+ "POST", url, headers=headers, data=data
805
+ )
746
806
  # Проверяем, что все переданные параметры соответствуют полученным
747
- is_updated = all(response_json.get(key) == value for key, value in data.items() if key != "url")
748
- if website: is_updated &= URL(website) == URL(response_json["entities"]["url"]["urls"][0]["expanded_url"])
807
+ is_updated = all(
808
+ response_json.get(key) == value
809
+ for key, value in data.items()
810
+ if key != "url"
811
+ )
812
+ if website:
813
+ is_updated &= URL(website) == URL(
814
+ response_json["entities"]["url"]["urls"][0]["expanded_url"]
815
+ )
749
816
  await self.request_user_data()
750
817
  return is_updated
751
818
 
@@ -757,12 +824,12 @@ class Client(BaseClient):
757
824
  pass
758
825
 
759
826
  async def update_birthdate(
760
- self,
761
- day: int,
762
- month: int,
763
- year: int,
764
- visibility: Literal["self", "mutualfollow"] = "self",
765
- year_visibility: Literal["self"] = "self",
827
+ self,
828
+ day: int,
829
+ month: int,
830
+ year: int,
831
+ visibility: Literal["self", "mutualfollow"] = "self",
832
+ year_visibility: Literal["self"] = "self",
766
833
  ) -> bool:
767
834
  url = "https://twitter.com/i/api/1.1/account/update_profile.json"
768
835
  headers = {"content-type": "application/x-www-form-urlencoded"}
@@ -773,15 +840,19 @@ class Client(BaseClient):
773
840
  "birthdate_visibility": visibility,
774
841
  "birthdate_year_visibility": year_visibility,
775
842
  }
776
- response, response_json = await self.request("POST", url, headers=headers, data=data)
843
+ response, response_json = await self.request(
844
+ "POST", url, headers=headers, data=data
845
+ )
777
846
  birthdate_data = response_json["extended_profile"]["birthdate"]
778
- is_updated = all((
779
- birthdate_data["day"] == day,
780
- birthdate_data["month"] == month,
781
- birthdate_data["year"] == year,
782
- birthdate_data["visibility"] == visibility,
783
- birthdate_data["year_visibility"] == year_visibility,
784
- ))
847
+ is_updated = all(
848
+ (
849
+ birthdate_data["day"] == day,
850
+ birthdate_data["month"] == month,
851
+ birthdate_data["year"] == year,
852
+ birthdate_data["visibility"] == visibility,
853
+ birthdate_data["year_visibility"] == year_visibility,
854
+ )
855
+ )
785
856
  return is_updated
786
857
 
787
858
  async def send_message(self, user_id: int | str, text: str) -> dict:
@@ -789,15 +860,15 @@ class Client(BaseClient):
789
860
  :return: Event data
790
861
  """
791
862
  url = "https://api.twitter.com/1.1/direct_messages/events/new.json"
792
- payload = {"event": {
793
- "type": "message_create",
794
- "message_create": {
795
- "target": {
796
- "recipient_id": user_id
797
- }, "message_data": {
798
- "text": text}
863
+ payload = {
864
+ "event": {
865
+ "type": "message_create",
866
+ "message_create": {
867
+ "target": {"recipient_id": user_id},
868
+ "message_data": {"text": text},
869
+ },
799
870
  }
800
- }}
871
+ }
801
872
  response, response_json = await self.request("POST", url, json=payload)
802
873
  event_data = response_json["event"]
803
874
  return event_data
@@ -806,45 +877,49 @@ class Client(BaseClient):
806
877
  """
807
878
  :return: Messages data
808
879
  """
809
- url = 'https://twitter.com/i/api/1.1/dm/inbox_initial_state.json'
880
+ url = "https://twitter.com/i/api/1.1/dm/inbox_initial_state.json"
810
881
  params = {
811
- 'nsfw_filtering_enabled': 'false',
812
- 'filter_low_quality': 'false',
813
- 'include_quality': 'all',
814
- 'include_profile_interstitial_type': '1',
815
- 'include_blocking': '1',
816
- 'include_blocked_by': '1',
817
- 'include_followed_by': '1',
818
- 'include_want_retweets': '1',
819
- 'include_mute_edge': '1',
820
- 'include_can_dm': '1',
821
- 'include_can_media_tag': '1',
822
- 'include_ext_has_nft_avatar': '1',
823
- 'include_ext_is_blue_verified': '1',
824
- 'include_ext_verified_type': '1',
825
- 'include_ext_profile_image_shape': '1',
826
- 'skip_status': '1',
827
- 'dm_secret_conversations_enabled': 'false',
828
- 'krs_registration_enabled': 'true',
829
- 'cards_platform': 'Web-12',
830
- 'include_cards': '1',
831
- 'include_ext_alt_text': 'true',
832
- 'include_ext_limited_action_results': 'true',
833
- 'include_quote_count': 'true',
834
- 'include_reply_count': '1',
835
- 'tweet_mode': 'extended',
836
- 'include_ext_views': 'true',
837
- 'dm_users': 'true',
838
- 'include_groups': 'true',
839
- 'include_inbox_timelines': 'true',
840
- 'include_ext_media_color': 'true',
841
- 'supports_reactions': 'true',
842
- 'include_ext_edit_control': 'true',
843
- 'include_ext_business_affiliations_label': 'true',
844
- 'ext': 'mediaColor,altText,mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,superFollowMetadata,unmentionInfo,editControl',
882
+ "nsfw_filtering_enabled": "false",
883
+ "filter_low_quality": "false",
884
+ "include_quality": "all",
885
+ "include_profile_interstitial_type": "1",
886
+ "include_blocking": "1",
887
+ "include_blocked_by": "1",
888
+ "include_followed_by": "1",
889
+ "include_want_retweets": "1",
890
+ "include_mute_edge": "1",
891
+ "include_can_dm": "1",
892
+ "include_can_media_tag": "1",
893
+ "include_ext_has_nft_avatar": "1",
894
+ "include_ext_is_blue_verified": "1",
895
+ "include_ext_verified_type": "1",
896
+ "include_ext_profile_image_shape": "1",
897
+ "skip_status": "1",
898
+ "dm_secret_conversations_enabled": "false",
899
+ "krs_registration_enabled": "true",
900
+ "cards_platform": "Web-12",
901
+ "include_cards": "1",
902
+ "include_ext_alt_text": "true",
903
+ "include_ext_limited_action_results": "true",
904
+ "include_quote_count": "true",
905
+ "include_reply_count": "1",
906
+ "tweet_mode": "extended",
907
+ "include_ext_views": "true",
908
+ "dm_users": "true",
909
+ "include_groups": "true",
910
+ "include_inbox_timelines": "true",
911
+ "include_ext_media_color": "true",
912
+ "supports_reactions": "true",
913
+ "include_ext_edit_control": "true",
914
+ "include_ext_business_affiliations_label": "true",
915
+ "ext": "mediaColor,altText,mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,superFollowMetadata,unmentionInfo,editControl",
845
916
  }
846
917
  response, response_json = await self.request("GET", url, params=params)
847
- messages = [entry["message"] for entry in response_json["inbox_initial_state"]["entries"] if "message" in entry]
918
+ messages = [
919
+ entry["message"]
920
+ for entry in response_json["inbox_initial_state"]["entries"]
921
+ if "message" in entry
922
+ ]
848
923
  return messages
849
924
 
850
925
  async def request_tweets(self, user_id: str | int, count: int = 20) -> list[Tweet]:
@@ -855,7 +930,7 @@ class Client(BaseClient):
855
930
  "includePromotedContent": True,
856
931
  "withQuickPromoteEligibilityTweetFields": True,
857
932
  "withVoice": True,
858
- "withV2Timeline": True
933
+ "withV2Timeline": True,
859
934
  }
860
935
  features = {
861
936
  "responsive_web_graphql_exclude_directive_enabled": True,
@@ -878,28 +953,29 @@ class Client(BaseClient):
878
953
  "longform_notetweets_rich_text_read_enabled": True,
879
954
  "longform_notetweets_inline_media_enabled": True,
880
955
  "responsive_web_media_download_video_enabled": False,
881
- "responsive_web_enhance_cards_enabled": False
882
- }
883
- params = {
884
- 'variables': to_json(variables),
885
- 'features': to_json(features)
956
+ "responsive_web_enhance_cards_enabled": False,
886
957
  }
958
+ params = {"variables": to_json(variables), "features": to_json(features)}
887
959
  response, response_json = await self.request("GET", url, params=params)
888
960
 
889
961
  tweets = []
890
- for instruction in response_json['data']['user']['result']['timeline_v2']['timeline']['instructions']:
891
- if instruction['type'] == 'TimelineAddEntries':
892
- for entry in instruction['entries']:
893
- if entry['entryId'].startswith('tweet'):
894
- tweet_data = entry["content"]['itemContent']['tweet_results']['result']
962
+ for instruction in response_json["data"]["user"]["result"]["timeline_v2"][
963
+ "timeline"
964
+ ]["instructions"]:
965
+ if instruction["type"] == "TimelineAddEntries":
966
+ for entry in instruction["entries"]:
967
+ if entry["entryId"].startswith("tweet"):
968
+ tweet_data = entry["content"]["itemContent"]["tweet_results"][
969
+ "result"
970
+ ]
895
971
  tweets.append(Tweet.from_raw_data(tweet_data))
896
972
  return tweets
897
973
 
898
974
  async def _confirm_unlock(
899
- self,
900
- authenticity_token: str,
901
- assignment_token: str,
902
- verification_string: str = None,
975
+ self,
976
+ authenticity_token: str,
977
+ assignment_token: str,
978
+ verification_string: str = None,
903
979
  ) -> tuple[requests.Response, str]:
904
980
  payload = {
905
981
  "authenticity_token": authenticity_token,
@@ -918,11 +994,26 @@ class Client(BaseClient):
918
994
  return
919
995
 
920
996
  response, html = await self.request("GET", self._CAPTCHA_URL, bearer=False)
921
- authenticity_token, assignment_token, needs_unlock, needs_press_continue_button = parse_unlock_html(html)
997
+ (
998
+ authenticity_token,
999
+ assignment_token,
1000
+ needs_unlock,
1001
+ start_button,
1002
+ finish_button,
1003
+ ) = parse_unlock_html(html)
922
1004
  attempt = 1
923
1005
 
924
- if needs_press_continue_button:
925
- raise TwitterException("Пока не умею так! Ждите обновление")
1006
+ if start_button or finish_button:
1007
+ response, html = await self._confirm_unlock(
1008
+ authenticity_token, assignment_token
1009
+ )
1010
+ (
1011
+ authenticity_token,
1012
+ assignment_token,
1013
+ needs_unlock,
1014
+ start_button,
1015
+ finish_button,
1016
+ ) = parse_unlock_html(html)
926
1017
 
927
1018
  funcaptcha = {
928
1019
  "api_key": self.capsolver_api_key,
@@ -942,17 +1033,38 @@ class Client(BaseClient):
942
1033
  while needs_unlock:
943
1034
  solution = await FunCaptcha(**funcaptcha).aio_captcha_handler()
944
1035
  token = solution.solution["token"]
945
- response, html = await self._confirm_unlock(authenticity_token, assignment_token,
946
- verification_string=token)
947
-
948
- if attempt > self.max_unlock_attempts or response.url == "https://twitter.com/?lang=en":
1036
+ response, html = await self._confirm_unlock(
1037
+ authenticity_token,
1038
+ assignment_token,
1039
+ verification_string=token,
1040
+ )
1041
+
1042
+ if (
1043
+ attempt > self.max_unlock_attempts
1044
+ or response.url == "https://twitter.com/?lang=en"
1045
+ ):
949
1046
  await self.establish_status()
950
1047
  return
951
1048
 
952
- authenticity_token, assignment_token, needs_unlock, needs_press_continue_button = parse_unlock_html(html)
953
-
954
- if needs_press_continue_button:
955
- raise TwitterException("Пока не умею так! Ждите обновление")
1049
+ (
1050
+ authenticity_token,
1051
+ assignment_token,
1052
+ needs_unlock,
1053
+ start_button,
1054
+ finish_button,
1055
+ ) = parse_unlock_html(html)
1056
+
1057
+ if finish_button:
1058
+ response, html = await self._confirm_unlock(
1059
+ authenticity_token, assignment_token
1060
+ )
1061
+ (
1062
+ authenticity_token,
1063
+ assignment_token,
1064
+ needs_unlock,
1065
+ start_button,
1066
+ finish_button,
1067
+ ) = parse_unlock_html(html)
956
1068
 
957
1069
  attempt += 1
958
1070
 
@@ -960,7 +1072,7 @@ class Client(BaseClient):
960
1072
  """
961
1073
  :return: flow_token, subtasks
962
1074
  """
963
- url = 'https://api.twitter.com/1.1/onboarding/task.json'
1075
+ url = "https://api.twitter.com/1.1/onboarding/task.json"
964
1076
  response, response_json = await self.request("POST", url, **kwargs)
965
1077
  return response_json["flow_token"], response_json["subtasks"]
966
1078
 
@@ -975,31 +1087,63 @@ class Client(BaseClient):
975
1087
  "input_flow_data": {
976
1088
  "flow_context": {
977
1089
  "debug_overrides": {},
978
- "start_location": {"location": "splash_screen"}
1090
+ "start_location": {"location": "splash_screen"},
979
1091
  }
980
1092
  },
981
1093
  "subtask_versions": {
982
- "action_list": 2, "alert_dialog": 1, "app_download_cta": 1, "check_logged_in_account": 1,
983
- "choice_selection": 3, "contacts_live_sync_permission_prompt": 0, "cta": 7,
984
- "email_verification": 2, "end_flow": 1, "enter_date": 1, "enter_email": 2,
985
- "enter_password": 5, "enter_phone": 2, "enter_recaptcha": 1, "enter_text": 5,
986
- "enter_username": 2, "generic_urt": 3, "in_app_notification": 1, "interest_picker": 3,
987
- "js_instrumentation": 1, "menu_dialog": 1, "notifications_permission_prompt": 2,
988
- "open_account": 2, "open_home_timeline": 1, "open_link": 1, "phone_verification": 4,
989
- "privacy_options": 1, "security_key": 3, "select_avatar": 4, "select_banner": 2,
990
- "settings_list": 7, "show_code": 1, "sign_up": 2, "sign_up_review": 4, "tweet_selection_urt": 1,
991
- "update_users": 1, "upload_media": 1, "user_recommendations_list": 4,
992
- "user_recommendations_urt": 1, "wait_spinner": 3, "web_modal": 1
993
- }
1094
+ "action_list": 2,
1095
+ "alert_dialog": 1,
1096
+ "app_download_cta": 1,
1097
+ "check_logged_in_account": 1,
1098
+ "choice_selection": 3,
1099
+ "contacts_live_sync_permission_prompt": 0,
1100
+ "cta": 7,
1101
+ "email_verification": 2,
1102
+ "end_flow": 1,
1103
+ "enter_date": 1,
1104
+ "enter_email": 2,
1105
+ "enter_password": 5,
1106
+ "enter_phone": 2,
1107
+ "enter_recaptcha": 1,
1108
+ "enter_text": 5,
1109
+ "enter_username": 2,
1110
+ "generic_urt": 3,
1111
+ "in_app_notification": 1,
1112
+ "interest_picker": 3,
1113
+ "js_instrumentation": 1,
1114
+ "menu_dialog": 1,
1115
+ "notifications_permission_prompt": 2,
1116
+ "open_account": 2,
1117
+ "open_home_timeline": 1,
1118
+ "open_link": 1,
1119
+ "phone_verification": 4,
1120
+ "privacy_options": 1,
1121
+ "security_key": 3,
1122
+ "select_avatar": 4,
1123
+ "select_banner": 2,
1124
+ "settings_list": 7,
1125
+ "show_code": 1,
1126
+ "sign_up": 2,
1127
+ "sign_up_review": 4,
1128
+ "tweet_selection_urt": 1,
1129
+ "update_users": 1,
1130
+ "upload_media": 1,
1131
+ "user_recommendations_list": 4,
1132
+ "user_recommendations_urt": 1,
1133
+ "wait_spinner": 3,
1134
+ "web_modal": 1,
1135
+ },
994
1136
  }
995
1137
  return await self._task(params=params, json=payload, auth=False)
996
1138
 
997
1139
  async def _send_task(self, flow_token: str, subtask_inputs: list[dict], **kwargs):
998
1140
  payload = kwargs["json"] = kwargs.get("json") or {}
999
- payload.update({
1000
- "flow_token": flow_token,
1001
- "subtask_inputs": subtask_inputs,
1002
- })
1141
+ payload.update(
1142
+ {
1143
+ "flow_token": flow_token,
1144
+ "subtask_inputs": subtask_inputs,
1145
+ }
1146
+ )
1003
1147
  return await self._task(**kwargs)
1004
1148
 
1005
1149
  async def _finish_task(self, flow_token):
@@ -1017,11 +1161,16 @@ class Client(BaseClient):
1017
1161
  "setting_responses": [
1018
1162
  {
1019
1163
  "key": "user_identifier",
1020
- "response_data": {"text_data": {"result": self.account.email or self.account.username}}
1164
+ "response_data": {
1165
+ "text_data": {
1166
+ "result": self.account.email
1167
+ or self.account.username
1168
+ }
1169
+ },
1021
1170
  }
1022
1171
  ],
1023
- "link": "next_link"
1024
- }
1172
+ "link": "next_link",
1173
+ },
1025
1174
  }
1026
1175
  ]
1027
1176
  return await self._send_task(flow_token, subtask_inputs, auth=False)
@@ -1032,8 +1181,8 @@ class Client(BaseClient):
1032
1181
  "subtask_id": "LoginEnterPassword",
1033
1182
  "enter_password": {
1034
1183
  "password": self.account.password,
1035
- "link": "next_link"
1036
- }
1184
+ "link": "next_link",
1185
+ },
1037
1186
  }
1038
1187
  ]
1039
1188
  return await self._send_task(flow_token, subtask_inputs, auth=False)
@@ -1042,21 +1191,24 @@ class Client(BaseClient):
1042
1191
  subtask_inputs = [
1043
1192
  {
1044
1193
  "subtask_id": "AccountDuplicationCheck",
1045
- "check_logged_in_account": {
1046
- "link": "AccountDuplicationCheck_false"
1047
- }
1194
+ "check_logged_in_account": {"link": "AccountDuplicationCheck_false"},
1048
1195
  }
1049
1196
  ]
1050
1197
  return await self._send_task(flow_token, subtask_inputs, auth=False)
1051
1198
 
1052
1199
  async def _login_two_factor_auth_challenge(self, flow_token):
1053
1200
  if not self.account.totp_secret:
1054
- raise TwitterException(f"Failed to login. Task id: LoginTwoFactorAuthChallenge")
1201
+ raise TwitterException(
1202
+ f"Failed to login. Task id: LoginTwoFactorAuthChallenge"
1203
+ )
1055
1204
 
1056
1205
  subtask_inputs = [
1057
1206
  {
1058
1207
  "subtask_id": "LoginTwoFactorAuthChallenge",
1059
- "enter_text": {"text": self.account.get_totp_code(), "link": "next_link"}
1208
+ "enter_text": {
1209
+ "text": self.account.get_totp_code(),
1210
+ "link": "next_link",
1211
+ },
1060
1212
  }
1061
1213
  ]
1062
1214
  return await self._send_task(flow_token, subtask_inputs, auth=False)
@@ -1064,15 +1216,15 @@ class Client(BaseClient):
1064
1216
  async def _viewer(self):
1065
1217
  url, query_id = self._action_to_url("Viewer")
1066
1218
  features = {
1067
- 'responsive_web_graphql_exclude_directive_enabled': True,
1068
- 'verified_phone_label_enabled': False,
1069
- 'creator_subscriptions_tweet_preview_api_enabled': True,
1070
- 'responsive_web_graphql_skip_user_profile_image_extensions_enabled': False,
1071
- 'responsive_web_graphql_timeline_navigation_enabled': True,
1219
+ "responsive_web_graphql_exclude_directive_enabled": True,
1220
+ "verified_phone_label_enabled": False,
1221
+ "creator_subscriptions_tweet_preview_api_enabled": True,
1222
+ "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
1223
+ "responsive_web_graphql_timeline_navigation_enabled": True,
1072
1224
  }
1073
1225
  field_toggles = {
1074
- 'isDelegate': False,
1075
- 'withAuxiliaryUserLabels': False,
1226
+ "isDelegate": False,
1227
+ "withAuxiliaryUserLabels": False,
1076
1228
  }
1077
1229
  variables = {"withCommunitiesMemberships": True}
1078
1230
  params = {
@@ -1088,11 +1240,11 @@ class Client(BaseClient):
1088
1240
 
1089
1241
  :return: guest_token
1090
1242
  """
1091
- url = 'https://twitter.com'
1243
+ url = "https://twitter.com"
1092
1244
  response = await self._session.request("GET", url)
1093
1245
  # TODO Если в сессии есть рабочий auth_token, то не вернет нужную страницу.
1094
1246
  # Поэтому нужно очищать сессию перед вызовом этого метода.
1095
- guest_token = re.search(r'gt\s?=\s?\d+', response.text)[0].split('=')[1]
1247
+ guest_token = re.search(r"gt\s?=\s?\d+", response.text)[0].split("=")[1]
1096
1248
  return guest_token
1097
1249
 
1098
1250
  async def _login(self):
@@ -1110,12 +1262,14 @@ class Client(BaseClient):
1110
1262
 
1111
1263
  subtask_ids = [subtask["subtask_id"] for subtask in subtasks]
1112
1264
 
1113
- # TODO Обработчик
1265
+ # TODO IMAP Обработчик
1114
1266
  if "LoginAcid" in subtask_ids:
1115
1267
  raise TwitterException(f"Failed to login: email verification!")
1116
1268
 
1117
1269
  if "LoginTwoFactorAuthChallenge" in subtask_ids:
1118
- flow_token, subtasks = await self._login_two_factor_auth_challenge(flow_token)
1270
+ flow_token, subtasks = await self._login_two_factor_auth_challenge(
1271
+ flow_token
1272
+ )
1119
1273
 
1120
1274
  # TODO Возможно, стоит добавить отслеживание этих параметров прямо в request
1121
1275
  self.account.auth_token = self._session.cookies["auth_token"]
@@ -1138,13 +1292,15 @@ class Client(BaseClient):
1138
1292
  await self._login()
1139
1293
  await self.establish_status()
1140
1294
 
1141
- async def is_enabled_2fa(self):
1295
+ async def totp_is_enabled(self):
1142
1296
  if not self.account.id:
1143
1297
  await self.request_user_data()
1144
1298
 
1145
- url = f'https://twitter.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2'
1146
- response, response_json = await self.request("GET", url)
1147
- return 'Totp' in [i['twoFactorType'] for i in response_json['methods']]
1299
+ url = f"https://twitter.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2"
1300
+ response, data = await self.request("GET", url)
1301
+ return "Totp" in [
1302
+ method_data["twoFactorType"] for method_data in data["methods"]
1303
+ ]
1148
1304
 
1149
1305
  async def _request_2fa_tasks(self):
1150
1306
  """
@@ -1154,22 +1310,55 @@ class Client(BaseClient):
1154
1310
  "flow_name": "two-factor-auth-app-enrollment",
1155
1311
  }
1156
1312
  payload = {
1157
- "input_flow_data": {"flow_context": {"debug_overrides": {}, "start_location": {"location": "settings"}}},
1158
- "subtask_versions": {"action_list": 2, "alert_dialog": 1, "app_download_cta": 1,
1159
- "check_logged_in_account": 1, "choice_selection": 3,
1160
- "contacts_live_sync_permission_prompt": 0, "cta": 7, "email_verification": 2,
1161
- "end_flow": 1, "enter_date": 1, "enter_email": 2,
1162
- "enter_password": 5, "enter_phone": 2, "enter_recaptcha": 1, "enter_text": 5,
1163
- "enter_username": 2, "generic_urt": 3,
1164
- "in_app_notification": 1, "interest_picker": 3, "js_instrumentation": 1,
1165
- "menu_dialog": 1, "notifications_permission_prompt": 2,
1166
- "open_account": 2, "open_home_timeline": 1, "open_link": 1, "phone_verification": 4,
1167
- "privacy_options": 1, "security_key": 3,
1168
- "select_avatar": 4, "select_banner": 2, "settings_list": 7, "show_code": 1,
1169
- "sign_up": 2, "sign_up_review": 4, "tweet_selection_urt": 1,
1170
- "update_users": 1, "upload_media": 1, "user_recommendations_list": 4,
1171
- "user_recommendations_urt": 1, "wait_spinner": 3, "web_modal": 1
1172
- }
1313
+ "input_flow_data": {
1314
+ "flow_context": {
1315
+ "debug_overrides": {},
1316
+ "start_location": {"location": "settings"},
1317
+ }
1318
+ },
1319
+ "subtask_versions": {
1320
+ "action_list": 2,
1321
+ "alert_dialog": 1,
1322
+ "app_download_cta": 1,
1323
+ "check_logged_in_account": 1,
1324
+ "choice_selection": 3,
1325
+ "contacts_live_sync_permission_prompt": 0,
1326
+ "cta": 7,
1327
+ "email_verification": 2,
1328
+ "end_flow": 1,
1329
+ "enter_date": 1,
1330
+ "enter_email": 2,
1331
+ "enter_password": 5,
1332
+ "enter_phone": 2,
1333
+ "enter_recaptcha": 1,
1334
+ "enter_text": 5,
1335
+ "enter_username": 2,
1336
+ "generic_urt": 3,
1337
+ "in_app_notification": 1,
1338
+ "interest_picker": 3,
1339
+ "js_instrumentation": 1,
1340
+ "menu_dialog": 1,
1341
+ "notifications_permission_prompt": 2,
1342
+ "open_account": 2,
1343
+ "open_home_timeline": 1,
1344
+ "open_link": 1,
1345
+ "phone_verification": 4,
1346
+ "privacy_options": 1,
1347
+ "security_key": 3,
1348
+ "select_avatar": 4,
1349
+ "select_banner": 2,
1350
+ "settings_list": 7,
1351
+ "show_code": 1,
1352
+ "sign_up": 2,
1353
+ "sign_up_review": 4,
1354
+ "tweet_selection_urt": 1,
1355
+ "update_users": 1,
1356
+ "upload_media": 1,
1357
+ "user_recommendations_list": 4,
1358
+ "user_recommendations_urt": 1,
1359
+ "wait_spinner": 3,
1360
+ "web_modal": 1,
1361
+ },
1173
1362
  }
1174
1363
  return await self._task(params=params, json=payload)
1175
1364
 
@@ -1177,70 +1366,98 @@ class Client(BaseClient):
1177
1366
  subtask_inputs = [
1178
1367
  {
1179
1368
  "subtask_id": "TwoFactorEnrollmentVerifyPasswordSubtask",
1180
- "enter_password": {"password": self.account.password, "link": "next_link"}
1369
+ "enter_password": {
1370
+ "password": self.account.password,
1371
+ "link": "next_link",
1372
+ },
1181
1373
  }
1182
1374
  ]
1183
1375
  return await self._send_task(flow_token, subtask_inputs)
1184
1376
 
1185
- async def _two_factor_enrollment_authentication_app_begin_subtask(self, flow_token: str):
1377
+ async def _two_factor_enrollment_authentication_app_begin_subtask(
1378
+ self, flow_token: str
1379
+ ):
1186
1380
  subtask_inputs = [
1187
1381
  {
1188
1382
  "subtask_id": "TwoFactorEnrollmentAuthenticationAppBeginSubtask",
1189
- "action_list": {"link": "next_link"}
1383
+ "action_list": {"link": "next_link"},
1190
1384
  }
1191
1385
  ]
1192
1386
  return await self._send_task(flow_token, subtask_inputs)
1193
1387
 
1194
- async def _two_factor_enrollment_authentication_app_plain_code_subtask(self, flow_token: str):
1388
+ async def _two_factor_enrollment_authentication_app_plain_code_subtask(
1389
+ self, flow_token: str
1390
+ ):
1195
1391
  subtask_inputs = [
1196
- {
1197
- "subtask_id": "TwoFactorEnrollmentAuthenticationAppPlainCodeSubtask",
1198
- "show_code": {"link": "next_link"}
1392
+ {
1393
+ "subtask_id": "TwoFactorEnrollmentAuthenticationAppPlainCodeSubtask",
1394
+ "show_code": {"link": "next_link"},
1395
+ },
1396
+ {
1397
+ "subtask_id": "TwoFactorEnrollmentAuthenticationAppEnterCodeSubtask",
1398
+ "enter_text": {
1399
+ "text": self.account.get_totp_code(),
1400
+ "link": "next_link",
1199
1401
  },
1200
- {
1201
- "subtask_id": "TwoFactorEnrollmentAuthenticationAppEnterCodeSubtask",
1202
- "enter_text": {"text": self.account.get_totp_code(), "link": "next_link"}
1203
- }
1204
- ]
1402
+ },
1403
+ ]
1205
1404
  return await self._send_task(flow_token, subtask_inputs)
1206
1405
 
1207
1406
  async def _finish_2fa_task(self, flow_token: str):
1208
1407
  subtask_inputs = [
1209
- {
1210
- "subtask_id": "TwoFactorEnrollmentAuthenticationAppCompleteSubtask",
1211
- "cta": {"link": "finish_link"}
1212
- }
1213
- ]
1408
+ {
1409
+ "subtask_id": "TwoFactorEnrollmentAuthenticationAppCompleteSubtask",
1410
+ "cta": {"link": "finish_link"},
1411
+ }
1412
+ ]
1214
1413
  return await self._send_task(flow_token, subtask_inputs)
1215
1414
 
1216
- async def _enable_2fa(self):
1415
+ async def _enable_totp(self):
1217
1416
  flow_token, subtasks = await self._request_2fa_tasks()
1218
- flow_token, subtasks = await self._two_factor_enrollment_verify_password_subtask(flow_token)
1219
- flow_token, subtasks = await self._two_factor_enrollment_authentication_app_begin_subtask(flow_token)
1417
+ flow_token, subtasks = (
1418
+ await self._two_factor_enrollment_verify_password_subtask(flow_token)
1419
+ )
1420
+ flow_token, subtasks = (
1421
+ await self._two_factor_enrollment_authentication_app_begin_subtask(
1422
+ flow_token
1423
+ )
1424
+ )
1220
1425
 
1221
1426
  for subtask in subtasks:
1222
- if subtask["subtask_id"] == 'TwoFactorEnrollmentAuthenticationAppPlainCodeSubtask':
1223
- self.account.totp_secret = subtask['show_code']['code']
1427
+ if (
1428
+ subtask["subtask_id"]
1429
+ == "TwoFactorEnrollmentAuthenticationAppPlainCodeSubtask"
1430
+ ):
1431
+ self.account.totp_secret = subtask["show_code"]["code"]
1224
1432
  break
1225
1433
 
1226
- flow_token, subtasks = await self._two_factor_enrollment_authentication_app_plain_code_subtask(flow_token)
1434
+ flow_token, subtasks = (
1435
+ await self._two_factor_enrollment_authentication_app_plain_code_subtask(
1436
+ flow_token
1437
+ )
1438
+ )
1227
1439
 
1228
1440
  for subtask in subtasks:
1229
- if subtask["subtask_id"] == 'TwoFactorEnrollmentAuthenticationAppCompleteSubtask':
1230
- result = re.search(r'\n[a-z0-9]{12}\n', subtask['cta']['secondary_text']['text'])
1441
+ if (
1442
+ subtask["subtask_id"]
1443
+ == "TwoFactorEnrollmentAuthenticationAppCompleteSubtask"
1444
+ ):
1445
+ result = re.search(
1446
+ r"\n[a-z0-9]{12}\n", subtask["cta"]["secondary_text"]["text"]
1447
+ )
1231
1448
  backup_code = result[0].strip() if result else None
1232
1449
  self.account.backup_code = backup_code
1233
1450
  break
1234
1451
 
1235
1452
  await self._finish_2fa_task(flow_token)
1236
1453
 
1237
- async def enable_2fa(self):
1454
+ async def enable_totp(self):
1238
1455
  if not self.account.password:
1239
1456
  raise ValueError("Password is required for this action")
1240
1457
 
1241
- if await self.is_enabled_2fa():
1458
+ if await self.totp_is_enabled():
1242
1459
  return
1243
1460
 
1244
- # TODO Перед началом работы вызываем request_user_data, чтоб убедиться что нет других ошибок
1461
+ # TODO Осторожно, костыль! Перед началом работы вызываем request_user_data, чтоб убедиться что нет других ошибок
1245
1462
  await self.request_user_data()
1246
- await self._enable_2fa()
1463
+ await self._enable_totp()