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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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()